Linux上で動く既知の簡易なプログラムに対し、前回はgdbで実行して一時中断し、パラメータを確認しました。
さらに、Ghidraとgdbを併用して解析を続けていきたいと思います。
Ghidraが表示するアドレスと実際のアドレスの換算
Ghidraとgdbを併用した分析をしようと思ったとき、最大の問題がココです。今回の記事は、この換算の方法をメモしておくことが目的といっても過言ではありません。なにしろ、せっかくGhidraで分析したコードを元に解析のポイントに目星をつけても、アドレスが合わないとブレークポイントも設定できません。
では、アドレスはどのようになっているでしょうか?
例えば、下図のような状態で、Cのソースコードで「fgets関数」に着目したとします。Ghidraはコードにカーソルを合わせると、対応するアセンブルコードがハイライトされます。この対応するアセンブルコードのアドレスが0x00100925なので、この箇所でのパラメータを見てみたい、と思ったとします。
gdbでは、IDAなどと同様にブレークポイントを設定することができます。この場合、解析者はGhidraのコードを見て「fgetsが実際に渡す値を見るために、0x00100925にブレークポイントを貼りたい」と思うわけですよ。
でも、前回記事で、gdbでプログラムを実行したときのアドレスが表示されていましたね?
a.outのアドレスは、info proc mappingsの結果から、
0x555555554000
0x555555555000
のどちらかにロードされているようなマッピングの出力だったと思います。
つまり、仮に0x00100925にブレークポイントを設定しても、ブレークするはずがないんですね。
ということは、マッピングのアドレス上のどこかにあるコードにブレークポイントを設定しなければいけません。
では、それはどこでしょうか?
Ghidraのコードのマッピングをみると、0x00100000から開始していることが分かります。
つまり、開始位置の+0x00000925が相対位置であるオフセットアドレスになります。
では、0x555555554000と0x555555555000のどちらかのオフセット先に当該コードがあるのではないか?ということになります。
では、このパラメータを確認してみましょう。
オフセットを計算すると、+0x925をするので、0x555555554925と0x555555555925ということになります。
このパラメータを見てみましょう。gdbの「x (アドレス)」コマンドで表示します。スラッシュ(/)の後にフォーマットを指定できます。
今回は、16バイト(16)、符号なし16進数(x)、単一バイト(b)とするため、「x/16xb (アドレス)」になります。
結果を見れば一目瞭然で、アドレスの後ろに「<main+203>」とシンボルからのオフセット位置を表示しているうえ、「48 8d 85 f0 fe ff ff」のデータの並びはGhidraの逆アセンブルコードの値と一致します。
つまり、0x555555554000がGhidraの0x00100000に対応するということが分かったわけです。
これが分かれば、換算が可能です。
このケースでは、Ghidraで表示されるアドレスに0x555555454000を加算すれば、実際のコードのアドレスになります。
逆に、実際のコードのアドレスから0x555555454000を減算すれば、Ghidraのアドレスになります。
この換算をすることで、Ghidraとgdbを併用して分析していけるというわけです。
この換算とgdbの使い方だけ分かればいいだけの記事なんですけどね、本当はw
とはいえ、その換算に必要なパラメータの求め方とか、そもそも実演するためのプログラムの準備まで記事にしてたらこんなに長くなったんですけどね(汗)
gdbでの分析
ここからは、ほとんどただのgdbの使い方になってしまいますが。
Ghidraの0x00100925のコードの部分にブレークポイントを設定してみましょう。
先ほどの換算をすると、
0x00100925 + 0x555555454000 = 0x555555554925
このアドレスにブレークポイントを設定します。
アドレス指定でブレークポイントを設定する場合は、以下の書式になります。
b *(アドレス)
このケースでは、
b *0x555555554925
となります。
info breakpointコマンドで、ブレークポイントが設定されたことを確認できます。
では、プログラムを継続してみましょう。
現在はCtrl + Cによるプログラム中断状態なので、c (continue)でプログラムを再開します。
すると、設定したブレークポイントでプロセスが中断されました。
この状態で、逆アセンブルコードを見てみましょう。
なお、デフォルトはATT形式ですが、私はIntel形式で慣れているため、そちらに切り替えています。
Intel形式での表示はset disassembly-flavor、逆アセンブルコード表示にはdisassembleコマンドを使用します。
set disassembly-flavor intel
disassemble (アドレス)
逆アセンブルコードが示され、ブレークしているアドレスが「=>」で示されています。
更に解析を進めます。
例として、少し先の0x0000555555554934の「fgets」で値を受け取る、第1引数のrdiの値が関数実行前と実行後でどのように変化しているか見てみます。
まずは、EIPを0x0000555555554934まで進めます。
進める方法として、
stepi
または
nexti
を使う方法があります。
stepi はステップイン、nextiはステップオーバーです。違いは、callの場合にステップインは関数の中にステップを進めます。ステップオーバーはcallで呼び出す関数を実行し、リターンした後(つまりcallの次のステップ位置)まで進めます。
※注意:gdbにはstep、nextが別にありますが、これらは実行ファイルがデバッグ版でコンパイルされ、ソースをロードしてデバッグする場合にソースの1行分を進めるためのコマンドです。今回のような環境で使用すると、予想外のところまで実行されてしまいます。
また、ブレークポイントを同様に設定してcontinueしてもいいでしょう。
0x0000555555554934まで進めたら、パラメータを確認します。
実行前にレジスタの値を確認します。
レジスタの値を表示するには、メモリと同様に x コマンドを使用します。
x (レジスタ名)
で出力可能です。
今回は第1引数が rdi なので、この値を確認すると、0x7fffffffde50となっています。
この領域の値を確認すると、全て0になっています。ループの先頭で0クリアしているので、当然そうなりますね。
では、fgets実行後まで進めましょう。nextiまたはリターンの位置にブレークポイントを設定してcontinueです。
例では、ブレークポイントを設定してcontinueしています。
結果をダンプしてみると、ちゃんと文字列のデータが入っていることが確認できます。
まとめ
今回は、Linux環境でGhidraを活用しつつ実際にプログラムを分析することができないか試してみました。
結論として、Ghidraのデコンパイルで示されるアドレスと実際の実行時のアドレスが異なるため、換算しないと紐づけができません。そのための換算方法として、メモリのマッピングからベースになるアドレスを割り出し、オフセットで計算することで換算可能、ということが分かりました。
そして、どのようなパラメータを見れば換算ができるかを例示しました。
併せて、分析に必要になる可能性のあるデータの抽出のためのコマンドやgdbの使い方を確認しました。
アドレス換算よりもそれ以外の記事の方がよっぽど長く、最後はgdbの使い方講座みたいになってしまいましたが。(^^;
今回のgdbでの解析方法は基本的なことしか触れていないので、gdbの使い方はさらに調べるとより解析がしやすいでしょう。
gdbの使い方だけなら、もっとわかりやすい記事がたくさんあります。
(次回くらいに、解析に使いそうなgdbコマンドくらいはまとめるかな・・・。)
もっとも、今回の技法は基礎的な分、色々応用は効くんじゃないかと思います。
例えば、Androidなどはadbを使った解析をします。この時、アプリのapkがネイティブコードの.soを呼び出していることがあります。
こういった場合は、Android内でgdbserverを使ってアタッチした上でリモートデバッグという方法が使われることがありま。
すると、gdbで動かしながら解析するわけですが、この時.soファイルをGhidraで分析しておけば、今回と同じ方法が使える、というわけです。
つまり、.soファイルのCデコンパイルソースを並列して見れるため、解析におけるコードの可読性は上がると思いますし、次のブレークポイントの割り出しや注意すべきパラメータの見極めに大いに役立つと思います。
Linuxのバイナリ実行ファイルの解析ではgdbを使うことがあると思いますが、その際にはGhidraも上手く併用していきたいですね。
あとは、やはりデバッガ機能の追加、もしくはデバッガとのリンク機能があるとやはり便利ですので、実装を首を長くして待ちたいところです。
(「アドオンを作ってもいいんじゃよ?」という声はキコエナーイ)