Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

NepCTF2025逆向方向Writeups

强度好大

realme

初步静态看的话是输入和密钥流进行取模操作, 但是很明显这是不可逆的:

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
size_t __cdecl sub_401C90(_BYTE *a1, char *in, size_t i_2)
{
size_t i_1; // eax
char v4; // [esp+D3h] [ebp-35h]
size_t i; // [esp+DCh] [ebp-2Ch]
int v6; // [esp+F4h] [ebp-14h]
int v7; // [esp+100h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_41200F);
v7 = 0;
v6 = 0;
for ( i = 0; ; ++i )
{
i_1 = i;
if ( i >= i_2 )
break;
v7 = (v7 + 1) % 256;
v6 = (v6 + (unsigned __int8)a1[v7]) % 256;
v4 = a1[v7];
a1[v7] = a1[v6];
a1[v6] = v4;
in[i] = (unsigned __int8)in[i]
% (__int16)(unsigned __int8)a1[((unsigned __int8)a1[v6] + (unsigned __int8)a1[v7]) % 256];
}
return i_1;
}

然后就开始翻全局构造函数, 发现了这个:

image-20250727234055232

有反调试, 被检测到了就不会修改程序的控制流, 改zf标志位调试就能bypass, 目前已知inline hook了输入函数, 修改了加密函数, 调试进一步看看还有什么修改, 结果在被hook代码里发现了经典的关闭无效句柄反调试, 直接改EIP来bypass:

image-20250727234449250

此时就能进入真正的加密控制流:

image-20250727234659555

分别在运算的时候下两个条件断点来获取密钥流:

1
2
3
4
from ida_dbg import get_reg_val as regs
print(f'-{regs("ecx"):#02x}',end=', ') #for sub
from ida_dbg import get_reg_val as regs
print(f'{regs("ecx"):#02x}',end=', ') #for add

解密脚本:

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
PY = [0] * 35
PY[0] = b'P'[0]
PY[1] = b'Y'[0]
PY[2] = -94
PY[3] = -108
PY[4] = 46
PY[5] = -114
PY[6] = 92
PY[7] = -107
PY[8] = 121
PY[9] = 22
PY[10] = -27
PY[11] = 54
PY[12] = 96
PY[13] = -57
PY[14] = -24
PY[15] = 6
PY[16] = 51
PY[17] = 120
PY[18] = -16
PY[19] = -48
PY[20] = 54
PY[21] = -56
PY[22] = 115
PY[23] = 27
PY[24] = 101
PY[25] = 64
PY[26] = -75
PY[27] = -44
PY[28] = -24
PY[29] = -100
PY[30] = 101
PY[31] = -12
PY[32] = -70
PY[33] = 98
PY[34] = -48

key = [-0xfe, 0xf4, -0xce, 0x51, -0x26, 0x48, -0x1f, 0x3c, -0xb7, 0xa1, -0x7a, 0xf0, -0x9, 0x79, -0x49, 0x93, -0x15, 0x19, -0x64, 0x68, -0xfb, 0x55, -0xec, 0xd6, -0xce, 0xcd, -0xc4, 0x75, -0x6b, 0x2f, -0xfe, 0xd3, -0x67, 0x41, -0xad, 0xb, -0x7f, 0x68, -0x69, 0x25, -0xdf, 0x6, -0x1d, 0x80, -0x18]

for e, k in zip(PY, key):
print(chr((e - k) % 256), end='')

crackme

通过字符串交叉引用可以发现验证流程在4021C0, 第一处会引发错误提示的点:

image-20250727235001348

看起来是输入格式的要求, 要求十六进制字符串

第二处:

image-20250727235032995

看起来是长度, 一开始以为是16个字符, 但是调试可以发现是输入的十六进制字符串转化成字节串后要16个字节, 也就是32个字符

第三处:

image-20250727235138472

