VCTF Reverse wp + 复现

闻香识女人

修花指令

花指令结构1:

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000002CB9                 push    rbx
.text:0000000000002CBA xor rbx, rbx
.text:0000000000002CBD test rbx, rbx
.text:0000000000002CC0 jnz short near ptr unk_2CC4
.text:0000000000002CC2 jz short loc_2CC5
.text:0000000000002CC2 ; ---------------------------------------------------------------------------
.text:0000000000002CC4 unk_2CC4 db 0E9h ; CODE XREF: .text:0000000000002CC0↑j
.text:0000000000002CC5 ; ---------------------------------------------------------------------------
.text:0000000000002CC5
.text:0000000000002CC5 loc_2CC5: ; CODE XREF: .text:0000000000002CC2↑j
.text:0000000000002CC5 pop rbx

把从 xor 到 unk_2CC4 的内容给 nop 掉

花指令结构2:

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000002DC5                 call    loc_2DCB
.text:0000000000002DC5 ; ---------------------------------------------------------------------------
.text:0000000000002DCA db 83h
.text:0000000000002DCB ; ---------------------------------------------------------------------------
.text:0000000000002DCB
.text:0000000000002DCB loc_2DCB: ; CODE XREF: .text:0000000000002DC5↑j
.text:0000000000002DCB add qword ptr [rsp], 8
.text:0000000000002DD0 retn
.text:0000000000002DD1 ; ---------------------------------------------------------------------------
.text:0000000000002DD1 rep cmp dword ptr [rbp-164h], 1
.text:0000000000002DD9 jle loc_2EDC

call 先将下一条指令的地址即 0x2DCA 压栈,然后跳转到 loc_2DCB, add qword ptr [rsp], 8 会把栈顶的地址加 8,变成 0x2DD2,然后 retn 会跳转到这个地址。在 text:0000000000002DD1U 后再在 2DD2C 将其变成代码,得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000000002DC5                 call    loc_2DCB
.text:0000000000002DC5 ; ---------------------------------------------------------------------------
.text:0000000000002DCA db 83h
.text:0000000000002DCB ; ---------------------------------------------------------------------------
.text:0000000000002DCB
.text:0000000000002DCB loc_2DCB: ; CODE XREF: .text:0000000000002DC5↑j
.text:0000000000002DCB add qword ptr [rsp], 8
.text:0000000000002DD0 retn
.text:0000000000002DD0 ; ---------------------------------------------------------------------------
.text:0000000000002DD1 db 0F3h
.text:0000000000002DD2 ; ---------------------------------------------------------------------------
.text:0000000000002DD2 cmp dword ptr [rbp-164h], 1
.text:0000000000002DD9 jle loc_2EDC

把 call 到 2DD1 部分的内容 nop 掉

花指令结构3:

call 后面的函数点进去是这样的

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
unsigned __int64 __fastcall sub_55555555638B(__int64 a1)
{
int i; // [rsp+1Ch] [rbp-24h]
void *addr; // [rsp+20h] [rbp-20h]
size_t len; // [rsp+28h] [rbp-18h]
int v5; // [rsp+33h] [rbp-Dh]
char v6; // [rsp+37h] [rbp-9h]
unsigned __int64 v7; // [rsp+38h] [rbp-8h]

v7 = __readfsqword(0x28u);
addr = (void *)sub_555555556361(a1);
len = getpagesize();
if ( mprotect(addr, len, 7) )
{
perror("mprotect");
}
else
{
v5 = 0x84D58179;
v6 = 0x89;
for ( i = 0; i <= 4; ++i )
*(_BYTE *)(i + a1) ^= *((_BYTE *)&v5 + i);
mprotect(addr, len, 5);
}
return v7 - __readfsqword(0x28u);
}

jmp 0x19144511(rel32) 处的机器码是 E9 11 45 14 19,与79 81 D5 84 89 逐字节做 XOR,得到 90 90 90 90 90,即把 jmp 指令的五个字节全部 nop 掉

之后在函数头的位置 U P 之后即可成功反编译

分析

接着打断点动调调试分析每个函数的功能

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
__int64 __fastcall sub_55555555646B(const char *input, const char *a2)
{
unsigned int v3; // [rsp+1Ch] [rbp-194h]
size_t i; // [rsp+20h] [rbp-190h]
size_t size; // [rsp+50h] [rbp-160h]
void *src; // [rsp+58h] [rbp-158h]
unsigned __int64 v7; // [rsp+60h] [rbp-150h]
void *dest; // [rsp+68h] [rbp-148h]
void *ptr; // [rsp+70h] [rbp-140h]
unsigned __int64 v10; // [rsp+7Fh] [rbp-131h] BYREF
_BYTE v11[9]; // [rsp+87h] [rbp-129h] BYREF
_BYTE v12[32]; // [rsp+90h] [rbp-120h] BYREF
_QWORD v13[8]; // [rsp+B0h] [rbp-100h] BYREF
char v14[80]; // [rsp+F0h] [rbp-C0h] BYREF
_BYTE v15[104]; // [rsp+140h] [rbp-70h] BYREF
unsigned __int64 v16; // [rsp+1A8h] [rbp-8h]

v16 = __readfsqword(0x28u);
sub_55555555638B((__int64)&loc_5555555564B4);
v10 = 0xD9CBC6D9CBF5C3DCLL;
v13[0] = 0x999D939CCF9F9B9FLL;
v13[1] = 0x9A93C89CC898CF93LL;
v13[2] = 0x93CB9FCE9998989DLL;
v13[3] = 0x9ACCCC9D9BCFC999LL;
v13[4] = 0x999D939CCF9F9B9FLL;
v13[5] = 0x9A93C89CC898CF93LL;
v13[6] = 0x93CB9FCE9998989DLL;
v13[7] = 0x9ACCCC9D9BCFC999LL;
func1((__int64)v13, 0x40uLL, (__int64)v14);
func1((__int64)&v10, 8uLL, (__int64)v11);
hex2byte(v14, (__int64)v12, 0x20uLL);
hex2byte(a2, (__int64)v15, 0x64uLL);
size = strlen(input);
src = malloc(size);
enc1((__int64)input, size, (__int64)v12, (__int64)v11, 0LL, (__int64)src);
v7 = (size + 15) & 0xFFFFFFFFFFFFFFF0LL;
dest = malloc(v7);
memcpy(dest, src, size);
for ( i = size; i < v7; ++i )
*((_BYTE *)dest + i) = v7 - size;
ptr = malloc(v7);
aes((__int64)dest, v7, (__int64)"venom2025venom25", (__int64)ptr);
v3 = compare((__int64)ptr, (__int64)v15, v7);
free(src);
free(dest);
free(ptr);
return v3;
}

