以下の内容はhttps://daisuke20240310.hatenablog.com/entry/ghidra2より取得しました。


Ghidraで始めるリバースエンジニアリング(使い方編)

前回は、リバースエンジニアリングツールである Ghidra を使って、STM32 の ELFファイルを解析してみました。

今回も、書籍「リバースエンジニアリングツールGhidra実践ガイド (Compass Booksシリーズ)」を読みながら、さらに、STM32 の ELFファイルの解析を進めていきます。

また、書籍「マスタリングGhidra ―基礎から学ぶリバースエンジニアリング完全マニュアル」も入手しました。こちらで覚えた内容も追記していきたいと思います。

それでは、やっていきます。

参考文献

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回: Ghidraで始めるリバースエンジニアリング(使い方編) ← 今回
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:HTTPを題材にtcpdumpの出力を理解する
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方(GPU実行時間の見積りとパスワード付きZIPファイル)
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ
・第59回:書籍「解題pwnable」の第3章「login2(スタックバッファオーバーフロー2)」を読んだ
・第60回:書籍「解題pwnable」の第4章「login3(スタックバッファオーバーフロー3)」を読んだ
・第61回:書籍「解題pwnable」の第5章「rot13(書式文字列攻撃)」を読んだ
・第62回:GitHubが開発した静的解析ツール(脆弱性検出ツール)のCodeQLを使ってみる
・第63回:CodeQL(静的解析ツール)で使われるクエリの選ばれ方を調べた
・第64回:CodeQL(静的解析ツール)のクエリの書き方を調べた
・第65回:CodeQL(静的解析ツール)で使われているアラートクエリの中身を調べる
・第66回:CodeQL(静的解析ツール)で使われているパスクエリの中身を調べる
・第67回:CodeQL(静的解析ツール)をVSCodeで使う方法を理解する
・第68回:CodeQL(静的解析ツール)の挙動を確認するための対象ソースコードとしてTinyhttpdを調査する
・第69回:Tinyhttpdを使用してCodeQL(静的解析ツール)のクエリの挙動を確認する
・第70回:セキュアコーディング:CERT C INT02-C 「整数変換のルールを理解する」を調べる
・第71回:CodeQL(静的解析ツール)の挙動を確認するための対象ソースコードとしてlibusbを調査する

まず、書籍「リバースエンジニアリングツールGhidra実践ガイド (Compass Booksシリーズ)」のサポートサイトは以下です。

ここからサンプルプログラムがダウンロードできます。

book.mynavi.jp

今回も、前回に引き続き、実行プログラムは、STM32 の ELFファイルを使いますが、できるだけ、実行プログラムに依存しない内容にしたいと思います。

Ghidraの設定

Ghidra の設定、というか、使いやすいようにカスタマイズする方法が、書籍「リバースエンジニアリングツールGhidra実践ガイド (Compass Booksシリーズ)」に書かれています。その中で、私が設定した内容を書いておきます。

ハイライトの設定

Edit → Tool Options を開きます。

Listing Fields の Cursor Text Hightlight を開きます。

対象とする文字列を、マウスの真ん中ボタンを押すと、それと同じ文字列がハイライトする機能です。真ん中ボタンだと使いにくいので、左クリックに変更します。

Mouse Button To Active を MIDDLE から LEFT に変更して、Apply をクリックします。

ハイライトのボタン設定
ハイライトのボタン設定

キーバインドの設定

Edit → Tool Options を開きます。

Key Bindings を開きます。

以下に、便利なキーバインドも書いておきます。

  • Previous Location in History(ALT-LEFT):関数の中に入った後、元の位置に戻りたいときがありますが、そのときに使える機能です、Altキーを押しながら←と→で行き来できます、今のところ、キーバインドは変更してません
  • Find References To(Ctrl-Shift-F):関数名や、アドレスなどにカーソルを置いた状態で、このキーバインドを押すと、そのアドレスを参照している箇所が一覧で表示されます。関数の先頭アドレスにカーソルを置いて、このキーバインドを押すと、関数の呼び出し元の一覧が表示される機能です、Ctrl+Shift+Fキーで起動できます。書籍で勧めているように、xキーに割り当てました
  • Find References to Symbol(Ctrl-Shift-F):上のFind References Toと同じ機能で、デコンパイルウィンドウで同じ機能が使えるようにする機能です。デフォルトではキーが割り当てられていませんので、Find References Toと同じ、xキーを割り当てました

