第一个JLRE0203.exe是一个典型的SMC(Self Modified Code),关键部分在于它VirtualAlloc的0x74字节的机器码(位于ida中地址0x140005060处)。
0x74字节的机器码分为两个部分,一部分是开头和结尾的解密代码,另一部分是中间(地址范围0x14000506B ~ 0x1400050A5)被加密的代码,长度是0x3a字节。
从VirtualAlloc前面输入flag的代码逻辑来看,flag[8:10]处的两个字节为加密的key,加密算法为word by word的xor加密。
所以这里比较麻烦的是必须爆破出对应的key。key的取值空间大约是0x10000量级。所以我们用unicode模拟执行跑一下就行:
#!/usr/bin/env python3
import struct
import unicorn
# idaapi.get_bytes(0x140005060, 0x74).hex()
data = bytes.fromhex('415741564155e83c000000293e293f21e0a728e8ae7d20e98e9769686921ea8f6621e0a620e98f9769686921a8966d21ea8e6621a88f6d25609625e1996c7f5c7f29372936eb26415e4d31ff4983ff3a731866438b043e66448b2a664431e8664389043e4983c702ebe241ffe6415d415e415fc3')
for i in range(0, 0x10000):
k = struct.pack('<H', i)
uc = unicorn.Uc(unicorn.UC_ARCH_X86, unicorn.UC_MODE_64)
uc.mem_map(0x3000, 0x1000) # code区域
uc.mem_map(0x4000, 0x1000, unicorn.UC_PROT_READ | unicorn.UC_PROT_WRITE) # data区域
uc.mem_map(0x5000, 0x1000, unicorn.UC_PROT_READ | unicorn.UC_PROT_WRITE) # stack区域
uc.mem_write(0x3000, data)
uc.mem_write(0x4000, b'flag{aaa' + k + b'a' * 28 + b'\x00') # 用flag填充
uc.reg_write(unicorn.x86_const.UC_X86_REG_RCX, ord('f')) # flag的第一个字符,也就是f
uc.reg_write(unicorn.x86_const.UC_X86_REG_RDX, 0x4000 + 8) # 0x4000 + 8,即key所在的地址
uc.mem_write(0x5200, struct.pack('<Q', 0xdead0000)) # 设置ret返回地址
uc.reg_write(unicorn.x86_const.UC_X86_REG_RSP, 0x5200)
try:
uc.emu_start(0x3000, 0xdead0000, timeout = 1000) # 防止尝试解密后的代码死循环
al = uc.reg_read(unicorn.x86_const.UC_X86_REG_AL) # 读取代码执行后的结果
if al == 0xda: # 从题目代码逻辑来看,flag的第一个字符f经过代码运算之后应当与0x140005050处的第一个字节相等
print(k)
except:
pass # 模拟过程如果出现任何异常,即解密失败
del uc
运行之后结果应该会输出b'hi',即hi为解密的key。
之后直接在ida控制台里解密对应代码:
import itertools
bytes(a ^ b for a, b in zip(idaapi.get_bytes(0x14000506B, 0x3a), itertools.cycle(b'hi'))).hex()
得到
415741564989cf4180c7154981e7ff0000004983e70f4989ce4981e6ff00000049c1fe044983e60f49c1e7044d09fe4c89f004163416415e415f
丢到cstool反汇编
$ cstool x64 415741564989cf4180c7154981e7ff0000004983e70f4989ce4981e6ff00000049c1fe044983e60f49c1e7044d09fe4c89f004163416415e415f
0 41 57 push r15
2 41 56 push r14
4 49 89 cf mov r15, rcx
7 41 80 c7 15 add r15b, 0x15
b 49 81 e7 ff 00 00 00 and r15, 0xff
12 49 83 e7 0f and r15, 0xf
16 49 89 ce mov r14, rcx
19 49 81 e6 ff 00 00 00 and r14, 0xff
20 49 c1 fe 04 sar r14, 4
24 49 83 e6 0f and r14, 0xf
28 49 c1 e7 04 shl r15, 4
2c 4d 09 fe or r14, r15
2f 4c 89 f0 mov rax, r14
32 04 16 add al, 0x16
34 34 16 xor al, 0x16
36 41 5e pop r14
38 41 5f pop r15
rcx就是call时传入的第一个参数。代码逻辑还是很简单的,无非就是xor、add、交换高低4位blablabla...
照这个逻辑写个解密脚本
# idaapi.get_bytes(0x140005050, 8).hex()
# idaapi.get_bytes(0x1400050D8, 0x1c).hex()
data = bytes.fromhex('da3a6aca0baaba8a' + '9aafdf6a8aefafda9acf9f7fdfdfdfda8a6fdf6f8f6f6ada7faf6f2b')
buf = []
for b in data:
b ^= 0x16
b = (b - 0x16) & 0xff
lo = b & 0xf
hi = (b >> 4) & 0xf
b = (lo << 4) | ((hi - 0x15) & 0xf)
buf.append(b)
flag = bytes(buf)
print(flag[:8] + b'hi' + flag[8:])
即得到flag{edchib56ac95fb720666fc16131af051}