pAnalyzer 技術說明
1. 目的與適用範圍
pAnalyzer 是以 EfiPy2 為執行環境的 UEFI 呼叫追蹤與參數調整構想。其用途是在 UEFI Shell 階段觀察指定 Protocol 函式與部分 Boot Services 的呼叫,並以 Python 回呼設定測試行為。
相較於在目標程式中加入 DEBUG((...)) 或 Print(...),此方式透過替換介面結構中的函式指標進行攔截 (Hook)。對於未修改原始碼的 .efi 程式,若其呼叫經過已設定的介面,也可記錄該介面的呼叫資料。
目前範例涵蓋引數與回傳值記錄、呼叫前的參數調整,以及 XML 輸出。可追蹤的範圍取決於型別宣告、目標實例與已啟用的 Hook;記錄內容亦受引數解析方式限制。X64 可變參數 (VarArgs) 處理針對特定 Boot Services 設計,不能據此推定支援所有 UEFI 介面或架構。
2. 架構與呼叫流程
pAnalyzer 使用 BaseAnalyze 建立結構欄位資訊與 Filter,並由 dOut 記錄呼叫資料。以下以 PaTest.py 的 QueryMode 測試為例,說明初始化、攔截與還原流程:
流程說明
- 結構體反射與型別識別 (
Build_Capability):pAnalyzer藉由 Pythonctypes的結構內省功能,遍歷 Protocol 結構(如EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL),自動識別哪些欄位是函式指標 (_CFuncPtr)、各欄位的記憶體位移 (offset) 以及型別宣告。 - 實例探索 (
Detect_Protocol):
呼叫 UEFI 原生gBS.LocateHandleBuffer與gBS.HandleProtocol,獲取當前記憶體中已註冊的所有目標協定實例與 Handle。 - 動態替換函式指標 (VTable Hooking):
在start()執行時,pAnalyzer直接改寫目標結構體在記憶體中的函式指標位址(以指標賦值的方式改寫記憶體)。- 對於一般函式:利用 ctypes 建立 C 語言可呼叫的 callback wrapper。
- 對於目前支援的可變參數函式:由
VarArgs.py使用 CorePy 產生 X64 組合語言跳板 (Thunk),保存引數資訊並交由 Python 處理。
- 呼叫前後的處理 (Pre-func / Post-func):
當外部驅動或程式呼叫該函式時,控制權進入 Hook:- Pre-func:檢視傳入參數,並可回傳調整後的引數供原始函式使用。
- Native-func:呼叫原始韌體函式。
- Post-func:取得韌體回傳值與引數,供後續檢查或處理。目前一般函式路徑仍回傳原始
rest,並未採用回呼回傳的fRest覆寫函式回傳值。
- 函式指標還原 (
stop/uninstall):stop()或uninstall(FuncName)用於還原函式指標;物件解構時亦會執行清理。此機制不代表所有異常情況下均能完成還原,使用時仍需配合 Hook 與回呼物件的生命週期管理。
3. 範例程式與紀錄
pAnalyzerSample 目錄包含以下四個測試腳本:
pAnalyzerSample/
├── PaTest.py # Protocol 呼叫追蹤與參數調整
├── PaTest2.py # 外部 UEFI Application 執行期間的呼叫記錄
├── PaTest3.py # Driver 載入與 Application 執行
└── MultiProtocol.py # X64 可變參數 (VarArgs) 呼叫測試
以下程式碼與 XML 為節錄,省略部分初始化、追蹤設定與清理步驟。XML 中的位址、回傳值及畫面欄列數屬於既有範例紀錄,可能隨執行環境而異。
範例一:API 攔截與參數調整 (PaTest.py)
程式碼節錄
# 1. 定義回呼:如果參數 1 不是 0,將其設為 0
def _filter_func (dOut, *args):
if args[1] != 0x00:
dOut.append ('<Change argument="1" before="0x%X" after="0x00"/>\n' % args[1])
args = (args[0], 0x00) + args[2:]
return None, args
# 2. 建立 Capability 並鎖定 Console Out
t = pAnalyzer(dOut)
t.Build_Capability(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL)
t.Detect_Protocol(gEfiSimpleTextOutProtocolGuid, EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL)
for target, handle in t.Target:
t.Filter.append(t.Build_Filter(target, handle))
# 3. 設定回呼並啟動攔截
t.install("QueryMode", _filter_func)
t.start("QueryMode")
# 4. 傳入 ModeNumber = 10 進行測試
ModeNumber = EfiPy.UINTN(10)
Columns = EfiPy.UINTN(0)
Rows = EfiPy.UINTN(0)
EfiPy.gST.ConOut[0].QueryMode(EfiPy.gST.ConOut, ModeNumber, EfiPy.byref(Columns), EfiPy.byref(Rows))
XML 紀錄節錄 (PaTest.xml)
<Section name="Testing_2">
<Function name="QueryMode" fAddre="0x7DF65218" Structure="EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL" sAddr="0x7E127AA0">
<Input length="4">
<arg0 Type="EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *" Address="0x7E127AA0" ></arg0>
<arg1 Type="UINTN" Value="0xA" ></arg1> <!-- 原始輸入為 10 (0xA) -->
<arg2 Type="UINTN *" Address="0x7C78F818" Value="0x0" ></arg2>
<arg3 Type="UINTN *" Address="0x7D3E9318" Value="0x0" ></arg3>
</Input>
<!-- 參數調整紀錄 -->
<Change argument="1" before="0xA" after="0x00"/>
<Function name="QueryMode" fAddre="0x7EAE1DD2" Structure="EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL" sAddr="0x7EAE6E10">
<Input length="4">
<arg1 Type="UINTN" Value="0x0" ></arg1> <!-- 底層實際接收到 0 -->
</Input>
<Output Type="UINTN" Value="0x0">
<!-- 成功取得 Mode 0 的解析度:80 (0x50) x 25 (0x19) -->
<arg2 Type="UINTN *" Address="0x7C78F818" Value="0x50"></arg2>
<arg3 Type="UINTN *" Address="0x7D3E9318" Value="0x19"></arg3>
</Output>
</Function>
<Output Type="UINTN" Value="0x0"> ... </Output>
</Function>
</Section>
紀錄中的原始 ModeNumber 為 10,回呼將其改為 0,後續呼叫回傳狀態為 0,欄列數為 80 × 25。此範例呈現呼叫前調整參數的處理方式;未攔截時 Mode 10 是否受支援,取決於目標 Console 提供的模式。
範例二:外部 UEFI Application 呼叫記錄 (PaTest2.py)
程式碼節錄
AppList = ["FS0:\\EFI\\Apps\\pAnalyzerSample\\pAnalyzeApp2.efi"]
t = pAnalyzer(dOut)
t.Build_Capability(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL)
# 啟用對 OutputString, TestString, QueryMode 的追蹤...
t.LoadEfiFiles([], AppList)
XML 紀錄節錄 (PaTest2.xml)
<Application Name ="FS0:\EFI\Apps\pAnalyzerSample\pAnalyzeApp2.efi">
<Function name="HandleProtocol" fAddre="0x7FEFC02D" Structure="EFI_BOOT_SERVICES" sAddr="0x7FF17CA0">
<Input length="3">
<arg1 Type="GUID *" Address="0x7FF18D50" GUID="964E5B22-6459-11D2-8E39-00A0C969723B" ></arg1>
</Input>
<Output Type="UINTN" Value="0x0"> ... </Output>
</Function>
<Function name="OutputString" fAddre="0x7DF650F4" Structure="EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL" sAddr="0x7E127AA0">
<Input length="2">
<arg1 Type="CHAR16 *" Value="Locate EFIPY_PANALYZE_TEST_PROTOCOL Error Not Found " ></arg1>
</Input>
...
</Function>
</Application>
LoadEfiFiles 透過 LoadImage / StartImage 載入與執行應用程式,並設定 EFI_SHELL_PARAMETERS_PROTOCOL。此流程使用既有 UEFI 執行環境,未提供沙盒隔離機制。
上述既有 XML 節錄包含 HandleProtocol 呼叫及 OutputString 的錯誤訊息。實際可取得的紀錄取決於執行版本與已啟用的 Hook,不代表可觀察應用程式的所有內部行為。
範例三:驅動註冊與應用程式查詢 (PaTest3.py)
程式碼節錄
先載入驅動程式 pAnalyzeDrv.efi,緊接著執行客戶端應用程式 pAnalyzeApp2.efi:
DrvList = ["FS0:\\EFI\\Apps\\pAnalyzerSample\\pAnalyzeDrv.efi"]
AppList = ["FS0:\\EFI\\Apps\\pAnalyzerSample\\pAnalyzeApp2.efi"]
t = pAnalyzer(dOut)
t.LoadEfiFiles(DrvList, AppList)
XML 紀錄節錄 (PaTest3.xml)
<Section name="Prepare">
<Driver Name ="FS0:\EFI\Apps\pAnalyzerSample\pAnalyzeDrv.efi">
<!-- 1. Driver 註冊初始版本 Protocol -->
<Function name="InstallProtocolInterface" fAddre="0x7FEFCE1A" Structure="EFI_BOOT_SERVICES" sAddr="0x7FF17CA0">
<Input length="4">
<arg1 Type="GUID *" Address="0x7BFE2460" GUID="82F60AAD-45D1-4AF7-BA45-A29084F6E41B" ></arg1>
<arg3 Type="VOID *" Value="0x7BFE2490" ></arg3>
</Input>
<Output Type="UINTN" Value="0x0"> ... </Output>
</Function>
<!-- 2. Driver 替換 Protocol 實例 -->
<Function name="ReinstallProtocolInterface" fAddre="0x7FEFD28B" Structure="EFI_BOOT_SERVICES" sAddr="0x7FF17CA0">
<Input length="4">
<arg2 Type="VOID *" Value="0x7BFE2490" ></arg2>
<arg3 Type="VOID *" Value="0x7BFE2480" ></arg3>
</Input>
<Output Type="UINTN" Value="0x0"> ... </Output>
</Function>
<return Func="0" Driver="0"/>
</Driver>
<Application Name ="FS0:\EFI\Apps\pAnalyzerSample\pAnalyzeApp2.efi">
<!-- 3. 後續 Application 執行 HandleProtocol 查詢 -->
<Function name="HandleProtocol" fAddre="0x7FEFC02D" Structure="EFI_BOOT_SERVICES" sAddr="0x7FF17CA0">
<Input length="3">
<arg1 Type="GUID *" Address="0x7DFBF458" GUID="DD9E7534-7762-4698-8C14-F58517A625AA" ></arg1>
</Input>
<Output Type="UINTN" Value="0x0"> ... </Output>
</Function>
</Application>
</Section>
此節錄記錄驅動程式的 InstallProtocolInterface、ReinstallProtocolInterface,以及後續應用程式的 HandleProtocol 呼叫。節錄中的安裝與查詢 GUID 不同,因此僅憑此段紀錄,無法判定應用程式查詢的是前述安裝的同一個 Protocol。
範例四:X64 可變參數呼叫處理 (MultiProtocol.py)
UEFI 規範中的 InstallMultipleProtocolInterfaces 宣告為:
EFI_STATUS EFIAPI InstallMultipleProtocolInterfaces (
IN OUT EFI_HANDLE *Handle,
... // 成對出現的 (EFI_GUID *, VOID *),以 NULL 結尾
);
在 Python ctypes 中,回呼函式必須在編譯/宣告期就確定引數個數,無法處理變動長度的 ...。
跳板處理方式 (VarArgs.py)
pAnalyzer 藉由 CorePy 在記憶體中動態生成 X64 組合語言跳板:
instructions = (
x86.sub(reg.rsp, 0x68, ignore_active=True),
x86.mov(MemRef(reg.rsp, 0x20), reg.rcx, ignore_active=True), # 保存暫存器引數
x86.mov(MemRef(reg.rsp, 0x28), reg.rdx, ignore_active=True),
x86.mov(MemRef(reg.rsp, 0x30), reg.r8, ignore_active=True),
x86.mov(MemRef(reg.rsp, 0x38), reg.r9, ignore_active=True),
x86.lea(reg.rax, MemRef(reg.rsp, 0x68), ignore_active=True),
x86.mov(MemRef(reg.rsp, 0x40), reg.rax, ignore_active=True), # 保存呼叫端 RSP
x86.lea(reg.rcx, MemRef(reg.rsp, 0x20), ignore_active=True), # 傳入 Snapshot
x86.mov(reg.rax, callback_address, ignore_active=True),
x86.call(reg.rax, ignore_active=True), # 呼叫 Python Dispatcher
x86.add(reg.rsp, 0x68, ignore_active=True),
x86.ret(ignore_active=True),
)
Python Dispatcher 取得堆疊與暫存器快照後,依該介面的 GUID / Interface 配對規則讀取引數,直到 GUID 位置出現 0x0 (NULL 終止符),再建立對應該次引數數量的 ctypes 函式簽章進行轉發。
XML 紀錄節錄 (MultiProtocol.xml)
以下為 4 組 Protocol 同時註冊時(長度為 10,含 Handle 與結尾 NULL)的輸出:
<Function name="InstallMultipleProtocolInterfaces" fAddre="0x7FEFD490" Structure="EFI_BOOT_SERVICES" sAddr="0x7FF17CA0">
<Input length="10">
<arg0 Type="VOID **" Address="0x7D412698" Value="0x0" ></arg0>
<arg1 Type="GUID *" Address="0x7D4126A0" GUID="72FC9000-5AF1-43AC-A981-647BD12E3000" ></arg1>
<arg2 Type="VOID *" Address="0x7D4126E0" ></arg2>
<arg3 Type="GUID *" Address="0x7D4126B0" GUID="72FC9001-5AF1-43AC-A981-647BD12E3001" ></arg3>
<arg4 Type="VOID *" Address="0x7D4126F0" ></arg4>
<arg5 Type="GUID *" Address="0x7D4126C0" GUID="72FC9002-5AF1-43AC-A981-647BD12E3002" ></arg5>
<arg6 Type="VOID *" Address="0x7D412700" ></arg6>
<arg7 Type="GUID *" Address="0x7D4126D0" GUID="72FC9003-5AF1-43AC-A981-647BD12E3003" ></arg7>
<arg8 Type="VOID *" Address="0x7D412710" ></arg8>
<arg9 Type="GUID *" Address="0x0" ></arg9> <!-- NULL 終止符 -->
</Input>
<Output Type="UINTN" Value="0x0">
<arg0 Type="VOID **" Address="0x7D412698" Value="0x7C7EBB18"></arg0> <!-- 分配成功之 Handle -->
...
</Output>
</Function>
此範例說明 InstallMultipleProtocolInterfaces 的可變參數記錄方式。目前的跳板依 X64 呼叫慣例與特定介面參數規則設計,其他架構或其他可變參數函式需另行處理與驗證。