一次性栈上格式化字符串漏洞利用研究(2026楚慧杯house_1)
字数 2858
更新时间 2026-03-18 13:13:57

一次性栈上格式化字符串漏洞利用详解

——基于2026楚慧杯house_1题目的非预期解法

1. 题目与背景

  • 赛题名称: 2026楚慧杯house_1
  • 目标文件: house (挑战程序),附带libc.so.6库文件。
  • 安全保护: 程序为64位,开启了全保护 (FULL RELRO, Canary, NX, PIE等),属于典型的高保护级别环境。
  • 核心挑战: 在栈上存在一个长度受限的格式化字符串漏洞,并且程序的返回地址非libc基址。需要在一次利用中,通过格式化字符串修改栈上的返回地址,实现控制流劫持。

2. 程序功能与漏洞点分析

程序主要包含以下几个函数:

  • add函数:固定申请0x100字节的堆块。malloc(0x100u);

  • show函数:存在格式化字符串漏洞的核心函数。

    unsigned __int64 show()
    {
      char buf[56]; // [rsp+0h] [rbp-40h] BYREF
      unsigned __int64 v2; // [rsp+38h] [rbp-8h]
      v2 = __readfsqword(0x28u);
      puts("Please write your name:");
      read(0, buf, 0x30u); // 读取最多0x30(48)字节到buf
      puts("the name is:");
      printf(buf); // 格式化字符串漏洞点
      return __readfsqword(0x28u) ^ v2;
    }
    
    • 漏洞利用缓冲区buf大小为56字节,但read最多可读入48字节,未发生栈溢出。
    • 关键点: printf的参数buf是用户可控的字符串,若其中包含%x, %p, %n等格式化符,可造成信息泄露或任意地址写。
  • edit函数

    _BYTE *edit()
    {
      _BYTE buf[72]; // [rsp+0h] [rbp-50h] BYREF
      unsigned __int64 v2; // [rsp+48h] [rbp-8h]
      v2 = __readfsqword(0x28u);
      puts("Please write your content");
      read(0, buf, nbytes); // nbytes是一个全局变量
      return buf;
    }
    
    • 文档提到预期解可能是通过修改全局变量nbytes来制造堆或栈溢出,但本解法采用了非预期路径。
  • show_函数:辅助打印函数。printf("content :%s\n", a1);

3. 漏洞利用思路与约束

在保护全开且输入长度有限(48字节)的情况下,常规的格式化字符串利用手段受限:

  1. 无法修改GOT表:因为开启了FULL RELRO,GOT表不可写。
  2. 利用目标:栈上储存的返回地址。目标是将其修改为one_gadget地址,从而获取shell。
  3. 核心难点:返回地址位于栈上,但其值并非libc基址,而是程序的PIE地址。因此,在单次格式化字符串调用中,需要精确计算并一次性(或最少次数)将返回地址修改为libc中的one_gadget地址。

4. 利用步骤详解

4.1 信息泄露

首先,需要通过格式化字符串漏洞泄露必要的地址:

  1. 泄露libc基址:通过%p等格式化符,从栈上或寄存器中打印出libc函数的地址(如__libc_start_main的返回地址),从而计算出libc的基址。
  2. 泄露栈地址:通过%p泄露栈上的某个地址(如保存的RBP值),用于定位和计算目标返回地址在栈上的具体位置。

4.2 确定利用目标与栈布局

通过调试分析栈布局,找到包含返回地址的栈位置。从文档中给出的栈布局片段可知:

05:0028│-018 0x7ffc4960dcc8 —▸ 0x7ffc4960de00 ◂— 1
06:0030│-010 0x7ffc4960dcd0 ◂— 0
07:0038│-008 0x7ffc4960dcd8 ◂— 0x23b81f8d5ab36600
pwndbg>
08:0040│ rbp 0x7ffc4960dce0 —▸ 0x7ffc4960dd10 ◂— 0
09:0048│+008 0x7ffc4960dce8 —▸ 0x59779979947c ◂— nop
  • 偏移+008处(0x7ffc4960dce8)储存的0x59779979947c即为函数的返回地址(PIE地址)。
  • 利用目标:修改0x7ffc4960dce8处的值为one_gadget地址。

