All Articles

はじめての Ghidra Script で CTF の問題を解く

リバーシングエンジニアリングツールの Ghidra で使える Scripting の機能がかなりパワフルでいい感じなんですが、CTF でバイナリを読む時にはいまいち使えておらず、もてあましてしまっていました。

便利なものは積極的に使っていこうということで、今回は CTF の問題を通して実際に Ghidra Script を使ってみることにします。

Ghidra Script は便利なんですが結構最初のとっかかりの情報が少なくてコードかリファレンス読めないと何していいのかわからなくなるので、その辺を解消できるような内容にしたいです。

(リバーシングやるなら自分でコード読めという話なのかもしれませんが、、、)

もくじ

Ghidra の Scripting について

Ghidra Script を使用することで、バイナリ内での検索やコメント付与、解析処理などを自動化することができます。

Ghidra の Scripting は、Ghidra API というインターフェースを通して実行でき、Java や Python から実行できます。

Python の場合は Script Manager から実行できるスクリプトだけでなく、インタプリタ経由でスクリプトを実行することもできます。

ただし、現在利用している Ghidra 10.2.3 に組込みのインタプリタは Jython ベースのものであり、Python 3 系と互換性がありません。

組込みの Python インタプリタではなく Python 3 系で操作できる Ghidrathon というサードパーティのモジュールもありますが、残念ながらこちらは(少なくともインタプリタに関しては)使い勝手があまりよくないので、今回は標準のインタプリタを使用していきます。

※ Ghidrathon のセットアップ方法は以前書いた CTFのためのGhidra環境構築メモ を参照してください。

ただ、今後 Python3 系に移行してもそのまま使えるように、できるだけ Python3 の記法に寄せていきます。

Ghidra API について

先に Ghidra API の大まかな構造について整理しておきます。

Ghidra で解析するプログラムに対するインターフェースは、Program API によって用意されているようです。

この Program API は、Ghidra の中では低レイヤとの位置づけになっており、Program API のすべての API へのアクセスは FlatProgramAPI によって公開されています。

また、FlatProgramAPI を拡張した GhidraScript というものがあります。

GhidraScript は FlatProgramAPI のサブクラスとして用意されています。

参考:Program

参考:FlatProgramAPI

参考:GhidraScript

よく使うクラスに関するまとめ

ghidra.program.flatapi.FlatProgramAPI

前述した通り、Python で Ghidra Script を使用する場合はこの FlatProgramAPI が主なインターフェースになります。

インタプリタから currentProgram で参照可能な現在のプログラムに対応する ghidra.program.database.ProgramDB オブジェクトを引数として与えることでクラスオブジェクトを作成できます。

クラスのメソッドは非常に多くあり、例えば currentAddress などで参照可能な ghidra.program.model.address.GenericAddress オブジェクトを引数としてそのアドレスが含まれる関数を取得できる getFunctionContaining 関数などが存在します。

他にも、任意のオフセットの GenericAddress オブジェクトを返す toAddr 関数なども非常に便利です。

# 以下で取得できるオブジェクト
fpapi = FlatProgramAPI(currentProgram)

# currentAddress が含まれる関数の FunctionDB オブジェクトを返す
func = fpapi.getFunctionContaining(currentAddress)

# 指定のオフセットの GenericAddress オブジェクトを返す
addr = fpapi.toAddr(0x10000)

参考:FlatProgramAPI

ghidra.program.database.ProgramDB

currentProgram で取得できる Program オブジェクトがこのクラスです。

Program API の階層の中では一番上のオブジェクトになります。

getListing 関数による ListingDB オブジェクトなどが含まれます。

# 現在のプログラムの ListingDB オブジェクトを取得
listing = currentProgram.getListing()

# すべての関数へのアクセスを提供する FunctionManagerDB オブジェクトを取得
func_mgr = currentProgram.getFunctionManager()

# プログラムのバイナリコンテンツを扱える MemoryMapDB オブジェクトを取得
mem = currentProgram.getMemory()

参考:Program

ghidra.program.database.function.FunctionDB

# 様々な関数から取得できる
func = fpapi.getFirstFunction() # 最初の関数を返す
func = fpapi.getFunctionContaining(currentAddress)

# 関数の先頭と末尾アドレスを持つ ghidra.program.model.address.AddressSet を返す
func_body = func.getBody()

# 先頭と末尾のアドレスの ghidra.program.model.address.GenericAddress オブジェクトを取得する
start = func.getEntryPoint()
end = func.getBody().getMaxAddress()

参考:FunctionDB

ghidra.program.database.ListingDB

ListingDB を FunctionDB と組み合わせて使用すると、InstructionRecordIterator オブジェクトとしてイテラブルなディスアセンブル結果を含む情報を取得できます。

# 特定の関数の
listing = currentProgram.getListing()
func_body = func.getBody()

# 関数内の命令を順に列挙する(line_instruct は InstructionDB オブジェクト)
for line_instruct in listing.getInstructions(func_body, True):
    print(line_instruct)
    print(line_instruct.toString())
    print(line_instruct.getMnemonicString())
    print(line_instruct.getNumOperands())

参考:Listing

ghidra.program.model.address.GenericAddress

