DLL注入技术

文章目录

1.显式载入|卸载DLL模块

在任何时候,进程中的一个线程可以调用下面两个函数来将一个DLL映射到进程的地址空间中:

HMODULE LoadLibrary(PCTSTR pszDLLPathName);

HMODULE LoadLibraryEx(
	PCTSTR pszDLLPathName,
	HANDLE hFile,  //默认NULL
   	DWORD  dwFlags //默认0
);

卸载函数

BOOL FreeLibrary(HMODULE hInstDll);

VOID FreeLibraryAndExitThraed(
	HMODULE hInstDll,
	DWORD   dwExitCode
);

2.得到DLL符号地址

FARPROC GetProcAddress(
	HMODULE hInstDll,
	PCSTR  	pszSymbolName
);

3.Dll入口点函数

BOOL WINAPI DllMain(HINSTANCE hInstDll,DWORD fdwReason,PVOID fImpLoad){
	switch(fdwReason){
		//第一次将DLL映射到进程地址空间中调用
		case DLL_PROCESS_ATTACH:
			break;
		//只有dll已经映射到进程地址空间后,有新线程才会调用这个
		case DLL_THREAD_ATTACH:
			break;
		case DLL_THREAD_DETACH:
			break;
		//将DLL从进程地址空间释放时调用
		case DLL_PROCESS_DETACH:
			break;
	}
	return TRUE;
}

4.DLL注入

4.1.使用注册表注入DLL

首先查找注册表的下面表项:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\

该表项中包含AppInit_DLLs和LoadAppInit_DLLs两个键
我们要做的就是将DLL路径写入AppInit_DLLs中并且将LoadAppInit_DLLs置为1

如果想注入多个DLL,注意将DLL放到系统路径,AppInit_DLLs中每一个DLL用空格或者逗号隔开,且第二个DLL开始就不能写路径了。

原理:User32.dll被映射到一个新进程时,会受到DLL_PROCESS_ATTACH通知,当User32.dll对其进行处理的时候,会取得上述注册表的值并调用LoadLibrary载入DLL。

优点:最方便

缺点:
(1)DLL只会被映射到那些使用了User32.dll的进程中
(2)DLL会被映射到所有使用了User32.dll的进程中。

4.2.使用windows挂钩来注入DLL

首先了解一下挂钩函数

HHOOK hHook = SetwindowHookEx(
	int idHook,  //安装的挂钩类型
	HOOKPROC,		//一个函数的地址
	HINSTANCE hMod,		//一个DLL,这个DLL包含了函数
	DWORD dwThreadId				//哪个线程安装hook,为0代表所有线程
);

原理:如果最后一个参数设置为0,那么安装的就是全局消息钩子,这时要求HOOKPROC必须在DLL中,并且指定第3个参数hMod。这样,系统在其他进程中调用HOOKPROC时,如果发现目标DLL尚未加载,就会使用KeUserModeCallback函数回调User32.dll的__ClientLoadLibrary()函数,由User32.dll把这个DLL加载到目标进程中,从而实现将DLL注入到其他进程的目的。

4.3.使用远程线程注入DLL

远程线程API

HANDLE CreateRemoteThread(
HANDLE hProcess,                            //用来表示新创建的线程所属进程
LPSECURITY_ATTRIBUTES 1pThreadAttributes,   //线程安全属性,NULL
SIZE_T dwStacksize,                         //新线程的堆栈空间大小
LPTHREAD_START_ROUTINE lpStartAddress       //新线程的函数地址
LPVOID lpParameter,                         //传递给新线程的数据
DWORD dwCreationFlags,						//创建线程的方法
LPDWORD lpThreadId                          //用来接收新线程的ID,若为NULL,不返回线程ID
);

两个问题:
(1)LoadLibrary不能直接放到CreateRemoteThread中去,因为对方进程不知道基址,应该先得到对方进程所在内存LoadLibrary的机制
(2)动态库字符串不能直接传递,因为对方内存地址没有该字符串,应该先把字符串传递到对方进程中。