使っていくうちに、また必要なキーバインドがあれば、追記していきます。

Ghidraの機能

Function Call Graph

Window → Function Call Graph で起動します。

現在のカーソル位置にある関数を起点に Function Call Graph が起動します。

各関数をダブルクリックすると、その関数の中に入っていきます。

Function Call Graph
Function Call Graph

ちなみに、マウスのホイールを回すと、ズームイン、ズームアウトします。Ctrlキーを押しながらホイールを回すと、移動できます。

Function Graph

Window → Function Graph で起動します。

現在のカーソル位置にある関数の分岐が表示されます。

マウスのホイールの動きは、Function Call Graph と同じです。

矢印にマウスを置くと、その分岐のコードが表示されます。関数の構造が可視化されて、理解が進みます。

Function Graph
Function Graph

使い方のメモ

ここからは、これどうやるのかな?という内容を個別にメモしておきます。

プログラムのエクスポート

Ghidra に表示されるデコンパイルされた C言語の内容をまとめてエクスポートすることが出来ます。

File → Export Program... を開き、Format で C/C++ を選択して、ファイルパスを指定して OK をクリックすると、デコンパイルした C/C++ のソースコードを、1ファイルにエクスポートできます。

デコンパイルしたソースコードをまとめてエクスポート
デコンパイルしたソースコードをまとめてエクスポート

個別のウィンドウを別ウィンドウにした場合に元に戻す方法

これは、かなり困りました。こういう操作系の悩みってググっても出ないし、ChatGPT はそれらしいこと言ってくるだけで全然ダメだし。。というわけで、ここに書いておきます。

分かってしまえば簡単です。いったん、以下のように、フローティングウィンドウにしたとします。

Consoleウィンドウをフローティングにした場合
Consoleウィンドウをフローティングにした場合

これを、もとのメインウィンドウにドッキングさせる方法です。フローティングにしたウィンドウのタイトルバー(Console [CodeBrouser] と書かれたところ)をクリックしてドラッグするのではなく、下図のように、フローティングにしたウィンドウの中の Console のタイトルバー(青背景の Console - Scripting と書かれたところ)をクリックして、メインウィンドウにドラッグすると元のようにドッキングした状態に戻せます。

もとのメインウィンドウにドッキングさせる方法
もとのメインウィンドウにドッキングさせる方法

気づかないときは、一生気づかない難問でした(笑)。

Symbol Tree

Ghidra をしばらく使っていると、この Symbol Tree のウィンドウはよく使うようになりました。

Exports

対象のバイナリに対して、最初は、main関数か、エントリポイントを探す場合が多いと思います。

strip されていなければ、Functions の main をクリックすれば、main関数が見つかります。しかし、strip されたバイナリの場合、main関数が見つからない場合があります。

そのときは、Symbol Tree の Exports を見ます。この中に、おそらく entry が入っているので、それをダブルクリックすると、main関数が表示されます。もしくは、__libc_start_main() が表示された場合、__libc_start_main() の第1引数の関数をダブルクリックすると、main関数が表示されます。

Exports は、外部公開しているシンボルのことだと思います。strip されたプログラムであっても、エントリポイントや、API関数は、他プログラムから見つけることができるはずで、これらを探す場合にも使えると思います。

Imports

小さいプログラムの場合は、main関数から順番に見ていけばいいのですが、規模が大きいプログラムの場合は、見たいところから見ていくという方法の方が効率がいいと思います。

そのときは、Imports の <EXTERNAL> を見ます。ここには、使用している外部ライブラリのシンボルが格納されています。