exp

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
from Crypto.Cipher import AES

# 还原 v12(Salsa20 的 key

v13_qwords = [
0x999D939CCF9F9B9F,
0x9A93C89CC898CF93,
0x93CB9FCE9998989D,
0x9ACCCC9D9BCFC999,
0x999D939CCF9F9B9F,
0x9A93C89CC898CF93,
0x93CB9FCE9998989D,
0x9ACCCC9D9BCFC999,
]

def func1(a1_bytes: bytes) -> bytes:
a2 = len(a1_bytes)
out = bytearray(a2)
for i in range(a2):
out[i] = a1_bytes[a2 - 1 - i] ^ 0xAA
return bytes(out)

def hex2byte(hex_bytes: bytes) -> bytes:
s = hex_bytes.decode()
if len(s) % 2 != 0:
raise ValueError("hex length must be even")
return bytes(int(s[i:i+2], 16) for i in range(0, len(s), 2))

# v13 在内存中是 8 个 64-bit,小端拼成 64 字节
v13_bytes = b"".join(q.to_bytes(8, "little") for q in v13_qwords)
hex_str = func1(v13_bytes)
v12 = hex2byte(hex_str) # 32-byte Salsa20 key

print("[+] Salsa20 key (v12) =", v12.hex())


# AES-128-ECB 解密
AES_KEY = b"venom2025venom25"

ciphertext_hex = (
"59cc9e17b97199cad788f5f9d919531e"
"e9f09bb233b5565233aef1de39a1d663"
"5237703c819d47cf9ca5c53ca8adcdec"
)
ciphertext = bytes.fromhex(ciphertext_hex)

aes = AES.new(AES_KEY, AES.MODE_ECB)
dest = aes.decrypt(ciphertext) # 对应 C 中的 dest(填充后)

print("[+] dest len =", len(dest), "dest =", dest.hex())


#去掉 PKCS#7 填充得到 src

def pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise ValueError(f"Invalid padding length: {pad_len}")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("Invalid padding content")
return data[:-pad_len]

src = pkcs7_unpad(dest)

print("[+] src len =", len(src), "src =", src.hex())


#Salsa2
SALSA_IV_HEX = "73616c73615f6976"
nonce = bytes.fromhex(SALSA_IV_HEX)

def rotl32(x, n):
return ((x << n) & 0xffffffff) | (x >> (32 - n))

def salsa20_quarterround(y0, y1, y2, y3):
y1 ^= rotl32((y0 + y3) & 0xffffffff, 7)
y2 ^= rotl32((y1 + y0) & 0xffffffff, 9)
y3 ^= rotl32((y2 + y1) & 0xffffffff, 13)
y0 ^= rotl32((y3 + y2) & 0xffffffff, 18)
return y0 & 0xffffffff, y1 & 0xffffffff, y2 & 0xffffffff, y3 & 0xffffffff

def salsa20_block(key: bytes, nonce: bytes, counter: int) -> bytes:

assert len(key) == 32
assert len(nonce) == 8

def u32(b): return int.from_bytes(b, "little")

k = [u32(key[i:i+4]) for i in range(0, 32, 4)]
n = [u32(nonce[0:4]), u32(nonce[4:8])]
c0 = 0x61707865
c1 = 0x3320646e
c2 = 0x79622d32
c3 = 0x6b206574
ctr0 = counter & 0xffffffff
ctr1 = (counter >> 32) & 0xffffffff

x = [
c0, k[0], k[1], k[2],
k[3], c1, n[0], n[1],
ctr0, ctr1, c2, k[4],
k[5], k[6], k[7], c3,
]

# 保存原始 state
orig = x[:]

# 10 次 double-round = 20 轮
for _ in range(10):
# column rounds:
# (0,4,8,12), (5,9,13,1), (10,14,2,6), (15,3,7,11)
x[0], x[4], x[8], x[12] = salsa20_quarterround(x[0], x[4], x[8], x[12])
x[5], x[9], x[13], x[1] = salsa20_quarterround(x[5], x[9], x[13], x[1])
x[10],x[14],x[2], x[6] = salsa20_quarterround(x[10],x[14],x[2], x[6])
x[15],x[3], x[7], x[11] = salsa20_quarterround(x[15],x[3], x[7], x[11])
# row rounds:
# (0,1,2,3), (5,6,7,4), (10,11,8,9), (15,12,13,14)
x[0], x[1], x[2], x[3] = salsa20_quarterround(x[0], x[1], x[2], x[3])
x[5], x[6], x[7], x[4] = salsa20_quarterround(x[5], x[6], x[7], x[4])
x[10],x[11],x[8], x[9] = salsa20_quarterround(x[10],x[11],x[8], x[9])
x[15],x[12],x[13], x[14] = salsa20_quarterround(x[15],x[12],x[13], x[14])

# 加回原 state
for i in range(16):
x[i] = (x[i] + orig[i]) & 0xffffffff

# 16 * 4 = 64 字节 keystream
return b"".join(w.to_bytes(4, "little") for w in x)

def salsa20_stream_xor(data: bytes, key: bytes, nonce: bytes, counter0: int = 0) -> bytes:

out = bytearray(len(data))
offset = 0
block_index = 0
while offset < len(data):
ks = salsa20_block(key, nonce, counter0 + block_index)
block_len = min(64, len(data) - offset)
for i in range(block_len):
out[offset + i] = data[offset + i] ^ ks[i]
offset += block_len
block_index += 1
return bytes(out)


# 逆 Salsa20 层,得到原始输入
a1 = salsa20_stream_xor(src, v12, nonce, counter0=0)

print("[+] Recovered input (hex) =", a1.hex())
try:
print("[+] Recovered input (ascii)=", a1.decode())
except UnicodeDecodeError:
print("[+] Recovered input not all ascii")

输出

1
2
3
4
5
[+] Salsa20 key (v12) = 0ff71ec39a5d322709b6b2e93796e5150ff71ec39a5d322709b6b2e93796e515
[+] dest len = 48 dest = 5b397a7aa04ecbc2d800d1d1dbf748df96bc36ead33e1aada6b74f946191089bc6e9cbc2c1425448f569060606060606
[+] src len = 42 src = 5b397a7aa04ecbc2d800d1d1dbf748df96bc36ead33e1aada6b74f946191089bc6e9cbc2c1425448f569
[+] Recovered input (hex) = 766374667b61623932313035625f303437655f616133385f306232355f6136393839623865356564357d
[+] Recovered input (ascii)= vctf{ab92105b_047e_aa38_0b25_a6989b8e5ed5}

flag 为vctf{ab92105b_047e_aa38_0b25_a6989b8e5ed5}

ezapp

太好了,是安卓题

分析

jadx 打开 MainActivity

1
2
3
4
5
6
7
8
 if (obj.length() == 0) {
mainActivity.showMessage("请输入flag", false);
} else if (mainActivity.checkFlag(obj)) {
mainActivity.showMessage("ok", true);
} else {
mainActivity.showMessage("no", false);
}
}

输入 flag 后调用了一个 Native 方法 checkFlag,打开 libmyapplication.so,直接找 checkFlag 函数没找到,但是发现了另外一个关键函数 sub_18BC

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
__int64 __fastcall sub_18BC(JNIEnv *a1, void *a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

v21 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v2 = AAssetManager_fromJava(a1, a2);
v3 = AAssetManager_open(v2, "qwqer1", 3);
Length = AAsset_getLength(v3);
if ( Length >= 1
&& (v5 = Length, (Length & 0xF) == 0)
&& (Buffer = AAsset_getBuffer(v3)) != 0LL
&& (v7 = Buffer, (v8 = (char *)malloc(v5)) != 0LL) )
{
v9 = v8;
v20 = *(_OWORD *)"p0l1st";
v10 = sub_14DC(&v20, 16LL, v7, v8, (unsigned int)v5);
AAsset_close(v3);
if ( v10 )
goto LABEL_6;
v12 = (unsigned __int8)v9[v5 - 1];
if ( (unsigned int)(v12 - 1) <= 0xF )
{
if ( v5 < v12 )
{
LABEL_16:
v5 -= v12;
}
else
{
v13 = -(__int64)v12;
while ( (unsigned __int8)v9[v5 + v13] == (_DWORD)v12 )
{
if ( __CFADD__(v13++, 1LL) )
goto LABEL_16;
}
}
}
getpid();
sub_1AAC(v19);
v15 = fopen(v19, "wb");
if ( !v15 )
{
LABEL_6:
free(v9);
}
else
{
v16 = v15;
v17 = fwrite(v9, 1uLL, v5, v15);
free(v9);
fclose(v16);
if ( v17 == v5 )
{
chmod(v19, 0x1EDu);
v18 = dlopen(v19, 1);
qword_40A0 = (__int64)v18;
if ( v18 )
{
off_40A8 = (__int64 (*)(void))dlsym(v18, "Java_com_qwq_myapplication_MainActivity_checkFlag");
if ( off_40A8 )
{
unlink(v19);
result = 1LL;
byte_40B0 = 1;
return result;
}
dlclose((void *)qword_40A0);
qword_40A0 = 0LL;
}
}
unlink(v19);
}
}
else
{
AAsset_close(v3);
}
return 0LL;
}

从 asset 里面读取一个加密文件 qwqer1,并使用密钥 p0l1st 对其进行解密,写入临时路径,dlopen 加载它,然后在其中找到真正的 Java_com_qwq_myapplication_MainActivity_checkFlag 函数,推测这个解密之后的文件应该就是真正要找的 so 文件

dump so 文件

用 frida dump 出解密后的 so

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
Java.perform(function(){
let dumped = false;
let fwritePtr = Module.findExportByName(null,"fwrite");
console.log("[+] fwrite @ " + fwritePtr);

Interceptor.attach(fwritePtr,{
onEnter:function(args){
if(dumped) return;
let buf = args[0];
let size = args[1].toInt32();
let nmemb = args[2].toInt32();
let total = size * nmemb;
if(total < 0x1000) return;
try{
let magic = Memory.readU32(buf);
if(magic !== 0x464c457f) return; //0x7F 'E' 'L' 'F'
}catch(e){
return;
}
console.log("[+] Detected ELF in fwrite, size = " + total);
try{
let soData = Memory.readByteArray(buf, total);
let path = "/data/data/com.qwq.myapplication/files/dump_from_fwrite.so";
let f = new File(path,"wb");
f.write(soData);
f.close();
dumped = true;
console.log("[+] dump ok: " + path);
}catch(e){
console.log("[-] dump failed: " + e);
}
}
})
})

执行命令之后成功得到解密后的文件

1
frida -U -f com.qwq.myapplication -l dump_fwrite.js

