NewStarCTF 2024 Week2

本文最后更新于 2024年11月29日 下午

有 Java 代码审计,好难,Java 我讨厌你qwq,但是我觉得这些题出得太好啦,复现完学到了很多!!!

Pangbai 泰拉记(1)

ida 打开反汇编查看,flag 和一个 key 异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main()
{
std::ostream *v0; // rax
std::ostream *v1; // rax
int i; // [rsp+24h] [rbp+4h]

j___CheckForDebuggerJustMyCode(&_6D15E8DE_Pangbai____1__Pangbai____1__1_cpp);
v0 = std::operator<<<std::char_traits<char>>(std::cout, "Use your debugger to discover the hidden flag!");
std::ostream::operator<<(v0, std::endl<char,std::char_traits<char>>);
for ( i = 0; i < 32; ++i )
flag[i] ^= key[i];
v1 = std::operator<<<std::char_traits<char>>(std::cout, "Click on the flag to obtain?");
std::ostream::operator<<(v1, std::endl<char,std::char_traits<char>>);
return 0i64;
}

key 交叉引用能看见还没 main0 函数调用,进去看看

发现有两个函数 IsDebuggerPresent()CheckRemoteDebuggerPresent(),这是两个反调试函数

对于 CheckRemoteDebuggerPresent() 函数:

kernel32CheckRemoteDebuggerPresent() 函数用于检测指定进程是否正在被调试. Remote 指同一个机器中的不同进程

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

如果说检测到正在被调试,那么 pbDebuggerPresent 指向的值会被设置为 0xffffffff

对于这道题,调试一下可以看得出来流程,进程会跳转到右边这里,这里执行的是将 key 替换为不正确的 key ,所以可以将 jz 指令改为 jnz 指令,让程序跳转不了

1
2
jz: Jump if Zero,零标志位 ZF=1 时,jz 指令执行跳转
jnz: Jump if Not Zero,零标志位 ZF=0 时,jnz 指令执行跳转

然后进行调试

提取数据,写脚本

1
2
3
4
5
6
7
8
9
10
11
flag = [0x63, 0x61, 0x6E, 0x20, 0x79, 0x6F, 0x75, 0x20, 0x66, 0x69,
0x6E, 0x64, 0x20, 0x6D, 0x65, 0x20, 0x63, 0x61, 0x6E, 0x20,
0x79, 0x6F, 0x75, 0x20, 0x66, 0x69, 0x6E, 0x64, 0x20, 0x6D,
0x65, 0x3F]
key = [0x05, 0x0D, 0x0F, 0x47, 0x02, 0x02, 0x0C, 0x7F, 0x22, 0x5A,
0x0C, 0x11, 0x47, 0x0A, 0x56, 0x52, 0x3C, 0x0C, 0x0F, 0x59,
0x26, 0x5E, 0x06, 0x7F, 0x04, 0x08, 0x00, 0x0A, 0x45, 0x09,
0x5A, 0x42]
for i in range(0, 32):
flag[i] ^= key[i]
print(chr(flag[i]), end="")

drink_tea

先读一遍流程,输入长度为 32 的字符串,进入 sub_140001180,然后再比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+20h] [rbp-28h]
__int64 v5; // [rsp+28h] [rbp-20h]

printf("Please Input: \n");
sub_140001120("%32s", byte_140004700);
v5 = -1i64;
do
++v5;
while ( byte_140004700[v5] );
if ( v5 == dword_140004078 )
{
for ( i = 0; i < dword_140004078; i += 8 )
sub_140001180(&byte_140004700[i], aWelcometonewst);
if ( !memcmp(byte_140004700, &unk_140004080, dword_140004078) )
printf("Right! \n");
else
printf("wrong!");
return 0;
}
else
{
printf("Wrong! \n");
return 0;
}
}

进入 sub_140001180 函数查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall sub_140001180(unsigned int *a1, _DWORD *a2)
{
__int64 result; // rax
unsigned int v3; // [rsp+0h] [rbp-38h]
unsigned int v4; // [rsp+4h] [rbp-34h]
int v5; // [rsp+8h] [rbp-30h]
unsigned int i; // [rsp+Ch] [rbp-2Ch]

v3 = *a1;
v4 = a1[1];
v5 = 0;
for ( i = 0; i < 0x20; ++i )
{
v5 -= 1640531527;
v3 += (a2[1] + (v4 >> 5)) ^ (v5 + v4) ^ (*a2 + 16 * v4);
v4 += (a2[3] + (v3 >> 5)) ^ (v5 + v3) ^ (a2[2] + 16 * v3);
}
*a1 = v3;
result = 4i64;
a1[1] = v4;
return result;
}

看到这个加密流程就能发现是 tea 算法加密找到key 和输入的值直接逆

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdint.h>

