babyobfu
,题中使用了非常多的SMC保护函数原本的代码,在原代码运行前对代码进行解密,在原代码运行结束后对代码进行重加密,这种切片式保护能很好的防止静态分析,而原本代码中100层的循环加密也阻挠了一些动态分析选手。切片式SMC解析
SMC(self-Modifying Code 自修改代码),就是在真正执行某一段代码时,程序会对自身的该段代码进行自修改,只有在修改后的代码才是可汇编、可执行的。在程序未对该段代码进行修改之前,在静态分析状态下,均是不可读的字节码,IDA之类的反汇编器无法识别程序的正常逻辑。
初始化阶段
memset(str, c, n)
)构成,主要用0填充了首地址分别为[rbp-47h]
和[rbp-0C4h]
的两个局部变量数组,大小分别为0x1F
和0x7C
,从后续阶段可知两个数组可命名为state
和data
。即:memset(state, 0, 0x1F); // [rbp-47h]
memset(data, 0, 0x7C); // [rbp-0C4h]
密钥设置阶段
smc_keyset
函数(0x4035C0),可以看到该函数接收了五个参数,其中除了常量以外也有上一阶段中的两个局部变量数组。该函数的具体实现:a1
判断是否进行密钥更改、a2
限制大小,将a4[a3[i]]
与a5
异或。由函数调用前的传参可知,a1
为上一阶段中初始化的state
数组的首字节(根据栈偏移确认索引),a4
为上一阶段中初始化的data
数组(即密钥),a3
为.data段中的一段已知数据,a2
和a5
都是常数。即:smc_keyset(state[0], 0x1E, (int *)&unk_4062B0, data, 0x109CF92E);
代码解密阶段
smc_decrypt
函数(0x403630),将上面的state
设置为1以后,该函数接收了三个参数,其中除了常量以外还调用到了data
数组。该函数的具体实现:getpagesize
和mprotect
两个函数用于暂时更改部分地址的权限,具体为代码段[a1, a1+a2)
范围(a1
为需要恢复代码的首地址,a2
为代码长度)增加可写权限,以便SMC对代码段字节进行更改。接着用a3
(data
数组的首四字节,同样需要用栈偏移确认索引,可以看成SMC过程中的密钥)初始化了一个临时数组s
,使a1[i] ^= (i-50) ^ s[i%4]
,其中i-50
为unsigned char类型。结合函数调用前的传参可得:smc_decrypt(off_406940, qword_406948, data[0]);
[rbp-0D8h]
、[rbp-0E0h]
、[rbp-0CCh]
),方便代码执行完成后的重新加密。代码重加密阶段
smc_encrypt
函数(0x403760),0x402488处的汇编为正常运行的原代码,可以忽略(在其余smc_encrypt
函数前也有类似需忽略的正常代码)。可以看到在函数调用前直接把栈对应偏移的参数传入了寄存器中,继而被函数调用。该函数直接调用了smc_decrypt
函数,由异或的性质也可知加密与解密可以完全相同:IDAPython 自动化反混淆
_memset
时初始化两个数组,并记录他们的栈偏移;搜索到调用smc_keyset
时,往上寻找各参数并调用自己复刻的smc_keyset
进行密钥设置;搜索到调用smc_decrypt
时,同样往上寻找各参数并调用自己复刻的smc_decrypt
进行代码解密;搜索到调用smc_encrypt
时,往上寻找各参数并记录参数设置的地址。所有的相关指令都被记录在一个nop数组中,遍历结束以后nop掉这些指令,防止干扰对程序的分析,并重建函数方便反编译。smc_keyset
和smc_decrypt
两个函数复刻一下就可以了,注意常量需要从程序中获取,所以传进来的是处理后的地址,使用get_wide_byte和get_wide_dword进行获取: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
C
(MakeCode)对该字节创建指令,就会直接覆盖后面的explored字节。C
能转指令但是create_insn转不了指令的情况。于是这个函数就是为了解决create_insn失败的问题:把后面的字节逐个无条件转成undefined,直到能成功创建ea处的指令。(其实为了鲁棒性还应该检查最后return的时候off==create_insn(ea)的,但没出bug就不管了)def create_insn_with_check(ea):
off = 1
while create_insn(ea) == 0:
del_items(ea, 0, off)
off += 1
return
smc_keyset
和smc_decrypt
,向前遍历并使用正则匹配指令的操作数,获取到参数后调用对应的函数;对初始化阶段的两个memset
,由于一定会在函数的最开头(不然后面解密就没有局部变量可用了),那么匹配到以后就不会再匹配了,并且记录他们创建的局部变量数组的偏移,在smc_keyset
和smc_decrypt
两个函数调用时需要用到这个偏移计算参数;对smc_encrypt
,同样需要向上寻找参数,但只是用于寻找这些添加参数的指令。这些涉及到的指令都会添加进nop的列表g_nop_addrs
里,防止赋值干扰后续分析(需要保证后续使用到这些寄存器时会重新赋值)。ea += get_item_size(ea)
而不是next_head()
是因为对于后续不是指令的地址(因为后面的是没反混淆的所以必然会出现这种情况),next_head()
会直接跳过,从而影响程序的顺序扫描。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)
search_up_for_args
也是自己实现的函数,用于向上寻找参数,当遇到形如lea rcx, [rbp-0C4h]
、 mov rdi, rcx
的指令组合时也能正常获取,在memset
和smc_encrypt
中会出现(smc_keyset
和smc_decrypt
是没有这种组合指令的,所以直接向前遍历提高效率,用assert上一层保险)。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
g_nop_addrs
里的地址nop并隐藏(便于后续的反编译和反汇编分析),最后重新遍历一次把前面没创指令对象的全部创建,并把ida之前错误分析时生成的函数删除,最后重建函数。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])
babyobfu解题过程
sub_401980(in, out, len)
的函数,可以通过调用猜测三个参数分别为输入、输出的位置和输入长度,一些局部变量可以通过汇编上下跟踪进行确认,并改为方便辨认的名字以助分析。arr1 = [0 for _ in range(32)]
sum1 = 0
for i in range(32):
arr1[i] = (get_secret()%3+1) & 0xFF
sum1 += arr1[i]
sum1 &= 0xFFFFFFFF
get_secret
函数(0x4011B0)通过简单计算更改了四个全局变量,并返回一个相乘的累加值,用Python表达有: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
charset = "1234567890abcdef"
arr2 = [0 for _ in range(sum1+3)]
for i in range(sum1+3):
arr2[i] = charset[get_secret()&0xF]
sum2 = 0
for i in range(32):
sum2 += arr1[i]
out[2*i] = arr2[sum2]
out[2*i+1] = charset[(arr2[sum2]+3*a2n(in[i])) % 16]
a2n
函数(0x401580)就是一个转换,将ascii码转成对应的数字(比如'0' -> 0、'a' -> 10)。sub_401980
的输出均是在程序的自定义charset("1234567890abcdef")中的,这里就是把这些字符两两一组unhex成字节("ab" -> 0xab)。for i in range(4):
for j in range(100): # 动态调试跳不出来的原因
for k in range(8)[::-1]:
v12 = nums[(get_secret()+dst[i*8+k])&0xFF]
v13 = ((dst[i*8+(k+1)%8]<<7) | (dst[i*8+(k+1)%8]>>1)) & 0xFF
dst[i*8+(k+1)%8] = (v13 - v12) & 0xFF
arr1 = [0 for _ in range(32)]
sum1 = 0
for i in range(32):
arr1[i] = (get_secret()%3+1) & 0xFF
sum1 += arr1[i]
sum1 &= 0xFFFFFFFF
charset = "1234567890abcdef"
arr2 = [0 for _ in range(sum1+3)]
for i in range(sum1+3):
arr2[i] = charset[get_secret()&0xF]
sum2 = 0
for i in range(32):
sum2 += arr1[i]
out[2*i] = arr2[sum2]
out[2*i+1] = charset[(ord(arr2[sum2])+3*a2n(in[i])) % 16]
dst = list(bytes.fromhex(out))
for i in range(4):
for j in range(100): # 动态调试跳不出来的原因
for k in range(8)[::-1]:
v12 = nums[(get_secret()+dst[i*8+k])&0xFF]
v13 = ((dst[i*8+(k+1)%8]<<7) | (dst[i*8+(k+1)%8]>>1)) & 0xFF
dst[i*8+(k+1)%8] = (v13 - v12) & 0xFF
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的例题解析及其自动化反混淆
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论