BOOL WINAPI InjectDLLToProcess(DWORD dwTargetPid, LPCSTR DLLPath){
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwTargetPid);
    if (hProc == NULL) {
        printf("OpenProcess Faild\n");
        return FALSE; 
    }
    //使用VirtualAllocEx函数在远程进程内存地址分配DLL文件名缓冲区
    LPTSTR psLibFileRemote = (LPTSTR)VirtualAllocEx(hProc, NULL, lstrlen((LPTSTR)DLLPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    if (psLibFileRemote == NULL) {
        printf("VirtualAllocEx Faild\n");
        return FALSE;
    }
    //使用WriteProcessMemory函数将DLL路径名复制到远程的内存空间中
    if (WriteProcessMemory(hProc, psLibFileRemote, (LPCVOID)DLLPath, lstrlen((LPTSTR)DLLPath) + 1, NULL) == 0) {
        printf("WriteMemory Failed\n");
        return FALSE;
    }
    //获得远程进程的LoadLibrary地址
    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");

    HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, pfnThreadRtn,psLibFileRemote, 0, NULL);
    if (hThread == NULL) {
        printf("CreateRemoteThread Failed\n");
        return FALSE;
    }
    printf("Inject Successfull\n");
    return TRUE;
}

4.4.QueueUserApc/NtQueueAPCThread APC注入法

当一个线程从等待状态中苏醒时(线程调用SleepEx,SingalObjectAndWait,MsgWaitForMultiple等等)他会检测有没有APC交付给自己。如果有,它就会执行这些APC过程。在用户层,我们可以像创建远程线程一样,使用QueueUserAPC把APC过程添加到目标线程的APC队列中,等这个线程恢复执行时,就会执行我们插入的APC过程了。

DWORD WINAPI QueueUserAPC(
	PAPCFUNC pfnAPC,    //APC函数的地址
	HANDLE   hThread,
	ULONG_PTR dwData    //APC函数的地址
);

在添加用户模式APC后线程不会直接调用APC函数,所以为了增加调用机会,应向所有线程插入APC。

BOOL WINAPI InjectDLLToProcessAPC(DWORD dwTargetPid, LPCSTR DLLPath) {
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwTargetPid);
    if (hProc == NULL) {
        printf("OpenProcess Faild\n");
        return FALSE;
    }
    //使用VirtualAllocEx函数在远程进程内存地址分配DLL文件名缓冲区
    LPTSTR psLibFileRemote = (LPTSTR)VirtualAllocEx(hProc, NULL, lstrlen((LPTSTR)DLLPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    if (psLibFileRemote == NULL) {
        printf("VirtualAllocEx Faild\n");
        return FALSE;
    }
    //使用WriteProcessMemory函数将DLL路径名复制到远程的内存空间中
    if (WriteProcessMemory(hProc, psLibFileRemote, (LPCVOID)DLLPath, lstrlen((LPTSTR)DLLPath) + 1, NULL) == 0) {
        printf("WriteMemory Failed\n");
        return FALSE;
    }
    CloseHandle(hProc);
    BOOL status = FALSE;
    //定义线程信息结构
    THREADENTRY32 te32 = { sizeof(te32) };
    //创建系统当前线程快照
    HANDLE hThreadShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hThreadShot == INVALID_HANDLE_VALUE)
        return FALSE;
    //循环枚举线程信息
    if (Thread32First(hThreadShot, &te32)) {
        do {
            //判断是不是目标进程的子进程
            if (te32.th32OwnerProcessID == dwTargetPid) {
                HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
                if (hThread){
                    //向指定线程添加APC
                    DWORD dwRet = QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)psLibFileRemote);
                    if (dwRet > 0)
                        status = TRUE;
                    CloseHandle(hThread);
                }
            }
        } while (Thread32Next(hThreadShot, &te32));
    }
    CloseHandle(hThreadShot);
    return status;
}

在实际使用中,由于条件苛刻,能成功利用的机会并不多。但是,如果能够加载驱动,就可以在驱动中向目标进程插入APC,并直接修改线程对象的某些域,使得该线程满足调用APC的条件了。

