羊城杯 2023 re

闲着无事看看老题做做

Ez加密器

image-20250829235035488

必须输入6位验证码,DASCTF{}开头结尾,此外输入的验证码会经过base64编码,似乎大小写也会变

image-20250829235124743

那么其实就是用验证码作为key去加密,这里是DES加密

base64码表:abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+/

发现了这串,是des的特征

image-20250829235837440

image-20250829235909881

那么我们只用爆破验证码去解密即可

中间还对str进行异或了

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
print('DASCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}')

c = 'MDAwMDAw'
for i in range(0,len(c)):
print(hex(ord(c[i])),end=' ')
# 58,50,42,34,26,18,10,2,
print(hex(58))
print(hex(50))

print(chr(0x37 ^ 7))
print(chr(0x30 ^ 7))
import base64
from Crypto.Cipher import DES

enc = [0x37,0x30,0x35,0x34,0x36,0x37,0x32,0x43,0x32,0x44,0x36,0x35,0x35,0x36,0x30,0x43,0x44,0x43,0x44,0x34,0x31,0x37,0x36,0x41,0x32,0x42,0x44,0x45,0x32,0x33,0x43,0x46,0x3E,0x44,0x44,0x42,0x44,0x35,0x35,0x30,0x3E,0x41,0x36,0x31,0x3F,0x33,0x46,0x36,0x34,0x46,0x37,0x43,0x30,0x36,0x31,0x43,0x36,0x30,0x35,0x36,0x30,0x41,0x33,0x44,0x3E,0x42,0x46,0x3F,0x32,0x41,0x41,0x36,0x46,0x33,0x35,0x30,0x3E,0x32,0x30,0x34,0x36,0x44,0x46,0x34,0x44,0x32,0x32,0x43,0x34,0x46,0x33,0x43,0x30,0x45,0x42,0x46]

enc = [enc[i] ^ 7 for i in range(len(enc))]
enc_str = ''.join(chr(i) for i in enc)

def base64_encode(data):
oldtable='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
newtable='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+/'
tmp = base64.b64encode(data.encode()).decode()
result=''
for ch in tmp:
result +=newtable[oldtable.index(ch)]
return result

for i in range(0,1000000):
tmp_key = str(i).rjust(6,"0")
tmp_key = base64_encode(tmp_key)
des = DES.new(tmp_key.encode(),mode=DES.MODE_ECB)
c = bytes.fromhex("0723105D5C12217DCDC3601F5ECB54DA9CCEC2279F1684A13A0D716D17217F4C9EA85FF1A42795731CA3C55D3A4D7BEA")
flag = des.decrypt(c)
if(b"DASCTF" in flag):
print(flag)

DASCTF{f771b96b71514bb6bc20f3275fa9404e}

CSGO

有反调试,ScyllaHide开启反反调试即可

image-20250830205802631

发现不大对

image-20250830210011741

换表了

主要打那个表操作有一个+11,感觉是从11位开始取,越界再从0开始取试一下直接出了

image-20250830210808617

DASCTF{73913519-A0A6-5575-0F10-DDCBF50FA8CA}

Blast

发现一大串

image-20250830212437395

似乎是md5

image-20250830212458914

程序的逻辑似乎是输入的flag都在这个列表中,那肯定都是单字节的加密成md5,爆破应该是最快的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import hashlib

def md5(s):
return hashlib.md5(s).hexdigest()

