PWN——ret2dl_resolve

PWN——ret2dl_resolve

ret2dl_resolve是栈溢出中,底层且高级的一种利用手段,这里特此做一篇笔记学习一下。

个人认为,要掌握这种方法,自己去写demo来多次调试分析是不二法门。

demo是XDCTF 2015的一道例题。

#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~!
";
    setbuf(stdout,buf);
    write(1,buf,strlen(buf));
    vuln();
    return 0;
}
gcc -g -o XDCTF2015 -m32 -fno-stack-protector XDCTF2015.c

为了方便调试,也可以提前关闭一下aslr

//关闭aslr
echo 0 > /proc/sys/kernel/randomize_va_space
//开启aslr
echo 2 > /proc/sys/kernel/randomize_va_space

1.ELF文件前置知识

ELF可执行文件由ELF头部,程序头部表和其对应的段,节头部表和其对应的节组成。如果一个可执行文件参与动态链接,它的程序头部将包含类型为PT_DYNAMIC段。PT_DYNAMIC段中包含很多在动态链接中所需要的节。每个节都是一个相应的结构体数组。

借助readelf,我们可以分析这些节中的信息。readelf   -d命令可以显示PT_DYNAMIC段中的信息。

下图是Bamboofox的社课中的图片,标注了几个漏洞利用相关的节

JMPREL节中存放了动态链接中的重定位信息

有.rel.plt节和.rel.dyn节。.rel,plt用于函数重定位,.rel.plt节用于变量重定位。

.rel.plt节相关的ELF32_REL结构如下:

typedef struct {
    Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
    Elf32_Word r_info; // 符号表索引 
} Elf32_Rel;
 
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))
/*

r_offset是该函数在.got.plt中的地址。

ELF32_R_SYM(Elf32_Rel->r_info)=(Elf32_Rel->r_info)>>8

SYMTAB节中包含了动态链接符号表。

.dynsym节相关的Elf32_Sym节构如下:

typedef struct
{
    Elf32_Word st_name; // Symbol name(string tbl index)
    Elf32_Addr st_value; // Symbol value
    Elf32_Word st_size; // Symbol size
    unsigned char st_info; // Symbol type and binding
    unsigned char st_other; // Symbol visibility under glibc>=2.2
    Elf32_Section st_shndx; // Section index
} Elf32_Sym;


注:为了表示动态链接这些模块之间的符号导入导出关系,ELF专门有一个叫做动态符号表的段来保存这些信息,这个段叫做“.dynsym”段,动态符号表只保存了与动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。

STRTAB节中包含了动态链接的字符串。这个节以"x00"结尾,中间每个字符串也以"x00"间隔

2.延迟绑定技术

程序在执行前,如果对整个动态链接库函数进行符号解析的话,是非常浪费资源的,因为一个程序不可能调用动态链接库中所有的函数。我们最好能做到只对用到的函数进行函数解析,这样可以大大提高文件链接的效率,加快程序的启动速度。

这时候,就出现了延迟绑定技术,我们通过plt表(过程链接表),在第一次调用函数的时候,来确定函数的地址,把函数的实际地址存储在got表相对的偏移处。

试想一下,在这个过程中,当我们第一次调用函数,要向got表写入函数真实地址的时候,我们是不是需要一个管理的工具?这个管理的工具就是_dl_runtime_resolve()函数。函数需要两个参数,一个是要被绑定的函数所在的模块,一个是要被绑定函数的符号名。

函数原型:_dl_runtime_resolve(link_map,reloc_arg)

注:got表实际上是分为.got表和got.plt表,got.plt表(全局函数偏移表)中存放的是动态链接库函数,.got表(全局变量偏移表)里面的偏移主要是全局变量。我们在这里讨论的是got.plt表。

.got.plt表的前三项的含义分别如下:

1.got[0],第一项保存的是".dynamic"段的地址,这个段描述了本模块动态链接相关的信息;

2.got[1],第二项保存的是本模块的ID;

3.got[2],第三项保存的是_dl_runtime_resolve()函数的地址。

lazy binding是通过plt表和got表之间巧妙的代码跳转实现的,我们通过例子来研究一下。

一个简单的demo,在gdb下用info function查看他的函数信息,看到了plt表中函数的地址。

然后查看plt表的汇编代码

以read函数为例,他这里有三条指令

jmp DWORD PTR[ebx+0x10]
push 0x8
jmp 0x56556020

jmp这里表示一个跳转,综合我们上面说的,它应该是跳转到了read函数在got.plt表的偏移处,但是这里是[ebx+0x10]。我们来看一下ebx寄存器中存储的值是多少。

ebx中存储的值是0x56559000,那么我们看一下0x56559000+0x10地址处存储着什么。

0x56559010处确实是read@got.plt的地址,说明我们的猜想是正确的。我们同时看一下这里存储的值,可以看出,这里存储的是前面read函数中push 8在plt表的地址,说明跳转到got表之后,got表并没有实际地址,这时候,程序又跳转回plt表的下一条指令处执行。

这里的push 8又是什么呢,我们这里先猜测一下:8应该是read函数对应的Elf32_Rel结构体在.rel.plt节中的相对偏移。

执行完push 8之后,执行下一条语句:jmp 0x56556020。

我们现在回忆一下前面提到的_dl_runtime_resolve()函数,它有两个参数,link_map和reloc_arg。

第一条指令是将[ebx+0x4]入栈,第二条指令是跳转到0x56559000+0x8地址处。0x56559000+0x8存放的是_dl_runtime_resolve函数的地址。

算上前面的push的参数,正好push了两个参数,按照参数入栈的顺序,我们知道[ebx+0x4]就是link_map的指针,0x8就是reloc_arg的偏移。这时候,填入的正好是got表项的前三项,got[0]是0x8(reloc_arg用来计算函数重定位的入口),这个参数就是Elf_Rel条目在重定位表.rel.plt段中对应当前请求函数的偏移,动态链接器将这个值加上.rel.plt的基地址来得到目标Elf_Rel结构的绝对地址;got[1]是link_map;got[2]是_dl_runtime_resolve函数的入口地址。

 _dl_runtime_resolve是在glibc-2.23/sysdeps/i386/dl-trampoline.S中用汇编实现的。调用_dl_fixup函数(call 0xf7fe37f0),通过寄存器传参。_dl_fixup是在glibc-2.23/elf/dl_runtime.c实现的。

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
       ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
       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]);
  // 通过参数 reloc_arg 计算重定位入口,这里的 DT_JMPREL 即 .rel.plt,reloc_offset 即 reloc_arg
//DT_JMPREL存放着.rel.plt节的地址,.rel.plt是Elf32_Rel结构体数组 const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 根据函数重定位表中的动态链接符号表索引,即 reloc->r_info,获取函数在动态链接符号表中对应的条目 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; const ElfW(Sym) *refsym = sym; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; /* Sanity check that we're really looking at a PLT relocation. */ assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/*Elf32_R_SYM(Elf32_Rel->r_info)=(Elf32_Rel->r_info)>>8*/
/*eg:Elf32_R_TYPE(0x607)=7*/
/* 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; } /* We need to keep the scope around so do some locking. This is not necessary for objects which cannot be unloaded or when we are not using any threads (yet). */ int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif // 根据 strtab+sym->st_name 在字符串表中找到函数名,然后进行符号查找获取 libc 基址 result 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. */ if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif /* Currently result contains the base load address (or link map) of the object that defines sym. Now add in the symbol offset. */ // 将要解析的函数的偏移地址加上 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; } /* And now perhaps the relocation addend. */ value = elf_machine_plt_value (l, reloc, value); // 将已经解析完成的函数地址写入相应的 GOT 表中 if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0)) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); /* Finally, fix up the plt itself. */ if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); }

 3.漏洞利用

 简单来想,我们要利用这种漏洞,肯定要想办法伪造_dl_runtime_resolve()函数的第二个参数。定义在动态链接库中的函数,在导入的时候,在装载阶段作为外部符号导入可执行文件。导入符号的解析需要重定位的支持,重定位项以Elf_Rel结构体来描述。这个结构的实例保存在.rel.plt段(用于导入函数)和.rel.dyn段中(用于导入全局变量),这里的重定位可以类比静态链接时候的.rel.text段,只不过静态链接的重定位都是在链接阶段完成的。

当程序导入一个正常函数是,连接器会在.dynstr段中包含一个函数名称的字符串,在.dynsym段中包含一个指向它的符号:Elf_Sym,在.rel.plt段中包含一个指向这个符号的重定位项:Elf_Rel。

dl_runtime.c中,首先分别获取动态链接符号表和动态链接字符串的基址,然后通过dl_runtime_resolve函数的第二个参数reloc_arg计算重定位入口

重定位的目标(即Elf_Rel结构中的r_offset域)将会是got表中的一个条目。GOT表保存于.got.plt段,由能够解析.rel.plt段中的重定位的动态链接器来填写。

.dynstr段是不可写的,所以我们想要覆写.dynstr段来进行任意函数的调用是不可能的。动态链接器是从.dynamic段的DT_STRTAB表中获得.dynstr段的地址的,而且DT_STRTAB地址是已知且可写的(可以通过readelf这种elf文件解析器获取DT_STRTAB和DT_SYMTAB地址)。所以图(a)中将.dynamic段中DT_STRTAB条目覆写,将.dynstr的地址写到bss段,然后再bss段布置一段假的字符串表。当它尝试解析某个函数的时候会使用不同的基址来寻找函数名,最终执行execve,这种方式非常简单,但仅当二进制程序的.dynamic段可写时有效,当RELRO部分开启的时候,.dynamic段将会被标记为只读,这时候上述方法无法使用。

图(b)中,r_info表示该函数在.rel.plt段的偏移,.rel.plt段的基址加上r_info的偏移,得到目标结构体的绝对地址,当_dl_runtime_resolve第二个参数reloc_indoex超出.rel.plt段,最终落在.bss段中时,攻击者可以在这个位置伪造一个Elf_Rel结构,并填写r_offset的值为一个可写的内存地址来将解析后的函数地址写在那里。r_info也会是一个将动态链接器导向攻击者控制内存的下标。这个下标就只想一个位于它后面的Elf_Sym结构体(我们前面说到过,这个结构体中的st_name会指向.dynstr段中对应的一个函数名称)。所以我们可以在bss段布置一个Elf_Sym结构体,然后控制st_name指向我们要构造的函数名。这是一般通用的方法,在可以绕过部分RELRO的检测。

我们用readelf和gdb查看一下dynsym节

可以看出,.dynsym节遵循16字节对齐,前四个字节就是st_name,这里Elf32_Sym[6]处保存着write函数的符号表信息,Elf32_Sym[6]->st_name=0x4c,.dynstr节的基地址再加上0x4c的偏移量,就是字符串write。

这时候,我们就可以利用图(b)中的方法进行漏洞利用

原文地址:https://www.cnblogs.com/L0g4n-blog/p/12977300.html