c++反汇编与逆向分析 小结
第一章 熟悉工作环境和相关工具
1.1 熟悉OllyDBG 操作技巧
1.2 反汇编静态分析工具 IDA(最专业的逆向工具)
快捷键 功能
Enter 跟进函数实现
Esc 返回跟进处
A 解释光标处的地址为一个字符串的首地址
B 十六进制数与二进制数转换
C 解释光标处的地址为一条指令
D 解释光标处的地址为数据,没按一次将会转换这个地址的数据长度
G 高速查找到相应的地址
H 十六进制数与十进制数转换
K 将数据解释为栈变量
; 加入�凝视
M 解释为枚举成员
N 又一次命名
O 解释地址为数据段偏移量
T 解释数据为一个结构体成员
X 转换视图到交叉參考模式
Shift+F9 加入�结构体
认识各视图功能
IDA View-A:分析视图窗体,用于显示分析结果。
Hex View-A:二进制视图窗体,打开文件的二进制信息。
Exports: 分析文件里的导出函数信息窗体
Imports: 分析文件里的导入函数信息窗体
Names: 名称窗体,分析文档中用到的标题号名称。
Functions: 分析文件里的函数信息窗体
Structures:加入�结构体信息窗体
Enums: 加入�枚举信息窗体
SIG文件的制作步骤 1、从LIB文件里提取出OBJ文件 2、将每一个OBJ文件制作成PAT文件 3、多PAT文件联合编译SIG文件
SIG文件制作批处理文件的过程
md %l_objs
cd %l_objs
for /F %%i in ('link - lib /list %l.lib') do link -lib /extract:%%i %l.lib
for %%i in (*.obj) do pcf %%i
sigmake -n"%l.lib" *.pat %l.sig
if exist %l.exc for %%i in (%l.exc) do find /v ";" %%i >adc.exc
if exist %l.exc for %%i in (%l.exc) do > abc.exc more +2 "%%i"
copy abc.exc %l.exc
del abc.exc
sigmake -n"%l.lib" *.pat %l.sig
copy %l.sig ..\%l.sig
cd ..
del %l_objs /s /q
rd %l_objs
代码说明
通过 md%l_objs 创建文件夹,文件夹名称为參数1
通过 cd %l_objs 进入刚刚创建的文件夹
在当前文件夹下 循环取出 LIB 中的OBJ名称 并逐个生成相应的OBJ
循环将生成的OBJ文件通过PCF转换相应的PAT文件
将文件夹下的全部的PAT文件生成一个SIG文件
将生成的SIG文件复制到上级文件夹中
返回上级文件夹,删除创建的文件夹中的全部数据
将创建文件夹删除
1.3 反汇编引擎的工作原理
Instruction Prefixes: 指令前缀
反复指令:REP、REPE\REPZ
跨段指令:如 MOV DWORD PTR FS:[xxxx],0
32位 16位 互换 如:MOV AX,WORD PTR DS:[EAX] MOV EAX,DWORD PTR DS:[BX+SI]
Opcode: 指令操作码
Mode R/M:操作数类型
SIB:辅助 Mode R/M,计算地址偏移
Displacement:辅助 Mode R/M, 计算地址偏移
Immediate:马上数
第二章 基本数据类型的表现形式
2.1 整数类型
无符号整数 有符号整数 差别就是最大位
2.2 浮点数类型
浮点数的编码方式 在内存中占4字节 最高位符号 剩余的31位中 左取8位指数 其余尾数
浮点寄存器的使用就是压栈/出栈的过程
比如
float fFloat =(float)argc; //argc为命令行參数
fild dword ptr [ebp+8] ;将地址ebp+8处的整形数据转换浮点型,并入st(0)中,相应变量 argc
fst dword ptr [ebp-4] ;从st(0)中取出数据以浮点编码方式放入地址ebp-4中,相应变量fFloat
printf("%f",fFloat);
;这里对esp运行减8操作 是因为浮点数作为变參函数的參数时 须要转换为双精度浮点值
;这步操作时提前准备8字节的栈空间,以便于存放double 数据
sub esp,8
fstp dword ptr [esp] ;将st(0) 中的 数据传入esp中,并弹出st(0)
push offset string "%f"
call printf
add esp,0ch
argc =(int)fFloat;
fld dwrod ptr [ebp-4] ;将地址 ebp-4处的数据以浮点数类型压入st(0)中
call _ftol ;调用函数_ftol 进行浮点类型转换
mov dword ptr [ebp+8],eax ;转换后结果放入eax中,并转递到ebp+8地址处
类型转换函数 _ftol 的实现
push ebp
mov ebp,esp
add esp,0f4h
;浮点异常检查,CPU 与 FPU的同步工作
wait
fnstcw word ptr [ebp-2]
wait
mov ax,word ptr [ebp-2]
or ah,0ch
mov word ptr [ebp-4],ax
fldcw word ptr [ebp-4]
;从st(0)中取出8字节数据转换成整形并传入ebp-0ch中
;将st(0) 从栈中弹出
flstp dword ptr [ebp-och]
fldcw word ptr [ebp-2]
;使用eax 保存长整形的低4位,用于返回
mov eax,dword ptr [ebp-0ch]
;使用edx 保存长整形数据的高4字节,用于返回
mov edx,dword ptr [ebp-8]
;释放栈
leave
ret
2.3 字符和字符串
字符串是由一系列依照一定的编码顺序线性排列的字符组成的
2.4 布尔类型
2.5 地址、指针和引用
地址是一个有32位二进制数字组成的值。指针是用于保存这个编号的一种变量类型。
指针的取内容操作分为两个步骤:先取出指针中保存的地址信息,然后针对这个地址进行去内容。
两指针相加是没有意义的
引用类型就是指针类型
2.6 常量
数据在程序执行前就已经存在,他们被编译到可执行文件里。
#define定义的常量名称,编译器对其进行编译时,会将代码中的宏名称替换成相应的信息。
const 是为了添加�程序的健壮性而存在的
#define 是一个真常量,而const 却是由编译器推断实现的常量,是一个假常量。
第三章 认识启动函数,找到用户入口
mian函数被调用前要先调用的函数 例如以下
GetVersion() 版本
_heap_init() 初始化堆空间
GetCommandLineA() 获得命令行首地址
_crtGetEnviromentStringsA() 环境变量首地址
_setargv() 命令行參数首地址
_setenvp() 把得到的每条环境变量字符串的首地址存放在字符指针数组中
_cinit() 全局数据和浮点寄存器的初始化
第四章 观察各种表达式的求值过程
加法 add 常量传播 和 常量折叠
常量传播--将编译期间可计算出结果的变量转换成常量,这样就降低了变量的使用。
常量折叠--全部的常量计算都将被计算结果取代。
减法 sub
比如:
int nVarOne=argc;
012E13CE mov eax,dword ptr [argc]
012E13D1 mov dword ptr [nVarOne],eax
int nVarTwo=0;
012E13D4 mov dword ptr [nVarTwo],0
scanf("%d",&nVarTwo);
012E13DB mov esi,esp
012E13DD lea eax,[nVarTwo]
012E13E0 push eax
012E13E1 push offset string "%d" (12E5750h)
012E13E6 call dword ptr [__imp__scanf (12E82BCh)]
012E13EC add esp,8
012E13EF cmp esi,esp
012E13F1 call @ILT+325(__RTC_CheckEsp) (12E114Ah)
nVarOne=nVarOne-100;
012E13F6 mov eax,dword ptr [nVarOne]
012E13F9 sub eax,64h
012E13FC mov dword ptr [nVarOne],eax
nVarOne=nVarOne+5-nVarTwo;
012E13FF mov eax,dword ptr [nVarOne]
012E1402 add eax,5
012E1405 sub eax,dword ptr [nVarTwo]
012E1408 mov dword ptr [nVarOne],eax
printf("nVarOne = %d \r\n",nVarOne);
012E140B mov esi,esp
012E140D mov eax,dword ptr [nVarOne]
012E1410 push eax
012E1411 push offset string "nVarOne = %d \r\n" (12E573Ch)
012E1416 call dword ptr [__imp__printf (12E82C4h)]
012E141C add esp,8
乘法 有符号 imul 和 无符号 mul 两种 样例直接编译
除法 有符号 idiv 和 无符号 div 两种 样例直接编译
推断除法 总结 1
mov eax,MagicNumber
imul …
sar edx,…
mov reg,edx
shr reg,1fh
add edx,reg
;此后直接仅仅用edx的值,eax弃而不用
推断除法 总结 2
mov eax,MagicNumber
;这里的reg表示通用寄存器,上例中为ecx,实际分析中还可能是其它寄存器
mul reg
sub reg,edx
shr reg,1
add reg,edx
shr reg,A ;这句也许没有
;此后直接仅仅用edx的值,eax弃而不用
推断除法 总结 3
mov eax,MagicNumber (大于7ffffffffh)
imul reg
add edx,reg
sar edx,…
mov reg,edx
shr reg,1fh
add edx,reg
;此后直接使用edx的值
推断除法 总结 4
mov eax,MagicNumber (大于7ffffffffh)
imul reg
sar edx,…
mov reg,edx
shr reg,1fh
add edx,reg
;此后直接使用edx的值
推断除法 总结 5
mov eax,MagicNumber (大于7ffffffffh)
imul reg
sub edx,reg
sar edx,…
mov reg,edx
shr reg,1fh
add edx,reg
;此后直接使用edx的值
算数结果溢出
溢出是因为数据进位后超出数据的保存范围导致的
进位-- 无符号数超出存储范围叫做进位
自增自减实例
int nVarOne=argc;
0098138E mov eax,dword ptr [argc]
00981391 mov dword ptr [nVarOne],eax
int nVarTwo=0;
00981394 mov dword ptr [nVarTwo],0
nVarTwo=5+(nVarOne++);
0098139B mov eax,dword ptr [nVarOne]
0098139E add eax,5
009813A1 mov dword ptr [nVarTwo],eax
009813A4 mov ecx,dword ptr [nVarOne]
009813A7 add ecx,1
009813AA mov dword ptr [nVarOne],ecx
nVarTwo=5+(++nVarOne);
009813AD mov eax,dword ptr [nVarOne]
009813B0 add eax,1
009813B3 mov dword ptr [nVarOne],eax
009813B6 mov ecx,dword ptr [nVarOne]
009813B9 add ecx,5
009813BC mov dword ptr [nVarTwo],ecx
nVarOne=5+(nVarTwo++);
009813BF mov eax,dword ptr [nVarTwo]
009813C2 add eax,5
009813C5 mov dword ptr [nVarOne],eax
009813C8 mov ecx,dword ptr [nVarTwo]
009813CB add ecx,1
009813CE mov dword ptr [nVarTwo],ecx
nVarOne=5+(++nVarTwo);
009813D1 mov eax,dword ptr [nVarTwo]
009813D4 add eax,1
009813D7 mov dword ptr [nVarTwo],eax
009813DA mov ecx,dword ptr [nVarTwo]
009813DD add ecx,5
009813E0 mov dword ptr [nVarOne],ecx
表达式短路--通过逻辑与运算和逻辑或运算使语句依据条件在运行时发生中断。
位运算
int BitOperation(int argc)
{
00BE1370 push ebp
00BE1371 mov ebp,esp
00BE1373 sub esp,0C0h
00BE1379 push ebx
00BE137A push esi
00BE137B push edi
00BE137C lea edi,[ebp-0C0h]
00BE1382 mov ecx,30h
00BE1387 mov eax,0CCCCCCCCh
00BE138C rep stos dword ptr es:[edi]
argc=argc<<3;
00BE138E mov eax,dword ptr [argc]
00BE1391 shl eax,3
00BE1394 mov dword ptr [argc],eax
argc=argc>>5;
00BE1397 mov eax,dword ptr [argc]
00BE139A sar eax,5
00BE139D mov dword ptr [argc],eax
argc=argc|0xffff0000;
00BE13A0 mov eax,dword ptr [argc]
00BE13A3 or eax,0FFFF0000h
00BE13A8 mov dword ptr [argc],eax
argc=argc&0x0000ffff;
00BE13AB mov eax,dword ptr [argc]
00BE13AE and eax,0FFFFh
00BE13B3 mov dword ptr [argc],eax
argc=argc^0xffff0000;
00BE13B6 mov eax,dword ptr [argc]
00BE13B9 xor eax,0FFFF0000h
00BE13BE mov dword ptr [argc],eax
argc=~argc;
00BE13C1 mov eax,dword ptr [argc]
00BE13C4 not eax
00BE13C6 mov dword ptr [argc],eax
return argc;
00BE13C9 mov eax,dword ptr [argc]
}
00BE13CC pop edi
00BE13CD pop esi
00BE13CE pop ebx
00BE13CF mov esp,ebp
00BE13D1 pop ebp
00BE13D2 ret
reg==A?C:b+c -->等效于以下
;遇到sub/neg/sbb 就表明是等值比較了
sub reg,A
neg reg
sbb reg,reg
and reg,B
add reg,C 若等值条件成立,其结果为C 否则b+c
比如
int BitOperation(int argc)
{
00DC1370 push ebp
00DC1371 mov ebp,esp
00DC1373 sub esp,0C0h
00DC1379 push ebx
00DC137A push esi
00DC137B push edi
00DC137C lea edi,[ebp-0C0h]
00DC1382 mov ecx,30h
00DC1387 mov eax,0CCCCCCCCh
00DC138C rep stos dword ptr es:[edi]
return argc ? 8 : 20;
00DC138E mov eax,dword ptr [argc]
00DC1391 neg eax
00DC1393 sbb eax,eax
00DC1395 and eax,0FFFFFFF4h
00DC1398 add eax,14h
}
00DC139B pop edi
00DC139C pop esi
00DC139D pop ebx
00DC139E mov esp,ebp
00DC13A0 pop ebp
00DC13A1 ret
代码优化一般有四个方向:
1 运行速度优化
2 内存存储空间优化
3 磁盘存储空间优化
4 编译时间优化
第五章 流程控制语句的识别
if语句
总结:
;先运行各类影响标志位的指令
;其后是各种条件跳转指令
jxx xxxxx
if...else...语句
总结:
;先运行影响标志位的相关指令
jxx ELSE_BEGIN
IF_BEGIN:
……
IF_END:
jmp ELSE_END
ELSE_BEGIN:
……
ELSE_END;
用if构成的多分支流程
总结:
;会影响标志位的指令
jxx ELSE_IF_BEGIN
IF_BEGIN:
……
IF_END:
jmp END
ELSE_IF_BEGIN:
;可影响标志位的指令
jxx ELSE_BEGIN
……
ELSE_IF_END:
jmp END
ELSE_BEGIN:
……
END:
……
Switch 的真相
总结 1
mov reg,mem
;影响标志位的指令
jxx xxxx
;影响标志位的指令
jxx xxx
;影响标志位的指令
jxx xxx
jmp END
……
jmp END
……
jmp END
……
END:
……
总结 2
mov reg,mem
; 对变量进行运算,对齐case地址表的0下标,非必要
;上例中的eax 也可用其它寄存器替换,这里也能够是其它类型的运算
lea eax,[reg+xxxx]
;影响标志位的指令,执行范围检查
jxx DEFAULT_ADDR
jmp dword ptr [eax*4+xxxx] ;
总结 3
mov reg,mem
sub reg,1
mov mem,reg
;影响标志位的指令
jxx xxxx
mov reg , [mem]
;eax不是必要用的,可是之后的数组查询用到的寄存器一定是此处使用的寄存器
xor eax,eax
mov al,byte ptr [xxxx] [reg]
jmp dword ptr [eax*4+xxxx]
do/while/for的比較
do 总结
DO_BEGIN:
.....
;影响标志位的指令
jxx DO_BEGIN ;向上转
while 总结
WHILE_BEGIN:
;影响标志位的指令
jxx WHILE_END
.....
jmp WHILE_BEGIN
WHILE_END:
for 总结
mov mem/reg,xxx
jmp FOR_CMP
FOR_STEP:
;改动循环变量step
mov reg,step
add reg,xxxx ; 改动循环变量的计算过程
mov setp,eax
FOR_CMP:
mov ecx,dword ptr step
;推断循环变量和循环终止条件stepend的关系,满足条件则推出 循环
cmp ecx,setpend
jxx FOR_END
……
jmp FOR_STEP
FOR_END:
第六章 函数的工作原理
栈帧的形成和关闭
栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”.
esp 与 ebp 栈顶 与 栈底
当 esp 小于 ebp时 就形成了栈帧,栈帧中能够寻址的数据有局部 函数返回地址 函数參数等
vc环境下的调用约定有三种
_cdecl: c/c++默认的调用方式,调用方平衡堆栈,不定參数的函数能够使用。
_stdcall:被调用方平衡堆栈,不定參数的函数无法使用
_fastcall:寄存器方式传參,被调用方平衡堆栈,不定參数的函数无法使用。
使用ebp 或 esp 寻址 寄存器相对间接寻址方式
函数的參数 通过栈结构进行传递。
C/C++将不定长參数的函数定义为:
至少要有一个參数,
全部不定长的參数类型传入时都是dword类型
需在某一个參数中描写叙述參数总个数或将最后一个參数赋值为结尾标记
函数调用的一般工作流程
1 參数传递
通过栈或寄存器方式传递參数
2 函数调用,将返回地址压栈
使用 call 指令调用參数,并将返回地址压入栈中
3 保存栈底
使用栈空间保存调用方的栈底寄存器 ebp
4 申请栈空间和保存寄存器环境
依据函数内部局部变量的大小抬高栈顶让出相应的栈空间
而且将要改动的寄存器保存在栈中
5 函数实现代码
6 还原环境
7 平衡栈空间
平衡局部变量使用的栈空间
8 ret 返回,结束函数调用
更新eip
9 调整 esp,平衡栈顶
此处为_cdecl特有的方式,用于 平衡參数占用的栈顶
两种编译选项下的函数识别
debug
push reg/mem/imm
……
call reg/mem/imm
add esp,xxx
jmp FUN_ADDR
FUN_ADDR:
push ebp
……
mov eax,0cccccccch
rep stos dword ptr [edi];
……
pop ebp
ret
Release
push reg/mem/imm
……
call reg/mem/imm
add esp,xxxx
;函数实现内没有将局部变量初始化为0cccccccch
;若在函数体内不存在内联汇编或异常处理等代码 则使用esp 寻址