md5_list = ['14d89c38cd0fb23a14be2798d449c182', 'a94837b18f8f43f29448b40a6e7386ba', 'af85d512594fc84a5c65ec9970956ea5', 'af85d512594fc84a5c65ec9970956ea5', '10e21da237a4a1491e769df6f4c3b419', 'a705e8280082f93f07e3486636f3827a', '297e7ca127d2eef674c119331fe30dff', 'b5d2099e49bdb07b8176dff5e23b3c14', '83be264eb452fcf0a1c322f2c7cbf987', 'a94837b18f8f43f29448b40a6e7386ba', '71b0438bf46aa26928c7f5a371d619e1', 'a705e8280082f93f07e3486636f3827a', 'ac49073a7165f41c57eb2c1806a7092e', 'a94837b18f8f43f29448b40a6e7386ba', 'af85d512594fc84a5c65ec9970956ea5', 'ed108f6919ebadc8e809f8b86ef40b05', '10e21da237a4a1491e769df6f4c3b419', '3cfd436919bc3107d68b912ee647f341', 'a705e8280082f93f07e3486636f3827a', '65c162f7c43612ba1bdf4d0f2912bbc0', '10e21da237a4a1491e769df6f4c3b419', 'a705e8280082f93f07e3486636f3827a', '3cfd436919bc3107d68b912ee647f341', '557460d317ae874c924e9be336a83cbe', 'a705e8280082f93f07e3486636f3827a', '9203d8a26e241e63e4b35b3527440998', '10e21da237a4a1491e769df6f4c3b419', 'f91b2663febba8a884487f7de5e1d249', 'a705e8280082f93f07e3486636f3827a', 'd7afde3e7059cd0a0fe09eec4b0008cd', '488c428cd4a8d916deee7c1613c8b2fd', '39abe4bca904bca5a11121955a2996bf', 'a705e8280082f93f07e3486636f3827a', '3cfd436919bc3107d68b912ee647f341', '39abe4bca904bca5a11121955a2996bf', '4e44f1ac85cd60e3caa56bfd4afb675e', '45cf8ddfae1d78741d8f1c622689e4af', '3cfd436919bc3107d68b912ee647f341', '39abe4bca904bca5a11121955a2996bf', '4e44f1ac85cd60e3caa56bfd4afb675e', '37327bb06c83cb29cefde1963ea588aa', 'a705e8280082f93f07e3486636f3827a', '23e65a679105b85c5dc7034fded4fb5f', '10e21da237a4a1491e769df6f4c3b419', '71b0438bf46aa26928c7f5a371d619e1', 'af85d512594fc84a5c65ec9970956ea5', '39abe4bca904bca5a11121955a2996bf']

flag = ""
for h in md5_list:
for i in range(32,128):
char = chr(i)
h1 = hashlib.md5(char.encode()).hexdigest()
h2 = hashlib.md5(h1.encode()).hexdigest()
if h2 == h:
flag += char
break

print(f"flag{{{flag}}}")

flag{Hello_Ctfer_Velcom_To_my_Mov_and_md5(md5)_world}

vm_wo

看着是个虚拟机,想调试发现是ios的…

0x20D01011903001ALL应该是opcode

1
2
3
4
[0x1A, 0x00, 0x03, 0x19, 0x01, 0x01, 0x0D, 0x02, 0x07, 0x18, 0x01, 0x02, 0x01, 0x00, 0x03]
[0x1A, 0x00, 0x03, 0x19, 0x01, 0x02, 0x0D, 0x02, 0x06, 0x18, 0x01, 0x02, 0x01, 0x00, 0x04],
[0x1A, 0x00, 0x03, 0x19, 0x01, 0x03, 0x0D, 0x02, 0x05, 0x18, 0x01, 0x02, 0x01, 0x00, 0x05],
[0x1A, 0x00, 0x03, 0x19, 0x01, 0x04, 0x0D, 0x02, 0x04, 0x18, 0x01, 0x02, 0x01, 0x00, 0x06]
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
vm_body[0] = 3
vm_body[1] = vm_body[0] >> 1
vm_body[2] = vm_body[0] << 7
vm_body[0] = vm_body[2] | vm_body[1]
vm_body[0] ^= 0xEF

vm_body[0] = a1
vm_body[1] = vm_body[0] >> 2
vm_body[2] = vm_body[0] << 6
vm_body[0] = vm_body[2] | vm_body[1]
vm_body[0] ^= 0xBE

vm_body[0] = a1
vm_body[1] = vm_body[0] >> 3
vm_body[2] = vm_body[0] << 5
vm_body[0] = vm_body[2] | vm_body[1]
vm_body[0] ^= 0xED

vm_body[0] = a1
vm_body[1] = vm_body[0] >> 4
vm_body[2] = vm_body[0] << 4
vm_body[0] = vm_body[2] | vm_body[1]
vm_body[0] ^= 0xBE

*a1++ = (vm_body[0] >> 5) | (vm_body[0] << 3);

整理下

1
2
3
4
5
vm_body[0] = ((vm_body[0] << 7) | (vm_body[0] >> 1)) ^ 0xEF
vm_body[0] = ((vm_body[0] >> 2) | (vm_body[0] << 6)) ^ 0xBE
vm_body[0] = ((vm_body[0] >> 3) | (vm_body[0] << 5)) ^ 0xED
vm_body[0] = ((vm_body[0] >> 4) | (vm_body[0] << 4)) ^ 0xBE
vm_body[0] = (vm_body[0] >> 5) | (vm_body[0] << 3)
1
2
3
4
5
6
7
8
9
10
enc = [0xDF,0xD5,0xF1,0xD1,0xFF,0xDB,0xA1,0xA5,0x89,0xBD,0xE9,0x95,0xB3,0x9D,0xE9,0xB3,0x85,0x99,0x87,0xBF,0xE9,0xB1,0x89,0xE9,0x91,0x89,0x89,0x8F,0xAD]

