其他
由易到难全面解析CTF中的花指令
一
概念
二
原理
反编译器的线性反编译(理解花指令的重点)
CALL 0x地址
。push ebp
,这条语句每个函数开始都会有。同样,有些字节码又需要两个、三个、四个字节来反编译为一条指令。CALL 0x地址
指令。但实际上0xE8后面的四个字节是单独的字节码指令。这大概就是线性反编译。线性扫描和递归下降
线性扫描的特点:从入口开始,一次解析每一条指令,遇到分支指令不会递归进入分支。
当使用线性扫描时,比如遇到call或者jmp的时候,不会跳转到对应地址进行反汇编,而是反汇编call指令的下一条指令,这就会导致出现很多问题。
递归下降分析当遇到分支指令时,会递归进入分支进行反汇编。
使反汇编引擎解析错误
机器码
对于出题人
三
写花指令的原则:保持堆栈的平衡
常用指令含义
pop ebp ----把基址指针寄存器弹出堆栈
push eax ----把数据寄存器压入堆栈
pop eax ----把数据寄存器弹出堆栈
nop -----不执行
add esp,1-----指针寄存器加1
sub esp,-1-----指针寄存器加1
add esp,-1--------指针寄存器减1
sub esp,1-----指针寄存器减1
inc ecx -----计数器加1
dec ecx -----计数器减1
sub esp,1 ----指针寄存器-1
sub esp,-1----指针寄存器加1
jmp 入口地址----跳到程序入口地址
push 入口地址---把入口地址压入堆栈
retn ------ 反回到入口地址,效果与jmp 入口地址一样
mov eax,入口地址 ------把入口地址转送到数据寄存器中.
jmp eax ----- 跳到程序入口地址
jb 入口地址
jnb 入口地址 ------效果和jmp 入口地址一样,直接跳到程序入口地址
xor eax,eax 寄存器EAX清0
CALL 空白命令的地址 无效call
四
栈指针平衡(引子)
(Options-General-Disassembly-"Stack pointer")
手动修改地址
注意:每条语句前的栈指针是这条语句未执行的栈指针。
使用插件nop掉
五
花指令的编写
此处笔者踩了个大坑,值得一提的是,win下的gcc只支持x86下的内联汇编。
asm
__asm
关键字用于调用内联汇编程序,并且可在 C 或 C++ 语句合法时出现。它不能单独显示。它后面必须跟一个程序集指令、一组括在大括号中的指令,或者至少是一对空大括号。此处的术语“__asm
块”指任何指令或指令组(无论是否在大括号中)。
asm语法
__asm
assembly-instruction**;
opt__asm {
assembly-instruction-list}
****;
**optassembly-instruction**
;
optassembly-instruction
;
assembly-instruction-list;
**optasm示例
括在大括号里的简单
__asm
块:
mov al, 2
mov dx, 0xD007
out dx, al
}
__asm
放在每个程序集指令前面:__asm mov dx, 0xD007
__asm out dx, al
__asm
关键字是语句分隔符,因此还可将程序集指令放在同一行中:__asm
块)具有一些优势。大括号可清楚地将程序集代码与 C 或 C++ 代码分隔开,并避免了不必要的__asm
关键字重复。大括号还可防止二义性。如果要将 C 或 C++ 语句放在与__asm
块相同的行上,则必须将此块括在大括号中。如果没有大括号,编译器无法判断程序集代码停止的位置以及 C 或 C++ 语句的开始位置。花指令实现
_emit 立即数
__asm
{
_emit 0xE8
}
//代表在这个位置插入一个字节数据0xE8
{
jmp Label1
db thunkcode1; 垃圾数据
//垃圾数据例如:_emit 0xE8
Label1:
}
花指令分类
emit指令的作用:
1.编译器不认识的指令,拆成机器码来写。 2.插入垃圾字节来反跟踪,又称花指令。
用emit就是在当前位置直接插入数据(实际上是指令),一般是用来直接插入汇编里面没有的特殊指令,多数指令可以用asm内嵌汇编来做,没有必要用emit来做,除非你不想让其它人看懂你的代码。
我们来看用IDA反汇编的效果吧。
1.最简单的花指令
a.最简单的jmp
db thunkcode1;垃圾数据
Labe1:
b.过时的多节形式与多层乱序。
JMP Label1
Db thunkcode1
Label1:
……
JMP Label2
Db thunkcode2
Label2:
……
JMP Label1
Db thunkcode1
Label2:
……
JMP Label3
Db thunkcode3
Label1:
…….
JMP Label2
Db thunkcode2
Label3:
……
2.简单花指令
a.互补条件代替jmp跳转
{
Jz Label
Jnz Label
Db thunkcode;垃圾数据
Label:
}
b.跳转指令构造花指令
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL7;
jz LABEL8;
LABEL7:
_emit 0xC7;
LABEL8:
pop ebx;
}
jz LABEL8
,也就是说中间0xC7永远不会执行。通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支)。也可以调用某些函数会返回确定值,来达到构造永真或永假条件,ida和OD都被骗过去了。
push ebx
xor ebx,ebx
test ebx,ebx
jnz label1
jz label2
label1:
_emit junkcode
label2:
pop ebx//需要恢复ebx寄存器
}
__asm{
clc
jnz label1:
_emit junkcode
label1:
}
c.call&ret构造花指令
call LABEL9;
_emit 0x83;
LABEL9:
add dword ptr ss : [esp] , 8;
ret;
__emit 0xF3;
}
call指令的本质: push 函数返回地址
然后jmp 函数地址
ret指令的本质: pop eip
3.进阶花指令(自定义花指令)
a.替换ret指令
{
call LABEL9;
_emit 0xE8;
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0];
#将ebp的地址存放于eax
add dword ptr ss : [eax-0x50] , 26;
#该地址存放的值正好是函数返回值,
#不过该地址并不固定,根据调试所得。
#加26正好可以跳到下面的mov指令,该值也是调试计算所得
pop eax;
pop ebx;
pop eax;
jmp eax;
_emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax,dword ptr ss:[esp-8];
#将原本的eax值返回eax寄存器
}
call指令的本质: push 函数返回地址
然后jmp 函数地址
ret指令的本质: pop eip
b.控制标志寄存器跳转
c.利用函数返回确定值
LPCSTR lpLibFileName
);
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
4.花指令原理另类利用
{
Jz Label
Jnz Label
_emit 'h'
_emit 'E'
_emit 'l'
_emit 'L'
_emit 'e'
_emit 'w'
_emit 'o'
_emit 'R'
_emit 'l'
_emit 'D'
Label:
}
hElLowoRlD
嵌入到代码中,那我们只需要在当前进程中搜索hElLowoRlD
字符串,就可以定位到当前代码位置,然后对下面的代码进行SMC自解密。花指令 指令小结
jz jnz/jmp
_emit 075h #jmp $+4
_emit 2h
_emit 0E9h
_emit 0EDh
}
call ret
call+pop/add esp/add [esp] + retn
#include <windows.h>
void main()
{
DWORD p;
_asm
{
call l1
l1:
pop eax
mov p,eax//确定当前程序段的位置
call f1
_EMIT 0xEA//花指令,此处永远不会执行到
jmp l2//call结束以后执行到这里
f1://这里用F8OD会终止调试,F7跟进的话就正常,why?
pop ebx
inc ebx
push ebx
mov eax,0x11111111
ret
l2:
call f2//用ret指令实现跳转
mov ebx,0x33333333//这里永远不会执行到
jmp e//这里永远不会执行到
f2:
mov ebx,0x11111111
pop ebx//弹出压栈的地址
mov ebx,offset e//要跳转到的地址
push ebx//压入要跳转到的地址
ret//跳转
e:
mov ebx,0x22222222
}
cout<<hex<<p<<endl;
}
reference
实例
源码
void func1()
{
__asm
{
lea eax, lab1
jmp eax
_emit 0x90
};
lab1:
printf("func1\n");
}
void func2()
{
__asm
{
cmp eax, ecx
jnz lab1
jz lab1
_emit 0xB8
};
lab1:
printf("func2\n");
}
int main()
{
func1();
func2();
return 0;
}
花指令逆向分析
IDA有栈跟踪的功能,它在函数内部遇到ret(retn)指令时会做判断:栈指针的值在函数的开头/结尾是否一致,如果不一致就会在函数的结尾标注"sp-analysis failed"。 一般编程中,不同的函数调用约定(如stdcall&_cdcel call)可能会出现这种情况;另外,为了实现代码保护而加入代码混淆(特指用push/push+ret实现函数调用)技术也会出现这种情况。
六
花指令分析实操
1._emit 0xe8
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;
}
.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
// 使用 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;
}
2.看雪.TSRC 2017CTF秋季赛
第二题
看雪.TSRC 2017CTF秋季赛
第二题作为讲解. 题目下载链接:ctf2017_Fpc.exe(https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/anti-debug/2017_pediy/ctf2017_Fpc.exe)。scanf
函数, 用户输入的缓冲区只有0xCh
长,我们双击v1
进入栈帧视图。scanf
之前写的几个混淆视听的函数, 是一些简单的方程式但实际上是无解的。程序将真正的验证逻辑加花混淆, 导致 IDA 无法很好的进行反编译。所以我们这道题的思路就是,通过溢出转到真正的验证代码处继续执行。C
键 (code) 将这块数据反汇编成代码。0x00413131
,0x41
是'A'
的 ascii 码,而0x31
是'1'
的 ascii 码. 由于看雪比赛的限制, 用户输入只能是字母和数字, 所以我们也完全可以利用溢出漏洞执行这段代码。Ctrl+G
到达0x413131
处设下断点, 运行后输入12345612345611A
回车, 程序成功地到达0x00413131
处,然后右键分析->从模块中删除分析
识别出正确代码。0x413131
处后, 点击菜单栏的"查看"
, 选择"RUN跟踪"
, 然后再点击"调试"
, 选择"跟踪步入"
, 程序会记录这段花指令执行的过程, 如下图所示:0x413420
处的jnz ctf2017_.00413B03
. 我们就要重新来过, 并在0x413420
设下断点:0041362E jnz ctf2017_.00413B03
需要满足). 保证逻辑正确后, 将有效指令取出继续分析就好了。3._asm _emit 0E9 + 可执行花指令
reference
#include <stdint.h>
void encrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1], sum = 0, i; /* set up */
uint32_t delta = 0x9e3779b9; /* a key schedule constant */
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */
for (i = 0; i < 32; i++) { /* basic cycle start */
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
} /* end cycle */
v[0] = v0; v[1] = v1;
}
int main() {
int a = 1;
uint32_t flag[] = { 1234,5678 };
uint32_t key[] = { 9,9,9,9 };
encrypt(flag, key);
printf("%d,%d", flag[0], flag[1]);
return 0;
}
加入花指令
#include <stdint.h>
#define JUNKCODE __asm{
__asm jmp junk1
__asm __emit 0x12
__asm junk2:
__asm ret
__asm __emit 0x34
__asm junk1:
__asm call junk2
}
void encrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1], sum = 0, i; /* set up */
uint32_t delta = 0x9e3779b9; /* a key schedule constant */
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */
for (i = 0; i < 32; i++) { /* basic cycle start */
JUNKCODE
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
} /* end cycle */
v[0] = v0; v[1] = v1;
}
int main() {
int a = 1;
uint32_t flag[] = { 1234,5678 };
uint32_t key[] = { 9,9,9,9 };
__asm {
_emit 075h
_emit 2h
_emit 0E9h
_emit 0EDh
}
encrypt(flag, key);
printf("%d,%d", flag[0], flag[1]);
return 0;
}
去除方法
七
花指令练习
jz jnz/jmp
现在许多的花指令采用生成确定标志位并搭配两个互补的条件跳转指令替代一个强制跳转指令(如jmp)以增加汇编难度的方式。
其实如果一个程序出现下面互补跳转代码,而且跳转代码前某一个标志位一定是确定值,同时指令下方还出现报错的情况,基本可以断定是花指令(可以作为花指令起始或者存在的标志,是充分不必要条件)。
而且运行过程中必然不发生跳转的那一个指令的目标地址或者标号是干扰项。
花指令1
E8 --> call
test eax, eax //产生确定标志位
je LABEL1
jne LABEL2
LABEL2 :
/*干扰项所在*/
LABEL1:
/*正常代码*/
#include<windows.h>
int a = 0;
int main()
{
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result = 1;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
__asm {
xor eax, eax
test eax, eax
je LABEL1
jne LABEL2
LABEL2 :
_emit 0xe8
LABEL1 :
}
for (int i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
return 0;
}
retn
-->int __cdecl main
花指令2
#include<windows.h>
int a = 0;
int main()
{
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result = 1;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
__asm {
xor eax, eax
test eax, eax
je LABEL1
jne LABEL2
LABEL2 :
_emit 0x5e //与pop si机器码相同
and eax, ebx
_emit 0x50 //与push ax机器码相同
xor eax, ebx
_emit 0x74 //与汇编助记符 jz 机器码相同
add eax, edx
LABEL1 :
}
for (int i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
return 0;
}
0x5e
与pop si
机器码相同,0x50
与push ax
机器码相同,0x74
与汇编助记符jz
机器码相同。test eax, eax //ZF=1,是确定标志位【test不会将结果放在寄存器上,它只影响ZF的状态,如果EAX == 0,那么ZF = 1】
je LABEL1 //ZF=1时触发跳转到正常指令处LABEL1
jne LABEL2 //ZF=0时触发跳转到干扰项处LABEL2,与上面的指令形成互补跳转
LABEL2 :
/*干扰项所在*/
LABEL1:
/*正常代码*/
0x74
翻译成了jz
指令,致使这个指令的下一个本该翻译成指令(即add eax, edx)的机器码却被翻译成了跳转的地址,进而出现了后面的指令不正常反汇编的情况。IDC去花
jz short loc_45E421
的字节码如下图,我们只要取74 03
即可,因为0x74
代表jz助记符,而add reg16,reg16
的字节码刚好为0x03
,对应了add eax,eax
API
和语法写出如下idc脚本
static main()
{
auto StartVa, StopVa, Size, i;
StartVa=0x00411960;
StopVa=0x00411A27;
Size=StopVa-StartVa;
for (i=0; i<Size; i++){
if (Byte(StartVa)==0x74)
{
if(Byte(StartVa+1)==0x03)
{
PatchByte(StartVa, 0x90);
MakeCode(StartVa);
StartVa++;
Message("Find FakeJmp Opcode!!\n");
continue;
}
}
StartVa++;
}
Message("Clear FakeJmp Opcode Ok\n");
}
需要手动改成数据后再改成代码,最后申明函数。
call ret
需要先了解一下一些汇编转移指令 call
和ret
的原理
1、call指令
看不懂CS:IP的同学可以先去笔者的8086(https://dua0g.top/archives/327)汇编一文中学习,本文不再赘述。
这个指令是先将call + 标号的下一条语句的IP放入栈中,然后使当前的IP+16位位移,相当于:
jmp near ptr 标号
这个指令是先将call指令的下一个指令的代码段地址入栈,再把call下一条指令的偏移地址入栈,然后使其跳到标号所在处,相当于:
push IP
jmp far ptr 标号
这个指令先将call的下一条指令的IP入栈,然后再以寄存器的值为IP的代码处。
jmp reg16
这个指令的是先将call指令的下一条指令的IP入栈,然后跳到以内存单元地址为IP的代码处。
jum word ptr 内存单元地址
这个指令先将call指令的下一条指令的CS入栈,再将call指令的下一条指令的IP入栈,然后跳到以内存单元的高位为CS,低位为IP的代码处。
push IP
jmp dword ptr 内存单元地址
2、ret指令
pop IP
花指令1
#include<stdio.h>
#include<windows.h>
int a = 0;
int main()
{
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result = 1;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
__asm {
call LABEL9;
_emit 0x83;
LABEL9:
add dword ptr ss : [esp] , 8;
ret;
__emit 0xF3;
}
for (int i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
return 0;
}
push eax;
xor eax, eax;
test eax, eax;
jnz LABEL1;
jz LABEL2;
LABEL1:
_emit 0xE8; //与call助记符的机器码相同
LABEL2:
mov byte ptr[a], 0;
call LABEL3;
_emit 0xFF; //与adc助记符的字节码相同
LABEL3:
add dword ptr ss : [esp], 8;
ret;
__emit 0x11;
mov byte ptr[a], 2;
pop eax;
}
#include<stdio.h>
#include<windows.h>
int a = 0;
int main()
{
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result = 1;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
__asm {
push eax;
xor eax, eax;
test eax, eax;
jnz LABEL1;
jz LABEL2;
LABEL1:
_emit 0xE8;
LABEL2:
mov byte ptr[a], 0;
call LABEL3;
_emit 0xFF;
LABEL3:
add dword ptr ss : [esp] , 8;
ret;
__emit 0x11;
mov byte ptr[a], 2;
pop eax;
}
for (int i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
return 0;
}
push eax;
xor eax, eax;
test eax, eax;
jnz LABEL1;
jz LABEL2;
LABEL1:
_emit 0xE8; //与call助记符的字节码相同
LABEL2:
mov byte ptr[a], 0;
call LABEL3; //相当于push IP;jmp near ptr LABEL3
_emit 0xFF; //干扰项,与adc助记符的字节码相同
LABEL3:
add dword ptr ss : [esp], 8; //sp寄存器(栈顶寄存器)的值(IP)自增8
ret;
__emit 0x11; //干扰项
mov byte ptr[a], 2;
pop eax;
}
对一些疑问的回答:
ret
指令会将栈顶指针的值弹出到 IP 寄存器中,从而改变了代码执行的下一条指令。add dword ptr ss:[esp], 8
的作用是将栈顶指针加上 8,这实际上是跳过了_emit 0xFF
这个干扰项的地址。因此,ret
指令弹出的是mov byte ptr[a], 2
的地址,而不是干扰项的地址。由于ret
指令会将 IP 寄存器中的值作为下一条指令的地址,所以程序执行的下一条指令就变成了mov byte ptr[a], 2
。ret
指令的行为,程序在运行时成功避免了干扰项(_emit 0xFF
)的影响,实现了预期的逻辑。ret
指令会将栈顶指针的值弹出到 IP 寄存器中,从而改变了代码执行的下一条指令。mov byte ptr[a], 2
指令可以将值 2 移动到地址 a 所对应的内存位置上。该指令不会直接影响后续的代码逻辑,因为它只是修改了一个内存位置的值,并没有改变程序流程。反汇编分析。
回归到IDA分析部分
push eax
,然后紧跟着垃圾指令,所以推断最后势必有个pop eax
,所以完全可以以push eax
(0x50)为起始,pop eax
(0x58)为中止,中间的代码全部nop,从而达到去花的目的。IDC去花
/*匹配字符串的函数*/
static matchBytes(StartAddr, Match)
{
auto Len, i, PatSub, SrcSub;
Len = strlen(Match);
while (i < Len)
{
PatSub = substr(Match, i, i+1);
SrcSub = form("%02X", Byte(StartAddr));
SrcSub = substr(SrcSub, i % 2, (i % 2) + 1);
if (PatSub != "?" && PatSub != SrcSub) //以问号作为匹配函数中止条件
return 0;
if (i % 2 == 1) StartAddr++;
i++;
}
return 1;
}
static main()
{
auto Addr, Start, End, Condition, i;
Start = 0x411960; //起始地址
End = 0x11A3B; //中止地址
Condition = "5033C085C07502????"; //目标字符串
for (Addr = Start; Addr < End; Addr++) //遍历区域内字节码
{
if (matchBytes(Addr, Condition))
{
Message("Find FakeJmp Opcode!!\n");
for (i = 1; Byte(Addr+i)!=0x58; i++) //出现pop eax则停止patch
{
PatchByte(Addr+i, 0x90); //nop填充
MakeCode(Addr+i); //反汇编转代码
}
}
}
AnalyzeArea(Start, End);
Message("Clear FakeJmp Opcode Ok ");
}
花指令2
与花指令1类似
call LABEL9;
_emit 0x83;
LABEL9:
add dword ptr ss : [esp], 8;
ret;
__emit 0xF3;
}
配合裸函数的花指令(笔者未分析 转载)
此部分未经验证。
{
//55 8b ec 83
__asm
{
/*保留栈底*/
push ebp
/*开辟栈空间*/
mov ebp, esp
sub esp, 0x40//0x40是缓冲区大小
/*保留现场(寄存器状态)*/
push ebx
push esi
push edi
/*缓冲区写入数据*/
mov eax, 0xCCCCCCCC //0xCCCC在gb2312中是'烫'字
mov ecx, 0x10 //cx为下面填'烫'操作计数
lea edi, dword ptr ds : [ebp - 0x40]
rep stos dword ptr es : [edi]
}
/*执行的操作*/
*a = 1;
//花指令
_asm
{
call LABEL9;
_emit 0xE8;
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0]
add dword ptr ss : [eax - 0x50], 26;
pop eax;
pop ebx;
pop eax;
jmp eax;
__emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax, dword ptr ss : [esp - 8];
}
__asm
{
/*恢复现场*/
pop edi
pop esi
pop ebx
/*释放栈空间*/
mov esp, ebp
pop ebp
ret
}
}
无函数返回值的裸函数
中加花后的源程序源码如下:#include<stdio.h>
#include<windows.h>
int a = 0;
void __declspec(naked)__cdecl cnuF(int* a)//裸函数,开辟和释放堆栈由我们自己写。
{
//55 8b ec 83
__asm
{
/*保留栈底*/
push ebp
/*开辟栈空间*/
mov ebp, esp
sub esp, 0x40 //0x40是缓冲区大小
/*保留现场(寄存器状态)*/
push ebx
push esi
push edi
/*缓冲区写入数据*/
mov eax, 0xCCCCCCCC //0xCCCC在gb2312中是'烫'字
mov ecx, 0x10 //cx为下面填'烫'操作计数
lea edi, dword ptr ds : [ebp - 0x40]
rep stos dword ptr es : [edi] //用烫填充
}
/*执行的操作*/
*a = 1;
MessageBox(NULL, "加花指令8", "YQC", 0);
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result, i;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
/*花指令*/
_asm
{
call LABEL9;
_emit 0xE8; //垃圾指令
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0]
add dword ptr ss : [eax - 0x50], 26;
pop eax;
pop ebx;
pop eax;
jmp eax;
__emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax, dword ptr ss : [esp - 8];
}
for (result = 1, i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
__asm
{
/*恢复现场*/
pop edi
pop esi
pop ebx
/*释放栈空间*/
mov esp, ebp
pop ebp
ret
}
}
int main()
{
cnuF(&a);
return 0;
}
(一)反汇编代码比对与分析
加花的程序
的关键函数部分的反汇编代码。.text:004613F0
.text:004613F0 var_14 = byte ptr -14h
.text:004613F0 var_8 = byte ptr -8
.text:004613F0 arg_0 = dword ptr 8
.text:004613F0
.text:004613F0 push ebp
.text:004613F1 mov ebp, esp
.text:004613F3 sub esp, 40h
.text:004613F6 push ebx
.text:004613F7 push esi
.text:004613F8 push edi
.text:004613F9 mov eax, 0CCCCCCCCh
.text:004613FE mov ecx, 10h
.text:00461403 db 3Eh
.text:00461403 lea edi, [ebp-40h]
.text:00461407 rep stosd
.text:00461409 mov eax, [ebp+arg_0]
.text:0046140C mov dword ptr [eax], 1
.text:00461412 mov esi, esp
.text:00461414 push 0 ; uType
.text:00461416 push offset Caption ; "YQC"
.text:0046141B push offset Text ; "加花指令8"
.text:00461420 push 0 ; hWnd
.text:00461422 call ds:MessageBoxA
.text:00461428 cmp esi, esp
.text:0046142A call j___RTC_CheckEsp
.text:0046142F push offset aNM ; "请输入n和m:\n"
.text:00461434 call sub_45748D
.text:00461439 add esp, 4
.text:0046143C lea eax, [ebp+var_14]
.text:0046143F push eax
.text:00461440 lea ecx, [ebp+var_8]
.text:00461443 push ecx
.text:00461444 push offset aDD ; "%d%d"
.text:00461449 call sub_4584E6
.text:0046144E add esp, 0Ch
.text:00461451 call sub_46145B
.text:00461456 call sub_46145C
.text:00461456 sub_4613F0 endp ; sp-analysis failed
.text:00461456
.text:0046145B
.text:0046145B ; =============== S U B R O U T I N E =======================================
.text:0046145B
.text:0046145B
.text:0046145B sub_46145B proc near ; CODE XREF: sub_4613F0+61↑p
.text:0046145B push eax
.text:0046145B sub_46145B endp ; sp-analysis failed
.text:0046145B
.text:0046145C
.text:0046145C ; =============== S U B R O U T I N E =======================================
.text:0046145C
.text:0046145C
.text:0046145C sub_46145C proc near ; CODE XREF: sub_4613F0+66↑p
.text:0046145C push ebx
.text:0046145D db 3Eh
.text:0046145D lea eax, [ebp+0]
.text:00461461 add dword ptr ss:[eax-50h], 1Ah
.text:00461466 pop eax
.text:00461467 pop ebx
.text:00461468 pop eax
.text:00461469 jmp eax
.text:00461469 sub_46145C endp ; sp-analysis failed
.text:00461469
.text:0046146B ; ---------------------------------------------------------------------------
.text:0046146B call loc_461473
.text:0046146B ; ---------------------------------------------------------------------------
.text:00461470 db 36h, 8Bh, 44h
.text:00461473 ; ---------------------------------------------------------------------------
.text:00461473
.text:00461473 loc_461473: ; CODE XREF: .text:0046146B↑j
.text:00461473 and al, 0F8h
.text:00461475 mov dword ptr [ebp-20h], 1
.text:0046147C mov dword ptr [ebp-2Ch], 0
.text:00461483 jmp short loc_46148E
.text:00461485 ; ---------------------------------------------------------------------------
.text:00461485
.text:00461485 loc_461485: ; CODE XREF: .text:004614A0↓j
.text:00461485 mov eax, [ebp-2Ch]
.text:00461488 add eax, 1
.text:0046148B mov [ebp-2Ch], eax
.text:0046148E
.text:0046148E loc_46148E: ; CODE XREF: .text:00461483↑j
.text:0046148E mov eax, [ebp-2Ch]
.text:00461491 cmp eax, [ebp-14h]
.text:00461494 jge short loc_4614A2
.text:00461496 mov eax, [ebp-20h]
.text:00461499 imul eax, [ebp-8]
.text:0046149D mov [ebp-20h], eax
.text:004614A0 jmp short loc_461485
.text:004614A2 ; ---------------------------------------------------------------------------
.text:004614A2
.text:004614A2 loc_4614A2: ; CODE XREF: .text:00461494↑j
.text:004614A2 mov eax, [ebp-20h]
.text:004614A5 push eax
.text:004614A6 push offset aD_0 ; "结果为%d"
.text:004614AB call sub_45748D
.text:004614B0 add esp, 8
.text:004614B3 pop edi
.text:004614B4 pop esi
.text:004614B5 pop ebx
.text:004614B6 mov esp, ebp
.text:004614B8 pop ebp
.text:004614B9 retn
裸函数。
裸函数
对于一个裸函数而言,就是编译器不会为这个函数生成代码,如开辟和释放栈空间还有ret,这些指令在裸函数中都需要我们自己写,且最后一定不能缺少ret指令。
一般在函数名前面加上 __deplspec(naked),此时这个函数便是裸函数,同时编译器对裸函数也不会进行任何处理。
下面以实现两个传入参数相加的功能为例给出不同裸函数的基本框架(如果对这些指令不是很理解可以参考堆栈图):
(1)无参数无返回值的函数框架void __declspec(naked) Fun()
{
__asm
{
//提升堆栈
push ebp
mov ebp,esp
sub ebp,0x40
//保护现场
push ebx
push esi
push edi
//向缓冲区填充数据
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd ;rep stos dword ptr es:[edi]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回函数调用前的下一行地址
ret
}
}
(2)有参数有返回值的函数框架
int __declspec(naked) plus(int x, int y)
{
__asm
{
//提升堆栈
push ebp
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//向缓冲区填充数据
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stos dword ptr es:[edi]
//函数核心功能块
mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回函数调用前的下一行地址
ret
}
}
(3)带局部变量的函数框架
int __declspec(naked) plus(int x, int y)
{
__asm
{
//提升堆栈
push ebp
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//向缓冲区填充数据
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stos dword ptr es:[edi]
//局部变量入栈
mov dword ptr ds:[ebp-0x4]
mov dword ptr ds:[ebp-0x8]
//函数核心功能块
mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回函数调用前的下一行地址
ret
}
}
call LABEL9
处单步执行,注意观察,如下图。mov eax, dword ptr ss : [esp - 8];
(在OD中没有反汇编成功)的地址。mov eax, dword ptr ss : [esp - 8];
,绕过垃圾指令。(二)去花
jmp eax
下面的call
语句nop
掉jmp eax
的存在,反汇编引擎
不知道跳转的位置,所以部分代码会丢失,被分隔开,产生如下情况;jmp eax
用nop
填充掉,这样就可以F5了IDC去花
static matchBytes(StartAddr, Match)
{
auto Len, i, PatSub, SrcSub;
Len = strlen(Match);
while (i < Len)
{
PatSub = substr(Match, i, i+1);
SrcSub = form("%02X", Byte(StartAddr));
SrcSub = substr(SrcSub, i % 2, (i % 2) + 1);
if (PatSub != "?" && PatSub != SrcSub)
return 0;
if (i % 2 == 1) StartAddr++;
i++;
}
return 1;
}
static main()
{
auto Addr, Start, End, Condition, junk_len, i;
Start = 0x004613F0;
End = 0x004614B9;
Condition = "E805000000E801000000????";
for (Addr = Start; Addr < End; Addr++)
{
if (matchBytes(Addr, Condition))
{
Message("Find FakeJmp Opcode!!\n");
for (i = 0;!matchBytes(Addr+i,"368B44????"); i++)
{
PatchByte(Addr+i, 0x90);
MakeCode(Addr+i);
}
}
}
AnalyzeArea(Start, End);
Message("Clear Fake-Jmp Opcode Ok ");
and
0x21对应汇编and,原理跟0xe8相同,此部分不再过多讲解
花指令
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL5;
jz LABEL6;
LABEL5:
_emit 0x21; //与and助记符的机器码相同
LABEL6:
pop ebx;
}
#include<stdio.h>
#include<windows.h>
int a = 0;
int main()
{
//MessageBox(NULL, "未加花指令", "YQC", 0);
int n, m, result = 1;
printf("请输入n和m:\n");
scanf_s("%d%d", &n, &m);
__asm {
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL5;
jz LABEL6;
LABEL5:
_emit 0x21;
LABEL6:
pop ebx;
}
for (int i = 0; i < m; i++)
result *= n;
printf("结果为%d", result);
return 0;
}
IDC去花
xor ebx, ebx;test ebx, ebx;jnz short loc_45E417;jz short near ptr loc_45E417+1
前面有个push ebx
,那就以pop ebx
为中止,中间代码为花指令全部nop。static matchBytes(StartAddr, Match)
{
auto Len, i, PatSub, SrcSub;
Len = strlen(Match);
while (i < Len)
{
PatSub = substr(Match, i, i+1);
SrcSub = form("%02X", Byte(StartAddr));
SrcSub = substr(SrcSub, i % 2, (i % 2) + 1);
if (PatSub != "?" && PatSub != SrcSub)
return 0;
if (i % 2 == 1) StartAddr++;
i++;
}
return 1;
}
static main()
{
auto Addr, Start, End, Condition, junk_len, i;
Start = 0x0045E3A0;
End = 0x0045E480;
//"xor ebx, ebx;test ebx, ebx;jnz short loc_45E417;jz short loc_45E418"的字节码作为识别花指令的标识。
Condition = "33DB85DB7502740121????";
for (Addr = Start; Addr < End; Addr++)
{
if (matchBytes(Addr, Condition))
{
Message("Find FakeJmp Opcode!!\n");
Message(Addr);
for (i = 1; Byte(Addr+i)!=0x21; i++);
PatchByte(Addr+i, 0x90);
MakeCode(Addr+i);
}
}
AnalyzeArea(Start, End);
Message("Clear Fake-Jmp Opcode Ok ");
}
其余(随笔者做题更新)
{
__asm {
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL7;
jz LABEL8;
LABEL7:
_emit 0xC7;
LABEL8:
pop ebx;
}
a = 4;
}
return 1;
else
return 0;
_asm {
cmp eax, 0;
jc LABEL7_1;
jz LABEL7_2;
LABEL7_1:
_emit 0xE8;
LABEL7_2:
}
reference
八
编写IDC脚本自动化去除花指令
针对花指令较多的情况,建议采用idc脚本去花
MakeCode(ea) #分析代码区,相当于ida快捷键C
ItemSize(ea) #获取指令或数据长度
GetMnem(ea) #得到addr地址的操作码
GetOperandValue(ea,n) #返回指令的操作数的被解析过的值
PatchByte(ea, value) #修改程序字节
Byte(ea) #将地址解释为Byte
MakeUnkn(ea,0) #MakeCode的反过程,相当于ida快捷键U
MakeFunction(ea,end) #将有begin到end的指令转换成一个函数。如果end被指定为BADADDR(-1),IDA会尝试通过定位函数的返回指令,来自动确定该函数的结束地址
jz short loc_45E421
的字节码如下图,我们只要取74 03
即可,因为0x74
代表jz助记符,而add reg16,reg16
的字节码刚好为0x03
,对应了add eax,eax。
API
和语法写出如下idc脚本:
static main()
{
auto StartVa, StopVa, Size, i;
StartVa=0x00411960;
StopVa=0x00411A27;
Size=StopVa-StartVa;
for (i=0; i<Size; i++){
if (Byte(StartVa)==0x74)
{
if(Byte(StartVa+1)==0x03)
{
PatchByte(StartVa, 0x90);
MakeCode(StartVa);
StartVa++;
Message("Find FakeJmp Opcode!!\n");
continue;
}
}
StartVa++;
}
Message("Clear FakeJmp Opcode Ok\n");
}
需要手动改成数据后再改成代码,最后申明函数。
九
摘录
1.阅读
2.以下内容出自逆向工程入门指南(https://wizardforcel.gitbooks.io/re-for-beginners/content/Part-III/Chapter-50.html)
50.1 文本字符串(提供了隐藏文本字符串的思路)
mov byte ptr [ebx+1], ’e’
mov byte ptr [ebx+2], ’l’
mov byte ptr [ebx+3], ’l’
mov byte ptr [ebx+4], ’o’
mov byte ptr [ebx+5], ’ ’
mov byte ptr [ebx+6], ’w’
mov byte ptr [ebx+7], ’o’
mov byte ptr [ebx+8], ’r’
mov byte ptr [ebx+9], ’l’
mov byte ptr [ebx+10], ’d’
cmp byte ptr [ebx], ’j’
jnz fail
cmp byte ptr [ebx+1], ’o’
jnz fail
cmp byte ptr [ebx+2], ’h’
jnz fail
cmp byte ptr [ebx+3], ’n’
jnz fail
jz it_is_john
50.2 可执行代码
50.2.1 插入垃圾
mul ecx
add esi, eax ; garbage
add eax, ebx
mov edx, eax ; garbage
shl edx, 4 ; garbage
mul ecx
xor esi, ecx ; garbage
50.2.2 替换与原有指令等价的指令。
jmp label可以替换为 push label/ret这两条指令,IDA将不会显示被引用的label。
call label可以替换为push label_after_call_instruction/push label/ref这三条指令。
push op可以替换为 sub esp, 4(或者8)/mov [esp], op这两条指令。
50.2.3 绝对被执行的代码与绝对不被执行的代码
... ; some code not touching ESI
dec esi
... ; some code not touching ESI
cmp esi, 0
jz real_code
;fakeluggage
real_code:
mul ecx ; real code
add eax, esi ; opaque predicate. XOR, AND or SHL, etc, can be here instead of ADD.
50.2.4打乱执行流程
instruction 2
instruction 3
jmp ins1_label
ins2_label:
instruction 2
jmp ins3_label
ins3_label:
instruction 3
jmp exit
ins1_label:
instruction 1
jmp ins2_label
exit:
50.2.4使用间接指针:
message1 db ’hello world’,0
dummy_data2 db 200h dup (0)
message2 db ’another message’,0
func proc
...
mov eax, offset dummy_data1 ; PE or ELF reloc here
add eax, 100h
push eax
call dump_string
...
mov eax, offset dummy_data2 ; PE or ELF reloc here
add eax, 200h
push eax
call dump_string
...
func endp
50.3 虚拟机/伪代码
50.4 其它
50.5练习
十
结尾
看雪ID:Dua0g
https://bbs.kanxue.com/homepage-957393.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同
球分享
球点赞
球在看
点击阅读原文查看更多