总结

这场比赛里面的re题目对于打这次比赛时的我,总结就是一个字:难!
最有希望能做出来的realme竟然是一道反调试SMC,没见过反调试的我兜兜转转还是没做出来,其他题更别说了。
感觉目前自己对动态调试比较生疏,缺乏一些技巧,比如动态地去patch代码,或者修改内存地址中的值或者寄存器中的值来满足判断条件的要求,以及很多时候不清楚如何快速定位到关键部分,比如QRS中就通过在有关键输出的函数中断在返回的位置快速回到调用方函数中。

复现

Realme

这是一道反调试SMC的题目。
反调试就是通过各种手段检测程序是否正处在调试模式,如果是,则不按原本的控制流执行,甚至可能直接退出。所以直接运行程序的时候结果是正确的,而动调(不绕过反调试语句时)得到的结果是错误的。
打开题目一眼以为是魔改的RC4,但是写出来解密脚本运行之后发现不对,由于没见过反调试,一度以为是脚本写错了,看了其他人的WP知道这是一道反调试题,于是尝试复现了一下。

可以发现_main()函数下方有两个含反调试判断的函数没有编译出来,按P转为函数之后F5反编译出来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// write access to const memory has been detected, the output may be wrong!
void *sub_401500()
{
void *result; // eax

result = (void *)(NtCurrentPeb()->NtGlobalFlag & 0x70); // BreakPoint
if ( !result )
{
*(&loc_40902A + 1) ^= 0x65u;
*(&loc_40902A + 2) ^= 0xFAu;
*(&loc_407080 + 2) ^= 5u;
result = &loc_40B077;
loc_40B077 ^= 0x10u;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// write access to const memory has been detected, the output may be wrong!
void *__cdecl sub_4015C0(int a1, char a2)
{
void *result; // eax

sub_401023("%s", a2);
if ( CloseHandle((HANDLE)0x1234) || GetLastError() != 6 ) // BreakPoint
return 0;
*(&loc_407080 + 1) ^= 0xD0u;
*(&loc_40B104 + 2) ^= 0xCBu;
loc_40B078 ^= 0x61u;
result = &loc_40B079;
loc_40B079 ^= 0x5Fu;
return result;
}

分别在注释处打上断点,动调,第一个函数修改为jmp跳转(忽略result的结果),第二个函数直接nop掉整个if语句。
realme1
realme2

来到smc加密处,重新反编译两个函数得到魔改RC4如下,发现KSA多异或了两次,PRGA将原本的异或改为了加减法。

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
void *__cdecl sub_40B000(int a1, int a2, unsigned int a3)
{
void *result; // eax
char v4; // [esp+D3h] [ebp-129h]
char v5[264]; // [esp+DCh] [ebp-120h] BYREF
int v6; // [esp+1E4h] [ebp-18h]
int i; // [esp+1F0h] [ebp-Ch]

v6 = 0;
result = memset(v5, 0, 0x100u);
for ( i = 0; i < 256; ++i )
{
*(i + a1) = i ^ 0xCF;
v5[i] = *(a2 + i % a3);
result = (void *)(i + 1);
}
for ( i = 0; i < 256; ++i )
{
v6 = (v5[i] + v6 + *(i + a1)) % 256;
v4 = *(i + a1);
*(i + a1) = *(v6 + a1);
*(v6 + a1) = v4 ^ 0xAD;
result = (void *)(i + 1);
}
return result;
}
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
unsigned int __cdecl sub_401A60(int a1, int a2, unsigned int a3)
{
unsigned int result; // eax
int v4; // ecx
char v5; // al
char v6; // [esp+D3h] [ebp-35h]
unsigned int i; // [esp+DCh] [ebp-2Ch]
int v8; // [esp+F4h] [ebp-14h]
int v9; // [esp+100h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_41200F);
v9 = 0;
v8 = 0;
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a3 )
break;
v9 = (v9 + 1) % 256;
v8 = (v8 + v9 * (v9 + a1)) % 256;
v6 = *(v9 + a1);
*(v9 + a1) = *(v8 + a1);
*(v8 + a1) = v6;
v4 = (*(v8 + a1) + *(v9 + a1)) % 256;
if ( i % 2 )
v5 = *(v4 + a1) + *(i + a2);
else
v5 = *(i + a2) - *(v4 + a1);
*(i + a2) = v5;
}
return result;
}