for i in range(len(enc)):
res = enc[i]
res = (((res >> 3) | (res << 5) & 0xff)) ^ 0xBE
res = (((res >> 4) | (res << 4)) & 0xff) ^ 0xED
res = (((res >> 5) | (res << 3)) & 0xff) ^ 0xBE
res = (((res >> 6) | (res << 2)) & 0xff) ^ 0xEF
res = (((res << 1) | (res >> 7)) & 0xff)
print(chr(res),end='')

DASCTF{you_are_right_so_cool}

Babyobfu

main中变成了这样

image-20250830233234738

要么是花指令要么是smc加密了

看着像后者,先分析下汇编

参考了详细的wp

切片式SMC的例题解析及其自动化反混淆 | CN-SEC 中文网

初始化阶段

image-20250831211635433

该阶段主要由两个memset函数(memset(str, c, n))构成,主要用0填充了首地址分别为[rbp-47h][rbp-0C4h]的两个局部变量数组,大小分别为0x1F0x7C,从后续阶段可知两个数组可命名为statedata。即:

把一个较大的局部缓冲(124 字节,位于 [rbp-0xC4])清零,作为后续密钥准备/SMC 解密的工作区;同时把 [rbp-0xC8] 这个局部变量置 0 以复用为 memset 的填充值或后续标志。

准备key

image-20250831211750326

image-20250831211857631

该函数通过a1判断是否进行密钥更改、a2限制大小,将a4[a3[i]]a5异或。由函数调用前的传参可知,a1为上一阶段中初始化的state数组的首字节(根据栈偏移确认索引),a4为上一阶段中初始化的data数组(即密钥),a3为.data段中的一段已知数据,a2a5都是常数。即:

prepare_key 的作用是“按一张索引表对一块缓冲区做定点异或搅动”,把原本清零的栈上缓冲区改造成一个“派生密钥缓冲”。随后,调用者会从这块缓冲区的固定偏移处取出一个32位值,作为第二段 SMC 解密的密钥使用。

a1:模式/开关标志:控制是否进入异或搅动流程,或选择分支。实测场景下为开启搅动的常量值(例如非零)。

a2:计数/长度:指明需要处理的索引数量(循环次数),也就是会从索引表里取前 a2 个索引来改写缓冲区相应位置。

a3:索引表指针:指向一组字节/DWORD 索引的数组;本题中就是全局的 key_index_table(原 unk_406330)。函数用这些索引决定“改写缓冲区的哪些下标”。

a4:目标缓冲区指针:指向调用者栈上的那块工作区(main 里先 memset 清零的 [rbp-0xC4] 开始的 0x7C 字节)。prepare_key 会按索引逐个位置做异或写回。

a5:32 位种子/基键:一个固定常量(本题看到的是 0x7FDCC233)。函数把它拆成 4 个字节,配合索引位置做组合异或,作为“搅动掩码”。

取 buf 内某固定偏移处的 4 字节(在本题里是 [rbp-0xBC])作为 key2;

以 key2 调用第二次 SMC 解密函数,解出函数区(地址来自 [QWORD 0x406980],长度来自 [QWORD 0x406988])。

代码解密阶段

image-20250831220954577

作用:这是题目中的 SMC(自修改代码)解码器。它会把一段内存页临时改成可写/可执行,然后按固定算法逐字节异或,把“加密的代码段”还原成可执行的明文,再在同一地址原地覆盖。还原后,程序流会跳到这段被解出来的代码继续执行。

函数参数(SysV x64 调用约定)

  • 第1参 rdi:addr,待解密/修改的代码区首地址
  • 第2参 rsi:size,字节长度
  • 第3参 rdx:key,32 位解密键(通常作为 uint32 使用,循环取其 4 个字节)

在本题里它被 main 调用两次:第一次解开“中间逻辑”,第二次解开“真正的校验函数”。

要达到全自动的目的,IDAPython脚本需要关注这几个关键点:函数调用的参数获取、SMC的密钥设置与解密、SMC代码的nop与函数重建

整个反混淆的思路是在指定的函数开头与结尾地址中进行遍历,函数开头搜索到调用_memset时初始化两个数组,并记录他们的栈偏移;搜索到调用preparekey时,往上寻找各参数并调用自己复刻的preparekey进行密钥设置;搜索到调用decrypt_and_modify_code时,同样往上寻找各参数并调用自己复刻的decrypt_and_modify_code进行代码解密;搜索到调用smc_encrypt时,往上寻找各参数并记录参数设置的地址。所有的相关指令都被记录在一个nop数组中,遍历结束以后nop掉这些指令,防止干扰对程序的分析,并重建函数方便反编译。

