Linux pwn入门教程(6)——格式化字符串漏洞

    xiaoxiao2022-07-13  183

    https://bbs.ichunqiu.com/thread-43624-1-1.html

    0x00 printf函数中的漏洞

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

    char s[20] = “Hello world!\n”; printf(“%s”, s);

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

    char s[20] = “Hello world!\n”; printf(s);

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

    0x01 利用格式化字符串漏洞实现任意地址读

    首先我们来看一个自己写的简单例子~/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附加的方式进行调试。 我们成功地泄露出了地址0x08048001内的内容。 经过刚刚的试验,我们用来泄露指定地址的payload对读者来说应该还是能够理解的。由于我们的输入本体恰好在printf读取参数的第六个参数的位置,所以我们把地址布置在开头,使其被printf当做第六个参数。接下来是格式化字符串,使用%x处理掉第二到第五个参数(我们的输入所在地址是第一个参数),使用%s将第六个参数作为地址解析。但是如果输入长度有限制,而且我们的输入位于printf的第几十个参数之外要怎么办呢?叠加%x显然不现实。因此我们需要用到格式化字符串的另一个特性。 格式化字符串可以使用一种特殊的表示形式来指定处理第n个参数,如输出第五个参数可以写为%4$s,第六个为%5$s,需要输出第n个参数就是%(n-1)$[格式化控制符]。因此我们的payload可以简化为”\x01\x80\x04\x08%5$s”

    0x02 使用格式化字符串漏洞任意写

    虽然我们可以利用格式化字符串漏洞达到任意地址读,但是我们并不能直接通过读取来利用漏洞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 48c%5$n”,成功把0804978c里的内容修改成0x804 现在我们已经验证了任意地址读写,接下来可以构造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”

    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加上四个要修改的字节

    printf_got = 0x08049778 system_plt = 0x08048320 payload = p32(printf_got) payload += p32(printf_got+1) payload += p32(printf_got+2) payload += p32(printf_got+3)

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

    payload += “%” payload += str(0x20-16) payload += “c%5$hhn”

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

    payload += “%” payload += str(0x83-0x20) payload += “c%6$hhn”

    继续修改0x08049778+4,需要修改为0x04,然而我们前面已经输出了0x83个字节,因此我们需要输出到0x04+0x100=0x104字节,截断后变成0x04

    payload += “%” payload += str(0x104-0x83) payload += “c%7$hhn”

    修改0x08049778+6

    payload += “%” payload += str(0x08-0x04) payload += “c%8$hhn”

    最后的payload为'\x78\x97\x04\x08\x79\x97\x04\x08\x7a\x97\x04\x08\x7b\x97\x04\x08c%5$hhn

    转载请注明原文地址: https://yun.8miu.com/read-56136.html
    最新回复(0)