这里看起来就是最后校验的部分了, 直接开调分析校验过程, 用到的第一个变量来自从libcrypto.dll导出的sign, 根据调用前一刻的寄存器分析, 第一个参数应该是要签名的数据, 第二个参数是长度:

image-20250727235432318

第一次签名获取了用户名的签名, 第二次获取了用户名拼接"Showmaker11"的签名:

image-20250727235532190

哎我, 许哥是区

然后貌似是将第一个签名当作key对密钥进行AES加密然后和第二个签名对比, 但是直接上Cyberchef会发现解不出正确密钥, 再加上这个导出的AES加密被平坦化, 合理怀疑是魔改了AES, 首先猜测是修改了加密的轮数(逆向手看到魔改AES就走不动道了还是平坦化的 球球别在逆向出这种了), 然后准备找一处进行测试, 其他的一堆位移换位操作看不懂, 一眼相中这个异或:

image-20250728000052508

然后拷打Gemini这是AES的哪一步, 会执行多少次:

image-20250728000158841

image-20250728000225811

标准的AES加密就是没有这个异或0x55? 一眼就给我盯真了? 直接下个条件断点记录:

1
2
3
4
last = open('log.txt', 'r').read().strip()
last = int(last)
with open('log.txt', 'w') as f:
f.write(str(last + 1))

结果得到记录还真是144次, 开始拷打Gemini写加解密脚本验证:

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
# Constants for AES
# S-box (Substitution Box)
S_BOX = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
]

INV_S_BOX = [
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
]

# Round Constant (Rcon) values
RCON = [
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39
]

# Galois Field (GF(2^8)) multiplication by 0x02
def gmul_2(byte):
if byte & 0x80:
return (byte << 1) ^ 0x1B
else:
return byte << 1

# Galois Field (GF(2^8)) multiplication by 0x03
def gmul_3(byte):
return gmul_2(byte) ^ byte

# Galois Field (GF(2^8)) multiplication for InvMixColumns
def gmul_e(byte): # 0x0E
return gmul_2(gmul_2(gmul_2(byte))) ^ gmul_2(gmul_2(byte)) ^ gmul_2(byte)
def gmul_b(byte): # 0x0B
return gmul_2(gmul_2(gmul_2(byte))) ^ gmul_2(byte) ^ byte
def gmul_d(byte): # 0x0D
return gmul_2(gmul_2(gmul_2(byte))) ^ gmul_2(gmul_2(byte)) ^ byte
def gmul_9(byte): # 0x09
return gmul_2(gmul_2(gmul_2(byte))) ^ byte

# Key Expansion
def key_expansion(key):
# Nk is the key length in 32-bit words (16 bytes = 4 words)
# Nr is the number of rounds (10 for 128-bit key)
# Nb is the block size in 32-bit words (128 bits = 4 words)
Nk = 4
Nr = 10
Nb = 4

# Convert key to a list of 32-bit words
key_words = [int.from_bytes(key[i:i+4], 'big') for i in range(0, len(key), 4)]

# Initialize the expanded key list
expanded_key = list(key_words)