当然还想到一种方案,就是在程序运行时,动态的获取寄存器里的值,这样就不用去模拟获取key这种操作了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def prepare_key(state, size, idx_arr, data_arr, key):
if state == 0:
for i in range(size):
idx = get_wide_dword(idx_arr + i*4)
data_arr[idx] ^= key
return

def smc_decrypt(code_addr, size, key):
s = key.to_bytes(4, 'little')
for i in range(size):
code = get_wide_byte(code_addr + i)
smc_key = ((i-50) & 0xff) ^ s[i%4]
patch_byte(code_addr+i, code ^ smc_key)
del_items(code_addr+i)
create_insn_with_check(code_addr)
print("* Patch at", hex(code_addr), "for", size, "bytes with key", hex(key))
return

patch_byte后的del_items是为了保证这些地址中的字节都是undefined对象,在后续create_insn时能正常创建指令。

create_insn_with_check是我自定义的函数,为的是能保证ea处的字节能被创建指令。

IDAPython的create_insn函数有点小坑,对于undefined的字节,只要这个字节开始反汇编出的指令覆盖到后面已经被定义(explored)的字节,那么就会create_insn失败。而在IDA界面中按快捷键C(MakeCode)对该字节创建指令,就会直接覆盖后面的explored字节。

所以就会出现按C能转指令但是create_insn转不了指令的情况。于是这个函数就是为了解决create_insn失败的问题:把后面的字节逐个无条件转成undefined,直到能成功创建ea处的指令。(其实为了鲁棒性还应该检查最后return的时候off==create_insn(ea)的,但没出bug就不管了)

1
2
3
4
5
6
def create_insn_with_check(ea):
off = 1
while create_insn(ea) == 0:
del_items(ea, 0, off)
off += 1
return

遍历部分就是一个纯粹的顺序遍历反汇编,主要是对汇编指令的特征进行匹配,匹配到对应的关键词进行对应操作。这需要分别对三个smc函数更名以后才会正常匹配,不然脚本啥都匹配不到。

smc_keysetsmc_decrypt,向前遍历并使用正则匹配指令的操作数,获取到参数后调用对应的函数;对初始化阶段的两个memset,由于一定会在函数的最开头(不然后面解密就没有局部变量可用了),那么匹配到以后就不会再匹配了,并且记录他们创建的局部变量数组的偏移,在smc_keysetsmc_decrypt两个函数调用时需要用到这个偏移计算参数;对smc_encrypt,同样需要向上寻找参数,但只是用于寻找这些添加参数的指令。这些涉及到的指令都会添加进nop的列表g_nop_addrs里,防止赋值干扰后续分析(需要保证后续使用到这些寄存器时会重新赋值)。

遍历时使用ea += get_item_size(ea)而不是next_head()是因为对于后续不是指令的地址(因为后面的是没反混淆的所以必然会出现这种情况),next_head()会直接跳过,从而影响程序的顺序扫描。

太难了……这个静态分析,逻辑上简单,但是代码实现上非常困难,实战很难打出来

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
import re

SET_PTN = [
r'byte ptr \[rbp-([0-9A-F]{1,3})h?]', # state
r'([0-9A-F]{1,3})h?',
r'offset (?:dword|unk)_([0-9A-F]{6})',
r'\[rbp-([0-9A-F]{1,3})h?]', # data
r'([0-9A-F]{1,9})h?'
]
DEC_PTN = [
r'ds:(?:qword|off)_([0-9A-F]{6})',
r'ds:qword_([0-9A-F]{6})',
r'\[rbp-([0-9A-F]{1,3})h?]' # data
]
memset_searchDict = {
"di": (r'\[rbp-([0-9A-F]{1,3})h?]', 0),
"si": (r'(.*)', 1),
"dx": (r'([0-9A-F]{1,3})h?', 2)
}
encrypt_searchDict = {
"di": (r'\[rbp-([0-9A-F]{1,3})h?]', 0),
"si": (r'\[rbp-([0-9A-F]{1,3})h?]', 1),
"dx": (r'\[rbp-([0-9A-F]{1,3})h?]', 2)
}
NOP_TYPE = ["MEMSET", "KEY", "DEC", "ENC"]
g_func_addrs = [(0x4022B0, 0x4035BF), (0x401980, 0x4022B0), (0x401580, 0x401980), (0x4011B0, 0x401580)]
g_nop_addrs = []

def smc_keyset(state, size, idx_arr, data_arr, key):
if state == 0:
for i in range(size):
idx = get_wide_dword(idx_arr + i*4)
data_arr[idx] ^= key
return