在 dump_from_fwrite.so 里面找到了 Java_com_qwq_myapplication_MainActivity_checkFlag 函数

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
bool __fastcall Java_com_qwq_myapplication_MainActivity_checkFlag(__int64 a1, __int64 a2, __int64 a3)
{
const char *v5; // x0
const char *v6; // x21
__int64 v7; // x2
_OWORD *v8; // x22
unsigned int *v9; // x25
__int64 v10; // x24
unsigned int v11; // t1
_BOOL4 v12; // w22
int v14; // [xsp+Ch] [xbp-154h] BYREF
_OWORD s1[16]; // [xsp+10h] [xbp-150h] BYREF
_BYTE v16[64]; // [xsp+118h] [xbp-48h] BYREF
__int64 v17; // [xsp+158h] [xbp-8h]

v17 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
v6 = v5;
if ( *v5 )
{
sub_92C(v5, dword_670, (__int64)v16, &v14);
memset(s1, 0, sizeof(s1));
if ( v14 >= 1 )
{
v8 = s1;
v9 = (unsigned int *)v16;
if ( 2 * v14 <= 1 )
v10 = 1LL;
else
v10 = (unsigned int)(2 * v14);
do
{
v11 = *v9++;
v8 = (_OWORD *)((char *)v8 + (int)sub_888((__int64)v8, -1LL, v7, v11));
--v10;
}
while ( v10 );
}
v12 = strcmp(
(const char *)s1,
"c2f6aade913f38fa407aceea7679a9db017ea632769a2c530f9917cbf2388a8dc89f80174a0d961820ebf4b20328e9ae") == 0;
(*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL))(a1, a3, v6);
}
else
{
(*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL))(a1, a3, v5);
return 0;
}
return v12;
}

其中加密是一个魔改 tea

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
__int64 __fastcall sub_92C(const char *a1, _DWORD *a2, __int64 a3, int *a4)
{
int v8; // w23
int v9; // w8
int v10; // w25
__int64 result; // x0
unsigned __int64 v12; // x9
__int64 v13; // x11
char v14; // w13
__int64 v15; // x14
const char *v16; // x15
__int64 v17; // x16
char v18; // w17
_BYTE *v19; // x16
unsigned __int64 v20; // x11
char v21; // w12
unsigned __int64 v22; // x13
__int64 v23; // x8
__int64 v24; // x10
unsigned int *v25; // x14
unsigned int v26; // w12
unsigned int v27; // w13
int v28; // w2
unsigned int v29; // w3
unsigned int v30; // w5
__int64 v31; // x15
_BYTE v32[64]; // [xsp+8h] [xbp-48h] BYREF
__int64 v33; // [xsp+48h] [xbp-8h]

v33 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v8 = strlen(a1);
v9 = v8 + 7;
if ( v8 < -7 )
v9 = v8 + 14;
v10 = v9 >> 3;
result = __memset_chk(v32, 0LL, (int)(v9 & 0xFFFFFFF8), 64LL);
if ( v8 <= 0 )
{
*a4 = v10;
return result;
}
if ( (unsigned int)v8 < 2uLL )
{
v12 = 0LL;
do
{
LABEL_10:
v20 = v12 & 7;
v21 = a1[v12];
v22 = v12++ & 0x7FFFFFF8;
v32[v22 + v20] = v21;
}
while ( v8 != v12 );
goto LABEL_11;
}
v13 = 0LL;
v12 = (unsigned int)v8 - (unsigned __int64)(v8 & 1);
do
{
v14 = v13 + 1;
v15 = v13 & 6;
v16 = &a1[v13];
v17 = v13 & 0x7FFFFFF8;
v13 += 2LL;
v18 = *v16;
v19 = &v32[v17];
LOBYTE(v16) = v16[1];
v19[v15] = v18;
v19[v14 & 7] = (_BYTE)v16;
}
while ( v12 != v13 );
if ( (v8 & 1) != 0 )
goto LABEL_10;
LABEL_11:
*a4 = v10;
v23 = 0LL;
if ( v10 <= 1 )
v24 = 1LL;
else
v24 = (unsigned int)v10;
do
{
v25 = (unsigned int *)&v32[8 * v23];
v27 = *v25;
v26 = v25[1];
result = (unsigned int)a2[2];
v28 = 32;
v29 = 0x9E3779B9;
do
{
--v28;
v27 += (*a2 + 16 * v26) ^ (v26 + v29) ^ (a2[1] + (v26 >> 5));
v30 = v29 + v27;
v29 -= 1640531527;
v26 += (result + 16 * v27) ^ v30 ^ (a2[3] + (v27 >> 5));
}
while ( v28 );
v31 = 8 * v23++;
v25[1] = v26;
*(_DWORD *)&v32[v31] = v27;
*(_DWORD *)(a3 + v31) = v27;
*(_DWORD *)(a3 + (v31 | 4)) = v26;
}
while ( v23 != v24 );
return result;
}

密钥

1
.rodata:0000000000000670 dword_670       DCD 0xA56BABCD, 0x1F1234F1, 0x12345678, 0xFEDCBA98

exp

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
57
58
59
60
61
62
63
64
65
66
67
68
K0 = 0xA56BABCD
K1 = 0x1F1234F1
K2 = 0x12345678
K3 = 0xFEDCBA98

DELTA = 0x61C88647
SUM_INIT = 0x9E3779B9

TARGET_HEX = (
"c2f6aade913f38fa407aceea7679a9db"
"017ea632769a2c530f9917cbf2388a8d"
"c89f80174a0d961820ebf4b20328e9ae"
)

def decrypt_block(v0, v1):
"""
还原自 sub_92C 里的那段 TEA-like 32 轮加密的逆过程:
y += f(z, sum)
tmp = sum + y
sum -= DELTA
z += g(y, tmp)
这里我们已知 (y,z,sum_after),反推 (y,z,sum_before)。
"""
sum_ = (SUM_INIT - 32 * DELTA) & 0xFFFFFFFF # 32 轮之后的 sum 值
y1, z1 = v0, v1
for _ in range(32):
# 恢复到本轮开始时的 sum
sum_prev = (sum_ + DELTA) & 0xFFFFFFFF

