LilacCTF2026 逆向 Wp + 复现
ezPython
解包
先用 pyinstxtractor 解包,解包后的文件中找到 main.pyc, 用 pylingual 在线反编译
1 | |
反编译出来的逻辑还是比较清晰的,注意到它还 import 了一个 myalgo
模块,所以再反编译一下 myalgo.py 的内容: 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# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'myalgo.py'
# Bytecode version: 3.9.0beta5 (3425)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
import dis
import struct
def MX(y, z, sum, k, p, e):
return (z >> 5 ^ y >> 2) + (y << 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z)
def btea(v, n, k):
u32 = lambda x: x & 4294967295
y = v[0]
sum = 0
DELTA = 1163219540
if n > 1:
z = v[n - 1]
q = 6 + 52 // n
while q > 0:
q -= 1
sum = u32(sum + DELTA)
e = u32(sum >> 2) & 3
p = 0
while p < n - 1:
y = v[p + 1]
z = v[p] = u32(v[p] + MX(y, z, sum, k, p, e))
p += 1
y = v[0]
z = v[n - 1] = u32(v[n - 1] + MX(y, z, sum, k, p, e))
return True
else:
return False
if __name__ == '__main__':
print('WOW')
魔改
最开始的时候根据上面的信息解
flag,但是无论怎么解都解不出来,卡了很久,后面才注意到 cypto 库是魔改的
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# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'crypto.py'
# Bytecode version: 3.9.0beta5 (3425)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
from types import CodeType
import dis
import sys
from myalgo import *
import re
import struct
import binascii
class RC4:
def __init__(self, key: bytes):
"""\n 初始化 RC4 类\n :param key: 密钥,字节类型\n """
self.key = key
self.s = list(range(256))
self._ksa()
def _ksa(self):
"""\n 密钥调度算法 (Key Scheduling Algorithm, KSA)\n """
j = 0
key_length = len(self.key)
for i in range(256):
j = (j + self.s[i] + self.key[i % key_length]) % 256
self.s[i], self.s[j] = (self.s[j], self.s[i])
def _prga(self):
"""\n 伪随机数生成算法 (Pseudo-Random Generation Algorithm, PRGA)\n :yield: 生成的伪随机字节\n """
i = j = 0
while True:
i = (i + 1) % 256
j = (j + self.s[i]) % 256
self.s[i], self.s[j] = (self.s[j], self.s[i])
yield self.s[(self.s[i] + self.s[j]) % 256]
def encrypt(self, plaintext: bytes) -> bytes:
"""\n 加密明文\n :param plaintext: 明文,字节类型\n :return: 密文,字节类型\n """
keystream = self._prga()
return bytes([p ^ next(keystream) for p in plaintext])
def decrypt(self, ciphertext: bytes) -> bytes:
"""\n 解密密文\n :param ciphertext: 密文,字节类型\n :return: 明文,字节类型\n """
return self.encrypt(ciphertext)
class ArrangeSimpleDES:
def __init__(self):
self.ip = [58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7]
self.ip1 = [40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25]
self.E = [32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1]
self.P = [16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25]
self.K = '0111010001101000011010010111001101101001011100110110100101110110'
self.k1 = [57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4]
self.k2 = [14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32]
self.k0 = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
self.S = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 4, 1, 14, 8, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 14, 12, 5, 0, 14, 10, 6, 6, 4, 2, 1, 10, 6, 3, 11, 5, 1, 10, 14, 5, 6, 4, 2, 1, 13, 1, 10, 6, 3, 11, 6, 4, 2, 1, 13, 3, 8, 10, 6, 1, 11,
def __substitution(self, table: str, self_table: list) -> str:
"""\n :param table: 需要进行置换的列表,是一个01字符串\n :param self_table: 置换表,在__init__中初始化了\n :return: 返回置换后的01字符串\n """
sub_result = ''
for i in self_table:
sub_result += table[i - 1]
return sub_result
def str2bin(self, string: str) -> str:
"""\n 将明文转为二进制字符串:\n :param string: 任意字符串\n :return:二进制字符串\n """
plaintext_list = list(bytes(string, 'utf8'))
result = []
for num in plaintext_list:
result.append(bin(num)[2:].zfill(8))
return ''.join(result)
def bin2str(self, binary: str) -> str:
"""\n 二进制字符串转成字符串\n :param binary:\n :return:\n """
list_bin = [binary[i:i + 8] for i in range(0, len(binary), 8)]
list_int = []
for b in list_bin:
list_int.append(int(b, 2))
result = bytes(list_int).decode()
return result
def __bin2int(self, binary: str) -> list:
"""\n 由于加密之后的二进制无法直接转成字符,有不可见字符在,utf8可能无法解码,所以需要将二进制字符串每8位转成int型号列表,用于转成bytes再转hex\n :param binary: 二进制字符串\n :return: int型列表\n """
list_bin = [binary[i:i + 8] for i in range(0, len(binary), 8)]
list_int = []
for b in list_bin:
list_int.append(int(b, 2))
return list_int
def __int2bin(self, list_int: list) -> str:
result = []
for num in list_int:
result.append(bin(num)[2:].zfill(8))
return ''.join(result)
def __get_block_list(self, binary: str) -> list:
"""\n 对明文二进制串进行切分,每64位为一块,DES加密以64位为一组进行加密的\n :type binary: 二进制串\n """
len_binary = len(binary)
if len_binary % 64!= 0:
binary_block = binary + '0' * (64 - len_binary % 64)
return [binary_block[i:i + 64] for i in range(0, len(binary_block), 64)]
else:
return [binary[j:j + 64] for j in range(0, len(binary), 64)]
def modify_secretkey(self):
"""\n 修改默认密钥函数\n :return: None\n """
print('当前二进制形式密钥为:{}'.format(self.K))
print('当前字符串形式密钥为:{}'.format(self.bin2str(self.K)))
newkey = input('输入新的密钥(长度为8):')
if len(newkey)!= 8:
print('密钥长度不符合,请重新输入:')
self.modify_secretkey()
else:
bin_key = self.str2bin(newkey)
self.K = bin_key
print('当前二进制形式密钥为:{}'.format(self.K))
def __f_funtion(self, right: str, key: str):
"""\n :param right: 明文二进制的字符串加密过程的右半段\n :param key: 当前轮数的密钥\n :return: 进行E扩展,与key异或操作,S盒操作后返回32位01字符串\n """
e_result = self.__substitution(right, self.E)
xor_result = self.__xor_function(e_result, key)
s_result = self.__s_box(xor_result)
p_result = self.__substitution(s_result, self.P)
return p_result
def __get_key_list(self):
"""\n :return: 返回加密过程中16轮的子密钥\n """
key = self.__substitution(self.K, self.k1)
left_key = key[0:28]
right_key = key[28:56]
keys = []
for i in range(1, 17):
move = self.k0[i - 1]
move_left = left_key[move:28] + left_key[0:move]
move_right = right_key[move:28] + right_key[0:move]
left_key = move_left
right_key = move_right
move_key = left_key + right_key
ki = self.__substitution(move_key, self.k2)
keys.append(ki)
return keys
def __xor_function(self, xor1: str, xor2: str):
"""\n :param xor1: 01字符串\n :param xor2: 01字符串\n :return: 异或操作返回的结果\n """
size = len(xor1)
result = ''
for i in range(0, size):
result += '0' if xor1[i] == xor2[i] else '1'
return result
def __s_box(self, xor_result: str):
"""\n :param xor_result: 48位01字符串\n :return: 返回32位01字符串\n """
result = ''
for i in range(0, 8):
block = xor_result[i * 6:(i + 1) * 6]
line = int(block[0] + block[5], 2)
colmn = int(block[1:5], 2)
res = bin(self.S[i][line * 16 + colmn])[2:]
if len(res) < 4:
res = '0' * (4 - len(res)) + res
result += res
return result
def __iteration(self, bin_plaintext: str, key_list: list):
"""\n :param bin_plaintext: 01字符串,64位\n :param key_list: 密钥列表,共16个\n :return: 进行F函数以及和left异或操作之后的字符串\n """
left = bin_plaintext[0:32]
right = bin_plaintext[32:64]
for i in range(0, 16):
next_lift = right
f_result = self.__f_funtion(right, key_list[i])
next_right = self.__xor_function(left, f_result)
left = next_lift
right = next_right
bin_plaintext_result = left + right
return bin_plaintext_result[32:] + bin_plaintext_result[:32]
def encode(self, plaintext):
"""\n :param plaintext: 明文字符串\n :return: 密文字符串\n """
bin_plaintext = self.str2bin(plaintext)
bin_plaintext_block = self.__get_block_list(bin_plaintext)
ciphertext_bin_list = []
key_list = self.__get_key_list()
for block in bin_plaintext_block:
sub_ip = self.__substitution(block, self.ip)
ite_result = self.__iteration(sub_ip, key_list)
sub_ip1 = self.__substitution(ite_result, self.ip1)
ciphertext_bin_list.append(sub_ip1)
ciphertext_bin = ''.join(ciphertext_bin_list)
result = self.__bin2int(ciphertext_bin)
return bytes(result).hex().upper()
def decode(self, ciphertext):
"""\n :param ciphertext: 密文字符串\n :return: 明文字符串\n """
b_ciphertext = binascii.a2b_hex(ciphertext)
bin_ciphertext = self.__int2bin(list(b_ciphertext))
bin_plaintext_list = []
key_list = self.__get_key_list()
key_list = key_list[::(-1)]
bin_ciphertext_block = [bin_ciphertext[i:i + 64] for i in range(0, len(bin_ciphertext), 64)]
for block in bin_ciphertext_block:
sub_ip = self.__substitution(block, self.ip)
ite = self.__iteration(sub_ip, key_list)
sub_ip1 = self.__substitution(ite, self.ip1)
bin_plaintext_list.append(sub_ip1)
bin_plaintext = ''.join(bin_plaintext_list).replace('00000000', '')
return self.bin2str(bin_plaintext)
_a85chars = None
_a85chars2 = None
_A85START = b'<~'
_A85END = b'~>'
bytes_types = (bytes, bytearray)
def _bytes_from_decode_data(s):
if isinstance(s, str):
try:
return s.encode('ascii')
except UnicodeEncodeError:
raise ValueError('string argument should contain only ASCII characters')
else:
if isinstance(s, bytes_types):
return s
else:
try:
return memoryview(s).tobytes()
except TypeError:
raise TypeError('argument should be a bytes-like object or ASCII string, not %r' % s.__class__.__name__) from None
def b64decode(s, altchars=None, validate=False):
"""Decode the Base64 encoded bytes-like object or ASCII string s.\n\n Optional altchars must be a bytes-like object or ASCII string of length 2\n which specifies the alternative alphabet used instead of the \'+\' and \'/\'\n characters.\n\n The result is returned as a bytes object. A binascii.Error is raised if\n s is incorrectly padded.\n\n If validate is False (the default), characters that are neither in the\n normal base-64 alphabet nor the alternative alphabet are discarded prior\n to the padding check. If validate is True, these non-alphabet characters\n in the input result in a binascii.Error.\n """
s = _bytes_from_decode_data(s)
if altchars is not None:
altchars = _bytes_from_decode_data(altchars)
assert len(altchars) == 2, repr(altchars)
s = s.translate(bytes.maketrans(altchars, b'+/'))
if validate and (not re.fullmatch(b'[A-Za-z0-9+/]*={0,2}', s)):
raise binascii.Error('Non-base64 digit found')
else:
return binascii.a2b_base64(s)
def a85decode(b, *, foldspaces=False, adobe=False, ignorechars=b' \t\n\r\x0b'):
# irreducible cflow, using cdg fallback
"""\n """
b = _bytes_from_decode_data(b)
if adobe:
if not b.endswith(_A85END):
raise ValueError('Ascii85 encoded byte sequences must end with {!r}'.format(_A85END))
else:
if b.startswith(_A85START):
b = b[2:(-2)]
else:
b = b[:(-2)]
packI = struct.Struct('!I').pack
decoded = []
decoded_append = decoded.append
curr = []
curr_append = curr.append
curr_clear = curr.clear
for x in b + b'uuuu':
if 33 <= x <= 117:
curr_append(x)
if len(curr) == 5:
acc = 0
for x in curr:
acc = 85 * acc + (x - 33)
try:
decoded_append(packI(acc))
except struct.error:
raise ValueError('Ascii85 overflow') from None
curr_clear()
if x == 122:
if curr:
raise ValueError('z inside Ascii85 5-tuple')
else:
decoded_append(b'\x00\x00\x00\x00')
else:
if foldspaces and x == 121:
if curr:
raise ValueError('y inside Ascii85 5-tuple')
else:
decoded_append(b' ')
else:
if x in ignorechars:
continue
else:
raise ValueError('Non-Ascii85 digit found: %c' % x)
payload = MX.__code__.co_code
magic_code1 = b'?'
magic_code2 = b'>'
payload = payload[:4] + magic_code2 + payload[5:10] + magic_code1 + payload[11:18] + magic_code2 + payload[19:24] + magic_code1 + payload[25:]
payload = payload[:3] + b'\x03' + payload[4:9] + b'\x01' + payload[10:17] + b'\x04' + payload[18:23] + b'\x02' + payload[24:]
fn_code = MX.__code__
MX.__code__ = CodeType(int(fn_code.co_argcount), int(fn_code.co_posonlyargcount), int(fn_code.co_kwonlyargcount), int(fn_code.co_nlocals), int(fn_code.co_stacksize), payload, fn_code.co_consts, fn_code.co_names, fn_code.co_varnames, fn_code.co_filename, fn_code.co_name, int(fn_code.co_firstlineno), fn_code.co_lnotab, fn_code.co_freevars, fn_code.co_cellvars)
result = b''.join(decoded)
padding = 4 - len(curr)
if padding:
result = result[:-padding]
return result
def chacha20_decrypt(key, counter, nonce, ciphertext):
return chacha20_encrypt(key, counter, nonce, ciphertext)
def chacha20_encrypt(key, counter, nonce, plaintext):
byte_length = len(plaintext)
full_blocks = byte_length // 64
remainder_bytes = byte_length % 64
encrypted_message = b''
for i in range(full_blocks):
key_stream = serialize(chacha20_block(key, counter + i, nonce))
plaintext_block = plaintext[i * 64:i * 64 + 64]
encrypted_block = [plaintext_block[j] ^ key_stream[j] for j in range(64)]
encrypted_message += bytes(encrypted_block)
if remainder_bytes!= 0:
key_stream = serialize(chacha20_block(key, counter + full_blocks, nonce))
plaintext_block = plaintext[full_blocks * 64:byte_length]
encrypted_block = [plaintext_block[j] ^ key_stream[j] for j in range(remainder_bytes)]
encrypted_message += bytes(encrypted_block)
return encrypted_message
def chacha20_block(key, counter, nonce):
BLOCK_CONSTANTS = [1634760805, 857760878, 2036477234, 1797285236]
init_state = BLOCK_CONSTANTS + key + [counter] + nonce
current_state = init_state[:]
for i in range(10):
inner_block(current_state)
for i in range(16):
current_state[i] = add_32(current_state[i], init_state[i])
return current_state
def inner_block(state):
quarterround(state, 0, 4, 8, 12)
quarterround(state, 1, 5, 9, 13)
quarterround(state, 2, 6, 10, 14)
quarterround(state, 3, 7, 11, 15)
quarterround(state, 0, 5, 10, 15)
quarterround(state, 1, 6, 11, 12)
quarterround(state, 2, 7, 8, 13)
quarterround(state, 3, 4, 9, 14)
def xor_32(x, y):
return (x ^ y) & 4294967295
def add_32(x, y):
return x + y & 4294967295
def rot_l32(x, n):
return (x << n | x >> 32 - n) & 4294967295
def quarterround(state, i1, i2, i3, i4):
a = state[i1]
b = state[i2]
c = state[i3]
d = state[i4]
a = add_32(a, b)
d = xor_32(d, a)
d = rot_l32(d, 16)
c = add_32(c, d)
b = xor_32(b, c)
b = rot_l32(b, 12)
a = add_32(a, b)
d = xor_32(d, a)
d = rot_l32(d, 8)
c = add_32(c, d)
b = xor_32(b, c)
b = rot_l32(b, 7)
state[i1] = a
state[i2] = b
state[i3] = c
state[i4] = d
def serialize(block):
return b''.join([word.to_bytes(4, 'little') for word in block])
def encrypt(v, k):
v0 = v[0]
v1 = v[1]
key0, key1, key2, key3 = (k[0], k[1], k[2], k[3])
sum = 0
delta = 2654435769
for _ in range(32):
sum = sum + delta & 4294967295
v0 = v0 + ((v1 << 3) + key0 ^ v1 + sum ^ (v1 >> 4) + key1 ^ 596) & 4294967295
v1 = v1 + ((v0 << 3) + key2 ^ v0 + sum ^ (v0 >> 4) + key3 ^ 2310) & 4294967295
return (v0, v1)
def decrypt(v, k):
v0 = v[0]
v1 = v[1]
key0, key1, key2, key3 = (k[0], k[1], k[2], k[3])
sum = 3337565984
delta = 2654435769
for _ in range(32):
v1 = v1 - ((v0 << 3) + key2 ^ v0 + sum ^ (v0 >> 4) + key3 ^ 2310) & 4294967295
v0 = v0 - ((v1 << 3) + key0 ^ v1 + sum ^ (v1 >> 4) + key1 ^ 596) & 4294967295
sum = sum - delta & 4294967295
return (v0, v1)
def encrypt_all(v, k):
encrypted = []
for i in range(0, len(v), 2):
encrypted.extend(encrypt(v[i:i + 2], k))
return encrypted
def decrypt_all(v, k):
decrypted = []
for i in range(0, len(v), 2):
decrypted.extend(decrypt(v[i:i + 2], k))
return decrypted
真实的逻辑是,在 main.py 调用 a85decode
时,该函数内部通过 python 反射修改了 myalgo.MX 函数的字节码
原本的 z >> 5 变为 z << 3 原本的
y >> 2 变为 y >> 5 原本的
y << 3 变为 y << 4 原本的
z << 4 变为 z >> 2 所以篡改后的
MX_new = ((z << 3 ^ y >> 5) + (y << 4 ^ z >> 2)) ^ ((sum ^ y) + (k[(p & 3) ^ e] ^ z))
Exp
1 | |
得到 flag 为 LilacCTF{e@sy_Pyth0n_SMC!}
JustROM
反编译
题目附件只给了个 rom.bin,开始啥没头绪,后面拷打 ai
确定架构,push ai 试了好久好久…最后得知是 SPARC,扔进 Ghidra
反编译 FUN_00004238 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
190undefined8 FUN_00004238(int param_1,int param_2)
{
uint uVar1;
uint uVar2;
uint local_44 [4];
uint local_34;
uint local_30;
uint local_2c;
uint local_28;
uint local_24;
uint local_20;
uint local_1c;
uint local_18;
uint local_14;
uint local_10;
uint local_c;
uint local_8;
int local_4;
for (local_4 = 0; local_4 < 0x10; local_4 = local_4 + 1) {
local_44[local_4] = *(uint *)(param_2 + local_4 * 4);
}
for (local_4 = 0; local_4 < 8; local_4 = local_4 + 2) {
local_44[0] = local_44[0] + local_34;
uVar1 = local_14 ^ local_44[0];
local_14 = uVar1;
FUN_00004060(uVar1,0x10);
local_24 = local_24 + uVar1;
uVar2 = local_34 ^ local_24;
local_34 = uVar2;
local_14 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[0] = local_44[0] + uVar2;
uVar1 = local_14 ^ local_44[0];
local_34 = uVar2;
local_14 = uVar1;
FUN_00004060(uVar1,8);
local_24 = local_24 + uVar1;
uVar2 = local_34 ^ local_24;
local_34 = uVar2;
local_14 = uVar1;
FUN_00004060(uVar2,7);
local_44[1] = local_44[1] + local_30;
uVar1 = local_10 ^ local_44[1];
local_34 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar1,0x10);
local_20 = local_20 + uVar1;
uVar2 = local_30 ^ local_20;
local_30 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[1] = local_44[1] + uVar2;
uVar1 = local_10 ^ local_44[1];
local_30 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar1,8);
local_20 = local_20 + uVar1;
uVar2 = local_30 ^ local_20;
local_30 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar2,7);
local_44[2] = local_44[2] + local_2c;
uVar1 = local_c ^ local_44[2];
local_30 = uVar2;
local_c = uVar1;
FUN_00004060(uVar1,0x10);
local_1c = local_1c + uVar1;
uVar2 = local_2c ^ local_1c;
local_2c = uVar2;
local_c = uVar1;
FUN_00004060(uVar2,0xc);
local_44[2] = local_44[2] + uVar2;
uVar1 = local_c ^ local_44[2];
local_2c = uVar2;
local_c = uVar1;
FUN_00004060(uVar1,8);
local_1c = local_1c + uVar1;
uVar2 = local_2c ^ local_1c;
local_2c = uVar2;
local_c = uVar1;
FUN_00004060(uVar2,7);
local_44[3] = local_44[3] + local_28;
uVar1 = local_8 ^ local_44[3];
local_2c = uVar2;
local_8 = uVar1;
FUN_00004060(uVar1,0x10);
local_18 = local_18 + uVar1;
uVar2 = local_28 ^ local_18;
local_28 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[3] = local_44[3] + uVar2;
uVar1 = local_8 ^ local_44[3];
local_28 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar1,8);
local_18 = local_18 + uVar1;
uVar2 = local_28 ^ local_18;
local_28 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar2,7);
local_44[0] = local_44[0] + local_30;
uVar1 = local_8 ^ local_44[0];
local_28 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar1,0x10);
local_1c = local_1c + uVar1;
uVar2 = local_30 ^ local_1c;
local_30 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[0] = local_44[0] + uVar2;
uVar1 = local_8 ^ local_44[0];
local_30 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar1,8);
local_1c = local_1c + uVar1;
uVar2 = local_30 ^ local_1c;
local_30 = uVar2;
local_8 = uVar1;
FUN_00004060(uVar2,7);
local_44[1] = local_44[1] + local_2c;
uVar1 = local_14 ^ local_44[1];
local_30 = uVar2;
local_14 = uVar1;
FUN_00004060(uVar1,0x10);
local_18 = local_18 + uVar1;
uVar2 = local_2c ^ local_18;
local_2c = uVar2;
local_14 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[1] = local_44[1] + uVar2;
uVar1 = local_14 ^ local_44[1];
local_2c = uVar2;
local_14 = uVar1;
FUN_00004060(uVar1,8);
local_18 = local_18 + uVar1;
uVar2 = local_2c ^ local_18;
local_2c = uVar2;
local_14 = uVar1;
FUN_00004060(uVar2,7);
local_44[2] = local_44[2] + local_28;
uVar1 = local_10 ^ local_44[2];
local_2c = uVar2;
local_10 = uVar1;
FUN_00004060(uVar1,0x10);
local_24 = local_24 + uVar1;
uVar2 = local_28 ^ local_24;
local_28 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar2,0xc);
local_44[2] = local_44[2] + uVar2;
uVar1 = local_10 ^ local_44[2];
local_28 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar1,8);
local_24 = local_24 + uVar1;
uVar2 = local_28 ^ local_24;
local_28 = uVar2;
local_10 = uVar1;
FUN_00004060(uVar2,7);
local_44[3] = local_44[3] + local_34;
uVar1 = local_c ^ local_44[3];
local_28 = uVar2;
local_c = uVar1;
FUN_00004060(uVar1,0x10);
local_20 = local_20 + uVar1;
uVar2 = local_34 ^ local_20;
local_34 = uVar2;
local_c = uVar1;
FUN_00004060(uVar2,0xc);
local_44[3] = local_44[3] + uVar2;
uVar1 = local_c ^ local_44[3];
local_34 = uVar2;
local_c = uVar1;
FUN_00004060(uVar1,8);
local_20 = local_20 + uVar1;
uVar2 = local_34 ^ local_20;
local_34 = uVar2;
local_c = uVar1;
FUN_00004060(uVar2,7);
local_34 = uVar2;
}
for (local_4 = 0; local_4 < 0x10; local_4 = local_4 + 1) {
*(uint *)(param_1 + local_4 * 4) = local_44[local_4] + *(int *)(param_2 + local_4 * 4);
}
return CONCAT44(param_2,param_1);
}
FUN_00004060 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
38undefined8 FUN_00004060(uint param_1,undefined4 param_2)
{
return CONCAT44(param_2,param_1 >> (-(byte)param_2 & 0x1f) | param_1 << ((byte)param_2 & 0x1f)) ;
}
FUN_0000409c
undefined8 FUN_0000409c(undefined4 *param_1,undefined4 param_2)
{
undefined4 uVar1;
uVar1 = 0x40008018;
FUN_00004028();
*param_1 = uVar1;
uVar1 = 0x4000801c;
FUN_00004028();
param_1[1] = uVar1;
uVar1 = 0x40008020;
FUN_00004028();
param_1[2] = uVar1;
uVar1 = 0x40008024;
FUN_00004028();
param_1[3] = uVar1;
param_1[4] = _DAT_40010000;
param_1[5] = _DAT_40010004;
param_1[6] = _DAT_40010008;
param_1[7] = _DAT_4001000c;
param_1[8] = _DAT_40010010;
param_1[9] = _DAT_40010014;
param_1[10] = _DAT_40010018;
param_1[0xb] = _DAT_4001001c;
param_1[0xc] = 1;
param_1[0xd] = 0x41414141;
param_1[0xe] = 0x42424242;
param_1[0xf] = 0x43434343;
return CONCAT44(param_2,param_1);
}
FUN_00004060
是一个循环左移操作,FUN_00004238 实现了
ChaCha20 的轮函数,FUN_0000409c 初始化了
ChaCha 的初始状态 这里从 DAT_40010000 开始到
DAT_4001001c 有部分数据在 Ghidra 中没找到
在 010 中搜索内存找到了 1
2
311 22 33 44 55 66 77 88 99 AA BB CC DD EE FF 00
DE AD BE EF CA FE BA BE 0B AD F0 0D 13 37 13 37
41 41 41 41 42 42 42 42 43 43 43 43 00 00 00 00
关键函数
FUN_00004ab0 是关键函数 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
91undefined8 FUN_00004ab0(undefined4 param_1,undefined4 param_2)
{
byte local_c8 [128];
undefined auStack_48 [68];
int local_4;
local_c8[0x20] = 0x54;
local_c8[0x21] = 0x68;
local_c8[0x22] = 0x65;
local_c8[0x23] = 0x72;
local_c8[0x24] = 0x65;
local_c8[0x25] = 0x5f;
local_c8[0x26] = 0x69;
local_c8[0x27] = 0x73;
local_c8[0x28] = 0x5f;
local_c8[0x29] = 0x6e;
local_c8[0x2a] = 0x6f;
local_c8[0x2b] = 0x74;
local_c8[0x2c] = 0x68;
local_c8[0x2d] = 0x69;
local_c8[0x2e] = 0x6e;
local_c8[0x2f] = 0x67;
local_c8[0x30] = 0x5f;
local_c8[0x31] = 0x79;
local_c8[0x32] = 0x6f;
local_c8[0x33] = 0x75;
local_c8[0x34] = 0x5f;
local_c8[0x35] = 0x77;
local_c8[0x36] = 0x61;
local_c8[0x37] = 0x6e;
local_c8[0x38] = 0x6e;
local_c8[0x39] = 0x61;
local_c8[0x3a] = 0x5f;
local_c8[0x3b] = 0x67;
local_c8[0x3c] = 0x65;
local_c8[0x3d] = 0x74;
local_c8[0x3e] = 0x2e;
local_c8[0x3f] = 0x2e;
local_c8[0] = 0x37;
local_c8[1] = 0x32;
local_c8[2] = 0x9b;
local_c8[3] = 0xf6;
local_c8[4] = 0x36;
local_c8[5] = 0xa9;
local_c8[6] = 0x18;
local_c8[7] = 0xfc;
local_c8[8] = 0xf2;
local_c8[9] = 0xe7;
local_c8[10] = 0x49;
local_c8[0xb] = 0x73;
local_c8[0xc] = 0x61;
local_c8[0xd] = 0x49;
local_c8[0xe] = 0xf8;
local_c8[0xf] = 0xd4;
local_c8[0x10] = 0x4c;
local_c8[0x11] = 0xf2;
local_c8[0x12] = 0x6a;
local_c8[0x13] = 0xc9;
local_c8[0x14] = 0x3c;
local_c8[0x15] = 0x4c;
local_c8[0x16] = 0x62;
local_c8[0x17] = 0x83;
local_c8[0x18] = 0x78;
local_c8[0x19] = 0x12;
local_c8[0x1a] = 0x5c;
local_c8[0x1b] = 5;
local_c8[0x1c] = 0x5f;
local_c8[0x1d] = 0x30;
local_c8[0x1e] = 0x95;
local_c8[0x1f] = 0x9d;
FUN_0000409c(auStack_48);
for (local_4 = 0; local_4 < 0x20; local_4 = local_4 + 1) {
local_c8[local_4 + 0x20] = local_c8[local_4 + 0x20] ^ local_c8[local_4];
}
FUN_00004238(local_c8 + 0x40,auStack_48);
for (local_4 = 0; local_4 < 0x20; local_4 = local_4 + 1) {
*(byte *)(local_4 + 0x42000000) = *(byte *)(local_4 + 0x42000000) ^ local_c8[local_4 + 0x40];
}
for (local_4 = 0; local_4 < 0x20; local_4 = local_4 + 1) {
if ((int)*(char *)(local_4 + 0x42000000) != (uint)local_c8[local_4 + 0x20])
goto code_r0x00004d14;
}
DAT_42000100 = 0x57;
DAT_42000101 = 0x69;
DAT_42000102 = 0x6e;
for (local_4 = 0; local_4 < 0xffff; local_4 = local_4 + 1) {
}
code_r0x00004d14:
return CONCAT44(param_2,param_1);
}
S (0x20-0x3F):
"There_is_nothing_you_wanna_get.." K
(0x00-0x1F):
37 32 9b f6 36 a9 18 fc f2 e7 49 73 61 49 f8 d4 4c f2 6a c9 3c 4c 62 83 78 12 5c 05 5f 30 95 9d
将 S 与 K
进行按位异或:Target[i] = S[i] ^ K[i],调用 FUN_00004238
生成 ChaCha 密钥流,最终对比推导出
Input[i] = Target[i] ^ Stream[i]
Exp
1 | |
Flag 为 LilacCTF{d0ntl@@kl1kechch4atall}
C++++
反编译
直接 ida 反编译,从 main 函数开始,动调找逻辑
sub_7FF72E7EFC30->sub_7FF72E6FC6C0
主要逻辑在 sub_7FF72E6FC6C0 ####
sub_7FF72E6FC6C0 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__int64 sub_7FF646B1C6C0()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
sub_7FF646BE4AB0(&off_7FF646D517A0);
sub_7FF646BE4FF0(10LL);
printf(off_7FF646D47600);
printf(off_7FF646D461E0);
printf(off_7FF646D47600);
printf_0(off_7FF646D46020);
v0 = (char **)sub_7FF646BE4AF0(); // input
if ( !v0 )
v0 = &off_7FF646D46008;
result = sub_7FF646B27650((__int64)v0, 3); // 格式化清理
v2 = result;
if ( result && *(_DWORD *)(result + 8) )
{
v3 = sub_7FF646AA3B40(qword_7FF646CE9950, 0x10uLL);// new
*(_OWORD *)(v3 + 16) = xmmword_7FF646C99460;
v4 = (unsigned __int16 **)sub_7FF646B1C850((unsigned __int16 **)v3, 0x41);// xor 0x41 -> "WONDERFUL&&PEACE"
v5 = sub_7FF646AA3B40(qword_7FF646CE9950, 0x10uLL);// memalloc
*(_OWORD *)(v5 + 16) = xmmword_7FF646C99450;
v6 = (unsigned __int16 **)sub_7FF646B1C850((unsigned __int16 **)v5, 0x31);// xor 0x31 -> "somedanctoforget"
v7 = sub_7FF646AA3A20(&byte_7FF646CA9038);
sub_7FF646B1C490(v7, v4, v6); // string_opt
v8 = (_QWORD **)sub_7FF646AA1A9E();
v9 = (*(__int64 (__fastcall **)(_QWORD *, __int64))(*v8[1] + 112LL))(v8[1], v2);
sub_7FF646B1C4E0(v7, v9);
v11 = sub_7FF646B2DD10(v10);
string_replace(v11, (__int64)&off_7FF646D46DE8, (__int64 **)&off_7FF646D46008);
if ( (unsigned int)sub_7FF646B240D0(v12, (__int64)&off_7FF646D47738) )
{
sub_7FF646BE4FF0(11LL);
printf(off_7FF646D460E0); // WELCOME
}
else
{
sub_7FF646BE4FF0(12LL);
printf(off_7FF646D46068); // DENIED
}
return sub_7FF646BE5060();
}
return result;
}
sub_7FF646B1C4E0
1 | |
sub_7FF646B1CA40
1 | |
分析
1 | |
在进入主循环前,输入的数据块与轮密钥的前 4 个 DWORD 进行异或
进入 16 轮主循环后: v7 和 v8 是通过 S
盒变换((sub_7FF646B1CC20))得到的值,然后使用
__ROL4__循环左移,注意 HIDWORD(v13) 左移 5
位,最后整个 v13 左移 27 位,每一轮都会从 a1
中提取两个不同的轮密钥:*(_DWORD *)(a1 + 4LL * (2 * j + 8) + 16)
和 *(_DWORD *)(a1 + 4LL * (2 * j + 9) + 16),
在每一轮结束时(除了最后一轮),v12 和 v13
交换
1 | |
最后再次与一组轮密钥进行异或
sub_7FF646B1CC20
1 | |
关键函数分析
结合 ai 分析出这是 Twofish 加密算法中核心函数
g 函数(或称 h 函数)的 T-Table
优化版实现,它负责处理 32 位数据,将其拆分为 4 个字节,通过 S
盒变换、密钥异或,最后通过 MDS 矩阵(这里被优化成了 4 个 T-Table
查询)
详细的逻辑分析: #### 输入参数
a1: 指向一个整数数组(可能是子密钥数组的一部分,用于非
a3 模式) a2: 输入的 32 位数据(即 X) a3:
指向加密上下文结构体(Context) - *(a3 + 8):
可能代表密钥长度或 S 盒的层数(Twofish 的 S 盒根据密钥长度有 2, 3, 4
层变换) - *(a3 + 16): 对应 k_sbox[0] -
*(a3 + 20): 对应 k_sbox[1] - *(a3 + 24) /
*(a3 + 28): 对应更高级别的 S 盒密钥(如果密钥长度更长)
字节预处理(S 盒前置变换)
代码开头将 a2 拆分为四个字节 v5,
v6, v7, v8: 1
2
3
4v5 = (unsigned __int8)a2; // Byte 0
v6 = BYTE1(a2); // Byte 1
v7 = BYTE2(a2); // Byte 2
v8 = HIBYTE(a2); // Byte 3*(a3 + 8) 的值,对这些字节进行了多层变换,在
Twofish 中,如果密钥是 256 位,S 盒会有 4 层;192 位是 3 层;128 位是 2
层 如果 v9 >= 4,字节会先异或 *(a3 + 28)
然后查一次 S 盒 如果 v9 >= 3,字节会异或
*(a3 + 24) 再查一次 S 盒 这对应了 Twofish 规范中 h
函数根据密钥长度进行的迭代
核心 T-Table 查找
通过 sub_7FF646AA1557() 获取 S 盒表,通过
sub_7FF646AA1577() 获取 MDS 组合表(T-Table)
对于每一个字节,它执行了以下逻辑(以第一个字节 v5 为例): 第一次 S
盒变换: v40 = Sbox[v5]。 异或密钥: v40 ^ key1
(即 (a3 + 20) 的对应字节) 第二次 S 盒变换:
v46 = Sbox[v40 ^ key1] 异或密钥: v46 ^ key0
(即 (a3 + 16) 的对应字节) T-Table 最终查询:
v52 = T_Table_0[v46 ^ key0]
结果合并
代码最后将四个字节的 T-Table 查询结果进行异或合并: 1
2// 伪代码逻辑
return T0[idx0] ^ T1[idx1] ^ T2[idx2] ^ T3[idx3];
与 Twofish 算法的对应关系
| 反汇编代码位置 | Twofish 算法组件 | 说明 |
|---|---|---|
a3 + 16 |
S-word 0 | 密钥生成的 S 盒相关密钥 |
a3 + 20 |
S-word 1 | 密钥生成的 S 盒相关密钥 |
v32 (from ...1577) |
T-Tables | 预计算好的 MDS 矩阵与 S 盒组合表 |
v33 (from ...1557) |
S-Boxes | 基础 S 盒(RS 矩阵生成的固定 S 盒) |
sub_7FF646BAEC50() |
Panic/Error | 数组越界或完整性检查失败时的报错函数 |
S 盒提取
sub_7FF646B1CC20 中进行 S 盒变换,动调找到两个 256
字节的 S 盒和逆 S 盒


