exit

[TOC] exit_hook攻击 ​glibc-2.34​ 后失效

区分_exit()与exit()

  • _exit()就是一个简单的系统调用syscall
#include <unistd.h> 
void _exit(int status);

  • exit() => 进行用户层面的资源析构 + 调用_exit()进行系统级别的析构 libc将负责这个工作的函数定义为exit(), 其声明如下
#include <stdlib.h> 
extern void exit (int __status);

exit()源码分析

  • 在pwn中, _exit()是无法利用的(但是可以用assert), 但是exit()是有很多攻击点的, 因此本文会着重分析libc中exit()函数实现, 相关机制, 及其利用手法
  • exit()为libc中定义的函数, 是对__run_exit_handlers()的包装
void exit(int status)
{
  //退出时调用__exit_funcs链表中的函数,__exit_funcs位于libc的.data段
  __run_exit_handlers(status, &__exit_funcs, true); 
}
  • 其中有一个重要的数据结构:__exit_funcs, 是一个指针, 指向 /保存析构函数的数组链表/, 其定义如下
static struct exit_function_list initial;           //initial定义在libc的可写入段中
struct exit_function_list *__exit_funcs = &initial; //exit函数链表
  • exit_function_list结构体定义, 里面保存了多个析构的函数的描述
struct exit_function_list
{
  struct exit_function_list *next; //单链表, 指向下一个exit_function_list结构体
  size_t idx;                      //记录有多少个函数
  struct exit_function fns[32];    //析构函数数组
};
  • struct exit_function是对单个析构函数的描述, 可以描述多种析构函数类型
//描述单个析构函数的结构体
struct exit_function
{
    long int flavor; 
    /*
    函数类型, 可以是{ef_free, ef_us, ef_on, ef_at, ef_cxa}
        - ef_free表示此位置空闲
        - ef_us表示此位置被使用中, 但是函数类型不知道
        - ef_on, ef_at, ef_cxa 分别对应三种不同的析构函数类型, 主要是参数上的差异
    */

    union            //多个种函数类型中只会有一个有用, 所以是联合体
    {
        void (*at)(void); //ef_at类型 没有参数
        struct
        {
            void (*fn)(int status, void *arg);
            void *arg;
        } on; //ef_on类型
        struct
        {
            void (*fn)(void *arg, int status);
            void *arg;
            void *dso_handle;
        } cxa; //ef_cxa类型
    } func;
};

相关知识点

  1. __exit_funcs如何添加析构函数()

    • atexit()用来注册exit()时调用的析构函数
  2. ELF的入口点_start() __libc_start_main()

    • __libc_start_mian()主要做了以下几件事 为libc保存一些关于main的参数, 比如__environ… 通过atexit()注册fini 与 rtld_fini 这两个参数 调用init为main()进行构造操作 然后调用main()函数
    • 最关键的是 result = main(argc, argv, __environ MAIN_AUXVEC_PARAM); /* 如果main()返回后, __libc_start_main()回帮他调用exit()函数 */ exit(result);
  3. rtld_global结构体

    • 命名空间->模块->节
  4. _dl_fini()

    • _dl_fini的任务就显而易见了: 1.遍历rtld_global中所有的命名空间 2.遍历命名空间中所有的模块 3.找到这个模块的fini_array段, 并调用其中的所有函数指针 4.找到这个模块的fini段, 调用
  5. 还有许多相关知识看参考博客即可

利用方式

rtdl_fini()十分依赖与rtld_global这一数据结构, 并且rtld_global中的数据并没有被加密, 这就带来了两个攻击面

  • 劫持rtld_global中的锁相关函数指针
  • 修改rtld_global中的l_info, 伪造fini_array/ fini的节描述符, 从而劫持fini_array/ fini到任意位置, 执行任意函数

劫持锁相关函数

释放锁的操作也是类似的, 调用的是_dl_rtld_unlock_recursive函数指针, 这两个函数指针再rtld_global中定义如下 并且ld作为mmap的文件, 与libc地址固定.也就是说, 当有了任意写+libc地址后, 我们可以通过覆盖_rtld_global中的lock/ unlock函数指针来getshell

