11/06/2026

 

pAnalyzer 技術說明

Important

開發階段說明:
pAnalyzer 目前僅處於初期構想階段,現有程式與範例用於概念驗證 (PoC)。
本文件說明目前的架構、Hook 機制與範例紀錄,不代表功能已定型或已完成跨平台驗證。API 介面、日誌格式與封裝方式仍可能調整。

(由AI編輯)


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 測試為例,說明初始化、攔截與還原流程:

dOut (XML Logger)UEFI Firmware / Target ProtocolpAnalyzer Filter / ThunkpAnalyzer / BaseAnalyzeSample Script (PaTest.py)dOut (XML Logger)UEFI Firmware / Target ProtocolpAnalyzer Filter / ThunkpAnalyzer / BaseAnalyzeSample Script (PaTest.py)1. 初始化與能力建立2. Hook 安裝與啟動alt[一般函式][VarArgs 可變參數函式 (X64)]3. 執行期呼叫攔截 (Runtime Interception)4. 還原與日誌儲存pAnalyzer(dOut)1初始化指定 Boot Services 的 Hook (_ProtocolFilter)2Build_Capability(TargetProtocol)3ctypes 反射解析結構體 Offset / Size / Type4Detect_Protocol(Guid, Protocol)5LocateHandleBuffer() 取得現有實例記憶體位址6Filter.append(Build_Filter(Target, Handle))7install("QueryMode", _filter_func)8start("QueryMode")9產生 ctypes.CFUNCTYPE 回呼位址10CorePy 動態組譯生成 X64 Entry Thunk11將目標結構中的函式指標設為 Hook Entry12呼叫 ConOut.QueryMode(Mode=10, ...)13寫入 <Function>, <Input> 原始引數 (Mode=10)14觸發 Pre_func(_filter_func)15調整引數 (Mode 10 -> 0) 並記錄 <Change>16呼叫原始函式 Native_func(Mode=0, ...)17回傳 EFI_SUCCESS (0x0)18寫入 <Output> 回傳值與輸出引數19回傳結果給原呼叫者20stop() / uninstall("QueryMode")21還原 Protocol VTable 為 Native 函式位址22dOut.terminate() (寫入 XML 檔案)23

流程說明

  1. 結構體反射與型別識別 (Build_Capability):
    pAnalyzer 藉由 Python ctypes 的結構內省功能,遍歷 Protocol 結構(如 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL),自動識別哪些欄位是函式指標 (_CFuncPtr)、各欄位的記憶體位移 (offset) 以及型別宣告。
  2. 實例探索 (Detect_Protocol):
    呼叫 UEFI 原生 gBS.LocateHandleBuffer 與 gBS.HandleProtocol,獲取當前記憶體中已註冊的所有目標協定實例與 Handle。
  3. 動態替換函式指標 (VTable Hooking):
    在 start() 執行時,pAnalyzer 直接改寫目標結構體在記憶體中的函式指標位址(以指標賦值的方式改寫記憶體)。
    • 對於一般函式:利用 ctypes 建立 C 語言可呼叫的 callback wrapper。
    • 對於目前支援的可變參數函式:由 VarArgs.py 使用 CorePy 產生 X64 組合語言跳板 (Thunk),保存引數資訊並交由 Python 處理。
  4. 呼叫前後的處理 (Pre-func / Post-func):
    當外部驅動或程式呼叫該函式時,控制權進入 Hook:
    • Pre-func:檢視傳入參數,並可回傳調整後的引數供原始函式使用。
    • Native-func:呼叫原始韌體函式。
    • Post-func:取得韌體回傳值與引數,供後續檢查或處理。目前一般函式路徑仍回傳原始 rest,並未採用回呼回傳的 fRest 覆寫函式回傳值。
  5. 函式指標還原 (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&#13;&#10;" ></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 呼叫慣例與特定介面參數規則設計,其他架構或其他可變參數函式需另行處理與驗證。

No comments:

Post a Comment