缓冲区溢出原理
1.程序内存布局
在程序运行的生命周期中,内存中比较重要的四部分数据是程序数据、堆、库数据、栈.此外内核空间也会映射到程序的内存中.

- 程序数据(Proc)
- 代码段(Text seg)
- 主要存放可执行文件的代码指令,是可执行程序在内存中的镜像,代码段一般是只读的- 数据段(Data seg)
- 存放可执行文件中已经初始化的变量,包括静态分配的变量和全局变量
- BSS seg
- 包含程序中未初始化的全局变量,在内存中bss 段全部置0
- 堆(HEAP)
- 存放进程运行过程中动态申请的内存段.进程调用malloc、alloca、new等函数来申请内存,利用free、delete函数释放内存.这部分大小不固定,以方便程序灵活使用内存.
- 库数据(Memory Mapping)
- 存放映射的系统库文件,其中比较重要的是libc库,很多的程序所使用的系统函数都会动态的连接到libc库中
- 栈(Stack)
- 栈存放程序临时创建的局部变量.包括函数内部的临时变量和调用函数时压入的参数.由于
- 代码段(Text seg)
1. 函数调用栈
栈介绍
栈内存一般根据函数栈来进行划分,不同函数栈之间是相互隔离的,从而能够实现有效的函数切换
函数栈上存储的信息一般包括
临时变量 (canary)
函数的返回栈基址(bp)
函数的返回地址(ip)
函数栈的调用机制
程序运行时,为了实现函数之间的相互隔离,需要在进入新的函数之前保存当前函数的状态
函数调用时,首先将参数压入栈,然后压入返回地址和栈底指针寄存器bp,其中压入返回地址是通过call实现的
函数结束时,将sp重新指向bp位置,并弹出bp和返回地址==ip==,通常bp是通过leave或者(pop ebp实现的)
函数栈示意图
- 在函数栈中bp存储了上一个函数栈的基址,ip存储的是调用处的下一条指令的位置.
- 返回当前函数时,会从栈上弹出这两个值,从而恢复上一个函数的信息
函数参数的传递
函数调用协议
- _stdcall: wimdows API默认的函数调用协议
- 参数由由右向左入栈
- 调用函数结束后由被调用函数来平衡栈
- _cdecl: c++/c默认的函数调用协议
- 参数由右向左入栈
- 函数调用结束后由函数调用者来平衡栈
- _fastcall:适用于对性能要求较高的场合
- 从左开始将不大于4字节的参数放入CPU的ecx和edx寄存器,其余参数从右向左入栈
- 函数调用结束后,由被调用者来平衡栈
- _stdcall: wimdows API默认的函数调用协议
对于X86程序
普通函数传参:参数基本都压在栈上(也有寄存器传参的情况)
syscall传参: eax对应系统调用号,ebx,ecx,edx,esi,edi,ebp分别对应前六个参数,多余的参数压在栈上
==对于x86程序而言,参数传递是通过栈来实现的,在调用完以后,需要清除栈中参数,所以一般函数调用完之后需要用形如“pop;pop *;,,,,;ret;”的 gadget来调整.因为函数调用时返回地址会压入栈中,既汇编中的“call func”指令等同于“pop ret_addr;jump func”,因此在执行jmp func的时候,ret_addr已经压入栈中了==

- 将ret_addr改成”pop*;ret“指令的gadget,用来弹出后续的args,即成为ROP的形式,这也是ROP的原理

对于X64程序
普通函数传参:先使用rdi,rsi,rdx,r8,r9寄存器作为函数参数的前六个参数,多余的参数依次压在栈上
syscall传参:rax对应系统调用号,传参规则和普通函数一样
对于X64的程序来说,一般情况下,函数的参数较少,通常用寄存器来传递参数,所以在进入函数之前,因该先将寄存器设置好

函数运行时内存中的一段连续的区域,用来保存函数运行时的状态信息,包括参数与局部变量

