【软件与系统安全】三、基础技术
这是《【软件与系统安全】笔记与期末复习》系列中的一篇
2022-02-21 第三次课后部分
2022-02-28 第四次课
2022-03-21 第四次课前半部分
x86 概念与内存模型
基本概念
x86: 基于 Intel 8086/8088 CPU 的一系列向后兼容的 ISA 的总称
-
IA-32: 32 位版本的 x86 指令集体系结构
-
三种主要操作模式:
- 实模式 (Real Mode)
- 保护模式 (Protected Mode)
- 系统管理模式 (System Management Mode) (大概就是 BIOS 下,对操作系统透明)
-
-
CISC(Complex Instruction Set Computer)体系结构
-
x64: 又称 x86-64, 是 x86 的扩展, 是与 x86 兼容的 64 位 ISA
字节序:多字节数据在内存中存储或在网络上传输时各字节的存储/传输顺序
小端序(little endian):低位字节存储在内存中低位地址, 效率较高(Intel CPU 使用)
大端序(big endian):低位字节存储在内存中高位地址, 符合思维逻辑。RISC 架构处理器(如 MIPS, PowerPC)采用
IA-32 内存模型
分段内存模型
程序内存由一系列独立的地址空间(称为“段”)组成。代码、数据和栈在不同的段中
逻辑地址=段选择器 + 偏移量
保护模式下的内存管理:分段(必须)+ 分页(可选)
程序线性地址空间 ≤4GB, 物理地址空间 ≤64GB
每个段最大 232 字节, IA-32 程序最多使用 16383 个段
x86 Linux 系统的线性地址空间分层
- 最大 3G ?
寄存器与数据类型
-
通用寄存器
-
x86 通用寄存器(GPR): 8 个 32 位
- EAX:(操作数和结果数据的)累加器
- EBX:(在 DS 段中数据的指针)基址寄存器
- ECX:(字符串和循环操作的)计数器
- EDX:(I/O 指针)数据寄存器
- EDI: 变址寄存器, 字符串/内存操作的目的地址
- ESI: 变址寄存器, 字符串/内存操作的源地址
- EBP:(SS 段中的)栈内数据指针, 栈帧的基地址, 用于为函数调用创建栈帧
- ESP:(SS 段中的)栈指针, 栈区域的栈顶地址
-
x64 通用寄存器:16 个 64 位
-
-
指令指针寄存器
-
x86 上 32 位的 EIP, 存放当前代码段中将被执行的下一条指令的线性地址偏移
-
程序运行时, CPU 根据 CS 段寄存器和 EIP 寄存器中的地址偏移读取下一条指令, 将指令传送到指令缓冲区, 并将 EIP 寄存器值自增, 增大的大小即被读取指令的字节数
-
不能直接修改 EIP, 修改途径:
- 指令 JMP, Jcc, CALL, RET
- 中断或异常
-
-
程序状态与控制寄存器
-
段寄存器
-
控制寄存器(CR0-CR4)
-
调试寄存器(DR0-DR7)
-
系统表指针寄存器(GDTR, LDTR, IDTR, task register)
-
MMX(MM0-MM7, XMM0-XMM7)
-
FPU(ST0-ST7, …)
RIP 相对寻址
x64 允许指令在引用数据时使用相对于 RIP 的地址,常用于访问全局变量, 全局变量 a 常通过 a(%rip)
进行访问
Relative Instruction-Pointer
基本数据类型
数据类型——指针类型
x64 规范地址(Canonical Address)
- X64 的虚拟地址宽度为 64 位,但实际 CPU 只使用 48 位地址空间
- 虚拟地址的规范形式指 48~63 位都与第 47 位相同的地址
- 若代码试图解引用一个非规范地址,则触发系统异常
指令集与调用惯例
汇编指令风格
AT&T: source 在 destination 前,在较早期的 GNU 工具中普遍使用(如 gcc, gdb 等)
mov $4, %eax
mov $4, %(eax)
Intel: destination 在 source 前, “[. . .]”含义类似于解引用, MASM, NASM 等工具中使用
mov eax, 4
mov [eax], 4
IA-32 指令编码
IA-32 指令的典型内存寻址
典型内存寻址方式
MOVS
(MOVSB / MOVSW / MOVSD): 用于实现字符串或内存的复制
- 在两个内存地址之间移动 1 字节/2 字节/4 字节数据
- 使用 EDI 保存目标地址;使用 ESI 保存源地址
- 源/目标地址自动更新, DF 标识决定自增(DF=0)/自减(DF=1)
SCAS
: 用 AL/AX/EAX 减去[EDI],更新 EFLAGS,并对 EDI 自增/自减
STOS
: 将 AL/AX/EAX 的值写入 EDI 指向的内存
x64 算术运算: 多数算术运算都提升到 64 位,即使操作数只有 32 位
MOV RAX, 1122334455667788H
XOR EAX, EAX ;也会清除RAX的高32位,执行后RAX=0
MOV RAX, 0FFFFFFFFFFFFFFFFH
INC EAX ;执行后RAX=0
CMP
: 算数比较
- 比较两个操作数(通过相减), 并设置 EFLAGS 中的适当标识位
- 常与条件跳转 Jcc 指令配合使用, 跳转依据即 CMP 运算结果
TEST
: 逻辑比较
- 比较两个操作数(通过逻辑 AND 运算), 并设置 EFLAGS 中的适当标识位
JMP
: 无条件跳转到目标指令地址(可用相对地址或绝对地址)
Jcc (cc=conditional code)
栈操作与函数调用指令
栈: 线性地址空间中连续的内存区域, 后进先出, 存在于一个栈段内,该栈段可由段寄存器 SS 检索的段描述符指向:
- 任意时刻, ESP 寄存器所包含的栈指针均指向栈顶位置
- 所有针对栈的指令操作, 均基于 SS 对当前栈的引用
- 通常由高地址向低地址扩展(PUSH 时 ESP 自减,POP 时 ESP 自增)
栈操作指令
- PUSH: 将字(或双字)压栈 (ESP 先自减)
- POP: 将字(或双字)弹出栈 (ESP 后自增)
- PUSHA/POPA/PUSHAD/POPAD
- ENTER:进入栈帧, 等价于
PUSH EBP; MOV EBP, ESP
- LEAVE:退出栈帧, 等价于
MOV ESP, EBP; POP EBP
“ESP:(SS 段中的)栈指针, 栈区域的栈顶地址”1
“栈帧基指针: 由 EBP 指向的被调用函数栈帧的固定参考点”2
栈帧: 是将调用函数和被调用函数联系起来的机制,栈被分割为栈帧,栈帧组成栈。栈帧的内容包含:
- 函数的局部变量
- 向被调用函数传递的参数
- 函数调用的联系信息(栈帧相关的指针: 栈帧基址针,返回指令指针)
栈帧基指针: 由 EBP
指向的被调用函数栈帧的固定参考点
返回指令指针: 由 CALL 指令压入栈中的 EIP
寄存器中的指令地址
- 该地址是被调用函数返回后首先执行的调用函数指令的地址,即调用函数中 CALL 指令的下一条指令的地址
函数的指令框架
PUSH EBP
MOV EBP, ESP
…
MOV ESP, EBP ;opt
POP EBP
RETN
EBP “EBP:(SS 段中的)栈内数据指针, 栈帧的基地址, 用于为函数调用创建栈帧”3
ESP “ESP:(SS 段中的)栈指针, 栈区域的栈顶地址”1
EIP “x86 上 32 位的 EIP, 存放当前代码段中将被执行的下一条指令的线性地址偏移”4
esp,eip,ebp -->对应64位的 rsp,rip,rbp
CALL 指令语义(及后继指令语义)
- 将 EIP 的当前值(返回指令指针)压栈
- 将 CALL 的目标指令(被调用函数的第一条指令)的地址偏移载入 EIP 寄存器
- CALL 指令结束后开始执行被调用函数
- PUSH EBP: 将调用函数的栈帧的 EBP 压栈
- ESP -> EBP,确立新栈帧(被调用函数栈帧)的基址针
- 执行被调用函数的具体功能
RET 指令语义(及前序指令语义)
-
RETN 指令执行之前
- EBP -> ESP: 清空当前被调用函数的栈帧(此时 ESP 指向的栈顶内容恰好为调用者函数的 EBP)(可选)
- POP EBP: 将 EBP 恢复为调用者函数的原始 EBP
-
将栈顶的内容(返回指令指针)弹出到 EIP
-
若 RETN 指令有参数 n, 则将 ESP 增加 n 字节, 从而释放栈上的参数
-
恢复对调用函数的执行
近调用/近返回
控制流转移到/出当前代码段中的被调用函数, 通常提供对本地函数的访问
远调用/远返回
控制流转移到/出其他代码段中的被调用函数, 通常提供对 OS 函数或其他进程函数的访问
- 前述指令语义针对近调用. 对于远调用(调用函数和被调用函数不在同一代码段中), 除需要保存 EIP 之外还要保存 CS
- CPU 不跟踪返回指令指针的位置, 由程序员确保在执行 RET 指令时,栈顶内容为返回指令指针
- CPU 不要求返回指令指针必须指回调用者函数, 在执行 RET 指令前,返回指令指针可以被修改并指向当前代码段中任一地址(脆弱性)
调用惯例 (Calling Convention)
是对函数调用时如何传递参数和返回值的约定
- 参数传递用寄存器?用栈?两者都用?
- 参数从左到右/从右到左压栈?
- 返回值存储在栈?寄存器?两者都存?
x86 主要调用惯例
- cdecl: C 语言中使用(GCC, GNU libraries); 参数从右到左压栈; EAX, ECX, EDX 不保存(调用者应自己保存); 返回值由 EAX 返回;调用者清理栈 (ESP 自增)
- stdcall: 常用于 Win32 API, 被调用者清理栈(RETN n)
- fastcall: 类似于 stdcall, 但使用寄存器 ECX、EDX 传递函数的前 2 个参数
System V AMD64 ABI 调用惯例
long myfunc(long a, long b, long c, long d,
long e, long f, long g, long h) {
long xx = a * b * c * d * e * f * g * h;
long yy = a + b + c + d + e + f + g + h;
long zz = utilfunc(xx, yy, xx % yy);
return zz + 20;
}
Red-Zone 优化
rsp 指针向下(低地址)的 128 字节栈空间可保留为不被信号或中断处理程序更改,,从而作为函数的临时数据空间, 称为 red zone
叶函数(即不调用其他函数的函数)使用 red zone 作为其栈帧,而不需要在其序言语句和收尾语句中修改栈指针
叶函数实例
long utilfunc(long a, long b, long c){
long xx = a + 2;
long yy = b + 3;
long zz = c + 4;
long sum = xx + yy + zz;
return xx * yy * zz + sum;
}
栈帧基址针优化
对于需要栈帧的函数, 处理栈帧基址针有两种方法
-
方法 1: 传统方法(GCC 无优化选项时)
- 函数序言: 保存 caller 的栈帧基址针; 创建新的栈帧基址针
- 函数体: 对栈单元的引用采用相对于栈帧基址针的方式
- 函数结尾: 恢复 caller 的栈帧基址针
-
方法 2: 更快的方法(GCC 优化选项时)
- 不保存/恢复栈帧基址针; rbp 作为通用寄存器使用
- 对栈单元的访问通过相对于 rsp 来完成
- 初始进行栈帧分配; rsp 在函数执行过程中保持在一个固定位置
中断指令
中断: 通常指由I/O设备触发的异步事件
异常(exception): CPU在执行指令时, 检测到一个或多个预定义条件时产生的同步事件
- 故障 (fault): 可修正的异常。故障处理后执行产生故障的指令
- 陷入 (trap): 调用特定指令(如SYSENTER)时产生的异常。陷入处理后执行产生陷入的指令的下一条指令
中断和异常的处理
中断与一个索引值相关, 该索引值是一个函数指针数组(中断向量表IVT/中断描述符表IDT)的索引, 当中断发生时,CPU执行对应索引处的函数, 然后恢复中断发生前的执行
INT n
-
生成一个软件中断, 其中n为中断向量编号
- INT3 / INT 3H
- 显式地调用断点异常处理程序
- 指令的十六进制值: 0xCC或0xCD 0x03
-
INT 80H
- IA-32的Unix系统调用 (对于x64, 系统调用为SYSCALL指令)
IRET
- 从中断处理程序返回被中断函数
- 与RET的区别: IRET还会从栈上恢复EFLAGS
控制流图
-
节点:由一系列汇编指令组成的基本块
- 一个基本块由一系列顺序执行的汇编代码组成, 其间没有跳转指令,也没有其他外部跳转指令以其间为跳转目标
-
有向边:连接各基本块
- 从基本块b1到基本块b2的有向边的含义是:在执行完b1后,有可能开始执行b2
-
从一个基本块可以发出多条有向边
- 例如, 当该基本块的最后一条指令为条件跳转指令时
可执行文件格式
(略)
可执行文件格式
被装载器和链接器使用的可执行文件的规范格式
典型代表
Unix/Linux 下: Executable and Linkable Format(ELF)格式
Windows 下: Portable Executable(PE)格式
可执行程序中的元信息(meta information)
ELF 格式
对象文件的分类与生成
对象文件的视图
由于对象文件参与程序的链接和执行,因此 ELF 格式提供两个并发的视图
链接视图(Linking view)
执行视图(Execution view)
节头表(Section header table)
用于链接视图, 包含 ELF 文件的各个节的描述信息
每个节包含
- 名称和类型
- 在运行时请求的内存位置
- 权限
名称 含义
.interp 程序解释器(动态链接器)的路径名
.text 程序代码(可执行指令序列)
.data 已初始化的全局数据
.rodata 只读的数据
.bss 未初始化的全局数据
.init 用于进程初始化的可执行指令序列
.fini 用于进程终止的可执行指令序列
.plt 保存过程链接表(procedure linkage table), 其中包含了
动态链接器调用从共享库中导入的函数所必需的相关代码
.rel. 节 的重定位信息
.dynamic 动态链接信息
程序头表(Program header table)
用于执行视图, 告诉系统如何在内存中创建一个进程
将文件的主体看作一系列的段, 每个段包含段类型, 被请求的内存位置, 权限, (在文件中或在内存中的)大小
段类型
- PHDR 该段保存了程序头表本身的位置和大小
- LOAD 可装载的段, 此类型的段将被装载到内存中,LOAD 类型的段例如
- 代码段: 可读且可执行
- 数据段: 可读且可写, 或只可读
- INTERP 指向当前可执行文件的动态链接器的路径(.interp section)
- DYNAMIC 指向动态链接信息(.dynamic section)
- NOTE 指向操作系统相关的规范信息, 运行时不需要, 因而易被感染
段 vs 节
段是程序执行的必要组成部分, 每个段中划分为不同的节
对程序内存布局的描述由程序头表完成
每个 ELF 目标文件都有节, 但不一定有节头
节头对程序执行不是必需的
节头表信息主要用于反汇编(如 objdump)和调试
缺少节头并不意味着节不存在, 只是无法通过节头引用节
ELF 可执行文件的装载过程
对象文件的装载
绝对代码 vs 位置独立代码(position-independent code, PIC)
静态链接与重定位过程
程序运行前的链接: 多个目标文件(.o, .so) ⇒ 一个可执行文件或.so
以多个目标文件作为输入
将类型相同的节合并到结果目标文件中, 例如,将多个.text 节合并到一个.text 节
重定位代码/数据(通过重定位信息)
// file1
int x = 5;
extern int function();
int main() {
int r = x +function();
exit (0);
}
// file2
int v = 10;
int u = 32;
int y;
int function() {
return v+u;
}
重定位
动态链接