4.3 One_gadget约束条件

通过one_gadget工具分析题目附带的libc.so.6,得到三个可能的gadget:

0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL || rsi is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

需要根据程序执行到返回时的寄存器状态,选择一个约束条件能被满足的gadget。利用者通过调试确认了其中一个one_gadget可以直接使用。

4.4 格式化字符串写操作

这是利用的核心技术环节。目标是将一个8字节的one_gadget地址写入栈上指定位置。

  1. 使用%n系列格式化符%n将其出现位置之前已输出的字符数写入对应的指针参数。%hn(写2字节)、%hhn(写1字节)可以用于更精细的写入。
  2. 分块写入:由于one_gadget地址值较大,通常需要分成多个小块(如2字节)进行写入,以控制每次写入的数值,避免输出过多的字符导致缓冲区或长度限制问题。
  3. 参数定位:通过泄露的栈地址,计算出目标返回地址所在内存位置相对于格式化字符串参数在栈上的偏移。例如,通过类似%X$p的方式,确认目标地址是格式化字符串的第几个参数。
  4. 地址对齐与避免空字节
    • 在64位系统中,地址通常有高位0x00字节,printf遇到\x00会终止字符串。需要将地址放在格式化字符串的末尾,或者利用格式化字符串的特性(如通过%c等产生数值)来间接构造地址,避免直接包含空字节。
    • 需要调整写入顺序,避免先写入的低位字节值过小,导致后续写入高位时,因累计输出字符数(即要写入的值)小于已有值而无法覆盖(即“自我覆写”问题)。
  5. 数值计算:利用%<num>c来精确控制输出的字符数,从而控制%hn%hhn写入的数值。计算每次需要写入的2字节或1字节的目标值,并根据之前已输出的字符数,计算%<num>c中的<num>

4.5 利用流程总结

  1. 触发show函数,构造格式化字符串泄露libc基址和栈地址。
  2. 计算one_gadget的绝对地址。
  3. 计算目标返回地址在栈上的位置(相对于格式化字符串参数的偏移)。
  4. 精心构造一个不超过48字节的格式化字符串payload。这个payload需要:
    • 包含指向目标地址(及相邻地址)的指针。
    • 包含一系列%<num>c%X$hnX为偏移)的组合,分批次将one_gadget地址的各个部分(例如低4位和高4位,或分4个2字节)写入目标内存。
    • 妥善处理地址中的空字节和写入顺序,确保每次写入都能成功将目标内存修改为正确值。
  5. 再次触发show函数,输入构造好的payload,一次性完成对返回地址的修改。
  6. 函数返回时,跳转到one_gadget,获取shell。

5. 关键技巧与注意事项

  • 长度极限利用:在48字节的限制下,每一个字节都需要精打细算,格式化字符串的构造需要高度优化。
  • 偏移优先级:当使用多个%n写操作时,printf处理参数的顺序是固定的。需要根据这个顺序来安排指针参数在payload中的位置和写入的顺序,避免相互干扰。
  • 地址放置:将需要写入的目标地址(指针)放在格式化字符串的合适位置,使其能被正确的%X$n引用。
  • 概率性成功:文档指出,此利用有成功率限制。成功率条件是:one_gadget地址的低6位数值的最高位,需要小于高6位数值的最高位。这与分块写入时的进位处理有关。满足此条件时,构造的payload才能确保写入顺序正确,避免数值覆盖问题。

6. 总结

本题的解法展示了一次性利用栈上格式化字符串漏洞的高阶技巧。其核心在于在极度受限的条件下(全保护、长度限制、返回地址非libc地址),通过精确计算栈布局、巧妙构造格式化字符串,利用%n系列格式化符分块覆写返回地址为one_gadget。这种方法绕过了修改GOT、ROP等常规手段,直接针对栈上的控制流信息进行修改,是格式化字符串漏洞利用中一种精妙且具有挑战性的方式。成功利用需深刻理解栈内存布局、printf函数对格式化字符串的解析机制以及%n写操作的细节。

相似文章
相似文章
 全屏