# 正向: tmp = sum_prev + y1
tmp = (sum_prev + y1) & 0xFFFFFFFF

# 正向: z1 = z + g(y1, tmp)
# 反向: z = z1 - g(y1, tmp)
z = (z1 - ((K2 + 16 * y1) ^ tmp ^ (K3 + (y1 >> 5)))) & 0xFFFFFFFF

# 正向: y1 = y + f(z, sum_prev)
# 反向: y = y1 - f(z, sum_prev)
y = (y1 - ((K0 + 16 * z) ^
((z + sum_prev) & 0xFFFFFFFF) ^
(K1 + (z >> 5)))) & 0xFFFFFFFF

# 更新到上一轮的状态
y1, z1, sum_ = y, z, sum_prev

return y1, z1


def main():
# 把 96 位 hex 切成 12 个 32-bit 整数
words = [int(TARGET_HEX[i:i+8], 16)
for i in range(0, len(TARGET_HEX), 8)]

# 两个 32-bit 一组,作为一个 64-bit block 进行解密
plaintext = bytearray()
for i in range(0, len(words), 2):
v0, v1 = words[i], words[i + 1]
y, z = decrypt_block(v0, v1)
plaintext += y.to_bytes(4, "little")
plaintext += z.to_bytes(4, "little")

flag_bytes = plaintext.rstrip(b"\x00")
print("Plain bytes:", plaintext)
print("Flag:", flag_bytes.decode(errors="ignore"))


if __name__ == "__main__":
main()

得到 flag vctf{ab92105b_047e_aa38_0b25_a6989b8e5ed5}

ez_ollvm_m4yb3

拿到这道题题的时候看到是 linux 反调试有点头大,之前没怎么遇到过,感觉自己好像不会反反调 linux,后面学了一下发现其实也还好

解密字符串

main 函数

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
int __fastcall main(int argc, const char **argv, const char **envp)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

