逆向工程核心原理DLL注入
前言:学这一部分知识最好先学一部分win32API编程的知识,分析源代码就会容易一些。
DLL注入概念
DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。从技术细节来说,DLL注入命令其他进程自行调用LoadLibrary()API,加载用户指定的DLL文件。DLL注入与其他一般的DLL加载在于,加载的目标进程是其自身或其他进程。
DLL注入技术的作用
DLL注入技术可以被正常软件用来添加/扩展其他程序,调试或逆向工程的功能性;该技术也常被恶意软件以多种方式利用。
三种DLL注入的方法:
- 创建远端线程(CreateRemoteThread()API)
- 使用注册表(AppInit_DLLs值)
- 消息钩取(SetWindowsHookEx()API)
- 通过修改pe文件来加载DLL文件
创建远端线程(CreateRemoteThread()API)
大概原理就是通过CreateRemoteThread()API来使目标进程调用LoadLibrary()API,加载我们写好的DLL。
InjectDll.exe
这个程序用来像目标进程注入我们的DLL,源代码如下
#include"Windows.h"
#include"tchar.h"
#include"stdio.h"
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;//目标进程的句柄
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL; //向目标进程申请内存空间的首地址。
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);//我们写的DLL路径的字符串长度大小,记得加一个1,字符串以'\0'结尾。
LPTHREAD_START_ROUTINE pThreadProc; //线程的起点函数地址。
//获得目标进程的句柄,让我们可以操作这个进程
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return false;
}
//向目标进程申请一个可以存放DLL路径字符串长度大小的空间。
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
//将DLL路径字符串写人这个申请的空间。
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
//获取"kernel32.dll"动态链接库的模块句柄,用来调用其中的"LoadLibraryW"函数。
hMod = GetModuleHandle(L"kernel32.dll");
//所执行的线程函数地址设置为LoadLibraryW()API函数,使这个函数调用pRemoteBuf这个参数,也就是加载我们的DLL。
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
//创建一个线程,hProcess为目标进程, pThreadProc为线程函数地址, pRemoteBuf为向这个函数传入的参数。
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread); //关闭线程句柄
CloseHandle(hProcess); //关闭进程句柄
return TRUE;
}
int _tmain(int argc, TCHAR* argv[])
{
if(argc!=3)//这里是描述使用方法
{
_tprintf(L"USAGE:%s pid dll_path\n", argv[0]);
return 1;
}
if (InjectDll((DWORD)_tstol(argv[1]), argv[2])) //argv[1]是目标进程的PID, argv[2]是我们要注入DLL的绝对地址。 然后_tstol是将字符串型的PID转为整型的。
_tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
else
_tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);
return 0;
}
将这个代码在Visual Studio 2019中用win32来生成exe文件,得到InjectDll.exe。下面是一些函数的解释。
GetModuleHandle()API
获取一个应用程序或动态链接库的模块句柄。
HMODULE GetModuleHandle(
LPCTSTR lpModuleName //lpModuleName String,指定模块名,这通常是与模块的文件名相同的一个名字。
);
VirtualAllocEx()API
在指定进程的虚拟空间保留或提交内存区域
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
hProcess:申请内存所在的进程句柄。
lpAddress:保留页面的内存地址;一般用NULL自动分配 。
dwSize:欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍
flAllocationType
可取下列值:
MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
MEM_PHYSICAL :分配物理内存(仅用于地址窗口扩展内存)
MEM_RESERVE:保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用
MEM_RESET :指明在内存中由参数lpAddress和dwSize指定的数据无效
MEM_TOP_DOWN:在尽可能高的地址上分配内存(Windows 98忽略此标志)
MEM_WRITE_WATCH:必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98)
flProtect
可取下列值:
PAGE_READONLY: 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访
PAGE_READWRITE 区域可被应用程序读写
PAGE_EXECUTE: 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。
PAGE_EXECUTE_READ :区域包含可执行代码,应用程序可以读该区域。
PAGE_EXECUTE_READWRITE: 区域包含可执行代码,应用程序可以读写该区域。
PAGE_GUARD: 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限
PAGE_NOACCESS: 任何访问该区域的操作将被拒绝
PAGE_NOCACHE: RAM中的页映射到该区域时将不会被微处理器缓存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页(guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存。这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。
返回值:
执行成功就返回分配内存的首地址,不成功就是NULL。
WriteProcessMemory()
此函数能写入某一进程的内存区域,故需此函数入口区必须可以访问,否则操作将失败。
BOOL WriteProcessMemory(
HANDLE hProcess, //由OpenProcess返回的进程句柄。
LPVOID lpBaseAddress, //要写的内存首地址
LPVOID lpBuffer, //指向要写的数据的指针。
DWORD nSize, //要写入的字节数。
LPDWORD lpNumberOfBytesWritten
);
GetProcAddress()
获得函数地址
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);
CreateRemoteThread()
在目标进程中创建一个线程
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess, //线程所属进程的进程句柄.
__in LPSECURITY_ATTRIBUTES lpThreadAttributes, //一个指向 SECURITY_ATTRIBUTES 结构的指针, 该结构指定了线程的安全属性.
__in SIZE_T dwStackSize, //线程栈初始大小,以字节为单位,如果该值设为0,那么使用系统默认大小.
__in LPTHREAD_START_ROUTINE lpStartAddress, //在远程进程的地址空间中,该线程的线程函数的起始地址.
__in LPVOID lpParameter, //传给线程函数的参数.
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId //指向所创建线程ID的指针,如果创建失败,该参数为NULL.
);
myhack.dll
根据书上改了一下,变成了弹出一个窗口的dll动态链接库
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "windows.h"
#include "tchar.h"
#include "pch.h"
DWORD WINAPI ThreadProc(LPVOID lParam)//弹出一个输出hellow world的窗口
{
CHAR message[]="hellow world";
CHAR title[]="messagebox";
MessageBoxA(NULL,message,title,MB_OK);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
HANDLE hThread = NULL;
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
OutputDebugString(L"<myhack.dll> Injection!!!");//在Debugview里面输出字符串
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //创建一个线程
CloseHandle(hThread);
break;
}
return TRUE;
}
将InjectDll.exe,和myhack.dll都放在D盘下,打开32位的notepad.exe然后用管理员权限的cmd来执行命令。
效果如下
看了后面的内容,根据代码改了一下InjectDll.exe的代码,可以不用查看PID了,只需要提供进程的名称,详细内容请看另一篇博客win32编程学习笔记。
#include"Windows.h"
#include"tchar.h"
#include "tlhelp32.h"
#include"stdio.h"
DWORD FindProcessID(LPCTSTR szProcessName)
{
DWORD dwPID = 0xFFFFFFFF;
HANDLE hSnapShot = INVALID_HANDLE_VALUE;
PROCESSENTRY32 pe;
// Get the snapshot of the system
pe.dwSize = sizeof(PROCESSENTRY32);
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
// find process
Process32First(hSnapShot, &pe);
do
{
if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile))
{
dwPID = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnapShot, &pe));
CloseHandle(hSnapShot);
return dwPID;
}
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return false;
}
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD PID;
if (argc != 3)
{
_tprintf(L"USAGE:%s processname dll_path\n", argv[0]);
return 1;
}
PID = FindProcessID(argv[1]);
if (InjectDll(PID, argv[2]))
_tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
else
_tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);
return 0;
}
使用注册表(AppInit_DLLs值)
通过修改注册表来注入DLL。
myhack2.dll
简单做了一些改动,变成了,不隐藏打开一个网页。如果要隐藏的话改一下pi结构体的一些内容就行了。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "windows.h"
#include "tchar.h"
#include "stdio.h"
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szPath[MAX_PATH] = { 0, };
TCHAR szChildname[] = TEXT("D:/Firefox/firefox.exe");
TCHAR szURL[] = TEXT(" https://the_itach1.gitee.io/");
TCHAR process[] = TEXT("notepad.exe");
TCHAR* p = NULL;
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi = { 0, };
si.cb = sizeof(STARTUPINFO);
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
if (!GetModuleFileName(NULL, szPath, MAX_PATH))
break;
if (!(p = _tcsrchr(szPath, '\\')))
break;
if (_tcsicmp(p + 1, process)) //确保当前进程是notepad.exe,才会进行这些操作,也可以根据需要来改变进程对象。
break;
if (!CreateProcess(szChildname, (LPTSTR)(LPCTSTR)szURL,
NULL, NULL, FALSE,
0,
NULL, NULL, &si, &pi))
break;
if (pi.hProcess != NULL)
CloseHandle(pi.hProcess);
break;
}
return TRUE;
}
在运行里面输入regedit.exe,打开注册表编辑器,然后就如下图所示进行改动。
然后重启计算机,就可以发现myhack.dll已经注入到了user32.dll加载的所有进程。(不知道什么原因我电脑好像不行,哈哈哈)。
消息钩取(SetWindowsHookEx()API)
目前不知道怎么将截取的键盘信息输出到debugview。
hookmain.exe
hookmain.exe就相当于一个药引吧,主要用来加载dll里面的两个函数。至于为什么不能直接写一个exe文件来钩取键盘记录,我认为是以因为SetWindowsHookEx()这个函数的特性就就决定了它只能在DLL中存在,下面会给SetWindowsHookEx()函数的一些介绍。
#include "stdio.h"
#include "conio.h"
#include "windows.h"
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
typedef void (*PFN_HOOKSTART)();//这里是定义了两个函数指针。
typedef void (*PFN_HOOKSTOP)();
void main()
{
HMODULE hDll = NULL;
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
char ch = 0;
hDll = LoadLibraryA(DEF_DLL_NAME);//加载KeyHook.dll,获得dll的句柄
if (hDll == NULL)
{
printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
return;
}
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);//利用GetProcAddress()获得HookStart的函数地址
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);//利用GetProcAddress()获得HookStop的函数地址
HookStart();//开始钩取函数
printf("press 'q' to quit!\n");
while (_getch() != 'q');//遇到键盘输入q就退出
HookStop();//退出钩取函数
FreeLibrary(hDll);
}
KeyHook.dll
最重要的就是SetWindowsHookEx(),和KeyBoardProc()这个回调函数了。
SetWindowsHookEx()
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook, 设置钩子的类型.意思就是我要设置的钩子是什么钩子. 可以是监视窗口过程.可以是监视消息队列.
_In_ HOOKPROC lpfn, 根据钩子类型.设置不同的回调函数.
_In_ HINSTANCE hMod, 钩子设置的Dll实例句柄,就是DLL的句柄
_In_ DWORD dwThreadId 设置钩子的线程ID. 如果为0 则设置为全局钩子.
);
HHOOK 返回值. 是一个钩子过程句柄.
原型 SetWindowsHookEx(WH_KEYBOARD, KeyBoardProc, HInstance, 0);
KeyBoardProc()
首先键盘钩子处理函数的函数名是可以自定义的 ,例如:MyKeyboardProc()
函数原型:
LRESULT CALLBACK KeyboardProc(
int code,
WPARAM wParam,
LPARAM lParam
);
code:
根据这个数值决定怎样处理消息
如果 code 小于0,则 必须让KeyboardProc()函数返回CallNextHookEx()
wParam:
按键的虚拟键值消息,例如:VK_F1
lParam:
32位内存,内容描述包括:指定扩展键值,扫描码,上下文,重复次数。这里只用到了第31位就只给31位的解释吧。
31位,指定转变状态:31位为0时候,按键正在被按下,为1时候,按键正在被释放
返回值:
如果参数1:code小于0,则必须 返回CallNextHookEx(),也就是返回CallNextHookEx()的返回值
如果参数1:code大于等于0,并且钩子处理函数没有处理消息,强烈建议您 返回CallNextHookEx()的返回值,否则当您安装WH_KEYBOARD钩子时
,钩子将不会得到通知,并返回错误结果。
**如果钩子处理的消息,您可以返回一个非0值,防止系统把消息传递给钩子链中的下一个钩子,或者把消息发送到目标窗口。**
可以根据需要来决定截取那一个进程的键盘消息。
#include "stdio.h"
#include "windows.h"
#include "pch.h"
#define DEF_PROCESS_NAME "notepad.exe"
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;//获取该DLL的句柄。
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = { 0, };
char* p = NULL;
if (nCode >= 0)
{
// 根据lParam的31位判断是否有按键输入
if (!(lParam & 0x80000000))
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
if (!_stricmp(p + 1, DEF_PROCESS_NAME))//看当前进程是否是notepad.exe
{
OutputDebugString(L"successful");//用Debugview输出successful来确定注入成功
return 1;//返回一个非0值,防止系统把消息传递给钩子链中的下一个钩子,这样notepad就不会有键盘消息了。
}
}
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0); //调用SetWindowsHookEx()函数
}
__declspec(dllexport) void HookStop()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook); //结束钩取函数
g_hHook = NULL;
}
}
#ifdef __cplusplus
}
#endif
根据需要选择32位还是64位的,我这里是选择的64位的,查看结果如下
但是会出现一些问题,就是如果尝试在其他64位进程中输入消息,会出现未响应的情况,网上搜了一下,感觉这个消息比较合理。
如果你认真看SetWindowsHookEx的文档的话,会看到在你的DLL因为CPU架构不匹配而不能注入目标程序的时候,被钩的程序是通过SendMessage来调用你的KeyboardProc的。然后你的KeyboardProc卡在了对话框那里没有返回,所以这些程序的界面就卡在SendMessage那里了。 在64位Windows上,大多数程序是32位。资源管理器是64位。
通过修改pe文件来加载DLL文件
myhack3.dll
改了书上的,书上代码太复杂了,而且不知道什么原因(可能是访问的Google吧),注入成功了也看不到效果,就改了一个输出一个窗口(简单嘛)。
#include "stdio.h"
#include "windows.h"
#include "pch.h"
#include "tchar.h"
#pragma comment(lib, "Wininet.lib")
HWND g_hWnd = NULL;
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void fun() //根据形式要求,必须设置一个导出函数,我这里是将函数名设置为fun。
{
return;
}
#ifdef __cplusplus
}
#endif
DWORD WINAPI ThreadProc(LPVOID lParam)
{
MessageBoxA(NULL, "The_Itach1", "Reverse Corse", MB_OK);
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;
}
修改TextView.exe
Import Directory Table由IMAGE_IMPORT_DESCRIPTOR结构体组成。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //library name string address (RVA)
DWORD FirstThunk; //IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //ordinal
BYTE Name[1]; //function name string
用pe view打开可以发现Import Directory Table的后面已经有了其他代码,所以不能添写更多的IMAGE_IMPORT_DESCRIPTOR结构体了,所以要选择另一片区域来设置Import Directory Table,然后设置到了.rdate节区。
先将原来Import Directory Table(RAV=84CC)的64个字节的数据复制到选择的null-padding位置。然后再加上myhack3.dll的IMAGE_IMPORT_DESCRIPTOR。
然后修改新Import Directory Table在文件中的位置和大小,还要修改IAT节区的可写属性。
然后保存,将修改后的TextView.exe和myhack.dll放在一起,运行TextView.exe,得到