IDA PRO 实践01 - 指令

​原文: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,BSHR 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 处,就是调用这个函数指令的下一条。

关注我的微信公众号:二手的程序员

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二手的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值