Exp
1 | |
flag 为 LilacCTF{I_ju3t_w@nnA_b3_hapPy}
NineApple
初步分析
附件的文件结构如下 1
2
3
4
5
6embedded.mobileprovision
Info.plist
Nine
PkgInfo
_CodeSignature
└─CodeResources

根据字符串交叉引用定位到相关函数
相关函数
sub_100006D08
1 | |
是处理输入的主要逻辑
获取用户在九宫格输入数字序列,然后遍历输入的数字,将其转换为字符串并连接起来
其中有一个
map_list,会根据拼接好的字符串去查找对应的映射值,找到后会将结果存储在
current_flag 中,最后和 target_all
进行比较
sub_100006258
1 | |
负责初始化,代码的前半部分通过调用
Published.init(initialValue:) 初始化了几个与 UI
绑定的属性: _nodePositions: 初始化为空数组
_swiftEmptyArrayStorage,可能存储九宫格中每个点的位置
_selectedNodes: 初始化为空数组,存储用户当前划过的点
_currentPosition: 初始化为一个元组或结构体,值为 0, 0,
1,记录当前触摸位置 _message: 初始化为字符串
"Keep Going!" _isAuthenticated: 初始化为
false
之后是核心校验数据初始化
current_key / current_idx / weight_idx: 均初始化为 0
current_flag: 初始化为空字符串 key_all:
初始化为空数组 weight : 使用 unk_100010320
处的静态数据初始化 target_all:使用
unk_100010390
处的静态数据初始化,这个数组存储了解锁所需的正确手势序列
map_list:调用 sub_100008678
构建,将手势字符串(Key)映射为单个字符(Value),数据来源是
unk_1000104C0
在代码末尾,还设置了两个 UI 相关的浮点常数:
nodeSize: 0x404E000000000000 -> 浮点数 60.0
gridSize: 0x4072C00000000000 -> 浮点数
300.0 这进一步证实了这是一个 300x300
像素的九宫格界面,每个圆点的大小为 60 像素
九宫格布局 1
2
31 2 3
4 5 6
7 8 9
sub_100004D74
1 | |
这个函数是 SwiftUI DragGesture 的回调函数:通过
StateObject 获取 LockViewModel 实例
获取坐标:从 DragGesture.Value
中提取当前的触摸点坐标(x,y),将坐标传递给核心处理函数
sub_1000069A4
sub_1000069A4
1 | |
这个函数负责判断用户的手势划过了哪个圆点,并更新状态,一旦确定划过了一个新圆点,程序会执行以下操作:
- 更新已选列表:将该点索引 v14 加入
_selectedNodes 数组 - 更新 current_keys:
$$
\text{CurrentKey} = \sum_{i=0}^{n} (\text{Weight}[i] \times
(\text{NodeIndex}_i + 1))
$$
关键数据
map_list


| 路径字符串(九宫格序列) | 对应字符 |
|---|---|
| 1478 | i |
| 582 | l |
| 147 | a |
| 2147859 | c |
| 6589 | { |
| 248 | 1 |
| 125879 | 0 |
| 2587413 | S |
| 321456987 | _ |
| 789 | / |
| 27 | \ |
| 18 | N |
| 7415963 | d |
| 825479 | w |
| 1475963 | n |
| 4758 | 3 |
| 23598 | f |
| 21745 | r |
| 475 | y |
| 14257 | o |
| 58746 | u |
| 47869 | } |
| 157 | 2 |
| 125478 | 4 |
| 14528 | 5 |
| 214587 | 6 |
| 458712 | 7 |
| 1238 | 9 |
target_all
对应 33 个 flag 字符的校验值

weight

Exp
1 | |
flag 为 Lilac{10S_aNd_l1lac_w1n3_f0r_you}