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;
};
相关知识点
__exit_funcs如何添加析构函数()
- atexit()用来注册exit()时调用的析构函数
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);
rtld_global结构体
- 命名空间->模块->节
_dl_fini()
- _dl_fini的任务就显而易见了: 1.遍历rtld_global中所有的命名空间 2.遍历命名空间中所有的模块 3.找到这个模块的fini_array段, 并调用其中的所有函数指针 4.找到这个模块的fini段, 调用
还有许多相关知识看参考博客即可
利用方式
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利用
直接看这篇 参考博客