本文作者:Tangerine@SAINTSEC

原文来自:https://bbs.ichunqiu.com/thread-42943-1-1.html

0×00 printf函数中的漏洞printf函数族是一个在C编程中比较常用的函数族。通常来说,我们会使用printf([格式化字符串],参数)的形式来进行调用,例如

然而,有时候为了省事也会写成

事实上,这是一种非常危险的写法。由于printf函数族的设计缺陷,当其第一个参数可被控制时,攻击者将有机会对任意内存地址进行读写操作。

0×01 利用格式化字符串漏洞实现任意地址读首先我们来看一个自己写的简单例子~/format_x86/format_x86

这是一个代码很简单的程序,为了留后门,我调用system函数写了一个showVersion().剩下的就是一个无线循环的读写,并使用有问题的方式调用了printf().正常来说,我们输入什么都会被原样输出

但是当我们输入一些特定的字符时输出出现了变化。

可以看到,当我们输入printf可识别的格式化字符串时,printf会将其作为格式化字符串进行解析并输出。原理很简单,形如printf(“%s”,“Hello world”)的使用形式会把第一个参数%s作为格式化字符串参数进行解析,在这里由于我们直接用printf输出一个变量,当变量也正好是格式化字符串时,自然就会被printf解析。那么后面输出的内容又是什么呢?我们继续做实验。

我们直接在call _printf一行下断点然后以调试方式启动程序,然后输入一大串%x.,输出结果如图

此时的栈情况如图

我们很容易发现输出的内容正好是esp-4开始往下的一连串数据。所以理论上我们可以通过叠加%x来获取有限范围内的栈数据。那么我们有可能泄露其他数据吗?我们知道格式化字符串里有%s,用于输出字符。其本质上是读取对应的参数,并作为指针解析,获取到对应地址的字符串输出。我们先输入一个%s观察结果。

我们看到输出了%s后还接了一个换行,对应的栈和数据如下:

栈顶是第一个参数,也就是我们输入的%s, 第二个参数的地址和第一个参数一样,作为地址解析指向的还是%s和回车0x0A。由于此时我们可以通过输入来操控栈,我们可以输入一个地址,再让%s正好对应到这个地址,从而输出地址指向的字符串,实现任意地址读。

通过刚刚的调试我们可以发现,我们的输入从第六个参数开始(上图从栈顶往下数第六个‘000A7325’ = %s\n\x00)。所以我们可以构造字符串”\x01\x80\x04\x08%x.%x.%x.%x.%s“。这里前面的地址是ELF文件加载的地址08048000+1,为什么不是08048000后面再说,有兴趣的可以自己试验一下。

由于字符串里包括了不可写字符,我们没办法直接输入,这回我们用pwntools+IDA附加的方式进行调试。

我们成功地泄露出了地址0×08048001内的内容。

经过刚刚的试验,我们用来泄露指定地址的payload对读者来说应该还是能够理解的。由于我们的输入本体恰好在printf读取参数的第六个参数的位置,所以我们把地址布置在开头,使其被printf当做第六个参数。接下来是格式化字符串,使用%x处理掉第二到第五个参数(我们的输入所在地址是第一个参数),使用%s将第六个参数作为地址解析。但是如果输入长度有限制,而且我们的输入位于printf的第几十个参数之外要怎么办呢?叠加%x显然不现实。因此我们需要用到格式化字符串的另一个特性。

格式化字符串可以使用一种特殊的表示形式来指定处理第n个参数,如输出第五个参数可以写为%4$s,第六个为%5$s,需要输出第n个参数就是%(n-1)$[格式化控制符]。因此我们的payload可以简化为”\x01\x80\x04\x08%5$s”

0×02 使用格式化字符串漏洞任意写虽然我们可以利用格式化字符串漏洞达到任意地址读,但是我们并不能直接通过读取来利用漏洞getshell,我们需要任意地址写。因此我们在本节要介绍格式化字符串的另一个特性——使用printf进行写入。

printf有一个特殊的格式化控制符%n,和其他控制输出格式和内容的格式化字符不同的是,这个格式化字符会将已输出的字符数写入到对应参数的内存中。我们将payload改成“\x8c\x97\x04\x08%5$n”,其中0804978c是.bss段的首地址,一个可写地址。执行前该地址中的内容是0

printf执行完之后该地址中的内容变成了4,查看输出发现输出了四个字符“\x8c\x97\x04\x08”,回车没有被计算在内。

我们再次修改payload为“\x8c\x97\x04\x08%2048c%5$n”,成功把0804978c里的内容修改成0×804

现在我们已经验证了任意地址读写,接下来可以构造exp拿shell了。

由于我们可以任意地址写,且程序里有system函数,因此我们在这里可以直接选择劫持一个函数的got表项为system的plt表项,从而执行system(“/bin/sh”)。劫持哪一项呢?我们发现在got表中只有四个函数,且printf函数可以单参数调用,参数又正好是我们输入的。因此我们可以劫持printf为system,然后再次通过read读取”/bin/sh”,此时printf(“/bin/sh”)将会变成system(“/bin/sh”)。根据之前的任意地址写实验,我们很容易构造payload如下:

printf_got = 0×08049778

system_plt = 0×08048320

payload = p32(printf_got)+”%”+str(system_plt-4)+”c%5$n”           #p32(printf_got)占了4字节,所以system_plt要减去4

将payload发送过去,可以发现此时got表中的printf项已经被劫持

此时再次发送”/bin/sh”就可以拿shell了。

但是这里还有一个问题,如果读者真的自己调试了一遍就会发现单步执行时call_printf一行执行时间额外的久,且最后io.interactive()时屏幕上的光标会不停闪烁很长一段时间,输出大量的空字符。使用io.recvall()读取这些字符发现数据量高达128.28MB。这是因为我们的payload中会输出多达134513436个字符

由于我们所有的试验都是在本机/虚拟机和docker之间进行,所以不会受到网络环境的影响。而在实际的比赛和漏洞利用环境中,一次性传输如此大量的数据可能会导致网络卡顿甚至中断连接。因此,我们必须换一种写exp的方法。

我们知道,在64位下有%lld,%llx等方式来表示四字(qword)长度的数据,而对称地,我们也可以使用%hd, %hhx这样的方式来表示字(word)和字节(byte)长度的数据,对应到%n上就是%hn,%hhn。为了防止修改的地址有误导致程序崩溃,我们仍然需要一次性把got表中的printf项改掉,因此使用%hhn时我们就必须一次修改四个字节。那么我们就得重新构造一下payload

首先我们给payload加上四个要修改的字节

然后我们来修改第一位。由于x86和x86-64都是小端序,printf_got对应的应该是地址后两位0×20

这时候我们已经修改了0×08049778处的数据为0×20,接下来我们需要修改0×08049778+2处的数据为0×83。由于我们已经输出了0×20个字节(16个字节的地址+0×20-16个%c),因此我们还需要输出0×83-0×20个字节

继续修改0×08049778+4,需要修改为0×04,然而我们前面已经输出了0×83个字节,因此我们需要输出到0×04+0×100=0×104字节,截断后变成0×04

修改0×08049778+6

最后的payload为[C] 纯文本查看 复制代码

‘\x78\x97\x04\x08\x79\x97\x04\x08\x7a\x97\x04\x08\x7b\x97\x04\x08%16c%5$hhn%99c%6$hhn%129c%7$hhn%4c%8$hhn’

当然,对于格式化字符串payload,pwntools也提供了一个可以直接使用的类Fmtstr,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html,我们较常使用的功能是

。第一个参数offset是第一个可控的栈偏移(不包含格式化字符串参数),代入我们的例子就是第六个参数,所以是5。第二个字典看名字就可以理解,numbwritten是指printf在格式化字符串之前输出的数据,比如printf(“Hello [var]“),此时在可控变量之前已经输出了”Hello”共计六个字符,应该设置参数值为6。第四个选择用 %hhn(byte), %hn(word)还是%n(dword).在我们的例子里就可以写成

获取本例子shell的脚本见于附件,此处不再赘述。

0×03 64位下的格式化字符串漏洞利用学习完32位下的格式化字符串漏洞利用,我们继续来看现在已经变成主流的64位程序。我们打开例子~/format_x86-64/format_x86-64。

事实上,这个程序和上一节中使用的例子是同一个代码文件,只不过编译成了64位的形式。和上一个例子一样,我们首先看一下可控制的栈地址偏移。

根据上个例子,我们的输入位于栈顶,所以是第一个参数,偏移应该是0.但是问题来了,栈顶不应该是字符串地址吗?别忘了64位的传参顺序是rdi, rsi, rdx, rcx, r8, r9,接下来才是栈,所以这里的偏移应该是6.我们可以用一串%llx.来证明这一点。

有了偏移,got表中的printf和plt表中的system也可以直接从程序中获取,我们就可以使用fmtstr_payload来生成payload了。

然而我们会发现这个payload无法修改got表中的printf项为plt的system

然而查看内存,发现payload并没有问题

那么问题出在哪呢?我们看一下printf的输出