このクラスでは、Address インターフェースを提供します。

Ghidra ではすべてのアドレスは最大 64 bit のオフセットで表されています。

# 指定のオフセットの GenericAddress オブジェクトを返す
addr = fpapi.toAddr(0x10000)

# GenericAddress からアドレスオフセットを long 型で取得
addr_offset = addr.getOffset()
hex(int(addr_offset))

参考:GenericAddress

ghidra.program.model.address.AddressSet

AddressSet は 1 つ以上のアドレス範囲で構成されいるオブジェクトで、関数のコードがメモリの連続しない複数の範囲に割り当てられている場合にも表現することができます。

# 関数の先頭と末尾アドレスを持つ ghidra.program.model.address.AddressSet を返す
func_body = func.getBody()

# AddressSet 内のアドレスは  AddressIterator オブジェクトで探索できる
for line_addr in func_body.getAddresses(True):
    print(line_addr)

# 逆順で列挙
for line_addr in func_body.getAddresses(False):
    print(line_addr)


# 指定の範囲で任意の AddressSet を取得する
from ghidra.program.model.address import Address, AddressSet
factory = currentProgram.getAddressFactory()

# 空の AddressSet を作成
addr_set = AddressSet()
start = factory.getAddress("0x1000")
end = start.add(0x1000);

# 指定の範囲の AddressSet を取得する
addr_set.add(start, end)

参考:AddressSet

Hurry up! Wait!(Rev)

svchost.exe

ここまでで最低限の Ghidra Script について整理できたので、最後に実際に CTF の問題を解いてみます。

今回は picoCTF の Hurry up! Wait! という問題を解きました。

まず、svchost.exe というファイルが与えられますが、フォーマットは普通に ELF でした。

とりあえず手元で実行しようとしたものの、error while loading shared libraries: libgnat-7.so.1 というエラーで失敗しました。

しかたがないので、一旦静的解析を試みます。

main 関数を特定して処理を追ってみたところ、以下のように非常に多くの関するを呼び出す箇所に到達しました。

image-20230615230853997

この関数ですが、p、i、c、o … と、上から順に 1 文字ずつ Flag を表示する関数になっていることがわかります。

image-20230615230931508

これらの関数の取得を Ghidra スクリプトを使って取得していきます。

以下のスクリプトで、関数内で呼び出す関数の一覧と、Call 命令の呼び出し順序を取得できます。

from ghidra.program.model.listing import CodeUnit
from ghidra.program.model.symbol import SourceType

# currentAddress is ghidra.program.model.address.GenericAddress
program = getCurrentProgram()
fm = program.getFunctionManager()
function = fm.getFunctionAt(currentAddress)
calls = function.getCalledFunctions(monitor)
for c in calls:
    print(c)

# 呼び出し順序を維持する
for i in program.listing.getInstructions(function.body, True):
    print(i)

参考:Function call sequence for each function · Issue #2134 · NationalSecurityAgency/ghidra

上記を元に、以下のスクリプトを作成しました。

from ghidra.program.flatapi import FlatProgramAPI

program = getCurrentProgram()
listing = currentProgram.getListing()
fpapi = FlatProgramAPI(currentProgram)
addr = fpapi.toAddr(0x10298a)
func = fpapi.getFunctionContaining(addr)

flag = ""

# 関数内の呼び出しアドレスを列挙する(呼び出し順ではない)
for p in program.listing.getInstructions(func.body, True):
    oprand = str(p.getDefaultOperandRepresentation(0))
    if oprand[:2] == "0x":
        # 呼び出し関数のオブジェクトを取得
        addr = fpapi.toAddr(int(oprand,16))
        func = fpapi.getFunctionContaining(addr)
		
        # 各関数のデータ参照位置まで移動
        start = func.getEntryPoint()
        end = func.getBody().getMaxAddress()
        instr = listing.getInstructionAt(start)
        for i in range(4):
            instr = instr.getNext()
        
        # データアドレスの取得
        operands = []
        i = 0
        while len(instr.getOpObjects(i)) > 0:
            i += 1
            for op in instr.getOpObjects(i):
                operands.append(op)
        d_addr = "".join(str(op) for op in operands)
        if d_addr[:2] == "0x":
            addr = fpapi.toAddr(int(d_addr,16))
            data = listing.getDataAt(addr)
            flag += chr(int(data.getValue().getValue()))


print(flag)

インタプリタで上記のスクリプトを実行すると以下のように Flag を取得することができます。

image-20230618235345892

Ghidra Scriptingに役に立つサイトや資料について

Ghidra Scriptingに役に立つサイトや資料について以下にまとめておきます。

ユースケース別サンプルスクリプト

以前はこの記事でまとめていたサンプルスクリプトですが、以下の記事に分割しました。

参考:ユースケース別サンプルスクリプト

マスタリング Ghidra

参考:O’Reilly Japan - マスタリングGhidra

Ghidra 実践ガイド

参考:リバースエンジニアリングツールGhidra実践ガイド | マイナビブックス

Malware Analysis at Scale ~ Defeating EMOTET by Ghidra ~

Ghidra 実践ガイドの著者のスライドです。

参考:Malware Analysis at Scale ~ Defeating EMOTET by Ghidra ~