首先啥是格式化字符串

这里借用维基百科中的内容:

格式化字符串(英语:format string)是一些[程序设计语言]的输入/输出[库]中能将[字符串]参数转换为另一种形式输出的[函数]。例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。[1]

关闭地址随机化

image-20230531132925084

可以先查看一下

1
cat /proc/sys/kernel/randomize_va_space

然后再root用户下:

1
echo 0 > /proc/sys/kernel/randomize_va_space 

这样就关闭的地址空间随机化


首先来看看printf中的%

%[标志][输出最小宽度][.精度][长度]类型

1.输出最小宽度:用十进制整数来表示输出的最小位数。

  • 若实际位数多于定义的宽度,则按实际位数输出
  • 若实际位数少于定义的宽度则补空格或者0

2.类型

  • %c:输出字符,配上%n可用于向指定地址写数据。

    • 这个配上%n就比牛了
  • %d:输出十进制整数,配上%n可用于向指定地址写数据。

  • %x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。

  • %p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。

  • %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。

  • %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。

  • %n:是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。

来看看printf函数的使用

1
2
3
char str[100];
scanf("%s",str)
printf("%s",str)

错误的用法

1
2
3
char str[100];
scanf("%s",str);
printf(str)

在第二种的时候,我们就可以对printf进行控制

看一个正常的程序

1
2
3
4
5
6
7
#include <stdio.h>
int main(void){
int a=1,b=2,c=3;
char buf[]="test";
printf("%s %d %d %d\n",buf,a,b,c);
return 0;
}

修改一下

1
2
3
4
5
6
7
#include <stdio.h>
int main(void){
int a=1,b=2,c=3;
char buf[]="test";
printf("%s %d %d %d %x\n",buf,a,b,c);
return 0;
}

用gcc对它进行编译

这里我踩了一个坑

需要用 -g 不然它会有 without debugging 的警告

1
gcc test.c -g -o test

image-20230531165133555

可以看到前面的test 和 123 是正常的输出,

但是后面那个f7fcf6a0是啥呢?

我们用gdb来动态调试一下

image-20230531183518826

首先是设置断点在printf 这里

image-20230531183556205

然后程序就停止在了这里,接着用n【next】

image-20230531183650988

image-20230531183843078

从format 的 读取方向就可以看到 %x 这个读取了下一个内存单元的地址

所以,只要能控制这个format 就可以一直读取内存中的数据

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char *argv[])
{
char str[200];
fgets(str,200,stdin);
printf(str);
return 0;
}

我们可以去直接读取str[]的内容

输入:AAAA%08x%08x%08x%08x%08x%08x

gdb 调试一下

image-20230531204217493

image-20230531204602497

image-20230531204643476

我们可以用%s来获取指针指向的内存数据

比如说:我们来构造尝试获取0x41414141的数据

输入:

x41x41x41x41%08x%08x%08x%08x%08x%s

image-20230531205501193


用%n格式来写入数据

它的作用是把前面打印的长度写入某个内存地址

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
int num=66666666;
printf("Before: num = %d\n", num);
printf("%d%n\n", num, &num);
printf("After: num = %d\n", num);
}

image-20230531210026094

我们发现可以够着格式化字符串去访问栈内的数据,并且利用%n向内存中写入东西。

但是%n的作用是返回打印字符串的长度写到内存中,当我们向写入一个地址的时候,应该如何操作呢?


肯定就是要用自定义打印字符串宽度啦

比如:我们想把0x8048000 这个地址写入数据,我们就要做的就是把10进制的134512640作为格式符来控制宽度就可以了

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int num=66666666;
printf("Before: num = %d\n", num);
printf("%.100d%n\n", num, &num);
printf("After: num = %d\n", num);
printf("%.134512640d%n\n", num, &num);
printf("After: num = %xn", num);
}

image-20230531210548949

这就就能把num 的值 改成相应的地址。