逆向工程核心原理DLL注入

前言:学这一部分知识最好先学一部分win32API编程的知识,分析源代码就会容易一些。

DLL注入概念

DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。从技术细节来说,DLL注入命令其他进程自行调用LoadLibrary()API,加载用户指定的DLL文件。DLL注入与其他一般的DLL加载在于,加载的目标进程是其自身或其他进程。

DLL注入技术的作用

DLL注入技术可以被正常软件用来添加/扩展其他程序,调试或逆向工程的功能性;该技术也常被恶意软件以多种方式利用。

三种DLL注入的方法:

  1. 创建远端线程(CreateRemoteThread()API)
  2. 使用注册表(AppInit_DLLs值)
  3. 消息钩取(SetWindowsHookEx()API)
  4. 通过修改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,得到