本文以XDCTF2015的bof这题为例记录一下利用ret2 dl-runtime resolve来控制程序函数解析,最终getshell的过程。想要实现整个过程,需要非常清楚动态链接的程序调用libc中函数时的解析过程,原理可以参考《程序员的自我修养》chapter7.4和chapter7.5,之后可以继续阅读pediy的文章,见参考[1]。了解原理后,就可以看ctf-wiki或本文的实践过程了。本文所有源码放在https://github.com/thinkycx/pwn/ 中的XDCTF2015文件夹下。

XDCTF bof源码

$ cat bof.c
/*
 * gcc bof.c -m32 -fno-stack-protector -o bof
 * https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced_rop/
 * ret2dlresolve
 * */
#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;
}

exploit

stage1 ret2write@plt

stage1-5的利用,调用write输出bss上的字符串算利用成功。

利用思路:

  • 栈溢出利用的第一个payload:read rop到bss,后利用stack pivot跳转执行读入的rop
  • payload2:rop 调用write 并布置字符串

注意点:关于如何给题目中的read函数输入,要么read 满足够的字符,要么exp中sleep一会儿不去管它都可以完成read操作。此外,exp中需要设置交互,不能让exp程序退出。

image-20181116150208622

stage2 push relplt offset & ret2plt0

stage1中利用write@plt,write函数已经调用过,因此第二次调用会调用write@GOT中的地址来执行,如果利用write@plt+6,也就是再走一遍ret2dlresolve,也可以继续执行write函数。

stage2中利用的方法就是再走一遍ret2dlreolve,只不过rop中push了write@reltab的offset 0x20(这里i386和amd64似乎有写区别,暂时只要知道i386上是offset),直接跳转到plt0来调用write函数。注意由于调用了ret2dlresolve,因此stack pivot到bss上的空间要大一点,这里设置为0x800。如果设置的太小会出现问题,会出现什么问题可以看后面的crash log!!!

image-20181116143425504

期间遇到一个神奇的问题:由于stack pivot到bss segment上时,开辟的栈空间不够,stack_base太小,导致ESP指向了不可写的区域,因此程序在push的时候crash了。一开始很懵,看了很久,第一次遇到push的时候crash了,当时完全想不到往栈上ESP考虑,问了p4nda大佬一眼看出来了这问题。crash log如下:

image-20181115190057643

stage3 set fake Elf32_Rel & change reltab_offset before plt0

image-20181116143901553

将reltab中的Elf32_Rel布置在bss段,同时修改调用rop中调用plt0前push的reltab_offset,fake_reltab_offset = & fake_Elf32_Rel - & reltab。

解析函数地址时,会根据距离relplt的offset来寻找函数的Elf32_Rel,构造fake_reltab_offset就会解析在bss段上的fake_Elf32_Rel。

image-20181116144209660

stage4 set fake Elf32_sym & change fake Elf32_Rel->r_info

在bss段上伪造好symtab中的Elf32_sym,同时修改relplt中的r_info。计算方法:

  1. 先算symtab 中的Elf32_sym offset =(&fake Elf32sym - & symtab) /0x10
  2. 再算reltab中的r_info = (offset << 8) +0x7。

解析函数地址时,解析fake_Elf32_Rel后,构造 fake_Elf32_Rel中的r_info,就会解析在bss段上的fake Elf32_sym。

image-20181116144714679

注意,r_info的修改要注意优先级的问题,举例说明:

In [9]: hex(1<<8 +0x7)
Out[9]: '0x8000'

In [10]: hex( (1<<8) +0x7 )
Out[10]: '0x107

如果r_info设置的不对,会遇到如下错误:

image-20181116095000996

Inconsistency detected by ld.so: dl-runtime.c: 79: _dl_fixup: Assertion `ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT' failed!

stage5 set fake function name & change fake Elf32_sym->st_name (function name offset)

在bss段上伪造String Tab中的String字符串"write\x00",同时伪造好fake Symtab中的offset。elf32sym_st_name_offset = &fake string - &strtab。

函数解析到Symtab时,就会根据offset解析bss上的函数名。

image-20181116145607082

stage6 change fake function name & params

将bss上伪造的String Table中的string "write\x00"修改成"system\x00",并且修改write原来的三个参数为"/bin/sh" addr,就可以getshell。

image-20181116155539665

Getshell:

image-20181116155643415

补充

搞定6个stage后,我发现write_got似乎没有作用。调试一番后得出结论,在利用function str解析出libc中的地址后,会向GOT处写入地址,也就是在调用函数前,我们还拥有任意地址写4byte的能力。

image-20181116135047140

参考

[1]《程序员的自我修养》chapter 7.4 7.5
[2][原创][新手向]ret2dl-resolve详解 https://bbs.pediy.com/thread-227034.htm
[3] https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced_rop/#ret2_dl_runtime_resolve