angrでシンボリック実行をやってみる
シンボリック実行とは、プログラム上の変数をシンボルとして扱い、シンボルに対する一連の操作を分析することで条件を満たす入力値を特定するプログラム解析手法である。 ここでは、CTFチームShellphishが開発しているバイナリ解析ツールangrを使い、簡単なプログラムに対してシンボリック実行を適用してみる。
環境
Ubuntu 14.04.4 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.4 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4
angrのインストール
まず、GithubリポジトリのREADMEを参考に、必要なパッケージをインストールする。
$ sudo apt-get install build-essential python-dev libffi-dev python-virtualenv
ディレクトリを作成し、virtualenv環境のもとでangrをインストールする。
$ mkdir angr $ cd angr/ $ virtualenv ENV $ . ENV/bin/activate (ENV)$ pip install angr
簡単なプログラムを解析してみる
ここでは、次のような簡単な認証を行うプログラムについて考える。
/* test.c */ #include <stdio.h> #include <string.h> void succeeded() { puts("auth succeeded!"); } void failed() { puts("auth failed."); } int main(int argc, char *argv[]) { char username[256]; char password[256]; printf("Enter username: "); scanf("%s", username); printf("Enter password: "); scanf("%s", password); if (strcmp(username, "admin") == 0 && strcmp(password, "letmein") == 0) { succeeded(); } else { failed(); } return 0; }
ドキュメントを参考に、angrを使って上のプログラムの解析を行うスクリプトを書くと次のようになる。
# solve.py import angr p = angr.Project('./test', load_options={'auto_load_libs': False}) addr_main = p.loader.main_bin.get_symbol('main').addr addr_succeeded = p.loader.main_bin.get_symbol('succeeded').addr addr_failed = p.loader.main_bin.get_symbol('failed').addr print "main = %x" % addr_main print "succeeded = %x" % addr_succeeded print "failed = %x" % addr_failed initial_state = p.factory.blank_state(addr=addr_main) initial_path = p.factory.path(initial_state) pg = p.factory.path_group(initial_path) e = pg.explore(find=(addr_succeeded,), avoid=(addr_failed,)) if len(e.found) > 0: print 'Dump stdin at succeeded():' s = e.found[0].state print "%r" % s.posix.dumps(0)
上のスクリプトでは、main関数のアドレスを初期状態とし、succeeded関数に到達するパスを探索した後、その時点での標準入力をダンプする。 このとき、failed関数に到達したパスについてはそれ以上の探索を打ち切る。
(ENV)$ gcc test.c -o test (ENV)$ python solve.py main = 4006a8 succeeded = 400686 failed = 400697 Dump stdin at succeeded(): 'admin\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x00letmein\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01'
上の結果から、関数の一覧とsucceeded関数に到達する標準入力のダンプが出力されていることがわかる。
さらに、標準入力のダンプにおいて、認証を通過する入力であるadmin\x00
およびletmein\x00
が含まれていることが確認できる。
(ENV)$ ./test Enter username: admin Enter password: letmein auth succeeded!