硬编码
硬编码
ZEROKO14硬编码
硬编码简介
硬编码就是指令编码格式
x64比x86主要是多了一组指令前缀,还有比如地址偏移和立即数可以允许8个字节(x86只允许最多4个)
这里学习的主要是x86
无论是x86还是x64的指令格式,都是最短一个字节,最长15个字节
硬编码转换为汇编语言的过程叫反汇编
哪些人需要学习硬编码
- 病毒与反病毒
- 加密与破解
- 外挂与反外挂
所有与计算机底层相关的行业都需要学习
汇编指令有六部分构成
- Prefix—-指令前缀(可选项)
- Opcode—-操作指令(必选项)
- Mod R/M—操作数辅助说明(可选项)
- SIB—-Mod R/M辅助说明(可选项,但是出现Mod R/M 这个必须有)
- Displacement—操作数作为内存地址时用来表示位移(可选项)
- Immediate —-表示操作数为立即数(可选项)
前缀指令(Instruction Prefixes)
该指令是可选的,最多可以在每组中选一个,最少可以什么都不写。并且前缀指令和顺序没关系
在od中会把前缀指令和后面的指令中间加上冒号:方便用户识别
前缀指令分组
LOCK和REPEAT前缀指令
LOCK===F0 REPNE/REPNZ===F2 REP/REPZ===F3
段前缀指令
CS(2E) SS(36) DS(3E) ES(26) FS(64) GS(65)
操作数宽度前缀指令
66
地址宽度前缀指令
67
LOCK和REPEAT前缀指令
LOCK
锁地址总线,在多核下才有意义,使同一时刻只有一个核可以读这个地址。(单核下,该指令没有意义)
REPNE/REPNZ
重复执行(zero flag为0的时候执行)
REP/REPZ
重复执行(zero flag为1的时候执行)
段前缀指令
CS(2E) SS(36) DS(3E) ES(26) FS(64) GS(65)
明确地告诉cpu要使用的是哪个段
不写段前缀指令的情况,如果访问的是全局地址,则默认访问DS;若访问的是ebp,esp,则默认是访问SS段
操作数宽度前缀指令
66
用来改变操作数宽度的前缀指令(默认32位,加66就改成16位;默认16位,加66就改成32位)
1 | 55 |
地址宽度前缀指令
67
用来改变寻址方式的(默认32位,加66就改成16位;默认16位,加66就改成32位)
1 | 8965 E8 |
定长指令和变长指令
整个硬编码的长度取决于Opcode(operand code操作码),ModR/M和SIB这三个字段
Opcode决定了后面有没有ModR/M(没有ModR/M就是定长)
ModR/M决定了后面有没有SIB
Opcode可以决定整个指令是否定长还是变长(此处的定长是不包含前缀的)
变长指令:仅仅通过Opcode没办法确定长度的指令
上图中Opcode为50的那行指令是定长指令。
Opcode为00的那三行指令是变长指令。
一个字节的操作码查表(下图只是一个字节的操作码,还有两个和三个字节的操作码在因特尔白皮书中)
(intel白皮书1737页)
查表方式:
图中的Eb是因特尔的Zz表示法,即一个大写带一个小写的字母表示,对应的含义也需要查表
(intel白皮书1731页)
- Codes for Addressing Method(大写字母)
- Codes for Operand Type(小写字母)
p.s.图中大写E和大写G表示的一定是后面接了ModR/M的(即变长指令)
1735页
部分表中标志含义解读:
- i64 invalid,在64位系统64位操作数无效,只能32位操作数
- o64 only,在64位系统只有在64位模式有效
- d64 default,在64位系统默认操作数宽度64,不能编码宽度32位的操作数,意思是其他位可以
- f64 forced,在64位系统强制64位操作数,哪怕前面加了操作数宽度前缀指令。
经典定长指令_修改寄存器
PUSH/POP
- 0x50~0x57为push
- 0x58~0x5F为pop
1 | 0x50 PUSH EAX |
INC/DEC(i64)
- 0x40~0x47为INC
- 0x48~0x4F为DEC
MOV Rb,lb(定长2个字节)
0xb0~0xb7
1 | //B0是操作码,A4是操作数 |
MOV ERX,ld(定长5个字节)
0xb8~0xbF
1 | B8 8DA42400 MOV EAX,24A48D |
XCHG EAX,ERX
0x90~0x97 XCHG EAX,ERX
1 | 90 NOP //XCHG EAX,EAX没有任何意义,所以给90赋予新的意义,NOP,CPU遇到他不做任何事情直接跳过他,他唯一的作用就是用来对齐其他指令 |
可通过NOP指令构造花指令使得调试器的汇编混乱
假如上图中框选的部分为我们要跳过来77DE01E8执行的代码段,这是没使用花指令的情况下。
我们把77DE01E7的90改成B0
由于B0是定长2个字节的操作码,因此会吃掉后面一个字节的数据,所以汇编的过程中就显示出错了,但是实际上执行的时候,程序还是跳转到77DE01E8正常执行代码。
如何去除这种花指令,从上面执行下来的时候,跳转到的应该是77DE01E8,但在上图中,左列地址列却没有显示77DE01E8的地址,很显然该地址被花指令了
经典定长指令_修改EIP
0x70~0x7F短跳(定长两个字节)
- 条件跳转,后跟一个字节立即数的偏移(有符号),共两个字节。
- 如果条件成立,跳转到当前指令地址+当前指令长度+lb
- 向前跳0
7f(向大地址跳),向后跳FF80
向前跳:
$$
目标地址-(当前命令地址+当前命令长度)的原码
$$大地址-小地址
向后跳:
$$
目标地址-(当前命令地址+当前命令长度)的原码舍弃溢出位
$$小地址-大地址(舍弃溢出位)
也可以理解成
$$
(当前命令地址+当前命令长度)-目标地址的补码
$$
1 | 0x70 JO |
0x0F 0x80~0x0F 0x8F长跳(定长6个字节)
和短跳的语法完全一样
向前跳0000 00007FFF FFFF(向大地址跳),向后跳FFFF FFFF8000 0000
1 | 0x0F 0x70 JO |
其他指令
Jb:J表示间接寻址,即操作数是相对偏移;b表示操作数一个字节
下面的暂时不讲(没进0环之前下面指令基本用不到)
Ap:A表示直接寻址,p表示当前操作数宽度是16位,p就表示32位,操作数32位,p表示48位,操作数64位,p表示80位
1 | EA 410064A1 0000 JMP FAR 0000:A1640041//跨段跳转,讲调用门的时候会用到 |
EB是一个字节内的跳转,E9是超越一个字节的跳转,EA是跨段跳转
C2的return用于内平衡
EA,CB,CA都常用于0环
经典变长指令_ModR/M
Mod R/M 大小为一个字节,由三部分组成,分别为 Mod(字节前两位),Reg(字节中间三位),R/M(字节后三位)。Mod R/M的主要功能就是说明操作数的寻址方式,包括寄存器选择,内存操作数的偏移等等。例如:
1 | 66:81FE 4746 CMP SI,474 |
因特尔白皮书中指令查表中,E和G开头的表示变长指令。
经典变长指令_ModR/M
1 | 0x88 MOV Eb,Gb G:通用寄存器 |
这里应该注意的是,E中已经说明了要使用通用寄存器或者内存操作数,并且要使用Mod R/M来进行辅助说明,所以在操作码89后的C1就是ModR/M,Ev统一起来就是可以使用双子寄存器操作数或者双子内存操作数,具体的要使用ModR/M字段来辅助说明。
ModR/M的目的就是告诉我们G和E到底是谁
这8个位究竟是如何工作的,Inter操作手册给出了一张表
因为操作指令是88,所以MOV Eb,Gb,由于b所以无论E和G都是一个字节,所以上图说EAX还是AL是由操作码决定的
经典变长指令_RegOpcode
Reg/Opcode除了可以确定G,还有时候作为opcode的拓展位来确定操作指令
举例说明0x80 0x65 0x08 0xFF查表
DIS8表示8位偏移
1A可以查表看意思:
1A标明了ModR/M中的3,4,5位作为opcode的扩展
80代表的Opcode未标明操作指令,凡是写了Grp的,均参见TableA-6(1748页)
上图红线处Grp和上标1A中间的1代表第1组,可以在下表TableA-6中的Group中对应第1组
总结一下0x80 0x65 0x08 0xFF查表流程
首先看到了0x80作为opcode查到了格式为xxx Eb,Ib(此时不确定操作指令是什么),通过1A知道操作指令取决于opcode和ModR/M的3,4,5位,并且知道了查表找操作指令的时候要查的是第一组
通过ModR/M的1,2,6,7,8,位:0x65=01 100 101(参考上一章知识)确定Eb是[EBP+DIS8],该指令当前为xxx byte ptr ds:[EBP+DIS8],Ib
通过ModR/M的3,4,5位:
确定操作指令为AND,当前的指令为AND byte ptr ds:[EBP+DIS8],Ib
完整编码的后半部分0x08和0xFF按硬编码结构顺序填入DIS8(8位偏移)和Ib(一个字节的立即数),由此得到完整指令:AND byte ptr ds:[EBP+0x08],0xFF
经典变长指令_SIB
SIB—-Mod R/M辅助说明(可选项)
阶段性总结
Opcode决定了后面是否有ModR/M,ModR/M决定了后面是否存在SIB,Displacement和immediate
查ModR/M表的时候如果ModR/M1,2,6,7,8位查到上图红线[–]这种形式,标明ModR/M后面需要再跟一个字节,就是SIB来确定[–]里面的信息
sib用到的表
比如0x88 0x84 0x48查表确定到MOV Eb,Gb
MOV [–][–]+DISP32,AL
SIB为0x48 拆分01 001 000
000的base部分查到EAX(下图红线)
01的Scale部分决定了拿个SS
001的Index部分
(此处缺了一张图)
得出结论SIB== [EAX+ECX*2]
所以结论为MOV byte ptr DS:[EAX+ECX*2+DISP32],AL
这也意味着0x88 0x84 0x48后面还会吃掉4个字节来作为DISP32.
1 | 888448 6AFF6818 MOV BYTE PTR DS:[EAX+ECX*2+1868FF6A],AL |
32位二进制中含操作数的汇编代码总结汇总
| 汇编指令 | 汇编二进制形式 | 大小(字节) |
|---|---|---|
| retn 0~0xFFFF | C2 FFFF |
3 |
| 直接call 0x1 | E8 (4字节)目标地址-call地址-5 |
5 |
| 间接call [0x12345678] | FF 15 78 56 34 12 |
6 |
| 短jmp | EB (1字节)目标地址-jmp地址-2 |
2 |
| 长jmp | E9 (4字节)目标地址-jmp地址-5 |
5 |
| 间接jmp [0x12345678] | FF 25 78 56 34 12 |
6 |
| 直接push 0x12345678 | 68 78 56 34 12 |
5 |
| 间接push [0x12345678] | FF 35 78 56 34 12 |
6 |
| 直接add eax 0x100 | 05 00 01 00 00 |
5 |
| 间接add eax [0x12345678] | 03 05 00 01 00 00 |
6 |
| 等等等等(非常多) | ||



