例えば、libc の strcpy関数があるかどうかを確認できますし、system関数があるか、なども確認できます。また、通信系のプログラムの場合、send、recv などが格納されていると思うので、そこからデータの入出力を見ていく、という方法が効率的だったりすると思います。

もちろん、C++ のプログラムの場合は、Symbol Tree の Classes や、Namespaces を使うのも効率がいいと思います。

Function Call Trees

デフォルトでは表示されてないですが、Function Call Trees も便利です。

下図のように、ツールバーの矢印のアイコンをクリックすると、Function Call Trees のウィンドウが表示されます。現在の関数の呼び出し元をたどっていけますし、呼び出し先もたどっていきます。とても便利です。

Function Call Trees
Function Call Trees

GhidraのInternal Decompiler Functions(内部関数)

逆コンパイルで表示される C言語には、SUB41()CONCAT31() など、見慣れない関数が出てきます。これらは、Ghidra の内部関数のようで、ヘルプにその機能について説明されています。ヘルプは英語なので、ここで簡単に説明を書いておきます。

SUB()

切り捨て操作関数で、SUBPIECE の SUB のようです。

実際は、SUB41(x, c) のようになっていて、4 と 1 は別の値になったりします。それぞれの意味を説明します。

  • 4:入力 x のバイト数を示します
  • 1:出力のバイト数を示します
  • x:入力値です
  • c:最下位バイトから切り捨てられるバイト数です

例が掲載されています。

SUB42(0xaabbccdd,1) = 0xbbcc は、入力が 4byte の 0xaabbccdd で、第2引数が 1 なので、最下位バイトの 0xdd は捨てられます。出力バイト数は 2 なので、結果は、真ん中 2byte の 0xbbcc になります。

いくつか例を示しておきます。

SUB41(0xaabbccdd, 0) = 0xdd
SUB42(0xaabbccdd, 0) = 0xccdd
SUB84(0xaabbccddeeffgghh, 4) = 0xaabbccdd
CONCAT()

連結操作関数です。

実際は、CONCAT31(x, y) のようになっていて、3 と 1 は別の値になったりします。それぞれの意味を説明します。

  • 3:入力 x のバイト数を示します
  • 1:入力 y のバイト数を示します
  • x:入力値です
  • y:入力値です

例が掲載されています。

CONCAT31(0xaabbcc,0xdd) = 0xaabbccdd は、入力が 3byte の 0xaabbcc と 1byte の 0xdd で、これを連結すると、結果は、0xaabbccdd になります。

ZEXT()

ゼロ拡張関数です。

実際は、ZEXT14(x) のようになっていて、1 と 4 は別の値になったりします。それぞれの意味を説明します。

  • 1:入力 x のバイト数を示します
  • 4:出力バイト数を示します
  • x:入力値です

例が掲載されています。

ZEXT24(0xaabb) = 0x0000aabb は、入力が 2byte の 0xaabb で、これを 4byte にゼロ拡張すると、結果は、0x0000aabb になります。

SEXT()

符号拡張関数です。

実際は、SEXT14(x) のようになっていて、1 と 4 は別の値になったりします。それぞれの意味を説明します。

  • 1:入力 x のバイト数を示します
  • 4:出力バイト数を示します
  • x:入力値です

例が掲載されています。

SEXT48(0xaabbccdd) = 0xffffffffaabbccdd は、入力が 4byte の 0xaabbccdd で、これは最上位ビットがセットされてるので、8byte に符号拡張すると、結果は、0xffffffffaabbccdd になります。

SBORROW()

符号付き借用演算子のテスト関数です。

実際は、SBORROW4(x,y) のようになっていて、4 は別の値になったりします。それぞれの意味を説明します。

  • 4:入力 x、y のバイト数を示します
  • x:入力値です
  • y:入力値です

符号付き整数として「x」から「y」を減算したときに算術オーバーフローが発生する場合は true を返します。

CARRY()

符号なしオーバーフロー演算子のテスト関数です。

実際は、CARRY4(x,y) のようになっていて、4 は別の値になったりします。それぞれの意味を説明します。

  • 4:入力 x、y のバイト数を示します
  • x:入力値です
  • y:入力値です

