硬编码逆向分析——定长指令与变长指令
写在前面,助记清单
帮助记忆:
1、要成为武林(50)盟主,必须拼命push,直到最后放下武器(57)!==》50~57表示push。
2、58同城搞了一个集五福(5F)活动,说是邀请了上面的50盟主,要和他对着干(push--》pop)。==》58~5F表示pop。
1、许仕林(40)因为鼓舞了士气(47),最后考取状元加分(inc)了!成功夺魁。==》40~47对应inc
2、DEC公司是你爸(48)还是佛(4f)?你竟然这样向着他!==》48~4F对应DEC
1、他是一个2b(B2),必须立即(立即数)移走(move),然后切成牛肉(BF)==》B0~BF表示move立即数。
一个90后的97年出生的程序员,为了叫eax的女生,交换了自己的青春年华去搬砖。==》90~97对应XCHG EAX
70后的要跳槽(jmp)太难了!处处被人欺负(7f)==》70~7f对应跳转类jmp指令,只能跳转0~255。
80后的程序员,因为了CFO(OF)的buffer(8f)加持,所以可以跳槽的空间大些。==》0f 80~0f8f对应jmp四字节。
1、你这小子是个爸宝男,每次都依你爸(E8)的意见,一出事就打电话(call)给他!==》E8 call
2、在泰坦尼克号里,让男主再选择一次,他依旧跳了(E9 jmp)!==》E9 jmp
你乘坐的国航(CA打头)CCC(c3)航班已经返航(RET)==》C3,CA对应ret。
爸爸和芭比(8b)娃娃参加89薛超,最后都移动(mov)用AK干掉了!==》88~8B对应mov
定长指令与变长指令
如下图是硬编码的结构,第二部分的Opcode是整个指令的灵魂,硬编码结构中的任何部分都可以没有,但是Opcode是必须要有的。
Opcode最少1个字节,最多3个字节;如下图我们可以看见硬编码排列是不整齐的,有的一行是1个字节,有的则是2个、5个字节,Opcode、ModR/M、SIB这三个组合在一块就可以决定一行指令的宽度(抛开前缀指令),后面的Displacement、Immediate就是配角,当前面的三个确定了,这两个也就确定了。
Opcode、ModR/M、SIB之间的关系是这样的:Opcode决定有没有ModR/M,ModR/M决定有没有SIB。==》其实很容易理解,就是贪心解析的思路。
什么是定长、变长指令
如下图所示50、52之类的硬编码实际上就是定长指令,但并不表示定长指令就只有一个字节;同样,如00D4、0034C3之类的硬编码就是变长指令。
简而言之,定长指令可以直接通过Opcode确定硬编码长度,变长指令就无法通过Opcode确定硬编码长度。
如何区分指令是定长或变长
如何区分指令是定长或变长,这需要去根据官方的文档来看:
如下图所示展开到「A.3 ONE, TWO, AND THREE-BYTE OPCODE MAPS」,向下拉就可以看到一张表,这张表是1个字节的Opcode的对应表,但实际上其他字节的Opcode就是通过这张表进行扩展的,所以这张表就是主表,也是所有x86硬编码中最重要的一张表(这张图中的表是不完整的,向下拉还有一张表,两张表拼在一块才是完整的):
通过这张表,我们可以直接看到之前举例的定长指令50就正是对应着PUSH EAX,举一反三,51就是PUSH ECX...,图中的rAX表示着这里可以是64位的RAX(64位模式下才有)、32位的EAX、16位的AX,而eAX则表示这里可以是32位的EAX、16位的AX(默认取决于你的CPU运行模式):
但是我们之前举例的变长指令00却让有点让人摸不着头脑:
这里我们知道对应的汇编代码是ADD,但是表格中的Eb,Gb却不清楚是什么;实际上这是Intel定义的一种Zz表示法,第一个字母为大写,第二个字母为小写。
在文档的「A.2.1 Codes for Addressing Method」、「A.2.2 Codes for Operand Type」中有解释每个字母的含义,
结合内容,再根据之前知道的Opcode决定有没有ModR/M,返过来一推,Opcode后面有ModR/M则表示这是一个变长指令,没有则是一个定长指令,也就是说操作数只要存在Ex或Gx的就为定长指令,没有的则为定长指令。
经典定长指令
经典定长指令,就是我们以后会经常见到、使用的定长指令;注意以下都是以x86环境去讲解,在实际的硬编码对应的汇编指令中其他环境对应的指令并不是这些。
修改ERX
标题中的ERX就表示EAX、ECX、EDX等等32位的寄存器。
PUSH/POP
PUSH:压入栈;POP:推出堆。
定长指令 |
汇编代码 |
0x50 |
PUSH EAX |
0x51 |
PUSH ECX |
0x52 |
PUSH EDX |
0x53 |
PUSH EBX |
0x54 |
PUSH ESP |
0x55 |
PUSH EBP |
0x56 |
PUSH ESI |
0x57 |
PUSH EDI |
0x58 |
POP EAX |
0x59 |
POP ECX |
0x5A |
POP EDX |
0x5B |
POP EBX |
0x5C |
POP ESP |
0x5D |
POP EBP |
0x5E |
POP ESI |
0x5F |
POP EDI |
帮助记忆:
1、要成为武林(50)盟主,必须拼命push,直到最后放下武器(57)!==》50~57表示push。
2、58同城搞了一个集五福(5F)活动,说是邀请了上面的50盟主,要和他对着干(push--》pop)。==》58~5F表示pop。
INC/DEC
INC:加1;DEC:减1。
定长指令 |
汇编代码 |
0x40 |
INC EAX |
0x41 |
INC ECX |
0x42 |
INC EDX |
0x43 |
INC EBX |
0x44 |
INC ESP |
0x45 |
INC EBP |
0x46 |
INC ESI |
0x47 |
INC EDI |
0x48 |
DEC EAX |
0x49 |
DEC ECX |
0x4A |
DEC EDX |
0x4B |
DEC EBX |
0x4C |
DEC ESP |
0x4D |
DEC EBP |
0x4E |
DEC ESI |
0x4F |
DEC EDI |
帮助记忆:
1、许仕林(40)因为鼓舞了士气(47),最后考取状元加分(inc)了!成功夺魁。==》40~47对应inc
2、DEC公司是你爸(48)还是佛(4f)?你竟然这样向着他!==》48~4F对应DEC
MOV Rb, Ib
MOV:数据传送。
定长指令 |
汇编代码 |
0xB0 |
MOV AL, Ib |
0xB1 |
MOV CL, Ib |
0xB2 |
MOV DL, Ib |
0xB3 |
MOV BL, Ib |
0xB4 |
MOV AH, Ib |
0xB5 |
MOV CH, Ib |
0xB6 |
MOV DH, Ib |
0xB7 |
MOV BH, Ib |
标题里的Rb表示着8位寄存器,Ib表示着是8位立即数,这些都可以通过之前的PDF文档查阅得知(1字节等于8位):==》人家那写的是大写的i,表示immiditely立即数。
在官方的表格中也可以很直观的看见:
MOV ERX, Id
MOV:数据传送。
定长指令 |
汇编代码 |
0xB8 |
MOV EAX, Id |
0xB9 |
MOV ECX, Id |
0xBA |
MOV EDX, Id |
0xBB |
MOV EBX, Id |
0xBC |
MOV ESP, Id |
0xBD |
MOV EBP, Id |
0xBE |
MOV ESI, Id |
0xBF |
MOV EDI, Id |
实验:
助记:
1、他是一个2b(B2),必须立即(立即数)移走(move),然后切成牛肉(BF)==》B0~BF表示move立即数。
标题里的Id,I我们都知道是立即数了,再来看一下官方文档的d:
也就表示这里的Id是32位的立即数,其实你也可以不用看官方的释义,我们可以这样推出:首先这是一个定长指令,长度是固定的,其次这里的ERX就表示着32位寄存器,由此可以得出后面的立即数是必须是固定的长度,所以只能是32位的立即数。
需要注意的是我们当前环境是x86的所以用Id来代替立即数的表示,在实际表格中立即数是由Iv来表示的:
这是由于寄存器是rAX,在64位模式下有三种表达方式,所以Iv表示的立即数的大小是取决于操作数的属性的:
XCHG EAX, ERX
XCHG:内容交换。
定长指令 |
汇编代码 |
0x90 |
|
0x91 |
XCHG EAX, ECX |
0x92 |
XCHG EAX, EDX |
0x93 |
XCHG EAX, EBX |
0x94 |
XCHG EAX, ESP |
0x95 |
XCHG EAX, EBP |
0x96 |
XCHG EAX, ESI |
0x97 |
XCHG EAX, EDI |
XCHG是用来做内容交换的,0x90对应着XCHG EAX, EAX,这就没有任何意义了,所以Intel给其定义了一个新的指令叫NOP,这个我们称之为无效指令,也就表示这个指令是没有任何意义的。
助记:
一个90后的97年出生的程序员,为了叫eax的女生,交换了自己的青春年华去搬砖。
修改EIP
我们在学习会变的时候都知道无法通过MOV、ADD之类的指令去修改EIP,所以要修改EIP需要借助JCC、CALL、JMP之类的指令进行,接下来我们学习的硬编码就跟这些指令有关的。
0x70 - 0x7F(近跳)
条件跳转,后跟一个字节立即数的偏移(有符号),共两个字节。 如果条件成立,跳转到当前指令地址 + 当前指令长度 + Ib ,向下跳的范围是0x0 - 0x7f,向上跳的范围是0x80 - 0xFF。
定长指令 |
汇编代码 |
0x70 |
JO |
0x71 |
JNO |
0x72 |
JB/JNAE/JC |
0x73 |
JNB/JAE/JNC |
0x74 |
JZ/JE |
0x75 |
JNZ/JNE |
0x76 |
JBE/JNA |
0x77 |
JNBE/JA |
0x78 |
JS |
0x79 |
JNS |
0x7A |
JP/JPE |
0x7B |
JNP/JPO |
0x7C |
JL/JNGE |
0x7D |
JNL/JGE |
0x7E |
JLE/JNG |
0x7F |
JNLE/JG |
助记:
70后的要跳槽(jmp)太难了!处处被人欺负(7f)==》70~7f对应跳转类指令,只能跳转0~255。
0x0F 0x80 - 0x0F 0x8F(远跳)
条件跳转,后跟四个字节立即数的偏移(有符号),共五个字节。如果条件成立,跳转到当前指令地址 + 当前指令长度 + Id,向下跳的范围是0x0 - 0x7FFFFFFFF,向上跳的范围是:0x80000000 - 0xFFFFFFFF。
定长指令 |
汇编代码 |
0x0F 0x80 |
JO |
0x0F 0x81 |
JNO |
0x0F 0x82 |
JB/JNAE/JC |
0x0F 0x83 |
JNB/JAE/JNC |
0x0F 0x84 |
JZ/JE |
0x0F 0x85 |
JNZ/JNE |
0x0F 0x86 |
JBE/JNA |
0x0F 0x87 |
JNBE/JA |
0x0F 0x88 |
JS |
0x0F 0x89 |
JNS |
0x0F 0x8A |
JP/JPE |
0x0F 0x8B |
JNP/JPO |
0x0F 0x8C |
JL/JNGE |
0x0F 0x8D |
JNL/JGE |
0x0F 0x8E |
JLE/JNG |
0x0F 0x8F |
JNLE/JG |
助记:
80后的程序员,因为了CFO(OF)的buffer(8f)加持,所以可以跳槽的空间大些。==》0f 80~0f8f对应jmp四字节。
0xE0 - 0xE9
如下表格中的J就表示偏移量,宽度根据后面b或者d决定(具体可以看文档释义)。
定长指令 |
汇编代码 |
宽度 |
作用 |
0xE0 |
LOOPNE/LOOPNZ Ib (Jb) |
共2字节 |
先进行 ECX = ECX - 1 当 ZF = 0 && ECX!=0 时跳转到当前指令地址 + 当前指令长度 + Ib |
0XE1 |
LOOPE/LOOPZ Ib (Jb) |
共2字节 |
先进行 ECX = ECX - 1 当 ZF = 1 && ECX != 0 时跳转到当前指令地址 + 当前指令长度 + Ib |
0XE2 |
LOOP Ib (Jb) |
共2字节 |
先进行 ECX = ECX - 1 当 ECX!=0 时跳转到当前指令地址 + 当前指令长度 + Ib |
0XE3 |
JrCXZ Ib (Jb) (在32位模式中rCX为ECX) |
共2字节 |
当 ECX = 0 时跳转到当前指令地址 + 当前指令长度 + Ib(自己控制步长) |
0xE8 |
CALL Id (Jd) |
共5字节 |
CALL指令的下一条指令地址入栈后,跳转到当前指令地址 + 当前指令长度 + Id |
0xE9 |
JMP Id (Jd) |
共5字节 |
跳转到当前指令地址 + 当前指令长度 + Id |
助记:
1、你这小子是个爸宝男,每次都依你爸(E8)的意见,一出事就打电话(call)给他!==》E8 call
2、在泰坦尼克号里,让男主再选择一次,他依旧跳了(E9 jmp)!==》E9 jmp
其他指令
定长指令 |
汇编代码 |
宽度 |
作用 |
0xEA |
JMP Ap (Ap:六字节长度的直接地址) |
共7字节 |
JMP CS:Id 将Ap中的高2位赋值给CS,低4位直接赋值给EIP, 即跳转 |
0xEB |
JMP Ib (Jb) |
共1字节 |
跳转到当前指令地址 + 当前指令长度 + Ib |
0xC3 |
RET |
共1字节 |
EIP出栈 |
0xC2 |
RET Iw |
共3字节 |
EIP出栈后,进行 ESP = ESP + Iw |
0XCB |
RETF (return far) |
共1字节 |
出栈8个字节,低4个字节赋值给EIP,高4个字节中低2位赋值给CS |
0xCA |
RETF Iw |
共3字节 |
出栈8个字节,低4个字节赋值给EIP,高4个字节中低2位赋值给CS后,ESP = ESP + Iw |
助记:
你乘坐的国航(CA打头)CCC(c3)航班已经返航(RET)==》C3,CA对应ret。
经典变长指令
ModR/M
当指令中出现内存操作对象的时候,就需要在操作码后面附加一个字节来进行补充说明,这个字节被称为ModR/M,其只有一个字节宽度,但如果你看PDF官方文档中那个表的话就会发现其有两个参数,这也正是它复杂的地方。
如下所示就几个经典的变长指令:
变长指令 |
汇编代码 |
0x88 |
MOV Eb, Gb |
0x89 |
MOV Ev, Gv |
0x8A |
MOV Gb, Eb |
0x8B |
MOV Gv, Ev |
助记:
爸爸和芭比(8b)娃娃参加89薛超,最后都移动(mov)用AK干掉了!==》88~8B对应mov
指令中参数的释义如下所示:
G:通用寄存器
E:寄存器/内存
b:字节
v:字、双字或四字
理解ModR/M
ModR/M这个字节的8个位被拆分成了3个部分:
其中,Reg/Opcode(第3、4、5位,共3个位)描述指令中的G部分,即寄存器,如下就是这三个位对应寄存器的表示:
Mod(第6、7位,共2个位)和R/M(第0、1、2位,共3个位)共同描述指令中的E部分,即寄存器/内存。
那么,这8个位具体是如何工作的呢,Intel操作手册给出了一张表(Table 2-2):
手动解析指令
现在有一个指令为:0x88 0x84 0x48,其对应的Opcode、ModR/M、SIB如下:
Opcode |
ModR/M |
SIB |
0x88 - MOV Eb, Gb |
0x84 |
0x48 |
ModR/M转为二进制:1000 0100,拆分如下:
Mod |
Reg/Opcode |
R/M |
|||||
1 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
Mod与R/M字段查Table 2-2得到对应的结构:[--][--]+disp32,这就表示需要SIB来进行补充。
SIP转为二进制:0100 1000,拆分如下:
Scale |
Index |
Base |
|||||
0 |
1 |
0 |
0 |
1 |
0 |
0 |
0 |
接着查Table 2-3,Base对应着EAX,Base和Index就是[ECX*2],最终得到[EAX + ECX*2]。
最终指令就是:MOV [EAX + ECX * 2 + disp32], AL。