これは脆弱エンジニアの Advent Calendar 2024の12日目の記事です。
はじめに
本記事は CyberDefenders(以下リンク参考)の「$tealer Lab」にチャレンジした際のWalkthroughになります
※本チャレンジについてはRed側のペネトレというよりはBlue側の分析力を問われるものになります。CTFだとRevとかForensicとかの分野のチャレンジが詰まってる感じです。
※今回のチャレンジはMalware解析メインでしたね。
チャレンジ開始前
問題について
以下の画像の「Download Lab Files」に問題ファイルのリンクがあります。
アーカイブファイルで圧縮されているので仮想環境で解凍してください。
※ホストで解凍しないでください。中には本物のマルウェアが入ってたりします。
環境
この CyberDefenders を解く際には仮想環境でマルウェア解析やメモリフォレンジックを行う環境を用意する必要があります。
今回は以下のような環境を用意しました。
- remnux
- Windows 10
今回はWindows 10の環境を利用しました。
主にBinaryNinja
を利用しました。
Q1
The provided sample is fully unpacked. How many sections does the sample contain?
Malwareの基本的な構成や簡易なPacking技術を判断するためにDiEへ食わせます。
エントロピー高めなので難読化はしっかり入ってそう。この結果からセクション数は判断できます。
4n4lDetector
にも食わせます。
Q2
How many imported windows APIs are being used by the sample?
Q3
The sample is resolving the needed win APIs at run-time using API hashing. Looking at the DllEntryPoint, which function is responsible for resolving the wanted APIs?
とりあえずどういった難読化をされているかを事前に把握できると楽なのでcapa
に食わせます。
RC4、XORが気になりますね。Heavens gateもあってアンチデバッグもありそう。
ここで静的解析を行うためBinary Ninjaに食わせます。エントリーポイントから見ていきます。
sub_6015C0
にハッシュ化されたような文字列を渡しています。数回呼び出されてそうなのでここら辺がAPI Hashの関数なのかなと予測できます。
そのままhashdb_automatedで調べてみます。
別の0xa8d05acb
の方もダメだったので、この値をそのまま使う感じではなさそう。
Q4
Looking inside the function described in question 3, which function is responsible for locating & retrieving the targetted module (DLL)?
sub_6015C0
の中身を見てみるとreturn
付近に以下のようなデコンパイル結果があります。
sub_607564
が第一引数をもらってるのでおそらくそれがDLLの復号化、sub_6067c8
がその結果を受けて第二引数を受けているのでAPIの復号化と予測できます。
sub_607564
の中身を確認します。
予測あってそうですね。
Q5
What type of hashing is being used for the API hashing technique?
sub_607564
の内部を調べているとsub_61D620
の関数を見つけます。
中身を見ると以下のデコンパイル結果となります。
ぱっと見以下のような動作はCRC32の動作に似た挙動のように見えます。
eax_14 u>> 8 ^ var_420[zx.d(*(arg1 + (ecx_3 << 1)) ^ eax_14.b)]
Q6
What is the address of the function which performs the hashing?
Q5を見つけたところですね。
Q7
What key is being used for XORing the hashed names?
sub_607564
の中に以下が見つかります。
先ほどのsub_61D620
の結果eax_7
にXORして第一引数と比較しています。
この値がXORKeyと予想してAPI Hash解決できなかった0xa8d05acb
を調べます。
あってそうですね!
Auto Resolve API Hash
このままHashで難読化されたデコンパイル結果を眺めていても解析しにくいので、自動でAPI名を解決してコメントを残すBinary Ninjaのスクリプトを作成します。APIの名前解決はedx
にmov
でハッシュを渡しているのでそのオペランドから取ってきます。
import requests ,time, re
from binaryninja import *
def hash_lookup(offset):
try:
while True:
hunt_url = 'https://hashdb.openanalysis.net/hunt'
hash_url = 'https://hashdb.openanalysis.net/hash'
time.sleep(2)
# Request the user to enter the hashing API values
hashing_api_input = offset
hashing_apis = [int(hash_value, 0) for hash_value in hashing_api_input.split(',')]
for hashing_api in hashing_apis:
# Create the payload for the hunt request
hashdb_req = {"hashes": [hashing_api]}
# Perform the hunt request
hunt_req = requests.post(hunt_url, json=hashdb_req)
# Check if there was a hit in the search
hits = hunt_req.json()['hits']
if hits:
# Extract the algorithm from the hit
algorithm = hits[0]['algorithm']
# Resolve the hash with the found algorithm
hash_resolve = requests.get(f"{hash_url}/{algorithm}/{hashing_api}")
# Extract DLL and API information
string_info = hash_resolve.json()['hashes'][0]['string']
dll_value = string_info['modules'][0]
api_value = string_info['api']
# Display the desired information
print(f"\nHashing Algorithm: {algorithm}")
print(f"DLL: {dll_value}")
print(f"API: {api_value}\n")
return api_value
else:
print(f"\nNo match found for hash {hashing_api}")
return None
except Exception as e: # 例外処理
print(f"Error occurred: {e}")
return None
def is_hex(text):
pattern = r'^0x[0-9a-fA-F]+$'
return bool(re.match(pattern, text))
def add_comment(bv, address, text):
try:
bv.get_functions_containing(address)[0].set_comment_at(address, text)
except Exception as e: # 例外処理
print(f"Error occurred: {e}")
return
def gather_offsets(bv, xref_address):
new_address = xref_address
# 指定されたアドレスが含まれている関数を取得
functions = bv.get_functions_containing(xref_address)
if not functions:
print("Function not found at address:", hex(xref_address))
return None
while new_address >= new_address - 0x30: #大体ここくらいまで調べる。
# 現在のアドレスにおける命令を取得
instruction_length = bv.get_instruction_length(new_address)
if instruction_length == 0:
new_address -= 0x1
continue
# 現在の命令を取得(ディスアセンブリ)
instruction_text = bv.get_disassembly(new_address)
if "mov" in instruction_text and "edx" in instruction_text: # "mov","edx"命令を探す
operands = instruction_text.split()
if len(operands) > 2:
print(operands)
# 3番目のオペランド
if is_hex(str(operands[2])):
return operands[2]
else:
pass
new_address -= 0x1
return None
def locate_xrefs(bv, function_address):
xref_list = []
func = bv.get_function_at(function_address)
for xref in bv.get_code_refs(function_address):
if xref.address not in xref_list:
xref_list.append(xref.address)
return xref_list
def main_fuc():
func_addr = 0x006067c8
xref_list = locate_xrefs(bv, func_addr) #呼び出し元
print(xref_list)
for xref in xref_list:
offset = gather_offsets(bv, xref) #XOR前のoffset
if offset and is_hex(str(offset)):
offset = int(offset, 16)^0x38ba5c7b #XOR key
print(hex(offset))
decrypted_string = hash_lookup(str(offset))
time.sleep(1)
if decrypted_string:
add_comment(bv, xref, decrypted_string)
else:
pass
main_fuc()
print("Done!")
これBinary Ninjaで回していきます。
終りました。コメントが残っているか確認します。
イケてそう!このAPIはHeavens gateとかで使われてそう。
※sub_6067c8
の関数はAPIfunc_Resolve
とか名前を適当につけてます。
※sub_607564
はDll_resolve
とか(命名規則...)
Q8
What information is being accessed at the address 0X60769A?
DllBaseの表記からすぐに判断できると思われる。それに関する名称を入れればいい。
Q9
Looking inside the function described in question 3, which function is responsible for locating & retrieving the targetted API from the module export table?
これはすでに予測出来てますね。
Q10
Diving inside the function described in question 8, what is being accessed at offset 0X3C within the first passed parameter?
0x3C
を見たらe_lfanew
、IMAGE_NT_HEADERS
への筋道というのを覚えておいて損はないです。
Q11
Which windows API is being resolved at the address 0X5F9E47 ?
Q7で見てますね。
Q12
Looking inside sub_607980, which DLL is being resolved?
関数を見てみます。
0x38ba5c7b
とXORした値をhashdb-cliで確認します。
Q13
Also Looking inside sub_607980, which API is being resolved?
先ほど回したScriptでコメントされてます。
Q14
What is the appropriate data type of the only argument at function sub_607D40?
関数を見てみます。
argがeaxで各HEX値と比較されてます。この値を調べてみるといいかもですね。
例外コードっぽい。というわけで修正した結果の関数を以下に記す。
Q15
After reverse-engineering sub_607980 and knowing its purpose, Which assembly instruction is being abused for further anti-analysis complication? (especially when running the sample)
AntiDebugSeekerを使ってみる。
対象の関数では見つからなかった。
というわけで直接見てみると、その関数から呼び出されてるsub_607d40
で以下のディスアセンブル結果での命令を見つける。
末尾のint3
これは有名かなと思う。以下の記事などを参考にしてみてほしい。
Q16
After reverse-engineering sub_607980 and knowing its purpose, Which assembly instruction is being used for altering the process execution flow? (Also adds anti-disassembly complication)
見るだけ。
Q17
There are important encrypted strings in the .data section. Which encryption algorithm is being used for decryption?
capaの結果から判断できる。
Q18
What is the address of the function that is responsible for strings decryption?
Binary Ninjaの拡張機能を使う。
この関数を確認しにいく。
とても長ったらしい関数なのですが内部を探っていると、とある関数sub_61e5d0
が見つかります。
0x100
ループ...見つけたぞRC4。
Q19
What are the two first decrypted words (strings) at 0X629BE8?
先ほどの関数が呼び出しているXrefを適当に探ります。すると第二引数が0x28
で安定して渡されており、おそらく鍵の長さ、Offsetであると推測できます。以下に変数名を変更した関数sub_61e5d0
(RC4)を記します。
問題で言われている箇所の前0x28
バイト長がKeyと予想できます。完全にGUESSです。この0x28
バイト以前は0x00
のバイトで埋められてますしね。
選択部分がKeyと予想できるので、これで復号を試してみます。
ダメだった。。。
適当にRC4関数の前の挙動を追っていると、気になる関数が2つ呼ばれているのが分かります。
以下は関数名変更後の表示ですが、大まかに何をやっているかわかるはずです。
中身はこんな感じ。
というわけで、鍵を反転させて、それで復号してみます。
復号出来ました!
Q20
What is the key used for decrypting the strings in question 16?
keyは先ほどの問題が解けていれば解けるはず!
Q21
What is the length (in bytes) of the used key in question 16?
wininet
辺りのAPI関数が欲しかったのですが、以前回したPythonスクリプトでは全てのAPI関数を復号してコメントを残せているわけではないので、見つかりませんでした。
Wrap関数を用意してAPI復号関数sub_6067C8
に渡してるパターンもあるからです。
というわけでwrapしてる関数sub_6015c0
を探して、先ほどと同様にコメントを残すスクリプトを記述します。
import requests ,time, re
from binaryninja import *
def hash_lookup(offset):
try:
while True:
hunt_url = 'https://hashdb.openanalysis.net/hunt'
hash_url = 'https://hashdb.openanalysis.net/hash'
time.sleep(2)
# Request the user to enter the hashing API values
hashing_api_input = offset
hashing_apis = [int(hash_value, 0) for hash_value in hashing_api_input.split(',')]
for hashing_api in hashing_apis:
# Create the payload for the hunt request
hashdb_req = {"hashes": [hashing_api]}
# Perform the hunt request
hunt_req = requests.post(hunt_url, json=hashdb_req)
# Check if there was a hit in the search
hits = hunt_req.json()['hits']
if hits:
# Extract the algorithm from the hit
algorithm = hits[0]['algorithm']
# Resolve the hash with the found algorithm
hash_resolve = requests.get(f"{hash_url}/{algorithm}/{hashing_api}")
# Extract DLL and API information
string_info = hash_resolve.json()['hashes'][0]['string']
dll_value = string_info['modules'][0]
api_value = string_info['api']
# Display the desired information
print(f"\nHashing Algorithm: {algorithm}")
print(f"DLL: {dll_value}")
print(f"API: {api_value}\n")
return api_value
else:
print(f"\nNo match found for hash {hashing_api}")
return None
except Exception as e: # 例外処理
print(f"Error occurred: {e}")
return None
def is_hex(text):
pattern = r'^0x[0-9a-fA-F]+$'
return bool(re.match(pattern, text))
def add_comment(bv, address, text):
try:
bv.get_functions_containing(address)[0].set_comment_at(address, text)
except Exception as e: # 例外処理
print(f"Error occurred: {e}")
return
def gather_offsets(bv, xref_address):
new_address = xref_address
# 指定されたアドレスが含まれている関数を取得
functions = bv.get_functions_containing(xref_address)
if not functions:
print("Function not found at address:", hex(xref_address))
return None
count = 0
while new_address >= new_address - 0x30: #大体ここまで調べる。
# 現在のアドレスにおける命令を取得
instruction_length = bv.get_instruction_length(new_address)
if instruction_length == 0:
new_address -= 0x1
continue
# 現在の命令を取得(ディスアセンブリ)
instruction_text = bv.get_disassembly(new_address)
if "push" in instruction_text: # 2つめの"push"命令を探す
operands = instruction_text.split()
if len(operands) > 1:
print(operands)
if is_hex(str(operands[1])) and count == 1:
return operands[1]
elif is_hex(str(operands[1])) and count == 0:
count += 1
else:
pass
new_address -= 0x1
return None
def locate_xrefs(bv, function_address):
xref_list = []
func = bv.get_function_at(function_address)
for xref in bv.get_code_refs(function_address):
if xref.address not in xref_list:
xref_list.append(xref.address)
return xref_list
def main_fuc():
func_addr = 0x006015c0
xref_list = locate_xrefs(bv, func_addr) #呼び出し元
print(xref_list)
for xref in xref_list:
offset = gather_offsets(bv, xref) #XOR前のoffset
if offset and is_hex(str(offset)):
offset = int(offset, 16)^0x38ba5c7b #XOR key
print(hex(offset))
decrypted_string = hash_lookup(str(offset))
time.sleep(1)
if decrypted_string:
add_comment(bv, xref, decrypted_string)
else:
pass
main_fuc()
print("Done!")
APIのハッシュはDLLのハッシュの前にStackにPushされているので、2つ目のpush
命令を探してます。
このスクリプトを回して以下の出力を確認しました。
おー、使ってそうですね。
ここらの関数にwininet
のAPI関数が終結してます。
Q23
What is the first C&C IP address in the embedded configuration?
InternetConnectW
APIの第二引数辺りを確認すればアドレスが見えるはずです。
このAPIが使われている部分を確認します。
色々追っていたのですが以下のdata構造の中にC2アドレスが難読化されてそうです。
と言ってもわけわからな過ぎた。。。
わけがわからなかったので先人の知恵を借ります。Reversingなので自力で難読化解除したかったのですが、脅威インテル利用します。
HashをVTに投げます。
Dridexマルウェアぽいですね。こいつを調べてみると、以下のブログにヒットします。
ここにいいToolの情報が記載されています。
これでDridexマルウェアのコンフィグを抽出します。
大体見えましたね。ありがとう。
Q24
What is the port associated with the first C&C IP address?
先ほどのToolで見えます。
Q25
How many C&C IP addresses are in the sample configuration?
先ほどのToolで見えます。
Q26
What is the address of the function which downloads additional modules to extend the malware functionality?
HttpSendRequestW
のAPIを呼び出している以下の関数が怪しいですね。
最後に
最初動的解析が一切できずに鬼ムズで静的解析に挑んでました。見てみるとアンチデバッグもりもりだったのでそりゃそうかともなりました。
BinaryNinjaのいい練習になったと思います。
Malware解析時間溶ける。16進数がこびり付く。