for i in range(Nk, Nb * (Nr + 1)):
temp = expanded_key[i-1]
if i % Nk == 0:
# RotWord: Cyclic shift left by one byte
temp = ((temp << 8) & 0xFFFFFFFF) | (temp >> 24)
# SubWord: Apply S-box to each byte of the word
temp = (S_BOX[(temp >> 24) & 0xFF] << 24) | \
(S_BOX[(temp >> 16) & 0xFF] << 16) | \
(S_BOX[(temp >> 8) & 0xFF] << 8) | \
(S_BOX[temp & 0xFF])
# Rcon: XOR with round constant
temp ^= (RCON[i // Nk] << 24)

expanded_key.append(expanded_key[i - Nk] ^ temp)

# Convert expanded key words back to bytes for easier access in rounds
# Each round key is 16 bytes (4 words)
round_keys = []
for i in range(Nr + 1):
round_key_bytes = bytearray()
for j in range(Nb):
word = expanded_key[i * Nb + j]
round_key_bytes.extend(word.to_bytes(4, 'big'))
round_keys.append(round_key_bytes)

return round_keys

# AddRoundKey
def add_round_key(state, round_key):
for i in range(16):
state[i] ^= round_key[i]

# SubBytes
def sub_bytes(state):
for i in range(16):
state[i] = S_BOX[state[i]]

def inv_sub_bytes(state):
for i in range(16):
state[i] = INV_S_BOX[state[i]]

# ShiftRows
def shift_rows(state):
new_state = bytearray(16)
# Row 0: No shift
new_state[0] = state[0]
new_state[4] = state[4]
new_state[8] = state[8]
new_state[12] = state[12]

# Row 1: Left shift by 1 byte
new_state[1] = state[5]
new_state[5] = state[9]
new_state[9] = state[13]
new_state[13] = state[1]

# Row 2: Left shift by 2 bytes
new_state[2] = state[10]
new_state[6] = state[14]
new_state[10] = state[2]
new_state[14] = state[6]

# Row 3: Left shift by 3 bytes
new_state[3] = state[15]
new_state[7] = state[3]
new_state[11] = state[7]
new_state[15] = state[11]

state[:] = new_state[:] # Update the original state bytearray

def inv_shift_rows(state):
new_state = bytearray(16)
# Row 0: No shift
new_state[0] = state[0]
new_state[4] = state[4]
new_state[8] = state[8]
new_state[12] = state[12]

# Row 1: Right shift by 1 byte
new_state[1] = state[13]
new_state[5] = state[1]
new_state[9] = state[5]
new_state[13] = state[9]

# Row 2: Right shift by 2 bytes
new_state[2] = state[10]
new_state[6] = state[14]
new_state[10] = state[2]
new_state[14] = state[6]

# Row 3: Right shift by 3 bytes
new_state[3] = state[7]
new_state[7] = state[11]
new_state[11] = state[15]
new_state[15] = state[3]

state[:] = new_state[:] # Update the original state bytearray

# MixColumns (Modified with XOR 0x55)
def mix_columns(state):
new_state = bytearray(16)
for i in range(4): # Iterate through columns
s0 = state[i * 4 + 0]
s1 = state[i * 4 + 1]
s2 = state[i * 4 + 2]
s3 = state[i * 4 + 3]

# Standard MixColumns calculations
new_state[i * 4 + 0] = (gmul_2(s0) ^ gmul_3(s1) ^ s2 ^ s3) & 0xFF
new_state[i * 4 + 1] = (s0 ^ gmul_2(s1) ^ gmul_3(s2) ^ s3) & 0xFF
new_state[i * 4 + 2] = (s0 ^ s1 ^ gmul_2(s2) ^ gmul_3(s3)) & 0xFF
new_state[i * 4 + 3] = (gmul_3(s0) ^ s1 ^ s2 ^ gmul_2(s3)) & 0xFF

# --- MODIFICATION: Add XOR 0x55 to each calculated byte ---
new_state[i * 4 + 0] ^= 0x55
new_state[i * 4 + 1] ^= 0x55
new_state[i * 4 + 2] ^= 0x55
new_state[i * 4 + 3] ^= 0x55
# ---------------------------------------------------------

state[:] = new_state[:] # Update the original state bytearray

def inv_mix_columns(state):
new_state = bytearray(16)
for i in range(4): # Iterate through columns
# --- MODIFICATION: Reverse the XOR 0x55 and apply AND 0xFF ---
# Apply XOR 0x55 first to reverse the encryption's modification
# Then apply AND 0xFF as per user's instruction
s0 = (state[i * 4 + 0] ^ 0x55) & 0xFF
s1 = (state[i * 4 + 1] ^ 0x55) & 0xFF
s2 = (state[i * 4 + 2] ^ 0x55) & 0xFF
s3 = (state[i * 4 + 3] ^ 0x55) & 0xFF
# ---------------------------------------------------------------

# Standard InvMixColumns calculations
new_state[i * 4 + 0] = (gmul_e(s0) ^ gmul_b(s1) ^ gmul_d(s2) ^ gmul_9(s3)) & 0xFF
new_state[i * 4 + 1] = (gmul_9(s0) ^ gmul_e(s1) ^ gmul_b(s2) ^ gmul_d(s3)) & 0xFF
new_state[i * 4 + 2] = (gmul_d(s0) ^ gmul_9(s1) ^ gmul_e(s2) ^ gmul_b(s3)) & 0xFF
new_state[i * 4 + 3] = (gmul_b(s0) ^ gmul_d(s1) ^ gmul_9(s2) ^ gmul_e(s3)) & 0xFF

state[:] = new_state[:] # Update the original state bytearray

# AES ECB Encryption Function
def aes_ecb_encrypt_modified(plaintext, key):
if len(plaintext) != 16 or len(key) != 16:
raise ValueError("Plaintext and key must be 16 bytes long for this AES-ECB implementation.")

# Convert plaintext to a mutable bytearray (the state)
state = bytearray(plaintext)

# Generate all round keys
round_keys = key_expansion(key)

# Initial Round
add_round_key(state, round_keys[0])

# Main Rounds (9 rounds for 128-bit key)
for i in range(1, 10): # Rounds 1 to 9
sub_bytes(state)
shift_rows(state)
mix_columns(state) # This is where the 0x55 XOR modification is
add_round_key(state, round_keys[i])

# Final Round (Round 10 for 128-bit key) - No MixColumns
sub_bytes(state)
shift_rows(state)
add_round_key(state, round_keys[10])

return bytes(state)

# AES ECB Decryption Function
def aes_ecb_decrypt_modified(ciphertext, key):
if len(ciphertext) != 16 or len(key) != 16:
raise ValueError("Ciphertext and key must be 16 bytes long for this AES-ECB implementation.")

# Convert ciphertext to a mutable bytearray (the state)
state = bytearray(ciphertext)

# Generate all round keys
round_keys = key_expansion(key)

# Initial Round (Decryption's first round is encryption's last round)
add_round_key(state, round_keys[10]) # Use the last round key
inv_shift_rows(state)
inv_sub_bytes(state)

# Main Rounds (9 rounds for 128-bit key)
# Iterate from round 9 down to 1
for i in range(9, 0, -1):
add_round_key(state, round_keys[i])
inv_mix_columns(state) # This is where the 0x55 XOR and 0xFF AND modification is
inv_shift_rows(state)
inv_sub_bytes(state)

# Final Round (Decryption's last round)
add_round_key(state, round_keys[0]) # Use the first round key

return bytes(state)

# --- Usage Example ---
# Example Plaintext (16 bytes)
plaintext_hex = "c20b9ab583f9cb629880a7648b15904f"
# Example Key (16 bytes)
key_hex = "24 25 8A 5B 6A 20 62 5D D2 71 64 32 FD E7 5E C4"

plaintext = bytes.fromhex(plaintext_hex)
key = bytes.fromhex(key_hex)
ciphertext = b''

print(f"Plaintext: {plaintext.hex().upper()}")
print(f"Key: {key.hex().upper()}")

try:
ciphertext = aes_ecb_encrypt_modified(plaintext, key)
print(f"Ciphertext: {ciphertext.hex().upper()}")
except ValueError as e:
print(f"Error: {e}")

# You can now use this script to encrypt a known plaintext with a known key
# and compare the output with the one from the original modified binary.
# If they match, it strongly suggests that the only modification was indeed
# the XOR 0x55 in the MixColumns step.
enc = bytes.fromhex('22 EB B5 40 91 62 9C F7 E2 13 FF A8 8C 54 D9 80')
# enc = ciphertext # Use the ciphertext from the encryption step
key = bytes.fromhex(key_hex)
decrypted = aes_ecb_decrypt_modified(enc, key)
print(f"Decrypted: {decrypted.hex().upper()}")

结果还真就是, 那还说啥了直接开解! 但是打开网页天塌了, 要填100个人的, 只能自动化完成签名外加生成用户名密钥对的解密然后继续拷打Gemini写个自动填表单的Js脚本了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if __name__ == "__main__":
from subprocess import run, PIPE
usr_key = {}

to_gen_key = open('.\\to_decrypt.txt', 'r').read().split()
# print(to_gen_key)
for usr_name in to_gen_key:
res = run(['.\\autosign.exe', usr_name], stdout=PIPE, stderr=PIPE).stdout.strip().decode()
# print(res)
res = res.split(' -')
key = res[0]
enc = res[1]
decrypted = aes_ecb_decrypt_modified(bytes.fromhex(enc), bytes.fromhex(key))
usr_key[usr_name] = decrypted.hex()
print(usr_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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h> // Required for LoadLibrary, GetProcAddress, FreeLibrary

const char * XuGeShiQu = "Showmaker11\x00";
typedef int (__cdecl *SIGN_FUNCTION)(const char* data, int data_len, char* signature_buffer, int buffer_len);

int main(int argc, char* argv[]) {
if(argc != 2) {
return 1;
}
HMODULE hDll;
SIGN_FUNCTION signFunc;
const char* s_key = argv[1];
// printf("Message to sign: \"%s\"\nlength: %d\n", s_key, (int)strlen(s_key));
unsigned char key[16] = {0}; // Buffer to store the signature
unsigned char enc[16] = {0}; // Buffer for the encrypted signature
int signature_len = sizeof(key);

// printf("Attempting to load libcrypto.dll...\n");
hDll = LoadLibrary("libcrypto.dll");

if (hDll == NULL) {
fprintf(stderr, "Error: Could not load libcrypto.dll. GetLastError: %lu\n", GetLastError());
return 1;
}

// printf("libcrypto.dll loaded successfully. Attempting to get 'sign' function address...\n");

signFunc = (SIGN_FUNCTION)GetProcAddress(hDll, "sign");

if (signFunc == NULL) {
fprintf(stderr, "Error: Could not find function 'sign' in libcrypto.dll. GetLastError: %lu\n", GetLastError());
FreeLibrary(hDll); // Free the DLL before exiting
return 1;
}

// printf("Function 'sign' found. Calling the function...\n");
signFunc(s_key, strlen(s_key), key, signature_len);

// printf("Sign function returned: %d\n", result);
// printf("Message to sign: \"%s\"\n", s_key);
for (int i = 0; i < 0x10; ++i) {
printf("%02X ", key[i]);
}
printf("-");
char *s_enc = malloc(strlen(s_key) + strlen(XuGeShiQu) + 1);
strcpy(s_enc, s_key);
strcat(s_enc, XuGeShiQu);
signFunc(s_enc, strlen(s_enc), enc, sizeof(enc));
// printf("Encrypted signature: ");
for (int i = 0; i < 0x10; ++i) {
printf("%02X ", enc[i]);
}
printf("\n");

// Free the loaded DLL
FreeLibrary(hDll);
// printf("libcrypto.dll unloaded.\n");

return 0;
}

把用户名都复制下来存放在to_decrypt.txt编译好自动签名程序运行python脚本就能生成用户名密钥对了, 最后是自动填表脚本:

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
const userPasswords = ...;

// Function to display messages in the message box
function showMessage(message, type = 'info') {
const messageBox = document.getElementById('messageBox');
messageBox.textContent = message;
messageBox.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-yellow-100', 'text-yellow-800');
if (type === 'success') {
messageBox.classList.add('bg-green-100', 'text-green-800');
} else if (type === 'error') {
messageBox.classList.add('bg-red-100', 'text-red-800');
} else { // info or default
messageBox.classList.add('bg-yellow-100', 'text-yellow-800');
}
}

// Function to automatically fill the form
function autoFillForm() {
let filledCount = 0;
let notFoundCount = 0;
const form = document.getElementById('userForm'); // Changed ID to userForm
const inputFields = form.querySelectorAll('input[type="password"]');

if (Object.keys(userPasswords).length === 0) {
showMessage("密码字典为空,无法自动填写。", 'error');
return;
}

if (inputFields.length === 0) {
showMessage("页面上没有找到密码输入框。", 'info');
return;
}

inputFields.forEach(input => {
const username = input.id; // The id of the input is the username
if (userPasswords.hasOwnProperty(username)) {
input.value = userPasswords[username];
filledCount++;
} else {
console.warn(`Password for user '${username}' not found in the dictionary.`);
notFoundCount++;
}
});

if (filledCount > 0) {
showMessage(`成功填写了 ${filledCount} 个密码。`, 'success');
}
if (notFoundCount > 0) {
// If all fields were not found, it's an error. Otherwise, it's an info/warning.
showMessage(`有 ${notFoundCount} 个用户密码未找到。`, notFoundCount === inputFields.length ? 'error' : 'info');
}
if (filledCount === 0 && notFoundCount === 0) {
showMessage("没有找到密码输入框或字典为空。", 'info'); // Should be caught by earlier checks, but as a fallback
}
}

QRS

运行发现起了个HTTP服务, 直接搜索字符串无果, 加上不熟悉什么Web框架, 只知道是个rust程序, 遂决定调试

一调就发现程序不见了还没办法结束调试, 一眼剥离调试器, 交叉引用NtSetInformationThread发现果然是, 直接patch成NOP来bypass:

image-20250728001408355

接下来就是找路由了(是叫这个吗, 咱也不是学Web的咱也不懂), 但是因为没有任何开发相关经验所以根本找不到, 直接访问发现需要一个'input'参数, 然后拷打Gemini这个框架用什么API来解析参数结果根本没找到这个API, 然后直接搜索缺少参数的提示直接搜到了:

image-20250728002531067

image-20250728002548060

在函数头下个断点看看什么时候能获得输入, 带参发送GET请求果然在函数头断下, 接下来我直接在内存中搜索我的GET请求然后下硬件断点看看什么时候被用到:

image-20250728002834391

经过API对参数一通处理最后终于进了个memcpy, 最后再在拷贝出来的数据上下硬件断点并定位到了输入在何处被加密:

image-20250728003230335

TEA加密, GetTickCount点进去就会发现被hook成返回固定值, 调试获得key, 据此写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

void main(){
uint32_t enc[] = {0x83EA621, 0xC745973C, 0xE3B77AE8, 0xCDEE8146, 0x7DC86B96, 0x6B8C9D3B, 0x79B14342, 0x2ECF0F0D};
uint32_t key[] = {0x1234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210};
for(int i = 0; i < 8; i += 2){
uint32_t delta = 0x68547369, round = 48, sum = delta * round, v0 = enc[i], v1 = enc[i + 1];
for(int j = 0; j < round; j++){
sum -= delta;
v1 -= (v0 + ((16 * v0) ^ (v0 >> 5))) ^ (sum + delta + *(uint32_t *)((char *)key + (((sum + delta) >> 9) & 0xC)));
v0 -= (v1 + ((16 * v1) ^ (v1 >> 5))) ^ (key[sum & 3] + sum);
}
enc[i] = v0;
enc[i + 1] = v1;
}
for(int i = 0; i < 32; i++){
printf("%02X ", ((uint8_t *)&enc[i / 4])[(i % 4)]);
}
}

剩下一道内核把能想到的反调试全都试了一遍全都过不了, 拦截跳板只能看出多次在ntoskrnl.exe导出函数表里找几个看起来很像用来反调试的API, 但是看起来都没有调用过, 似了

还有一道魔改lua, IDA调试有问题用CE调的前面拦截输入什么的都挺简单, 到最后输入转成浮点数之后的操作即使跟着硬件断点走也看不懂到底怎么操作那些数据的, 似了

等有wp再复现吧