-
-
[原创]逆向实操
-
发表于: 2023-2-5 23:23 20715
-
逆向实操
汇编
《汇编语言》,王爽
《天书夜读》邵坚磊等
《Intel汇编指令集手册》
基础
学习汇编的原则
- 不推荐写纯汇编程序,一般都是通过
_asm{}
方式嵌入部分汇编代码 - 学习汇编的目的是:解底层,调试,逆向分
.c
-编译->.s
-汇编→.o
(linux平台)/.obj
(windows平台)-链接->.elf
/.exe
C源程序(.c/.cpp)->汇编程序(.S) ->二进制(.exe/.dll/.sys或者.elf/.so)->加壳保护
冯.诺伊曼体系
- 图灵在理论上证明了计算机可以被制造出来
- 冯.诺伊曼将理论中的计算机变成现实--冯.诺伊曼体系(存储程序)
通用寄存器
(通用指的是可以存放任何数据,寄存器名字不区分大小写):- EAX,EBX,ECX, EDX
源变址目标变址寄存器
:变
表示ESI和EDI会自++/--- ESI
- EDl(S表示source,D表示Destination)
栈相关寄存器
:- SS(只要在16位系统才有用,在32/64位系统在SS存放的是个常数),
- ESP(栈顶),
- EBP(栈底)
代码段寄存器,程序指令寄存器:
- CS(代码段的起始地址),
- EIP(下一条要执行指令的地址)
数据段寄存器
:- DS(常与ESI寄存器结合使用)(data)
附加段寄存器
:- ES(常与EDI寄存器集合使用)(extra)
控制寄存器
:- CR0-CR3(control)
- CR0包括指示处理器工作方式的控制位,包含启用和禁止分页管理机制的控制位,包含控制浮点协处理器操作的控制位
- CR1被保留,供今后开发的处理器使用。
- CR2及CR3由分页管理机制使用。CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起员异常的线性地址保存在CR2中。操作系统中的页异常处理程序可以检查CR2的内容,从而查出线性地址空间中的哪一页引起本次异常.
- CR3用于保存页目录表页面的物理地址,因此被称为
PDBR
。
系统地址寄存器
:- GDTR
- LDTR
- IDTR,中断描述符表寄存器
- TR寄存器包含了当前正在CPU运行的进程的TSSD(任务段描述符)选择符
Flag标志寄存器
ZF
零标志,零标志ZF用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0;AF
辅助进位标志,运算过程中第三位有进位值,置AF=1,否则,AF=0;PF
奇偶标志,当结果操作数中偶数个"1",置PF=1,否则,PF=0;SF
符号标志,当结果为负时,SF=1;否则,SF=0。溢出时情形例外;CF
进/借位标志,最高有效位产生进位值,例如,执行加法指令的,MSB最高位)有进位,置CF=1;否,CF=0;OF
溢出标志,若操作数结果超出了机器能表示的范围,则产生溢出,置OF=1,否则,OF=0。
- 在win2000以上操作系统对于
ebx,esi,edi
拿来就用,没有进行保护和恢复
,如果你的程序中使用了这几个寄存器,请一定先压栈,用完后恢复。64位汇编寄存器
- 在64位系统中,寄存器的表示方法为:
- 寄存器作用一样,就是保存数据的位数增加了(16位ax->32位eax->64位rax),名字加一个
r
- 通用寄存器:rax, rbx,rcx, rdx
- 栈寄存器:rsp,rbp
- 寄存器作用一样,就是保存数据的位数增加了(16位ax->32位eax->64位rax),名字加一个
- r8,r9,r10,r11,r12,r13,r14,r15(这8个是64位汇编中新增加的)
-fastcall传递参数的寄存器
:(rdx,rcx,) r8, r9(arguments)Scratch寄存器
:(rbx),r12,r13, r14, r15(scratch-乱写),即可以随时改写的奇存器
汇编中的整数
- intel汇编:
- 1aH/0ffffh(后面加,16进制)
- 17O(8进制)
- 12D(10进制)
- 110B(2进制)
- C语言
- 0x(前面加,16进制)
- 0(8进制)
- (什么都没加,16进制)
- (C语言中不存在2进制表示方式)
CPU,寄存器,和内存之间的关系
内存寻址
16位汇编-Hello world
- demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | data segment ;这里定义一个数据段 str db 'hello world$’ ;这里用内存存放字节数据 "hellow world" ,$用来判断字符串是否输出完毕 data ends ;数据段的结束标志 code 'segment' ;这里定义了一个代码段 assume cs:code, ds:data ;程序中定义的段与对应的段寄存器关联起来 start: ;start标号是程序的开始位置 mov ax,data mov ds,ax ;这里把数据段的地址放到数据段寄存器ds中 lea dx, str ;dx中放将要显示数据的偏移地址 mov ah, 9h ;al(ax低 8 位) ah(ax高 8 位) int 21h ;调用 21 号中断的 9 号功能来显示字符串 mov ah, 4ch int 21h ;程序返回 code ends ;代码段的结束语 end start ;定义程序从哪个标号处开始执行 |
- 16位汇编开发编译平台:masm,dosbox
- 安装masm包(含masm,link,debug等工)
c:\masm
- 安装并启动dosbox0.74
mount c: c:\masm5
- 切换到masm安装路径:直接输入
c:
- 编辑a.asm汇编代码
- 编译
masm a.asm
Link a.obj
debug a.exe
- 安装masm包(含masm,link,debug等工)
- VC中开发调试汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | / / x86 int_tmain( int argc,_TCHAR * arg[]) { char str [] = "hello world" ; char format [] = "%s \n" ; / / printf( "%s\n" , str ) __asm { LEA eax, str PUSH eax LEA eax, format PUSH eax Mov ebx,[pintf] call ebx add esp, 8 / / 栈平衡,入栈时是 - 8 ,因为栈是从高地址向地址增长 } return 0 ; } / / x64中: 需要把汇编独立写在一个文件中 |
汇编指令
- 汇编指令分类
- 传送指令:mov/lea/push/pop
- 算术指令
- 逻辑指令
- 串操作指令
- 控制转移指令(jmp,call,int)
- 处理机控制指令
- 汇编指令操作对象:立即数(常量),内存,寄存器
- 汇编指令构成
- 1.汇编指令:机器码助记符,有对应的机器码,比如mov指令等;
- 2.伪指令:无对应的机器码,编译器执行比如
assume
语句; - 3.其他符号:
+,-,*,/
,由编译器识别,无对应机器码。
- 常见机器码:
- 1.指令集
- RISC指令集(reduced instruction set computer,简单的指令集)只提供很有限的操作,基本上单周期执行每条指令,其指令长度也是固定(一般4个字节)。
- CISC指令(complex instruction set computer,复杂的指令集)复杂丰富,
功耗大
,长度不固定(1到6个字节)
- 2.Load-Store 结构
- 在RISC 中,
CPU并不会对内存中的数据进行操作
,所有的计算都要求在寄存器中完成。而寄存器和内存的通信则由单独的指令来完成。 - 而在CISC中,
CPU是可以直接对内存进行操作的。
- 在RISC 中,
- 3.更多的寄存器
- 和CISC相比,基于RISC的处理器有更多的通用寄存器可以使用,且每个寄存器都可以进行数据存储或者寻址。
- 4.RISC指令集能够非常有效地适合于采用流水线、超流水线和超标量技术,从而实现
指令级并行
操作,提高处理器的性能。 - 5.x86是cisc代表,arm,Macintosh是risc代表
- 6.CISC存在的问题:指令系统庞大,指令功能复杂,指令格式、寻址方式多;执行速度慢;难以优化编译,编译程序复杂;80%的指令在20%的运行时间使用;无法并行;无法兼容;
传送指令
mov
- MOV DST,SRC
- MOV Reg/Mem, Reg/Mem/lmm
- Reg-Register(寄存器)
- Mem-Menory(存储器)
- lmm-lmmediate(立即数)
- eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ;段寄存器之间不能用MOV指令直接传送 mov eax, 5 mov ds,eax ;正确 mov ds,es ;错误,据说是设计寄存器的时候电路不同,电路上不通 mov ds, 5 ;错误 mov ax,word ptr value mov eax,dword ptr value mov al,byte ptr value 这里的word ptr / dword ptr / byte ptr实际上是指获取value的宽度(分别对应 2 字节, 4 字节和 1 个字节 MOV EAX,DWORD PTR SS:[EBP + 1C ] ;把 ebp - 1C 地址存放的值给eax, ;类似C语言中的DWORD * p = EBP - 1C ;eax = * p; MOV DWORD PTR DS:[ESI + 1C ],EAX; ;类似C语言中DWORD * p = ESI + 1C ;eax = * p; |
PUSH/POP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | push reg / mem / seg ;ss:[sp] = reg / mem / seg; sp = sp - 2 pop reg / seg / mem ;sp = sp + 2 ; reg / seg / mem = ss:[sp] ;PUSH进栈指令 格式为:PUSH SRC push eax ;等价于mov [esp],eax;sub esp 4 ;POP出栈指令 格式为:POP DST pop eax ;等价于add esp 4 ;mov eax,[eax] push AX PUSH [ 2000H ] PUSH CS POP AX POP[ 2000H ] POP SS |
lea: load effective address
1 2 3 4 5 6 | mov eax,[ 00400000 ];DWORD * p = 00400000 ; eax = * p lea eax,[ 00400000 ];eax = 00400000 LEA EAX,[EBX + ECX * 2 + 1 ] ;eax = ebx + ecx * 2 + 1 mov eax, ebxtecx * 2 + 1 ;不能这样写,mov不能跟表达式 mov eax,[ebx + edx + 1 ] ;DWORD * p = ebx + edx * 2 + 1 ;eax = * p |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int val = 10 ; char * s = "hello world" ; int a = { 1 , 2 , 3 , 4 , 5 }; __asm { lea eax,val / / 等同lea eax,[val],都把val的地址放到eax寄存器 mov eax,val / / 等同mov eax,[val],都是把val的值放到eax寄存器 mov eax,s / / 等同于mov eax,[s],都是把s的值放到eax lea eax,s / / 等同于lea eax,[s],都是把s的地址放到eax中 lea eax,[ebx + edx + 1 ] mov eax,[ebx + edx + 1 ] lea eax,a / / 把数组首地址存放eax mov ebx,a / / 把a[ 0 ]存放到ebx mov ecx,[ea×] / / 把eax地址的内存值 lea edx,[eax] / / 把eax直接放入edx mov edx,[eax] / / 把eax地址的内存值 } |
标志寄存器操作
- 用来备份和恢复标志寄存器
LAHF
(Load AH with flags)flags送AH- 用于将标志寄存器的低八位送入AH,即将标志寄存器FLAGS中的SF、ZF、A PF、CF五个标志位分别传送到AH的对应位(八位中有三位是光效的)
SAHF
(store AH into flags)AH送标志寄存器PUSHF
(push the flags)标志进栈push eflagsPOPF
(pop the flags)标志出栈pop eflags算术指令
ADD
(add)加法
1 2 3 4 | add eax, 1 ;eax = eax + 1 add eax,ebx ;eax = eax + ebx add Leax],ebx ; * eax = * eax + ebx add eax,[ebx] ;eax = eax + * eb |
ADC
(add with carry)带进位加法
1 2 3 4 5 6 7 8 9 | adc eax, 1 ;eax = eax + 1 + CF ;add和adc是否有区别取决于在计算过程中cf寄存器是否为 1 xor eax,eax add eax, 1 xor eax,eax adx eax, 1 ;两者没有区别 |
INC
(increment)加1DEC
(Decrement)减1
1 2 | inc eax ; eax = eax + 1 dec eax ; eax = eax - 1 |
SUB
(subtract)减法SBB
(subtract with borrow)带借位减法- (DST)=(DST)-(SRC)-CF,其中CF为进位的值
1 2 3 4 5 6 7 8 | sub eax, ebx ;eax = eax - ebx sbb eax, 5 ;eax = eax - 5 - CF ;ADC: 同下 ;SBB:将DX:AX中存放的 32 位无符号数减去BX尚的 16 位无符号数 SUB AX,BX ;结果的低 16 位,如果AX小于BX将产生借位,导致CF = 1 SBB DX, 0 ;高 16 位 - CF,若前一步出现借位,则据此调整高 16 位的内容 |
NEG
(Negate)求补(把正变负,把负变正)
1 | neg eax ;eax = 0 - eax |
CMP
(Compare)比较,相减
1 2 3 4 5 6 7 8 9 | cmp eax, 5 ;eax - 5 ,只影响标志寄存器,不会修改eax的值 jz L1 ;为 0 jnz L2 ;不为 0 L1: ... L2: ... |
MUL
(Unsigned Multiple)无符号数乘法IMUL
(Signed Multiple)带符号数乘法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ;imul 1 个操作数 imul al ;ax = al * al imul bx ;dx:ax = ax * bx imul ebx ;edx:eax = eax * ebx imul dword ptr [ebx] ;edx:eax = eax * [ebx] ; 5 * 6 mov eax, 5 ; mov ebx, 6 ; imul ebx ;edx:eax = eax * ebx ;imul 2 个操作数 imul al r / m8 ;ax = al * r / m8 ;imul 3 个操作数 imul r32,r / m32,imm32 ;r32 = r / m32 * imm32 |
-DIV
(Unsigned divide)无符号数除法
1 2 3 4 5 6 7 | div ebx ; eax = (edx:eax) / ebx edx = (edx:eax) % ebx ; 100 / 2 xor edx, edx ;高 32 位置 0 mov eax, 100 mov ebx, 2 div ebx |
IDIV
(Signed divide)带符号数除法- 与 DIV一样的
- 与 DIV一样的
CBW
(Convert byte to word)字节转换为字CWD
(Contert word to double word)字转换为双字
1 2 3 4 5 6 | ;用符号位去填充 mov al 10001101B ; CBW al ; ax = 11111111 10001101B mov ax 12H ; CBW ax ; eax = FF12H |
用汇编实现+-*/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | int add_asm( int x, int y) { __asm { mov eax,x mov ebx,y add eax,ebx / / 函数返回值是放在eax中的 } } int sub_asm( int x, int y) { __asm { mov eax,x sub eax,y / / 函数返回值是放在eax中的 } } int imul_asm( int x, int y) { __asm { mov eax,x mov ebx,y imul ebx / / 函数返回值是放在eax中的,edx的值怎么返回?假设不会结果不会超过 4Byte , / / 如果超过 4Byte ,在函数中定义多一个,传入参数的指针,edx部分可以存放在指针指向的变量来返回 } } int idiv_asm( int x, int y, int * r) { __asm { xor edx, 0 mov eax,x idiv y / / (edx:eax) / y - - >q - - >eax,r - - >edx mov ebx,r mov [ebx],edx / / r } } |
逻辑指令
逻辑(位)运算
- AND(and) 与
- OR(or) 或
- NOT(not) 取反
- XOR(exclusive or) 异或
1 2 3 4 5 | and eax, 1 ;eax = eax& 1 or eax, 1 ;eax = eax| 1 not eax ;eax = ~eax xor eax,eax ;eax = eax^eax xor eax,ebx ;eax = eax^ebx |
TEST
(test) 测试:(DST)&(SRC)
test两个操作数相与
的结果不保存,只根据其特征置条件码(影响EFLAGS寄存器ZF位,配合jz/jnz等条件跳转指令)
1 2 3 4 5 6 7 8 | Mov eax, 2 Test eax, 1 ; 10 & 01 = 0 jz L1 L1: xor eax,eax L2: mov eax, 1 |
移位运算
- SHL(shift logical left) 逻辑左移
- SAL(shift arithmetic left) 算术左移,与shl一样
- SHR(shift logical right) 逻辑右移
- SAR(shift arithmetic right) 算术右移
- ROL(Rotate left) 循环左移
- ROR(Rotate right) 循环右移
- RCL(Rotate left through carry) 带进位循环左移
- RCR(Rotate right through carry) 带进位循环右移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ;格式: SHL OPR,CNT(其余的类似) ;其中OPR可以是除立即数以外的任何寻址方式。移位次数由CNT决定,CNT需要放在CL(CX寄存器的低 8 位)寄存器中 mov al 00110011B mov cl, 3 shl al cl ; al = 11001100B 逻辑左移 sal al cl ; al = 11001100B 算术左移(C语言中对应的左移) shr al cl; al = 00001100B 逻辑右移(左边用 0 填充) sar al cl ; al = 11001100B 算术右移(左边用符号位填充)(C语言中对应的右移) shr al cl ; al = 10011001B 循环左移 shr al cl ; al = 01100110B 循环右移 mov cl, 1 clc ; CF = 0 mov al, 88h ; CF,AL = 0 10001000b rcl al,cl ; CF,AL = 1 00010000b rcl a1,cl ; CF,AL = 0 00100001b ;rcr 同理 |
用汇编实现算术左/右移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | / / / x<<y int sal_asm( int x, int y) { __asm { mov eax,x mov cl,byte ptr y / / 不会超过 127 的,目前 128 位系统并不常见 sal eax,cl } } / / / x>>y int sar_asm( int x, int y) { __asm { mov eax,x mov cl,byte ptr y / / 不会超过 127 的,目前还没有 128 位系统比不通用 sar eax,cl } } |
串操作指令
movs
MOVS BYTE PTR[DI],BYTE PTR[SI]
(双字)- MOVSB(字节)
- MOVSW(字)
- 执行的操作:
- 1.((DI))<-((SI))
- 2.当方向标志DF=0时用+,当方向标志DF会1时用-,默认
DF=0
。 - a.字节操作
(SI)++或者(SI)--
(DI)++或者(DI)-- - b.字操作:
(SI)+=2或者(SI)-=2
(DI)+=2或者(DI)-=2 - c.双字操作
(SI)+=4或者(SI)-=4
(DI)+=4或者(DI)-=4
REP MOVS BYTE PTR[EDI],BYTE PTR[ESI]
- REP执行的操作
- 1.如(ECX)=0则退出REP,否则往下执行。
- 2.(ECX)--
- 3.执行其中的串操作
- 4.重复1~3
CLD
(Clear direction flag)该指令使DF=0,在执行串操作指令时可使地址自动++;STD
(Clear direction flag)该指令使DF=1,执行串操作指令时可使地址自动--。CLD
和STD
指令不影响条件码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | / / / 实现串拷贝 void strncpy_asm(char * dst,char * src,size_t len ) { __asm { / / mov edi,dst; / / mov esi,src; / / mov ecx, len ; mov edi,[ebp + 8 ] / / 目标地址 mov esi,[ebp + 0xc ] / / 源地址 mov ecx,[ebp + 0x10 ] / / ecx里rep的次数 cld / / 设置DF = O,(ESI),(EDI)就会自动 + + rep movs byte ptr[edi],byte ptr[esi] / / rep表示每执行这条指令后ecx - - ,直到ecx的值变为 0 ,才停止重复执行 / / rep movsb [edi],[esi] } } / * * * 低 函数参数右往左次入栈 / \ * | |局部变量| | * | - - - - - - - < - - - ebp | * | |老ebp| | * | - - - - - | * | |老eip| | * 内 - - - - - < - - - ebp + 8 栈 * 存 |dst| 增 * 增 - - - - - < - - - ebp + ch 长 * 长 |src| 方 * 方 - - - - - < - - - ebp + 10h 向 * 向 | len | | * | - - - - - < - - - | * \ / * 高 * / |
stos
- STOS存入串指令(store string):把eax(ax,al)存放在[edi]
STOS DST
(双字)- STOSB(字节)
- STOSW(字)
- 执行的操作:
- 1.((DI))=(AL)
- 2.当方向标志DF=0时用+,当方向标志DF会1时用-,默认
DF=0
。 - a.字节操作:(DI)+=1或者(DI)-=1
- b.字操作: (DI)+=1或者(DI)+=2
- c.双字: (DI)+=4或者(DI)+=4
- 该指令把AL或AX的内容存入由(DI指定的附加段的某单元中,并根据
DF
的值及数据类型修改DI的内容,当它与REP联用时,可把AL或AX的内容存入1个长度为CX)的缓冲区中。
1 2 3 4 5 6 7 | / / 例子:函数开头汇编部分理解 / / 局部变量初始化成CCCCCCCCh,即 int 3 指令(断点),起保护作用 / / 在debug版本,如果程序出问题(eip指向了局部变量空间),执行int3指令,程序就会断下来,防止程序继续执行破坏系统。 lea edi,[ebp - 0C0h ] ; 12 * 16 = 192Byte mov ecx, 30h ; 30h * 4Btye = c0h mov eax, 0CCCCCCCCh ; int 3 rep stos dword ptr es:[edi] ;ecx - 1 ,edi + 4 |
lods
- LODS 从串取指令(load string):把中的值放到eax(ax,al)中
- LODS SRC(双字)
- LODSB(字节)
- LODSW(字)
- 执行的操作:
- 1.(AX)<-((SI))
- 2.当方向标志DF=0时用+,当方向标志DF会1时用-,默认
DF=0
。 - a.字节操作:(SI)+=1或者(SI)-=1
- b.字操作: (SI)+=1或者(SI)+=2
- c.双字: (SI)+=4或者(SI)+=4
- 该指令把由(SI)指定的数据段中某单元的内容送到AL或AX中,并根据方向标志及数据类型修改SI的内容。指令允许使用段跨越前缀来指定非数据段的存储区。该指令也不影响条件码。一般说来,该指令不和REP联用。有时缓冲区中的一串字符需要逐次取出来测试时,可使用本指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / / lods实现strlen函数 unsigned int strlen_asm(char * s) { __asm { xor eax,eax xor ebx,ebx mov esi,s L0: lodsb / / 把(esi)中一个字节存放到al中,esi + + test al,al / / al&al 判断(esi)是否已经指向字符串s的 '\0' jz L1; INC ebx / / ebx寄存器存放着非零字符个数 jmp L0 L1: mov eax,ebx / / 返回值存放在eax } } |
cmps和scas
- CMPS和SCAS指令与REPE/REPZ和REPNZ/REPNE联合工作
REPE/REPZ
当相等/为零时重复串操作
格式:REPE(或REPZ) cmps/scas- 执行的操作:
- 1.如(CX)=0或ZF=0(即某次比较的结果两个操作数不等)时退出,否则
(cx)!=0&&zf==1
往下执行 - 2.(CX)-=1
- 3.执行其后的串指令
- 4.重复1)~3)
- 1.如(CX)=0或ZF=0(即某次比较的结果两个操作数不等)时退出,否则
REPNE/REPNZ
当不相等/不为零时重复串操作
街- 格式:REPNE(或RERNZ) cmps/scas
- 执行的操作:
- 1.
(cx)!=0&&zf==0
继续执行,否则(CX=0)或ZF=1退出 - 其他操作与REPE完全相同。
- 1.
- CMPS串比较指令:把[esi]和[edi]作差比较
- 格式: CMPS SRC, DST(双字)
- CMPSB(字节)
- CMPSW(单字)
- 执行的操作:
- 1.((SI))-((DI))
- 2.当方向标志DF=0时用+,当方向标志DF会1时用-,默认
DF=0
。 - a.字节操作:(SI)+=1,(DI)+=1或者(SI)-=1,(DI)-=1
- b.字操作:(SI)+=2,(DI)+=2或者(SI)-=2,(DI)-=2
- c.双字: (SI)+=4,(DI)+=4或者(SI)-=4,(DI)-=4
- 指令把由(Sl)指向的数据段中的一个字(或字节)与由(DI)指向的附加段中的一个字(字节)相减,但不保存结果只根据结果设置条件码,指令的其他特性和MOVS指令的规定相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | / / / strncmp int strncmp_asm(const char * s1, const char * s2, size_t len ) { __asm { mov ecx, len mov esi, s1 mov edi, s2 repz cmpsb / / 当ecx不为零并且zf = 1 ([esi] - [edi] = = 0 ),重复执行 jecxz L1 / / 前 len 个字节比较完毕, len - 1 字符相等 js L2 / / ecx不为 0 就退出,说明前 len 个字符就已经发现不相等的字符了 jns L2 L1: js L2 / / 负数,最后一个字符不等 jns L2 / / 非负,最后一个字符相等 xor eax, eax / / 最后一个字符相等 jmp L4 L2: xor eax, eax xor ebx, ebx mov al,byte ptr [esi - 1 ] / / 最后不相等的字符 mov bl,byte ptr [edi - 1 ] sub eax, ebx jmp L4 L4: } } |
- SCAS串扫描指令:把[eax]和[edi]做差比较
- 格式:SCAS DST(双字)
- SCASB(字节)
- SCASW(字)
- 执行的操作:
- 1.(AL)-((DI))
- 2.当方向标志DF=0时用+,当方向标志DF会1时用-,默认
DF=0
。 - a.字节操作:(DI)+=1或者(DI)-=1
- b.字操作:(DI)+=2或者(DI)-=2
- c.双字: (DI)+=4或者(DI)-=4
- 该指令把AL(或AX)的内容与由(DI)指定的在附加段中的一个字节(或字)进行比较,并不保存结果,只根据结果置条件码。指令的其他特性和MOVS的规定相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / / / strcmp int strcmp_asm(const char * s1, const char * s2) { __asm { xor eax,eax mov esi,s1 mov edi,s2 L2: lodsb / / (al) < - - byte ptr[esi] scasb / / (al) - byte ptr[edi] jne L1; test al,al / / 判断是否为 '\0' jne L2 / / 不是 '\0' ,到L2继续比较 xor eax,eax jmp L3; L1: xor ebx,ebx mov bl,byte ptr[edi - 1 ] sub eax,ebx L3: } } |
控制转移指令
跳转指令分三类:
1 2 3 4 5 6 7 | short jump:eb near jump:e9 far jump:ea jmp offset(relative = addr - current) jmp address |
- 1.短转移(8位)
- 格式:JMP SHORT QPR;
- -128--127
- 2.近转移(16位)
- 格式:JMP NEAR PTR OPR
- 同一个段内-32768至32761
- 3.段间跳转(32位)
- 根据CX、ECX寄存器的值跳转:
- JCXZ(CX为0则跳转)
- JECXZ(ECX也为0则跳转);
- LOOP循环指令
- 格式:LOOP OPR
- 测试条件:
(CX)!=0
- 和
rep
指令类似
- LOOPZ/LOOPE当为零或相等时循环指令
- 格式:LOOPZ(或LOOPE)OPR
- 测试条件:
(CX)!=0&&ZF==1
- 和
repz
指令类似
- LOOPNZ/LOOPNE当不为零或不相等时循环指令
- 格式:LOOPNZ(或LOOPNE)OPR
- 测试条件:
(CX)!=0&&ZF==0
- 和
repnz
指令类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | / / 计算 2 ^n需要n - 1 条重复的指令add ax,ax / / 2 ^index,类似报纸对折 unsigned int power_2(unsigned int index) { __asm { mov eax, 2 mov edx, index sub edx, 1 mov ecx, edx; s: add eax, eax loop s } } |
3.根据EFLAGS寄存器的标志位跳转
- EFLAGS寄存器的变化依赖
算术指令
、 逻辑指令(带进位循环左/右移
)、串操作指令(cmps和scas
)- JE:等于则跳转
- JNE:不等于则跳转
- JZ:为0则跳转
- JNZ:不为0则跳转
- JS:为负则跳转
- JNS :不为负则跳转
- JC:进位则跳转
- JNC:不进位则跳转
- JO:溢出则跳转
- JNO:不溢出则跳转
- A:above,无符号
- JA:无符号大于则跳转
- JNA:无符号不大于则跳转
- JAE:无符号大于等于则跳转
- JNAE:无符号不大于等于则跳转
- G: great,有符号
- JG:有符号大于则跳转
- JNG:有符号不大于则跳转
- JGE:有符号大于等于则跳转
- JGE:有符号不大于等于则跳转
- B: below,无符号小于
- JB:无符号小于则跳转
- JNB:无符号不小于则跳转
- JBE:无符号小于等于则跳转
- JNBE:无符号不小于等于则跳转
- L:less,有符号小于
- 子程序
- CALL调用指令(段内,段间,涉及到CS和IP入栈)
1 | call printf / / 在扁平模型下只需要push eip |
- RET返回指令
RETN
/RETF
- 在扁平模型下不会涉及到CS寄存器,所以不考虑这种情况,忽略RETF即可
- N表示near ,retn等价于
pop eip
- F表示far,retn等价于`pop eip;pop ecs``
RETN 操作数
/RETF 操作数
- 带有操作数的RETN/RETF指令则是在POP之后,执行
ESP=ESP+操作数
- 带有操作数的RETN/RETF指令则是在POP之后,执行
1 2 3 4 5 6 7 | ret / / cdecl,由调用者去负责栈平衡 / / pop eip ret 8 / / stdcall,fastcal / / pop eip / / add esp, 8 / / 被调用者栈平衡 |
RET
既有可能是retn,也有可能是retf,由编译器决定。
- 中断INT指令
调用:INT TYPE
返回:INT iret
处理机控制指令
- 标志处理指令
CLC
(Clear carry)进位位置0指令CF P=0CMC
(Complement carry)进位位求反指令~CFSTC
(Set carry)进位位置1指令CF=1CLD
(Clear direction)方向标志置0指令DF=0STD
(Set direction)方向标志置1指令DF=1CLI
(Clear interrupt)中断标志置0指令IF=0STI
(Set interrupt)中断标志置1指令IF=1
- 其他处理机控制指令
NOP
(No Opreation)无操作0x90HLT
(Halt) 停机
-WAIT
(Wait) 等待
INTEL和AT&T汇编的区别
- windows使用的是intel汇编
- Linux使用的是AT&T汇编
- intel和AT&T都属于x86汇编,属于CISC
- Android使用的是arm汇编,属于RISC
- 寄存器前面加
%
- 数据流动方向相反
- 操作数据的长度体现在指令的操作码中
[]
变成()
- 地址+offset寻址
- 常数在括号外
- 乘法体现在操作数的次序上
- 立即数
1 2 3 4 5 6 7 8 9 10 | / / 立即数 / / intel moveax, 8 mov ebx,Offffh int 80h / / at&t movI $ 8 , % eax movl $ 0xffff , % ebx int $ 0x80 |
花指令
花指令是程序中的无用指令
或者垃圾指令
,故意干扰各种反汇编静态分析工具,但是程序不受任何影响,缺少了它也能正常运行。加花指令后,IDA Pro
等分析工具对程序静态反汇编时,往往会出现错误或者遭到破环,加大逆向静态分析的难度,从而隐藏自身的程序结构和算法从而较好的保护自己。
1 2 3 4 5 6 7 8 | / / 花指令 1 / / 代码没有任何作用,但会混淆调试者 push edx pop edx inc ecx dec ecx add esp, 1 sub esp, 1 |
1 2 3 4 | / / 花指令 2 jmp Labe1 db opcode / / 在jmp后面加上一个字节的机器码,并不完整(完整的汇编指令是一个机器码 + 操作数),但不影响程序的执行(jmp会跳过这条残缺的汇编指令)。在IDE工具反汇编的时候,看到机器码(以为紧接着的就是操作数,接着后面的反汇编都错位了) Label1: |
但IDE采用的反汇编算法是递归如果没有指令调整到这个位置,就拒绝对这条指令进行反汇编。
1 2 3 4 5 | / / 花指令 3 jz Label / / 花指令对代码依旧没有影响,为 0 的时候会跳过花指令。 jnz Label / / 非 0 才跳转到 Label,为 0 则执行下面的花指令,IDE此时就认为这条花指令需要反汇编。 db opcode Label: |
C语言对应的底层汇编本质
栈的4种形态
- FD:Full Descend Stack满递减栈
- ED:Empty Descend Stack空递减栈
- FA:Full Ascend Stack 满递增栈
- EA:Empty Ascend Stack空递增栈
满递减栈(FD)
满
:栈顶指针指向最后一个入栈的有效
数据项,递减
:栈往低地址生长,栈的增长方向和内存的增长方向相反- 入栈时:栈顶指针
先减
,再存数据; - 出栈时:先取数据,栈顶指针
再加
; x86
和arm
都是满递减栈,所以在这种情况下:
1 2 3 4 5 6 7 8 9 | push eax / / 等价于: sub esp 4 mov [esp],eax pop eax / / 等价于 mov eax,[esp] add esp, 4 |
空递减栈(ED)
空
:栈顶指针指向下一个入栈的有效
数据项,递减
:栈往低地址生长,栈的增长方向和内存的增长方向相反- 入栈时:先存数据,栈顶指针
再减
; - 出栈时:栈顶指针
先加
,再取数据;
1 2 3 4 5 6 7 8 9 | push eax / / 等价于: mov [esp],eax sub esp 4 pop eax / / 等价于 add esp, 4 mov eax,[esp] |
满递增栈(FA)
满
:栈顶指针指向最后一个入栈的有效
数据项,递增
:栈往高地址生长,栈的增长方向和内存的增长方向相同- 入栈时:栈顶指针
先加
,再存数据; - 出栈时:先取数据,栈顶指针
再减
;
1 2 3 4 5 6 7 8 9 | push eax / / 等价于: add esp 4 mov [esp],eax pop eax / / 等价于 mov eax,[esp] sub esp, 4 |
空递增栈(EA)
满
:栈顶指针指向下一个入栈的有效
数据项,递增
:栈往高地址生长,栈的增长方向和内存的增长方向相同- 入栈时:先存数据,栈顶指针
再加
; - 出栈时:栈顶指针
先减
,再取数据;
1 2 3 4 5 6 7 8 9 | push eax / / 等价于: mov [esp],eax add esp 4 pop eax / / 等价于 sub esp, 4 mov eax,[esp] |
X86系统中函数调约定
- cdecl:
- 参数从右往左依次入栈,栈对齐
- 调用者栈平衡(程序员自己负责栈平衡)
- 变参函数(比如printf)必须要使用
cdecl
调用约定,因为被调用者是不知道函数的参数的个数的,只有调用者才知道,必须要调用者自己负责栈平衡。
- stdcall
- 参数从右往左依次入栈(栈对齐)
- 被调用者栈平衡(函数本身)
- 驱动中使用的是
stdcall
- fastcall
- 前两个参数放入ecx,edx,后面参数从右往左依次入栈(栈对齐),比如函数有5个参数,后面的第3,4,5参数从右往左依次入栈,第2参数放入edx,第1参数放入ecx。
- 被调用者栈平衡
- 在x64采用的都是fastcall,而且增加了r8,r9寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | / * * * 函数参数右往左次入栈,栈对齐,参数大小会提升到 4 个字节,比如char,short 1Byte / 2Byte 数据存放在 4Byte 的空间中 * 在printf中 float 会提升到double( 4Byte - > 8Byte ) * * 低 - - - - - - - - - < - - - esp * | |局部变量区| / \ * | - - - - - - - - - - < - - - ebp | * | | 老ebp | | * | - - - - - - - - - - | * | | 老eip | | * 内 - - - - - - - - - - < - - - - ebp栈平衡 栈 * 存 | 参数 1 | 增 * 增 - - - - - - - - - - 长 * 长 | ... | 方 * 方 - - - - - - - - - - 向 * 向 | 参数n | | * | - - - - - - - - - - | * \ / * 高 * / |
- 如果函数声明的时候没有显示标明调用约定,默认是
_cdecl
,也可以设置默认成其他。
demo:汇编分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | / / / 在debug版本才能看到详细的汇编代码,release版本,编译优化之后,汇编变得非常简洁了 / / / caller 调用者 fun1( 'a' , 1 , 2 ); / / cdecl 00D320AD 6A 02 push 2 00D320AF 6A 01 push 1 00D320B1 6A 61 push 61h 00D320B3 E8 35 F3 FF FF call fun1 ( 0D313EDh ) / / push eip; jmp 00D320B8 83 C4 0C add esp, 0Ch / / 栈平衡,在main函数中做,即由调用者负责栈平衡 fun2( 'b' , 3 , 4 ); / / stdcall 00D320BB 6A 04 push 4 00D320BD 6A 03 push 3 00D320BF 6A 62 push 62h 00D320C1 E8 39 F0 FF FF call fun2 ( 0D310FFh ) fun3( 'c' , 5 , 6 ); / / fastcall 00D320C6 6A 06 push 6 / / 第 3 参数从右往左依次入栈 00D320C8 BA 05 00 00 00 mov edx, 5 / / 第 2 参数放入edx 00D320CD B1 63 mov cl, 63h / / 第 1 参数放入ecx。 00D320CF E8 06 F2 FF FF call fun3 ( 0D312DAh ) / / / callee 被调用者 int _cdecl fun1(char c, short s, int x) { 00D31840 55 push ebp 00D31841 8B EC mov ebp,esp 00D31843 81 EC C0 00 00 00 sub esp, 0C0h / / 局部变量的空间 00D31849 53 push ebx / / 备份 00D3184A 56 push esi 00D3184B 57 push edi 00D3184C 8B FD mov edi,ebp / / 用 int 3 填充局部变量空间 00D3184E 33 C9 xor ecx,ecx 00D31850 B8 CC CC CC CC mov eax, 0CCCCCCCCh 00D31855 F3 AB rep stos dword ptr es:[edi] 00D31857 B9 03 C0 D3 00 mov ecx,offset _8C6D00FD_TestAsm@cpp ( 0D3C003h ) 00D3185C E8 F6 FA FF FF call @__CheckForDebuggerJustMyCode@ 4 ( 0D31357h ) return c + s + x; 00D31861 0F BE 45 08 movsx eax,byte ptr [c] 00D31865 0F BF 4D 0C movsx ecx,word ptr [s] 00D31869 03 45 10 add eax,dword ptr [x] 00D3186C 03 C1 add eax,ecx / / 结果放在eax寄存器中 } 00D3186E 5F pop edi / / 恢复,后进先出 00D3186F 5E pop esi 00D31870 5B pop ebx 00D31871 81 C4 C0 00 00 00 add esp, 0C0h / / 释放局部变量的空间 00D31877 3B EC cmp ebp,esp 00D31879 E8 EE F9 FF FF call __RTC_CheckEsp ( 0D3126Ch ) 00D3187E 8B E5 mov esp,ebp 00D31880 5D pop ebp 00D31881 C3 ret / / pop eip ;jmp eip int _stdcall fun2(char c, short s, int x) { 00D318A0 55 push ebp 00D318A1 8B EC mov ebp,esp 00D318A3 81 EC C0 00 00 00 sub esp, 0C0h 00D318A9 53 push ebx 00D318AA 56 push esi 00D318AB 57 push edi 00D318AC 8B FD mov edi,ebp 00D318AE 33 C9 xor ecx,ecx 00D318B0 B8 CC CC CC CC mov eax, 0CCCCCCCCh 00D318B5 F3 AB rep stos dword ptr es:[edi] 00D318B7 B9 03 C0 D3 00 mov ecx,offset _8C6D00FD_TestAsm@cpp ( 0D3C003h ) 00D318BC E8 96 FA FF FF call @__CheckForDebuggerJustMyCode@ 4 ( 0D31357h ) return c + s + x; 00D318C1 0F BE 45 08 movsx eax,byte ptr [c] 00D318C5 0F BF 4D 0C movsx ecx,word ptr [s] 00D318C9 03 45 10 add eax,dword ptr [x] 00D318CC 03 C1 add eax,ecx } 00D318CE 5F pop edi 00D318CF 5E pop esi 00D318D0 5B pop ebx 00D318D1 81 C4 C0 00 00 00 add esp, 0C0h 00D318D7 3B EC cmp ebp,esp 00D318D9 E8 8E F9 FF FF call __RTC_CheckEsp ( 0D3126Ch ) 00D318DE 8B E5 mov esp,ebp 00D318E0 5D pop ebp 00D318E1 C2 0C 00 ret 0Ch / / 栈平衡,在fun2函数中做,即由被调用者负责栈平衡 int _fastcall fun3(char c, short s, int x) { 00D31900 55 push ebp 00D31901 8B EC mov ebp,esp 00D31903 81 EC D8 00 00 00 sub esp, 0D8h 00D31909 53 push ebx 00D3190A 56 push esi 00D3190B 57 push edi 00D3190C 51 push ecx 00D3190D 8D 7D E8 lea edi,[ebp - 18h ] 00D31910 B9 06 00 00 00 mov ecx, 6 00D31915 B8 CC CC CC CC mov eax, 0CCCCCCCCh 00D3191A F3 AB rep stos dword ptr es:[edi] 00D3191C 59 pop ecx 00D3191D 66 89 55 EC mov word ptr [s],dx / / edx存放第 2 参数,放入局部变量区,位置随机 00D31921 88 4D F8 mov byte ptr [c],cl / / ecx存放第 1 参数,放入局部变量区 00D31924 B9 03 C0 D3 00 mov ecx,offset _8C6D00FD_TestAsm@cpp ( 0D3C003h ) 00D31929 E8 29 FA FF FF call @__CheckForDebuggerJustMyCode@ 4 ( 0D31357h ) return c + s + x; 00D3192E 0F BE 45 F8 movsx eax,byte ptr [c] 00D31932 0F BF 4D EC movsx ecx,word ptr [s] 00D31936 03 45 08 add eax,dword ptr [x] 00D31939 03 C1 add eax,ecx } 00D3193B 5F pop edi 00D3193C 5E pop esi 00D3193D 5B pop ebx 00D3193E 81 C4 D8 00 00 00 add esp, 0D8h 00D31944 3B EC cmp ebp,esp 00D31946 E8 21 F9 FF FF call __RTC_CheckEsp ( 0D3126Ch ) 00D3194B 8B E5 mov esp,ebp 00D3194D 5D pop ebp 00D3194E C2 04 00 ret 4 |
x64系统中函数调约定
- x64x86的fastcall的区别:
- 比ecx(rcx),edx(rdx)寄存器多了2个
r8
,r9
。
- 比ecx(rcx),edx(rdx)寄存器多了2个
- 参数入栈,会对齐到
8
个字节,参数的栈整体的大小是按照16
个字节对齐,即能被16整除。
- 参数入栈,会对齐到
- 函数的前4个参数存放到了rcx,rdx,r8,r9四个寄存器中,但在栈出也会预留4个空间(称为
shadow space
,x32上不会预留)
- 函数的前4个参数存放到了rcx,rdx,r8,r9四个寄存器中,但在栈出也会预留4个空间(称为
- 统一
调用者
来负责栈的平衡(因为在x64统一使用fastcall
,对于变参函数,只有调用者才知道参数个数,所以必须由调用者负责栈平衡)
- 统一
- 局部变量空间分配和初始化由调用者完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | int main() { 00007FF69B5A2030 40 55 push rbp 00007FF69B5A2032 57 push rdi 00007FF69B5A2033 48 81 EC 08 01 00 00 sub rsp, 108h / / 局部变量空间由调用者分配和初始化 00007FF69B5A203A 48 8D 6C 24 40 lea rbp,[rsp + 40h ] 00007FF69B5A203F 48 8D 0D E3 0F 01 00 lea rcx,[__CCDDBD56_TestAsm_x64@cpp ( 07FF69B5B3029h )] 00007FF69B5A2046 E8 84 F3 FF FF call __CheckForDebuggerJustMyCode ( 07FF69B5A13CFh ) / / / caller 调用者 func( 1 , 2 , 3 , 4 , 'a' , 5 , 6 ); 00007FF69B5A204B C7 44 24 30 06 00 00 00 mov dword ptr [rsp + 30h ], 6 00007FF69B5A2053 66 C7 44 24 28 05 00 mov word ptr [rsp + 28h ], 5 00007FF69B5A205A C6 44 24 20 61 mov byte ptr [rsp + 20h ], 61h 00007FF69B5A205F 41 B9 04 00 00 00 mov r9d, 4 00007FF69B5A2065 41 B8 03 00 00 00 mov r8d, 3 00007FF69B5A206B BA 02 00 00 00 mov edx, 2 00007FF69B5A2070 B9 01 00 00 00 mov ecx, 1 00007FF69B5A2075 E8 14 F3 FF FF call std::basic_ostream<char,std::char_traits<char> >::sentry::sentry ( 07FF69B5A138Eh ) } 00007FF69B5A207A 33 C0 xor eax,eax 00007FF69B5A207C 48 8D A5 C8 00 00 00 lea rsp,[rbp + 0C8h ] / / 调用者完成栈平衡,注意栈的整体大小要对齐到 16 个字节 00007FF69B5A2083 5F pop rdi 00007FF69B5A2084 5D pop rbp 00007FF69B5A2085 C3 ret / / / callee 被调用者 int func( int x1, int x2, int x3, int x4, char c, short s, int x) { 00007FF69B5A1E40 44 89 4C 24 20 mov dword ptr [rsp + 20h ],r9d / / shadow space 00007FF69B5A1E45 44 89 44 24 18 mov dword ptr [rsp + 18h ],r8d 00007FF69B5A1E4A 89 54 24 10 mov dword ptr [rsp + 10h ],edx 00007FF69B5A1E4E 89 4C 24 08 mov dword ptr [rsp + 8 ],ecx 00007FF69B5A1E52 55 push rbp / / push 老的rbp 00007FF69B5A1E53 57 push rdi 00007FF69B5A1E54 48 81 EC E8 00 00 00 sub rsp, 0E8h 00007FF69B5A1E5B 48 8D 6C 24 20 lea rbp,[rsp + 20h ] 00007FF69B5A1E60 48 8D 0D C2 11 01 00 lea rcx,[__CCDDBD56_TestAsm_x64@cpp ( 07FF69B5B3029h )] 00007FF69B5A1E67 E8 63 F5 FF FF call __CheckForDebuggerJustMyCode ( 07FF69B5A13CFh ) return x1 + x2 + x3 + x4 + c + s + x; 00007FF69B5A1E6C 8B 85 E8 00 00 00 mov eax,dword ptr [x2] 00007FF69B5A1E72 8B 8D E0 00 00 00 mov ecx,dword ptr [x1] 00007FF69B5A1E78 03 C8 add ecx,eax 00007FF69B5A1E7A 8B C1 mov eax,ecx 00007FF69B5A1E7C 03 85 F0 00 00 00 add eax,dword ptr [x3] 00007FF69B5A1E82 03 85 F8 00 00 00 add eax,dword ptr [x4] 00007FF69B5A1E88 0F BE 8D 00 01 00 00 movsx ecx,byte ptr [c] 00007FF69B5A1E8F 03 C1 add eax,ecx 00007FF69B5A1E91 0F BF 8D 08 01 00 00 movsx ecx,word ptr [s] 00007FF69B5A1E98 8B 95 10 01 00 00 mov edx,dword ptr [x] 00007FF69B5A1E9E 03 D0 add edx,eax 00007FF69B5A1EA0 8B C2 mov eax,edx 00007FF69B5A1EA2 03 C8 add ecx,eax 00007FF69B5A1EA4 8B C1 mov eax,ecx } 00007FF69B5A1EA6 48 8D A5 C8 00 00 00 lea rsp,[rbp + 0C8h ] 00007FF69B5A1EAD 5F pop rdi 00007FF69B5A1EAE 5D pop rbp 00007FF69B5A1EAF C3 ret / / 被调用者没有做栈平衡 |
函数传参的底层汇编
- 传值:栈上是对实参值的拷贝
- 传指针:栈上是对实参地址的拷贝
- 传引用:同传指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | func1(&a); / / 传指针 00CE1946 8D 45 F4 lea eax,[a] / / 把a的地址传给eax 00CE1949 50 push eax / / 入栈 00CE194A E8 91 F8 FF FF call func1 ( 0CE11E0h ) 00CE194F 83 C4 04 add esp, 4 / / 没有声明调用约定,默认是cdecl调用约定,需要调用者进行栈平衡 func2(a); / / 传引用 00CE1952 8D 45 F4 lea eax,[a] / / 传引用和传指针完全一样 00CE1955 50 push eax 00CE1956 E8 07 F9 FF FF call func2 ( 0CE1262h ) 00CE195B 83 C4 04 add esp, 4 func3(a); / / 传值 00CE195E 8B 45 F4 mov eax,dword ptr [a] / / 把a的值传给eax 00CE1961 50 push eax 00CE1962 E8 6F F8 FF FF call func3 ( 0CE11D6h ) 00CE1967 83 C4 04 add esp, 4 |
语句的底层汇编
i++/++i
1 2 3 4 5 6 7 8 9 10 | int i = 0 ; 00351825 C7 45 F8 00 00 00 00 mov dword ptr [i], 0 i + + ; / / c语言中i + + 不是原子操作,在汇编中可能对应多个汇编指令,在单核多线程环境中中不是多线程安全的 0035182C 8B 45 F8 mov eax,dword ptr [i] 0035182F 83 C0 01 add eax, 1 00351832 89 45 F8 mov dword ptr [i],eax i + + ; 00351835 8B 45 F8 mov eax,dword ptr [i] 00351838 83 C0 01 add eax, 1 0035183B 89 45 F8 mov dword ptr [i],eax |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int i = 0 ; 00D11825 C7 45 F8 00 00 00 00 mov dword ptr [i], 0 / / i + + ; / / c语言中i + + 不是原子操作,在汇编中可能对应多个汇编指令,在单核多线程环境中中不是多线程安全的 / / + + i; int a = 0 ; 00D1182C C7 45 EC 00 00 00 00 mov dword ptr [a], 0 int b = 0 ; 00D11833 C7 45 E0 00 00 00 00 mov dword ptr [b], 0 a = i + + ; / / a = i; i = i + 1 00D1183A 8B 45 F8 mov eax,dword ptr [i] 00D1183D 89 45 EC mov dword ptr [a],eax / / a = i 00D11840 8B 4D F8 mov ecx,dword ptr [i] 00D11843 83 C1 01 add ecx, 1 00D11846 89 4D F8 mov dword ptr [i],ecx / / i = i + 1 b = + + i; / / i = i + 1 ; a = i; 00D11849 8B 45 F8 mov eax,dword ptr [i] 00D1184C 83 C0 01 add eax, 1 00D1184F 89 45 F8 mov dword ptr [i],eax / / i = i + 1 00D11852 8B 4D F8 mov ecx,dword ptr [i] 00D11855 89 4D E0 mov dword ptr [b],ecx / / a = i |
控制语句
for
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | / / / for 模型 for : i = 0 ; jmp A; B: i + + A: cmp i, 0ah jge OUT; ... / / 循环体要执行的代码 jmp B; OUT: / / / 对应汇编代码 for ( i = 0 ; i < 10 ; i + + ) 00264488 C7 45 F8 00 00 00 00 mov dword ptr [i], 0 / / i = 0 0026448F EB 09 jmp __$EncStackInitStart + 5Eh ( 026449Ah ) / / 跳转到i< 10 ? 00264491 8B 45 F8 mov eax,dword ptr [i] 00264494 83 C0 01 add eax, 1 / / i + + 00264497 89 45 F8 mov dword ptr [i],eax 0026449A 83 7D F8 0A cmp dword ptr [i], 0Ah / / i - 10 0026449E 7D 13 jge __$EncStackInitStart + 77h ( 02644B3h ) / / 大于等于 10 ,跳出循环 { printf( "% d\n" , i); 002644A0 8B 45 F8 mov eax,dword ptr [i] 002644A3 50 push eax / / 入栈 002644A4 68 CC 7B 26 00 push offset string "% d\n" ( 0267BCCh ) / / 把格式化描述符入栈 002644A9 E8 F9 CE FF FF call _printf ( 02613A7h ) / / 调用printf 002644AE 83 C4 08 add esp, 8 / / 栈平衡,printf是变参函数,是cdecl调用约定,需要调用者进行栈平衡 } 002644B1 EB DE jmp __$EncStackInitStart + 55h ( 0264491h ) / / 跳转到i + + } 002644B3 33 C0 xor eax,eax / / 跳出循环 retrun 0 002644B5 5F pop edi 002644B6 5E pop esi 002644B7 5B pop ebx |
while
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | / / / while 模型 while : A: cmp i, 0ah ; jge OUT; ... / / 循环体要执行的代码 jmp A; OUT: / / / while 对应的汇编代码 while (i < 10 ) 007344BA 83 7D F8 0A cmp dword ptr [i], 0Ah / / i - 10 007344BE 7D 0B jge __$EncStackInitStart + 8Fh ( 07344CBh ) / / 大于等于 10 ,则跳出循环 { i + + ; 007344C0 8B 45 F8 mov eax,dword ptr [i] 007344C3 83 C0 01 add eax, 1 007344C6 89 45 F8 mov dword ptr [i],eax / / i + + } 007344C9 EB EF jmp __$EncStackInitStart + 7Eh ( 07344BAh ) / / 跳转到i< 10 ? return 0 ; 007344CB 33 C0 xor eax,eax / / retrun 0 } |
do-while
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / / / do{} while ()模型 do - while A: ... / / 循环体要执行的代码 cmp i, 0ah ; jl A; OUT: / / / do{} while ()对应的汇编代码 do { i + + ; 006344D2 8B 45 F8 mov eax,dword ptr [i] 006344D5 83 C0 01 add eax, 1 006344D8 89 45 F8 mov dword ptr [i],eax / / i + + } while (i < 10 ); 006344DB 83 7D F8 0A cmp dword ptr [i], 0Ah / / i - 10 006344DF 7C F1 jl __$EncStackInitStart + 96h ( 06344D2h ) / / 小于 10 则跳转到i + + return 0 ; 006344E1 33 C0 xor eax,eax |
if-else
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | / / / if - else 模型 if - else cmpi, 0 jle ELSE; ... jmp OUT; ELSE: ... OUT: / / / if - else 对应的汇编代码 if (i > 0 ) 008251E1 83 7D F8 00 cmp dword ptr [i], 0 / / i - 0 008251E5 7E 0B jle __$EncStackInitStart + 0B6h ( 08251F2h ) / / 小于等于 0 ,则跳转到i + + { i - - ; 008251E7 8B 45 F8 mov eax,dword ptr [i] 008251EA 83 E8 01 sub eax, 1 008251ED 89 45 F8 mov dword ptr [i],eax } 008251F0 EB 09 jmp __$EncStackInitStart + 0BFh ( 08251FBh ) / / 否则(即大于 0 ),i - - 后,跳转到retrun 0 退出判断 else { i + + ; 008251F2 8B 45 F8 mov eax,dword ptr [i] 008251F5 83 C0 01 add eax, 1 008251F8 89 45 F8 mov dword ptr [i],eax } return 0 ; 008251FB 33 C0 xor eax,eax |
switch语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | / / / switch模型 switch: cmp value, 1 je A; cmp value, 2 je B; cmp value, 3 je C; jmp DEFAULT; A: ... jmp OUT B: ... jmp OUT; C: ... jmp OUT; DEFAULT: ... OUT: / / / seitch对应的汇编指令 switch (value) 001C5286 8B 45 D0 mov eax,dword ptr [value] 001C5289 89 85 08 FF FF FF mov dword ptr [ebp - 0F8h ],eax / / value存入局部变量的内存 001C528F 83 BD 08 FF FF FF 01 cmp dword ptr [ebp - 0F8h ], 1 / / value - 1 001C5296 74 14 je __$EncStackInitStart + 100h ( 01C52ACh ) / / 相等,则跳到case 1 001C5298 83 BD 08 FF FF FF 02 cmp dword ptr [ebp - 0F8h ], 2 / / value - 2 001C529F 74 1A je __$EncStackInitStart + 10Fh ( 01C52BBh ) / / 相等,则跳到case 2 001C52A1 83 BD 08 FF FF FF 03 cmp dword ptr [ebp - 0F8h ], 3 / / value - 3 001C52A8 74 20 je __$EncStackInitStart + 11Eh ( 01C52CAh ) / / 相等,则跳到case 3 001C52AA EB 2D jmp __$EncStackInitStart + 12Dh ( 01C52D9h ) / / 前面都不符合则跳到default。 { case 1 : printf( "1\n" ); break ; 001C52AC 68 D8 7B 1C 00 push offset string "1\n" ( 01C7BD8h ) 001C52B1 E8 F1 C0 FF FF call _printf ( 01C13A7h ) 001C52B6 83 C4 04 add esp, 4 001C52B9 EB 2B jmp __$EncStackInitStart + 13Ah ( 01C52E6h ) / / break case 2 : printf( "2\n" ); break ; 001C52BB 68 DC 7B 1C 00 push offset string "2\n" ( 01C7BDCh ) 001C52C0 E8 E2 C0 FF FF call _printf ( 01C13A7h ) 001C52C5 83 C4 04 add esp, 4 001C52C8 EB 1C jmp __$EncStackInitStart + 13Ah ( 01C52E6h ) case 3 : printf( "3\n" ); break ; 001C52CA 68 E0 7B 1C 00 push offset string "3\n" ( 01C7BE0h ) 001C52CF E8 D3 C0 FF FF call _printf ( 01C13A7h ) 001C52D4 83 C4 04 add esp, 4 001C52D7 EB 0D jmp __$EncStackInitStart + 13Ah ( 01C52E6h ) default: printf( "default \n" ); 001C52D9 68 E4 7B 1C 00 push offset string "default \n" ( 01C7BE4h ) 001C52DE E8 C4 C0 FF FF call _printf ( 01C13A7h ) 001C52E3 83 C4 04 add esp, 4 } return 0 ; 001C52E6 33 C0 xor eax,eax |
数组,结构体,结构体数组访问
struct access
- 结构体首地址+成员相对结构体首地址偏移量(0)
1 2 3 4 | g_stu.age = 18 ; 009E5202 C7 05 7C A1 9E 00 12 00 00 00 mov dword ptr [g_stu ( 09EA17Ch )], 12h / / 结构体的首地址 + 0 ,age相对结构体首地址的偏移为 0 g_stu.sex = 'F' ; 009E520C C6 05 80 A1 9E 00 46 mov byte ptr ds:[ 9EA180h ], 46h |
array access
- 数组首地址+元素偏移
1 2 3 | g_a[i] = 1 ; 00A71A6B 8B 45 F4 mov eax,dword ptr [i] 00A71A6E C7 04 85 40 A1 A7 00 01 00 00 00 mov dword ptr g_a ( 0A7A140h )[eax * 4 ], 1 / / g_a[i] = 1 ; |
结体数组的访问
- 结构体数组首地址+
i*sizof(sruct)
+成员相对其结构体首地址偏移量
- 结构体数组首地址+
1 2 3 4 5 6 | g_stua[i].age = 18 ; 00241A93 8B 45 F4 mov eax,dword ptr [i] 00241A96 C7 04 C5 D0 A2 24 00 12 00 00 00 mov dword ptr g_stua ( 024A2D0h )[eax * 8 ], 12h / / g_stua[i].age = 18 ; g_stua[i].sex = 'F' ; 00241AA1 8B 45 F4 mov eax,dword ptr [i] 00241AA4 C6 04 C5 D4 A2 24 00 46 mov byte ptr [eax * 8 + 24A2D4h ], 46h |
C++反汇编特点
This指针
(对象的首地址,在函数参数固定的情况下,This指针是放在eax
寄存器传给被调用者的)虚函数表指针
(类中如果有虚函数,该对象头部都有虚函数表指针(存放在对象指向内存的前4Byte
中)指向一个虚函数表,所以在调用的时候可以通过虚函数表指针从虚函数表中找到正确的虚函数来执行)汇编指令格式详解
- 为什么mov有不同的指令形式?能不能自己诫算这个机器码?
- 参考学习:
- 可以用
__asm{想要验证的汇编指令}
在程序调试进行验证汇编指令格式
- 通用寄存器
- X86:
8
个通用寄存器,X64:16
个通用寄存器
-eax,ebx,ecx,edx,esi,edi,esp,ebp(e表示extend)- rax,rbx,rcx,rdx,rsi,rdi,rsp,rbp(re表示re extend)
r8,r9,r10,r11,r12,r13,r14,r15(新增寄存器)
- rax,rbx,rcx,rdx,rsi,rdi,rsp,rbp(re表示re extend)
- 对通用寄存器编号,寄存器不同,指令不同
- x86用
3
bit表示,x64需要用4
bit表示
- x86用
- 操作码
- 每条指令都有一个或多个编码
- 7种寻址方式:
- 寻址方式不同指令不同,比如mov指令,不同寻址方式,mov的指令也不一样
立即数
寻址: mov al,44h寄存器
寻址: mov ds,ax直接
寻址方式:mov al,DS: [2000h]- 寄存器
间接
寻址方式:mov ax,[bx] - 寄存器
相对
寻址方式:mov ax,[ebp + 1000] 基址变址
寻址方式:mov ax,[bx + si]相对
基址变址寻址方式:mov ax,[bx +si*n + 1000];n=1,2,4,8
X86汇编指令格式
- 字段(变长,1到17个字节(4+3+1+1+4+4)):
refix
: 可以有,可以没有,最多可以有四个
。比如rep movs byte ptr[edi],byte ptr[esi]
Opcode
:1,2,3Byte,唯一必须
的字段,其它都是optional的ModR/M
:一个字节- Mod:占两位,
00
寄存器间接寻址,01
寄存器相对寻址偏移8位。10
寄存器相对寻址偏移32位,11寄存器直接寻址。 - Reg/Opcode:Reg/Opcode有时候是
opcode
的补充操作码。有时候是第个操作数寄存器
(由指令定义决定) - R/M:RM占3位,指定寄存器编号
- 其中中Mod和RM定义了
32
种寻址方式
- Mod:占两位,
SIB
:ModRM的补充寻址方式基址变址寻址
- eg:相对基址变址寻址
[ebp+ebx*2+10h]
, - Scale倍率00表示
*1
,01表示*2
,10表示*4
,11表示*8
- index:倍率寄存器编号
- Bese基址寄存器编号
- eg:相对基址变址寻址
Dis
:偏移,1个,2个或者4个字节;IMM
: 立即数,1,2,4个字节
- eg:手工根据汇编指令计算出机器码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | / / 计算流程:Opcode 'mod reg/opcode r/m' 'ss index base' disp imm / / mov r / m32 imm32 对应的机器码是C7 / 0 (reg / opcode) / / 寻址方式是寄存器相对寻址偏移 8 位 对应Mod: 01 / / reg / opcode是 000 / / ebp编号是 101 / / C7 45 / / `SIB`字段没有 / / disp是 - 38h ,即C8 / / imm32是 1 ,低位优先,即 01 00 00 00 / / 合起来就是C7 45 C8 01 00 00 00 mov dword ptr [ebp - 38h ], 1 / / C7 45 C8 01 00 00 00 mov [ebp - 38h ],eax / / 89 45 c8 mov dword ptr [ebp + ebx - 38b ], 0 / / C7 84 1D C8 FF FF FF 00 00 00 00 mov ebx,eax / / 8B D8 ADD DWORD PTR DS[ESI + ECX * 4 ],EAX / / SiB: 8Eh |
X64汇编指令格式
@todo x64暂时先放一放
x64汇编
- x64汇编不能像x86汇编那样嵌入到C语言代码中,而是需要把汇编独立写在一个
.asm
文件中。 - @todo 待完善,x64很少遇到,暂时先放一放
什么是逆向(静态)分析?
- 正向开发:
C源程序(.c/.cpp)->汇编程序(.S) ->二进制(.exe/.dll/.sys或者.elf/.so)->加壳保护
- 逆向开发:
C源程序(.c/.cpp)<-汇编程序(.S)<-二进制(.exe/.dll/.sys或者.elf/.so)<-加壳保护
- 逆向分析的作用
- 病毒木马样本分析,比如勒索软件
- PC端和移动端竞品分析或破解
- 和开发相辅相成,其的并发是基础
- 工具:IDA PRO,APKTOOL,JEB, OllyDbg等
- 防止逆向分析手段:
- 1.加壳: VMProtect, ASProtect, ArmadiIIo, Theailda, UPX等人移动端壳,其中
VMProteot
(简称VMP)是目前最强的虚拟机保护壳,据说至今没有人能够公开号称成功还原过,并且支持内核驱动加密保护 - 2.加花指令
- 3.指令混淆: OLLVM(Obfuscator-Low Level Virtual Machine, 针对LLVM的代码混淆工具)
- 1.加壳: VMProtect, ASProtect, ArmadiIIo, Theailda, UPX等人移动端壳,其中
- 逆向带有很强的技术性。逆向存在各行各业,尤其是制药,军工和高科技行业:中国是逆向大国。中国曾经逆向美制响尾蛇导弹失败。为什么失败呢?
- 1958年9月24日温州以东空战台军发射了5枚美国AIM-9B响尾蛇导弹,其中一枚坠地而未爆炸。
- 霹雳1:无线电制导
- 霹雳2:红外线制导
- 霹雳10:第四代空空导弹
- 俄罗斯发动机
逆向的前置基础
- C(C++)语言开发基础
- X86 or ARM汇编基础
- 程序调试基础(断点,跟踪等)
- PE、ELF等可执行文件格式
- 脱壳基础
- 加壳原理
- 识别各种壳
- 逆向学习重要方法
- 全称: Interactive Disassembler Rrofessional
- 一款逆向里必用的“神器",世界顶级的交互式反汇编工具。总部位于比利时列日市的Hex-Rays公司开发。作者是一仪编程天才,名叫llfak Guilfanov(伊尔法克·吉尔法诺夫)。
- IDA是一个强大的二进制程序逆向分析工具,利用自身强大的反汇编引擎,能够将复杂的二进制程序反汇编成可供阅读和分析的汇编程序并可以在汇编程序的基础上,提供多种编辑加工的功能,汇编程序变得更加友好和可读。支持多个处理器平台(30到50多个)和多个格式的文件(exe,dll,sys, so, elf等),支持Win,Linux, MacOS等。
- IDA采用
递归下降反汇编算法
,在区分数据与代码的同时还设法确定这些数据的类型,并尽其所能地注释生成的反汇编代码 - IDA玉具还提供了强大的插件功能,其中的
Hex-Rays
插件甚至能够直接将汇编语言逆向成为可读性更强的C代码。 - IDA图标头像: Ada Lovelace(1815-1852),英国数学家,
穿孔机程序
创始人:19世纪诗人拜伦的女儿。她建立了循环和了程序概念计算程序拟定“算法”,被视为世界主第一位软件设计师
,她对讦算机的预见超前了整整一个世纪。美国国防部曾将种计算机语言命名为Ada语言,以纪念这位“世界上第一拉软件工程师"。IDA用她的头像作为图标是为了纪念这位女程序员 - IDA是收费软件
IDA安装目录简介
cfg
:配置文件目录,包括基本IDA配置文件ida,cfg、GUI配置文件idagui.cfg
以及文本模式用户界面配置文件idatui.cfg
。dbgsrv
: 调试端服务器,用于远程调试。idc
:包含IDA的内置脚本语言IDC所需的核心文件。ids
:包含符号文件:用于描述可被加载到IDA的二进制文件引用的共享库的内容和摘要信息,列出了由某个指定库导出的所有项目,比如描述某个函数所需的参数类型和数量的信息、函数的返回类型(如果有)以及与该函数的调用约定有关的信息。loaders
:包含在文件加载过程中用于识别和解析PE或ELF等文件格式的IDA扩展。procs
:包含所支持的处理器模块处理器模块为IDA提供机器语言与汇编语言的转换功能,并负责生成最终的汇编语言代码。sig
:包含IDA在各种模式匹配操作中利用的现有代码的签名。通过模式匹配
,IDA能够将代码序列确定为已知的库代码,节省分析时间。签名由IDA的“快速的库识别和鉴定技术”(FLIRT)生成。比如通过匹配入口点(start,不是main)签名来识别编译器(VC,BORLAND,GCC等)til
:包含一些类型库信息,通过这些信息记录各种编译器库的数据结构的布局。
线性反汇编与递归下降反汇编
- 什么是反汇编?
反汇编
:把机器语言转化为汇编语言反C
:把汇编语言转化为C语言- 这是一个逆向过程
线性扫描
:从代码段第一个字节
开始,逐条反汇编每条指令- 优点:能够覆盖程序的所有代码段
- 缺点:如果代码中
混有数据
可能会得到错误结果。
递归下降
:根据一条指令是否被令一条引用来决定是否对它反汇编。顺序流指令
:线性扫描;条件指令
:将真的目标地址放入到延迟地址列表
(延迟反汇编),为假分支继续线性反汇编。无条件跳转指令
:直接把目标地址放入延迟地址列表,问题:jmp eax;
(寄存器的值只有在程序运行的时候才能知道,IDA进行静态反汇编,拿不到值的)函数调用
:将目标地址放入到延迟地址列表(call eax问题
), 返回后线性扫描后续指令(如果返回地址被窜改,会有问题,ret指令:反汇编终止(因为函数在栈上,只能在运行的时候才能确定,静态分析ret的位置无法确定,)
),从延迟地址列表里取一个地址继续反汇编。- 能够区分代码和数据,无法识别通过代码指针表来进行的
间接跳转
(switch语句)(启发式方法解决)
Windbg
,objdump
采用线性扫描;IDA PRO
使用的递归下降法反汇编
启动与关闭IDA
IDA的启动
- 32位与64位
- 打开动态库、ex静态库、中间文件、apk等等二进制文件
- NEW->选择需要逆向程序->默认选项(文件加载器,处理器类型)->在分析的过程中可能会提示带符号(只有调试自己的程序才有符号提示,选择加载符号,可读性更佳,如果错过了,还有另一次加载符号的机会
File-Load file-PDB file
) - 生成数据库文件(存放在目标逆向文件的同一路径下)
- 用于调试正在运行的程序,或者暂时没有想好的情况
- 直接进入IDA,没有任何内容,需要手动Load要分析的文件进来
Previous
- 加载上次文件
- 加了upx壳的文件
- 有些程序在释放出驱动,加载驱动之后会把驱动文件删除,避免被分析。
- 思路:
- 不需要保存:勾选
DON’ T SAVE the database
- 需要保存
常见问题
找不到模块
- “汇编视图(IDA View)“是IDAPro静态分析程序的最主要的界面,该界面显示了各个函数的汇编代码。
- 汇编视图提供了2种形式的显示方法,一种是基于
文本
的显示,另1种是基于图形
的显示,二者之间可以通过空格键
进行快速的切换。 - 默认显示图形还是文本可以通过选项设置
- 在汇编视图里,既可能遇到代码指
令也可能会遇到数据。在分析的时候,它们之间可以用命令进行相互转化和重命 - “函数窗口”列出了所有模块中存在的函数,双击任意函数,就可以定位到该函数的实际位置。
- 如果加载符号,函数的返回类型,调用约定,函数名称,函数的参数列表,参数类型以注释的形式提供。
- 如果未加载符号,那么函数名可能以
sub_location
的形式存在。
- 如果函数解析不准确,可以修改或者删除
- R:返回给调用者
- F:far function
- L:库函数
- S:static function
- B:用ebp+来访问形参,ebp-来访问局部变量
- T:函数拥有类型信息
- =:栈帧的指针指向的是栈帧的基地址
名字窗口
- “名字窗口"列出了IDA分析出的所有全局的名字。这些名字就是对虚拟内存地址的一个符号描述(比如变量函数名等)。鼠标
双击
这些名字就会快速跳到对应的汇编窗口对应的位置。 - 名字前面的字符表示了不同的名字类别:
- F:普通函数
- L:库函数
- I:导入函数的名字,无代码部分(现在是个箭头,I表示标号位置)
- C:命名代代码。不属于任何函数
- D:命名数据,比如全局变量
- A:ASCII字符串,大于4个字符
字符串窗口
- “字符串窗口"列出了IDA识别出来的所有字符串该字符串窗口对于分析程序的关键点非常有用。因为过主一般都具有意义作可以猜测引用该字符串位置的代码的作用。尤其是那些进行
安全验证
的地方可以遇字符串进行快速定位。 - 双击某个字符串,就可以快速定位到字符串定义和引用的位置。或者也可以通过"ALT+T"快捷键来查找字符串的引用位置。
- 窗口右键setup配置
PE区段窗口
- View-open subviews-segmention或者CTRL+S快捷键
- "PE区段窗口“列出了分析文件的所有PE区块:
- HEADER:PE头
- .text:代码段
- .idata:导入表所在数据段
- .rdata:只读数据段
- .data:数据段
- INIT:初始化段
- .reloc:存放基地址重定向表的重定向节,用于当irmageBase改变后,对映像内的使用的地址进行重定位。
- 用鼠标双击这些区段,就可以直接跳到对应的汇编代码或者数据位置。
导入/出表窗口
- 导入表(导入那些库函数,可以从侧面分析程序的功能)
- 导出表(驱动时DriverEntry,应用程序则是Main)
其他窗口
- 输出窗口(ida分析过程中的信息、或者脚本运行的结果)
- 程序内存布局彩条
IDA基本操作
查找
- 文本查找 ATL+T/CTRL+T
- 以Byte为单位查找二进制值 ATL+B/CTRL+T
跳转
- 双击就可以直接跳转
- CTRL+S可以跳转到某个节
- CTRL+E可以跳转到入口点
- G可以跳转到指定目的地址
- CTRL+L按照名字跳转
编辑
- 编辑修改都是为了提高代码可读性,使得分析的时候更加清晰
1.重命名,快捷键N
2.代码与数据转换:C,U,D,A
3.注释: - 用
nop
替换花指令- Edit-Patch program-Change byte
- 或者点右键-Keypatch-Patcher
- 查看 Patch的历史
- Patched bytes
- 应用到文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | int read_id; int write_id; int ref_id; void callfunc() { printf ( "callfunc\n" ); } int _trmain( int argc,_TCHAR * argv[]) { int * p = &ref_id; / / 地址引用 * p = read_id; / / 读引用 write_id = * p; / / 写引用 callfunc(); if (read_id = = 5 ) write_id = 2 ; else write_id = 1 ; callfunc(); return 0 ; } |
- 数据引用
- View->Open Subview->Cross references或
X
(列举所有交叉引用当前符号的地方,比如strcpy,容易造成缓存区溢出的漏洞,可以找到所有调用的地方strcpy,更好利用漏洞) - o:地址应用
- r:读引用
- w:写引用
- View->Open Subview->Cross references或
- 代码引用
- 将光标定位到该函数内部本后直接按下快捷键F5,那么,整函数就会被直接翻译成对应的C代码
- 64位的IDA没有安装反C插件Hex-rays,所以要看C代码需要使用32位的IDA。
结构体
结构体(structures)视图
- 结构体视图显示IDA决定在一个二进制文件中使用的任何复杂的数据结构布局。
- 列举出了IDA在分析过程中识别出来的结构体信息,而那些IDA自己未成功识别的结构体,也可以通过IDA提供的方式来添加。
- 数组的某个元素或者结构体的某个成员在汇编代码中的特征并不明显。所以需要手动把人工识别的结构体放在结构体视图中,只有在该视图里出现的结构体才可以被整合到IDA的汇编视图里,提高代码的
可读性
(用结构体成员访问去重命名汇编代码中的偏移)。添加IDA未成功识别的结构体
- 方式1:
Insert
快捷键添加- 在该视图里,通过
Insert
快捷键添加,在弹出的对话框里指定结构体的名字,或者直接从对话框不面的按钮"Add standard structure"中选择标准库中的结构体。 - 在对话框中点击
OK
按钮之后,刚才定义的结构体名字将会在视图中出现。这个时候,将光标移动到结构体定义的最后行
,按D
键,可以往结构体里插入成员,再按D
,可以切换成员的类型。按A
键,可以往结构体里插入数组成员 - 删除结构体
中间
的成员:U
→开始菜单Edit->Shrink struct type - 在结构体
中间
增加一个成员: Edit->Expand struct type - 在结构体中
删除最后
一个成员: 将光标移动到结构体ENDS
一行,按U
键 - 重命名一个结构体成员:将光称移到该成员,按
N
键即可重命名。 - 效率低
- 在该视图里,通过
- 方式2:通过
Local Types
添加- 通过Local Types添加
- 这种方式可以通过菜单View→OpenSubviews Local Types→INSERT插入种个用C结构体方式定义的结构体。这种方式比第一种方式更容易和方便。通过这种方式新插入的结构体不会直接插入structures视图而是放在了
标准库
中,因此必须通过第一种方式中的insert
快捷键,然后选择从对话框下面的按钮Add standard structure
中选择标准库中的结构体来加入结构体视图中。 - 也可以通过在
Local Type
视图里双击该结构体,将它加入到Structures
视图中。
- 方式3:通过头文件添加
- 可以往结构体视图
加入
IDA未识别的结构体。加完自己定义的结构体之后,就可以在汇编代码中运用结构体。 - 对于普通代码,将光标移动到汇编代码中
存器+偏移
的位置,然后右键,在弹出的菜单中,选择Struct offset
就会将结构体视图中与这个偏移匹配的结构体成员引入。这样汇编代码就由[eax+4h]变为了[eax+mystruct.member] - 而对于栈的变量(结构体汇编代码特征不明显,每个成员都会被IDA识别成一个独立的变量),如里想运用相应的结构体类型,方法如下:
- 首先在函数内部栈空间上鼠标
双击
IDA会切换到函数的栈空间视图: - 这个时候,将光标移到对应的栈变量上,然后通过
Edit
→Struct Var(ALT+Q)
,选择要转换的结构体类型。
- 首先在函数内部栈空间上鼠标
- 还可以用
N
修改汇编代码中的结构体名,进一步增强可读性。IDA脚本编程
- IDA集成了2个脚本引擎(一个是
IDC
,一个是Python
),让用户从编程角度对IDA的操作进行全面控制,从而自动执行常规任务。以编程方式访问和查询IDA数据库,分析整理数据,可用于脱壳,去模糊,自动化分析方面。免去了手工操作的繁琐、低效和不便。 - lD
C
,类似于C语言语法与编程凤格Hello world
- 三种方式执行IDC或者Python代码
- 窗口 File->Script command
1 2 3 4 5 | / / IDC Message( "hello world" ); / / python 3.x print ( "hello world" ) |
- 文件 File->Script file
1 2 3 4 5 6 | / / / hello.idc #include <idc.idc> static main() { Message ( "hello world" ); } |
1 2 3 4 5 6 | #hello.py import idc def print_hello(); print "hellc world" print_hello() |
- 命令行
- 单语句可以使用命令行,少量语句可以使用窗口,复杂的语句可以使用文件
基础语法 @todo
函数遍历@todo
调用者遍历@todo
交叉引用查找@todo
发行版代码保护与反逆向方法
Release编译优化
- 一般拿到的程序都是release版本的
- 与调试版的程序不同,发行版的程序做了很多的
编译优化
,其对应的汇编代码将会更加的难以理解,可阅读性也会更差一些且没有对应的PDB符号文件
, - 如下图所示在没有符号文件的时候、IDA识别出来的函数名都是自动命名,可读性就变得很差。
- 甚至是通过花指令或者OLLVM或者加壳混淆对抗IDA等工具。这样分析起来就会更加困难一些。
反调试和反反调试
反调试
- DebugProt
- 对于未导出的函数,无法通过符号来找到DebugProt,只能通过硬编码来找到DebugProt
- 在内核层
KdDisableDebugger
在内核里调用KdDisableDebugger来禁用内核调试KdEnable Debugger
启用内核调试
- 在应用层
- 开两个线程来分别调用
IsDebugerPresent
和CheckRemoteDebuggerPresent
来检测自己是否被正在被调试,如果发现被调试,自己就退出,其他人就无法继续调试我了。
- 开两个线程来分别调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / / 没有加入反调试代码的时候MFC程序是可以被windbg调试的,加入反调试代码之后,windbg调试MFC程序的是,MFC就会一闪而过退出 / / / 反调试线程函数,放在MFC函数的执行入口 UINT AntiDebug(PVOIDparam) { while (g_bWillExit = = FALSE) { HANDLE hRrocess = GetCurrentProcess(); BOOL bDebuggerPresent = FALSE; CheckRemoteDebuggerPresent(hProces,&bDebuggerPresent); if (IsDebuggerPresent()||bDebuggerPresent) { ::ExitProce( 0 ); / / 把当前进程结束掉 } Sleep( 5000 ); / / 每隔 5s 检测程序是否被调试 } return 0 ; } AfxBeginThread((AFX_THREADPROC)AntiDebug, NULL); / / 启动反调试线程 |
- Hook-Anti-debug
- hook系统中一些与调试相关的函数,以可以防止各种调试器调试。
NTOpenThread()
hook这个函数,可以防止调试器在程序内部创建线程NTOpenProcess()
hook这个函数,可以防止OD(OllyDbg)等调试工具在进程列表中看到我们KiAttachProcess()
hook这个函数,可以防止被附加上NtReadVirtualMemory()
hook这个函数,可以防止被读内存NtWriteVirtualMemory()
hook这个函数,可以防止被内存被写
- hook这两个函数用来防止双机调试12
-
`KdReceivePacket()` KDCOM.dll中Com串口接收数据函数
-
`KdSendPack()` KDCOM.dll中Com串口发送数据函数
反反调试
ba w4 debugport
对DebugProt内存地址下断点
这样一旦有程序代码在修改DebugPort
,就会被断下,从而找到对应清零DebugPort的反调试代码,然后对这部分代码进行patch(用机器码0x90(nop)
或 者0xC3(ret)
取代)从而让它失去作用,当然有的程序会对代码进行校验,一旦发现代码被篡改,就会采取保护措施,比如抛出异常或者退出程序。- 针对调用系统函数如
KaDisabeDebugger()
来检测调试器存在,从而禁止被调试的方法,可以在对应的这些函数的地址下断点,然后对相关的代码进行patch,然后使该函数判断失效。- 比如
bp KdDisableDebugger, eb xxx
- 比如
- 针对通过HOOK系统函数来防止进程被调试的方法,可以直接将
这些系统函数的钩子直接恢复,可以通过检测和恢复这些函数钩子。花指令
花指令是程序中的无用指令
或者垃圾指令
,故意干扰各种反汇编静态分析工具,但是程序不受任何影响,缺少了它也能正常运行。加花指令后,IDA Pro
等分析工具对程序静态反汇编时,往往会出现错误或者遭到破环,加大逆向静态分析的难度,从而隐藏自身的程序结构和算法从而较好的保护自己。 - 遇到花指令,需要手动udefine掉,很多的话需要写脚本剔除花指令、用插件模拟执行。
- 花指令1 inc+dec
1 2 3 4 5 6 7 8 | / / 花指令 1 / / 代码没有任何作用,但会混淆调试者 push edx pop edx inc ecx dec ecx add esp, 1 sub esp, 1 |
- 花指令2 jmp
但IDE采用的反汇编算法是递归如果没有指令跳转到这个位置,就拒绝对这条指令进行反汇编。1234/
/
花指令
2
jmp Labe1
db opcode
/
/
在jmp后面加上一个字节的机器码,并不完整(完整的汇编指令是一个机器码
+
操作数),但不影响程序的执行(jmp会跳过这条残缺的汇编指令)。在IDE工具反汇编的时候,看到机器码(以为紧接着的就是操作数,接着后面的反汇编都错位了)
Label1:
- 花指令3 jz+jnz
1 2 3 4 5 | / / 花指令 3 jz Label / / 花指令对代码依旧没有影响,为 0 的时候会跳过花指令。 jnz Label / / 非 0 才跳转到 Label,为 0 则执行下面的花指令,IDE此时就认为这条花指令需要反汇编。 db opcode / / _emit 0e8h ;call机器码 Label: |
- emit伪指令,在当前位置直接插入数据(指令),一般用来插入汇编里面没有的特殊指令,和db,dw效果相同。
- 目的:编译器不认识的指令,拆成机器码来写。
- 花指令4 call+ret
- call一个地址,在call下面随便写入花指令并记住花指令字节长度
- 在call里面,也就是函数里面姚首先pop出压入的地址,.然后把这个地址减去花指令占用的字节数(绕过花指令),再重新push进栈,然后就ret(跳到了花指苓的下一条指令)
1 2 3 4 5 | pop ebx / / 压栈地址弹出 inc ebx / / 压栈地址 + 1 ,绕过花指令 push ebx / / 返回地址完成加绕过了花指令 mov eax, 0x1 ;设置返回值 ret |
1 2 | - call 0A04B0D7 迷惑了反汇编器,认为call之后会返回继续执行call的下一条指令,所以从 0A04B0D6 位置开始反汇编,实际上 0A04B0D6 位置放了一个Byte的垃圾值,call只是直接jmp 0A04B0D7 , 0A04B0D6 不会被执行 - 解决方法:在 0A04B0D6 执行`U`命令,然后在 0A04B0D7 执行`C`命令把这条花指令patch掉 |
1 | - jmp到自己的指令也有问题,必须去掉OA04BODB的定义(`U`快捷键),重新定义OA04BODC处的指令(`C`快捷键)。 |
1 | - 最后执行的是jmp eax (是为了对抗静态反汇编,运行时才计算) |
- 花指令5 相对跳转指令构造绝对跳转
1 2 | xor eax, eax jz short near ptr loc_40100A |
- 构成了一个绝对跳转(在40100A前的都不会执行)。
- 但反汇编编译器没有识别,继续对jz后面的指令比如0401009处的进行反汇编,出错。
动态计算目标地址
- 递归下降反汇编的缺陷是无法获得
动态计算目标地址
- 除非使用脚本或者模拟器来模拟执行
动态计算目标地址
可以用来干扰IDA导致无法得知目标地址,无法得到这部分反汇编代码- 使用一个调用语句将个返回地址压入栈中,然后这个返回地址直接由栈进入寄存器,再给寄存器加上一个常量值,得到最后的目标地址;
- 或者将寄存器中计算出来的返回地址移入栈顶部,然后返回指令(ret)将控制权转交给计算得出的位置。
- 这些情况下,分析人员必须动手运行代码,才能确定程序的具体控制路径。
OLLVM混淆
- LLVM
- LLVM命名最早源自于底层虚拟机(Low Level VirtualMachine)的缩写,目前LLVM就是该项目的全称。
- LLVM核心库提供了与编译器相关的支持可以作为多种语言编译器的后台来使用。能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。
- LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合。
- LLVM是伊利诺伊大学的一个研究项自,提供一个现代化的,基于SSA的编译策略能够同时支持
静态
和动态
的任意编程语言的编译目标。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多正在生产中使用的各种商业和开源的项目,以及被广泛用于学术研究。
- OLLVM (Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的个项自,该项自旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。OLLVM适用LLVM支持的所有语言(C,C++,Objective-C , Ada和 Fortran)和目标平台(x86, x86-64,PowerPC, PowerPC64,ARM,Thumb,SPARC,Alpha,CellsPU,MIPS,MSP430,SystemZ和XCore)混淆的方法有:
- 控制流平展模式
- control Flow Flattening控制流平展模式可以完全改变程序原本的控制流图经FLA(CCF)模式混淆后程序的执行流程已经被打乱,
出现许多代码分支
- 简而言之,
增加很多条件分支
- control Flow Flattening控制流平展模式可以完全改变程序原本的控制流图经FLA(CCF)模式混淆后程序的执行流程已经被打乱,
- 指令替换模式
- Instructions Substitution指令替换摸式主要是将正常的运算操作
+,-,*,/,&,|
等替换成功能相等但表达更复杂
的形式 - 简而言之,
把简单的运算变成复杂的运算
- Instructions Substitution指令替换摸式主要是将正常的运算操作
- 控制流伪造模式
- Bogus Control FIow控制流伪造模式也是对程序的控制流做操作与CFF不同的是,BCF模式会在原代码的块前后随机插入不确定的代码块,然后新代码块再通过条件判断跳转原代码块中。甚至原代码块可能会被克隆并插入随机的垃圾指令。同一份代码多次BCF模式的混淆时,得到的是不同混淆效果。原本简单的if else分支代码变得异常复杂,加大了逆向的难度。
- 简而言之:
不仅会打乱流程,还会添加花指令
- 利用OLLVM混淆Android Native代码
编译OLLVM
- 环境
- 虚拟机 ubuntu 16.04_64位,4核,16G内存,编译耗时
30min
左右
- 虚拟机 ubuntu 16.04_64位,4核,16G内存,编译耗时
- 安装gitl和cmake:
sudo apt-get install git
sudo apt-get install cmake
- 下载和编译ollvm
- 下载
cd ~/
mkdir workspace
cd workspace
git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git
(不稳定,下载不下来就换网或者用VPN)
- 编译
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ../obfuscator/
,如果出错,换下面命令cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF ../obfuscator/
make -j4
(并行编译任务,一般为CPU核数的双倍)
- 添加环境变量,方便使用命令行在任何位置都能直接调用
- 没有混淆前的代码
- mkdir hello
- cd hello
- vim hello.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <stdio.h> int main() { int a = 5 ; if (a< 10 ) { a + + ; } else { } printf( "hello ollvm\n" ); return 0 ; } |
- gcc hello.c -o hello
- ./hello
- gcc hello.c -S
- vim hello.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | / / / 没有用OLLVM混淆之前代码反汇编的逻辑是很清晰的 . file "hello.c" .section .rodata .LC0: .string "hello ollvm" .text .globl main . type main, @function main: .LFB0: .cfi_startproc pushq % rbp .cfi_def_cfa_offset 16 .cfi_offset 6 , - 16 movq % rsp, % rbp .cfi_def_cfa_register 6 subq $ 16 , % rsp movl $ 5 , - 4 ( % rbp) / / a = 5 cmpl $ 9 , - 4 ( % rbp) / / a< = 9 jg .L2 / / > 9 ,则跳转.L2 addl $ 1 , - 4 ( % rbp) / / a + + .L2: movl $.LC0, % edi / / printf参数入栈 call puts movl $ 0 , % eax / / retrun 0 leave .cfi_def_cfa 7 , 8 ret .cfi_endproc .LFE0: .size main, . - main .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609" .section .note.GNU - stack,"",@progbits |
- 混淆之后的代码
- 指令替换模式
- clang hello.c -mllvm
-sub
-o hellosub(编译成可执行文件) - clang hello.c
-emit-llvm
-mllvm -sub-S
-o hellosub.ll(编译成汇编文件)
- clang hello.c -mllvm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ; ModuleID = 'hello.c' source_filename = "hello.c" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" @. str = private unnamed_addr constant [ 13 x i8] c "hello ollvm\0A\00" , align 1 ; Function Attrs: noinline nounwind uwtable define i32 @main() #0 { % 1 = alloca i32, align 4 % 2 = alloca i32, align 4 store i32 0 , i32 * % 1 , align 4 store i32 5 , i32 * % 2 , align 4 % 3 = load i32, i32 * % 2 , align 4 % 4 = icmp slt i32 % 3 , 10 br i1 % 4 , label % 5 , label % 11 ; <label>: 5 : ; preds = % 0 % 6 = load i32, i32 * % 2 , align 4 % 7 = add i32 % 6 , 3116065 / / + 又 - ,将简单指令变成复杂的指令 % 8 = add i32 % 7 , 1 % 9 = sub i32 % 8 , 3116065 % 10 = add nsw i32 % 6 , 1 store i32 % 9 , i32 * % 2 , align 4 br label % 12 ; <label>: 11 : ; preds = % 0 br label % 12 ; <label>: 12 : ; preds = % 11 , % 5 % 13 = call i32 (i8 * , ...) @printf(i8 * getelementptr inbounds ([ 13 x i8], [ 13 x i8] * @. str , i32 0 , i32 0 )) ret i32 0 } declare i32 @printf(i8 * , ...) #1 attributes #0 = { noinline nounwind uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{! 0 } ! 0 = !{! "Obfuscator-LLVM clang version 4.0.1 (based on Obfuscator-LLVM 4.0.1)" } |
- 控制流平展模式
- clang hello.c
-mllvm -fla
-o hellofla(编译成可执行文件) - clang hello.c
-emit-llvm
-mllvm -fla-S
-o hellofla.ll(编译成可执行文件)
- clang hello.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | ; ModuleID = 'hello.c' source_filename = "hello.c" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" @. str = private unnamed_addr constant [ 13 x i8] c "hello ollvm\0A\00" , align 1 ; Function Attrs: noinline nounwind uwtable define i32 @main() #0 { % 1 = alloca i32 % 2 = alloca i32, align 4 % 3 = alloca i32, align 4 store i32 0 , i32 * % 2 , align 4 store i32 5 , i32 * % 3 , align 4 % 4 = load i32, i32 * % 3 , align 4 store i32 % 4 , i32 * % 1 % 5 = alloca i32 store i32 1783974709 , i32 * % 5 br label % 6 ; <label>: 6 : ; preds = % 0 , % 19 % 7 = load i32, i32 * % 5 switch i32 % 7 , label % 8 [ i32 1783974709 , label % 9 i32 1316248837 , label % 13 i32 1830851195 , label % 16 i32 - 89102849 , label % 17 ] ; <label>: 8 : ; preds = % 6 br label % 19 ; <label>: 9 : ; preds = % 6 % 10 = load volatile i32, i32 * % 1 % 11 = icmp slt i32 % 10 , 10 % 12 = select i1 % 11 , i32 1316248837 , i32 1830851195 store i32 % 12 , i32 * % 5 br label % 19 ; <label>: 13 : ; preds = % 6 % 14 = load i32, i32 * % 3 , align 4 % 15 = add nsw i32 % 14 , 1 store i32 % 15 , i32 * % 3 , align 4 store i32 - 89102849 , i32 * % 5 br label % 19 ; <label>: 16 : ; preds = % 6 store i32 - 89102849 , i32 * % 5 br label % 19 ; <label>: 17 : ; preds = % 6 % 18 = call i32 (i8 * , ...) @printf(i8 * getelementptr inbounds ([ 13 x i8], [ 13 x i8] * @. str , i32 0 , i32 0 )) ret i32 0 ; <label>: 19 : ; preds = % 16 , % 13 , % 9 , % 8 br label % 6 } declare i32 @printf(i8 * , ...) #1 attributes #0 = { noinline nounwind uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{! 0 } ! 0 = !{! "Obfuscator-LLVM clang version 4.0.1 (based on Obfuscator-LLVM 4.0.1)" } |
- 控制流伪造模式
- clang hello.c
-mllvm -bcf
-o hellobcf(编译成可执行程序) - clang hello.c
-emit-llvm
-mllvm -bcf-S
-o hellobcf.ll(编译成可执行程序)
- clang hello.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | ; ModuleID = 'hello.c' source_filename = "hello.c" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" @. str = private unnamed_addr constant [ 13 x i8] c "hello ollvm\0A\00" , align 1 @x = common global i32 0 @y = common global i32 0 ; Function Attrs: noinline nounwind uwtable define i32 @main() #0 { % 1 = alloca i32, align 4 % 2 = alloca i32, align 4 store i32 0 , i32 * % 1 , align 4 store i32 5 , i32 * % 2 , align 4 % 3 = load i32, i32 * % 2 , align 4 % 4 = icmp slt i32 % 3 , 10 br i1 % 4 , label % 5 , label % 8 ; <label>: 5 : ; preds = % 0 % 6 = load i32, i32 * % 2 , align 4 % 7 = add nsw i32 % 6 , 1 store i32 % 7 , i32 * % 2 , align 4 br label % 27 ; <label>: 8 : ; preds = % 0 % 9 = load i32, i32 * @x % 10 = load i32, i32 * @y % 11 = sub i32 % 9 , 1 % 12 = mul i32 % 9 , % 11 % 13 = urem i32 % 12 , 2 % 14 = icmp eq i32 % 13 , 0 % 15 = icmp slt i32 % 10 , 10 % 16 = or i1 % 14 , % 15 br i1 % 16 , label % 17 , label % 29 ; <label>: 17 : ; preds = % 8 , % 29 % 18 = load i32, i32 * @x % 19 = load i32, i32 * @y % 20 = sub i32 % 18 , 1 % 21 = mul i32 % 18 , % 20 % 22 = urem i32 % 21 , 2 % 23 = icmp eq i32 % 22 , 0 % 24 = icmp slt i32 % 19 , 10 % 25 = or i1 % 23 , % 24 br i1 % 25 , label % 26 , label % 29 ; <label>: 26 : ; preds = % 17 br label % 27 ; <label>: 27 : ; preds = % 26 , % 5 % 28 = call i32 (i8 * , ...) @printf(i8 * getelementptr inbounds ([ 13 x i8], [ 13 x i8] * @. str , i32 0 , i32 0 )) ret i32 0 ; <label>: 29 : ; preds = % 17 , % 8 br label % 17 } declare i32 @printf(i8 * , ...) #1 attributes #0 = { noinline nounwind uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{! 0 } ! 0 = !{! "Obfuscator-LLVM clang version 4.0.1 (based on Obfuscator-LLVM 4.0.1)" } |
- OLLVM三种混淆模式同时作用在hello.c文件下
- clang hello.c
-mllvm -sub
-mllvm -fla
-mllvm -bcf
-o helloall(编译成可执行文件) - clang hello.c
-emit-llvm
-mllvm -sub -mllvm -fla -mllvm -bcf-S
-o helloall(编译成汇编文件)
- clang hello.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | ; ModuleID = 'hello.c' source_filename = "hello.c" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" @. str = private unnamed_addr constant [ 13 x i8] c "hello ollvm\0A\00" , align 1 @x = common global i32 0 @y = common global i32 0 ; Function Attrs: noinline nounwind uwtable define i32 @main() #0 { % 1 = alloca i1 % 2 = alloca i32 * % 3 = alloca i1 % 4 = alloca i1 % 5 = load i32, i32 * @x % 6 = load i32, i32 * @y % 7 = sub i32 0 , 1 % 8 = add i32 % 5 , % 7 % 9 = sub i32 % 5 , 1 % 10 = mul i32 % 5 , % 8 % 11 = urem i32 % 10 , 2 % 12 = icmp eq i32 % 11 , 0 store i1 % 12 , i1 * % 4 % 13 = icmp slt i32 % 6 , 10 store i1 % 13 , i1 * % 3 % 14 = alloca i32 store i32 - 1874118603 , i32 * % 14 br label % 15 ; <label>: 15 : ; preds = % 0 , % 156 % 16 = load i32, i32 * % 14 switch i32 % 16 , label % 17 [ i32 - 1874118603 , label % 18 i32 - 592237300 , label % 26 i32 - 775182973 , label % 60 i32 - 1921775119 , label % 63 i32 1540843771 , label % 72 i32 - 1196761768 , label % 87 i32 - 2140678259 , label % 102 i32 464701823 , label % 103 i32 1734380246 , label % 131 i32 - 1548223981 , label % 147 i32 1197949853 , label % 148 i32 396207527 , label % 153 i32 - 58706652 , label % 154 ] ; <label>: 17 : ; preds = % 15 br label % 156 ; <label>: 18 : ; preds = % 15 % 19 = load volatile i1, i1 * % 4 % 20 = load volatile i1, i1 * % 3 % 21 = and i1 % 19 , % 20 |
- 分析汇编也可以把EFL x64文件加载到IDA中分析,可以看到混淆后的流程变得非常复杂了
- 未混淆的
- OLLVM指令替换模式
把简单的运算变成复杂的运算
OLLVM控制流平展模式
增加很多条件分支
OLLVM控制流伪造模式
不仅会打乱流程,还会添加花指令
- OLLVM三种混淆模式同时作用
加壳
壳
壳
是指在一个程序的外面再包裹上另外一段代码,保护里面的代码不被非法修改或反编译的程序。它们一般都是先于程序运行,拿到控制权,然后完成它们保护软件的任务。- 壳的加载过程:
- 压缩壳:
- UPX
- UPX是一个以命令行方式操作的可执行文件经典免费压缩程序,压缩算法自己实现,速度极快。
(开源)主页:http://upx.sourceforge.net
- UPX是一个以命令行方式操作的可执行文件经典免费压缩程序,压缩算法自己实现,速度极快。
- ASPack
- ASPack是Win32可执行文件庄缩软件,可压缩Windows 32位可执行文件(.exe)以及库文件(.dll、.ocx),文件压缩比率40%~70%
主页:http://www.aspack.com
- ASPack是Win32可执行文件庄缩软件,可压缩Windows 32位可执行文件(.exe)以及库文件(.dll、.ocx),文件压缩比率40%~70%
- 加密壳:
a. ASProtect
- ASProtect是一款非常强大的Windows 32位保护工具,开发者是俄国人Aexey Solodovnikov,拥有压缩、加密、反踪代码、反-反汇编代、CRC校验和花指令等保护措施。
- 使用Blowfish、Twofish、TEA等强劲的加密算法,还用RSA1024作为注册整钥生成器。它还通过API钩子(API hooks,包括Import hooks ( GPA hook)和Eport hooks)与加壳的程序进行通信。其至用到了多态变形引擎(Polymorphic Engine)反Apihook代码(Anti-Apihook Code)和BPE32的多态变形引擎(RPF32的Polvmornhic Engine).
- ASProtect为软件开发人员提供
SDK
(在自己代码内部打桩),实现加密程序内外结合。主页:http://www.aspack.com/
b. Armadillo
- Armadillo(穿山甲),是一款应用面较广的壳,可以运用各种手段来保护你的软件,同时也可以为软件加上种种限制,包括时间、次数,启动画面等等。很多商用软件采用其加壳。Armadllo中比较强太的保护选项是Nanomites保护(即
cc保护
),用的好能提高强度,其他选项没什么强度。主页:http://www.siliconrealms.com/
- Armadillo(穿山甲),是一款应用面较广的壳,可以运用各种手段来保护你的软件,同时也可以为软件加上种种限制,包括时间、次数,启动画面等等。很多商用软件采用其加壳。Armadllo中比较强太的保护选项是Nanomites保护(即
- c. Themida
- Themida是oreans(西班牙著名的软性系统保护公司)的一款商业壳。Themida 1.1以前版本带驱动,稳定性有些影响。
- Themida最大特点就是虚拟机保护技术,在程序中用
SDK
将关键的代码让Themmida用虚拟机保护起来。 - Themida的缺点是生成的软件有些大。
- Winkcense壳和Themida是同一公司的一个系列产品,主要多了一个协议,可以设定使用时间,运行次数等功能,两者核心保护是一样的
主页:www.oreans.com
- d. VMProtect
- VMProtect是一款纯虚拟机保护软件,是当前最强的虚拟机保护软件,经VMProtect处理过的代码,还原难度极大。
- 流行的做法,先用VMProtect将核心代码处理一下(VMProtect的稳定性不是特别高,如果对整个程序进行加壳会影响程序的执行效率,折中方案:用
SDK
只对关键的代码处理),再选用—款兼容性好的壳保护。比如继续用Asprotect, Themida等加壳软件进一步保护。 - VMP与传统的壳(压缩,加密)相比,它会修改目标,让目标的部分指令(形成虚拟机下的字节码)在它创建的虚拟机环境下运行,虚拟环境中无
操作数比较指令
、条件跳转
和无条件跳转指令
- vmp的虚拟机其实是一个
字节码解释器
,循环的读取指令并执行 - 虚假跳转和垃圾指令, vmp会使用大量的虚拟跳转和垃圾指令将原有简单的代码变得复杂,类似
OLLVM
- vmp中只有一个逻辑运算指令
nor
,它可以模拟not
and
or
xor
四个逻辑运算指令,进一步加大逆向分析的难度。主页:www.VMProtect.ru
UPX加壳
- 加壳过程:
下载工具包:http://upx.sourceforge.net
- cmd以管理员身份运行,
upx.exe Xxx.exe
加upx壳
- cmd以管理员身份运行,
经过UPX压缩的
win32/pe文件
,包含三个区段:UPX0,UPX1,.rsrc
或UPX0,UPX1,UPX2
(原文件无资源时)。UPX0
:在文件中没有内容它的Virtual size
加上UPX1
的构成了原文件全部区段需要的内存空间,相当于把原来区段合并了。UPX1
:起始位置为需解压缩的源数据,目标地址为UPX0
基址。紧接着源数据块是UPX stub
,即壳代码。典型的pushad/popad结构,常用ESP定律
找到OEP
来脱UPX。.rsrc
/UPX2
:原文件有资源时,含原资源段的完整头部和极少部分资源数据(类型为ICON、 GROUP ICON、VERSION和MANIFEST),以保证explorer.exe
能正常显示win32/pe文件
图标、版本信息。还有就是UPX自己的Imports内容,导出表的库名和函数名(如果有)。
用IDA打开加了UPX壳的.exe可以发现:
- 很多数据IDA无法进行反汇编,看到的都是压缩的数据
- 导出表的函数不属于程序本身,而是壳要用到的函数
- 没有.text和.data节,取而代之是UPX0,UPX1
VMP加壳
- 方法1:加壳时,须告诉VMProtect要加密的代码地址(可以用调试器,如OllyDbg跟踪)然后将该地址添加到VMProtect 。
- 方法2:由于VMP支持SDK,可以编程时插入一个
标记
,然后在加密时,VMProtect会认出这些标记,并在有标记的地方进行保护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | #define VMPBEGIN \ __asm_emit 0xEB \ __asm_emit 0x10 \ __asm_emit 0x56 \ __asm_emit 0x4D \ __asm_emit 0x50 \ __asm_emit 0x72 \ __asm_emit 0x6F \ __asm_emit 0x741 \ __asm_emit 0x65 \ __asm_emit 0x63 \ __asm_emit 0x74 \ __asm_emit 0x20 \ __asm_emit 0x62 \ __asm_emit 0x65 \ __asm_emit 0x67 \ __asm_emit 0x69 \ __asm_emit 0x6E \ __asm_emit 0x00 #define VMPEND \ __asm__emit 0xEB \ __asm_emit 0x0E \ __asm_emit 0x56 \ __asm_emit 0x4D \ __asm_emit 0x50 \ __asm_emit 0x72 \ __asm_emit 0x6F \ __asm_emit 0x74 \ __asm_emit 0x65 \ __asm_emit 0x63 \ __asm_emit 0x74 \ __asm_emit 0x20 \ __asm_emit 0x65 \ __asm_emit 0x6E \ __asm_emit 0x64 \ __asm_emit 0x00 / / VMProtect 的SDK 标志 VMPBEGIN / / 要加密的核心代码片断 VMPEND |
- 开发一个简单的MFC程序
1 2 3 4 5 6 | void CVMPDemoDlg::OnBnClickedOk() { / / TODO: 在此添加控件通知处理程序代码 / / CDialogEx::OnOK(); MessageBox(_T( "hello VMP" ), _T( "VMP" ), MB_OK); } |
- 没加壳之前逆向分析
- 可以直接搜索关键字
hello
就可以找到MessageBox函数,按F5可以查看到反C代码
- 可以直接搜索关键字
- 加壳再逆向分析 对照实验
3.1 安装VMProtect,安装目录获取库文件:
VMProtect Ultimate\Include\C
- VMProtectSDK.h 给应用程序加壳
- VMProtectDDK.h 给驱动加壳
VMProtect Ultimate\Lib\Windows\
- VMProtectDDK32.lib
- VMProtectDDK32.sys
- VMProtectDDK64.lib
- VMProtectDDK64.sys
- VMProtectSDK32.dll
- VMProtectSDK32.lib
- VMProtectSDK64.dll
- VMProtectSDK64.lib
3.2 拷贝到工程目录下
1 2 | - 把头文件拷贝到源代码目录下 - 把.dll / .sys和.lib文件拷贝到源代码目录下 |
3.3 包含头文件和加载.lib文件,给关键代码加上VMP壳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include "VMProtectSDK.h" #pragma comment(lib,"VMProtectSDK32.lib") / / 关键代码,加上VMP壳 void CVMPDemoDlg::OnBnClickedOk() { VMProtectBegin( "tagname" ); / / TODO: 在此添加控件通知处理程序代码 / / CDialogEx::OnOK(); MessageBox(_T( "hello VMP" ), _T( "VMP" ), MB_OK); VMProtectEnd(); } / / / 其他标记 / / 开始保护处标记 VMProtectBegin( const char * ); / / 开始虚拟化代码处标记(包括保护设置) VMProtectBeginvirtualization( const char * ); / / 开始变异代码处标记(包括保护设置) VMProtectBeginMutation(const charn * ) / / 开始虚拟 + 代码变异标记处 VMPrckctBeginLltra(cont char * ) VMProtectBeginvitualizationLoekEykey(const char * ); VMProtectBegimUltraLockEykKey(const char * ); / / 保护结束处标记 VMRrotectEnd(void) / / 检测调试 BOOL VKMProtectlsDebuggerPresent( BOOL ); / / 检测虚拟机 BOOL VMProtectlsValidlmageCRC(void) / / 映像文件CRC校验 BOoL VMProtectisValidimageCRC(void) / / 解密被保护的名为字符串A,A表示是多字节字符串 char * VMProtectDecryptStringA(const char * value); / / 解密被保护的名为字符串W,W表示宽字节字符串 wchar_t * VMProtectDecryptStringW(const wchar_t * value); / / / eg / / char Decrypt(chtar key, char * buff, long len ){} Decrypt(VMProtectDecryptStringA( "mm1 23456" ), / / 此时密文密钥key被解密 buff, 256 ); |
3.4 编译出release版本后,用VMProtect进行加壳
3.5 用IDA逆向分析
- 可以直接搜索关键字
hello
已经看不到结果了
去模糊化方法
- 基于
IDC
脚本:使用IDCC执行每一项CPU操作,模拟程序执行,修该程序数据库。- 不足:复杂的情况(加了upx壳,aspack壳,或者版本更新,脚本不适用了)缺乏通用性
- 模拟器去模糊:ida-x86emu(ALT+F8启动)
- 查看是否加壳以及壳的种类
- 查壳工具:PEiD、ExeinfoPe
- 查找程序的真正入口点(
OEP
,Original Entry Point);如何定位OEP: https://bbs.pediy.com/thread-218605.htm
- 抓取内存映像文件
- 用OD加载加了壳的程序,让其运行(加了壳的程序要运行,肯定会加载入内存中,把自己的壳脱掉,所以在内存中,程序是脱了壳的),当它脱完壳的时候,找到它的
OEP
后,把文件从内存中Dump出来
- 用OD加载加了壳的程序,让其运行(加了壳的程序要运行,肯定会加载入内存中,把自己的壳脱掉,所以在内存中,程序是脱了壳的),当它脱完壳的时候,找到它的
- 输入表重建
- 在PE文件还在磁盘中适合IMP表和IAT表和IAT指向同一个结构体数组
- 一旦PE文件运行起来之后(即加载入内存之后),导入表的OriginalFirstLink依然指向INT指向一个结构体数组(存放函数名+下标),但导入表的FirstLink和IAT表指向的是新的地方(存放函数的地址,而不是函数名+下标了),所以需要为抓取内存映像文件重建输入表(也就是导入表),否则抓取内存映像文件依然无法执行(因为调用的函数都不知道)
- EXE 的所有的
导入函数信息
都会写入输入表(也就是导入表)
中,在PE文件映射到内存后,windows将相应的DLL文件装入,EXE文件通过输入表
找到相应的DLL中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由输入表参与的。 - 重建输入表也有专门的工具可以处理
UPX脱壳
OllyDbg
- OllyDbg常见快捷键
- F8,单步执行,不进入call
- F7,单步执行,进入call
- F9(运行)
- F4,执行到光标处,执行循环的时候很好用,比如光标在循环外,按F4可以跳出循环
- CTRL+F9:执行直到返回
- ALT+F9:从系统领空直接返回,比如调试当前处于系统API,按ALT+F9直接执行完系统API返回
- F2,设置断点,int 3断点
- F3,装入要调试的程序
- Ctrl+ F2,重新开始调试程序,程序跑飞的时候用
- OllyDbg单步跟踪原则:
- 遇到近CALL 用F7
- 遇到远CALL 用F8
- 遇到跳转到向上地址(比如循环),光标选中下一个地址,按F4
- 遇到大的跳转,要注意,离OEP就近了,按F8小心单步跟踪
- OllyDbg抓取内存映像文件
- OllyDbg找
OEP
很繁琐,对于UPX壳这种简单的壳,可以使用upxshell.exe
进行脱壳其他类型的脱壳
- ASProtect脱壳
- ASProtect unpacker,PE_Kill出品,功能强大,能自动修复SDK,ASProtect2.X之前的版本完全支持。
- VMP脱壳
https://bbs.pediy.com/forum-88.htm 加壳脱壳精华区
实操练习
- 破解密钥
- 字符密文
- (密钥字符-5)^7 == 密文
- 纯数字密文
- 解密数组[数字密钥作为下标] == 密文
-看雪题库 (pediy.com)
- 解密数组[数字密钥作为下标] == 密文
- 看雪-安全培训|安全招聘|www.kanxue.com
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]java和smali汇编 2736
- [原创]Android逆向前期准备(下) 4545
- [原创]native层逆向分析(上篇) 14047
- [原创]Java层逆向分析方法和技巧 7281