[Windows] 逆向工程 C++ 中入口函數參數 main(argc, argv) 與如何正確的進行參數劫持
前言
我在 Github 上三年前開源了精簡的以 C/C++ 撰寫的 Windows 執行程式裝載器:aaaddress1/RunPE-In-Memory 用以從記憶體執行以 C/C++ 開發的 *.exe 程式不必以 CreateProcess() 來叫起,算是一種概念性 PoC 啦畢竟這種手段在惡意程式和殼上經常使用,所以寫一寫就丟出來給大家玩 XD
不過有人在我的專案發起了 Issue:既然能從記憶體中執行 *.exe 那能否偽造 *.exe 取得的參數呢?嗯好問題XD 正逢去年蠻忙的所以一直懶得弄,這個月比較閒於是就決定在專案中新增 參數偽造 的功能,這邊簡單提一下 C++ 中 main() 入口所收的 argc 與 argv 來的。
[附註] 以下逆向工程內容以 MinGW 生產的程式為例,系統逆向工程為 Win10 企業版 1809版
CRTStartup
在 Windows 下通常取得參數通常分為兩種狀況:
- WINAPI GetCommandLineW() 系統函數返回字串指針指向當前完整應用程式參數
- 從 main(argc, argv) 拿回來的參數,那麼第二種狀況在編譯器是怎麼吐參數給開發者 main 入口的呢?
在編譯器吐出執行程式後,連結器會生產一段 CRTStartup 的函數用於做一些基礎的例如像是 Security Cookies、Control Flow Guard 保護的初始化,在一系列初始化之後 .data 段的全域變數 _argv、argc 紀錄著要傳入給開發者入口函數的 參數陣列 與 數量,就以參數形式呼叫 main() 函數
好問題,那這兩個全域變數是在哪裡被初始化的?
你會發現以 MinGW 為例的話,會去呼叫 msvcrt!__getmainargs() 來取得參數資訊保存到 .data 全域變數上,後續再傳遞給 main() 函數;如果是 VC++ 吐出來的則是直接呼叫 GetCommandLineW() 在做文字切割取出參數
MSVCRT
MSVCRT.dll 有導出一系列 C 函數是引用 #include <stdio.h> 時會用到的,例如 printf, sprintf, gets 等等。我們剛剛提到了其導出函數 __getmainargs() 負責解析。事實上這個函數內部實作也只是把它全域變數所記錄的參數指過去到我們的指針上面而已XD
那麼到底是什麼時候初始化的呢?
逆向工程入 MSVCRT.dll 初始化階段,你會發現上述三個參數指針也只是調用了 GetCommandLineW() 與 GetEnvironmentStringA() 後,將參數內容先拷貝一份到 MSVCRT.dll 全域變數中,等到有人調用 __getmainargs() 時,就會把 MSVCRT.dll 先前所記錄到的參數資訊指針傳遞給你
那麼 GetCommandLineW 內部實作呢?
KERNELBASE
在 KernelBase.dll 中其實也有跟 MSVCRT 做法一致,會先將當前應用程式參數以 RtlUnicodeString() 備份一份到全域變數中、當有人調用 GetCommandLineW() 時,就會把先前備份的參數內容吐給你。而 KernelBase.dll 中備份的參數內容是來自於 Loader 初始化階段的 PEB() 結構塊所記錄的參數資訊
參數劫持
好,上述所有資訊擺在一起,就可以回答很多人疑惑的「直接覆蓋 PEB→ProcessParmeters→CommandLine 無法劫持參數」的問題了
當有一個應用程式是 Console 程式取得 main() 參數時,基本上參數傳遞流程會是:
- Loader 生成 PEB!ProcessParemeters 紀錄了完整程式命令參數字串
- KernelBase.dll 函數入口備份一份至 KernelBase.dll 全域變數
- GetCommandLineW() 函數被呼叫時將 KernelBase 全域變數紀錄的參數傳遞給 MSVCRT.dll
- MSVCRT.dll 函數入口將 GetCommandLineW() 取得的參數字串備份一份到 MSVCRT.dll 全域變數中紀錄
- __getmainargs() 被呼叫時,將 MSVCRT.dll 全域變數紀錄的參數內容下放給 CRTStartup
- main() 函數入口被呼叫時取得參數
就從劫持面來說,如果你想覆蓋掉的 參數文字變數 共計會有:
- PEB塊
- MSVCRT.dll 全域變數所紀錄的三組指針上保存的 MSVCRT 變數
- KERNELBASE.dll 全域變數所紀錄的三組指針上保存的 MSVCRT 變數
所以在看雪之類的地方通常大神會告訴你:直接去 IAT Hook 劫持 GetCommandLineW() 跟 __getmainargs() 比較快是有原因的 XD
當然還有另一種做法是可以採 RunPE 的手段,先創建 SUSPENDED 狀態的進程,這時候上述所有模組都沒被初始化的狀況下就先抹掉 PEB 上的參數紀錄,接著再恢復進程運作可以優雅地達成劫持,不過這種做法防毒 100% 會叫啦太敏感惹
感謝大大無私分享,樓主一生平安喜樂
回覆刪除#include "stdafx.h"
刪除#include
#include "peBase.hpp"
#include "fixIAT.hpp"
#include "fixReloc.hpp"
bool peLoader(const char *exePath, const wchar_t* cmdline)
{
LONGLONG fileSize = -1;
BYTE *data = MapFileToMemory(exePath, fileSize);//读取该文件到内存
BYTE* pImageBase = NULL;
LPVOID preferAddr = 0;//LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候在转换回来
IMAGE_NT_HEADERS *ntHeader = (IMAGE_NT_HEADERS *)getNtHdrs(data);//得到NT头的地址
if (!ntHeader)
{
printf("[+] File %s isn't a PE file.", exePath);
return false;
}
IMAGE_DATA_DIRECTORY* relocDir = getPeDir(data, IMAGE_DIRECTORY_ENTRY_BASERELOC);//对PE文件的地址进行重定位
preferAddr = (LPVOID)ntHeader->OptionalHeader.ImageBase;//内存预先加载的目标基址(RAV)
printf("[+] Exe File Prefer Image Base at %x\n", preferAddr);
HMODULE dll = LoadLibraryA("ntdll.dll");//便利API函数
((int(WINAPI*)(HANDLE, PVOID))GetProcAddress(dll, "NtUnmapViewOfSection"))((HANDLE)-1, (LPVOID)ntHeader->OptionalHeader.ImageBase);//这是啥?
pImageBase = (BYTE *)VirtualAlloc(preferAddr, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pImageBase && !relocDir)//检测基址和重定位
{
printf("[-] Allocate Image Base At %x Failure.\n", preferAddr);
return false;
}
if (!pImageBase && relocDir)//如果基址被占用则重新申请一块地址
{
printf("[+] Try to Allocate Memory for New Image Base\n");
pImageBase = (BYTE *)VirtualAlloc(NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pImageBase)
{
printf("[-] Allocate Memory For Image Base Failure.\n");
return false;
}
}
puts("[+] Mapping Section ...");
ntHeader->OptionalHeader.ImageBase = (size_t)pImageBase;
memcpy(pImageBase, data, ntHeader->OptionalHeader.SizeOfHeaders);//复制确认PE文件头部的大小
IMAGE_SECTION_HEADER * SectionHeaderArr = (IMAGE_SECTION_HEADER *)(size_t(ntHeader) + sizeof(IMAGE_NT_HEADERS));//扩展头的地址
for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
{
printf(" [+] Mapping Section %s\n", SectionHeaderArr[i].Name);
memcpy
(
LPVOID(size_t(pImageBase) + SectionHeaderArr[i].VirtualAddress),
LPVOID(size_t(data) + SectionHeaderArr[i].PointerToRawData),
SectionHeaderArr[i].SizeOfRawData
);
}
// for demo usage:
// masqueradeCmdline(L"C:\\Windows\\RunPE_In_Memory.exe Demo by aaaddress1");
masqueradeCmdline(cmdline);
fixIAT(pImageBase);//导入IAT表,同时输出了每个API调用的地址
if (pImageBase != preferAddr) //判断文件是否加载到目标地址,否则进行重定位
if (applyReloc((size_t)pImageBase, (size_t)preferAddr, pImageBase, ntHeader->OptionalHeader.SizeOfImage))
puts("[+] Relocation Fixed.");
size_t retAddr = (size_t)(pImageBase)+ntHeader->OptionalHeader.AddressOfEntryPoint;
printf("Run Exe Module: %s\n", exePath);
((void(*)())retAddr)();
}
int main(int argc, char **argv)
{
if (argc != 2)//运行的命令参数是两个,也就是自身运行文件,和另外一个运行文件
{
printf("Usage: %s [Exe Path]", strrchr(argv[0], '\\') ? strrchr(argv[0], '\\') + 1 : argv[0]);/*C 库函数 char* strrchr(const char* str, int c)
在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置该函数
返回值:
返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针*/
getchar();
return 0;
}
peLoader(argv[1], NULL);//读取PE文件的路径
getchar();
return 0;
}
感謝分享
回覆刪除