通過修改PE載入DLL
基本概念
除了DLL動態注入技術外,還可以通過手工修改PE檔案的方式來載入DLL,這種方式只要應用過一次之後,每當程序開始執行時便會自動載入指定的DLL。
整體思路如下:
1、檢視IDT是否有充足的空間,若無則移動IDT至其他位置,若有則直接新增至列表末尾;
2、若無,修改OPTIONAL頭IMPORT TABLE的RVA值並增大Size值,刪除繫結匯入表BOUND IMPORT Table,複製原IAT內容到目標地址並設定INT、NAME、IAT,最後到.rdata節區頭修改IAT屬性值新增可寫屬性。
編寫測試程式和DLL檔案
下面練習的目標是編寫簡單的文字檢視程式SKI12Viewer.exe,直接修改SKI12Viewer.exe檔案使其在執行時自動載入myhack3.dll檔案。
首先編寫SKI12Viewer.exe。
SKI12Viewer.cpp
//SKI12Viewer.cpp #include "windows.h" #include "stdio.h" TCHAR szAppName[] = L"SKI12Viewer" ; TCHAR szFile[MAX_PATH] = {0,}; TCHAR szMsg[2048] = {0,}; #define MAX_BUF_SIZE (32768) LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam){ static HWND hwndEdit ; HFONT hFont; switch(iMsg){ case WM_CREATE : hwndEdit = CreateWindow(L"Edit", NULL, WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL, 0, 0, 0, 0, hwnd,(HMENU) 1, ((LPCREATESTRUCT)lParam)->hInstance, NULL); hFont=CreateFont(16,0,0,0,0,0,0,0,0,0,0,0,0,L"Courier New"); SendMessage(hwndEdit, WM_SETFONT, (WPARAM)hFont, (LPARAM)FALSE); DragAcceptFiles(hwnd, TRUE); return 0; case WM_DROPFILES : if( DragQueryFile((HDROP)wParam, 0, szFile, MAX_PATH) ){ HANDLE hFile = CreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if( hFile == INVALID_HANDLE_VALUE ){ wsprintf(szMsg, L"[-]File(\"%s\") open error!!! [%d]\n", szFile, GetLastError()); MessageBox(hwndEdit, szMsg, szAppName, MB_OK); return 0; } DWORD dwBytesRead = 0; char *pBuf = new char[MAX_BUF_SIZE]; ZeroMemory(pBuf, MAX_BUF_SIZE); ReadFile(hFile, pBuf, MAX_BUF_SIZE, &dwBytesRead, NULL); SetWindowTextA(hwndEdit, pBuf); wsprintf(szMsg, L"SKI12Viewer (%s)", szFile); SetWindowText(hwnd, szMsg); delete []pBuf; CloseHandle(hFile); } return 0; case WM_SETFOCUS : SetFocus(hwndEdit); return 0; case WM_SIZE : MoveWindow(hwndEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); return 0; case WM_DESTROY : PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, iMsg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){ HWND hwnd ; MSG msg ; WNDCLASSEX wndclass ; wndclass.cbSize = sizeof(wndclass); wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wndclass); hwnd = CreateWindow( szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while( GetMessage(&msg, NULL, 0, 0) ){ TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }
注意,由於這次除錯一開始選擇控制檯程式,但程式程式碼的入口函式為WinMain(),所以在專案中右鍵>屬性>配置屬性>連結器>系統,在其中的子系統項中勾選視窗(/SUBSYSTEM:WINDOWS)即可。
開啟SKI12Viewer.exe,然後將SKI12Viewer.cpp拖入視窗中可檢視原始碼:
使用PEView檢視SKI12Viewer.exe的IDT(Import Directory Table),可以看到直接匯入的DLL檔案有KERNEL32.dll、USER32.dll、GDI32.dll、SHELL32.dll、MSVCR100.dll:
接著編寫DLL檔案。
myhack3.cpp
//myhack3.cpp
#include "stdio.h"
#include "windows.h"
#include "shlobj.h"
#include "tchar.h"
//包含InternetOpen(),InternetOpenUrl(),InternetReadFile()等API
#include "Wininet.h"
#pragma comment(lib, "Wininet.lib")
#define DEF_BUF_SIZE (4096)
#define DEF_URL L"http://127.0.0.1/phpinfo.php"
#define DEF_INDEX_FILE L"phpinfo.html"
HWND g_hWnd = NULL;
#ifdef __cplusplus
extern "C" {
#endif
//出現在IDT中的匯出函式dummy()是myhack3.dll向外部提供服務的匯出函式,並無任何功能
//僅為了保持形式上的一致性,使DLL檔案能夠順利新增到SKI12Viewer.exe程序中
__declspec(dllexport) void dummy(){
return;
}
#ifdef __cplusplus
}
#endif
//下載目標URL內容
BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile){
BOOL bRet = FALSE;
HINTERNET hInternet = NULL, hURL = NULL;
BYTE pBuf[DEF_BUF_SIZE] = {0,};
DWORD dwBytesRead = 0;
FILE *pFile = NULL;
errno_t err = 0;
//獲取Internet控制代碼
hInternet = InternetOpen(L"SKI12", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
if( NULL == hInternet ){
OutputDebugString(L"[-]InternetOpen() failed!");
return FALSE;
}
//開啟目標URL
hURL = InternetOpenUrl(hInternet, szURL, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if( NULL == hURL ){
OutputDebugString(L"[-]InternetOpenUrl() failed!");
goto _DownloadURL_EXIT;
}
if( err = _tfopen_s(&pFile, szFile, L"wt") ){
OutputDebugString(L"[-]fopen() failed!");
goto _DownloadURL_EXIT;
}
//讀取目標網頁資訊
while( InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead) ){
if( !dwBytesRead ){
break;
}
//將讀取的網頁資訊寫入本地檔案
fwrite(pBuf, dwBytesRead, 1, pFile);
}
bRet = TRUE;
//goto語句跳轉的地址,即程式終止時跳轉至此處執行
_DownloadURL_EXIT:
if( pFile ){
fclose(pFile);
}
if( hURL ){
InternetCloseHandle(hURL);
}
if( hInternet ){
InternetCloseHandle(hInternet);
}
return bRet;
}
//獲取目標程序PID
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam){
DWORD dwPID = 0;
GetWindowThreadProcessId(hWnd, &dwPID);
if( dwPID == (DWORD)lParam ){
g_hWnd = hWnd;
return FALSE;
}
return TRUE;
}
//從目標程序PID獲取目標程序主視窗控制代碼
HWND GetWindowHandleFromPID(DWORD dwPID){
EnumWindows(EnumWindowsProc, dwPID);
return g_hWnd;
}
//將下載的檔案拖入SKI12Viewer.exe程序中顯示
BOOL DropFile(LPCTSTR wcsFile){
HWND hWnd = NULL;
DWORD dwBufSize = 0;
BYTE *pBuf = NULL;
DROPFILES *pDrop = NULL;
char szFile[MAX_PATH] = {0,};
HANDLE hMem = 0;
WideCharToMultiByte(CP_ACP, 0, wcsFile, -1, szFile, MAX_PATH, NULL, NULL);
dwBufSize = sizeof(DROPFILES) + strlen(szFile) + 1;
//分配記憶體
if( !(hMem = GlobalAlloc(GMEM_ZEROINIT, dwBufSize)) ){
OutputDebugString(L"[-]GlobalAlloc() failed!!!");
return FALSE;
}
//鎖定緩衝區
pBuf = (LPBYTE)GlobalLock(hMem);
pDrop = (DROPFILES*)pBuf;
pDrop->pFiles = sizeof(DROPFILES);
strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile)+1, szFile);
//解鎖緩衝區
GlobalUnlock(hMem);
//獲取目標程序的主視窗控制代碼
if( !(hWnd = GetWindowHandleFromPID(GetCurrentProcessId())) ){
OutputDebugString(L"[-]GetWndHandleFromPID() failed!!!");
return FALSE;
}
//向目標程序主視窗傳送WM_DROPFILES訊息
PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);
return TRUE;
}
//執行緒函式
DWORD WINAPI ThreadProc(LPVOID lParam){
TCHAR szPath[MAX_PATH] = {0,};
TCHAR *p = NULL;
OutputDebugString(L"[*]ThreadProc() start...");
GetModuleFileName(NULL, szPath, sizeof(szPath));
//_tcsrchr:相容Unicode和ANSI編碼,從一個字串中查詢字元
if( p = _tcsrchr(szPath, L'\\') ){
//_tcscpy_s:字串拷貝函式,字尾_s表示使用安全的函式,防止緩衝區不夠大而引起錯誤
//wcslen:取寬字元字串中字元長度
_tcscpy_s(p+1, wcslen(DEF_INDEX_FILE)+1, DEF_INDEX_FILE);
OutputDebugString(L"[*]DownloadURL() start...");
//若下載URL成功,則將下載的檔案拖入SKI12Viewer.exe程序中
if( DownloadURL(DEF_URL, szPath) ){
OutputDebugString(L"[*]DropFlie() start...");
DropFile(szPath);
}
}
OutputDebugString(L"[*]ThreadProc() end...");
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
switch( fdwReason ){
case DLL_PROCESS_ATTACH:
//建立執行緒,執行完執行緒函式之後,關閉程序控制代碼
CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL));
break;
}
return TRUE;
}
在PE檔案中匯入某個DLL,實質是在檔案程式碼內呼叫該DLL提供的匯出函式。myhack3.dll至少需要向外提供1個以上的匯出函式才能保持形式上的一致,因此需要編寫形式完整但無任何功能的dummy()函式。
修改SKI12Viewer.exe實現DLL注入
基本思路為,PE檔案中匯入的DLL資訊以結構體列表的形式儲存在IDT中,只需將myhack3.dll新增到列表尾部即可,前提是檢視IDT是否有足夠的空間。
1、檢視IDT是否有足夠空間:
使用PEView檢視SKI12Viewer.exe的IDT地址(PE檔案頭的IMAGE_OPTIONAL_HEADER結構體中匯入表RVA值即為IDT的RVA):
可以看到,IDT的地址RVA為2334。接著在PEView設定地址檢視為RVA(View>Address>RVA),然後到RVA 2334中檢視,可以發現其在.rdata節區中:
IDT即IMAGE_IMPORT_DESCRIPTOR(IID)結構體組成的陣列,且陣列末尾以NULL結構體結束。由於每個匯入的DLL檔案都對應1個IID結構體(每個IID結構體的大小為14個位元組),因此,圖中整個IID區域為RVA 2334~23AC(整體大小為14*6=78)。
再將PEView檢視改為File Offset,可以看到IDT的檔案偏移為1334~13AC區域:
接著使用Win Hex檢視該檔案偏移:
可以看到,該區域有6個IID結構體,其中最後一個為NULL結構體。由於在IDT尾部存在其他資料,並沒有足夠的空間來新增myhack3.dll的IID結構體。
2、移動IDT:
移動IDT至其他位置主要有三種:
(1)查詢檔案中的空白區域;
(2)增加檔案最後一個節區的大小;
(3)在檔案末尾新增新節區。
下面使用第一種方法移動IDT。首先嚐試在.rdata節區尾部查詢空白區域:
可以看到,.rdata節區末尾雖然存在NULL-Padding區域,但其大小明顯不足以放入IDT。
接著換個節區檢視,到.reloc節區末尾檢視,可以發現存在一大片NULL-Padding區域:
然而,還要確認該區域是否全是空白可用的區域,因為並不是檔案中的所有區域都會被無條件載入到程序的虛擬記憶體的,只有節區頭中明確記錄的區域才會被載入。到.reloc節區頭檢視:
可以看到,.reloc節區在磁碟檔案中的大小為400,在記憶體中的大小為24E。剩餘未被使用的區域大小為400 - 24E = 1B2 >修改後 IDT的大小8C,即可以確定該NULL-Padding區域為624E~6400。
那麼,從RVA 6250(RAW 2050)開始建立IDT。
基本操作的步驟為,先使用PEView開啟SKI12Viewer.exe,檢視PE資訊,根據該資訊使用Win Hex對另外儲存的SKI12Viewer_Patch.exe程序修改。
1、修改匯入表的RVA值:
IMAGE_OPTIONAL_HEADER的匯入表結構成員用來指出IDT的位置(RVA)與大小:
將該匯入表的RVA值為2334,將其修改為新IDT的RVA值6250,在Size值的基礎上加上14即修改為8C:
2、刪除繫結匯入表:
BOUNG_IMPORT_TABLE是一種提高DLL載入速度的技術。若想正常匯入指定的DLL檔案,需要向繫結匯入表新增資訊。然而繫結匯入表是可選項,不是必須存在的,因而可以刪除(修改其值為0)以更方便地操作。但是若存在的話,當其中的內容記錄錯誤時,會引發程式執行出錯。
可以看到,此處繫結匯入表本來已經是刪除了的:
3、建立新IDT:
使用Win Hex完全複製原IDT(RAW 1334~13AC),然後覆寫到新IDT的位置(RAW 2050):
在新IDT的下面位置挑選一個地方,這裡選擇RAW 2100地址處(經過PEView可知RVA為6300)設定myhack3.dll的Name、INT和IAT:
地址2100處的6330為RVA地址,其為INT,即指向RAW 2130地址處;同理地址2120為IAT,同樣指向RAW 2130地址處;地址2110處儲存著Name,即包含匯入函式的“myhack3.dll”字串名稱。INT和IAT指向的2130地址,其中前面的0000為匯入函式的Ordinal,後面的儲存著myhack3.dll的匯入函式字串名稱“dummy”。
接著在IDT尾部(RAW 20B4)新增與myhack3.dll對應的IID:
4、修改IAT節區的屬性值:
載入PE檔案到記憶體,PE裝載器會修改IAT,寫入函式的實際地址,因此相關節區一定要有WRITE即可寫許可權。
使用PEView檢視.reloc節區頭:
可見並沒有可寫許可權。
使用Win Hex向該Characteristics項新增IMAGE_SCN_MEM_WRITE(80000000)屬性,執行bit OR異或操作後最終屬性值為C2000040,到RVA 29C中修改:
效果驗證
執行SKI12Viewer_Patch.exe,可以發現並沒有出現執行錯誤的資訊,開啟程式視窗一段時間後便下載了phpinfo.html檔案並用SKI12Viewer_Patch.exe程序開啟該檔案檢視原始碼:
使用PEView檢視SKI12Viewer_Patch.exe,可以看到PE檔案的一些修改的內容確實修改成功且成功注入了myhack3.dll: