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

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

特定のアドレスの関数、データを取得する

以下の例では、オフセットを指定して任意のアドレスの関数名を取得したり、指定のアドレスのデータを取得したりできます。

# Listing 情報を取得(ghidra.program.database.ListingDB)
listing = currentProgram.getListing()

# オフセットを指定して GenericAddress オブジェクトを取得
fpapi = FlatProgramAPI(currentProgram)
addr = fpapi.toAddr(0x1024aa)

# 指定したアドレスを含む関数を取得
func = fpapi.getFunctionContaining(addr)
print(func.getName()) # 関数名の表示

# データを取得するアドレスを指定
addr = fpapi.toAddr(0x102cd1)

# 指定アドレスのデータを取得
data = listing.getDataAt(addr)
print(data.getValue()) # ghidra.program.model.scalar.Scalar

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

# 指定のオフセットから 0x100 分の範囲を指定
addr_set = AddressSet()
addr_set.add(addr, addr.add(0x100))

# 指定の範囲のデータを先頭から取得
data_iterator = listing.getData(addr_set, True)
for data in data_iterator:
    print(data.getValue())
    
# 特定のアドレスのオペコードとオペランドを取得
addr = toAddr(0x1000000)
inst = getInstructionAt(addr)
# オペランドの取得
inst.getDefaultOperandRepresentation(0)
inst.getDefaultOperandRepesentation(1)
# 次の行の情報を取得できる
inst.getNext()
inst.getDefaultOperandRepresentation(0)
inst.getDefaultOperandRepesentation(1)

関数内の Call を列挙する

以下のコードでは関数内の Call 命令を列挙できます。

関数内の呼び出し順序を維持したい場合やそうでない場合で取得方法を変える必要があります。

# currentAddress is ghidra.program.model.address.GenericAddress
# currentAddress には、Listing で選択している行のアドレスが自動的に参照される
# そのため、事前にターゲットになる関数のアドレスを選択しておく
func_mgr = currentProgram.getFunctionManager()
func = func_mgr.getFunctionContaining(currentAddress)

# 関数内の呼び出しアドレスを列挙する(呼び出し順ではない)
calls = func.getCalledFunctions(monitor)
for c in calls:
    print(c)

# Listing 情報を取得(ghidra.program.database.ListingDB)
listing = currentProgram.getListing()

# 関数内の Call 命令を順に列挙することで呼び出し順序を維持して出力する
for i in listing.getInstructions(func.body, True):
    print(i)

指定アドレス範囲内で実行される Call 関数が呼び出す関数名を列挙する

from ghidra.program.flatapi import FlatProgramAPI
from ghidra.program.model.address import AddressSet

listing = currentProgram.getListing()
fpapi = FlatProgramAPI(currentProgram)
start_addr = fpapi.toAddr(0x1043c9)
end_addr = fpapi.toAddr(0x104825)

addr_set = AddressSet()
addr_set.add(start_addr, end_addr)

for p in listing.getInstructions(addr_set, True):
	code = p.toString()
	if "CALL" in code:
		func_addr = int(code.split(" ")[1],16)
		fpapi.getFunctionContaining(fpapi.toAddr(func_addr)).getName()

プログラムの関数のデコンパイル結果を取得する

from ghidra.app.decompiler import DecompInterface

# Decompile インターフェースを取得
decomp = DecompInterface()
decomp.openProgram(currentProgram)

# currentAddress には、Listing で選択している行のアドレスが自動的に参照される
# そのため、事前にターゲットになる関数のアドレスを選択しておく
func = fpapi.getFunctionContaining(currentAddress)
decomp_results = decomp.decompileFunction(func, 30, monitor)

# Determine if the Decompiler completed successfully or failed
if decomp_results.decompileCompleted():
    pp = PrettyPrinter(fn, decomp_results.getCCodeMarkup())
    code = pp.print(False).getC()
    print(code)
else:
    print("There was an error in decompilation!")

ディスアセンブル結果から連続してハードコードされたバイト配列を取得する

# 特定アドレスから 0x26 バイト分のオペランドを取得する
addr = toAddr(0x109011)
inst = getInstructionAt(addr)
result = []
for i in range(0x26):
    result.append(inst.getDefaultOperandRepresentation(1))
    inst = inst.getNext()

print(result)

特定のアドレスの構造体情報を識別して値を取得する

from ghidra.app.script import GhidraScript

# list の取得
start_address = toAddr("0x404000")
data_section = currentProgram.getMemory().getBlock(start_address)
data_address = toAddr("0x404060")
data_object = getDataAt(data_address)

# 自作した list 構造体が解釈される
data_structure = data_object.dataType
data_component = data_structure.getComponent(0x0)
# Get the relative offset, length and data type of the component
offset = data_component.offset
length = data_component.length
data_type = data_component.dataType

# list 構造体から int 分の値を取得
byte_array = getBytes(data_address.add(offset), length)
print(hex(byte_array[0]))

# Get the address of the .data section
start_address = toAddr("0x404000")
data_section = currentProgram.getMemory().getBlock(start_address)
# list のアドレス
data_address = toAddr("0x404060")
data_object = getDataAt(data_address)

特定のアドレスに構造体を割り当て、値を取得する

# listnode の取得
data_type_manager = currentProgram.getDataTypeManager()
my_structure = data_type_manager.getDataType("main.coredump/listnode")
start_address = toAddr("0x405000")
data_section = currentProgram.getMemory().getBlock(start_address)

flag = ""
listnode_addr = 0x4052a0

# listnode
data_address = toAddr(hex(listnode_addr))
data_object = createData(data_address, my_structure)

# 自作した listnode 構造体が解釈される
data_structure = data_object.dataType
data_component = data_structure.getComponent(0x0)

# Get the relative offset, length and data type of the component
offset = data_component.offset
length = data_component.length
data_type = data_component.dataType

# listnode 構造体から byte 分の値を取得
byte_array = getBytes(data_address.add(offset), length)
flag += chr(byte_array[0])

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 ~