v118 = xmmword_427E;
v117 = xmmword_426E;
*(_OWORD *)v116 = xmmword_425E;
v119 = 99;
v99 = 233665123;
sub_3B00(v116, &v99, 49LL, 4LL);
qmemcpy(v108, "9J<T(D&F6D&V{%", 14);
v98 = 861021530;
sub_3B00(v108, &v98, 14LL, 4LL);
v84 = 17386;
v83 = 142619130;
v97 = 1726956429;
sub_3B00(&v83, &v97, 6LL, 4LL);
v109 = xmmword_4236;
v110 = 527534212;
v96 = 521595368;
sub_3B00(&v109, &v96, 20LL, 4LL);
*(_OWORD *)&v115[13] = *(__int128 *)((char *)&xmmword_4219 + 13);
*(_OWORD *)v115 = xmmword_4219;
v114 = xmmword_4209;
v113 = xmmword_41F9;
v112 = xmmword_41E9;
v95 = 1303455736;
sub_3B00(&v112, &v95, 77LL, 4LL);
v86 = 25263;
v85 = 918487533;
v94 = 1540383426;
sub_3B00(&v85, &v94, 6LL, 4LL);
memcpy(dest, &unk_4100, 0xC9uLL);
v93 = 1350490027;
sub_3B00(dest, &v93, 201LL, 4LL);
*(_QWORD *)&v111[13] = 0xEC063B3D9C123462LL;
*(__m128i *)v111 = _mm_loadu_si128(&xmmword_40B2);
v92 = 1649760492;
sub_3B00(v111, &v92, 21LL, 4LL);
*(_DWORD *)&v87[3] = 2064426333;
*(_DWORD *)v87 = 1560892393;
v91 = 846930886;
sub_3B00(v87, &v91, 7LL, 4LL);
v3 = getpid();
v4 = 1;
if ( (unsigned __int8)check_tracer_pid(v3) )
return v4;
v5 = getppid();
v6 = -v5;
if ( v5 > 0 )
v6 = v5;
v7 = 1;
if ( v6 >= 0xA )
{
v7 = 4;
v8 = v6;
while ( 1 )
{
if ( v8 <= 0x63 )
{
v7 -= 2;
goto LABEL_13;
}
if ( v8 <= 0x3E7 )
break;
if ( v8 < 0x2710 )
goto LABEL_13;
v7 += 4;
v9 = v8 <= 0x1869F;
v8 /= 0x2710u;
if ( v9 )
{
v7 -= 3;
goto LABEL_13;
}
}
--v7;
}
LABEL_13:
v10 = (unsigned int)v5 >> 31;
s2 = v90;
std::string::_M_construct(&s2, v7 + ((unsigned int)v5 >> 31), 45LL);
v11 = (char *)s2 + v10;
if ( v6 < 0x64 )
{
v13 = v6;
if ( v6 >= 0xA )
{
LABEL_17:
v11[1] = dest[2 * (v13 & 0x7FFFFFFF) + 1];
v15 = dest[2 * v13];
goto LABEL_20;
}
}
else
{
v12 = v7 - 2;
do
{
v13 = v6 / 0x64;
v14 = v6 % 0x64;
v11[v12 + 1] = dest[2 * v14 + 1];
v11[v12] = dest[2 * v14];
v12 -= 2;
v9 = v6 <= 0x270F;
v6 /= 0x64u;
}
while ( !v9 );
if ( v13 >= 0xA )
goto LABEL_17;
}
v15 = v13 + 48;
LABEL_20:
*v11 = v15;
v16 = std::string::_M_replace(&s2, 0LL, 0LL, v87, 6LL);
v17 = v16;
*(_QWORD *)stat_loc = v132;
v18 = (_QWORD *)(v16 + 16);
if ( *(_QWORD *)v16 == v16 + 16 )
{
memcpy(v132, *(const void **)v16, *(_QWORD *)(v16 + 8) + 1LL);
}
else
{
*(_QWORD *)stat_loc = *(_QWORD *)v16;
v132[0] = *v18;
}
*(_QWORD *)&stat_loc[2] = *(_QWORD *)(v17 + 8);
*(_QWORD *)v17 = v18;
*(_QWORD *)(v17 + 8) = 0LL;
*(_BYTE *)(v17 + 16) = 0;
if ( (unsigned __int64)(*(_QWORD *)&stat_loc[2] - 0x3FFFFFFFFFFFFFFBLL) <= 4 )
std::__throw_length_error(v111);
v19 = std::string::_M_append(stat_loc, &v85, 5LL);
v20 = v19;
v120 = v122;
v21 = (_QWORD *)(v19 + 16);
if ( *(_QWORD *)v19 == v19 + 16 )
{
memcpy(v122, *(const void **)v19, *(_QWORD *)(v19 + 8) + 1LL);
}
else
{
v120 = *(void **)v19;
v122[0] = *v21;
}
v121 = *(char **)(v20 + 8);
*(_QWORD *)v20 = v21;
*(_QWORD *)(v20 + 8) = 0LL;
*(_BYTE *)(v20 + 16) = 0;
if ( *(_QWORD **)stat_loc != v132 )
operator delete(*(void **)stat_loc);
if ( s2 != v90 )
operator delete(s2);
std::ifstream::basic_ifstream(stat_loc, &v120, 8LL);
v100[0] = v101;
v100[1] = 0LL;
v101[0] = 0;
v22 = *(_QWORD *)(*(_QWORD *)stat_loc - 24LL);
if ( (*((_BYTE *)&v132[2] + v22) & 5) == 0 )
{
v23 = *(_BYTE **)((char *)&v132[28] + v22);
if ( !v23 )
std::__throw_bad_cast();
if ( v23[56] )
{
v24 = v23[67];
}
else
{
std::ctype<char>::_M_widen_init(*(_QWORD *)((char *)&v132[28] + v22));
v24 = (*(__int64 (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v23 + 48LL))(v23, 10LL);
}
std::getline<char,std::char_traits<char>,std::allocator<char>>(stat_loc, v100, (unsigned int)v24);
}
std::ifstream::~ifstream(stat_loc, &`VTT for'std::ifstream);
std::ios_base::~ios_base((std::ios_base *)v133);
if ( v120 != v122 )
operator delete(v120);
prctl(4, 0LL);
v25 = fork();
if ( v25 < 0 )
{
v4 = 1;
goto LABEL_114;
}
if ( v25 )
{
v26 = v25;
stat_loc[0] = 0;
while ( waitpid(v26, stat_loc, 1) != v26 )
{
if ( (unsigned __int8)check_tracer_pid(v26) || (v27 = getpid(), (unsigned __int8)check_tracer_pid(v27)) )
{
kill(v26, 9);
goto LABEL_113;
}
usleep(0x7A120u);
}
goto LABEL_113;
}
v28 = getpid();
if ( (unsigned __int8)check_tracer_pid(v28) )
_exit(1);
v29 = (_DWORD *)operator new(4uLL);
*v29 = 1179927382;
s2 = v90;
*(_QWORD *)stat_loc = 76LL;
v30 = (__m128i *)std::string::_M_create(&s2, stat_loc, 0LL);
s2 = v30;
v31 = *(_QWORD *)stat_loc;
v90[0] = *(_QWORD *)stat_loc;
*(__m128i *)((char *)v30 + 60) = *(__m128i *)&v115[12];
si128 = _mm_load_si128((const __m128i *)&v112);
v33 = _mm_load_si128((const __m128i *)&v113);
v34 = _mm_load_si128((const __m128i *)&v114);
v30[3] = _mm_load_si128((const __m128i *)v115);
v30[2] = v34;
v30[1] = v33;
*v30 = si128;
v89 = v31;
v30->m128i_i8[v31] = 0;
src = v107;
n = 0LL;
v107[0] = 0;
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, &v109, 19LL);
v35 = *(_BYTE **)((char *)&std::cout + *(_QWORD *)(std::cout - 24LL) + 240);
if ( !v35
|| (!v35[56]
? (std::ctype<char>::_M_widen_init(v35),
v36 = (*(__int64 (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v35 + 48LL))(v35, 10LL))
: (v36 = v35[67]),
v37 = (std::ostream *)std::ostream::put((std::ostream *)&std::cout, v36),
std::ostream::flush(v37),
v38 = *(_QWORD *)(std::cin - 24LL),
(v39 = *(_BYTE **)((char *)&std::cin + v38 + 240)) == 0LL) )
{
LABEL_118:
std::__throw_bad_cast();
}
if ( v39[56] )
{
v40 = v39[67];
}
else
{
std::ctype<char>::_M_widen_init(*(_QWORD *)((char *)&std::cin + v38 + 240));
v40 = (*(__int64 (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v39 + 48LL))(v39, 10LL);
}
v41 = (_QWORD *)std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, &src, (unsigned int)v40);
if ( (*((_BYTE *)v41 + *(_QWORD *)(*v41 - 24LL) + 32) & 5) == 0 )
{
v44 = n;
if ( (n & 0x8000000000000000LL) != 0LL )
std::__throw_length_error(v116);
if ( n )
{
v45 = src;
v46 = (char *)operator new(n);
v47 = &v46[v44];
memcpy(v46, v45, v44);
}
else
{
v46 = 0LL;
v47 = 0LL;
}
v49 = _mm_load_si128((const __m128i *)&xmmword_4010);
v50 = 56LL;
v51 = _mm_load_si128((const __m128i *)&xmmword_4020);
v52 = _mm_load_si128((const __m128i *)&xmmword_4030);
v53 = _mm_load_si128((const __m128i *)&xmmword_4040);
v54 = _mm_load_si128((const __m128i *)&xmmword_4050);
v55 = _mm_load_si128((const __m128i *)&xmmword_4060);
v56 = _mm_load_si128((const __m128i *)&xmmword_4070);
v57 = _mm_load_si128((const __m128i *)&xmmword_4080);
v58 = _mm_load_si128(xmmword_4090);
do
{
*(__m128i *)&dest[2 * v50 + 104] = v49;
*(__m128i *)&dest[2 * v50 + 120] = _mm_add_epi16(v49, v51);
*(__m128i *)&dest[2 * v50 + 136] = _mm_add_epi16(v49, v52);
*(__m128i *)&dest[2 * v50 + 152] = _mm_add_epi16(v49, v53);
*(__m128i *)&dest[2 * v50 + 168] = _mm_add_epi16(v49, v54);
*(__m128i *)&dest[2 * v50 + 184] = _mm_add_epi16(v49, v55);
*(__m128i *)&dest[2 * v50 + 200] = _mm_add_epi16(v49, v56);
*(__m128i *)&stat_loc[v50 / 2] = _mm_add_epi16(v49, v57);
v49 = _mm_add_epi16(v49, v58);
v50 += 64LL;
}
while ( v50 != 1080 );
v59 = 1LL;
LOWORD(v60) = 0;
do
{
v61 = *(_WORD *)&dest[2 * v59 + 214];
v62 = v61 + v60 + *((unsigned __int8 *)v29 + (((_BYTE)v59 - 1) & 2));
v63 = v62 & 0x3FF;
*(_WORD *)&dest[2 * v59 + 214] = *((_WORD *)stat_loc + v63);
*((_WORD *)stat_loc + v63) = v61;
v64 = *((unsigned __int16 *)stat_loc + v59);
v60 = (v62 + (_WORD)v64 + *((unsigned __int8 *)v29 + (v59 & 3))) & 0x3FF;
v65 = *((unsigned __int16 *)stat_loc + v60);
*((_WORD *)stat_loc + v59) = v65;
*((_WORD *)stat_loc + v60) = v64;
v59 += 2LL;
}
while ( v59 != 1025 );
v66 = v47 - v46;
if ( v47 != v46 )
{
v67 = 1LL;
if ( v66 >= 2 )
v67 = v47 - v46;
LOWORD(v66) = 0;
LOWORD(v65) = 0;
v64 = 0LL;
do
{
v66 = ((_WORD)v66 + 1) & 0x3FF;
v68 = *((_WORD *)stat_loc + v66);
v65 = (v68 + (_WORD)v65) & 0x3FF;
*((_WORD *)stat_loc + v66) = *((_WORD *)stat_loc + v65);
*((_WORD *)stat_loc + v65) = v68;
v46[v64++] ^= *((_BYTE *)stat_loc + 2 * ((v68 + *((_WORD *)stat_loc + v66)) & 0x3FF)) ^ 0x91;
}
while ( v67 != v64 );
}
std::ostringstream::basic_ostringstream(&v120, v64, v65, v66, *(double *)v49.m128i_i64);
v69 = v120;
v70 = *((_QWORD *)v120 - 3);
v71 = *(_DWORD *)((_BYTE *)&v122[1] + v70) & 0xFFFFFFB5 | 8;
*(_DWORD *)((char *)&v122[1] + v70) = v71;
v72 = *(v69 - 3);
if ( !v129[v72 + 113] )
{
v73 = *(_BYTE **)&v129[v72 + 128];
if ( !v73 )
std::__throw_bad_cast();
if ( !v73[56] )
{
std::ctype<char>::_M_widen_init(*(_QWORD *)&v129[v72 + 128]);
(*(void (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v73 + 48LL))(v73, 32LL);
v69 = v120;
}
v129[v72 + 113] = 1;
}
v129[v72 + 112] = 48;
*(_DWORD *)((char *)&v122[1] + *(v69 - 3)) &= ~0x4000u;
v74 = v47 - v46;
if ( v74 )
{
v75 = 1LL;
if ( v74 >= 2 )
v75 = v74;
for ( i = 0LL; i != v75; ++i )
{
*(_QWORD *)((char *)v122 + *((_QWORD *)v120 - 3)) = 2LL;
std::ostream::operator<<(&v120, (unsigned __int8)v46[i]);
}
}
s1 = v104;
v103 = 0LL;
v104[0] = 0;
v77 = v123;
if ( v125 > v123 )
v77 = v125;
if ( v125 && v77 )
std::string::_M_replace(&s1, 0LL, 0LL, v124, v77 - v124);
else
std::string::_M_assign(&s1, &v127, v71, v70, v77);
if ( v103 != v89 || v103 && bcmp(s1, s2, v103) )
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, &v83, 5LL);
v78 = *(_BYTE **)((char *)&std::cout + *(_QWORD *)(std::cout - 24LL) + 240);
if ( v78 )
{
if ( !v78[56] )
goto LABEL_101;
goto LABEL_100;
}
}
else
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, v108, 13LL);
v78 = *(_BYTE **)((char *)&std::cout + *(_QWORD *)(std::cout - 24LL) + 240);
if ( v78 )
{
if ( !v78[56] )
{
LABEL_101:
std::ctype<char>::_M_widen_init(v78);
v79 = (*(__int64 (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v78 + 48LL))(v78, 10LL);
goto LABEL_102;
}
LABEL_100:
v79 = v78[67];
LABEL_102:
v80 = (std::ostream *)std::ostream::put((std::ostream *)&std::cout, v79);
std::ostream::flush(v80);
if ( s1 != v104 )
operator delete(s1);
v81 = (void *)`VTT for'std::ostringstream[3];
v120 = (void *)`VTT for'std::ostringstream[0];
*(void **)((char *)&v120 + *(_QWORD *)(`VTT for'std::ostringstream[0] - 24LL)) = v81;
v121 = (char *)&`vtable for'std::stringbuf + 16;
if ( v127 != &v128 )
operator delete(v127);
v121 = (char *)&`vtable for'std::streambuf + 16;
std::locale::~locale((std::locale *)v126);
std::ios_base::~ios_base((std::ios_base *)v129);
if ( v46 )
operator delete(v46);
goto LABEL_108;
}
}
std::__throw_bad_cast();
}
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, &v83, 5LL);
v42 = *(_BYTE **)((char *)&std::cout + *(_QWORD *)(std::cout - 24LL) + 240);
if ( !v42 )
goto LABEL_118;
if ( v42[56] )
{
v43 = v42[67];
}
else
{
std::ctype<char>::_M_widen_init(v42);
v43 = (*(__int64 (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)v42 + 48LL))(v42, 10LL);
}
v48 = (std::ostream *)std::ostream::put((std::ostream *)&std::cout, v43);
std::ostream::flush(v48);
LABEL_108:
if ( src != v107 )
operator delete(src);
if ( s2 != v90 )
operator delete(s2);
operator delete(v29);
LABEL_113:
v4 = 0;
LABEL_114:
if ( v100[0] != v101 )
operator delete(v100[0]);
return v4;
}

