花指令 ¶
原理 ¶
花指令是企图隐藏掉不想被逆向工程的代码块 (或其它功能) 的一种方法, 在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行, 而程序无法很好地反编译, 难以理解程序内容, 达到混淆视听的效果.
花指令通常用于加大静态分析的难度。
编写 ¶
最简单的花指令使用了内联汇编的方式进行,下面以 VC 添加花指令的方式举例,gnu 的编译器也可以采用类似的方式添加花指令,但是使用 AT&T 汇编:
// 正常的函数代码
int add(int a, int b){
int c = 0;
c = a + b;
return c;
}
// 添加花指令的函数代码
int add_with_junk(int a, int b){
int c = 0;
__asm{
jz label;
jnz label;
_emit 0xe8; call 指令,后面加4bytes的地址偏移,因此导致反汇编器不能正常识别
label:
}
c = a + b;
return c;
}
使用 ida 的反编译时,添加了花指令的函数不能正常识别,结果如下:
伪代码:
// 添加了花指令
.text:00401070 loc_401070: ; CODE XREF: sub_401005↑j
.text:00401070 push ebp
.text:00401071 mov ebp, esp
.text:00401073 sub esp, 44h
.text:00401076 push ebx
.text:00401077 push esi
.text:00401078 push edi
.text:00401079 lea edi, [ebp-44h]
.text:0040107C mov ecx, 11h
.text:00401081 mov eax, 0CCCCCCCCh
.text:00401086 rep stosd
.text:00401088 mov dword ptr [ebp-4], 0
.text:0040108F jz short near ptr loc_401093+1
.text:00401091 jnz short near ptr loc_401093+1
.text:00401093
.text:00401093 loc_401093: ; CODE XREF: .text:0040108F↑j
.text:00401093 ; .text:00401091↑j
.text:00401093 call near ptr 3485623h
.text:00401098 inc ebp
.text:00401099 or al, 89h
.text:0040109B inc ebp
.text:0040109C cld
.text:0040109D mov eax, [ebp-4]
.text:004010A0 pop edi
.text:004010A1 pop esi
.text:004010A2 pop ebx
.text:004010A3 add esp, 44h
.text:004010A6 cmp ebp, esp
.text:004010A8 call __chkesp
.text:004010AD mov esp, ebp
.text:004010AF pop ebp
.text:004010B0 retn
在上面这个例子中,把混淆视听的花指令 patch 成 nop 即可修复,然后正常分析。
值得注意的是,ida 对于栈的判定比较严格,因此 push,ret 一类的花指令会干扰反汇编器的正常运行,下面给出一个具体的例子,读者可以自己编译复现:
#include <stdio.h>
// 使用 gcc/g++ 进行编译
int main(){
__asm__(".byte 0x55;"); // push rbp 保存栈
__asm__(".byte 0xe8,0,0,0,0;"); // call $5;
__asm__(".byte 0x5d;"); // pop rbp -> 获取rip的值
__asm__(".byte 0x48,0x83,0xc5,0x08;"); // add rbp, 8
__asm__(".byte 0x55;"); // push rbp -> 相当于将call的返回值修改到下面去
__asm__("ret;");
__asm__(".byte 0xe8;"); // 这是混淆指令不执行
__asm__(".byte 0x5d;"); // pop rbp 还原栈
printf("whoami \n");
return 0;
}
例题 ¶
这里以看雪.TSRC 2017CTF秋季赛
第二题作为讲解. 题目下载链接: ctf2017_Fpc.exe
程序写了几个函数混淆视听, 将关键的验证逻辑加花指令防止了 IDA 的静态分析. 我们用 IDA 打开 Fpc 这道题, 程序会先打印一些提示信息, 然后获取用户的输入.
这里使用了不安全的scanf
函数, 用户输入的缓冲区只有0xCh
长, 我们双击v1
进入栈帧视图
因此我们可以通过溢出数据, 覆盖掉返回地址, 从而转移到任意地址继续执行.
这里我还需要解释一下, 就是scanf
之前写的几个混淆视听的函数, 是一些简单的方程式但实际上是无解的. 程序将真正的验证逻辑加花混淆, 导致 IDA 无法很好的进行反编译. 所以我们这道题的思路就是, 通过溢出转到真正的验证代码处继续执行.
我们在分析时可以在代码不远处发现以下数据块.
因为 IDA 没能很好的识别数据, 因此我们可以将光标移到数据块的起始位置, 然后按下C
键 (code) 将这块数据反汇编成代码
值得注意的是, 这段代码的位置是0x00413131
, 0x41
是'A'
的 ascii 码,而0x31
是'1'
的 ascii 码. 由于看雪比赛的限制, 用户输入只能是字母和数字, 所以我们也完全可以利用溢出漏洞执行这段代码
用 OD 打开, 然后Ctrl+G
到达0x413131
处设下断点, 运行后输入12345612345611A
回车, 程序成功地到达0x00413131
处. 然后右键分析->从模块中删除分析
识别出正确代码
断在0x413131
处后, 点击菜单栏的"查看"
, 选择"RUN跟踪"
, 然后再点击"调试"
, 选择"跟踪步入"
, 程序会记录这段花指令执行的过程, 如下图所示:
这段花指令本来很长, 但是使用 OD 的跟踪功能后, 花指令的执行流程就非常清楚. 整个过程中进行了大量的跳转, 我们只要取其中的有效指令拿出来分析即可.
需要注意的是, 在有效指令中, 我们依旧要满足一些条件跳转, 这样程序才能在正确的逻辑上一直执行下去.
比如0x413420
处的jnz ctf2017_.00413B03
. 我们就要重新来过, 并在0x413420
设下断点
通过修改标志寄存器来满足跳转. 继续跟踪步入 (之后还有0041362E jnz ctf2017_.00413B03
需要满足). 保证逻辑正确后, 将有效指令取出继续分析就好了