涉及的寄存器
栈寄存器
- esp: 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化
- ebp:用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引函数参数或局部变量的位置
特殊寄存器
eip:用来存储即将执行的程序指令的地址,cpu按照eip的存储内容读取指令并执行,eip随之指向下一条指令,如此反复
Eflags:标识位寄存器
cs(code segment):储存代码段的地址
ds(data segment):储存数据段的地址
ss(stack segment):储存函数调用栈的地址
一般寄存器
- eax(Accumulate):累加寄存器,用于进行算数运算,返回函数结果
- ebx(Base):基址寄存器,在内存寻址时(比如数组运算),存放基地址
- ecx(Count):计数寄存器,用以在循环中计数
- edx(Data):数据寄存器,通常配合eax存放运算结果等数据
索引寄存器
- esi(Souce Index):指向要处理数据的地址
- edi(Destination Index): 指向存放数据结果的地址
涉及的汇编指令
- MOV DST,SRC; (数据传输指令) 将src中的数据传输到dst中
- PUSH SRC; (压入堆栈指令)将SRC压入栈
- POP DST;(弹出堆栈指令) 弹出堆栈指令,将栈顶的数据弹出并保存至DST
- LEA REG,MEM; (取地址指令)将MEM的地址存在REG中
- ADD/SUB DST,SRC;(加减指令)
- CALL PTR;(调用指令 )将当前的eip压入栈顶,并将PTR存入eip
- RET ;(返回指令)将栈顶数据弹出至eip
调用函数时,栈的变化(如何抛弃被调用函数的状态)







2.技术清单
覆盖缓冲区的具体用途
覆盖当前栈中函数的返回地址(当前函数或者之前的函数),获取控制流
覆盖栈中所存储的临时变量
覆盖栈底寄存器bp(之前的函数)
覆盖bp实现栈转移
这种情况主要针对“leave;ret”指令;该指令等价于“mov sp,bp;pop bp;ret”

覆盖bp,实现参数索引改变
- 一般来说,很多临时变量的索引,都是根据相对于bp的偏移量来进行的,如果bp发生了变化,那么后续的很多参数也会发生变化
关注敏感函数
- background
- 控制指令执行最关键的寄存器就是eip,因此我们的==目标是让eip载入攻击指令的地址==
- 如何让eip指向攻击指令,

- background
- list
- 修改返回地址,使其指向溢出数据中的一段指令==(shellcode)==
- 修改返回地址,使其指向内存中已有的某个函数==(return2libc==)
- 修改返回地址,使其指向内存中已有的某段指令==(ROP)==
- 修改某个被调用的函数的地址,让其指向另一个函数==(hijack GOT)==
1.Shellcode(关闭地址随机化以及有可执行权限)
- payload = padding1 + address of shellcode + padding2 + shellcode

padding1 中的数据随便填充(⚠️如果是字符串输入,避免\x00,可能会被截断),==长度应该刚好覆盖函数的基地址==
- ==问题一==:padding1因该多长?
- 可以通过调试工具(GDB)查看汇编代码来确定这个值==一般是lea指令+4==
- 也可以通过程序不断增加输入长度来试探(如果返回地址被无效地址如”AAA“覆盖)程序会报错
- ==问题一==:padding1因该多长?
- address of shellcode 是后面shellcode处的起始处的地址,==用来覆盖返回地址==
- ==问题二:==shellcode 的起始地址是多少?
- 可以通过调试工具查看返回值的地址(ebp中的内容+4, 32位机)
- 但是调试里的地址可能和正常运行时不一样,此时就可以通过在padding2中填充若干长度的==“\x09”==,此机器码对应的指令是NOP(No Operation),既告诉cpu啥也不用做,然后跳到下一指令,有了NOP的填充,只要返回地址能够命中这一段中的任一位置,都可以跳转到shellcode的起始处
- ==问题二:==shellcode 的起始地址是多少?
- padding2可以随意填充,长度任意.
- shellcode因该是16进制机器码的格式

