fmt

2025-03-23

1. Format Strings(格式化字符串)

1.1 printf

以常见的c语言函数printf为例,printf会接受不同数量的参数:

printf("Hello!\n");
printf("Hello %s!\n", name);
printf("There are %d lights!", 5);
printf("The average of %d, %d, and %d is %f", 1, 2, 3, float(1+2+3)/3.0);

printf知道自己到底有多少参数的能力,就体现格式化字符串(format strings)上,即printf接受的第一个参数
格式化字符串中的类似%d %s %f中出现的数量,就代表后面有几个参数。

1.2 printf argument locations

已知printf根据格式化字符串中类似%d %s %f的出现的数量来得知后面的参数,那么参数实际的位置又在哪里呢?
在x64架构中,参数一部分在寄存器当中,还有相当一部分在栈中

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

int main(){
    int a =1;
    int b = 2;
    int c = 3;
    int d = 4;
    int e = 5;
    int f = 6;
    int g = 7;
    printf("%d %d %d %d %d %d %d\n",a,b,c,d,e,f,g);
}

2. Leaking Data

格式化字符串漏洞非常强力,能够让你做很多事情,比如任意地址读和任意地址写。
以printf为例,我们可以通过下面几个来泄露栈中的内容。

%c: read a char off the stack
%d, %i, %x: read an int (4 bytes) off the stack
%x: read an int (4 bytes) in hex
%s: dereference a pointer and read out bytes until a null byte
%p: read an 8 bytes int hex 

控制泄露的字节大小:
%x leaks 4 bytes
%hx leaks 2 bytes
%hhx leaks 1 byte
%lx leaks 8 bytes

控制泄露第几个参数的值:
%7$x - print the 7th parameter (on the stack)

举个例子:

#include <stdio.h>
#include <string.h>
int main(int argc, char**argv){
    char * fmt_string[256];
    char * secret_value = "flag{wsxkwsxk}";
    strcpy(fmt_string,argv[1]);
    strcat(fmt_string,"\n");
    printf(fmt_string,0xdeadbeefabcd);
}

这里用''号是为了防止跟bash读取相冲突。

3. Writing Data

开发者发明了%n符号,用于解引用存放在栈中的指针变量,将目前已经打印出的字节数量写入指针所指向的内存.

%n: 写入4字节到指针指向的内存中
%ln:8字节
%hn:2字节
%hhn:1字节

举个例子:

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

char * secret_value = "flag{wsxkwsxk}";

int main(int argc, char**argv){
    char * fmt_string[256];
    int * print_flag = malloc(sizeof(int));
    *print_flag = 0;

    strcpy(fmt_string,argv[1]);
    strcat(fmt_string,"\n");
    printf(fmt_string,0xdeadbeefabcd);
    if (*print_flag){
        printf("%s",secret_value);
    }
}

除去rsi rdx rcx r8 r9外,申请的内存在栈中的第四个位置,所以是%9$n
这里也可以看出,%n符号要求有一个指针变量,指向我们想要写的内存,有几个办法可以得到它:

1. 如果我们格式化字符串的buffer位于栈上,我们可以在输入format string的时候,把我们想要写的内存地址也写入。

2. %7$n (and other offsets) ,直接使用栈上本来就有的指针

3. 使用frame指针,即存放原本rbp的值的位置。这是第2种情况的特例。

对于第1种情况,它的栈分布往往是这样的:
这意味着我们可以直接在格式化字符串里写下我们想要写入的内存地址
对于第3种情况:
我们可以格式化字符串在ebp1指向的内存(即ebp2的位置)写入我们想写入的内存地址,然后在ebp2指向的内存(我们想要覆盖的位置)写入我们想覆盖的值。

3.1 格式化字符串小tip

如果我们想要对某个地址写入一个非常大的值,那么应该怎么办?
一般人的思路如下:

char buf[4];
printf("%1145258561x%1$n", buf);//往buf中写入0xABCD,但是这也要在交互界面中写入非常多的padding,很可能撑爆你的交互界面!

4字节都很要命了,8字节我都不敢想。
这里有一个很聪明的办法:

char buf[4];
printf("%65x%1$hhn%c%2$hhn%c%3$hhn%c%4$hhn", buf, buf+1, buf+2, buf+3);
//1字节1字节的写入,这样你主要打一点点padding就能达到你的目的。

//如果写入的不是0xABCD,是0xDCBA怎么办?
char buf[4]
printf("%65x%1$hhn%c%2$hhn%c%3$hhn%c%4$hhn", buf+3, buf+2, buf+1, buf);//答案是倒过来就行~

3.2 动态padding size(不想成为二进制高手不用学)

以一个简单的神秘格式化字符串为例:%*10$c%11$n

1. 会取第10个参数的值(注意,实际上是printf的第11个参数,第一个参数为格式字符串)
2. 把它的值(四字节)作为打印padding的数量,并打印
3. 把打印出来的字符数量写入第11个参数指向的内存区域(这里要注意的是,第11个参数值得是printf中的第12个参数)