刷 BUUCTF 时遇到的一道中等难度 RE,由于 BUU 平台把题目源码一并打包发布了,所以网上大部分 WP 都是偷懒直接分析源码“逆向”,其他一些 WP 也多少存在一些错误
由于基本功不牢,加上对花指令不熟悉,自己花了三天空闲时间才真正完全搞明白,为了便于自己和其他新手学习,特地写得非常详细,相信新手也能顺利复现分析过程,能够从中学到一些基本分析思路和 IDA 操作方法,以及花指令的混淆原理和绕过方法。也欢迎各位师傅批评指正!
简单运行
运行程序,确定字符串,在 ida 中找到对应代码段
调试分析
动调后发现运行不到 main 函数程序就卡死了,CPU 负荷暴增,基本可以判定在程序初始化过程中有地方检测了调试器并进入了类似于死循环地方。
检查不同的函数,逐行调试后发现在这里进入死循环
交叉引用发现罪魁祸首是函数 sub_401010
我们可以把 jnz loc_4012E0 给 nop 掉,即可绕过死循环
之后将 patch 的部分保存到源程序即可正常调试
当然如果找不到哪里陷入死循环,更简单的方法是
可以先运行程序后使用 ida attach 方法绕过
先在程序用户输入处下好断点,运行后用 ida attach
Debugger —— Attach to process —— 找到程序进程
输入 24 个字符后,来到 IsDebuggerPresent()
函数 直接运行会弹窗
所以 F7 跟进函数
发现不是 IsDebuggerPresent()
而是进入了 一个 新的函数 里面就有一个 弹窗 所以我们要动态调试修改 ZF 标志寄存器,从而绕过判断。
之后程序判断是否输入长度为 24
之后进入下一个分支,
在 sub_401460 中会对时间进行检测,也就是说若是跟入研究这个函数就会被 check 到然后修改掉 box,而且里面写的非常复杂,包含花指令,所以正确的做法应该是把断点下在后面的 loc_4012F0 处。这个 loc_4012F0里面就是加密逻辑了
加密逻辑
将我们的输入 4 个字符的 ascii 码为一组保存在寄存器中
即 abcdefgh ——> abcd efgh
之后将这一组数据按照寄存器里得到的数字进行向右移位操作
继续分析,程序将字符串继续左移 0x20 - 3 = 0x1D
位,结合上述代码,其实就是将我们输入的 0x61626364
循环右移 3 位
花指令分析
再往下,遇到一处花指令,就是它导致我们无法 F5 反编译代码
这是一个较为经典的利用 call & ret
构造的可执行花指令
我们先回顾一下 call 和 ret 的本质
call指令的本质:push 函数返回地址
然后 jmp 函数地址
ret指令的本质:pop eip
逻辑其实特别清晰,就是先 call sub_3A13CA,此时会把返回地址 0x003A13C7 压栈,之后再把返回地址的值 +1 变为 0x003A13C8,然后 ret 把 0x003A13C8 pop 到 eip中,最后程序 jmp 到 loc_3A13CF 接下来程序继续正常执行。
该花指令原结构为
_asm
{
call sub1
_emit 0xE8
jmp label1
sub1:
add dword ptr[esp],1
retn
label1:
}
这里其实 ida 已经帮我们进行了识别,有时候该花指令会被识别成这样
其实本质上没有变化,esp 的值 +1 后,返回地址变为 0x003A13C8,即从第二个机器码 EB 开始,跳过了第一个 EB,机器码 EB 就是 jmp 指令,出现后,ida 会把其后四个字节直接当做跳转地址,所以导致分析出错。
我们将第一个 EB nop 掉,即 change byte 为 90
程序变回开始的样子
半自动 patch 花指令
我们只需要将上述花指令部分全部 patch 成 nop 即可
此处可以使用 IDAPython 的脚本帮助
def nop(addr, endaddr):
while addr < endaddr:
PatchByte(addr, 0x90)
addr += 1
nop(startaddr, endaddr)
MakeFunction(strataddr, -1)
MakeFunction(ea, end) 将有begin到end的指令转换成一个函数。如果end被指定为BADADDR(-1),IDA会尝试通过定位函数的返回指令,来自动确定该函数的结束地址
本题中此段代码共有两处相同的花指令
均 nop 后 MakeFunction,此时代码段变黑
发现该函数已经可以正常 F5 反汇编
解密还原
还原加密逻辑
bool encode(char* ur_flag)
{
unsigned int k=0,bk=0;
for(int i=0;i<strlen(ur_flag);i+=4)
{
k=(((int)ur_flag[i])<<24)|(((int)ur_flag[i+1])<<16)|(((int)ur_flag[i+2])<<8)|((int)ur_flag[i+3]);
k=(k>>key[i/4]) | (k<<(32-key[i/4]));
k=((~(k>>16))&0x0000ffff) | (k<<16);
k=(1<<key[i/4])^k;
if(i>0)
k^=bk;
bk=k;
if(k!=ks[i/4])
return false;
}
return true;
}
之后只需要动态出 &xmmword_405034 中的值即可(其中第一个值为 3 即最开始分析的循环右移 3 位)
dump 出来为 [3, 0x10, 0xd, 4, 0x13, 0xb]
ciper = [0x8C2C133A, 0xF74CB3F6, 0xFEDFA6F2, 0xAB293E3B, 0x26CF8A2A, 0x88A1F279]
sh_box = [0x3, 0x10, 0xd, 0x4, 0x13, 0xb]
for i in range(5, 0, -1):
ciper[i] ^= ciper[i-1]
for i in range(6):
ciper[i] ^= (1 << sh_box[i])
ciper[i] = ((~(ciper[i] & 0xffff) << 16) | ((ciper[i] & 0xffff0000) >> 16)) & 0xffffffff
ciper[i] = ((ciper[i] << (sh_box[i])) | (ciper[i] >> (0x20 - sh_box[i]))) & 0xffffffff
print(ciper)
print("".join([bytes.fromhex(hex(x).replace('0x','')).decode() for x in ciper]))
Flag
flag{a_3a2y_re_for_test}
参考链接
花指令总结
逆向学习笔记之花指令