符号なし整数として xy を加算したときに算術オーバーフローが発生する場合は true を返します。

SCARRY()

符号付きオーバーフロー演算子のテスト関数です。

実際は、SCARRY4(x,y) のようになっていて、4 は別の値になったりします。それぞれの意味を説明します。

  • 4:入力 x、y のバイト数を示します
  • x:入力値です
  • y:入力値です

xy を符号付き整数として加算したときに算術オーバーフローが発生する場合は true を返します。

Ghidraのコメント機能

Ghidra には、コメントを追加する機能があります。単純にコメントを付けたいところで、右クリック → Comment にいくつかありますが、いずれをクリックしても同じダイアログが出ます。

追加できるコメントの種類は、EOL Comment、Pre Comment、Post Comment、Plate Comment、Repeatable Comment の5種類です。いずれも、アセンブラの方には、いい感じのコメントが作られるんですが、逆コンパイラした C言語のソースの方は、デフォルトでは、Pre Comment しか表示されないようです。

右クリックして、Properties を選ぶと、以下のように、逆コンパイラウィンドウに表示できるコメントを増やすことが出来ます。Display EOL comments、Display Plate comments、Display POST comments に追加でチェックを入れました。

コメント機能のオプションを変更
コメント機能のオプションを変更

これで、逆コンパイラの方にも、EOL、Plate、POST のコメントも表示されるようになりますが、表示される位置は、全て、Pre Comment と同じです。できれば、コメントを付けたい文の前にだけ、コメントを追加されるのではなく、文の後ろにコメントが表示されてほしかったです。

デバッグ情報の無いファイルを使う

デバッグ情報の無いファイルと言っても、いろいろな形があります。ELFファイルで strip したファイルや、そもそも ELFファイルでもないファイルなどです。ここでは、いろいろなデバッグ情報の無いファイルについて見ていきたいと思います。

x86-64 の簡単な strip したプログラム

ここでは、Ghidra を深く理解したいので、対象のプログラムは出来るだけ簡単なものを選びます。

以下の記事で、strip された(デバッグ情報が無い)簡単なプログラムを、GDB で扱う方法を書きました。strip されたプログラムの場合、シンボル情報が削除されているため、そのままでは、main関数の場所が分かりません。そこで、Ghidra で、main関数を特定して、そのアドレスを使って、GDB で main関数にブレークポイントを設定する方法を書きました。

daisuke20240310.hatenablog.com

ここでも、この簡単なプログラムを使います。

ソースコードは以下です。

main関数から、sub関数を呼び出し、sub関数の中で、printf関数を実行、scanf関数を実行して、数値を受け取り、戻り値でmain関数に返します。main関数は、戻り値が 0 超ならシステムに 0 を返し、それ以外なら 1 を返します。

#include <stdio.h>

int sub( void )
{
  int data;
  
  printf( "input data: " );
  
  scanf( "%d", &data );
  
  return data;
}

int main( int argc, void *argv[] )
{
  int ret;
  
  ret = sub();
  
  if( ret > 0 )
    return 0;
  else
    return 1;
}

簡単に実行してみます。

$ gcc -g -o hello_world.out hello_world.c

$ cp hello_world.out hello_world_strip.out

$ strip hello_world_strip.out

$ ll hello_world.out hello_world_strip.out
-rwxr-xr-x 1 user user 18K Sep  7 22:19 hello_world.out*
-rwxr-xr-x 1 user user 15K Sep  8 17:59 hello_world_strip.out*

$ file hello_world_strip.out 
hello_world_strip.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=89d00684582cd697b573c0fd49c38d4f17146450, for GNU/Linux 3.2.0, stripped

$ ./hello_world_strip.out
input data: 0

$ echo $?
1

$ ./hello_world_strip.out
input data: 1

$ echo $?
0

ここからは、Ghidra と GDB の両方を見ていきます。

まず、GDB を起動して、Base Image Address(以下、ベースアドレス)に設定するアドレスを調べます。

ベースアドレスは、0x555555554000 ということが分かりました。

$ gdb ./hello_world_strip.out

pwndbg> start

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
    0x555555554000     0x555555555000 r--p     1000       0 hello_world_strip.out
    0x555555555000     0x555555556000 r-xp     1000    1000 hello_world_strip.out
    0x555555556000     0x555555557000 r--p     1000    2000 hello_world_strip.out
    0x555555557000     0x555555558000 r--p     1000    2000 hello_world_strip.out
    0x555555558000     0x555555559000 rw-p     1000    3000 hello_world_strip.out
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7db0000 r-xp   188000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7db0000     0x7ffff7dff000 r--p    4f000  1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dff000     0x7ffff7e03000 r--p     4000  1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e03000     0x7ffff7e05000 rw-p     2000  202000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e05000     0x7ffff7e12000 rw-p     d000       0 [anon_7ffff7e05]
    0x7ffff7fb3000     0x7ffff7fb6000 rw-p     3000       0 [anon_7ffff7fb3]
    0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000       0 [anon_7ffff7fbd]
    0x7ffff7fbf000     0x7ffff7fc3000 r--p     4000       0 [vvar]
    0x7ffff7fc3000     0x7ffff7fc5000 r-xp     2000       0 [vdso]
    0x7ffff7fc5000     0x7ffff7fc6000 r--p     1000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc6000     0x7ffff7ff1000 r-xp    2b000    1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ff1000     0x7ffff7ffb000 r--p     a000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rw-p    21000       0 [stack]

以下は、Ghidra を起動して、ベースアドレスを 0x555555554000 に設定し、main関数を表示した状態です。

main関数を探す方法は、Symbol Tree の entry をクリックして、デコンパイルウィンドウを見ます。__libc_start_main関数の第1引数が main関数なので、それをダブルクリックすると、main関数を表示することができます。分かりやすいように、関数名を main に変更しています。

Ghidraでmain関数を表示
Ghidraでmain関数を表示

Ghidra が出力したデコンパイルソースです。