4.5.SetThreadContext法

在注入DLL时,可以将目标进程中的线程暂停,然后向其写入ShellCode,把线程的context的eip设置为shellcode的地址,这样线程恢复执行时就会先执行我们的shellcode了。在ShellCode中加载目标DLL,然后调回原来的eip执行。

typedef struct INJECT_DATA {
    BYTE ShellCode[0x30];         //0x00
    ULONG_PTR AddrofLoadLibraryA; //0x30
    PBYTE lpDLLPath;              //0x34
    ULONG_PTR OriginalEIP;        //0x38
    char szDllPath[MAX_PATH];     //0x3C
};

__declspec(naked)
VOID ShellCodeFun(VOID) {
    __asm {
        push eax
        pushad
        popfd
        call L001
     L001:
        pop ebx
        sub ebx,8
        push dword ptr ds:[ebx+0x34]
        call dword ptr ds:[ebx+0x30]
        mov eax,dword ptr ds:[eax+0x38]
        xchg eax,[esp+0x24]
        popfd
        popad
        retn
    }
}

BOOL WINAPI InjectDLLToProcessSTC(DWORD dwTargetPid, LPCSTR DLLPath) {
    DWORD dwTidList[1024] = { 0 };
    BOOL status = FALSE;
    int index = 0;
    //首先获取进程中的线程ID
    //定义线程信息结构
    THREADENTRY32 te32 = { sizeof(te32) };
    //创建系统当前线程快照
    HANDLE hThreadShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hThreadShot == INVALID_HANDLE_VALUE)
        return FALSE;
    //循环枚举线程信息
    if (Thread32First(hThreadShot, &te32)) {
        do {
            //判断是不是目标进程的子进程
            if (te32.th32OwnerProcessID == dwTargetPid) {
                status = TRUE;
                dwTidList[index++] = te32.th32ThreadID;
            }
        } while (Thread32Next(hThreadShot, &te32));
    }
    CloseHandle(hThreadShot);
    if (!status) {
        printf("该进程无线程");
        return status;
    }
    //打开进程和线程,暂停线程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwTargetPid);
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwTidList[0]);
    if (hThread == NULL) {
        printf("打开线程失败");
        return FALSE;
    }
    DWORD swSuppendCnt = SuspendThread(hThread);
    //获得线程的CONTEXT
    CONTEXT context;
    ULONG_PTR uEIP = 0;
    ZeroMemory(&context, sizeof(CONTEXT));
    context.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &context);
    uEIP = context.Eip;
    //申请内存准备写入ShellCode
    PBYTE lpData = (PBYTE)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    INJECT_DATA data;
    PBYTE pShellCode = (PBYTE)ShellCodeFun;
    if (pShellCode[0] == 0xE9) {
        pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5;
    }
    memcpy(data.ShellCode, pShellCode, 0x30);
    lstrcpyA(data.szDllPath,DLLPath);
    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
    data.AddrofLoadLibraryA = (ULONG_PTR)pfnThreadRtn;
    data.OriginalEIP = uEIP;
    data.lpDLLPath = lpData + FIELD_OFFSET(INJECT_DATA,szDllPath);
    printf("Shellcode填充完毕");
    if (!WriteProcessMemory(hProcess, lpData, &data, sizeof(INJECT_DATA), NULL)) {
        printf("写入失败!");
        return FALSE;
    }
    context.Eip = (ULONG)lpData;
    SetThreadContext(hThread, &context);
    ResumeThread(hThread);
    CloseHandle(hProcess);
    CloseHandle(hThread);
    printf("注入动态库成功!");
    return TRUE;
}

4.6.输入表项DLL替换法(DLL劫持法)

注册表有一个叫KnownDLLs的设置项

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

这里面存在的库是不能替换的,因为加载的时候首先会查找该目录,如果该目录没有,可以放在exe根目录下面进行替换。

上一篇:SAP EPIC 银企直连 农业银行 Socket 接口项目实践


下一篇:逆向KeInitializeApc KiInsertQueueApc