摘要:最近对动态注入有一些兴趣因此搜索了些资料,简单整理了下相关的技术实现。本文只能够带你理解何如注入以及大概如何实现,对注入的方法描述的并不详细。
关键字:dll注入,hook,提权
读者须知:读者需要对Windows和Linux dll加载的基本流程比较熟悉。
注入就是将自己的代码注入到目标进程中强制目标进程执行,而动态注入就是将动态库强制加载进目标进程的进程空间从而对目标进程进行修改。动态注入有利有弊,可以用于反病毒、反外挂也可以用于投毒或者制作外挂。
1 DLL注入
DLL是windows平台的动态库,一些Windows的基础功能都是以DLL的形式提供的比如Kernel32.dll、GDI32.dll等。开发者将开发的功能按照模块拆分为不同的动态库,再程序需要使用时再将对应的动态库加载到进程的进程地址空间。而DLL注入就是将一个DLL二进制文件注入加载到目标进程中,强制目标进程执行对应DLL的代码的过程。
DLL注入的实现方式比较多,比如消息Hook注入,输入法注入,远程线程注入,APC注入,EIP注入,注册表注入等,这里简单描述几种。
1.1 Hook注入
Windows应用基于消息机制,每个UI程序都有自己的消息队列以及对应的消息处理函数。Windows上可以利用系统提供的Hook API来截获对应的消息。
SetWindowsHookEx
可以用来注册某个消息的回调函数。如果应用触发了注册的消息类型就会调用用户指定的回调函数从而劫持消息,在这里就可以对数据进行二次加工或者中间人攻击。API的详情见MSDN-SetWindowsHookEX。
进程与进程之间是互相独立隔离的,因此我们需要将Hook的代码写在DLL中,通过将该DLL加载到目标进程的进程空间来进行消息劫持。需要注意的是,当我们安装了全局钩子后,只要进程发出钩子可以劫持的消息,操作系统就会将DLL加载到目标进程中,也就是说实际的装载任务不需要我们来做,我们只要注册全局钩子即可。
全局Hook注入实验
首先,我们需要完成注册全局钩子和注销的相关实现。具体实现比较简单就是调用对应API进行钩子注册和注销以及输出一些简单的帮助信息方便查看当前进程信息,这里不对具体的API进行描述,需要了解的建议直接看MSDN,非常详细。需要注意的是在处理Hook的消息时不要重复触发已经Hook的消息,这样就会出现无限递归,除非电脑注销否则关不掉进程。
//dllmain.cpp
HMODULE gDllModule = nullptr;
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
{
gDllModule = hModule;
}break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
//myhook.h
#pragma once
#include <windows.h>
extern "C" {
_declspec(dllexport) BOOL registerHook();
_declspec(dllexport) BOOL unregisterHook();
_declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
}
//myhook.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "myhook.h"
#include <cstdio>
extern HMODULE gDllModule;
#pragma data_seg("hook_data")
HHOOK gHookHandle = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:hook_data,RWS")
bool hooked = false;
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
FILE *fp = fopen("G://1.txt", "a+");
char buff[1024]{};
char processFullName[_MAX_PATH] = { 0 };
DWORD dwpid = GetCurrentProcessId();
GetModuleFileNameA(NULL, processFullName, _MAX_PATH);
sprintf(buff, "process %s warning hook is coming %d code is %d WParam is %d, LParam is %d\n", processFullName, hooked, code, wParam, lParam);
fwrite(buff, strlen(buff), 1, fp);
fclose(fp);
printf("%s", buff);
if (!hooked) {
//MessageBox(NULL, L"warning hook is coming", L"hook", 0);
hooked = true;
}
return ::CallNextHookEx(gHookHandle, code, wParam, lParam);
}
BOOL registerHook() {
gHookHandle = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, gDllModule, 0);
return !!gHookHandle;
}
BOOL unregisterHook() {
gHookHandle ? UnhookWindowsHookEx(gHookHandle) : TRUE;
return TRUE;
}
然后我们只需要手动将DLL加载起来运行注册钩子的函数即可。
#include <iostream>
#include <windows.h>
typedef BOOL(*registerHook)();
typedef BOOL(*unregisterHook)();
int main(){
HMODULE hmodule = LoadLibraryW(TEXT("E:\\code\\tmp\\dll\\dllhook\\Debug\\dllhook.dll"));
if (!hmodule) {
printf("can not load library, error code is %d\n", GetLastError());
exit(1);
}
registerHook rh = (registerHook)GetProcAddress(hmodule, "registerHook");
if (!rh) {
printf("can not get the register hook address, error code is %d\n", GetLastError());
exit(1);
}
unregisterHook uh = (unregisterHook)GetProcAddress(hmodule, "unregisterHook");
if (!uh) {
printf("can not get the unregister hook address, error code is %d\n", GetLastError());
exit(1);
}
printf("load and get process address succ\n");
auto ret = rh();
if (!ret) {
printf("register hook failed\n");
exit(1);
}
MessageBox(NULL, L"warning hook is coming", L"hook", 0);
while (true) {
}
ret = uh();
if (!ret) {
printf("unregister hook failed\n");
exit(1);
}
printf("process running succ\n");
return 0;
}
下面是Hook成功的一部分日志,可以看到VS和Everthing的消息都劫持到了。
process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11530596
process D:\Everything\Everything.exe warning hook is coming 1 code is 0 WParam is 0, LParam is 13630404
process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11531188
我们看下Everything进程空间加载的DLL,可以看到我们的DLL已经在进程中了。
1.2 Remote Thread注入
Remote Thread就是通过创建远程线程,通过该线程对需要注入的DLL在目标进程空间中加载。
实现的步骤比较简单,我们需要准备三个产物,需要注入的DLL,注入程序,被注入的Rookie。被注入的DLL不用说了自己想怎么写都行,重点是注入程序,注入的过程和Hook不同,这个需要我们自己主动Load,具体过程如下:
- 根据目标进程的名称找到进程ID,如果知道进程ID忽略这一步即可;
- 打开目标进程,在目标进程上申请一块内存;
- 获取目标进程空间中的
LoadLibraryW
的函数地址; - 创建远程线程调用
LoadLibraryW
加载需要注入的DLL; - 打扫现场。
过程比较简单,但是重点是得有目标进程的权限。
肉鸡进程和注入程序(就是在Attach时显示了一个框)就不展示了,下面是注入的代码:
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
//生成系统进程快照,遍历快照根据名字查找对应进程的ID
DWORD fetchProcessID(LPCTSTR processName) {
HANDLE handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (handle == INVALID_HANDLE_VALUE) {
printf("can not get the snapshot of process\n"); exit(1);
}
PROCESSENTRY32 pe{};
pe.dwSize = sizeof(pe);
if (!Process32First(handle, &pe)) {
printf("can not fetch the process information\n"); exit(1);
}
DWORD ret{};
do {
if (!lstrcmp(pe.szExeFile, processName)) {
ret = pe.th32ProcessID; break;
}
} while (Process32Next(handle, &pe));
CloseHandle(handle);
return ret;
}
//远程线程注入目标DLL
BOOL createRemoteThreadAndInject(DWORD pid, LPCWSTR dllName) {
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!processHandle) {
printf("can not open process %d\n", pid); exit(1);
}
DWORD sz = (_tcslen(dllName) + 1) * sizeof(TCHAR);
LPVOID palloc = VirtualAllocEx(processHandle, NULL, sz, MEM_COMMIT, PAGE_READWRITE);
if (!palloc) {
printf("can not allocate memory on process %d\n", pid); exit(1);
}
if (!WriteProcessMemory(processHandle, palloc, dllName, sz, NULL)) {
printf("can not write memory on process %d\n", pid); exit(1);
}
HANDLE pfunc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
if (!pfunc) {
printf("can not fetch LoadLibraryW address on process %d\n", pid); exit(1);
}
LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pfunc;
HANDLE hThread = ::CreateRemoteThread(processHandle, NULL, 0, addr, palloc, 0, NULL);
if (!hThread){
printf("CreateRemoteThread - Error!");
return FALSE;
}
DWORD DllAddr = 0;
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
VirtualFreeEx(processHandle, palloc, sz, MEM_DECOMMIT);
::CloseHandle(processHandle);
return TRUE;
}
int main(){
DWORD pid = fetchProcessID(L"remote_hook_rookie.exe");
createRemoteThreadAndInject(pid, L"E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
}
1.3 突破Session 0远程线程注入
Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。
ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。
Windows的服务和应用程序运行与Session之上,Windows内核6.0之前服务运行于第一个启动运行的Session 0上,该用户的应用程序也运行在Session 0上,后续的用户的应用程序分别运行于Seccion 2,Session 3,…,Session n上。由于有些服务会提权运行,将应用程序和服务运行于相同的Session有安全风险。因此Windos Vista之后只有服务可以托管到Session 0上,应用程序都托管到后续的Session,这样可以将应用程序和服务隔离提高安全性。因此使用CreateRemoteThread
进行远程线程注入的时候会遇到权限问题。
可以通过一些底层的一些API对当前进程进行提权,然后进行注入,如下:
// session_0_inject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
#endif
void ShowError(const char* pszText){
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
// 提权函数
BOOL EnableDebugPrivilege(){
HANDLE hToken;
BOOL fOk = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)){
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fOk;
}
// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD PID, const char* pszDllFileName){
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
HANDLE hRemoteThread = NULL;
DWORD dwStatus = 0;
EnableDebugPrivilege();
// 打开注入进程,获取进程句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (!hProcess){
printf("OpenProcess - Error!\n\n");
return -1;
}
// 在注入的进程申请内存地址
dwSize = lstrlen(pszDllFileName) + 1;
pDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllAddr){
ShowError("VirtualAllocEx - Error!\n\n");
return FALSE;
}
//写入内存地址
if (!WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
ShowError("WriteProcessMemory - Error!\n\n");
return FALSE;
}
//加载ntdll
HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
if (!hNtdllDll){
ShowError("LoadLirbary");
return FALSE;
}
// 获取LoadLibraryA函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (!pFuncProcAddr){
ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
return FALSE;
}
//获取ZwCreateThreadEx函数地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
if (!ZwCreateThreadEx){
ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
return FALSE;
}
// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
if (!ZwCreateThreadEx){
ShowError("ZwCreateThreadEx - Error!\n\n");
return FALSE;
}
// 关闭句柄
::CloseHandle(hProcess);
::FreeLibrary(hNtdllDll);
return TRUE;
}
int main(int argc, char* argv[]){
#ifdef _WIN64
BOOL bRet = ZwCreateThreadExInjectDll(4924, "E:\code\tmp\dll\remote_thread_hook\Debug\remote_thread_hook.dll");
#else
BOOL bRet = ZwCreateThreadExInjectDll(15596, "E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
#endif
if (!bRet){
printf("Inject Dll Error!\n\n");
}else {
printf("Inject Dll OK!\n\n");
}
return 0;
}
2 Linux 动态注入
Linux下的动态注入相对比较简单,有两种方式:
- 符号表劫持。Linux上ld记载动态库之前会先加载
LD_PRELOAD
定义的动态库,而符号解析是按照库的载入顺序来的,也就是现在入的库的符号会覆盖后载入的库的符号,利用这种方式我们就可以劫持符号,但是意义不大; - 利用gdb或者lldb手动重定位期望劫持的API的地址。