它的字符串都是通过一个函数 sub_3B00 进行动态解密的
用 frida hook 这个函数,dump 出解密后的字符串(因为最开始不想 patch 文件绕过反调试动调于是就用的 frida)

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
57
58
59
60
61
62
63
64
65
66
# interact.py
import frida
import sys
import time

def on_message(message, data):
if message['type'] == 'send':
print("[*] {}".format(message['payload']))
elif message['type'] == 'error':
print("[!] Script error:", message.get('description', ''))
if 'stack' in message:
print(message['stack'])
else:
print(message)

def main():
with open('test.js', 'r', encoding='utf-8') as f:
jscode = f.read()

target = "./attachment"
device = frida.get_local_device()

try:
pid = device.spawn([target],stdio="pipe")
session=device.attach(pid)
print(pid)
except frida.ProcessNotFoundError:
print("No such process")
sys.exit(1)
except Exception as e:
print("spawn/attach error",e)
sys.exit(1)

try:
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
# time.sleep(2)
print("script loaded")
except Exception as e:
print("script load error",e)

try:
session.detach()
except:
pass
sys.exit(1)

try:
device.resume(pid) # 恢复目标进程
device.input(pid,b"flag{0123456789abcdef}\n")
print("process resumed")
except Exception as e:
print("resume error",e)
sys.exit(1)

try:
print("waiting for hooks to trigger...")
while True:
time.sleep(1)
except KeyboardInterrupt:
print("detaching...")
session.detach()

if __name__ == '__main__':
main()

hook 代码

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
function hook_xor_decrypt(){
console.log("[*] Hooking xor_decrypt function...");
let target_module = "attachment";
let mod = Process.getModuleByName(target_module);
let base = mod.base;
console.log("Module " + target_module + " base address: " + base);
let offset = 0x3b00
let target_addr = base.add(offset);
console.log("Target address: " + target_addr);

let globalId = 0;
Interceptor.attach(target_addr,{
onEnter:function(args){
this.buf = args[0];
this.key = args[1];
this.len = args[2].toInt32();
this.keylen = args[3].toInt32();
this.id = ++globalId;
console.log("[*] xor_decrypt id=" + this.id);
console.log("buf: " + this.buf,"key: " + this.key,"len: " + this.len,"keylen: " + this.keylen);

},
onLeave:function(retval){
try {
console.log("[*] Decrypted Data:");
console.log(hexdump(this.buf, {
length: this.len,
header: true,
ansi: false
}));
} catch (e) {
console.log("[-] Error reading decrypted data: " + e);
}
}
})
}


function main(){
hook_xor_decrypt();
}

setImmediate(main);

解密后的字符串,密文也 dump 出来了

绕过反调试

注意到 main 函数里面的这部分内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v25 = fork();
if ( v25 < 0 )
{
v4 = 1;
goto LABEL_114; // 出错,返回 1
}
if ( v25 )
{
// 父进程分支:v25 > 0,v25 是子进程 PID
...
}
else
{
// 子进程分支:v25 == 0
...
}

当调用 pid_t pid = fork(); 时,操作系统会把当前的进程复制一份,产生两个近乎一样的进程,原来的进程是父进程 ,新建的进程是子进程,然后这两个进程都会从 fork 调用处继续往下执行,但是它们拿到的 fork() 的返回值不同,在父进程里,fork() 返回新创建的子进程的 PID(一个正整数),而在子进程里,fork() 返回 0。

从这里就可以看出来后面的魔改 rc4 加密逻辑都是在子进程里实现的,由于用 frida hook 子进程也很麻烦,相关的资料也不多,最后还是选择了 patch 二进制文件绕过反调试。

main 函数里面有好几处先 getpid 然后 check_tracer_pid 的地方:
check_tracer_pid(a1) 构造出 /proc/<a1>/status,读取其中 TracerPid: 行,把后面的数字解析为整数,如果这个值非 0,就返回 1(表示当前进程被 ptrace/调试器跟踪),否则返回 0。

查看汇编,把要跳到 exit 或者 kill 部分逻辑的地方给 nop

由于程序里还有个 prctl(4, 0LL); 是防止被 ptrace 的,所以要把它也给 nop 掉

attach 子进程调试

apply patches 一下,然后启用一个 Linux 终端运行 patch 之后的文件,运行到等待输入的时候

然后用 ida attach 上去,选择进程的时候选择 pid 大一点的那个(fork 的子进程要比父进程晚一点)
同时我 ida 的 linux_server 还要 sudo 启动才行(不然它会 attach 失败)
然后就可以调试了

断点断在比较处可以拿到密文 s2
be8c49a8115f91236044f230289b556a5ef30c550f7e75263a6fb3cd1e4d02a63acab60489a5

可以打断点 dump 出魔改 RC4 的 S 盒或者密钥流解密,或者直接把密文 patch 到输入的位置,调试一遍直接出 flag


VCTF Reverse wp + 复现
http://example.com/2025/11/17/VCTF2025/
作者
Eleven
发布于
2025年11月17日
许可协议