基于《CTF竞赛权威指南 PWN篇》的总结。
CPU架构与指令集
CPU:中央处理单元,也称处理器,作用为从内存中读取指令,然后解码和执行。
CPU架构:CPU的内部设计和结构,也称微架构,用于实现指令集规定的操作或运算。
指令集架构:简称指令集,包含了一系列操作码以及由特定CPU执行的基本命令。
CPU操作模式
x86处理器:保护模式、实地址模式、系统管理模式,还有一个保护模式的子模式,称为8086模式。
保护模式:处理器的原生状态,此时所有指令和特性都可用,分配给程序的独立内存区域称为内存段,处理器将阻止程序使用自身段以外的内存区域。
实地址模式:早期Intel处理器的编程环境,程序可直接访问硬件及其实际内存地址。
系统管理模式:为操作系统提供了如电源管理和安全保护等特性机制。
语法风格
x86汇编语言主要的语法风格有:AT&T和Intel
AT&T语法风格 | Intel语法风格 |
---|---|
寄存器前加%符号 | 寄存器前无符号表示 |
立即数前加$符号 | 立即数前无符号表示 |
16进制数使用0x前缀 | 16进制数使用h后缀 |
源操作数在前,目标操作数在后 | 目标操作数在前,源操作数在后 |
间接寻址使用()表示 | 间接寻址使用[]表示 |
操作位数为指令+l、w、b | 操作位数为指令+dword ptr等 |
间接寻址格式%sreg:disp(%base,index,scale) | 间接寻址格式sreg:[basereg +index *scale +disp] |
寄存器与数据类型
寄存器
64位模式下,操作数的默认大小仍为32位,且有8个通用寄存器,当给每条汇编指令增加REX(寄存器扩展)的前缀后,操作数变为64位,且增加8个带有标号的通用寄存器(R8~R15)。
64位处理器与32位处理器有相同的标志位状态。64位模式下不能访问通用寄存器的高位字节(如AH、BH、CH、DH)。
整数常量
不同的进制需加后缀区分,此外,以字母开头的16进制数还需加上前缀’0’,如0ABCDh。
浮点数常量
也称实数常量,通常以十进制表示浮点数,而以十六进制编码浮点数,浮点数至少包含一个整数和一个十进制的小数点。
字符串常量
用单引号或双引号括起来的字符序列,包括空格符,可以嵌套。
数据传送与访问
MOV是最基本的数据传送指令,基本格式中,第一个参数为目的操作数,第二个参数为源操作数,如MOV EAX,ECX表示将ECX寄存器的值拷贝到EAX中。MOV支持从寄存器到寄存器、从内存到寄存器、从寄存器到内存、从立即数到内存、从立即数到寄存器的数据传送,但不支持从内存到内存的直接传输,想要完成内存到内存,需要一个寄存器作为中转。
对不同位数寄存器的数据传送eg:
MOV EAX,0 ;EAX=00000000h
MOV AL,78h ;EAX=00000078h
MOV AX,1234h ;EAX=00001234h
MOV EAX,12345678h ;EAX=12345678h
x86汇编语言使用变量名+偏移量表示一个直接偏移量操作数,如下表示一个数组:
.data
testArray BYTE 99h,98h,97h,96h
.code
MOV al,testArray ;al=99h
MOV bl,[testArray+1] ;bl=98h
MOV cl,[testArray+2] ;cl=97h
某些汇编器未实现数组的边界检查,如果偏移量超过了数组的实际定义范围,将出现数组越界错误。
算术运算与逻辑运算
最简单的算术运算指令为INC和DEC,分别用于操作数+1和-1,这两条指令的操作数既可以是寄存器,也可以是内存。
.data
testWord WORD 1000h
.code
INC EAX
DEC testWord
计算机底层数据表示均由补码表示。
ADD指令将长度相同的操作数相加。
.data
testData DWORD 10000h
testData2 DWORD 20000h
.code
MOV EAX,testData ;EAX=10000h
ADD EAX,testData2 ;EAX=30000h
SUB指令为减法操作。
.data
testData DWORD 20000h
testData2 DWORD 10000h
.code
MOV EAX,testData ;EAX=20000h
SUB EAX,testData2 ;EAX=10000h
汇编语言中存在标志位寄存器,使用ADD和SUB等指令都可能造成整数溢出、符号位等标志发生变化。
NEG指令是把操作数转换为二进制补码,并将操作数的符号位取反。
跳转指令与循环指令
条件跳转:满足特定条件时跳转
无条件跳转:无论标志位寄存器为何值,都跳转。
JMP指令:无条件跳转
JMP label1
MOV EBX,0
label1:
MOV EAX,0
JMP指令可创建循环,即循环结束时用JMP跳回开头。
LOOP指令也可以创建一个循环代码块,ECX寄存器为循环计数器,每循环一次,ECX-1
MOV AX,0
MOV ECX,3
L1:
INC AX
LOOP L1
XOR EAX,EBX
LOOP指令分为两步,一是ECX-1,二是将ECX与0比较,若不为0,则跳转;若为0,则不跳转。若将ECX初始值设为0,那么ECX-1实际为FFFFFFFFh,是一个非常大的循环。
栈与函数调用
栈的主要用途:存储局部变量、执行CALL指令调用函数时,保存函数地址、传递函数参数。
栈的常用指令为PUSH和POP,即入栈和出栈。
通过栈实现EAX和EBX的交换eg:
MOV EAX,1234h
MOV EBX,5678h
PUSH EAX
PUSH EBX
POP EAX
POP EBX
CALL指令调用某个子函数时,下一条指令的地址作为返回地址被保存到栈中,等价于PUSH返回地址与JMP函数地址的指令序列。被调用函数结束时,程序执行RET指令跳转到这个返回地址,将控制权交还给调用函数,等价于POP返回地址与JMP返回地址的指令序列。
调用子函数这一行为使用PROC与ENDP伪指令来定义,且需要分配一个有效标识符,所有x86程序中都包含标识符为main的函数,main函数不需要使用RET指令,其他的都需要使用。
1 ... .code
2 ... main PROC
3 0x00008000 MOV EBX,EAX
4 ... ...
5 0x00008020 CALL testFunc
6 0x00008025 MOV EAX,EBX
7 ... ...
8 ... main ENDP
9 ... ...
10 0x00008A00 testFunc PROC
11 ... MOV EAX,EDX
12 ... ...
13 ... RET
14 ... testFunc ENDP
第五行的CALL执行时,将下一条指令的地址0x00008025压入栈中,被调用函数testFunc的地址0x00008A00则被加载至EIP寄存器中。
当执行13行的RET时,ESP指向的数据将被弹出至EIP寄存器,ESP的数值增加,将只向栈中的上一个值。