def smc_decrypt(code_addr, size, key):
s = key.to_bytes(4, 'little')
for i in range(size):
code = get_wide_byte(code_addr+i)
smc_key = ((i-50)&0xff) ^ s[i%4]
patch_byte(code_addr+i, code ^ smc_key)
del_items(code_addr+i)
create_insn_with_check(code_addr)
print("* Patch at", hex(code_addr), "for", size, "bytes with key", hex(key))
return

def create_insn_with_check(ea):
off = 1
while create_insn(ea) == 0:
del_items(ea, 0, off)
off += 1
return

def search_up_for_args(ea, searchDict, nop):
global g_nop_addrs
tl = []
sDict = searchDict.copy()
while bool(sDict):
ea = prev_head(ea)
opn0 = print_operand(ea, 0)[-2:]
opn1 = print_operand(ea, 1)
if opn0 in sDict.keys():
g_nop_addrs.append((ea, ea+get_item_size(ea), nop))
matchobj = re.match(sDict[opn0][0], opn1)
if matchobj is not None:
try:
tl.append((int(matchobj.groups()[0], 16), sDict[opn0][1]))
except ValueError:
tl.append(("NONE", sDict[opn0][1]))
else:
sDict.update({opn1[-2:]: sDict[opn0]})
del sDict[opn0]
tl.sort(key=lambda t:t[1])
return tl