void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;
uint32_t delta=0x9e3779b9;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<32; i++) {
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}

int main()
{
int i,j;
unsigned char flag[] = {0x78, 0x20, 0xF7, 0xB3, 0xC5, 0x42, 0xCE, 0xDA, 0x85,
0x59, 0x21, 0x1A, 0x26, 0x56, 0x5A, 0x59, 0x29, 0x02, 0x0D, 0xED, 0x07,
0xA8,0xB9, 0xEE, 0x36, 0x59, 0x11, 0x87, 0xFD, 0x5C, 0x23, 0x24};
unsigned char keys[]="WelcomeToNewStar";

uint32_t *v = (uint32_t*)flag;
uint32_t *k = (uint32_t*)keys;

for(i=0;i<8;i+=2){ //每8个字节(2个 uint32)解密
decrypt(v+i,k);
}
for(j=0;j<32;j++){
printf("%c",flag[j]);
}
return 0;
}

ezencrypt

先查看 MainActivity 函数发现有加密逻辑,Enc enc = new Enc(tx),那就去 Enc 函数看看

Enc 的构造函数里进行第一次加密,ECB 模式的 AES 加密,密钥是 MainActivitytitle

doEncCheck 函数进行加密数据检查,发现有 native 关键字,在 Java 中,native 关键字用于声明一个方法是由本地代码(通常是C或C++)实现的,所以说明函数是 C/C++ 编写的,所以主体在 so 文件,进行 so 提取

IDA 打开 so 文件,找到 doEncCheck ,点进去查看

enc 函数的加密伪代码

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall enc(char *a1)
{
int i; // [rsp+0h] [rbp-20h]
int v3; // [rsp+4h] [rbp-1Ch]

v3 = __strlen_chk(a1, -1LL);
for ( i = 0; i < v3; ++i )
a1[i] ^= xork[i % 4];
return encc(xork, a1);
}