struct rtld_global
{
    ...;
    void (*_dl_rtld_lock_recursive)(void *);
    void (*_dl_rtld_unlock_recursive)(void *);
    ...;
}

一次任意写:直接将rtld_lock_default_lock_recursive修改成one_gadget

两次任意写:修改_rtld_global+2312可以控制rdi,任意写将rtld_lock_default_lock_recursive修改成后门函数即可

  • 攻击代码
ld_base = libc_base+0x213000
_rtld_global = ld_base + ld.sym['_rtld_global']
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _rtld_global + 0xf10

劫持l_info伪造fini_array节

我们的目标是伪造rtld_global中关于fini_array节与fini_arraysize节的描述

将fini_array节迁移到一个可控位置, 比如堆区, 然后在这个可控位置中写入函数指针, 那么在exit()时就会依次调用其中的函数指针

l_info中关于fini_array节的描述符下标为26, 关于fini_arraysz节的下标是28, 我们动态调试一下, 看一下具体内容 此时我们就可以回答ELF中fini_array中的析构函数是怎么被调用的这个问题了: 这里阐述一下如何找偏移,因为rtld_global结构体里面套结构体,很难数出偏移。但是我们可以直接search -p 0x600e18,这样就可以看出偏移 exit()调用__exit_funcs链表中的_rtdl_fini()函数, 由_rtdl_fini()函数寻找到ELF的fini_array节并调用 假设我们修改rtld_global中的l_info[0x1a]为addrA, 修改l_info[0x1c]为addrB

那么首先再addrA addrB中伪造好描述符

addrA: flat(0x1a, addrC) addrB: flat(0x1b, N) 然后在addrC中写入函数指针就可以在exit时执行了

fini_array与ROP

我们首先需要从汇编层面考察下fini_array中的函数是怎么被遍历并调用的, 因为这涉及到参数传递问题 我们可以看到在多个fini_array函数调用之间, 寄存器环境十分稳定, 只有: rdx r13会被破坏, 这是一个好消息

考察执行call时的栈环境, 我们发现rdi总是指向一个可读可写区域, 可以当做我们函数的缓冲区

那么就已经有了大致的利用思路,

我们让fini_array先调用gets()函数, 在rdi中读入SigreturnFrame

然后再调用setcontext+53, 即可进行SROP, 劫持所有寄存器

如果高版本libc, setcontext使用rdx作为参数, 那么在gets(rdi)后还需要一个GG, 能通过rdi设置rdx, 再执行setcontext

劫持fini

fini段在l_info中下标为13,这个描述符中直接放的就是函数指针, 利用手法较为简单, 但是只能执行一个函数, 通常设置为OGG 例如我们可以修改rtld_global中l_info[0xd]为addrA, 然后再addrA中写入

addrA: flat(0xd, OGG) 就可以在exit()时触发OGG

exit()与FILE

还记得一开始的run_exit_handlers么, 在遍历完exit_funcs链表后, 还有最后一句

if (run_list_atexit) //调用_atexit RUN_HOOK(__libc_atexit, ());

__libc_atexit其实是libc中的一个段

这个函数会调用_IO_cleanup()

int __fcloseall (void)
{
/* Close all streams.  */
return _IO_cleanup ();
}

_IO_cleanup()会调用两个函数 _IO_flush_all_lockp()会通过_IO_list_all遍历所有流, 对每个流调用_IO_OVERFLOW(fp), 保证关闭前缓冲器中没有数据残留 _IO_unbuffer_all()会通过_IO_list_all遍历所有流, 对每个流调用_IO_SETBUF(fp, NULL, 0), 来释放流的缓冲区 更多内容就是IO_FILE攻击的内容了

int _IO_cleanup(void)
{
  /* 刷新所有流 */
  int result = _IO_flush_all_lockp(0);

  /* 关闭所有流的缓冲区 */
  _IO_unbuffer_all();

  return result;
}

libc2.35的exit利用

直接看这篇 参考博客