早在 2002 年初學 VC++ 時,我曾編寫過一個文件捆綁工具,當時是為了配合自製的 QQ 木馬使用。後來在某技術論壇有網友詢問相關技術,我便整理了當時的代碼撰寫成文檔。今日整理硬盤時發現此文,雖然原理簡單,但對於理解 PE 文件結構與進程注入仍有參考價值,特發布供大家學習參考。
文件捆綁技術用途廣泛:除惡意軟體(如木馬)常用外,正規軟體發布時也常將 DLL 捆綁在 EXE 尾部,運行時動態釋放並調用,減少部署文件數量。
PE(Portable Executable)格式文件的一大特性:只要不修改文件頭與中間區段,僅在文件末尾追加數據,原程序仍能正常執行。例如在命令列執行 copy /b test1.exe+test2.exe target.exe,會將 test2.exe 內容附加到 test1.exe 末尾。雖然合併後的 target.exe 能正常運行(實際運行的是前段 test1.exe 代碼),但 test2.exe 並不會被執行。解決這一問題正是本文的目標。
我們將三個文件合併為一個 target.exe,結構如下:
--------------------------------------- | aaa.exe | test1.exe | test2.exe | ---------------------------------------
其中 aaa.exe 是我們自行編寫的「解包啟動器」,test1.exe 和 test2.exe 是待捆綁的目標程序。執行 target.exe 時,系統只會加載並運行前段的 aaa.exe。因此,aaa.exe 必須完成以下任務:
c:\test1.exe、c:\test2.exe)。CreateProcess 建立兩個新進程並行運行這兩個程序。ExitProcess 結束自身進程,實現「一鍵雙開」效果。為了隱蔽性,aaa.exe 可設置為無窗口模式(隱藏主窗口)。
aaa.exe 運行時,如何知道 test1.exe 和 test2.exe 在文件中的具體偏移與長度?解決方案是在文件末尾追加長度標識:
----------------------------------------------------- | aaa.exe | test1.exe | test2.exe | len2 | len1 | -----------------------------------------------------
其中 len1 和 len2 分別佔用固定 30 字節,存儲 test1.exe 和 test2.exe 的文件長度(以 ASCII 字符串形式)。aaa.exe 啟動時,先讀取文件末尾 60 字節,解析出兩個長度,再據此計算偏移精確讀取兩個文件內容。
// 獲取 target.exe 自身文件句柄與總長度
CFile fSource(_pgmptr, CFile::modeRead | CFile::modeNoTruncate);
int iSourceLength = fSource.GetLength();
// 定位到倒數第 60 字節(len2 起始位置)
fSource.Seek(iSourceLength - 60, CFile::begin);
char buffer[40];
ZeroMemory(buffer, 40);
fSource.Read(buffer, 30); // 讀取 len2(test2.exe 長度)
int iTargetLength = atoi(buffer); // 轉換為整數
// 定位到 test2.exe 數據起始處
fSource.Seek(iSourceLength - iTargetLength - 60, CFile::begin);
// 創建目標文件並寫入數據
CFile fTarget("c:\\test2.exe", CFile::modeCreate | CFile::modeWrite | CFile::modeNoTruncate);
char *pBuffer = new char[iTargetLength];
ZeroMemory(pBuffer, iTargetLength);
fSource.Read(pBuffer, iTargetLength);
fTarget.Write(pBuffer, iTargetLength);
delete []pBuffer;
同理處理 test1.exe。完整代碼請參考文末下載鏈接。
本文演示了基於 PE 文件特性的簡易文件捆綁實現。雖然代碼較早期,但核心思路——「尾部追加數據 + 長度標識 + 運行時釋放執行」——是許多打包器、加殼器的基礎原型。實際應用中還需考慮路徑動態獲取、錯誤處理、權限提升、殺毒軟體查殺等工程化問題。