ret2dl_resolve(ciscn2019_pwn)
ret2_dl_runtime_solve
- 因为之前并没有完整地整理复现ret2dl_resolve这个知识点, 所以特意另开一篇, 借这道题顺便理清思路
ELF关于动态链接的一些关键的section
- 这里提一下section和segment的区别: 前者是目标代码文件的组成部分, 后者是可执行文件的组成部分. 分别使用readelf -S和readelf -l查看, 具体请看我关于ELF文件解析的文章.
注: readelf -S可以查看所有节的地址, 其中类型位REL的节区包含重定位表项.
节中包含目标文件的所有信息,节的结构如下:1
2
3
4
5
6
7
8
9
10
11
12typedef struct {
Elf32_Word sh_name; // 节头部字符串表节区的索引
Elf32_Word sh_type; // 节类型
Elf32_Word sh_flags; // 节标志,用于描述属性
Elf32_Addr sh_addr; // 节的内存映像
Elf32_Off sh_offset; // 节的文件偏移
Elf32_Word sh_size; // 节的长度
Elf32_Word sh_link; // 节头部表索引链接
Elf32_Word sh_info; // 附加信息
Elf32_Word sh_addralign; // 节对齐约束
Elf32_Word sh_entsize; // 固定大小的节表项的长度
} Elf32_Shdr;
.dynamic
- ELF可执行文件由ELF头部,程序头部表和其对应的段,节头部表和其对应的节组成。如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节。结构如下:
1
2
3
4
5
6
7typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn; - 其中Tag对应这每个节.比如JMPREL对应.ret.plt (ELF JMPREL Relocation Table)
- 也可以适合用readelf -d 的命令查看.
.rel.plt
- 重定位表,也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:
1 | typedef struct |
- 也可通过readelf -r来获取相关信息,其中.rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位
- .got节保存全局变量偏移表,.got.plt节保存全局函数偏移表。.got.plt对应着Elf32_Rel结构中r_offset的值
.dynsym
- 是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。.dynsym节包含了动态链接符号表。Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义,
1
ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8其实就是直接看从第二个字节开始的值
- 结构体定义如下
1
2
3
4
5
6
7
8
9typedef struct
{
Elf32_Word st_name; //Symbol name(string tbl index) 符号名,是相对.dynstr起始地址的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //Symbol type and binding 对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;// Section index
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0 - 可以使用readelf -s 查看
- 根据上面我们可以知道read的ELF32_R_SYM(0x107), 所以应该是
Elf32_Sym[1]即保存着read的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMP_SLOT。
.dynstr
- 一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个read。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,在这里就是对0804827C的偏移.
Elf32_Sym[1]->st_name=0x20(调试方法.dynsym + Elf32_Sym_size * num),所以.dynstr加上0x20的偏移量,就是字符串write。
.plt
- .plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。
- 执行read@plt的时候,就会跳到0x804a00c去执行.
延迟绑定
- 先贴张有意思的图
- 来看刚才那三行汇编代码, 第一行:前面提到过0x804a00c是read的GOT表位置,当我们第一次调用read时,其对应的GOT表里并没有存放read的真实地址,而是read@plt的下一条指令地址。
- 第二、三行:把reloc_arg=0x0作为参数推入栈中,跳到0x08048380(PLT[0])继续执行.
- 0x08048380(PLT[0])再把link_map=(GOT+4)(即GOT[1],链接器的标识信息)作为参数推入栈中,而(GOT+8)(即GOT[2],动态链接器中的入口点)中保存的是_dl_runtime_resolve函数的地址。因此以上指令相当于执行了_dl_runtime_resolve(link_map, reloc_arg),该函数会完成符号的解析,即将真实的read函数地址写入其GOT条目中,随后把控制权交给read函数。
- _dl_runtime_resolve是在glibc-2.23/sysdeps/i386/dl-trampoline.S中用汇编实现的。0xf7feae1b处即调用_dl_fixup,并且通过寄存器传参。
- _dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的,这里只列出一些主要函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15_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_offset);
// 然后通过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);
}
_dl_runtime_resolve具体调用顺序
- 用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表
- 调用这个函数
漏洞利用方式
- 漏洞利用方式
- 控制eip为PLT[0]的地址,只需传递一个index_arg参数
- 控制index_arg的大小,使reloc的位置落在可控地址内
- 伪造reloc的内容,使sym落在可控地址内
- 伪造sym的内容,使name落在可控地址内
- 伪造name为任意库函数,如system
baby_pwn
因为开了RELRO, 所以.dynamic不可写, 前面的_dl_runtime_resolve在第二步时
.rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
这个时候,_dl_runtime_resolve并没有检查.rel.plt + 第二个参数后是否造成越界访问,所以我们能给一个很大的.rel.plt的offset(64位的话就是下标),然后使得加上去之后的地址指向我们所能操纵的一块内存空间,比方说.bss。
然后第三步
rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
所以在我们所伪造的Elf32_Rel,需要放一个r_info字段,大概长这样就行0xXXXXXX07,其中XXXXXX是相对.dynsym表的下标,注意不是偏移,所以是偏移除以Elf32_Sym的大小,即除以0x10(32位下)。然后这里同样也没有进行越界访问的检查,所以可以用类似的方法,伪造出这个Elf32_Sym。至于为什么是07,因为这是一个导入函数,而导入函数一般都是07,所以写成07就好。
然后第四步
.dynstr + sym->st_name得出符号名字符串指针
同样类似,没有进行越界访问检查,所以这个字符串也能够伪造。
我们需要做的就是在使调用函数的整个过程被我们所控制,首先劫持栈:
1 | payload+= p32(pop_rbp) + p32(mystack) + p32(leave_ret) |
然后需要在栈上布置这种结构:
1 | payload = p32(mystack) |
之后我们要做的事分三步:
1.伪造fake_index来使程序跳入我们自己的fake_rel结构体
2.构造fake_rel的r_info来使程序跳到我们自己的fake_sym结构体 (这里需要我们自己来构造字节对齐。)
3.构造fake_sym结构体的st_name来使程序跳到我们自己的fake_str字符串。
其中fake_index,fake_rel,fake_sym,fake_str的地址都需要我们自己能够精确地控制。(栈注意迁移即可)
exp
1 | #coding:utf-8 |