可以看到我们第一次输入的payload只剩下空格(\x20),\x10和`(\x60)三个字符。这是为什么呢?

我们回头看看payload,很容易发现紧接在\x20\x10\x60三个字符后面的是\x00,而\x00正是字符串结束符号,这就是为什么我们在上一节中选择0×08048001而不是0×08048000测试读取。由于64位下用户可见的内存地址高位都带有\x00(64位地址共16个16进制数),所以使用之前构造payload的方法显然不可行,因此我们需要调整一下payload,把地址放到payload的最后。

由于地址中带有\x00,所以这回就不能用%hhn分段写了,因此我们的payload构造如下

这个payload看起来好像没什么问题,不过如果拿去测试,你就会发现用io.recvall()读完输出后程序马上就会崩溃。

这是为什么呢?如果你仔细看右下角的栈,你就会发现构造好的地址错位了。

因此我们还需要调整一下payload,使地址前面的数据恰好为地址长度的倍数。当然,地址所在offset也得调整。调整后的结果如下:

这回就可以了。

0×04 使用格式化字符串漏洞使程序无限循环从上面的两个例子我们可以发现,之所以能成功利用格式化字符串漏洞getshell,很多时候都是因为程序中存在循环。如果程序中不存在循环呢?之前我们试过使用ROP技术劫持函数返回地址到start,这回我们将使用格式化字符串漏洞做到这一点。

我们打开例子~/MMA CTF 2nd 2016-greeting/greeting

同样的,这个32位程序的got表中有system(看左边),而且存在一个格式化字符串漏洞。计算偏移值和详细构造payload的步骤此处不再赘述。这个程序主要的问题在于我们需要用printf来触发漏洞,然而我们从代码中可以看到printf执行完之后就不会再调用其他got表中的函数,这就意味着即使成功触发漏洞劫持got表也无法执行system。这时候就需要我们想办法让程序可以再次循环。

之前的文章中我们就提到过,虽然写代码的时候我们以main函数作为程序入口,但是编译成程序的时候入口并不是main函数,而是start代码段。事实上,start代码段还会调用__libc_start_main来做一些初始化工作,最后调用main函数并在main函数结束后做一些处理。其流程见于链接http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html

大致如下图

简单地说,在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。

而我们的目标就是修改.fini_array数组的第一个元素为start。需要注意的是,这个数组的内容在再次从start开始执行后又会被修改,且程序可读取的字节数有限,因此需要一次性修改两个地址并且合理调整payload。可用的脚本同样见于附件。

0×05 一些和格式化字符串漏洞相关的漏洞缓解机制       在checksec脚本的检查项中,我们之前提到过了NX的作用,本节我们介绍一下另外两个和Linux pwn中格式化字符串漏洞常用的利用手段相关的缓解机制RELRO和FORTIFY

首先我们介绍一下RELRO,RELRO是重定位表只读(Relocation Read Only)的缩写。重定位表即我们经常提到的ELF文件中的got表和plt表。关于这两个表的来源和作用,我们会在介绍ret2dl-resolve的文章中详细介绍。现在我们首先需要知道的是这两个表,正如其名,是为程序外部的函数和变量(不在程序里定义和实现的函数和变量,比如read。显然你在自己的代码里调用read函数的时候不用自己写一个read函数的实现)的重定位做准备的。由于重定位需要额外的性能开销,出于优化考虑,一般来说程序会使用延迟加载,即外部函数的内存地址是在第一次被调用时(例如read函数,第一次调用即为程序第一次执行call read)被找到并且填进got表里面的。因此,got表必须是可写的。但是got表可写也给格式化字符串漏洞带来了一个非常方便的利用方式,即修改got表。正如前面的文章所述,我们可以通过漏洞修改某个函数的got表项(比如puts)为system函数的地址,这样一来,我们执行call puts实际上调用的却是system,相应的,传入的参数也给了system,从而可以执行system(“/bin/sh”)。可以这么操作的程序使用checksec检查的结果如下图

其RELRO项为PartialRELRO.

而开头的图中显示的RELRO: Full RELRO意即该程序的重定位表项全部只读,无论是.got还是.got.plt都无法修改。我们找到这个程序(在《stack canary与绕过的思路》的练习题中),在call read上下断点,修改第一个参数buf为got表的地址以尝试修改got表,程序不会报错,但是数据未被修改,read函数返回了一个-1

显然,当程序开启了Full RELRO保护之后,包括格式化字符串漏洞在内,试图通过漏洞劫持got表的行为都将会被阻止。

接下来我们介绍另一个比较少见的保护措施FORTIFY,这是一个由GCC实现的源码级别的保护机制,其功能是在编译的时候检查源码以避免潜在的缓冲区溢出等错误。简单地说,加了这个保护之后(编译时加上参数-D_FORTIFY_SOURCE=2)一些敏感函数如read, fgets,memcpy, printf等等可能导致漏洞出现的函数都会被替换成__read_chk,__fgets_chk, __memcpy_chk, __printf_chk等。这些带了chk的函数会检查读取/复制的字节长度是否超过缓冲区长度,通过检查诸如%n之类的字符串位置是否位于可能被用户修改的可写地址,避免了格式化字符串跳过某些参数(如直接%7$x)等方式来避免漏洞出现。开启了FORTIFY保护的程序会被checksec检出,此外,在反汇编时直接查看got表也会发现chk函数的存在

大家可以点击链接下载课后习题哦 >>>> https://bbs.ichunqiu.com/thread-42943-1-1.html

转载于:https://www.cnblogs.com/ichunqiu/p/9329387.html

Linux pwn入门教程——格式化字符串漏洞相关推荐

  1. c# 定位内存快速增长_CTF丨Linux Pwn入门教程:针对函数重定位流程的相关测试(下)...

    Linux Pwn入门教程系列分享已到尾声,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  2. Linux pwn入门教程——CTF比赛

    Linux pwn入门教程(1)--栈溢出基础 from:https://zhuanlan.zhihu.com/p/38985585 0x00 函数的进入与返回 要想理解栈溢出,首先必须理解在汇编层面 ...

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

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

  4. Linux pwn入门教程,i春秋linux_pwn入门教程复现之栈溢出基础

    i春秋linux_pwn入门教程复现之栈溢出基础 演示进程总览 1: main函数 2: hello函数 3: getShell函数 函数的入栈和出栈 1: F2断点于call hello 启动IDA ...

  5. Linux pwn入门教程,pwn堆入门系列教程1

    pwn堆入门系列教程1 因为自己学堆的时候,找不到一个系统的教程,我将会按照ctf-wiki的目录一步步学下去,尽量做到每周有更新,方便跟我一样刚入门堆的人学习,第一篇教程研究了4天吧,途中没人指导. ...

  6. Linux pwn入门教程,Linux Pwn入门 - 安全牛课堂 - 领先的信息安全在线教育平台

    { "i18nChapterName": "章", "i18nUnitName": "节", "i18nLes ...

  7. Linux下的格式化字符串漏洞利用姿势

    [转]http://www.cnblogs.com/Ox9A82/p/5429099.html linux最早的漏洞防护机制nx-stack刚刚出现后就有人想出了突破方法.那就是只有栈是不可执行,而除 ...

  8. linux获取字符格式化,Linux 格式化字符串漏洞利用

    目的是接觸一些常見的漏洞,增加自己的視野.格式化字符串危害最大的就兩點,一點是leak memory,一點就是可以在內存中寫入數據,簡單來說就是格式化字符串可以進行內存地址的讀寫.下面結合着自己的學習 ...

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

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

最新文章

  1. 从摩尔定律到人工智能,指数定律释放人类潜能
  2. 归并排序--数组和链表的实现
  3. 【12图】你管这破玩意叫Pulsar
  4. 【渝粤题库】陕西师范大学300008 历史教学论
  5. andriod studio 查看项目依赖_如何实施CRM项目管理
  6. 12999元!小米MIX FOLD致敬未来尊享礼盒上线:限量100套 想买先抽签
  7. 加拿大28历史开奖鸿蒙,本内特入选加拿大男篮集训名单,史上最水状元秀如今在何处?...
  8. 机器学习实战之Logistic回归
  9. Android项目实战之高仿网易云音乐创建项目和配置
  10. linux由哪些部分组成,linux内核处于什么位置?,Linux由几部分组成?Linux系统结构介绍!...
  11. 汽车行业DBC文件解析 | Python 解析dbc文件
  12. 开启win10电池方案卓越性能
  13. pycharm主题背景图片设置
  14. android 打开pdf文件
  15. 计算机专业英语问卷调查,关于英语调查问卷的总结
  16. 相对url和相对路径
  17. P3855 [TJOI2008]Binary Land
  18. spring-retry使用以及源码
  19. 京东关于区块链的发展历程
  20. html button 点击无效,HTML button点击不了

热门文章

  1. uCOS3源码解析视频教程-第4季第7部分-朱有鹏-专题视频课程
  2. 文本检测CTPN训练自己的数据集
  3. 2021年氯化工艺考试及氯化工艺考试内容
  4. matlab使用CVX求解优化问题时,如果变量搜索空间过大,导致求解的数值解相当不准确,通过变量替换,缩小搜索空间
  5. 笨鸟之Serlvet解析
  6. 【汇编语言】习题-转移地址在内存中的jmp指令
  7. 江西省高考英语计算机智能口试,高考英语口试江西试题
  8. warning: this decimal constant is unsigned only in ISO C90问题的处理及理解
  9. Facebook联合创始人Chris Hughes呼吁该公司进行分拆
  10. 二手闲置物品交易数据快照