原文:https://ricardonarvaja.info/WEB/IDA DESDE CERO/
实验材料:https://github.com/aprz512/reversing-with-ida-pro-from-scratch
在这个 IDA 系列教程中,会由浅入深地讲解包括:IDA基本操作、静态及动态逆向分析、脱壳破解、漏洞开发等。
以 windows 程序为例(x86汇编)。
将会比较少的介绍基础知识,如果遇到不能理解的可看前一个系列与《CSAPP》。
MOV
以 VEViewer.exe 为例。
.text:0042F302 A1 50 FC 46 00 mov eax, dword_46FC50
.text:0042F307 8B 50 0C mov edx, [eax+0Ch]
.text:0042F30A 8D 4C 90 14 lea ecx, [eax+edx*4+14h]
.text:0042F30E 8B 50 08 mov edx, [eax+8]
.text:0042F311 56 push esi
.text:0042F312 8B F0 mov esi, eax
.text:0042F314 51 push ecx
.text:0042F315 8D 44 90 14 lea eax, [eax+edx*4+14h]
.text:0042F319 50 push eax
.text:0042F31A B9 50 FC 46 00 mov ecx, offset dword_46FC50
看第一行与最后一行的 mov 指令的区别。
mov eax, dword_46FC50
是将 0x46FC50 地址上储存的内容放入 eax。
mov ecx, offset dword_46FC50
是将 0x46FC50 放入 ecx。
因此在 IDA 中,地址前面有 OFFSET 这个词时,指代的是这个地址本身的数值,而没有 OFFSET 这个词时,指代的是这个地址上存储的内容。
像 mov edx, [eax+8]
这种有方括号的也是表示取地址内容的意思(将地址为 eax+8 处的内容放到 edx 里面)。
XCHG
XCHG A, B
指令将 A 与 B 的值互换。
栈指令
PUSH 将一个对象保存在栈的顶部(压栈)。
// 将 64 (dword)压入栈顶,并将 ESP 的值更新
CODE:0040104F 6A 64 push 64h ; 'd' ; lpIconName
POP 将最后存入栈顶的对象取出。
// 将栈顶元素放入 ebx,并将 ESP 的值更新
CODE:004011E6 5B pop ebx
看 CRACKME.EXE 程序。
CODE:004010B7 68 E7 20 40 00 push offset aCrackmeV10 ; "CrackMe v1.0"
CODE:004010BC 68 F4 20 40 00 push offset ClassName ; "No need to disasm the code!"
aCrackmeV10 以 a 开头,说明它是一个 ASCII 字符串。
双击字符串名称可以跳转到下面地方:
DATA:004020E7 ; CHAR WindowName[]
DATA:004020E7 43 72 61 63 6B 4D 65 20 76 31+WindowName db 'CrackMe v1.0',0 ; DATA XREF: start+B7↑o
在一般 C 语言源代码中,字符数组是这样定义的:
Char mystring[] = "Hello"
IDA 用了 2 行对这个字符数组进行描述,第一行说明这个字符串是 char[] 类型,第二行说明改字符串内容是“Crackme v1.0”,最后的 0 表示字符串的结尾。
按D
键会改变 windowname 变量的数据类型,强制 IDA 不将它视作字符数组而作为 db 或者说字节。
改变之后,虽然字符串内容不变,但是引用该字符串的地方会发生变化,比如:
CODE:004010B7 68 E7 20 40 00 push offset WindowName ; "CrackMe v1.0"
将字符串类型改成 byte 数据类型后,这里会变成 push offset byte_4020E7
。
按A
键将其转换为 ASCII 字符串,恢复原状。
当发现其他作为单独字节显示的字符串时,都可以进行该操作。找到字符串的起点按A
键,使显示更加易读。
一般在向函数传递参数的时候,都会使用 PUSH offset xxxxx
指令,将字符串的地址传递给函数。如果没有 offset 关键词,传递进去的就是 0x402110
地址上存储的内容,就是字符串的具体字节 55 4E 45 4D
。但是 API 函数不是这样运行的,他们一般接受指针或者字符串的起始地址作为参数。
数据段/代码段
CODE:00401068 A3 7C 20 40 00 mov ds:WndClass.hCursor, eax
CODE:0040106D C7 05 80 20 40 00 05 00 00 00 mov ds:WndClass.hbrBackground, 5
CODE:00401077 C7 05 84 20 40 00 10 21 40 00 mov ds:WndClass.lpszMenuName, offset aMenu ; "MENU"
CODE:00401081 C7 05 88 20 40 00 F4 20 40 00 mov ds:WndClass.lpszClassName, offset ClassName ; "No need to disasm the code!"
在以上的指令中,DS:这个标记表示程序将在数据区块(DS=DATA)的内存上写入。在讨论结构体的章节将会研究相应内容。而现在只要明白程序将把这个字符串起始的地址存储在数据区块上。
LEA
LEA 即 LOAD EFFECTIVE ADDRESS。
LEA A, B
指令将 B 的地址传递给 A。
该指令不会获取 B 存储的内容,只会传递地址或者后一个操作数的运算结果。这种方法普遍运用于获取变量参数的地址。
看 VEViewer.exe:
.text:00401191 8D 45 F4 lea eax, [ebp+var_C]
这条指令使用了 LEA ,将这个栈上的地址传递出来,如果是 MOV 指令,传递出来的则是保存在这个地址的内容。
LEA 指令尽管使用了中括号,但它只计算中括号中的表达式然后传递地址而不读取其中的内容。
通常 EBP 寄存器用来存储的栈的基础地址,通过在 EBP 值加上或者减去一个常量来获得每一个函数参数和局部变量的地址。
在[ebp+var_C]
处右键单击,可以查看这个变量地址计算的表达式,按Q
键可以在反汇编窗口中显示为[EBP-0Ch]。
在程序运行时 LEA 在获取这个函数栈的基础地址,减去 0Ch 计算出 EBP-0Ch 的取值,得出这个变量的实际地址。
LEA 也可以用于将中括号中的运算结果传递到目标寄存器,而不会读取结果地址上存储的内容。
例如: LEA EAX,[4+5]
指令将运算结果 9 传给 EAX,而不会像 MOV EAX,[4+5]
指令那样将地址 0x9 上存储的内容传给 EAX。
掌握 LEA 的用法,并掌握和 MOV 用法之间的区别是很重要的。LEA 获取变量地址,MOV 获取变量地址上存储的值(OFFSET 除外)。
整数运算指令
ADD
ADD A,B
指令将 B 的值与 A 相加,并将结果保存到 A。
A 可以是一个寄存器或者内存值。
B 可以是一个寄存器、一个常量或者内存值。
在同一条指令中,A 和 B 不能同时是内存值。
.text:0040108D 83 C1 04 add ecx, 4
如果 ECX = 10000,加上常数 4,结果是 10004,结果重新保存到 ECX 当中。
.text:00413C86 83 41 30 FF add dword ptr [ecx+30h], 0FFFFFFFFh
假设 ECX = 0x10000 那么加上 30 就是 0x10030 ,如果这个地址保存的数是 100,加上常数 0xffffffff 也就是-1,结果是 99, 并且保存到 0x10030 地址上。
dword 表示 ecx+30h 处储存的是一个 dword 类型的值。
SUB
SUB A,B 这个指令和 ADD 指令一致,只不过它是对两个操作数相减,最后结果保存到 A。SUB 允许的操作数组合和 ADD 是一样的。
INC/DEC
对一个寄存器或者内存值±1 。这是加减操作的一个特例。一般这两个指令用来对计数器±1。
IMUL
IMUL 是带符号整数乘法指令,这里介绍两种使用方式。
IMUL A,B
和 IMUL A,B,C
。
第一种对 A 和 B 相乘,结果返回给 A。
第二种方式对 B 和 C 相乘,将结果返回给 A。
在这两种方式下,A 只能是一个寄存器,B 可以是寄存器或者内存值(第一种方式下可以是常数),而 C 只能是个常数。
例如:
Imul eax, [ecx]
Imul esi, edi, 25
IDIV
指令中,仅仅指定了除法的除数。被除数没有指定,因为存放的地方是固定的。
在 32 位运算中,EDX 和 EAX 组成一个 64 位数,EDX 在高位,EAX 在低位。这个 64 位数除以 A 后,商返回给 EAX,余数返回给 EDX。
.text:00421215 F7 F9 idiv ecx
假如 EAX = 5,EDX = 0,ECX = 2,那么 5÷2 的结果是 2,保存到 EAX,余数 1 保存到 EDX。
逻辑运算指令
AND/OR/XOR
AND A,B
对 A 和 B 进行与运算,将最终结果保存到 A 。对于 OR 和 XOR 运算也是一样的。
A 和 B 可以是寄存器或者内存值,但同一条指令中 A 和 B 都是内存值是不允许的。
最常用的例子对同一个寄存器 XOR(异或)运算,将它的值清零。例如,XOR EAX,EAX
。
NOT
将 A 所有的位取反然后保存到 A。
NEG
将 A 转变成 -A。NEG 运算和按位取反不一样,按位取反多减了 1。
SHL/SHR
SHL A,B
与SHR A,B
中 A 是一个寄存器或内存值,B 是一个常数或者一个 8位寄存器。这两个指令将操作数按位向左或向右偏移,缺少的位用 0 来填补。这是逻辑左移与右移,算术左移与右移是 SAL
和 SAR
。
还有 2 个类似的指令 ROL 和 ROR,指令会将每个比特偏移一定的位置,但是一端超出的字节会重新返回给另一端。
JMP
这个指令经常会用于 patch 执行流程,很重要。
EIP 寄存器指向下一条将要执行的命令,执行完毕后,EIP 指向再下一条。
但是程序本身也有一些指令可以控制执行的路径,跳转到一些预期的位置。
.text:00401324 EB 05 jmp short loc_40132B
JMP SHORT 指令是一个两字节的短程跳转指令,只能向前或者往回跳转。
第一个字节 EB 是跳转操作码(OPCODE)。跳转的方向由第二个字节的值确定。
这种跳转是有距离限制的。
这里的 EB 是 JMP 的操作码,这条指令将会向前跳转到指令结束位置后的 5 个字节。
指令的起始地址加上指令本身的长度(2 字节)在加上第二个字节中的跳转距离(5 字节):
0x401324 + 2 + 5 = 0x40132b
在IDA的视图左侧,也可以看到跳转箭头指向了 0x40132b
的位置。
显然,这个跳转距离用一个字节表示的话不是一个很大的范围。最大的正向跳转距离是 0x7f。
使用短跳转只能在当前地址的附近跳转,无法跳转到所有的地址。所以程序会使用长跳转。
图上显示了一些长跳转,图中 loc_ 表示那一条指令是一般指令。
长跳转的计算公式(E9 是 opcode):
终点指令起始地址 - 起点指令起始地址 - 5
说明操作数占据4个字节。
有条件跳转
一般来说,程序需要对一些值的比较来决定程序执行的路径。
例如:CMP A,B
对 A 和 B 进行比较,根据比较的结果程序将会进行某些操作。如果是另一种结果则进行不同的操作。
一般来说,比较会改变标志寄存器 EFLAGS 的状态,根据这个状态条件跳转指令会决定接下来如何执行。
图中是一个 JZ 条件跳转的例子。如果比较触发标志寄存器 EFLAGS中的 Z 或者说 Zero 标记,将执行跳转。例子中,如果 EAX 和 EBX 相等将执行跳转。CMP 指令类似于 SUB 指令,只不过不保存计算结果。
CMP 对这两个寄存器相减,如果它们相等,那么结果就是 0,就触发了标志寄存器中 Z 标记,JZ 指令检测到这个标记然后进行跳转。
如果触发 Z 标记,就会执行图中的绿色箭头路径,如果没有触发,那么就执行红色箭头路径。
条件跳转指令有很多种,但是都很好理解,找个图看看即可。
CALL/RET
CALL 指令用来调用一个函数。RET 指令用来返回调用这个函数的指令的下面一条指令处。
CODE:00401238 E8 9B 01 00 00 call sub_4013D8
CODE:00401238
CODE:0040123D 83 C4 04 add esp, 4
图中有一个 CALL 调用指令,程序将跳转到 0x4013d8 去执行这个函数。 在 0x4013d8 这个地址前面的sub_
表示这里是一个函数。
CALL 指令会将返回的地址 0x40123d 保存到栈上,有兴趣去看看栈帧结构。
CODE:004013F5 81 F7 34 12 00 00 xor edi, 1234h
CODE:004013FB 8B DF mov ebx, edi
CODE:004013FD C3 retn
sub_4013D8
函数最后会执行 RET 指令,跳转到栈上保存的返回地址 0x40123d 处,就是调用这个函数指令的下一条。
关注我的微信公众号:二手的程序员