Step.1_引擎大纲
Step.2_框架搭建
Step.3_经典定长指令的解析
Step.4_变长指令复习1
Step.5_变长指令复习2 - 指令前缀
Step.6_引擎核心->函数封装
上一章我们对经典定长指令
进行了解析,引擎已经能完成对定长指令的解析,但是硬编码的世界里绝大部分都是变长指令
,那么在解析变长指令
之前,我们需要复习一下指令
的相关知识
对于一条指令来说,我们可见的就是一个或多个字节组成,这些字节我们称为硬编码
但是指令真的就像表面上的几个字节那么简单吗?为了了解指令结构,我们打开Intel白皮书第1643页
上面清晰的为我们展示描述了一条指令的组成部分:前缀
,Opcode指令码
,ModRM
,SIB
,偏移
,立即数
例如C2 1234->RETN 0X3412
,则是由一个Opcode->C2
加上一个立即数1234
组成
既然Opcode 偏移 立即数
我们都已经接触到了,那么其余3部分在哪里呢?
这就是变长指令
所用到的部分
首先我们要明白ModRM的结构,它是由8位二进制数组成,也就是一个字节.这一个字节的数据并不是表面上看起来那样是一个整体,而是由三部分组成:0~2 -> R/M
,3~5 -> Reg/Opcode
,6~7 -> Mod
,上图中也清晰的描述了这种结构.
既然前面说到了变长指令
所用的部分为前缀 MODRM SIB
,那么MODRM
在其中起到了什么作用呢?什么时候才会起作用呢?
白皮书中依然为我们清晰的标明了什么时候MODRM
字段会发挥它的作用,打开1475页
,观察Table A-2
我们会发现上面有一部分是我们熟悉的汇编指令,另外一部分却是类似Eb,Gb Ev,Gv
一类的东西.当一个硬编码对应这种结构,那么就说明这个字节的硬编码将无法确定当前指令的长度,其后紧接的一个字节将变为ModRM吞入当前指令
,这就是我们所说的变长指令
,而整个指令的长度将由接下来的ModRM
字节继续进行确定
既然了解了ModRM的结构,作用,何时起效.那么接下来我们就要继续探寻ModRM字段的具体作用,打开白皮书第41页 -> Table 2-2
,上面依然为我们清晰的列出了ModRM字段的拆解说明
想要解析2-2
,我们需要先回到A-2
中弄清楚Ev,Gv
之类的东西是个啥
上面我们说了变长指令
是怎么形成的,而变长指令的最终样子则遵守Ev,Gv
这种格式,在这个格式中,每个字母都有其不同的含义:
E: 寄存器/内存
G: 通用寄存器
b: 字节
v: 字,双字,四字 最终V是什么取决于当前程序的运行环境,若是8位则是字.32位则是双字,64位则是四字
也就是说,EvGv
格式决定了当前指令操作的寄存器 内存是什么.那么操作的指令的决定因素之一则是ModR/M
决定变长指令最终样子的因素不仅仅是ModRM,还需要Opcode + SIB,此处我们先只讨论Opcode+ModRM的情况
下面我们将举两个个具体的例子说明:
如有一段编码为88 01
,我们首先在A-2
表中找到88
,其对应的指令为MOV Eb,Gb
,这并不是我们熟知的汇编指令,这是一个变长指令
,如果我们想要确定这条指令,就需要像上文提到的:取出后面一个字节01,吞入当前指令,将该字节作为ModR/M进行解析
我们已经知道了ModRM是由8位 3部分组成
,那么我们首先将其转换为二进制,并分成三部分
01 -> 0000 0001 -> Mod:00 Reg:000 R/M:001
接下来就到了我们解析ModRM
的时候了!全部操作严格按照Table 2-2
进行解析
首先我们解析Mod
部分00
,对照2-2
,我们发现当Mod = 00
时,对应的指令有8
种格式:
[EAX] ,[ECX] ,[EDX] ,[EBX] ,[--][--] ,disp32 ,[ESI] ,[EDI]
接下来我们需要确定是这8
种格式中的哪一个,那么我们就需要用到R/M
字段001
查看2-2
,我们发现R/M = 001
时,指令为[ECX]
,很好!我们已经锁定了指令的一部分为[ECX]
!
还剩一个Reg
没有解析,Reg
在哪里呢?我们观察2-2
最上面一行,发现Reg = 000
时,有6
种情况:
AL AX EAX MM0 XMM0 0
那么是哪一个呢?
我们上文说到Eb,Gb
中的b
代表了字节
的意思,这说明了当前指令操作数据的宽度为:1字节
,因此,我们需要选用1字节宽度的寄存器
,也就是AL
至此,我们已经解析出来的结果:指令格式为MOV Eb,Gb 一部分是[ECX] 一部分是AL
那么最终是什么样子的呢? 上文中也说到了: E代表寄存器/内存 G代表通用寄存器
因此我们对号入座:
[ECX]为一款内存 因此放在Eb处 变成了MOV byte ptr ds:[ECX],Gb
AL为一个8位通用寄存器 因此放在Gb处 变成了 MOV byte ptr ds:[ECX],AL
至此,整条指令解析结束 88 01
最终变成了两字节的变长指令 MOV byte ptr ds:[ECX],AL
接下来我们解析另一条编码:88 2D 12 34 56 78
上文已经详细描述了查表的步骤,这里不再赘述
88 -> MOV Eb,Gb 引出ModR/M = 2D 2D->0010 1101->Mod:00 Reg:101 R/M:101
解析Mod 与 R/M
得到disp32
解析Reg
得到CH
带入公式得到: MOV disp32,CH
我们发现这里出现了没见过的东西disp32
,对照2-2
其他部分,这里本应该是[EBP]
,为什么会变成disp32
?
知识点来了!
我们在学习基础,画堆栈图时应该记得这样一个知识点 : call调用时系统会提升堆栈,将新堆栈给这个call使用,在提升后,[EBP+4]为返回地址 [EBP+8]为第一个参数 [EBP-4]为第一个局部变量 [EBP]则为原始堆栈的栈底
而intel认为,任何对于原始栈底赋值是无意义的!这会破坏堆栈的结构
所以intel废弃了这种指令MOV [EBP],XX
,而将其扩展为disp32 : 一个32位的内存地址
知道了这个知识点,我们再会过来看这条指令MOV disp32,CH
,既然已经变成了32位内存地址
,那么就说明此条指令在ModR/M之后,应该再吞4个字节,存放至disp32位置处!
所以 88 2D 12 34 56 78
最终会变为 6字节的变长指令 MOV byte ptr ds:[0x78563412],CH
上文我们学习了ModR/M
的解析,接下来我们要学习SIB
的解析
在上面进行解析ModR/M
时,细心的小伙伴应该发现了,2-2
表中,不光有disp32
这种奇葩,还有一个[--][--]
这是个什么玩意?
观察后我们发现只有在R/M = 100
时才会出现,根据2-2
表中寄存器的顺序推理,R/M =100
时理应为[ESP]
之类的东西,为何又变成了[--][--]
?
知识点又来了!
没错,intel又想到了! ESP的值随时发生变化,任何通过ESP进行寻址的操作会变得不好维护!
,于是intel将其扩展,引出了下一个字节: SIB
SIB
的来源我们简单描述了一下,接下来我们看一下SIB
的解析
SIB
为一字节 8个二进制位
,其同ModR/M
相同,也是分成了三部分
,分别为:Scale:6~7 Index:3~5 Base:0~2
白皮书中第1643
页,表Table 2-3
为我们展示了SIB
各部分的含义:
多说无益,我们将通过两个例子来学习SIB
的解析与作用
88 84 48 12 34 56 78
ModR/M
的解析大家应该已经了解了,我就快速带过了
当我们看到[--][--]
时,我们就该知道,需要解析SIB
了
取出ModR/M
后面的一字节48
作为SIB
进行解析:
SIB = 48 -> 0100 1000 -> Scale:01 Index:001 Base:000
查询2-3
,我们发现当Scale = 01
时,存在8
种情况:
[EAX*2] [ECX*2] [EDX*2] [EBX*2] NONE [EBP*2] [ESI*2] [EDI*2]
当Index = 001
时,锁定为八种情况中的 [ECX*2]
观察2-3
第一行,我们得出当Base = 000
时得到EAX
这时我们已经得到了EAX ,[ECX*2]
,接下来我们需要记住一个公式:
SIB的寻址结构公式: [Base + Index*2^Scale]
将结果带入寻址结构公式为 : [EAX + ECX*2^1]
也就是[EAX + ECX*2]
至此,我们SIB
的解析已经结束了,将解析结果带入最开始的公式:
MOV [--][--]+disp32,AL -> MOV [EAX + ECX*2] +disp32,AL
这里不知道是不是intel书里写错了,但是各位记住就行,disp32是在括号里的,不要看他写在外面你也写在外面
所以在吞入4字节存放至disp32后
,整行指令变成了:
MOV byte ptr ds:[EAX + ECX*2 + 0x78563412],AL
虽然我觉得应该没有人会不知道,但是我还是说一下吧...
^ 的意思是次方,比如 2^3 代表2的3次方
88 84 A4 12 34 56 78
我们再来解析一条指令 : 88 84 A4 12 34 56 78
看到了[--][--]
我们取出下一个字节进行解析 :
SIB = A4 -> 1010 0100 -> Scale:10 Index:100 Base:100
对照2-3
得出结果为: none ESP
我们又看到了一个不认识的东西
none
: 这个我暂时不知道intel为啥舍弃了,各位知道的可以评论区说下. 这个none
代表此处不填值,可以相当于没有这个结果,在进行公式带入的时候跳过即可
带入寻址结构公式[Base + Index*2^Scale]
变为[ESP + none]
,none舍弃,结果就是[ESP]
带入第一个公式 ,变为 MOV [ESP]+disp32,AL
吞入4字节
作为偏移结果为: MOV byte ptr ds:[ESP + 0X78563412],AL
这就结束了吗?
当然不是
各位应该听说过段寄存器,指令6部分中的第一部分前缀
则是描述段寄存器的,当无前缀
时,指令有一个默认的段寄存器,这里面有一些默认规则,这里我们先说其中一条
当寻址公式里出现 ESP EBP时,默认段寄存器为 SS
所以,我们的汇编语句不能使用DS
作为段寄存器,而应该是SS
,所以最终结果为:
8884 12345678 ->MOV byte ptr ss:[ESP + 0X78563412],AL
88 84 85 12 34 56 78
我们最后再来解析一条:88 84 85 12 34 56 78
看到了[--][--]
我们取出下一个字节进行解析 :
SIB = 85 -> 1000 0101 -> Scale:10 Index:000 Base:101
查询2-3
得出两个结果:
Scale + Index -> [EAX*4]
和 Base -> [*]
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2020-10-13 17:43
被SSH山水画编辑
,原因: