ret2dl_resolve
这个貌似,,咕了很长时间的文章了……在Lacanva师傅的监督下,,,终于不咕了……呜呜呜~
前置知识
本节文章实验所用代码:
#include<stdio.h>
#include<stdlib.h>
int main(){
char s[16]
puts("Input:");
read(0, s, 0x100);
puts(s);
puts("This is a test.");
return 0;
}
.Dynamic段
由于_dl_runtime_resolve
主要是链接的过程,所以我们先研究一下ELF
文件里面的.Dynamic
段。我们在IDA的stucture
里面可以看到dyn
结构体
00000000 Elf32_Dyn struc ; (sizeof=0x8, align=0x4, copyof_4) 00000000 ; XREF: LOAD:_DYNAMIC/r 00000000 ; LOAD:08049668/r ... 00000000 d_tag dd ? 00000004 d_un Elf32_Dyn::$A263394DDF3EC2D4B1B8448EDD30E249 ? 00000008 Elf32_Dyn ends
对应的声明是:
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
Elf32_Sword d_tag
用于表示该结构体中d_un
的类型,在内存中:
这些内容也可以通过readelf
来获得:
blackbird@ubuntu ~/C/tmp> readelf -d test
Dynamic section at offset 0x660 contains 24 entries: 标记 类型 名称/值 0x00000001 (NEEDED) 共享库:[libc.so.6] 0x0000000c (INIT) 0x80482a8 0x0000000d (FINI) 0x80484f4 0x00000019 (INIT_ARRAY) 0x8049658 0x0000001b (INIT_ARRAYSZ) 4 (bytes) 0x0000001a (FINI_ARRAY) 0x804965c 0x0000001c (FINI_ARRAYSZ) 4 (bytes) 0x6ffffef5 (GNU_HASH) 0x804818c 0x00000005 (STRTAB) 0x804820c 0x00000006 (SYMTAB) 0x80481ac 0x0000000a (STRSZ) 79 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x804974c 0x00000002 (PLTRELSZ) 24 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x8048290 0x00000011 (REL) 0x8048288 0x00000012 (RELSZ) 8 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffe (VERNEED) 0x8048268 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x804825c 0x00000000 (NULL) 0x0
这里我们主要强调以下这些内容:
-
DT_STRTAB
处于
.dynamic
的地址加0x40的位置;该元素保存着字符串表地址
,包括了符号名,库名,和一些其他的在该表中的字符串。指向
.dynstr
-
DT_SYMTAB
处于
.dynamic
的地址加0x4c的位置;该元素保存着符号表的地址,对
32-bit
类型的文件来说,关联着一个Elf32_Sym
入口。指向.dynsym
对于每一个
Elf32_Sym
都有:typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; }Elf32_Sym;
我们可以对应着声明分别看一下部分内容:
-
st_name
:符号的名字;但它并不是一个字符串,而是字符串表中的一个索引值,在字符串表中该索引值的位置上存放的字符串就是该符号名字的实际文本;如果此值不为0,则它就代表符号名字在字符串表中的索引值;如果此值为0,则表示此符号没有名字;
-
st_info
:符号的类型和属性;该字段由一系列的二进制位构成,标识了"符号绑定(
symbol binding
)"、"符号类型(symbol type
)"和"符号信息(symbol information
)"三种属性
-
-
DT_JMPREL
处于
.dynamic
的地址加0x80的位置;包含了重定位表的信息:
声明:
typedef struct { Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址 函数got地址 Elf32_Word r_info; // 动态符号符号表索引 } Elf32_Rel; #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
我们以
read
函数为例,read
函数的got
表地址是0x8049758;read
函数在symtable
中的偏移为1,类型为7。
延迟绑定机制
在程序运行时,有很多函数在程序执行时不会被用到,比如错误处理或者 用户比较少用的功能模块等,所以不需要所有函数在一开始就链接好。
延迟绑定(Lazy Binding
) 的基本思想是 函数第一次被调用时才进行绑定(符号查找、重定位等),如果没有则不进行绑定。要实现 延迟绑定 需要使用到名为 PLT(Procedure Linkage Table)
的方法。
而通常延迟绑定机制又是通过调用 _dl_runtime_resolve
函数来实现的,这也正是此函数没有延迟绑定的原因。
_dl_runtime_resolve
函数
_dl_runtime_resolve
是重定位函数,该函数会在进程运行时动态修改函数地址来达到重定位的效果。此函数无无延迟绑定机制, 需要两个参数,一个是 reloc_arg
,就是函数自己的 plt
表项 push
的内容,一个是 link_map
,这个是公共 plt
表项 push
进栈的,通过它可以找到.dynamic
的地址
_dl_fixup()
函数
_dl_fixup()
函数在elf/dl_runtime.c
中实现,用于解析导入函数的真实地址,并改写got
表
调试
这一段实验用的代码:
#include<stdio.h>
#include<stdlib.h>
int main(){
char s[16];
puts("Input:");
read(0, s, 0x100);
puts(s);
puts("This is a test.");
return 0;
}
我们已经做了充足的准备,下来可以开始调试了, 调试的时候遇到什么问题可以再往前参考。
我们在第一个puts
的时候单步步进:
我们进入这个函数后进入了程序的.plt
段:
我们发现,.plt
段相当于是专门为后面_dl_runtime_resolve
函数寻找函数真实地址传递参数用的。第一个参数reloc_arg
,对于每一个函数都不一样,即该函数在Elf_JMPREL(ELF JMPREL Relocation Table)
中的偏移:
第一个参数压栈了以后,来到了.plt
的开头,压入第二个参数link_map
,`_GLOBAL_OFFSET_TABLE_+4,也就是
got`偏移4的内存:
我们可以看到,在link_map
中的第三个参数就是.Dynamic
段的地址。
接下来就进入了_dl_runtime_resolve
函数, 一开始直接进行传参:
从注释中我们清晰的看到,_dl_runtime_resolve
一开始将plt
压栈的参数放置到寄存器中,然后_dl_fixup
函数从寄存器中取参。
我们在glibc/elf/dl-run-time.c
中找到_dl_fixup
函数的定义,然后对着源码和IDA
、GDB
,,,emmm……调着看着,它大概主要干了这么几件事:
-
用
link_map
访问.dynamic
,取出.dynstr
,.dynsym
,.rel.plt
的指针 -
.rel.plt +
第二个参数求出当前函数的重定位表项Elf32_Rel
的指针,记作rel
-
rel->r_info >> 8
作为.dynsym
的下标,求出当前函数的符号表项Elf32_Sym
的指针,记作sym
-
.dynstr + sym->st_name
得出符号名字符串指针 -
在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset
,即GOT
表 -
调用这个函数
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) { // 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + ); // 然后通过reloc->r_info找到.dynsym中对应的条目 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM)(reloc->r_info)]; // 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7 assert(ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址 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); // 最后把value写入相应的GOT表条目中 return elf_machine_fixup_plt(l, result, reloc, rel_addr, value); }
emm……要是懒得看字的话,大致就是这亚的一个流程:
攻击方式
我们现在对整个流程有一个大致的了解,我们现在考虑一下针对这个过程如何攻击???我们的目标是获取
system
函数的地址,也就是LOOKUP_VALUE_ADDRESS
函数的参数为指向system
字符串的指针时的返回地址,那么我们很自然想到的就是修改DT_STRTAB
,但是我们在IDA
里面明显能看到这一段是不可写的,也就是DT_STRTAB
、DT_SYMTAB
、DT_JMPREL
都是不可写的。那么我们既然修改不了他的,,,那我们就自己伪造一个(最好在.bss
段上吧……)。对了,.Dynamic
段是有可写权限的,那如果可以的话,直接修改.Dynamic
段,把.dynstr
和.dynsym
段或者其他段定位到我们伪造的地方上。大致流程就是栈迁移+伪造+
ROP
……胡思乱想:
如果对
ROP
长度有限制或者ROP
不大能控制的话,那么修改.Dynamic
段,应该有用???就这些吧……