def dobf_func(func_addr_t):
arr_addrs = [] # [state_addr, state_size], [data_addr, data_size]
ea = func_addr_t[0]
print("START at", hex(ea))
while ea < func_addr_t[1]:
try:
create_insn_with_check(ea)
except:
print("FUNC END at", hex(ea))
break
opcode = print_insn_mnem(ea)
operand = print_operand(ea, 0)
if operand == "smc_keyset":
assert len(arr_addrs) == 2
addr = ea
args = []
for i in range(5)[::-1]:
addr = prev_head(addr)
opn = print_operand(addr, 1)
matchobj = re.match(SET_PTN[i], opn)
if matchobj is None:
matchobj = re.match(r'([0-9A-F]{1,9})h?', opn)
assert matchobj is not None
args.append(int(matchobj.groups()[0], 16))
g_nop_addrs.append((addr, ea+get_item_size(ea), NOP_TYPE.index("KEY")))
args = args[::-1]
args[0] = state_arr[arr_addrs[0][0] - args[0]]
args[3] = data_arr
print("* smc_keyset addr:", hex(ea))
smc_keyset(*args)
elif operand == "smc_decrypt":
assert len(arr_addrs) == 2
addr = ea
args = []
for i in range(10):
addr = prev_head(addr)
op = print_insn_mnem(addr)
opn0 = print_operand(addr, 0)
opn1 = print_operand(addr, 1)
if op == "mov" and opn1 == "1":
matchobj = re.match(SET_PTN[0], opn0)
state_addr = int(matchobj.groups()[0], 16)
assert matchobj is not None
state_arr[(arr_addrs[0][0]-state_addr)] = 1
break
g_nop_addrs.append((addr, ea+get_item_size(ea), NOP_TYPE.index("DEC")))
for i in range(3):
addr = next_head(addr)
opn = print_operand(addr, 1)
matchobj = re.match(DEC_PTN[i], opn)
assert matchobj is not None
args.append(int(matchobj.groups()[0], 16))
assert (arr_addrs[1][0] - args[2]) % 4 == 0
args[0] = get_qword(args[0])
args[1] = get_qword(args[1])
args[2] = data_arr[(arr_addrs[1][0]-args[2])//4]
print("* smc_decrypt addr:", hex(ea))
smc_decrypt(*args)
elif operand == "smc_encrypt":
search_up_for_args(ea, encrypt_searchDict, NOP_TYPE.index("ENC"))
g_nop_addrs.append((ea, ea+get_item_size(ea), NOP_TYPE.index("ENC")))
elif len(arr_addrs) != 2 and operand == "_memset":
addr = ea
g_nop_addrs.append((ea, ea+get_item_size(ea), NOP_TYPE.index("MEMSET")))
for i in range(5): # two _memset funcs apart <= 5 bytes
addr += get_item_size(addr)
create_insn_with_check(addr)
if print_operand(addr, 0) == "_memset":
g_nop_addrs.append((addr, addr+get_item_size(addr), NOP_TYPE.index("MEMSET")))
for x in [ea, addr]:
memset_args = search_up_for_args(x, memset_searchDict, NOP_TYPE.index("MEMSET"))
memset_args = memset_args[:1] + memset_args[2:]
arr_addrs.append([t[0] for t in memset_args])
state_arr = [0] * arr_addrs[0][1]
data_arr = [0] * arr_addrs[1][1]
break
print("INFO: init ", arr_addrs)
ea += get_item_size(ea)

def main():
for i in range(len(g_func_addrs)):
dobf_func(g_func_addrs[i])
for t in g_nop_addrs:
for i in range(t[0], t[1]):
patch_byte(i, 0x90)
create_insn(i)
add_hidden_range(t[0], t[1], NOP_TYPE[t[2]], '', '', 0xFFFFFF)
for t in g_func_addrs:
ea = t[0]
while ea < t[1]:
create_insn(ea)
del_func(ea)
ea += get_item_size(ea)
add_func(t[0], t[1])

if __name__ == '__main__':
print("\n-=-=-= start deobf =-=-=-")
main()

重建后效果图

image-20250831231938999

image-20250831231951248

image-20250831232113868

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
g_secret = [0xDEADBEEF, 0xAAC32EF5, 0x3548AC2D, 0xCACDEF2D]
def get_secret():
global g_secret
for i in range(8):
g_secret[0] += ((i+1)*g_secret[3]) & g_secret[1] ^ (289739946*g_secret[2]+g_secret[1]) ^ (16*g_secret[0])
g_secret[0] &= 0xFFFFFFFF # 32bits
g_secret[2] -= ((345*g_secret[3]+123*g_secret[1]+567*g_secret[2]) | (8*g_secret[3])) ^ (1146171994*g_secret[0]) ^ (i*g_secret[1])
g_secret[2] &= 0xFFFFFFFF
g_secret[1] -= (0xCCCCCCCC*g_secret[0]+7*g_secret[1]) | ((g_secret[2]>>(32-4*i))|(g_secret[2]<<(4*i))) ^ (g_secret[0]*g_secret[2])
g_secret[1] &= 0xFFFFFFFF
g_secret[3] += ((19076178*i)+g_secret[0]) * ((2026744383*g_secret[3]) ^ (16*g_secret[2]) ^ (g_secret[1]>>3))
g_secret[3] &= 0xFFFFFFFF
return (g_secret[3]*g_secret[2] + g_secret[2]*g_secret[1] + g_secret[1]*g_secret[0] + g_secret[0]*g_secret[3])&0xFFFFFFFF

dst = b''
dst += 0x376856ABEED8592A.to_bytes(8, 'little')
dst += 0x3CCF537F7ECA40AB.to_bytes(8, 'little')
dst += 0x92CC25F6240A7A19.to_bytes(8, 'little')
dst += 0x2DA210592DCCFF78.to_bytes(8, 'little')
dst = list(dst)
nums = [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]
charset = "1234567890abcdef"
arr1 = [0 for _ in range(32)]
sum1 = 0
sum2 = 0
for i in range(32): # 为了拿sum1和跑get_secret
arr1[i] = (get_secret()%3+1) & 0xFF
sum1 += arr1[i]
sum1 &= 0xFFFFFFFF
arr2 = [0 for _ in range(sum1+3)]
for i in range(sum1+3): # 只是为了跑get_secret
arr2[i] = charset[get_secret()&0xF]

# 三层循环的逆向
keys = [get_secret() for _ in range(4*100*8)]
for i in range(4):
tmpkeys = keys[i*100*8:(i+1)*100*8][::-1]
for j in range(100):
for k in range(8):
y = nums[(dst[i*8+k]+tmpkeys[j*8+k])&0xFF]
x = (dst[i*8+(k+1)%8]+y) & 0xFF
x = (x<<1)&0xFF | (x>>7)
dst[i*8+(k+1)%8] = x
dst = bytes(dst).hex()

# sub_401980的逆向
flag = []
for i in range(32):
# 爆破
for x in range(16):
a = ord(dst[2*i]) # out[2*i] = arr2[sum2]
b = charset.index(dst[2*i+1])
if (a+x*3)%16 == b: # out[2*i+1] = charset[(ord(arr2[sum2])+3*a2n(in[i])) % 16]
flag.append(x)
break
flag = ''.join([hex(x)[-1] for x in flag])
print("DASCTF{" + flag + "}")
# DASCTF{9d9b9ff1c62122ba7f5b54385b1f9d64}

这个静态方法太搞了,有空回来看看吧切片式SMC的例题解析及其自动化反混淆 | CN-SEC 中文网

动态方法如下:

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
import idaapi
import idautils
import ida_bytes
import idc

arr = [
{"base_addr": "0x40235a", "len": "0x135", "key": "0x0"},
{"base_addr": "0x4024de", "len": "0x21", "key": "0x109cf92e"},
{"base_addr": "0x403582", "len": "0x23", "key": "0x109cf92e"},
{"base_addr": "0x40256d", "len": "0x64", "key": "0x109cf92e"},
{"base_addr": "0x40263a", "len": "0x23", "key": "0x6f403b1d"},
{"base_addr": "0x4026d5", "len": "0x36", "key": "0x74afec82"},
{"base_addr": "0x402758", "len": "0x36", "key": "0x3508284b"},
{"base_addr": "0x402937", "len": "0x14", "key": "0x3508284b"},
{"base_addr": "0x4029f2", "len": "0x22", "key": "0x64937846"},
{"base_addr": "0x402a82", "len": "0x39", "key": "0x74afec82"},
{"base_addr": "0x401a35", "len": "0x10a", "key": "0x0"},
{"base_addr": "0x401ba2", "len": "0x39", "key": "0x41b71efb"},
{"base_addr": "0x401c52", "len": "0x57", "key": "0x3855b718"},
{"base_addr": "0x401236", "len": "0x1f", "key": "0x0"},
{"base_addr": "0x4012ac", "len": "0x14", "key": "0x6b8b4567"},
{"base_addr": "0x401323", "len": "0x15d", "key": "0x59f066a1"},
{"base_addr": "0x4014b5", "len": "0x19", "key": "0x3dccfec8"},
{"base_addr": "0x40150e", "len": "0x5c", "key": "0x59f066a1"},
{"base_addr": "0x401ce4", "len": "0x22", "key": "0x4d10565e"},
{"base_addr": "0x401d6e", "len": "0x3a", "key": "0x3855b718"},
{"base_addr": "0x401e0b", "len": "0x44", "key": "0x6385d5da"},
{"base_addr": "0x401ec9", "len": "0x3f", "key": "0x71a5dd8e"},
{"base_addr": "0x401f43", "len": "0x22", "key": "0x3c14fa76"},
{"base_addr": "0x401fcd", "len": "0x2e", "key": "0x71a5dd8e"},
{"base_addr": "0x40205e", "len": "0x39", "key": "0x6eb33466"},
{"base_addr": "0x40210e", "len": "0xd2", "key": "0x7f23f981"},
{"base_addr": "0x40160d", "len": "0x35", "key": "0x0"},
{"base_addr": "0x4016a5", "len": "0x15", "key": "0x19495cff"},
{"base_addr": "0x4016fe", "len": "0x25", "key": "0x33a1c8b5"},
{"base_addr": "0x40194a", "len": "0x20", "key": "0x19495cff"},
{"base_addr": "0x40221b", "len": "0x22", "key": "0x19ccba0c"},
{"base_addr": "0x40227d", "len": "0x14", "key": "0x7f23f981"},
{"base_addr": "0x402b24", "len": "0x23", "key": "0x082c08da"},
{"base_addr": "0x402bbf", "len": "0x84", "key": "0x2d5d3879"},
{"base_addr": "0x402c81", "len": "0x22", "key": "0x4fe6e123"},
{"base_addr": "0x40178b", "len": "0x21", "key": "0x19495cff"},
{"base_addr": "0x40181e", "len": "0x21", "key": "0x3ac743d6"},
{"base_addr": "0x401889", "len": "0x2b", "key": "0x7c2f3f1b"},
{"base_addr": "0x402d11", "len": "0x21", "key": "0x2d5d3879"},
{"base_addr": "0x402d9b", "len": "0x23", "key": "0x4fd1b124"},
{"base_addr": "0x402e33", "len": "0x46", "key": "0x7ceb0021"},
{"base_addr": "0x402edf", "len": "0x23", "key": "0x0ef6a336"},
{"base_addr": "0x402f77", "len": "0x21", "key": "0x2ab50b6e"},
{"base_addr": "0x402ffe", "len": "0x23", "key": "0x07a85187"},
{"base_addr": "0x403096", "len": "0xc7", "key": "0x60cbd5d9"},
{"base_addr": "0x403198", "len": "0x22", "key": "0x15697d0d"},
{"base_addr": "0x403225", "len": "0x14", "key": "0x60cbd5d9"},
{"base_addr": "0x403274", "len": "0x22", "key": "0x1948596b"},
{"base_addr": "0x403301", "len": "0x14", "key": "0x2ab50b6e"},
{"base_addr": "0x403350", "len": "0x22", "key": "0x21b6eba8"},
{"base_addr": "0x4033dd", "len": "0x64", "key": "0x7ceb0021"},
{"base_addr": "0x403526", "len": "0x21", "key": "0x280f9e95"},
{"base_addr": "0x402806", "len": "0x36", "key": "0x3508284b"},
{"base_addr": "0x40298e", "len": "0x21", "key": "0x7b62d32d"},
{"base_addr": "0x40348b", "len": "0x60", "key": "0x280f9e95"},
{"base_addr": "0x4018f4", "len": "0x1b", "key": "0x3ac743d6"},
{"base_addr": "0x402889", "len": "0x36", "key": "0x7b62d32d"},
]

def decrypt_block(base_addr, block_len, key):
code = ida_bytes.get_bytes(base_addr, block_len)
real_code = bytearray(block_len)
s = [(key >> (8 * i)) & 0xFF for i in range(4)]
for i in range(block_len):
real_code[i] = code[i] ^ (((i - 50) & 0xFF) ^ s[i % 4])
return bytes(real_code)

# 解密并打补丁,同时在块前后各Nop 5字节
for block in arr:
base_addr = int(block["base_addr"], 16)
block_len = int(block["len"], 16)
key = int(block["key"], 16)
code = decrypt_block(base_addr, block_len, key)
ida_bytes.patch_bytes(base_addr - 5, b"\x90" * 5)
ida_bytes.patch_bytes(base_addr, code)
ida_bytes.patch_bytes(base_addr + block_len, b"\x90" * 5)

# 回溯清理参数设置指令(块前最多回溯10条)
for block in arr:
base_addr = int(block["base_addr"], 16)
block_len = int(block["len"], 16)

regs = [idaapi.str2reg("rdi"), idaapi.str2reg("rsi"), idaapi.str2reg("rdx")]
addr = base_addr - 5
i = 0
while i < 10:
i += 1
addr = idc.prev_head(addr, 0x400000)
ins = idaapi.insn_t()
if not idaapi.decode_insn(ins, addr):
continue
mnem = ins.get_canon_mnem()
if "mov" not in mnem:
continue
op0 = ins.ops[0]
op1 = ins.ops[1]
if op0.type == idaapi.o_reg:
if op0.reg in regs:
regs.remove(op0.reg)
if op1.type == idaapi.o_reg:
regs.append(op1.reg)
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))
elif op0.type == idaapi.o_displ and op1.type == idaapi.o_imm:
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))
elif op1.type == idaapi.o_reg and (op1.reg == idaapi.str2reg("rax") or op1.reg == idaapi.str2reg("rcx")):
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))