实例
1 | from pwn import * |
2.return2libc–指向内存中的函数(操作系统关闭ASLR)
获取libc中的system函数的地址,使用gdb,给main函数打上断点,然后使用
1
p system
该方法可以获取任意libc函数的地址
设置system函数返回后的地址,以及为system函数构造我们预定的参数
由于我们使用system的函数地址替换了原本的ip寄存器,强制执行了system函数.破坏了原程序栈桢分配和释放策略,所以后续的操作必须基于这个被破坏的栈桢结构来实现
例如下面的payload中的padding2的操作应该是pop ip,也就是system函数调用完成后需要返回的地址
- 为什么呢?
- 因为在正常情况下,函数是通过call进行调用的,因此在进入system前,call指令已经通过push ip将返回地址push到函数调用栈中,所以在正常情况下ret指令pop到ip的数据就是call指令push到栈中的数据,也就是说两者是成对出现的
- 但是!!!!由于我们是直接通过覆盖ip的地址从而跳转到system函数,并没有经过call指令的调用,也就是并没有push ip的操作,但是system函数却照常执行了ret指令的pop ip的操作.
- 因此,ret 指令pop到ip中的到底是哪一处的数据呢,答案就是padding2中的数据,也就是我们自己设定的system函数的返回地址
- 为什么呢?
知道了system部分的payload,那么如何获得system的地址以及bin/sh的地址呢?
可以通过puts与gets函数,gets造成溢出,puts泄漏libc中的system以及/bin/sh的地址
bin在so文件中的地址
1
strings -t x libc.so | grep bin
可执行的system(“/bin/sh”)在so文件中的地址
1
one_gadget libc.so
由于我们可以控制栈,根据rop的思想,我们需要找到的就是pop rdi;ret 前半段用于给第一个参数rdi赋值,后半段用于跳到其他代码片段
如何找到可以赋值的参数呢?
可以通过,ROPgadget,这里的值通常是固定的0x400833
1
ROPgadget --binary file_name --only "pop|ret" | grep rdi
- 因此我们可以构造payload泄漏出puts函数的真实地址
有了真实地址,我们还需要知道程序使用的libc,算出libc的基址
这里可以使用LibcSearcher
1
2
3
4from LibcSearcher import *
#第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
obj = LibcSearcher("fgets", 0X7ff39014bd90)知道了程序使用的libc,以及puts函数的真实地址,我们就可算出libc的基址,再通过基址加偏移就能得到函数的真实地址
1
2
3
4
5libc_base = 0X7ff39014bd90 - obj.dump("fgets")
obj.dump("system") #system 偏移
obj.dump("str_bin_sh") #/bin/sh 偏移
#system的真实地址
system_addr =libc_base + obj.dump("system")
这里有可能会遇到,匹配不到libc的错误,可以通过libc-database,添加libc
1
2
3
4
5
6./get # List categories
./get ubuntu debian # Download Ubuntu's and Debian's libc, old default behavior
./get all # Download all categories. Can take a while!
#或者添加自身的libc
./add /usr/lib/libc-2.21.so
实例
- 64位
1 | from pwn import * |
- 32位
1 | from pwn import * |
- payload = padding1 + address of system() + padding2 + address of “/bin/sh”

- Padding1 随意填充,==长度刚好覆盖基地址==
- 长度与shellcode处的一样的方法
- address of system() 是system在内存中的地址,==用来覆盖返回地址==
- system()函数地址在哪里?
- 从动态库中获取,计算绝对地址
- system()函数地址在哪里?
- padding2 数据长度应该为 ==4== (32位机) 由于不关心退出shell后的行为,可随意填
- ⚠️当需要多次绕过时,这里应该填充system执行后需要返回的地址,否则程序栈会崩溃
- address of ”/bin/sh“ 是字符串在内存中的地址,作为传给system的参数
- 字符串哪里找?
- 动态库搜索,计算绝对地址
- 没有就将其加入环境变量,通过getenv()函数获取
- 字符串哪里找?
3.ROP(开启NX时可用)–指向内存中已有的某段指令
payload = padding + address of gadget


