x64dbg插件編寫基礎
?
一、x64dbg擴展功能的三種方式
x64dbg擴展功能的方式有三種:
① 寫腳本(python或者idc)。
② 腳本DLL,就是編寫一個DLL導出 AsyncStart() or Start(),然后通過命令scriptdll/dllscript來加載DLL執行代碼。
③ 編寫插件。
在x64dbg的文檔中并沒有說明 "插件" 到底是一個什么東西,只是說插件的后綴名叫dp32或者dp64,通過觀察其他的插件發現其實就是DLL,只不過是導出了一些指定函數的DLL。
本文的目的就是說明編寫x64dbg插件的步驟,然后編寫一個簡單的x64dbg插件。
二、環境配置
本機環境:win11 21h2、vs2022、x64dbg(snapshot_2023-01-25_11-53)
2.1 x64的SDK
要給x64dbg編寫插件肯定需要x64dbg提供的sdk包,sdk包就在x64dbg的根目錄之下的pluginsdk文件夾中:
├───pluginsdk│ ├───dbghelp│ ├───DeviceNameResolver│ ├───jansson│ ├───lz4│ ├───TitanEngine│ └───XEDParse└───release ├───...
在pluginsdk中有x64dbg自己提供的頭文件和lib文件,以及它使用的其他第三方庫的頭文件和lib文件,比如XEDParse、TitanEngine等。
2.2 配置vs工程
先創建一個VS的DLL工程,這一步不必多說。
將整個pluginsdk文件夾拷貝到工程目錄之下,雖然多了很多東西,但是比少拷貝了文件去找要好。
然后配置頭文件目錄和lib文件的目錄以及引入lib文件:
在工程屬性->vc++目錄->外部包含目錄加入pluginsdk路徑;
在工程屬性->vc++目錄->庫目錄加入pluginsdk路徑;
在工程屬性->鏈接器->輸入->附加依賴項中添加 x32bridge.lib和x32dbg.lib,這也是pluginsdk下唯二的兩個lib。
三、基本導出函數
3.1 pluginit
函數聲明:
extern "C" __declspec(dllexport) bool pluginit(PLUG_INITSTRUCT* initStruct);
pluginit函數是x64dbg插件必須導出的一個函數。
在整個函數中,要做的事件就是填充參數initStruct:
struct PLUG_INITSTRUCT{ [IN] int pluginHandle; //插件的句柄 [OUT] int sdkVersion; //填 PLUG_SDKVERSION 即可 [OUT] int pluginVersion; // 填插件的版本 [OUT] char pluginName[256]; // 填插件指針};
所以在pluginit中要做的事情就是填寫插件的基本信息:
//必需的,插件初始化函數extern "C" __declspec(dllexport) bool pluginit(PLUG_INITSTRUCT * initStruct){ _plugin_logprintf("[%s] pluginit\r", TAG);//打印日志 memcpy_s( initStruct->pluginName, sizeof(initStruct->pluginName), TAG, strlen(TAG)); initStruct->sdkVersion = PLUG_SDKVERSION; initStruct->pluginVersion = 1; if (!InitPlugin())//做一些初始化動作 { _plugin_logprintf("[%s] pluginit Failed\r", TAG);//打印日志 return false; } return true;}
3.2 plugstop和plugsetup
這兩個導出函數不是必須的,但是可以在里面做一些事情:
plugstop:插件被移除的時候被調用,可以用來清除注冊的回調和命令,清理資源。
plugsetup:當插件初始化成功的時候調用,可以在這里添加菜單、做其他的界面相關的事情。
本例在plugsetup中添加了兩個子菜單,代碼如下:
//非必需,插件被移除時調用extern "C" __declspec(dllexport) bool plugstop(){ _plugin_logprintf("[%s] plugstop\r", TAG); return true;} //非必需,啟動插件時調用//在這里執行UI操作,比如增加菜單extern "C" __declspec(dllexport) void plugsetup(PLUG_SETUPSTRUCT * setupStruct){ _plugin_logprintf("[%s] plugsetup\r", TAG); //添加子菜單 _plugin_menuaddentry(setupStruct->hMenu, 0, "enable UEH"); _plugin_menuaddentry(setupStruct->hMenu, 1, "disable UEH");}
四、事件回調函數
當我們添加了插件的子菜單之后,要如何響應菜單的點擊呢?
導出以CB開頭的函數就可以去接收到對應的事件,比如:
extern "C" __declspec(dllexport) void CBINITDEBUG(CBTYPE cbType, PLUG_CB_INITDEBUG* info); //初始化調試extern "C" __declspec(dllexport) void CBSTOPDEBUG(CBTYPE cbType, PLUG_CB_STOPDEBUG* info); //停止調試extern "C" __declspec(dllexport) void CBEXCEPTION(CBTYPE cbType, PLUG_CB_EXCEPTION* info); //異常extern "C" __declspec(dllexport) void CBDEBUGEVENT(CBTYPE cbType, PLUG_CB_DEBUGEVENT* info); //調試事件extern "C" __declspec(dllexport) void CBMENUENTRY(CBTYPE cbType, PLUG_CB_MENUENTRY* info); //點擊子菜單
參數中所用到的結構體可以在這里找到:https://help.x64dbg.com/en/latest/developers/plugins/Callbacks/index.html,只要是滿足CB*的導出函數,且屬于這里面的類型。https://help.x64dbg.com/en/latest/developers/plugins/API/registercallback.html,應該都可以注冊成功,注意函數名中不要有下劃線。
本例中,我們只需要簡單的響應一下子菜單的點擊:
//菜單響應回調extern "C" __declspec(dllexport) void CBMENUENTRY( CBTYPE bType, PLUG_CB_MENUENTRY * pEntry){ switch (pEntry->hEntry) { case 0: //注冊時填的菜單ID EnableUeh(); break; case 1: DisableUeh(); break; default: break; }}
五、庫函數
在插件中,我們可以調用Dbg開頭的函數來輔助功能,函數列表:https://help.x64dbg.com/en/latest/developers/functions/debug/index.html
比如讀寫被調試進程可以使用:
bool DbgMemRead( duint va, void* dest, duint size); bool DbgMemWrite(duint va,void* dest,duint size);
其他功能就暫時還沒探索,本例中用這兩個就夠啦。
六、對UnhandledExceptionFilter打補丁
本例中的插件需要實現對UnhandledExceptionFilter打補丁的功能,實現調試器可以調試UEH回調
6.1 為什么要打補丁
下面是筆者的通過測試的一點淺薄理解,沒有跟蹤系統的代碼,如有不對的地方還請大佬們指出。
應用層派發異常的流程大致如下:

根據上面的流程圖,在異常從SEH中出來的時候,根據是否有調試器,要么派發給調試器,要么派發給UEH回調,二選一,所以在調試的過程中不會之下UEH回調的代碼。
為了能在調試器中調試UEH回調,我們需要改變一下系統異常分發的流程,使其走到另一個分支去。
這個分支出現在kernelbase/kernel32!UnhandledExceptionFilter 中,所以需要對其打補丁。
6.2 實現功能
剩下的步驟就很簡單了,首先在插件初始化時,判斷系統版本,獲取UnhandledExceptionFilter 的地址,通過特征碼找到需要打補丁的地址。
bool InitPlugin(){ //獲取版本號 DWORD dwBuildVer = GetVerSion(); PUCHAR pFuncBegin = (PUCHAR)GetProcAddress( GetModuleHandleA("kernelbase"), "UnhandledExceptionFilter"); if (pFuncBegin == NULL) { pFuncBegin = (PUCHAR)GetProcAddress( GetModuleHandleA("kernel32"), "UnhandledExceptionFilter"); if (pFuncBegin == NULL) { _plugin_logprintf("[%s] Get kernelbase!UnhandledExceptionFilter Addr Failed\r", TAG); bIsEnabledUeh = false; return false; } } //獲取特征碼 if (dwBuildVer >= 22000) { //win11 g_pUehSig = UehSigWin11; _plugin_logprintf("[%s] OS Build Number: %d\r", TAG, dwBuildVer); } else if (dwBuildVer == 7600 || dwBuildVer == 7601) { //win7 g_pUehSig = UehSigWin7; _plugin_logprintf("[%s] OS Build Number: %d\r", TAG, dwBuildVer); } g_pUehPatchPoint = FindSignatureCode(pFuncBegin, 0x100, g_pUehSig); if (g_pUehPatchPoint == NULL) { _plugin_logprintf("[%s] Signatrue Not Found!\r", TAG); bIsEnabledUeh = false; return false; } return true;}
然后響應菜單事件,當點擊EnableUeh時,打補丁,點擊DisableUeh時,恢復補丁。
void EnableUeh(){ //保存原來的數據 DbgMemRead((duint)g_pUehPatchPoint, UehPatchRawData, strlen((const char*)g_pUehSig) / 2); //打補丁 if (DbgMemWrite((duint)g_pUehPatchPoint, UehPathCode, sizeof(UehPathCode))) { _plugin_logprintf("[%s] Enable UEH Success!\r", TAG); bIsEnabledUeh = true; } else { _plugin_logprintf("[%s] Enable UEH Failed!\r", TAG); bIsEnabledUeh = false; }} void DisableUeh(){ if (bIsEnabledUeh == true) { //Patch if (DbgMemWrite((duint)g_pUehPatchPoint, UehPatchRawData, strlen((const char*)g_pUehSig) / 2)) { _plugin_logprintf("[%s] Enable UEH Success!\r", TAG); bIsEnabledUeh = false; } else { _plugin_logprintf("[%s] Enable UEH Failed!\r", TAG); } }}
一個簡單的插件框架就這樣完成了。