解密脚本如下:

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
def rc4_variant_ksa(key: bytes) -> list[int]:
S = [(i ^ 0xCF) for i in range(256)]
T = [key[i % len(key)] for i in range(256)]

j = 0
for i in range(256):
j = (j + S[i] + T[i]) % 256
tmp = S[i]
S[i] = S[j]
S[j] = tmp ^ 0xAD

return S

def rc4_variant_prga_decrypt(ciphertext, S_init):
S = S_init
i = 0
j = 0
plaintext = ""

for x in range(len(ciphertext)):
i = (i + 1) % 256
j = (j + i * S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
if x % 2:
plaintext += (chr((ciphertext[x] - k) & 0xFF))
else:
plaintext += (chr((ciphertext[x] + k) & 0xFF))

return plaintext


key = b"Y0u_Can't_F1nd_Me!"

ciphertext = bytes([
80, 89, 162, 148, 46, 142, 92, 149, 121, 22, 229, 54,
96, 199, 232, 6, 51, 120, 240, 208, 54, 200, 115, 27, 101, 64,
181, 212, 232, 156, 101, 244, 186, 98, 208
])

S = rc4_variant_ksa(key)
flag = rc4_variant_prga_decrypt(ciphertext, S)

print(flag)
# NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

QRS

一道魔改xxtea题目,无壳,由Rust编写,所以反编译出来照旧一坨。
直接运行程序发现启动了一个本地Web服务,端口号是8887(http://localhost:8887/interface),不带参数访问之后页面打印出:
Failed to deserialize query string: missing field input
说明需要代入参数input访问,随便带个数字进去页面打印出:
{“msg”:”failed”}

但是我没有什么头绪怎么定位到加密函数,参考了其他大佬的WP之后我才反应过来可以通过访问URL之后页面打印出的字符串来搜索有关函数,于是搜索了一下”missing”,查看第一条:
qrs1
想直接去查看xrefs,但是不知道为什么找不到调用方函数,看了其他大佬的WP后选择动调,断点在函数返回的位置:
qrs2
返回上一层调用的位置,并再次断在return语句处:
qrs3
返回上一层调用的位置,猜测这里的函数QRS::e12c96f7a24fc73e1::axum_extract::b2a92c317a3cdec81::h3500f6e3d3784860是加密函数(这里我做了变量重命名,方便查看)。_xmm中存放的应该就是密钥和密文,从下面验证加密后的输入是否与密文相等也可以推测出。
qrs4
加密函数如下(取消了类型注释):

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
_QWORD *__fastcall QRS::e12c96f7a24fc73e1::axum_extract::b2a92c317a3cdec81::h3500f6e3d3784860(
_QWORD *a1,
__int64 a2,
const void *a3,
signed __int64 a4)
{
const void *v4; // rbx
__int64 v8; // r14
__int64 v9; // rax
__int64 v10; // r12
signed __int64 v11; // rax
__int64 v12; // rsi
__int64 v13; // r14
__int64 v14; // rbx
bool v15; // sf
__int64 v16; // rbx
__int64 v17; // r14
__int64 v18; // rax
__int64 v19; // r15
__int64 v20; // r12
__int64 v21; // r14
unsigned int *v22; // rsi
unsigned int v23; // ebx
unsigned int v24; // r13d
int v25; // r12d
DWORD TickCount; // eax
int v27; // r8d
void *v28; // r12
__int64 v29; // r14
_QWORD *v31; // [rsp+30h] [rbp-50h]
void *Src; // [rsp+50h] [rbp-30h]
void *Srca; // [rsp+50h] [rbp-30h]
unsigned int *v34; // [rsp+58h] [rbp-28h]
size_t Sizea; // [rsp+60h] [rbp-20h]
size_t Size; // [rsp+60h] [rbp-20h]
__int64 v37; // [rsp+68h] [rbp-18h] BYREF
__int64 v38; // [rsp+70h] [rbp-10h]
__int64 v39; // [rsp+78h] [rbp-8h]
__int64 v40; // [rsp+80h] [rbp+0h]

v40 = -2;
if ( a4 < 0 )
{
v8 = 0;
goto LABEL_27;
}
v4 = a3;
if ( a4 )
{
v8 = 1;
v9 = _rust_alloc(a4, 1);
if ( v9 )
{
v10 = v9;
goto LABEL_6;
}
LABEL_27:
alloc::raw_vec::handle_error::h7b8f8f97c1be85f2(v8, a4);
goto LABEL_28;
}
v10 = 1;
LABEL_6:
memcpy_0(v10, v4, a4);
v37 = a4;
v38 = v10;
v39 = a4;
v11 = a4 & 7;
v31 = a1;
if ( (a4 & 7) == 0 )
{
v34 = v10;
v16 = a4;
Size = a4;
if ( a4 )
goto LABEL_10;
LABEL_22:
v37 = 0;
v38 = 1;
v39 = 0;
goto LABEL_23;
}
v4 = (8 - v11);
Src = _rust_alloc_zeroed(8 - v11, 1);
if ( !Src )
{
LABEL_28:
alloc::raw_vec::handle_error::h7b8f8f97c1be85f2(1, v4);
goto LABEL_31;
}
Sizea = v4;
alloc::raw_vec::RawVecInner$LT$A$GT$::reserve::do_reserve_and_handle::h5c9a053f482a75b1(&v37, a4, v4, 1, 1);
v12 = v37;
v13 = v38;
v14 = v39;
memcpy_0((v38 + v39), Src, Sizea);
_rust_dealloc(Src, Sizea, 1);
v15 = (Sizea + v14) < 0;
v16 = Sizea + v14;
v34 = v13;
if ( v15 )
{
v17 = 0;
goto LABEL_30;
}
Size = v12;
if ( !v16 )
goto LABEL_22;
LABEL_10:
v17 = 1;
v18 = _rust_alloc(v16, 1);
if ( !v18 )
{
LABEL_30:
alloc::raw_vec::handle_error::h7b8f8f97c1be85f2(v17, v16);
LABEL_31:
BUG();
}
v19 = v18;
v37 = v16;
v38 = v18;
v39 = 0;
v20 = v16 & 0x7FFFFFFFFFFFFFF8;
if ( (v16 & 0x7FFFFFFFFFFFFFF8) != 0 )
{
v21 = 0;
v22 = v34;
do
{
Srca = v20;
v23 = *v22;
v24 = v22[1];
v25 = 48;
TickCount = GetTickCount();
v27 = 0;
do
{
v23 += (v24 + ((16 * v24) ^ (v24 >> 5))) ^ (*(a2 + 4 * (v27 & 3)) + v27);
v24 += (v23 + ((16 * v23) ^ (v23 >> 5))) ^ (v27 + TickCount + *(a2 + (((v27 + TickCount) >> 9) & 0xC)));
v27 += TickCount;
--v25;
}
while ( v25 );
if ( (v37 - v21) > 3 )
{
v28 = Srca;
}
else
{
alloc::raw_vec::RawVecInner$LT$A$GT$::reserve::do_reserve_and_handle::h5c9a053f482a75b1(&v37, v21, 4, 1, 1);
v28 = Srca;
v19 = v38;
v21 = v39;
}
*(v19 + v21) = v23;
v29 = v21 + 4;
v39 = v29;
if ( (v37 - v29) <= 3 )
{
alloc::raw_vec::RawVecInner$LT$A$GT$::reserve::do_reserve_and_handle::h5c9a053f482a75b1(&v37, v29, 4, 1, 1);
v29 = v39;
}
v22 += 2;
v20 = v28 - 8;
v19 = v38;
*(v38 + v29) = v24;
v21 = v29 + 4;
v39 = v21;
}
while ( v20 );
}
LABEL_23:
v31[2] = v39;
*v31 = v37;
v31[1] = v38;
if ( Size )
_rust_dealloc(v34, Size, 1);
return v31;
}

可以看出是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
import struct

def decrypt_block(l, r, tickCount, key):
v1 = (tickCount * 48) & 0xFFFFFFFF

for _ in range(48):
v1 = (v1 - tickCount) & 0xFFFFFFFF

part1 = (16 * l) & 0xFFFFFFFF
shift1 = l >> 5
add1 = (l + (part1 ^ shift1)) & 0xFFFFFFFF

index1 = ((v1 + tickCount) >> 9) & 0xC
key1 = key[index1 // 4]
xor1 = (v1 + tickCount + key1) & 0xFFFFFFFF

r = (r - (add1 ^ xor1)) & 0xFFFFFFFF

part2 = (16 * r) & 0xFFFFFFFF
shift2 = r >> 5
add2 = (r + (part2 ^ shift2)) & 0xFFFFFFFF

key2 = key[v1 & 3]
xor2 = (v1 + key2) & 0xFFFFFFFF

l = (l - (add2 ^ xor2)) & 0xFFFFFFFF

return l, r

key = [0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210]
ciphertext = [
0x083EA621, 0xC745973C,
0xE3B77AE8, 0xCDEE8146,
0x7DC86B96, 0x6B8C9D3B,
0x79B14342, 0x2ECF0F0D
]

plaintext = b''

for i in range(4):
l = ciphertext[i * 2]
r = ciphertext[i * 2 + 1]
l, r = decrypt_block(l, r, 0x68547369, key)
plaintext += struct.pack('<II', l, r)

flag = plaintext.decode()
print(f"NepCTF{{{flag}}}")
# NepCTF{a4747f82be106d3f8c4d747c744d7ee5}

Crackme

这道题算是我碰到的第一道非常偏向于比赛之外的正经逆向工程的re题目,是一道注册机题目(不过说实话我自己还没有好好做过Crackme)。这题复现花的时间很长,也参考了其他大佬的WP。
题目直接运行之后如下,应该是需要输入正确的用户名和密码。
crackme1
DIE查看发现无壳,ida打开之后直接Shift+F12查看字符串如下,很容易定位到关键函数sub_4021C0()。
crackme2

首先先通过qt控件获得了输入的用户名和密码,做了一个正则化匹配:

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
// ......
QLineEdit::text(&v46);
QString::toUtf8_helper(&username_utf8, &v46);
if ( *username_utf8 > 1u || (v0 = username_utf8, v44 = *(username_utf8 + 2), v44 != 24) )
{
QByteArray::reallocData(&username_utf8, (*(username_utf8 + 1) + 1), *(username_utf8 + 11) >> 31);
v0 = username_utf8;
v44 = *(username_utf8 + 2);
}
QLineEdit::text(&v58);
QString::trimmed_helper(&password_unspaced, &v58);
v2 = v58;
if ( !*v58 || *v58 != -1 && (v3 = _InterlockedSub(v58, 1u), v2 = v58, !v3) )
QArrayData::deallocate(v2, 2, 8);
v58 = QString::fromAscii_helper("^[0-9a-f]{2,}$", 0xE, v1);
QRegularExpression::QRegularExpression(v49, &v58, 0);
v4 = v58;
if ( !*v58 || *v58 != -1 && (v5 = _InterlockedSub(v58, 1u), v4 = v58, !v5) )
QArrayData::deallocate(v4, 2, 8);
QRegularExpression::match(&v58, v49, &password_unspaced, 0, 0, 0);
if ( !QRegularExpressionMatch::hasMatch(&v58) || (password_unspaced[1] & 1) != 0 )
{
QRegularExpressionMatch::~QRegularExpressionMatch(&v58);
password_unhexed = QArrayData::shared_null;
v58 = QString::fromAscii_helper("please try again!", 0x11, v6);
v56 = QString::fromAscii_helper("Wrong", 5, v7);
QMessageBox::warning(0, &v56, &v58, 1024, 0);

// ......
}

然后将密码转为二进制字节数组,检测长度是否为16,说明密码应该是32位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ......
QRegularExpressionMatch::~QRegularExpressionMatch(&v58);
password_unhexed = QArrayData::shared_null;
QString::toUtf8_helper(&v58, &password_unspaced);
QByteArray::fromHex(&v54, &v58);
v23 = password_unhexed;
v24 = v54;
v54 = password_unhexed;
password_unhexed = v24;
if ( !*v54 || *v54 != -1 && !_InterlockedSub(v23, 1u) )
QArrayData::deallocate(v54, 1, 8);
v25 = v58;
if ( !*v58 || *v58 != -1 && (v26 = _InterlockedSub(v58, 1u), v25 = v58, !v26) )
QArrayData::deallocate(v25, 1, 8);
if ( *(password_unhexed + 1) != 16 )
{
v58 = QString::fromAscii_helper("please try again!", 0x11, v22);
v56 = QString::fromAscii_helper("Wrong", 5, v27);
QMessageBox::warning(0, &v56, &v58, 1024, 0);

// ......
}

未完待续……