株式会社Ninjastars 技術研究部

「もっと楽しく安全なゲームの世界を創る」

自作ゲーム:チートチャレンジ2

代表の森島です。

前回の記事で「money変数のメモリアドレスの特定方法」など、詳しい説明をして欲しいというご要望をいくつかいただいたので、補足いたします。

前回の記事についてはこちらからご覧いただけます。
ninjastars.hatenablog.com


なお、今回の動作環境はUbuntu18.04を想定しております。
またgccは「version 7.3.0」を使用しています。

補足点1 「コンパイル時のオプションについて」
前回のプログラムでは「gcc -m32 -no-pie -o game game.c」といったコマンドでコンパイルしました。
「m32」や「no-pie」、「o」といったコンパイルオプションをつけた理由を順に説明していきます。

m32オプションについて
m32オプションをつけることで、32bitバイナリが作成されます。
前回の記事においては特に重要な意味はありませんので、本来はつける必要はありません。
ただCTFの問題や低レイヤーに関する参考書などで32bitバイナリの方が親しみがある方はこちらのオプションをつけて、検証する方が楽だと思われます。

実際に作成されたELFファイルが32bitであることは「file game」などのコマンドで確認できます。

f:id:Ninjastars:20181211231726p:plain
elfファイル

oオプションについて
こちらはコンパイル後に生成されるバイナリのファイル名を指定するためのオプションです。
今回はソースファイルがgame.cなので、そのままgameという名前にしました。
もし何も指定しなかった場合は「a.out」という名前で作成されます。

no-pieオプションについて
まずPIE(Position Independent Executable)とは、実行可能コードのアドレスを相対アドレスで参照することでコードがメモリ上のどこに配置されたとしても正常に実行できる機能のことです。
また、ASLR(Address Space Layout Randomization)ではランダム化されない実行可能コードのアドレスもランダム化が行われるなどのセキュリティ的利点が存在します。

位置独立コードに関して、詳しくは以下のリンクをご覧ください。
位置独立コード - Wikipedia

現在、gccのversion 7.3.0では、このPIEがデフォルトで有効化されます。
そのため、今回はデバッグをより簡単にするためにno-pieオプションで無効化しました。

もちろん、PIEが有効だからといって、リバースエンジニアリングができなくなる訳ではありません。
実行中のプロセスIDを指定して、アタッチするなどの方法でPIEが有効なバイナリに対して、動的解析をすることが可能になります。

補足点2 「money変数のメモリアドレスの特定方法」
前回の記事では当たり前のように、「moneyが格納されているアドレスが0x0804a034でしたので」などと書いていましたが、ここのアドレス特定部分を詳しく説明いたします。
具体的な特定方法なのですが、実はいくつかあります。

方法1「gameを実行中のプロセスでメモリ検索をする」
今回はgdbの拡張スクリプトであるgdb-pedaを使ってデバッグしていきます。

もしまだgdb-pedaをインストールされていない方は以下のリンクをご覧ください。
gdb-peda インストール for Ubuntu - miyagawNote

まずどのタイミングでメモリ検索をかけるかですが、ここではscanf関数で標準入力を受け取っている0x080485e7にブレークポイントを設定しました。
(毎ラウンド、じゃんけんというイベントは発生するため)
以下は「disass main」というコマンドでmain関数を逆アセンブルした画像です。

f:id:Ninjastars:20181211232359p:plain
main関数の逆アセンブル

「break *0x080485e7」または「b *0x080485e7」でeipが0x080485e7に移ったタイミングでプログラムが一時停止します。(このことをブレークポイントの設置などという)

この状態でsearchmemと呼ばれるコマンドでmoneyの値をメモリ検索してみます。
moneyの初期値は100(0x00000064)だから「searchmem 0x00000064」といったコマンドでmoney変数を探すことができます。

f:id:Ninjastars:20181211232624p:plain
アドレス候補

ずらっと候補が100個以上、表示されます。
つまり、money変数はこの中のどこかにあるということです。
ただ画像に表示されていない部分も含めほとんどはlibcやstackの領域のアドレスなので、money変数ではないと考えられます。

以上のことから、候補が3つになりましたが、さらに絞っていきます。
各セクションがどういったメモリアドレスに割り当てられているかは、「objdump -x game」などで調べることができます。

f:id:Ninjastars:20181211232903p:plain
.dataセクション

このことから.dataセクションは0x0804a02cから0x0804a037であることが分かります。
money変数は初期値が存在し、かつ静的変数ではないためこの.dataセクションに存在すると考えられ、上の結果と合わせて0x0804a034であると分かりました。

またmoney変数の値はじゃんけんをすることで変えられるため、再度別の値でメモリ検索をかけて候補を少しずつ減らしていきながら特定するという方法でもできます。

方法2 「逆アセンブラで静的解析をする」
次は逆アセンブルツールであるIDAを使って、money変数のアドレスを探してみます。

IDAでは条件分岐に従ってフローチャート形式で逆アセンブルしたコードを表示することができます。
以下はmain関数内のflag変数の値によって、分岐するあたりの逆アセンブル結果です。

f:id:Ninjastars:20181212165952p:plain
IDAでの逆アセンブル結果

flag変数の値が1であれば「Lose」、2であれば「Win」と表示している処理があることが確認できます。

前回の記事のC言語のソースと比較してもらえれば分かりやすいはずです。

ここで「Win」と表示する部分に注目してみましょう。
何やら「add eax,0xa」としていますが、これはCソース側での「money+=10」に対応すると予想できます。
つまり、このときのeaxレジスタのロード元がmoney変数ということです。
確かに1つ前のmov命令でeaxレジスタに値を代入していますね。
その命令のオペランドからmoney変数のアドレスが0x0804a034であることが確認できます。
(上の図の黄色い部分をクリックすると具体的なアドレスが分かります。)

まとめ
今回の記事ではコンパイルオプションについての補足と変数のアドレス特定方法について説明しました。

本トピックにて簡単なプログラムに対するリバースエンジニアリングの基本が理解できると思われます。

実際、方法1はメモリチートにて頻繁に用いられる方法です。

それに対して方法2に汎用性を持たせるためには一定のリバースエンジニアリング力が必要となります。

最後に、くれぐれも記事の内容を試す際には以下の注意事項を読んでから行ってください。

注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:morishima@ninjastars-net.com

株式会社Ninjastars代表取締役
島健