ELF文件利用延迟绑定技术来解决动态程序模块与函数的重定位问题。ret2dl_resolve的原理是基于延迟绑定技术而形成的利用技巧,它通过伪造数据结构以及对函数延迟绑定过程的拦截,实现任意函数调用的目的。 本篇文章主要描述延迟绑定的原理、32位以及64位ELF文件的ret2dl_resolve技术。 延迟绑定技术针对动态链接会减速程序运行速度的现状,操作系统实现了延迟绑定(Lazy Binding)的技术:函数第一次被用到时才对函数进行绑定。通过延迟绑定大大加快了程序的启动速度。而ELF 则使用了PLT(Procedure Linkage Table,过程链接表)的技术来实现延迟绑定。 下面根据一个程序具体来跟下程序看延迟绑定的实现,demo程序如下: #include <stdio.h>
// gcc -m32 -g demo.c -o demo
int main()
{
char data[20];
read(0,data,20);
return 0;
}
断点下在read函数调用的地方: 0x8048492 <main+39> call read@plt <0x8048330>
程序先调用read@plt,查看此时read的plt表的内容: pwndbg> x/3i 0x8048330
0x8048330 <read@plt>: jmp DWORD PTR ds:0x804a00c
0x8048336 <read@plt+6>: push 0x0
0x804833b <read@plt+11>: jmp 0x8048320
可以看到它直接跳转到了read的got表中的地址,此时read的got表中的刚好是read@plt下一条地址的值0x08048336: pwndbg> x/wx 0x804a00c
0x804a00c: 0x08048336
如上面代码所示,0x08048336地址处的两条指令,将0压入栈中,并跳转到0x8048320地址执行: 0x8048336 <read@plt+6>: push 0x0
0x804833b <read@plt+11>: jmp 0x8048320
0x8048320为plt表的起始地址,称其为plt0,其指令为: 0x8048320 push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>
0x8048326 jmp dword ptr [0x804a008] <0xf7feed90>
可以看到它将got+4的值压入到栈中,并跳转到了got+8的地方去执行。跟进去到got+8中的值0xf7feed90,可以看到程序进入到了_dl_runtime_resolve,因此got+8的地方存储的是_dl_runtime_resolve函数的地址。看_dl_runtime_resolve的源码,源码在/sysdeps/i386/dl-trampoline.S中: 0xf7feed90 <_dl_runtime_resolve> push eax
0xf7feed91 <_dl_runtime_resolve+1> push ecx
0xf7feed92 <_dl_runtime_resolve+2> push edx
0xf7feed93 <_dl_runtime_resolve+3> mov edx, dword ptr [esp + 0x10]
0xf7feed97 <_dl_runtime_resolve+7> mov eax, dword ptr [esp + 0xc]
0xf7feed9b <_dl_runtime_resolve+11> call _dl_fixup <0xf7fe85a0>
_dl_runtime_resolve在源码中是用汇编实现的只是压栈并调用_dl_fixup,在跟进去_dl_fixup前,先给出一些与动态链接相关的数据结构。 根据《程序员的自我修养》中的描述:动态链接中最重要的结构应该是dynamic段,这个段里面保存了动态链接器所需要的基本信息。比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。 使用readelf查看该demo文件中dynamic的信息如下:
ret2dl_resolve解析
其是一个结构体数组,结构体的定义为:
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];
Elf32_Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。下面给出和延迟绑定相关的类型值的定义。 d_tag类型d_un的定义
#define DT_STRTAB 5动态链接字符串表的地址,d_ptr表示.dynstr的地址 (Address of string table)
#define DT_SYMTAB 6动态链接符号表的地址,d_ptr表示.dynsym的地址 (Address of symbol table)
#define DT_JMPREL 23动态链接重定位表的地址,d_ptr表示.rel.plt的地址 (Address of PLT relocs)
#define DT_RELENT 19单个重定位表项的大小,d_val表示单个重定位表项大小 (Size of one Rel reloc )
#define DT_SYMENT 11单个符号表项的大小,d_val表示单个符号表项大小 (Size of one symbol table entry )如上图所示,可以看到字符串表.dynstr的地址为0x804822c,符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8 .rel.plt重定位表中包含了需要重定位的函数的信息,其也是一个结构体数组,结构体Elf32_Rel定义如下,其中r_offset表示got表地址,即动态解析函数后真正的函数地址需要填入的地方,r_info由两部分构成,r_info>>8表示该函数对应在符号表.dynsym中的下标,r_info&0xff则表示重定位类型。: typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
我们查看demo程序的重定位表0x80482d8: pwndbg> x/6wx 0x80482d8
0x80482d8: 0x0804a00c 0x00000107 0x0804a010 0x00000207
0x80482e8: 0x0804a014 0x00000407
以及使用readelf -r来查看demo程序的重定位表: $ readelf -r demo
Relocation section '.rel.plt' at offset 0x2d8 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 [email]read@GLIBC_2.0[/email]
0804a010 00000207 R_386_JUMP_SLOT 00000000 [email]__stack_chk_fail@GLIBC_2.4[/email]
0804a014 00000407 R_386_JUMP_SLOT 00000000 [email]__libc_start_main@GLIBC_2.0[/email]
可以看到重定位表.rel.plt为一个Elf32_Rel数组,demo程序中该数组包含三个元素,第一个是read的重定位表项Elf32_Rel结构体,第二个是__stack_chk_fail,第三个是__libc_start_main。read的重定位表r_offset为0x0804a00c,为read的got地址,即在动态解析函数完成后,将read的函数地址填入到r_offset为0x0804a00c中。r_info为0x00000107表示read函数的符号表为.dynsym数组中的0x00000107>>8(即0x1)个元素,它的类型为0x00000107&0xff(即0x7)对应为R_386_JUMP_SLOT类型。 接着我们去看符号表.dynsym节,它也是一个结构体Elf32_Sym数组,其结构体的定义如下: typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0
其中st_name指向的是函数名称在.dynstr表中的偏移。在dynamic段中我们知道了符号表.dynsym地址为0x80481cc,查看它的值: pwndbg> x/20wx 0x80481cc
0x80481cc: 0x00000000 0x00000000 0x00000000 0x00000000
0x80481dc: 0x0000002b 0x00000000 0x00000000 0x00000012
0x80481ec: 0x0000001a 0x00000000 0x00000000 0x00000012
0x80481fc: 0x00000042 0x00000000 0x00000000 0x00000020
0x804820c: 0x00000030 0x00000000 0x00000000 0x00000012
以及使用readelf -s查看符号表的内容: $ readelf -s demo
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND [email]read@GLIBC_2.0[/email] (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND [email]__stack_chk_fail@GLIBC_2.4[/email] (3)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND [email]__libc_start_main@GLIBC_2.0[/email] (2)
5: 0804853c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
从重定位表.rel.plt中,我们知道了read的r_info>>8为0x1,即read的符号表项对应的是.dynsym第二个元素,果然可以看到.dynsym第一个元素为read函数的Elf32_Sym结构体,可以看到它的st_name对应的是0x0000002b,即read字符串应该在.dynstr表偏移为0x2b的地方,由dynamic我们知道了.dynstr表的地址为地址为0x804822c,去验证下看其偏移0x2b是否为read字符串: pwndbg> x/s 0x804822c+0x2b
0x8048257: "read"
可以看到,确实如此。 到这里似乎对read函数的解析过程有了一个简单的了解: ·可以先通过dynamic段获取各个表的地址,包括有字符串表.dynstr的地址为0x804822c,符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8。·read函数为.rel.plt表中的第一个元素,定位它的重定位表项,知道了read函数的r_offset为0x0804a00c,以及它在符号表中的下标为0x000001,它的类型为0x7,R_386_JUMP_SLOT。·由0x000001知道了read函数的符号表是.dynsym第二个元素,获取到该结构体,得到了它对应的st_name对应的是0x0000002b,即获取了read字符串应该在.dynstr表偏移为0x2b的地方。·最后调用函数解析匹配read字符串所对应的函数地址,将其填至r_offset为0x0804a00c,即read的got地址中。 有了前面的基础,现在可以跟进去_dl_fixup,我们知道在调用_dl_runtime_resolve函数之前压入到栈中的参数是0,以及got+4中的值,参考下面_dl_fixup的源码,根据参数列表,知道了眼乳栈中的0为reloc_arg,got+4中的值为struct link_map *l,函数源码在/elf/dl-runtime.c中: _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
//获取符号表地址
const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取字符串表地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//获取函数对应的重定位表结构地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的符号表结构地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//得到函数对应的got地址,即真实函数地址要填回的地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
DL_FIXUP_VALUE_TYPE value;
//判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* 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;
}
...
// 接着通过strtab+sym->st_name找到符号表字符串
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
...
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}
...
// 最后把value写入相应的GOT表条目rel_addr中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
可以看到_dl_fixup函数就如上面描述的过程一般,去定位结构体,最终获取read字符串去libc中找到相应的函数地址并填回到got表中。 所以ELF文件的延迟绑定技术总结如下: 调用函数时,call func@plt,plt表内容如下: func@plt:
jmp *(func@GOT) //仍然首先进行GOT跳转,尝试是否是第一次链接
push n //压入需要地址绑定的符号在重定位表中的下标
jmp PLT0 //跳转到 PLT0
由于是第一次调用,func@GOTgot表中的值为jmp *(func@GOT)指令的下一条地址,即push n的地址,接着程序会执行push n; jmp PLT0,n则为该函数在.rel.plt表中的偏移。接着去看PLT0的指令为: push *(got+4)
jmp *(got+8)
其中got+4存储的是link_map的地址,got+8存储的是_dl_runtime_resolve函数的地址。进入到_dl_runtime_resolve函数后,函数会调用_dl_fixup函数,根据源码分析,可以看到该函数功能为: 1.程序先从第一个参数link_map获取字符串表.dynstr、符号表.dynsym以及重定位表.rel.plt的地址,2.通过第二个参数n即.rel.plt表中的偏移reloc_arg加上.rel.plt的地址获取函数对应的重定位结构的位置,从而获取函数对应的r_offset以及在符号表中的下标r_info>>8。3.根据符号表地址以及下标获取符号结构体,获得了函数符号表中的st_name,即函数名相对于字符串表.dynstr的偏移。4.最后可得到函数名的字符串,然后去libc中匹配函数名,找到相应的函数并将地址填回到r_offset即函数got表中,延迟绑定完成。 32位的ret2dl_resolve原理ret2dl_resolve的适用场景是在无法泄露程序地址时,通过拦截延迟绑定的过程,实现对函数地址解析过程的劫持,使得最终解析出来的函数为特定函数的函数地址,从而实现无泄露达到特定函数调用的目的。 32位ELF程序ret2dl_resolve攻击方法,目前最为普遍的是伪造reloc_arg,即伪造重定位表的下标实现相关的利用,具体包括如下步骤: 1.伪造reloc_arg,使得reloc_arg加上.rel.plt的地址指向可控的地址,在该地址可伪造恶意的Elf32_Rel结构体。2.伪造Elf32_Rel结构体中的r_offset指向某一可写地址,最终函数地址会写入该地址处;伪造r_info&0xff为0x7,因为类型需为ELF_MACHINE_JMP_SLOT以绕过类型验证;伪造r_info>>8,使得r_info>>8加上.dynsym地址指向可控的地址,并在该地址伪造符号表结构体Elf32_Sym。3.伪造Elf32_Sym结构体中的st_name,使得.dynstr的地址加上该值指向可控地址,并在该地址处写入特定函数的函数名入system。4.最终系统通过函数名匹配,定位到特定函数地址,获取该地址并写入到伪造的r_offset中,实现了函数地址的获取。 整个过程看起来比较简单,仍然需要注意一点的是dl_fixup中还存在以下一段代码: 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;
}
如果reloc->r_info伪造不当,会使得ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff偏大,导致version = &l->l_versions[ndx]出现访存错误,因此伪造的reloc->r_info,最好使得ndx为0,即vernum[reloc->r_info]为0。 实践--0ctf2018-babystack这题只是一个简单的栈溢出,但是由于它是被python将程序运行起来,只是将读入的0x100字节数据给程序并没有将任何数据输出,无法进行泄露,因此想到了使用ret2dl-resolve来解这道题。 在bss段上选择一个地址伪造Elf32_Sym,使得由它得到的ndx = vernum[ELFW(R_SYM) (reloc->r_info)]为0。并在一个地址填入特定目标函数名称如system字符串,将其相对于.dynstr的地址的偏移填入到伪造的Elf32_Sym结构体中的st_name中。再伪造Elf32_Rel结构体,将r_offset伪造成想要写目标函数的地址,将r_info>>8构造成伪造的Elf32_Sym相对于.dynsym数组的偏移,将r_info&0xff伪造成0x7。最后计算出伪造的Elf32_Rel结构体相对于.rel.plt的偏移,将该偏移压入栈中,最终调用plt0实现函数地址解析。 由于该程序无法输出,因此需要一个远程的vps来反弹shell或接收flag。 目前roputils[1]可以较好的支持构造ret2dl_resolve数据,但是它好像对于ndx这个没有考虑进去,参考roputils,pwn_debug[2]加入了对32位ret2dl_resolve数据的构造,提供ret2dl_resolve模块,其apibuild_normal_resolve会返回一个构造好的能够实现ret2dl_resolve攻击的数据,且其对应的ndx为0。 64位ELF程序的ret2dl_resolve前面描述的是32位程序的ret2dl_resolve,64位程序是否一样可行?对于64位程序而言,理论上而言上述方法也是可行的,但是在实际构造的过程中,有一点是会是程序崩溃的,还是前面提到的那段代码: 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;
}
因为64位程序构造的数据一般都是在bss段,如0x601000-0x602000,导致其相对于.dynsym的地址0x400000-0x401000很大,使得reloc->r_info也很大,最后使得访问ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;时程序访存出错,导致程序崩溃。 根据代码可以使得l->l_info[VERSYMIDX (DT_VERSYM)] != NULL这句话不成立来绕过该段代码,即使得l->l_info[VERSYMIDX (DT_VERSYM)]等于NULL,即使得(link_map + 0x1c8) 处为 NULL。这就使问题变成了往link_map写空值,由于link_map在ld.so中,还需要泄露地址。因此实现64位的上述方法的ret2dl_resolve,需要泄露与地址写两个漏洞,如果有这两个漏洞我们应该可以使用更轻松的方法来get shell,因此价值不大。 那么是否还有其他方法?可以看到该段代码还有一个条件if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0),我们是否可构造sym->st_other使它不为空,从而绕过该段代码,我们看假设sym->st_other使它不为空,dl_fixup的代码流程: _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
//获取符号表地址
const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取字符串表地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//获取函数对应的重定位表结构地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的符号表结构地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//得到函数对应的got地址,即真实函数地址要填回的地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
DL_FIXUP_VALUE_TYPE value;
//判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* 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)
{
...
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}
...
// 最后把value写入相应的GOT表条目rel_addr中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
可以看到当sym->st_other不为0时,会调用DL_FIXUP_MAKE_VALUE,根据代码的注释,该代码认为这个符号已经解析过,直接调用DL_FIXUP_MAKE_VALUE函数赋值。DL_FIXUP_MAKE_VALUE函数的定义如下,直接将l->l_addr + sym->st_value赋值给value: #define DL_FIXUP_MAKE_VALUE(map, addr) (addr)
/* Extract the code address from a value of type DL_FIXUP_MAKE_VALUE.
*/
也可以看到sym等都是从link_map中取出来的,如果我们将控制的目标不设定为reloc_arg,而是伪造第一个参数link_map。如果我们可以控制sym->st_value指向got表中的地址如__libc_start_main的got,而l->l_addr为目标地址如system到__libc_start_main的偏移,则最终得到的value会是l->l_addr + sym->st_value即system地址,从而实现无需leak地址的利用。 重新整理下利用思路,在利用中我们控制的不再是reloc_arg,而是struct link_map *l,假设我们可以覆盖got+4,即link_map的值,指向我们可控的目标。 首先看link_map的定义: pwndbg> print sizeof(*l)
$2 = 0x470
pwndbg> ptype l
type = struct link_map {
Elf64_Addr l_addr;
char *l_name;
Elf64_Dyn *l_ld;
struct link_map *l_next;
struct link_map *l_prev;
struct link_map *l_real;
Lmid_t l_ns;
struct libname_list *l_libname;
Elf64_Dyn *l_info[76]; //l_info 里面包含的就是动态链接的各个表的信息
...
size_t l_tls_firstbyte_offset;
ptrdiff_t l_tls_offset;
size_t l_tls_modid;
size_t l_tls_dtor_count;
Elf64_Addr l_relro_addr;
size_t l_relro_size;
unsigned long long l_serial;
struct auditstate l_audit[];
} *
我们最终的目标是伪造sym,使得sym->st_value的地址刚好指向got表中存在libc地址的值,如__libc_start_main,而l->l_addr存储的则是目标函数地址(system)与__libc_start_main的偏移,因此首先伪造l->l_addr为目标地址system与__libc_start_main的偏移。 接下来看sym->st_value如何构造才能指向__libc_start_main的got表的位置。我们看sym是如何得到的: //获取符号表地址
const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取函数对应的重定位表结构地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的符号表结构地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
最终sym是通过&symtab[ELFW(R_SYM) (reloc->r_info)]得到的,我们能够控制link_map,为简单起见,我们可以控制reloc->r_info为0,因此现在问题就变成了如何控制symtab第一个元素的st_value指向__libc_start_main的got表的位置。symtab是l_info[DT_SYMTAB],根据定义#define DT_SYMTAB 6,symtab即是l_info[0x6],根据Elf64_Dyn以及Elf64_Sym的定义,我们可以构造l_info[0x6]指向link_map+0x70,同时在link_map+0x78的位置填入__libc_start_maingot表地址减8的地址,这样就伪造了DT_SYMTAB对应的Elf64_Dyn为link_map+0x70,它的d_ptr为link_map+0x78,指向了__libc_start_main_got-8,即symtab指向了__libc_start_main_got-8,它的st_value偏移为8,因此实现了st_value指向了__libc_start_main_got,由于symtab指向了__libc_start_main_got-8,其地址一般仍然为got表地址,里面中的数据存在值,因此sym->st_other不为0的条件也是成立的。 pwndbg> ptype Elf64_Dyn
type = struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
}
pwndbg> ptype Elf64_Sym
type = struct {
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Section st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
}
最后再看写入地址rel_addr的由来: //获取函数对应的重定位表结构地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
...
//得到函数对应的got地址,即真实函数地址要填回的地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
假设我们要将最终得到的地址写在link_map+0x28的位置,它最终是由l->l_addr + reloc->r_offset得到的,l->l_addr的值已经确定,为目标地址到got表中libc地址的偏移,因此要控制reloc->r_offset使得它加上l->l_addr为link_map+0x28的地址。reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset,因此要伪造reloc,假设将伪造的reloc设定在link_map+0x30处,其中reloc_offset已经确定,由相对应的函数的reloc_arg*0x8确定。因此我们要伪造l_info[DT_JMPREL],根据定义#define DT_JMPREL 23,我们需要伪造.rel.plt表所对应的Elf64_Dyn结构体,我们将l_info[DT_JMPREL]指向link_map+0x80-8,即最后使得它对应的d_ptr为link_map+0x80,我们在link_map+0x80中写入link_map+0x30-reloc_offset的值,这样就使得.rel.plt表的地址为link_map+0x30-reloc_offset,最终经过reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset得到了reloc指向link_map+0x30,我们在link_map+0x30中构造伪造的Elf64_Rela结构体,并将r_offset填入link_map+0x28减去l->l_addr的值,最终得到rel_addr为link_map+0x28,同时将r_info填入0x7,以绕过前面对类型的判定。 pwndbg> ptype Elf64_Rela
type = struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
}
最终形成了sym->st_other不为0,rel_addr指向link_map+0x30,sym->st_value地址为__libc_start_maingot表的地址,l->l_addr为system到__libc_start_main的偏移,经过value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);,我们会得到value为system地址,并最后写入到了rel_addr即link_map+0x30中,实现对system函数的调用。 写的很绕,主要是里面结构体指针指来指去的,感觉自己也不太愿意回头再看一遍。。。。 实例--hitcon2015--blinkroot此题是ELF64程序,在bss段输入了一个0x400字节大小的数据。并给了一个任意地址写,由于使用了movaps指令,所以目标指令地址必须要16字节对齐。 由于没有泄露,只存在一次地址写任意值,且程序在读取输入后将输入、输出以及错误句柄都关闭了,所以考虑使用ret2dl_resolve攻击。因此考虑覆盖link_map指向bss段地址,通过伪造link_map实施上述的ret2dl_resolve攻击。 最后通过构造好的link_map,将sym->st_value地址为__libc_start_maingot表的地址,l->l_addr为system到__libc_start_main的偏移,通过后面的puts调用实施ret2dl_resolve攻击,它所对应的reloc_offset为0x18,最终实现调用system反弹shell。 我也在pwn_debug中加入了伪造link_map的apibuild_link_map,方便较快的构造link_map。 小结不得不说延迟绑定是一个比较绕的过程。特别是伪造它们的结构,由于指针的指来指去,感觉很绕,但是在理解了以后其实还是比较简单,可能也是能力不够,表述的比较粗糙,但也是尽力了。 exp和文件在github[3] 参考链接文章首发于安全客-ret2dl_resolve解析[9] References[1] roputils: https://github.com/inaz2/roputils[2] pwn_debug: https://github.com/ray-cp/pwn_debug[3] github: https://github.com/ray-cp/pwn_category/tree/master/stack/ret2dl_resolve[4] ROP之return to dl-resolve: http://rk700.github.io/2015/08/09/return-to-dl-resolve/[5] ret2dl_resolve学习笔记: https://veritas501.space/2017/10/07/ret2dl_resolve学习笔记/[6] roputils/examples/dl-resolve-i386.py: https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py[7] HITCON 2015 PWN 200 blinkroot: https://ddaa.tw/hitcon_pwn_200_blinkroot.html[8] https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 : https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849[9] 安全客-ret2dl_resolve解析: https://www.anquanke.com/post/id/184099
|