# 从块尾继续回溯,最多6条,清理残留mov
regs = [idaapi.str2reg("rdi"), idaapi.str2reg("rsi"), idaapi.str2reg("rdx")]
addr = base_addr + block_len
i = 0
while i < 6:
i += 1
addr = idc.prev_head(addr, 0x400000)
ins = idaapi.insn_t()
if not idaapi.decode_insn(ins, addr):
continue
mnem = ins.get_canon_mnem()
if "mov" not in mnem:
continue
op0 = ins.ops[0]
op1 = ins.ops[1]
if op0.type == idaapi.o_reg:
if op0.reg in regs:
regs.remove(op0.reg)
if op1.type == idaapi.o_reg:
regs.append(op1.reg)
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))

# 清理对 sub_4035C0 的调用点前若干指令
for ref in idautils.CodeRefsTo(0x4035C0, 0):
i = 0
regs = [
idaapi.str2reg("rdi"),
idaapi.str2reg("rsi"),
idaapi.str2reg("rdx"),
idaapi.str2reg("rcx"),
idaapi.str2reg("r8"),
]
addr = ref
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))
while i < 6:
i += 1
addr = idc.prev_head(addr, 0x400000)
ins = idaapi.insn_t()
if not idaapi.decode_insn(ins, addr):
continue
mnem = ins.get_canon_mnem()
if "mov" not in mnem and "lea" not in mnem:
continue
op0 = ins.ops[0]
op1 = ins.ops[1]
if op0.type == idaapi.o_reg:
if op0.reg in regs:
regs.remove(op0.reg)
if op1.type == idaapi.o_reg:
regs.append(op1.reg)
ida_bytes.patch_bytes(addr, b"\x90" * idc.get_item_size(addr))
i += 1

留下了很多nop,还是不能F5……

也可以手搓,每次运行可以记录下这里的几个地址然后脚本patch

image-20250831235328504

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def set_smc_key(key):
global smc_key
idx = [5]
for i in idx:
smc_key[i] ^= key

def smc_decrypt(addr, length, key):
key = key.to_bytes(4, 'little')
for i in range(addr, addr + length):
v = get_wide_byte(i)
patch_byte(i, ((i - addr - 50) & 0xff) ^ v ^ key[(i - addr) % 4])

# set_smc_key(0x46E87CCD)
smc_decrypt(0x401236, 0x1f, 0x0)