void processEntry entry(undefined8 param_1,undefined8 param_2)
{
  undefined1 auStack_8 [8];
  
  __libc_start_main(main,param_2,&stack0x00000008,0,0,param_1,auStack_8);
  do {
                    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}

bool main(void)
{
  int iVar1;
  
  iVar1 = FUN_555555555149();
  return iVar1 < 1;
}

undefined4 FUN_555555555149(void)
{
  undefined4 local_c;
  
  printf("input data: ");
  __isoc99_scanf(&DAT_555555556011,&local_c);
  return local_c;
}

main関数のアドレスが、0x555555555185 であることが分かったので、GDB でブレークポイントを設定して、c で、そこまで進めます。main関数のディスアセンブルを表示してみます。デバッグ情報がある場合は、disassemble を実行すれば、ディスアセンブラが表示できますが、デバッグ情報が無い場合は、失敗してしまいます。よって、xコマンドで表示しています。

pwndbg> b *0x555555555185
Breakpoint 1 at 0x555555555185

pwndbg> c
Continuing.

pwndbg> x/14i 0x555555555185
=> 0x555555555185:      push   rbp
   0x555555555186:      mov    rbp,rsp
   0x555555555189:      sub    rsp,0x20
   0x55555555518d:      mov    DWORD PTR [rbp-0x14],edi
   0x555555555190:      mov    QWORD PTR [rbp-0x20],rsi
   0x555555555194:      call   0x555555555149
   0x555555555199:      mov    DWORD PTR [rbp-0x4],eax
   0x55555555519c:      cmp    DWORD PTR [rbp-0x4],0x0
   0x5555555551a0:      jle    0x5555555551a9
   0x5555555551a2:      mov    eax,0x0
   0x5555555551a7:      jmp    0x5555555551ae
   0x5555555551a9:      mov    eax,0x1
   0x5555555551ae:      leave
   0x5555555551af:      ret

main関数のスタックを可視化します。

アドレス サイズ 内容
rbp - 0x20 8 rsi の退避
rbp - 0x18 4 未使用
rbp - 0x14 4 edi の退避
rbp - 0x10 8 未使用
rbp - 0x08 4 未使用
rbp - 0x04 4 ret
rbp 8 呼び出し元のrbp
rbp + 0x08 8 戻り番地

GDB のディスアセンブラの表示と、Ghidra を見比べてみます。ローカル変数に注目すると、GDB では、rbp - 0x14、rbp - 0x20、rbp - 0x4 の順にアクセスしています。Ghidra では、これらは、local_1c、local_28、local_c という名前が付けられており、対応しています。GDB の方は、rbp をベースにローカル変数を特定していますが、Ghidra の方は関数に入ったとき(戻り番地を push した後で、かつ、rbp を push する前)のスタックアドレスを基準として名前が付けられています。つまり、ret は、基準のスタックポインタ - 0x0c なので、local_c という名前が付けられています。

ちなみに、Ghidra のウィンドウの右下には、現在の行のディスアセンブラ表示も見ることができます(MOV dword ptr [RBP + -0x14],EDI)。 これは、書籍「マスタリングGhidra ―基礎から学ぶリバースエンジニアリング完全マニュアル」に書かれてました。

以下は、Ghidra が生成するラベルです。

接頭語 意味
LAB_アドレス 自動生成されるコードに付与されるラベル(たいていの場合、関数内のジャンプ先)
DAT_アドレス 自動生成されるグローバル変数にあたるデータ
FUN_アドレス 自動生成される関数名
SUB_アドレス 関数ではないが、CALL命令の宛先になってるもの
EXT_アドレス 外部からのエントリポイント(たいていの場合、関数)
OFF_アドレス コードの切れ端(たいていの場合、ディスアセンブルエラー)
UNK_アドレス 目的が特定できないデータ

STM32(ARM)の ELFファイル

ここまでで使ってきた、STM32 の通常の ELFファイルは、デバッグ情報を含んでいるので、ソースコードが無くても、それなりに解析が可能でした。

一方、デバッグ情報の無いファイルを試してみます。

以前(STM32(ARM Cortex-M)のELFファイル⇔バイナリの変換を行う - 土日の勉強ノート)、以下のコマンドで、ELFファイルをバイナリファイルに変換しました。

$ arm-none-eabi-objcopy -O binary stm32f4discovery_sample.elf stm32f4discovery_sample_objcopy.bin

このバイナリファイルを読み込ませてみます。

手順で異なるところだけ示します。

Import File... で、stm32f4discovery_sample_objcopy.bin を選択すると、Language を指定してください、と出ます。

STM32F4 は、Arm v7-M のはずです。ARM v7 32bit little endian は2つあります。Compiler が default と Visual Studio です。default を選んでおきます。

Languageの選択画面
Languageの選択画面

そういえば、前回は自動認識されましたが、v7 ではなく、v8 になっていました。

解析させて、Code Browser を開きます。

うーん、undefined がいっぱい出てます。命令が認識できていません。

v7で認識させて結果
v7で認識させて結果

前回にならって、v8 を選択してみます。ダメでした。v8-m や、v8T なども選んでみましたが、うまくいきません。

仕方ないので、以前(STM32(ARM Cortex-M)のELFファイル⇔バイナリの変換を行う - 土日の勉強ノート)、bin2elf.sh を使って、バイナリファイルから ELFファイルを再構築したもの(stm32f4discovery_sample_bin2elf_entry.elf)を使ってみます。

通常の ELFファイルと同様に、自動でファイルを認識してくれました。解析が完了すると、通常の ELFファイルと同じように認識しました。成功です。

bin2elf.shを使って再構築したELFファイルの解析結果
bin2elf.shを使って再構築したELFファイルの解析結果

デバッグ情報が無いと、プログラムの構造を理解するだけでも大変そうです。

今回はここまでにします。

おわりに

今回は、Ghidra の具体的な使い方と、機能について理解しました。

次回からは、書籍「リバースエンジニアリングツールGhidra実践ガイド (Compass Booksシリーズ)」に沿って、リバースエンジニアリングを理解していきたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/ghidra2より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14