源码2.27 先看两个exit.h中的重要结构体1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 struct exit_function { long int flavor; union //一个联合体func { void (*at) (void ); struct { void (*fn) (int status, void *arg); void *arg; } on; struct { void (*fn) (void *arg, int status); void *arg; void *dso_handle; } cxa; } func; }; struct exit_function_list { struct exit_function_list *next ; size_t idx; struct exit_function fns [32]; };
首先是exit的源码
1 2 3 4 5 6 void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true , true ); } libc_hidden_def (exit )
libc经典的套娃函数,可以看出exit的主体是__run_exit_handlers
run_exit_handlers()的主要工作就是调用exit_funcs中保存的各种函数指针
看其工作流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 void attribute_hidden __run_exit_handlers(int status, struct exit_function_list **listp, bool run_list_atexit){ #ifndef SHARED if (&__call_tls_dtors != NULL )#endif __call_tls_dtors(); while (*listp != NULL ){ struct exit_function_list *cur = *listp; while (cur->idx > 0 ) { const struct exit_function *const f = &cur->fns[--cur->idx]; switch (f->flavor) { void (*atfct)(void ); void (*onfct)(int status, void *arg); void (*cxafct)(void *arg, int status); case ef_free: case ef_us: break ; case ef_on: onfct = f->func.on.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE(onfct); #endif onfct(status, f->func.on.arg); break ; case ef_at: atfct = f->func.at; #ifdef PTR_DEMANGLE PTR_DEMANGLE(atfct); #endif atfct(); break ; case ef_cxa: cxafct = f->func.cxa.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE(cxafct); #endif cxafct(f->func.cxa.arg, status); break ; } } *listp = cur->next; if (*listp != NULL ) free (cur); } if (run_list_atexit) RUN_HOOK(__libc_atexit, ()); _exit(status); }
思考1:能否劫持__exit_funcs数组? 在exit调用run_exit_handlers()时下断点, 找到 exit_funcs指针
可以看到其中最重要的fns[0]被加密成乱码了,要想利用的话还要获得存储在fs:0x30的密钥,难度高,几乎难以利用
__exit_funcs如何添加析构函数() 既然难以攻击exit_funcs, 那么尝试从 exit_funcs中的函数入手
我们首先要弄明白, __exit_funcs中的函数是怎么添加的
libc提供了一个接口: atexit()用来注册exit()时调用的析构函数
1 2 3 4 5 6 7 8 extern void *__dso_handle __attribute__((__weak__));int atexit (void (*func)(void )) { return __cxa_atexit((void (*)(void *))func, NULL , &__dso_handle == NULL ? NULL : __dso_handle); }
cxa_atexit()是对internal_atexit()的封装
注意: __exit_funcs就是exit()时用的那个指针
1 2 3 4 5 6 int __cxa_atexit(void (*func)(void *), void *arg, void *d){ return __internal_atexit(func, arg, d, &__exit_funcs); } libc_hidden_def(__cxa_atexit)
internel_atexit()通过 new_exitfn()找到一个在__exit_funcs链表上注册析构函数的位置, 然后进行写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int attribute_hidden __internal_atexit(void (*func)(void *), void *arg, void *d, struct exit_function_list **listp){ struct exit_function *new = __new_exitfn(listp); if (new == NULL ) return -1 ; #ifdef PTR_MANGLE PTR_MANGLE(func); #endif new->func.cxa.fn = (void (*)(void *, int ))func; new->func.cxa.arg = arg; new->func.cxa.dso_handle = d; atomic_write_barrier(); new->flavor = ef_cxa; return 0 ; }
__new_exitfn()的逻辑大致为
先尝试在__exit_funcs中找到一个exit_function类型的ef_free的位置, ef_free代表着此位置空闲
如果没找到, 就新建一个exit_function节点, 使用头插法插入__exit_funcs链表, 使用新节点的第一个位置作为分配到的exit_function结构体
设置找到的exit_function的类型为ef_us, 表示正在使用中, 并返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 struct exit_function *__new_exitfn (struct exit_function_list **listp ){ struct exit_function_list *p = NULL ; struct exit_function_list *l ; struct exit_function *r = NULL ; size_t i = 0 ; __libc_lock_lock(lock); for (l = *listp; l != NULL ; p = l, l = l->next) { for (i = l->idx; i > 0 ; --i) if (l->fns[i - 1 ].flavor != ef_free) break ; if (i > 0 ) break ; l->idx = 0 ; } if (l == NULL || i == sizeof (l->fns) / sizeof (l->fns[0 ])) { if (p == NULL ) { assert(l != NULL ); p = (struct exit_function_list *)calloc (1 , sizeof (struct exit_function_list)); if (p != NULL ) { p->next = *listp; *listp = p; } } if (p != NULL ) { r = &p->fns[0 ]; p->idx = 1 ; } } else { r = &l->fns[i]; l->idx = i + 1 ; } if (r != NULL ) { r->flavor = ef_us; ++__new_exitfn_called; } __libc_lock_unlock(lock); return r; }
析构函数的注册—__libc_start_main() __libc_start_main() 函数初窥
首先是其参数列表也就是_start()传递的参数, 我们中重点注意下面三个
init: ELF文件 也就是main()的构造函数
fini: ELF文件 也就是main()的析构函数
rtld_fini: 动态链接器的析构函数
1 2 3 4 5 6 7 8 9 10 11 12 static int __libc_start_main( int (*main)(int , char **, char **MAIN_AUXVEC_DECL), int argc, char **argv, ElfW(auxv_t ) * auxvec, __typeof(main) init, void (*fini)(void ), void (*rtld_fini)(void ), void *stack_end ) { ...函数体; }
进入函数体, __libc_start_mian()主要做了以下几件事
为libc保存一些关于main的参数, 比如__environ…
通过atexit()注册fini 与 rtld_fini 这两个参数
调用init为main()进行构造操作
然后调用main()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static int __libc_start_main(...){ int result; char **ev = &argv[argc + 1 ]; __environ = ev; __libc_stack_end = stack_end; ...; __pthread_initialize_minimal(); uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard(_dl_random); uintptr_t pointer_chk_guard = _dl_setup_pointer_guard(_dl_random, stack_chk_guard); ...; if (__glibc_likely(rtld_fini != NULL )) __cxa_atexit((void (*)(void *))rtld_fini, NULL , NULL ); __libc_init_first(argc, argv, __environ); if (fini) __cxa_atexit((void (*)(void *))fini, NULL , NULL ); if (init) (*init)(argc, argv, __environ MAIN_AUXVEC_PARAM); ...; result = main(argc, argv, __environ MAIN_AUXVEC_PARAM); exit (result); }
至此我们知道libc_start_mian()会在exit_funcs中放入下面两个函数
ELF的fini函数 和ld的rtld_fini函数
然后会调用一个构造函数:
init()
ELF的fini() 被编译在elf的text段中, 由_start()传递地址给__libc_start_main()
发现其是一个空函数,因为其只有在静态编译下才会起作用,故而动态编译该函数为空
静态编译时:该函数会逐一取出fini_array数组中的函数指针执行,该函数指针数组位于bss段上
ELF的init() 让我们思考一个问题: 如果只有fini与init的话, ELF只能有一个构造/ 析构函数
当具有多个构造析构函数时改怎么办呢?
ELF的解决方法是, 把所有的构造函数的指针放在一个段: .init_array中, 所有的析构函数的指针放在一个段 .fini_array中
init()就负责遍历.init_array, 并调用其中的构造函数, 从而完成多个构造函数的调用
ld的rtdl_fini() 我们说完了.init_array, 那么对于.fini_array呢?
很明显不是ELF的fini()负责 , 因为他就是一个空函数, 那么就只能由rtdl_fini来负责
rtdl_fini实际指向_dl_fini()函数 , 源码在dl-fini.c文件中, 会被编译到ld.so.2中
我们把进程空间中的一个单独文件, 称之为模块
ld.so.2会通过dl_open()把所需文件到进程空间中, 他会把所有映射的文件都记录在结构体_rtld_global中
当一个进程终止, ld.so.2自然需要卸载所映射的模块, 这需要调用每一个非共享模块的fini_arrary段中的析构函数
一言以蔽之: _dl_fini()的功能就是调用进程空间中所有模块的析构函数
rtld_global结构体 接着来看_rtld_global结构体, 这个结构体很复杂, 我们只看与本文相关的
_rtld_global一般通过宏GL来引用, 这个结构体定义在ld.so.2的data段中
1 2 3 #define GL(name) _rtld_global._##name extern struct rtld_global _rtld_global __rtld_global_attribute__ ;
再看其结构体struct rtld_global的定义
一些缩写的含义:
ns代表着NameSpace
nns代表着Num of NameSpace
struct rtld_global先以命名空间为单位建立了一个数组 _dl_ns[DL_NNS]
在每个命名空间内部加载的模块以双向链表组织, 通过_ns_loaded索引
同时每个命名空间内部又有一个符号表_ns_unique_sym_table, 记录着所有模块导出的符号集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 struct rtld_global { #define DL_NNS 16 struct link_namespaces { struct link_map *_ns_loaded ; unsigned int _ns_nloaded; struct r_scope_elem *_ns_main_searchlist ; size_t _ns_global_scope_alloc; struct unique_sym_table { __rtld_lock_define_recursive(, lock) struct unique_sym { uint32_t hashval; const char *name; const ElfW (Sym) * sym; const struct link_map *map ; } * entries; size_t size; size_t n_elements; void (*free )(void *); } _ns_unique_sym_table; struct r_debug _ns_debug ; } _dl_ns[DL_NNS]; size_t _dl_nns; ...; }
接着我们分析下struct link_map, 来看看ld是怎么描述每一个模块的
ELF文件都是通过节的组织的, ld自然也延续了这样的思路,
l_info中的指针都指向ELF中Dyn节中的描述符, Dyn中节描述符类型是ElfW(Dyn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) * l_ld; struct link_map *l_next , *l_prev ; struct link_map *l_real ; Lmid_t l_ns; struct libname_list *l_libname ; ElfW(Dyn) * l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; const ElfW (Phdr) * l_phdr; ElfW(Addr) l_entry; ElfW(Half) l_phnum; ElfW(Half) l_ldnum; ...; }
ElfW(Dyn)是一个节描述符类型(也就是一个宏), 宏展开结果为Elf64_Dyn , 这个类型被定义在elf.h文件中, 与ELF中的节描述对应
这个结构体在elf的学习中很重要
1 2 3 4 5 6 7 8 9 typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
至此rtld_global的结构就清楚了, 他自顶向下按照: 命名空间->模块->节 的形式描述所有的模块, 通过_ns_unique_sym_table描述命名空间中所有的可见符号
_dl_fini()源码分析 理解了模块是如何组织的之后, _dl_fini的任务就显而易见了:
遍历rtld_global中所有的命名空间
遍历命名空间中所有的模块
找到这个模块的fini_array段, 并调用其中的所有函数指针
找到这个模块的fini段, 调用fini()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 void internal_function _dl_fini(void ){ #ifdef SHARED int do_audit = 0 ; again: #endif for (Lmid_t ns = GL(dl_nns) - 1 ; ns >= 0 ; --ns) { __rtld_lock_lock_recursive(GL(dl_load_lock)); unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded; if (nloaded == 0 || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit) __rtld_lock_unlock_recursive(GL(dl_load_lock)); else { struct link_map *maps[nloaded]; unsigned int i; struct link_map *l ; assert(nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL ); for (l = GL(dl_ns)[ns]._ns_loaded, i = 0 ; l != NULL ; l = l->l_next) if (l == l->l_real) { assert(i < nloaded); maps[i] = l; l->l_idx = i; ++i; ++l->l_direct_opencount; } ...; unsigned int nmaps = i; _dl_sort_fini(maps, nmaps, NULL , ns); __rtld_lock_unlock_recursive(GL(dl_load_lock)); for (i = 0 ; i < nmaps; ++i) { struct link_map *l = maps[i]; if (l->l_init_called) { l->l_init_called = 0 ; if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL ) { if (__builtin_expect(GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0 )) _dl_debug_printf("\ncalling fini: %s [%lu]\n\n" ,DSO_FILENAME(l->l_name),ns); if (l->l_info[DT_FINI_ARRAY] != NULL ) { ElfW(Addr) *array = (ElfW(Addr) *)(l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr))); while (i-- > 0 ) ((fini_t )array [i])(); } if (l->l_info[DT_FINI] != NULL ) DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr); } ...; } --l->l_direct_opencount; } } } ...; }
思考2:rtdl_fini()带来的可利用点 rtdl_fini()十分依赖与rtld_global这一数据结构, 并且rtld_global中的数据并没有被加密 , 这就带来了两个攻击面
劫持rtld_global中的锁相关函数指针
修改rtld_global中的l_info, 伪造fini_array/ fini的节描述符, 从而劫持fini_array/ fini到任意位置, 执行任意函数
0x1劫持rtld_global中的函数指针 ld相关函数在使用rtld_global时都需要先上锁, 以避免多进程下的条件竞争问题
相关函数包括但不限于:
上锁操作是通过宏进行的, 宏定义:
1 2 # define __rtld_lock_lock_recursive(NAME) GL(dl_rtld_lock_recursive) (&(NAME).mutex) # define GL(name) _rtld_global._##name
查看宏GL
的定义:
1 2 3 4 5 # if IS_IN (rtld) # define GL(name) _rtld_local._##name # else # define GL(name) _rtld_global._##name # endif
宏展开:
1 2 3 __rtld_lock_lock_recursive(GL(dl_load_lock)); => GL(dl_rtld_lock_recursive) (& GL(dl_load_lock).mutex) => _rtld_global.dl_rtld_lock_recursive(&_rtld_global.dl_load_lock.mutex)
可以看到实际调用的是dl_rtld_lock_recursive函数指针
释放锁的操作也是类似的, 调用的是_dl_rtld_unlock_recursive函数指针, 这两个函数指针再rtld_global中定义如下
1 2 3 4 5 6 7 struct rtld_global { ...; void (*_dl_rtld_lock_recursive)(void *); void (*_dl_rtld_unlock_recursive)(void *); ...; }
ld作为mmap的文件, 与libc地址固定
也就是说, 当有了任意写+libc地址后, 我们可以通过覆盖_rtld_global中的lock/ unlock函数指针来getshell
0x2劫持l_info伪造fini_array节 我们的目标是伪造rtld_global中关于fini_array节与fini_arraysize节的描述
将fini_array节迁移到一个可控位置, 比如堆区, 然后在这个可控位置中写入函数指针, 那么在exit()时就会依次调用其中的函数指针
l_info中关于fini_array节的描述符下标为26, 关于fini_arraysz节的下标是28,l_info中的指针正好指向的就是Dynamic段中相关段描述符
此时我们就可以回答ELF中fini_array中的析构函数是怎么被调用的这个问题了:
exit()调用__exit_funcs链表中的_rtdl_fini()函数, 由_rtdl_fini()函数寻找到ELF的fini_array节并调用
假设我们修改rtld_global中的l_info[0x1a]为addrA, 修改l_info[0x1c]为addrB
那么首先在addrA addrB中伪造好描述符
1 2 addrA: flat(0x1a, addrC) addrB: flat(0x1b, N)
然后在addrC中写入函数指针就可以在exit时执行了
0x3fini_array与ROP(SROP) 当我们可以劫持fini_array之后, 我们就具备了连续调用多个函数的能力, 那么有无可能像ROP一样, 让多个函数进行组合, 完成复杂的工作?
多个fini_array函数调用之间, 寄存器环境十分稳定, 只有: rdx r13会被破坏, 这是一个好消息
考察执行call时的栈环境, 我们发现rdi总是指向一个可读可写区域, 可以当做我们函数的缓冲区
那么就已经有了大致的利用思路,
让fini_array先调用gets()函数, 在rdi中读入SigreturnFrame
然后再调用setcontext+53, 即可进行SROP, 劫持所有寄存器
如果高版本libc, setcontext使用rdx作为参数, 那么在gets(rdi)后还需要一个gadget, 能通过rdi设置rdx, 再执行setcontext
0x4劫持fini fini段在l_info中下标为13,这个描述符中直接放的就是fini函数指针(前面有提到动态链接下这是个空函数,由_dl_fini调用), 利用手法较为简单, 但是只能执行一个函数, 通常设置为onegadget
例如我们可以修改rtld_global中l_info[0xd]为addrA, 然后再addrA中写入
1 addrA: flat(0xd, onegadget)
就可以在exit()时触发
0x5exit()与FILE 一开始的run_exit_handlers么, 在遍历完exit_funcs链表后, 还有最后一句
1 2 if (run_list_atexit) RUN_HOOK(__libc_atexit, ());
__libc_atexit其实是libc中的一个段
这个段中就是libc退出时的析构函数
其中默认只有一个函数fcloseall()
这个函数会调用_IO_cleanup()
1 2 3 4 5 int __fcloseall (void ){ 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)即无缓冲模式, 来释放流的缓冲区
1 2 3 4 5 6 7 8 9 10 int _IO_cleanup(void ){ int result = _IO_flush_all_lockp(0 ); _IO_unbuffer_all(); return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (_IO_FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL ; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif return result; } static void _IO_unbuffer_all (void ) { struct _IO_FILE *fp ; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (_IO_FILE *) _IO_list_all; fp; fp = fp->_chain) { if (! (fp->_flags & _IO_UNBUFFERED) && fp->_mode != 0 ) { #ifdef _IO_MTSAFE_IO int cnt; #define MAXTRIES 2 for (cnt = 0 ; cnt < MAXTRIES; ++cnt) if (fp->_lock == NULL || _IO_lock_trylock (*fp->_lock) == 0 ) break ; else __sched_yield (); #endif if (! dealloc_buffers && !(fp->_flags & _IO_USER_BUF)) { fp->_flags |= _IO_USER_BUF; fp->_freeres_list = freeres_list; freeres_list = fp; fp->_freeres_buf = fp->_IO_buf_base; } _IO_SETBUF (fp, NULL , 0 ); if (fp->_mode > 0 ) _IO_wsetb (fp, NULL , NULL , 0 ); #ifdef _IO_MTSAFE_IO if (cnt < MAXTRIES && fp->_lock != NULL ) _IO_lock_unlock (*fp->_lock); #endif } fp->_mode = -1 ; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif }
发现攻击点:
libc2.23以前三个标准流的vtable是可写的,可以修改函数指针
之后的版本因为位于libc段中的vtable是无法写入的,故可以选择伪造vtable中的setbuf或overflow(其中overflow需要达到一些条件)函数,来达到getshell
例题 2018hctf-the_end 这道题有两种解法,但都是利用exit函数