除了DLL动态注入技术外,还可以通过手工修改PE文件的方式来加载DLL,这种方式只要应用过一次之后,每当进程开始运行时便会自动加载指定的DLL。
整体思路如下:
1、查看IDT是否有充足的空间,若无则移动IDT至其他位置,若有则直接添加至列表末尾;
2、若无,修改OPTIONAL头IMPORT TABLE的RVA值并增大Size值,删除绑定导入表BOUND IMPORT Table,复制原IAT内容到目标地址并设置INT、NAME、IAT,最后到.rdata节区头修改IAT属性值添加可写属性。
下面练习的目标是编写简单的文本查看程序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()函数。
基本思路为,PE文件中导入的DLL信息以结构体列表的形式存储在IDT中,只需将myhack3.dll添加到列表尾部即可,前提是查看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结构体。
移动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进程修改。
IMAGE_OPTIONAL_HEADER的导入表结构成员用来指出IDT的位置(RVA)与大小:
将该导入表的RVA值为2334,将其修改为新IDT的RVA值6250,在Size值的基础上加上14即修改为8C:
BOUNG_IMPORT_TABLE是一种提高DLL加载速度的技术。若想正常导入指定的DLL文件,需要向绑定导入表添加信息。然而绑定导入表是可选项,不是必须存在的,因而可以删除(修改其值为0)以更方便地操作。但是若存在的话,当其中的内容记录错误时,会引发程序运行出错。
可以看到,此处绑定导入表本来已经是删除了的:
使用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:
加载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: