6.1.3 pwn XDCTF2015 pwn200
题目复现
出题人在博客里贴出了源码,如下:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}使用下面的语句编译:
checksec 如下:
在开启 ASLR 的情况下把程序运行起来:
这题提供了二进制文件而没有提供 libc.so,而且也默认找不到,在章节 4.8 中我们提供了一种解法,这里我们讲解另一种。
ret2dl-resolve 原理及题目解析
这种利用的技术是在 2015 年的论文 “How the ELF Ruined Christmas” 中提出的,论文地址在参考资料中。ret2dl-resolve 不需要信息泄露,而是通过动态装载器来直接标识关键函数的位置并调用它们。它可以绕过多种安全缓解措施,包括专门为保护 ELF 数据结构不被破坏而设计的 RELRO。而在 ctf 中,我们也能看到它的身影,通常用于对付无法获得目标系统 libc.so 的情况。
延迟绑定
关于动态链接我们在章节 1.5.6 中已经讲过了,这里就重点讲一下动态解析的过程。我们知道,在动态链接中,如果程序没有开启 Full RELRO 保护,则存在延迟绑定的过程,即库函数在第一次被调用时才将函数的真正地址填入 GOT 表以完成绑定。
一个动态链接程序的程序头表中会包含类型为 PT_DYNAMIC 的段,它包含了 .dynamic 段,结构如下:
一个 Elf_Dyn 是一个键值对,其中 d_tag 是键,d_value 是值。其中有个例外的条目是 DT_DEBUG,它保存了动态装载器内部数据结构的指针。
段表结构如下:
具体来看,首先在 write@plt 地址处下断点,然后运行:
由于是第一次运行,尚未进行绑定,0x804a01c 地址处保存的是 write@plt+6 的地址 0x8048436,即跳转到下一条指令。
将 0x20 压入栈中,这个数字是导入函数的标识,即一个 ELF_Rel 在 .rel.plt 中的偏移:
然后跳转到 0x80483e0,该地址是 .plt 段的开头,即 PLT[0]:
接下来就进入 PLT[0] 处的代码:
看一下 .got.plt 段,所以 0x804a004 和 0x804a008 分别是 GOT[1] 和 GOT[2]。继续调试:
PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。这两个 GOT 表条目有着特殊的含义,动态链接器在开始时给它们填充了特殊的内容:
GOT[1]:一个指向内部数据结构的指针,类型是 link_map,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的
l_info域中保存了.dynamic段中大多数条目的指针构成的一个数组,我们后面会利用它。GOT[2]:一个指向动态装载器中
_dl_runtime_resolve函数的指针。
函数使用参数 link_map_obj 来获取解析导入函数(使用reloc_index参数标识)需要的信息,并将结果写到正确的 GOT 条目中。在 _dl_runtime_resolve 解析完成后,控制流就交到了那个函数手里,而下次再调用函数的 plt 时,就会直接进入目标函数中执行。
_dl-runtime-resolve 的过程如下图所示:

重定位项使用 Elf_Rel 结构体来描述,存在于 .rep.plt 段和 .rel.dyn 段中:
32 位程序使用 REL,而 64 位程序使用 RELA。
下面的宏描述了 r_info 是怎样被解析和插入的:
举个例子:
每个符号使用 Elf_Sym 结构体来描述,存在于 .dynsym 段和 .symtab 段中,而 .symtab 在 strip 之后会被删掉:
下面的宏描述了 st_info 是怎样被解析和插入的:
所以 PLT[0] 其实就是调用的以下函数:
该函数在 glibc/sysdeps/i386/dl-trampoline.S 中用汇编实现,先保存寄存器,然后将两个值分别传入寄存器,调用 _dl_fixup,最后恢复寄存器:
还记得这两个值吗,一个是在 <write@plt+6>: push 0x20 中压入的偏移量,一个是 PLT[0] 中 push DWORD PTR ds:0x804a004 压入的 GOT[1]。
函数 _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg),其参数分别由寄存器 eax 和 edx 提供。继续调试:
即使我们使用单步进入,也不能调试 _dl_fixup,它直接就执行完成并跳转到 write 函数了,而此时,GOT 的地址已经被覆盖为实际地址:
再强调一遍:fixup 是通过寄存器取参数的,这似乎违背了 32 位程序的调用约定,但它就是这样,上面 gdb 中显示的参数是错误的,该函数对程序员来说是透明的,所以会尽量少用栈去做操作。
既然不能调试,直接看代码吧,在 glibc/elf/dl-runtime.c 中:
攻击
关于延迟绑定的攻击,在于强迫动态装载器解析请求的函数。

图a中,因为动态转载器是从
.dynamic段的DT_STRTAB条目中获得.dynstr段的地址的,而DT_STRTAB条目的位置已知,默认情况下也可写。所以攻击者能够改写DT_STRTAB条目的内容,欺骗动态装载器,让它以为.dynstr段在.bss段中,并在那里伪造一个假的字符串表。当它尝试解析 printf 时会使用不同的基地址来寻找函数名,最终执行的是 execve。这种方式非常简单,但仅当二进制程序的.dynamic段可写时有效。图b中,我们已经知道
_dl_runtime_resolve的第二个参数是 Elf_Rel 条目在.rel.plt段中的偏移,动态装载器将这个值加上.rel.plt的基址来得到目标结构体的绝对位置。然后当传递给_dl_runtime_resolve的参数reloc_index超出了.rel.plt段,并最终落在.bss段中时,攻击者可以在该位置伪造了一个Elf_Rel结构,并填写r_offset的值为一个可写的内存地址来将解析后的函数地址写在那里,同理r_info也会是一个将动态装载器导向到攻击者控制内存的下标。这个下标就指向一个位于它后面的Elf_Sym结构,而Elf_Sym结构中的st_name同样超出了.dynsym段。这样这个符号就会包含一个相对于.dynstr地址足够大的偏移使其能够达到这个符号之后的一段内存,而那段内存里保存着这个将要调用的函数的名称。
还记得我们前面说过的 GOT[1],它是一个 link_map 类型的指针,其 l_info 域中有一个包含 .dynmic 段中所有条目构成的数组。动态链接器就是利用这些指针来定位符号解析过程中使用的对象的。通过覆盖这个 link_map 的一部分,就能够将 l_info 域中的 DT_STRTAB 条目指向一个特意制造的动态条目,那里则指向一个假的动态字符串表。

pwn200
获得了 re2dl-resolve 所需的所有知识,下面我们来分析题目。
首先触发栈溢出漏洞,偏移为 112:
根据理论知识及对二进制文件的分析,我们需要一个 read 函数用于读入后续的 payload 和伪造的各种表,一个 write 函数用于验证每一步的正确性,最后将 write 换成 system,就能得到 shell 了。
分别获取伪造各种表所需要的段地址,将 bss 段的地址加上 0x600 作为伪造数据的基地址,这里可能需要根据实际情况稍加修改。gadget pppr 用于平衡栈, pop ebp 和 leave ret 配合,以达到将 esp 指向 base_addr 的目的(在章节3.3.4中有讲到)。
第一部分的 payload 如下所示,首先从标准输入读取 100 字节到 base_addr,将 esp 指向它,并跳转过去,执行 base_addr 处的 payload:
从这里开始,后面的 paylaod 都是通过 read 函数读入的,所以必须为 100 字节长。首先,调用 write@plt 函数打印出与 base_addr 偏移 80 字节处的字符串 "/bin/sh",以验证栈转移成功。注意由于 .dynstr 中的字符串都是以 \x00 结尾的,所以伪造字符串为 bin/sh\x00。
我们知道第一次调用 write@plt 时其实是先将 reloc_index 压入栈,然后跳转到 PLT[0]:
这次我们跳过这个过程,直接控制 eip 跳转到 PLT[0],并在栈上布置上 reloc_index,即 0x20,就像是调用了 write@plt 一样。
接下来,我们更进一步,伪造一个 write 函数的 Elf32_Rel 结构体,原结构体在 .rel.plt 中,如下所示:
该结构体的 r_offset 是 write@got 地址,即 0x0804a01c,r_info 是 0x707。动态装载器通过 reloc_index 找到它,而 reloc_index 是相对于 .rel.plt 的偏移,所以我们如果控制了这个偏移,就可以跳转到伪造的 write 上。payload 如下:
另外讲一讲 Elf32_Rel 值的计算方法如下,我们下面会得用到:
ELF32_R_SYM(0x707) = (0x707 >> 8) = 0x7,即.dynsym的第 7 行ELF32_R_TYPE(0x707) = (0x707 & 0xff) = 0x7,即#define R_386_JMP_SLOT 7 /* Create PLT entry */ELF32_R_INFO(0x7, 0x7) = (((0x7 << 8) + ((0x7) & 0xff)) = 0x707,即 r_info
这一次,伪造位于 .dynsym 段的结构体 Elf32_Sym,原结构体如下:
转储 .dynsym 段并找到第 7 行:
其中最重要的是 st_name 和 st_info,分别为 0x4c 和 0x12。构造 payload 如下:
一样地讲一下 st_info 的解析和插入算法:
ELF32_ST_BIND(0x12) = (((unsigned char) (0x12)) >> 4) = 0x1,即#define STB_GLOBAL 1 /* Global symbol */ELF32_ST_TYPE(0x12) = ((0x12) & 0xf) = 0x2,即#define STT_FUNC 2 /* Symbol is a code object */ELF32_ST_INFO(0x1, 0x2) = (((0x1) << 4) + ((0x2) & 0xf)) = 0x12,即 st_info
下一步,是将 st_name 指向我们伪造的字符串 "write",payload 如下:
最后,只要将 "write" 替换成任何我们希望的函数,并调整参数,就可以了,这里我们换成 "system",拿到 shell:
Bingo!!!
这题是 32 位程序,在 64 位下会有一些变化,比如说:
64 位程序一般情况下使用寄存器传参,但给
_dl_runtime_resolve传参时使用栈_dl_runtime_resolve函数的第二个参数reloc_index由偏移变为了索引。_dl_fixup函数中,在伪造 fake_sym 后,可能会造成崩溃,需要将link_map+0x1c8地址上的值置零
具体的以后遇到再说。
如果觉得手工构造太麻烦,有一个工具 roputils 可以简化此过程,感兴趣的同学可以自行尝试。
漏洞利用
完整的 exp 如下:
参考资料
Last updated
Was this helpful?