80386常用寄存器
一、常用寄存器
1.1 通用寄存器
- EAX:累加寄存器
- EBX:基址寄存器
- ECX:计数寄存器
- EDX:I/O指针–数据寄存器
- ESI:(字符串操作源指针)源变址寄存器
- EDI:(字符串操作目的指针)目的变址寄存器、
- ESP:堆栈指针寄存器,存放对战的栈顶地址,不可做为通用寄存器
- EBP:基址指针寄存器,表示栈区域的基地址,保存ESP,函数返回时把值返回ESP
1.2 段寄存器
Intel公司决定采用 (段地址<<4)+段偏移 的方式,组合成20位地址。 • 16bit段地址保存在段寄存器中;段偏移也是16bit。 • 通过将段地址左移4bit的方式,将两个16bit地址组合成20bit地址。
1.3 程序状态与控制寄存器
-
EFLAGS,标志寄存器:
- 存储相关指令的某些执行结果
- 为CPU执行相关指令提供行为数据
- 控制CPU的相关工作方式
-
CF:进位标识位:
- 无符号运算,记录运算结果的最高有效位向更高有效位的进位或借位值
- 加法,进位则CF=1
- 减法,借位则CF=1
-
PF:奇偶标志位
- 结果中1的个数为偶数,则PF=1
- 结果中1的个数为奇数,则PF=0
-
AF:辅助进位标志位
- 字操作时,发生低字节向高字节进位或借位,AF=1
- 字节操作时,低4位向高4位进位或借位时,AF=1
-
ZF:零标志位
-
SF:符号标志位
-
OF:溢出标志位
-
EIP:指令指针寄存器
- 保存着CPU下一条将要执行指令的地址
- EIP自动增加,不能直接修改EIP,可以由JMP、CALL、RET、中断或异常修改
二、常用基本指令
2.1 数据传送指令
- 将数据、地址或立即数传送到寄存器或存储单元
- MOV:把源操作数传送到目的操作数
- MOV EAX,EDX; 寄存器EDX --》EAX的数据传送
- MOV WORD PTR[BX+DI],2; 16位立即数2送偏移地址为位BX+DI的字单元
- LEA:将源操作数的有效地址传送到通用寄存器
- LEA EAX,[EBP-4]; 将SS段中EBP-4所指向的存储单元送入EAX
- 等价于MOV EAX,EBP-4
- PUSH:操作数压入栈
- PUSH EAX; 寄存器EAX的值存入栈顶ESP
- POP:出栈
- POP EAX; 栈顶ESP保存的数据取出,赋值给EAX,再将ESP+4
2.2 算术运算指令
- 加法指令:ADD,INC
- 减法指令:SUB,DEC
- 乘法指令:MUL
- 除法指令:DIV
2.2.1 加法指令
- ADD :将源操作数和目的操作数相加,结果从到目的操作数
- ADD EAX,EBX; 将EAX,EBX的值相加,结果送到EAX中
- ADD指令的执行会影响CF,ZF
- INC:将目的操作数+1
- INC EAX:将EAX的值加一
- 目的操作数可以是通用寄存器,也可以是内存单元
2.2.2 减法指令
- SUB:将目的操作数减去源操作数,结果送入目的操作数
- SUB EAX,EBX;计算EAX-EBX的值,结果保存到EAX中
- SUB 指令执行同样会影响CF,ZF等标志位
- DEC:将目的操作数减一
2.2.3 乘法指令
- MUL:无符号数乘法指令,将源操作数和目的操作数相乘,结果送入目的操作数
- IMUL:有符号数乘法指令,将源操作数和累加器中的操作数相乘,结果送入目的操作数
- MUL,IMUL指令的源操作数为通用寄存器或存储器操作数,目的操作数缺省存放在寄存器(EAX)中
- MUL EBX;将EAX*EBX的值保存在EAX中
2.2.4 除法指令
- DIV:无符号数除法指令
- IDIV:有符号数除法指令
- DIV、IDIV指令的源操作数为通用寄存器或存储器操作数,目的操作数缺省存放在寄存器(EAX)中
- DIV EBX;将EBX除以EAX的值保存在EAX中,余数放在EDX中
2.3 逻辑运算和移位指令
2.3.1 逻辑运算指令
- AND:逻辑与
- OR:逻辑或
- XOR:逻辑异或
- 指令格式: AND(OR/XOR) DEST,SRC
- 源操作数可以是通用寄存器、存储器操作数或立即数,目的操作数是通用寄存器或存储器操作数
- AND EAX,EBX;将EAX,EBX按逻辑与,结果存在EAX
- NOT:逻辑非,按位取反,结果存入目的操作数
2.3.2 移位指令
- SAL:算术左移
- SHL:逻辑左移
- 指令格式:SAL (SHL) DEST,OPR
- SAL ECX,4; ECX左移4位,结果存在ECX中
- 说明:SAL、ECX指令功能完全相同,按照操作数 OPRD 的规定的移位位 数对目的操作数进行左移操作,每移一位,最低位补0,最高位移入标志 位 CF 中。左移都是补0。
- SAR:算术右移,
- 每移一位, 最低位移入标志位 CF,最高位(符号位)保持不变。相当于对有符号数进行除 2 操作。最高位填充符号位。正数填充0,负数填充1
- SHR:逻辑右移
- 指令格式:SAR DEST,ORPD
2.3.3 循环移位指令
- ROL:循环左移
- ROR:循环右移
- 指令格式:ROL DEST,OPRD
2.4 控制转移指令
2.4.1 无条件转移指令
- JMP:无条件转移
- CALL:过程调用,等价于PUSH EIP+4;JMP addr
- RET:过程返回,从栈中将保存的返回地址取出,跳转到原函数执行,等价于POP EIP
2.4.2 条件转移指令
指令 |
转移条件 |
说明 |
JC $dest |
CF=1 |
有进位/借位时转移 |
JNC $dest |
CF=1 |
无进位/借位时转移 |
JE/JZ $dest |
ZF=1 |
相等/等于0时转移 |
JNE/JNZ $dest |
ZF=0 |
不相等/不等于0时转移 |
JS $dest |
SF=1 |
是负数时 转移 |
JNS $dest |
SF=0 |
不是负数时转移 |
- 通常条件转移指令会和能改变 EFLAGS 对应位的指令配合使用
- CMP 指令和 SUB 指令相同,但不会将计算结果保存到目的操作数。因此, CMP EAX, 3 执行之后会修改 EFLAGS 的 ZF 标志位:
- 如果 EAX – 3 = 0,则 ZF 置为 1
- 如果 EAX – 3 ≠ 0,则 ZF 置为 0
- JZ 指令查看 ZF 标志位,如果是 1 ,则跳转到对应位置,否则顺序执行
PLT表和GOT表
1、Linux编译过程
预处理、编译、汇编、链接
1.1 预处理
- 处理所有宏定义 #define 以及所有的条件预编译指令(#if #ifdef #elif #else #endif)
- 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置
- 删除所有注释
- 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号
- 保留所有#pragma编译器指令,后续编译过程需要使用
1.2 编译
- 编译:利用编译程序,将源语言编写的程序转化为目标语言的过程
- 目标语言就是汇编语言
1.3 汇编
- 汇编:对汇编语言编写的代码进行处理,生成处理器能够识别的指令,即机器码
- 生成目标文件
1.4 链接
- 汇编生成的目标文件 .o 不能直接运行
- 只有我们自己写的代码转换成了机器码,他需要和系统组件(标准库、动态链接库等)搭起来才能运行
- 链接LInk是一个打包的过程,将所有二进制形式的目标文件和系统组件组合 成一个可执行文件,完成连接的过程叫 链接器
2、静态编译动态链接
3、延迟绑定
- 把对某个函数地址进行重定位的工作,推迟到该函数被第一次调用时再进行
- ELF使用 PLT 过程链接表 技术实现延迟绑定
3.1 重定向
- 在程序编译的期间,引用外部函数和变量时,只需要知道其声明和类型。函数的定义以及变量(外部符号)时,只需要知道其生命和类型。函数的定义以及变量的值要到链接甚至执行时才能确定。
- call read:调用read
- 在编译时,会充填上一个占位值,使程序能够正确调用read函数,就是重定位
- 不直接在代码段放置并修改占位值,因为代码段不可修改,真正:在调用外部函数时,首先跳转到PLT表,PLT表中的代码会解析函数的真实地址,并将它充填到GOT表中,此后直接跳转GOT表就可以执行对应的外部函数。
3.2 GOT表
全局偏移表,用于记录在ELF文件中所用到的共享库中符号的真实地址
3.3 PLT表
在真正的实现中,并不直接在代码段放置并修改占位值,因为代码段是不可修改的。真正实现方法是:在调用外部函数时,首先跳转到PLT表,PLT标会解析函数真实地址,并填到GOT表中
过程链接表
4、GOT表的攻击利用
栈溢出基本原理
1、函数栈调用原理
- 函数调用栈:是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等
- 栈由高地址向低地址扩展。(相对,堆由低地址向高地址扩展)
- 函数栈的状态主要涉及三个传感器:
- ESP:栈顶指针
- EBP:栈底指针
- EIP:指向下一条执行的指令(PC)
1.1 参数逆序入栈
- 将被调用函数的参数逆序压入栈中
- 64位机首先用寄存器传参,前六个参数被传入RDI,RSI,RDX,RCX,R8,R9,其余参数再压入栈
1.2 保存返回地址
- 将调用函数进行调用之后的下一条指令地址作为返回地址,压入栈中,目的是保存caller的rip信息
1.3 保存(主调函数的)栈基地址
- 将调用函数的基地址(ebp的值)压入栈内
- 将当前栈顶地址传到ebp寄存器内
1.4 进入函数执行
- 抬高ESP,为新的函数栈留出空间,保留新函数的临时变量
- 新函数能使用的空间在ESP到EBP之间,EBP不会移动,保证之前栈中的临时变量不会改变
1.5 清理(被调函数的)局部变量
(函数返回的过程)
把EXP挪到EBP的位值,之前ESP和EBP之间的变量都作废
1.6 恢复(主调函数)函数栈
将主调函数的栈基地址从栈中取出,保存到EBP寄存器内,此使栈顶指针ESP指向返回地址
1.7 恢复EIP,清理参数
- 再将返回地址从栈内弹出,并存到EIP寄存器内
- 手动或自动清理参数
2、栈溢出
3、ROP技术
- ROP 返回导向编程,攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。
- 因为所有执行的指令来自原始程序内的可执行存储器区域,所以这避免了直接代码注入的麻烦,并绕过了用来组织来自用户控制的存储器的指令的执行的大多数安全措施(NX)
3.1 32位
- 32位架构下,函数的参数保存在栈中
- 将对应参数布置在栈中即可
- 返回地址下方是第二次的返回地址
3.2 64位
- 64位架构下,函数的前六个参数保存 在RDI,RSI,RDX,RCX,R8和 R9中, 如果还有更多的参数才保存在栈里。
- 劫持控制流后,必须先将参数设置到寄存器中,再跳转到相应的函数执行
- 对应上文的利用方法,必须先将字符串“/bin/sh”的地址保存在RDI,再跳转到system执行
-
参数布置在RDI
-
利用特殊的程序片段(gadget)来设置寄存器的值
-
利用工具 ROPgadget 来寻找可用的程序片段
-
ROPgadget --binary stack1-x64 --only “pop|ret”
格式化字符串漏洞
1、格式化字符串原理
- 格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其解析之后的参数,通俗的来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。
格式化字符串的基本格式如下:
2、格式化字符串漏洞
- C语言并未检查格式化字符串和参数的对应关系
- (printf(“%d %d %d %s”, 123 , 345))
- 如果用户能控制格式化字符串,就能够实现:
- 读取内存值:利用%d,%p打印栈中的值,%s获取指定内存地址的值
- 修改内存值:利用%n实现对指定内存地址的写入
- 引发程序崩溃:当%s对应的地址不是可读位置,将引发错误
- 假设我们能够控制栈的内容,在printf的第二个参数的位置设置为puts@got的地址
- “%2$p”:打印puts@got的内容
- ”aaaaa%2¥n“:将puts@got的内容修改为5
-
字符串存放
-
整数表示
-
大端 一般书写
-
小端
GLIBC HEAP
1、内存分配器
1.1 HEAP
- LInux提供两个系统调用来分配动态内存:
- brk():扩充或收缩堆的大小
- mmap():在内存空间寻找一块区域映射
1.2 内存分配器PTMALLOC2
- 仅使用这两个函数分配内存,对于应用程序非常不友好
- 现申请内存块a,再申请b;要想释放b,必须先释放a
- 内存分配器作为中间层
- 对应用程序提供接口 malloc/free
- 使用brk,mmap 系统调用获得内存,并负责管理
- Linux 使用 ptmalloc2 作为分配器
2、PTMALLOC数据结构
2.1 PTMALLOC 管理概述
- 我们称malloc申请所得的内存为 chunk 块
- chunk保存了内存分配器用于管理内存的数据结构
- chunk有两种状态:使用 (malloc得到), 空闲 (free释放)
- 空闲chunk不会立即还给操作系统,而被分配器统一管理,存放于bin中
2.2 CHUNK结构解析
- prev_size:物理地址前一chunk的大小
- size:当前chunk的大小,必须是2*SIZE_SZ的整 数倍,即32位架构下,必须是0x8的倍数;64位架 构下,必须是0x10的倍数。因此,低3bit被空出, 作为标志位:
- A标志:是否是主分配区(main arena)
- M标志:是否是mmap()映射区域
- P标志:物理地址前一chunk(prev chunk)是 否正在被使用
- prev_size: 物理地址前一chunk的大小
- fd/bk: (forward, back)
- fd 指向下一个(非物理相邻)空闲的 chunk
- bk 指向上一个(非物理相邻)空闲的 chunk
- fd_nextsize/bk_nextsize: 仅用于large bin
- fd_nextsize 指向前一个与当前 chunk 大 小不同的第一个空闲块,不包含 bin 的头指 针。
- bk_nextsize 指向后一个与当前 chunk 大 小不同的第一个空闲块,不包含 bin 的头指针。
2.3 CHUNK的空间复用
- chunk的空间复用
- 当chunk处于使用状态时,其 fd/bk 和 fd_nextsize /bk_nextsize 区域都不起作 用,作为用户数据使用;其释放后,该字段 用于bin中的链表管理。
- chunk 处于使用状态时,其物理地址后 一 chunk 的 prev_size 字段不起作用, 作为前一 chunk 的用户数据字段。
2.4 BINS介绍
- 用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk。
- 当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低 内存分配的开销。
- 按照chunk的大小和使用状态,设置4种bin进行管理:
- fast bin(单链表)
- small bin(双链表)
- large bin(双链表)
- unsorted bin(双链表)
- bins是一个数组,每个成员都是一个链 表指针,共127*2个成员。
- 为了方便起见,我们认为只有127个成员, 每个成员是一对双链表指针。
2.5 堆段的产生和初始化
- 第一次malloc后,进程会创建堆段 (heap segment)。除了会获得所申请空 间的内存大小,还会获得一个连续的内 存块,称为分配区(arena)
- 虽然程序可能只是向操作系统申请较小的 内存,操作系统会将一块相当大的空间 (20K)返回给用户程序,交由内存分配器 管理。这样做是为了避免过多地使用系统 调用。
- 当arena空间不足时,通过brk()来增加堆 空间。同理也可以缩小堆空间。
- arena的相关信息记录在arena header 中,即右图所示的结构体内。
2.51 TOP CHUNK
- topchunk 位于堆地址最高的位置,该chunk不属于任何bin,是特殊的chunk
- 当调用 malloc 时,分配器会先尝试在 bins中寻找合适的空闲块分配;如果 bins中没有合适的块,则会尝试 从 topchunk中切割出合适的大小,返回给 用户。
- 当释放一个chunk时,如果它与 topchunk物理地址相邻,则直接并入 topchunk 。
2.6 FAST BIN
大多数程序经常会申请以及释放一些比较小的内存块。如果将一 些较小的 chunk 释放之后发现存在与之相邻的空闲的 chunk 并 将它们进行合并,那么当下一次再次申请相应大小的 chunk 时, 就需要对 chunk 进行分割,这样就大大降低了堆的利用效率。 因为我们把大部分时间花在了合并、分割以及中间检查的过程中。 因此,ptmalloc 中专门设计了 fast bin,对应的变量就是 malloc state 中的 fastbinsY
- 32B <= fastbin <= 128B
- 单向链表。(所以只使用fd,废弃bk )
- free时不复位inuse位!(避免合并)
- 采用LIFO原则(后被释放的chunk将先 被申请到)
- fastbin 和 smallbin 的大小有重合,而 fastbin的优先级高于smallbin 。
2.7 SMALL BIN
- 32B <= smallbin <= 1008B
- 循环双向链表。为了简化结点的处理,头部也视为一 个结点
- free 时inuse位置为 0
- 采用FIFO原则(先被释放的chunk将先被申请到)
- 无论是fd指针,还是bk指针,指向的都是chunk header的起始处!
2.8 UNSORTED BIN
- unsortedbin 只有一个链表(在 bin 中只占一项),其链表结构组织 方式和 smallbin 相同。
- unsorted bin 可以视为空闲 chunk 回归其所属 bin 之前的缓冲区。
- 当一个较大的 chunk (非topchunk)被分割成两半后,如果剩下的部分大于 MINSIZE(=32B),就会被放到 unsorted bin 中。
- 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻 时,该 chunk 会被首先放到 unsorted bin 中。
- 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻 时,该 chunk 会被首先放到 unsorted bin 中。
- 在特定时刻,unsortedbin会清空,将相应的chunk放入smallbin与 largebin。
2.9 LARGE BIN
- smallbin 与 fastbin 中,每个链表chunk大小相同;而 largebin 仅限 定大小在一定范围即可。(范围太大,不便于细分)
- 循环双向链表,额外增设 fd_nextsize 和 bk_nextsize,用于快速适配。
- 同一链表内,按照chunk的大小进行排序,size大者靠前
- 相同size,则先释放者靠前。
3、MALLOC/FREE概述
3.1 MALLOC
- malloc函数对应libc中的 _libc_malloc函数
- _libc_malloc 仅是对 _ int_malloc 的封装,malloc的核心功能由 _int_malloc 实现
3.1.1 _LIBC_MALLOC
- 检查是否有内存分配函数的钩子函数(_malloc_hook)
- 如果有,则调用_malloc_hook指向的哈桑农户,并结束malloc
- 用于用户自定义的堆分配函数
- 寻找一个可供分配的arena
- 在指定的arena中申请对应的内存(_int_malloc)
- 如果分配失败,则尝试在其他的arena中分配
3.1.2 _INT_MALLO
- 将用户申请的内存大小转换为chunk大小
- 检查size的范围,先从bins中寻找是否有可用
- 如果符合fastbin大小,先在fastbin寻找
- 如果符合smallbin的大小,或者fastbin没有可用块,则在smallbin中寻找
- 如果都失败,调用malloc_consolidate函数,试图将fastbin的chunk合并,并将 合并后的chunk放入unsortedbin中(避免内存过于碎片化)
- 再将unsortedbin中的chunk依次取出判断,如果大小完全符合要求,则返回该 chunk;否则将chunk放入对应的bin(smallbin,largebin)
- 如果仍然失败,则从largebin中寻找最合适的块,必要时会切割chunk
- 到这里,说明bins中没有合适的chunk。此时将会从topchunk中切割
3.2 FREE
- 和malloc函数类似,free函数对应libc中的 _libc_free函数
- __libc_free 仅是对 _int_free 的封装, free 的核心功能由 _int_free 实现
3.2.1 __LIBC_FREE
- 检查是否有内存分配函数的钩子函数(__free_hook)
- 用于用户自定义的堆分配函数
- 判断当前chunk是否是mmap函数分配的,若是则调用munmap解除映射即
- 找到当前chunk所在的arena,并从该arena释放该chunk
3.2.2 _INT_FREE
- 对chunk进行简单检查
- 指针必须 2*SIZE_SZ 对齐(x64下即 0x10 对齐)
- chunk size必须大于 MINSIZE(x64下即 0x20)
- 检查其inuse标志
- 若chunk与topchunk相邻,则并入topchunk
- 检查chunk size的范围:
- 如果符合fastbin大小,则插入对应size的fastbin头部
- 如果不符合fastbin,就判断是否可以合并chunk——检查chunk物理地址相邻的前后chunk,若空闲则将它 们合并为一个大chunk。重复执行该步骤。
- 若发生了合并,且合并后chunk与topchunk相邻,也会并入topchunk,并结束。
- 将chunk放入对应的bin中(smallbin, largebin)