ROP的常见拼凑效果是实现一次系统调用
如何寻找指令片段?
- 使用工具寻找以ret结尾的片段
如何寻找系统调用的参数?
- 可以使用pop指令将栈顶数据存入寄存器
- 如果内存中有可以用move指令进行传输
对于单个gadget,pop所传输的数据因该在gadget地址之后
- 具体的ROP例子
- 具体的ROP例子
适用于题目中没有给出任何有关cat flag有关信息,并且题目中也没有任何与system有关的函数
- 此时需要我们leak除libc函数的地址,再通过libc构造ROP
3.2 中级ROP
ret2_libc_csu_init
在64位程序中,函数的前六个参数是通过寄存器传参的,但是大多数时候,我们很难找到每个寄存器对应的gadgets,这个时候我们可以用x64下的__libc_csu_init中的gadgets。这个函数是用来对libc进行初始化操作的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp- 这里如果巧妙的控制==rbx==与==r12==,我们就可以调用我们想要调用的函数.例如设置rbx=0,r12位存储我们想要调用的函数的地址.
实例
1 | from pwn import * |
- 可以利用的csu
- 用gdb+pwn 进行动态调试,得到函数栈中的状态与payload的对应关系,如下


4.hijiack got – 修改某个被调用函数的地址,让其指向另一个地址
动态链接与静态链接
- 静态链接
- 把需要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分,使得文件包含运行时的全部代码,当多个程序调用相同函数时,内存中就会出现多个这个函数的拷贝,浪费内存
- 动态链接
- 仅仅在调用处加入了所调用函数的描述信息(往往是一些重定位信息),当程序运行时,才回去动态的链接到相应的函数,动态链接库的文件后缀为.so

- 静态链接
GOT与PLT
GOT(global offset table) 全局偏移量表,用来储存外部函数在内存的确切地址
PLT(procedure linkage table) 程序链接表,用来存储外部函数的入口点
第一次调用函数时解析函数地址并存入GOT表
第二次调用函数直接读取GOT中的地址
整个调用的过程
如何确定函数A在GOT表中的位置?
- 程序调用函数是通过PLT表跳转到GOT表的对应条目,在汇编指令中找到该函数在PLT中的入口点位置,从而定位到该函数在GOT中 的条目
如何确定函数B在内存中的位置?
- 函数在动态链接库中的位置是相对固定的.如果知道了函数A的运行地址(读取GOT表中的内容),也知道A和B在动态链接库中的相对位置,就可一推断出B的运行时地址
如何实现GOT表中的数据修改?
- 格式化字符串漏洞
5.Return-to-dl_resolve
- 核心思想是利用dl_runtime_resolve函数解析出system函数的地址,通常在没有提供lib函库的情况下使用.
- 使用条件
- 未给出libc库
- 没有开启PIE保护,如果开启PIE需要通过泄漏获取基地址
- 没有开启FULL RELOAD
3. payload与函数调用栈的关系
- payload的长度可以用 payload.ljust(0x105,b’a’)进行填充,使得填充后的payload的总长度为0x106 Bytes
⚠️p32(0xdeadbeef) 占4个Byte
paylaod的实际意义是通过输入的溢出,将我们需要的指令送到ip让cpu执行
- 下图是64位,中的对应关系

经过gdb+pwn调试,得到正确的关系,main实际上是返回值

下图是32位中的对应关系

4.通用的gadget
__libc_csu_init()
在程序编译的过程中,会自动加入一些通用函数的初始化工作,这些初始化函数都是相同的,因此可以考虑在这些函数中找到一些通用的gadget
需要关注的寄存器
r12 保存着将要调用函数的指针的地址
rcx r8 r9 第四五六个参数
rdx 第三个参数
rsi 第二个参数
rdi 第一个参数
rbx 和 rbp 的值分别为0 1
5.Canary
linux中的cookies,用来检测溢出并阻止溢出操作
其原理是当程序启用canary编译后,在函数序言部分会取fs寄存器0x20处的值.存放在栈中
1
2mov rax, qword ptr fs:[0x28]
mov qword ptr [rbp-8], rax在函数返回之前,会将该值取出,并与fs:0x28的值进行亦或.这个操作既检测是否发生栈溢出
如果canary已经被非法修改,此时程序流程会走到__stack_chk_fail

通过格式化字符串绕过canary
- 由于格式化字符串漏洞会导致任意地址泄漏,因此,只要得到输入参数在栈中的位置,就可以通过偏移得到canary在栈中的位置
- 然后在栈溢出的padding块把canary所在的位置的值用正确的canary替换,从而绕过canary检测





