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~!\n";
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)中的方法进行漏洞利用。

PWN——ret2dl_resolve的更多相关文章

  1. CTF必备技能丨Linux Pwn入门教程——调整栈帧的技巧

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  2. ret2dl_resolve

    ret2dl_resolve是一种比较复杂的高级ROP技巧,利用它之前需要先了解动态链接的基本过程以及ELF文件中动态链接相关的结构. 我根据raycp师傅的文章,动手调试了一下: https://r ...

  3. Pwn~

    Pwn Collections Date from 2016-07-11 Difficult rank: $ -> $$... easy -> hard CISCN 2016 pwn-1 ...

  4. iscc2016 pwn部分writeup

    一.pwn1 简单的32位栈溢出,定位溢出点后即可写exp gdb-peda$ r Starting program: /usr/iscc/pwn1 C'mon pwn me : AAA%AAsAAB ...

  5. i春秋30强挑战赛pwn解题过程

    80pts: 栈溢出,gdb调试发现发送29控制eip,nx:disabled,所以布置好shellcode后getshell from pwn import * #p=process('./tc1' ...

  6. SSCTF Final PWN

    比赛过去了两个月了,抽出时间,将当时的PWN给总结一下. 和线上塞的题的背景一样,只不过洞不一样了.Checksec一样,发现各种防护措施都开了. 程序模拟了简单的堆的管理,以及cookie的保护机制 ...

  7. pwn学习(1)

    0x00 简介 入职之后,公司发布任务主搞pwn和re方向,re之前还有一定的了解,pwn我可真是个弟弟,百度了一番找到了蒸米大佬的帖子,现在开始学习. 0x01 保护方式 NX (DEP):堆栈不可 ...

  8. pwn学习之四

    本来以为应该能出一两道ctf的pwn了,结果又被sctf打击了一波. bufoverflow_a 做这题时libc和堆地址都泄露完成了,卡在了unsorted bin attack上,由于delete ...

  9. pwn学习之三

    whctf2017的一道pwn题sandbox,这道题提供了两个可执行文件加一个libc,两个可执行文件是一个vuln,一个sandbox,这是一道通过沙盒去保护vuln不被攻击的题目. 用ida打开 ...

随机推荐

  1. AcWing 342. 道路与航线

    #include<bits/stdc++.h> using namespace std; const int N=2e5+5; int h[N],cnt,to[N],nxt[N],vis[ ...

  2. APDU:APDU常用指令

    APDU= ApplicationProtocol data unit, 是智能卡与智能卡读卡器之间传送的信息单元, (给智能卡发送的命令)指令(ISO 7816-4规范有定义) CLA INS P1 ...

  3. awk中printf的用法

    printf函数   打印输出时,可能需要指定字段间的空格数,从而把列排整齐.在print函数中使用制表符并不能保证得到想要的输出,因此,可以用printf函数来格式化特别的输出. printf函数返 ...

  4. <jsp:param>传递参数,出现乱码问题

    今天在学习<jsp:forward>和<jsp:param>时,用<jsp:param>传递参数时,出现乱码问题,部分代码如下: 1 <jsp:forward ...

  5. WPF教程十:如何使用Style和Behavior在WPF中规范视觉样式

    在使用WPF编写客户端代码时,我们会在VM下解耦业务逻辑,而剩下与功能无关的内容比如动画.视觉效果,布局切换等等在数量和复杂性上都超过了业务代码.而如何更好的简化这些编码,WPF设计人员使用了Styl ...

  6. WPF教程二:理解WPF的布局系统和常用的Panel布局

    WPF的布局系统 了解元素的测量和排列方式是理解布局的第一步.在测量(measure)阶段容器遍历所有子元素,并询问子元素它们所期望的尺寸.在排列(arrange)阶段,容器在合适的位置放置子元素.理 ...

  7. DIY一个智能开关kwswitch

    源码地址:https://gitee.com/kerwincui/kwswitch 平台简介 该智能开关平台包括服务端.硬件端.PC端和安卓端.硬件使用ESP8266模块,成本相对较低,可以发挥想象力 ...

  8. Linux | 文件编辑命令

    cat cat 命令是是一次性显示文件的所有内容 cat 是 concatenate 的缩写,表示:连接/串联 cat 文件名 可以看到,cat 命令是一次性显示出所有的文件内容,这就导致了,有的文件 ...

  9. java基础---类和对象(1)

    一. 类和对象 面向对象:以属性和行为的观点去分析现实生活中的事物,将功能封装进对象, 强调具备了功能的对象,以类/对象为最小单位,考虑谁来做 面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做 ...

  10. 脱离OBDeploy工具,手工部署OceanBase方法

    [简介] OBDeploy是OceanBase集群部署的工具,可以通过简单的几行命令,就能快速的进行OceanBase部署.但对于初学者来讲,可能会比较困惑,Deploy到底做了哪些事情?里面的具体步 ...