又看见一个 encc 加密函数,(太好了,又有个算法加密,我们有救了!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
__int64 __fastcall encc(char *a1, char *a2)
{
unsigned __int64 v2; // rcx
__int64 result; // rax
unsigned __int8 v4; // [rsp+Ch] [rbp-34h]
int v5; // [rsp+14h] [rbp-2Ch]
int v6; // [rsp+18h] [rbp-28h]
int i; // [rsp+1Ch] [rbp-24h]

init_sbox(a1);
v5 = 0;
v6 = 0;
for ( i = 0; ; ++i )
{
v2 = __strlen_chk(a2, -1LL);
result = i;
if ( i >= v2 )
break;
v6 = (v6 + 1) % 256;
v5 = (sbox[v6] + v5) % 256;
v4 = sbox[v6];
sbox[v6] = sbox[v5];
sbox[v5] = v4;
a2[i] ^= sbox[(sbox[v5] + sbox[v6]) % 256];
}
return result;
}

一个异或,一个 RC4 加密,找到了异或的字符串 “meow” 和要解密的数据

解密流程:RC4->异或->Base64->AES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<string.h>

char sbox[257] = {0};
char xork[] = "meow";
//s盒
void init_sbox(char*a1){
int i,j,k,tmp;
for ( i = 0; i < 0x100; i++ )
sbox[i] = i;
for ( i = 0; i < 0x100; i++ )
{
tmp = sbox[i];
j = (a1[k] + tmp + j) % 256;
sbox[i] = sbox[j];
sbox[j] = tmp;
if ( ++k >= strlen(a1))
k = 0;
}
}

//解密 RC4
void encc(char *a1,char*data){
init_sbox(a1);
int i,j,k,tmp;
for ( i = 0;i<strlen(data) ; i++ )
{
j = (j + 1) % 256;
k = (sbox[j] + k) % 256;
tmp = sbox[j];
sbox[j] = sbox[k];
sbox[k] = tmp;
data[i] ^= sbox[(sbox[j] + sbox[k]) % 256];
}
}
//异或
void enc(char *a1){
int i;
int len=strlen(a1);
for ( i = 0; i < len; ++i )
a1[i] ^= xork[i % 4];
encc(xork, a1);
}

int main(){
int i;
char mm[]={0xC2, 0x6C, 0x73, 0xF4, 0x3A, 0x45, 0x0E, 0xBA, 0x47, 0x81,
0x2A, 0x26, 0xF6, 0x79, 0x60, 0x78, 0xB3, 0x64, 0x6D, 0xDC, 0xC9, 0x04,
0x32, 0x3B, 0x9F, 0x32, 0x95, 0x60, 0xEE, 0x82, 0x97, 0xE7, 0xCA, 0x3D,
0xAA, 0x95, 0x76, 0xC5, 0x9B, 0x1D, 0x89, 0xDB, 0x98, 0x5D};
enc(mm);
for(i=0;i<44;i++){
putchar(mm[i]);
}
puts("");
}

AES 密钥是``MainActivity.title` 也就是 “IamEzEncryptGame” ,厨子梭出来

Dirty_flowers

不能 f5,有花指令,那就按下 space 键看文本流程,直接将 push-pop 的指令也就是 0x4012f1~0x401302 的指令全部 nop 掉 ,然后在 main 函数头按下 U 和 P 重新编译

伪代码大概流程:输入 36 长度字符串,进入 sub_401100 加密,再与 sub_4011D0 判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( strlen(v4) == 36 )
{
sub_401100(v4, 36);
if ( sub_4011D0(v4, 36) )
printf("success!\n");
else
printf("no!\n");
return 0;
}
else
{
printf("wrong length!\n");
return 0;
}

sub_401100 也有花指令,和上面的一样流程来去花

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
signed int __cdecl sub_401100(int a1, int a2)
{
signed int v2; // kr00_4
signed int result; // eax
int i; // [esp+1Ch] [ebp-1Ch]
char v5[16]; // [esp+24h] [ebp-14h] BYREF

strcpy(v5, "dirty_flower");
v2 = strlen(v5);
result = v2;
for ( i = 0; i < a2; ++i )
{
result = i + a1;
*(_BYTE *)(i + a1) ^= v5[i % v2];
}
return result;
}

sub_4011D0 函数比较内容大概是:(我比较喜欢看汇编流程图,感觉比伪代码更方便 :) 嘻嘻)

解密脚本

1
2
3
4
5
6
7
8
9
enc = [0x02, 0x05, 0x13, 0x13, 0x02, 0x1e, 0x53, 0x1f, 0x5c, 0x1a, 0x27, 0x43, 0x1d, 0x36, 0x43,
0x07, 0x26, 0x2d, 0x55, 0x0d, 0x03, 0x1b, 0x1c, 0x2d, 0x02, 0x1c, 0x1c, 0x30, 0x38, 0x32,
0x55, 0x02, 0x1b, 0x16, 0x54, 0x0f, 0x00]
str = "dirty_flower"
flag = ""
for i in range(len(enc)):
enc[i] ^= ord(str[i % len(str)])
flag += chr(enc[i])
print(flag)

Ptrace

ida 居然还能反编译.txt ,真的是 tql。第一次见这种类型的题目,看 wp 理解了半天,复现了俩小时(

记得将 son.txtfather.txt 放在同一目录下,打开 father.txt 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp-14h] [ebp-30h]
int v5; // [esp-10h] [ebp-2Ch]
int v6; // [esp-Ch] [ebp-28h]
int v7; // [esp-8h] [ebp-24h]
int v8; // [esp-4h] [ebp-20h]
const char **v9; // [esp+0h] [ebp-1Ch]
char stat_loc[4]; // [esp+8h] [ebp-14h] BYREF
__pid_t v11; // [esp+Ch] [ebp-10h]
unsigned int v12; // [esp+10h] [ebp-Ch]
int *p_argc; // [esp+14h] [ebp-8h]

p_argc = &argc;
v9 = argv;
v12 = __readgsdword(0x14u);
puts("Please input your flag:");
__isoc99_scanf("%32s", &s, v4, v5, v6, v7, v8, v9);
v11 = fork(); // 创建子进程
if ( v11 )
{//父进程
if ( v11 <= 0 )
{
perror("fork");
return -1;
}
wait(stat_loc);
ptrace(PTRACE_POKEDATA, addr, addr, 3);
ptrace(PTRACE_CONT, 0, 0, 0);
wait(0);
}
else
{//子进程
ptrace(PTRACE_TRACEME, 0, 0, 0);
execl("./son", "son", &s, 0); //把 s 作为新进程的参数
}
return 0;
}

对于fork()execl() 函数:都是 Linux 中的进程控制函数

fork():创建新的进程,该进程几乎相当于当前进程的一个完全拷贝

execl():是函数族 exec() 之一,用来启动另外的进程以取代当前运行的进程

execl() 四个参数

参数 变量类型 解释
绝对路径 const char* 文件存储路径
标识符 const char* 大多数时候是文件名
参数 —— 选项
NULL —— NULL

所以 main() 函数中流程:fork() 创建子进程,返回的 pid 就是 v11v11>0 为父进程,v11=0 为子进程,子进程中使用了 execl() 函数,启动当前目录下的 son 文件,传入 s 作为新进程的参数,这里的新进程替换掉之前的子进程,使自己变成子进程。

打开 son.txt 查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl sub_600011AD(int a1, int a2)
{
signed int i; // [esp+2h] [ebp-28h]
signed int j; // [esp+6h] [ebp-24h]
char *s; // [esp+Ah] [ebp-20h]
signed int v6; // [esp+Eh] [ebp-1Ch]

s = *(char **)(a2 + 4);
v6 = strlen(s);
for ( i = 0; i < v6; ++i )
byte_60004080[i] = ((int)(unsigned __int8)s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));
for ( j = 0; j < v6; ++j )
{
if ( byte_60004080[j] != byte_60004020[j] )
{
puts("this is Wrong~");
return 0;
}
}
puts("this is right~");
return 0;
}

这里的 s = *(char **)(a2 + 4),其实它就是指向 father 传入的 sexecl 执行的命令为 ./son s,而对于 son 文件的主函数而言,第一个参数是 a1 表示执行命令参数的个数,这里就是 2,而后面的 a2 真实类型为 const char*,代表的就是命令的各个参数,所以这里的 a2 + 4 执行的就是第二个参数,也就是 s.

大概流程为:将 s 中的每个字节循环移位来进行变化,最后与密文进行比较

对于 father 文件的 ptarceptrace 是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。

而这里查看子进程,可以发现使用 ptrace(PTRACE_TRACEME, 0, 0, 0);,它就是允许父进程对自身进行调试的语句,然后在父进程中,使用 PTRACE_POKEDATA 对数据进行修改,然后使用 PTRACE_CONT 让子进程继续执行。因此我们要关注的就是父进程对子进程的什么数据进行了修改

我们能看到有语句ptrace(PTRACE_POKEDATA, addr, addr, 3);,就是将 addr 所指向的地址进行了数据修改,更改为了 3,点进去 addr 指向的就是 0x60004040 位置的数据

这个地址在 son 文件中也出现了

1
2
for ( i = 0; i < v6; ++i )
byte_60004080[i] = ((int)(unsigned __int8)s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));

所以这个 ptrace 修改的是偏移值,将 4 改为了 3

因此,按照偏移 3 进行逆向变换,脚本

1
2
3
4
5
6
7
enc = [0xCC, 0x8D, 0x2C, 0xEC, 0x6F, 0x88, 0xED, 0xEB, 0x2F, 0xED,
0xAE, 0xEB, 0x4E, 0xAC, 0x2C, 0x8D, 0x8D, 0x2F, 0xEB, 0x6D,
0xCD, 0xED, 0xEE, 0xEB, 0x0E, 0x8E, 0x4E, 0x2C, 0x6C, 0xAC,
0xE7, 0xAF]
for i in range(len(enc)):
enc[i] = (enc[i] << 3 | enc[i] >> 5)&0xff
print(chr(enc[i]), end='')

UPX

看了下 wp 需要脱壳,但是我拿到的附件已经是脱好壳的了,所以直接看伪代码

一般进行 upx 脱壳方法

1
upx -d "文件路径"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
int status; // [rsp+Ch] [rbp-4h]

puts("Please input your flag:");
__isoc99_scanf("%22s", s);
RC4(s, key);
for ( status = 0; status <= 21; ++status )
{
if ( s[status] != data[status] )
{
puts("this is Wrong~");
exit(status);
}
}
puts("this is right~");
return 0;
}

输入的字符串长度为 22,然后进入 RC4 函数加密,后续进行 for 循环中,把sdata 进行比较

RC4 函数内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall RC4(const char *a1, __int64 a2)
{
__int64 result; // rax
unsigned __int8 v3; // [rsp+14h] [rbp-Ch]
unsigned __int8 v4; // [rsp+15h] [rbp-Bh]
unsigned int i; // [rsp+18h] [rbp-8h]
unsigned int v6; // [rsp+1Ch] [rbp-4h]

v3 = 0;
v4 = 0;
init_sbox(a2);
v6 = strlen(a1);
for ( i = 0; ; ++i )
{
result = i;
if ( i >= v6 )
break;
v4 += sbox[++v3];
swap(&sbox[v3], &sbox[v4]);
a1[i] ^= sbox[(unsigned __int8)(sbox[v3] + sbox[v4])];
}
return result;
}

RC4 加密,好讨厌写解密算法。。。

看 wp 可以进行动调,又学到了,直接下断点

这是 elf 文件,所以只能远程动调,连接 kali

随便输入字符串,断点断在了加密函数,查看数据就是我们输入的字符串

然后找到 data ,把数据提取出来

1
2
3
4
5
6
7
8
from ida_bytes import *
addr = 0x55FE34CF0040 //s的起始地址
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
0x75, 0xB5]
for i in range(22):
patch_byte(addr + i, enc[i])
print('Done')

点进 run 之后,s 就会有变化,然后 f9 运行又会断在与 data 比较的地方,这个时候可以看 s 的值,与之前又不一样,按 a 可以转化为字符串(又学到了!)

over~


NewStarCTF 2024 Week2
http://example.com/2024/11/11/复现2/
作者
butt3rf1y
发布于
2024年11月11日
许可协议