当前位置:安全客 >> 知识详情

【技术分享】DLL注入那些事

2017-08-22 11:06:09 阅读:2165次 收藏 来源: blog.deniable.org 作者:shan66

http://p5.qhimg.com/t011b2047aa5101086b.jpg

译者:shan66

预估稿费:260RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿



在本文中,我们将向读者全面介绍各种DLL注入技术。所谓DLL注入,本来是软件用于向其他程序添加/扩展功能、调试或逆向工程的一种合法技术。不过,后来恶意软件也常用这种方式来干坏事。因此,这意味着从安全的角度来看,我们必须知道DLL注入是如何工作的。

之前,当我开始开发Red Team的定制工具(为了模拟不同类型的攻击者)时,我完成了这个小项目的大部分代码,并将该项目命名为“injectAllTheThings”。如果你想看一些使用DLL注入的具体示例,请访问这里。如果你想了解DLL注入,该项目也会很有帮助。实际上,我已经在一个单一的Visual Studio项目中组合了多种DLL注入技术(实际上是7种不同的技术),它们都有32位和64位版本,并且非常便于阅读和理解:每种技术都有自己单独的源文件。

以下是该工具的输出,给出了所有已经实现的方法。

http://p3.qhimg.com/t01f4ab3235a95e43d0.png

根据@SubTee的说法,DLL注入是“没什么大不了”的。我同意这种观点,但是,DLL注入远不止加载DLL这么简单。

http://p6.qhimg.com/t01f8270ae6279386c6.png

你虽然可以使用经过Microsoft签名的二进制代码来加载DLL,但是却无法附加到某个进程来利用其内存。大多数渗透测试人员实际上不知道什么是DLL注入以及它是如何工作的,主要是因为Metasploit可以代劳的事情太多了。他们一直在盲目地使用它。我相信,学习这个“怪异的”内存操纵技术的最佳地点,不是安全论坛,而是黑客论坛。如果你加入了红队,你可能需要鼓捣这种东西,除非你安于使用他人提供的工具。

大多时候,我们首先会使用一项高度复杂的技术展开攻击,如果我们没有被发现,才会开始降低复杂程度。这就是说,我们会先将二进制文件丢到磁盘上,然后使用DLL注入。

这篇文章将全面介绍DLL注入,同时也是GitHub托管的该项目的“帮助文档”。


概述


DLL注入简单来说就是将代码插入/注入到正在运行的进程中的过程。我们注入的代码是动态链接库(DLL)的形式。为什么可以做到这一点?因为DLL(如UNIX中的共享库)是在运行时根据需要来进行加载。在这个项目中,我将只使用DLL,但是实际上还可以使用其他各种形式(任何PE文件、shellcode / assembly等)来“注入”代码,这些在恶意软件中非常常见。

此外,请记住,您需要具有适当级别的权限才能鼓捣其他进程的内存。但是,这里不会探讨受和保护的进程Windows特权级别(由Vista引入)有关的内容——这是一个完全不同的主题。

如上所述,DLL注入可以用于合法目的。例如,防病毒和终端安全解决方案就需要使用这些技术将自己的软件代码/挂钩放置到系统上的“所有”运行的进程中。这使他们能够在运行过程中监视每个进程的行为,从而更好地保护我们。但是,该技术也可以用于恶意的目的。一般来说,常用技术是注入“lsass”进程以获取密码哈希值。恶意软件也广泛使用代码注入技术,例如,运行shellcode、运行PE文件或将DLL加载到另一个进程的内存中以隐藏自身,等等。


基础知识


我们将使用MS Windows API完成各种注入,因为这个API提供了非常丰富的功能,允许我们连接和操纵其他进程。自从微软第一个版本的操作系统以来,DLL一直是MS Windows的基石。事实上,MS Windows 所有API都涉及DLL。最重要的一些DLL有“Kernel32.dll”(其中包含用于管理内存、进程和线程的函数)、“User32.dll”(主要是用户界面函数)和“GDI32.dll”(用于绘制图形和文字显示)。

您可能奇怪为什么会提供这样的API,为什么微软给我们这么好的一套函数来操作进程的内存?实际上,它们的最初用途是扩展应用程序的功能。例如,一家公司创建一个了应用程序,并希望允许其他公司扩展或增强应用程序。所以,DLL最初是用于合法的目的。此外,DLL还可用于项目管理,节省内存,实现资源共享等。

下图讲解DLL注入技术的流程。

http://p2.qhimg.com/t01a23d6a30b98f78c7.png

就像上面看到的那样,DLL注入分为四个步骤:

1.附加到目标/远程进程

2.在目标/远程进程内分配内存

3.将DLL路径或DLL复制到目标/远程进程内存中

4.让进程执行DLL

所有这些步骤都是通过调用一组API函数来实现的。每种技术都需要一定的设置和选项才能完成。实际上,每种技术都有自己的优点和缺点。


技术详解


我们有多种方法来让进程执行我们的DLL。最常见的方法就是使用“CreateRemoteThread()”和“NtCreateThreadEx()”。但是,我们无法将DLL作为参数传递给这些函数。我们必须提供一个保存执行起始点的内存地址。为此,我们需要完成内存分配,使用“LoadLibrary()”加载我们的DLL,复制内存等等。

这个项目我称之为'injectAllTheThings'(取这个名字,只是因为我讨厌‘injector’的名字,加上GitHub上已经有太多的‘injector’了),它有7种不同的技术。当然,这些技术都不是我发明的。我只是使用了这七种技术(是的,还有更多)。一些API具有详细的文档说明(如“CreateRemoteThread()”),有些API则没有相关的文档说明(如'NtCreateThreadEx()')。以下是已经实现的技术的完整列表,它们全部适用于32位和64位。

CreateRemoteThread()

NtCreateThreadEx()

QueueUserAPC

SetWindowsHookEx()

RtlCreateUserThread()

通过SetThreadContext()获取代码洞

反射型DLL

其中,可能有一些是你早就接触过的技术。当然,这不是所有DLL注入技术的完整列表。如我所说,还有更多的技术,但是并没有包括在这里。这里给出的,是到目前为止,我在一些项目中使用过的技术。有些是稳定的,有些是不稳定的——当然,之所以不稳定,可能是由于我的代码的原因,而不是这些技术本身的原因。


LoadLibrary()


MSDN所述,“LoadLibrary()”函数的作用是将指定的模块加载到调用进程的地址空间中。而指定的模块可能会导致加载其他模块。

HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);

lpFileName [in]

The name of the module. This can be either a library module (a .dll file) or an executable module (an .exe file). (...)

If the string specifies a full path, the function searches only that path for the module.

If the string specifies a relative path or a module name without a path, the function uses a standard search strategy to find the module (...) 

If the function cannot find the module, the function fails. When specifying a path, be sure to use backslashes (\), not forward slashes (/). (...)

If the string specifies a module name without a path and the file name extension is omitted, the function appends the default library extension .dll to the module name. (...)

换句话说,它只需要一个文件名作为其唯一的参数。也就是说,我们只需要为DLL的路径分配一些内存,并将执行起始点设置为“LoadLibrary()”函数的地址,将路径的内存地址作为参数传递就行了。

实际上,这里最大的问题是“LoadLibrary()”使用程序来将加载的DLL添加到注册表中。意思是它可以轻松被检测到,但是实际上许多终端安全解决方案仍然无法检测到它们。无论如何,正如我之前所说,DLL注入也有合法的使用情况,所以...另外,请注意,如果DLL已经加载了'LoadLibrary()',它将不会被再次执行。如果使用反射型DLL注入,当然没有这个问题,因为DLL没有被注册。如果使用反射DLL注入技术而不是使用“LoadLibrary()”,会将整个DLL加载到内存中。然后找到DLL的入口点的偏移量来加载它。如果你愿意,还可以设法将其隐藏起来。取证人员仍然可以在内存中找到你的DLL,只是这不会那么容易而已。Metasploit使用了大量的DLL注入,但是大多数终端解决方案都能搞定这一切。如果你喜欢狩猎,或者你属于“蓝队”,可以看看这里这里

顺便说一句,如果你的终端安全软件无法搞定所有这一切...你可尝试使用一些游戏反欺骗引擎。一些反欺诈游戏的反rootkit功能比某些AV更加先进。


连接到目标/远程进程


首先,我们需要得到要与之进行交互的进程的句柄。为此,我们可以使用API调用OpenProcess()

HANDLE WINAPI OpenProcess(
  _In_ DWORD dwDesiredAccess,
  _In_ BOOL  bInheritHandle,
  _In_ DWORD dwProcessId
);

如果您阅读MSDN上的文档,就会明白,为此需要具备一定的访问权限。访问权限的完整列表可以在这里找到。

这些可能因MS Windows版本而异,不过几乎所有技术都需要用到以下内容。

HANDLE hProcess = OpenProcess(
    PROCESS_QUERY_INFORMATION |
    PROCESS_CREATE_THREAD |
    PROCESS_VM_OPERATION |
    PROCESS_VM_WRITE,
    FALSE, dwProcessId);


在目标/远程进程内分配内存


为了给DLL路径分配内存,我们需要使用VirtualAllocEx()。如MSDN所述,VirtualAllocEx()可以用来预留、提交或更改指定进程的虚拟地址空间内的内存区域的状态。该函数将其分配的内存初始化为零。

LPVOID WINAPI VirtualAllocEx(
  _In_     HANDLE hProcess,
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);

我们需要完成类似下面的工作:

// calculate the number of bytes needed for the DLL's pathname
DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t);
// allocate space in the target/remote process for the pathname
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);

此外,您还可以使用“GetFullPathName()”API调用。但是,我不会在整个项目中使用这个API调用。不过,这只是个人偏好的问题。

如果要为整个DLL分配空间,则必须执行以下操作:

hFile = CreateFileW(pszLibFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwSize, = GetFileSize(hFile, NULL);
PVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);


将DLL路径或DLL复制到目标/远程进程的内存中


现在只需要使用WriteProcessMemory()API调用将DLL路径或完整的DLL复制到目标/远程进程。

BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

这就像:

DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);

如果我们要复制完整的DLL,就像在反射DLL注入技术中那样,则还需要另外一些代码,因为这需要将其读入内存,然后再将其复制到目标/远程进程。

lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength);
ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL);
WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);

如前所述,通过使用反射DLL注入技术,并将DLL复制到内存中,DLL就不会被注册到进程中。

这稍微有点复杂,因为需要在内存中加载DLL时取得它的入口点。作为反射DLL项目用到的“LoadRemoteLibraryR()”函数可以为我们完成这些工作。如果你想查看源码的话,可以访问这里。

需要注意的一点是,我们要注入的DLL需要使用适当的include和options进行编译,使其与ReflectiveDLLInjection方法相适应。'injectAllTheThings'项目包括名为'rdll_32.dll / rdll_64.dll'的DLL,您可以使用它来完成这些工作。

 

让进程执行DLL


CreateRemoteThread()

CreateRemoteThread()是一种经典和最受欢迎的DLL注入技术。另外,它的说明文档也是最全面的。

它包括以下步骤:

1.用OpenProcess()打开目标进程

2.通过GetProcAddress()找到LoadLibrary()的地址

3.通过VirtualAllocEx()为目标/远程进程地址空间中的DLL路径预留内存

4.使用WriteProcessMemory()将DLL路径写入前面预留的内存空间中

5.使用CreateRemoteThread()创建一个新线程,该线程将调用LoadLibrary()函数,以DLL路径名称作为参数

如果浏览MSDN上的CreateRemoteThread()文档,会发现我们需要一个指向由线程执行的、类型为LPTHREAD_START_ROUTINE的应用程序定义函数的指针,它实际上是远程进程中线程的起始地址。

这意味着为了执行我们的DLL,只需要给我们的进程发出指示,让它来完成就行了。这样就简单了。

完整的步骤如下所示。

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessId);
// Allocate space in the remote process for the pathname
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
// Copy the DLL's pathname to the remote process address space
DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
// Get the real address of LoadLibraryW in Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
// Create a remote thread that calls LoadLibraryW(DLLPathname)
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL);

完整的源代码,请参阅CreateRemoteThread.cpp文件。

NtCreateThreadEx()

另一个选择是使用NtCreateThreadEx()。这是一个未公开的ntdll.dll函数,在将来它可能会消失或发生变化。这种技术实现起来有点复杂,因为我们需要使用一个结构体(见下文)作为参数传递给它,而使用另一个结构体接收来自它的数据。

struct NtCreateThreadExBuffer {
  ULONG Size;
  ULONG Unknown1;
  ULONG Unknown2;
  PULONG Unknown3;
  ULONG Unknown4;
  ULONG Unknown5;
  ULONG Unknown6;
  PULONG Unknown7;
  ULONG Unknown8;
};

这里有一篇对该调用的详细说明。这种方法与CreateRemoteThread()方法比较接近。

PTHREAD_START_ROUTINE ntCreateThreadExAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx"); 
LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr;
NTSTATUS status = funNtCreateThreadEx(
  &hRemoteThread,
  0x1FFFFF,
  NULL,
  hProcess,
  pfnThreadRtn,
  (LPVOID)pszLibFileRemote,
  FALSE,
  NULL,
  NULL,
  NULL,
  NULL
  );

完整的源代码,请参阅t_NtCreateThreadEx.cpp文件。

QueueUserAPC()

对于前面的方法,有一个替代:使用QueueUserAPC()函数。

如MSDN所述,这个调用“将用户模式异步过程调用(APC)对象添加到指定线程的APC队列。”

下面是具体定义。

DWORD WINAPI QueueUserAPC(
  _In_ PAPCFUNC  pfnAPC,
  _In_ HANDLE    hThread,
  _In_ ULONG_PTR dwData
);

pfnAPC [in]

A pointer to the application-supplied APC function to be called when the specified thread performs an alertable wait operation. (...)

hThread [in]

A handle to the thread. The handle must have the THREAD_SET_CONTEXT access right. (...)

dwData [in]

A single value that is passed to the APC function pointed to by the pfnAPC parameter.

所以,如果我们不想创建自己的线程,那么可以使用QueueUserAPC()来劫持目标/远程进程中的现有线程。也就是说,调用此函数将在指定的线程上对异步过程调用进行排队。

我们可以使用真正的APC回调函数代替LoadLibrary()。这里的参数实际上可以指向注入的DLL文件名的指针。

DWORD dwResult = QueueUserAPC((PAPCFUNC)pfnThreadRtn, hThread, (ULONG_PTR)pszLibFileRemote);

当你尝试这种技术的时候,你可能会注意到,这与MS Windows执行APC的方式有关。但是,这里没有查看APC队列的调度器,这意味着,只有当线程变为可警示状态时,队列才会被检查。

这样,我们就可以劫持每一个线程了,具体如下。

BOOL bResult = Thread32First(hSnapshot, &threadEntry);
  while (bResult)
  {
    bResult = Thread32Next(hSnapshot, &threadEntry);
    if (bResult)
    {
      if (threadEntry.th32OwnerProcessID == dwProcessId)
      {
        threadId = threadEntry.th32ThreadID;
 
        wprintf(TEXT("[+] Using thread: %i\n"), threadId);
        HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadId);
        if (hThread == NULL)
          wprintf(TEXT("[-] Error: Can't open thread. Continuing to try other threads...\n"));
        else
        {
          DWORD dwResult = QueueUserAPC((PAPCFUNC)pfnThreadRtn, hThread, (ULONG_PTR)pszLibFileRemote);
          if (!dwResult)
            wprintf(TEXT("[-] Error: Couldn't call QueueUserAPC on thread> Continuing to try othrt threads...\n"));
          else
            wprintf(TEXT("[+] Success: DLL injected via CreateRemoteThread().\n"));
          CloseHandle(hThread);
        }
      }
    }
  }

我们这样做,主要是想让一个线程变为可警示状态。

顺便说一句,很高兴看到这种技术被DOUBLEPULSAR应用。

完整的源代码,请参见“t_QueueUserAPC.cpp”文件。

SetWindowsHookEx()

为了使用这种技术,我们首先需要了解一下MS Windows钩子的工作原理。简单来说,钩子就是一种拦截事件并采取行动的方式。

你可能会猜到,会有很多不同类型的钩子。其中,最常见的是WH_KEYBOARD和WH_MOUSE。是的,你可能已经猜到了,它们可以用来监控键盘和鼠标的输入。

SetWindowsHookEx()的作用是“将应用程序定义的钩子装到钩子链中。”

HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);

idHook [in]

Type: int

The type of hook procedure to be installed. (...)

lpfn [in]

Type: HOOKPROC

A pointer to the hook procedure. (...)

hMod [in]

Type: HINSTANCE

A handle to the DLL containing the hook procedure pointed to by the lpfn parameter. (...)

dwThreadId [in]

Type: DWORD

The identifier of the thread with which the hook procedure is to be associated. (...)

MSDN上一个有趣的评论指出:

“SetWindowsHookEx可以用于将DLL注入到另一个进程中。32位DLL不能被注入到64位进程中,同时,64位DLL也不能被注入到32位进程中。如果应用程序需要在其他进程中使用钩子,则需要使用一个32位应用程序调用SetWindowsHookEx将32位DLL注入32位进程中,或者使用64位应用程序调用SetWindowsHookEx来把64位DLL注入64位进程。32位和64位DLL必须具有不同的名称。”

请大家务必记住这一点。

下面是取自具体实现中的一段代码。

GetWindowThreadProcessId(targetWnd, &dwProcessId);
HHOOK handle = SetWindowsHookEx(WH_KEYBOARD, addr, dll, threadID);

我们需要知道,发生的每个事件都将通过一个钩子链,这是一系列可以在事件中运行的过程。SetWindowsHookExe()要做的基本上就是如何将自己的钩子放入钩子链中。

上面的代码需要用到要安装的钩子类型(WH_KEYBOARD)、指向过程的指针、具有该过程的DLL的句柄以及将要挂钩的线程的ID。

为了获得指向程序的指针,我们需要首先使用LoadLibrary()调用加载DLL。然后,调用SetWindowsHookEx(),并等待我们想要的事件发生(这里而言就是按一个键)。一旦相应的事件发生,我们的DLL就会被执行。

完整的源代码,请参阅t_SetWindowsHookEx.cpp文件。

RtlCreateUserThread()

RtlCreateUserThread()是一个未公开的API调用。它的设置几乎与CreateRemoteThread()以及后面介绍的NtCreateThreadE()完全相同。

实际上,RtlCreateUserThread()会调用NtCreateThreadEx(),这意味着RtlCreateUserThread()是NtCreateThreadEx()的封装。所以,这里没有什么新玩意。但是,我们可能只想使用RtlCreateUserThread(),而不是NtCreateThreadEx()。即使后者发生了变动,我们的RtlCreateUserThread()仍然可以正常工作。

我们知道,mimikatz和Metasploit都使用RtlCreateUserThread()。如果你有兴趣的话,可以看看这里这里

所以,如果mimikatz和Metasploit正在使用RtlCreateUserThread()...是的,那些家伙都了解自己的代码...按照他们的“建议”,使用RtlCreateUserThread()——特别是,如果你打算鼓捣一些比简单的“injectAllTheThings”程序更复杂的事情的时候。

完整的源代码,请参阅t_RtlCreateUserThread.cpp。

SetThreadContext()

这实际上是一个很酷的方法。通过在目标/远程进程中分配一大块内存,以便将特制代码注入目标/远程进程。而该代码是负责加载DLL的。

下面给出的是32位的代码。

0x68, 0xCC, 0xCC, 0xCC, 0xCC,   // push 0xDEADBEEF (placeholder for return address)
0x9c,                           // pushfd (save flags and registers)
0x60,                           // pushad
0x68, 0xCC, 0xCC, 0xCC, 0xCC,   // push 0xDEADBEEF (placeholder for DLL path name)
0xb8, 0xCC, 0xCC, 0xCC, 0xCC,   // mov eax, 0xDEADBEEF (placeholder for LoadLibrary)
0xff, 0xd0,                     // call eax (call LoadLibrary)
0x61,                           // popad (restore flags and registers)
0x9d,                           // popfd
0xc3                            // ret

对于64位代码,没有找到任何可用的代码,只好自己动手了,具体如下。

0x50,                                                       // push rax (save rax)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (placeholder for return address)
0x9c,                                                       // pushfq
0x51,                                                       // push rcx
0x52,                                                       // push rdx
0x53,                                                       // push rbx
0x55,                                                       // push rbp
0x56,                                                       // push rsi
0x57,                                                       // push rdi
0x41, 0x50,                                                 // push r8
0x41, 0x51,                                                 // push r9
0x41, 0x52,                                                 // push r10
0x41, 0x53,                                                 // push r11
0x41, 0x54,                                                 // push r12
0x41, 0x55,                                                 // push r13
0x41, 0x56,                                                 // push r14
0x41, 0x57,                                                 // push r15
0x68,0xef,0xbe,0xad,0xde,                                   // fastcall convention
0x48, 0xB9, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rcx, 0CCCCCCCCCCCCCCCCh (placeholder for DLL path name)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (placeholder for LoadLibrary)
0xFF, 0xD0,                                                 // call rax (call LoadLibrary)
0x58,                                                       // pop dummy
0x41, 0x5F,                                                 // pop r15
0x41, 0x5E,                                                 // pop r14
0x41, 0x5D,                                                 // pop r13
0x41, 0x5C,                                                 // pop r12
0x41, 0x5B,                                                 // pop r11
0x41, 0x5A,                                                 // pop r10
0x41, 0x59,                                                 // pop r9
0x41, 0x58,                                                 // pop r8
0x5F,                                                       // pop rdi
0x5E,                                                       // pop rsi
0x5D,                                                       // pop rbp
0x5B,                                                       // pop rbx
0x5A,                                                       // pop rdx
0x59,                                                       // pop rcx
0x9D,                                                       // popfq
0x58,                                                       // pop rax
0xC3                                                        // ret

在将这个代码注入目标进程之前,需要填充/修补一些占位符: 

返回地址(代码完成执行后线程应该恢复的地址)。

DLL路径名。

LoadLibrary()的地址。

接下来就是劫持、暂停、注入和恢复线程发挥作用的时候。

我们首先需要连接到目标/远程进程,并将内存分配到目标/远程进程中。请注意,我们需要分配具有读取和写入权限的内存来保存DLL路径名,以及存放加载DLL的汇编代码。

LPVOID lpDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
stub = VirtualAllocEx(hProcess, NULL, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

接下来,我们需要获得在目标/远程进程上运行的一个线程的上下文(将要注入我们的汇编代码的线程)。

为找到线程,我们可以使用函数getThreadID(),它位于‘auxiliary.cpp’中。

一旦我们获得了线程id,就可以设置线程上下文了。

hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, threadID);

接下来,我们需要暂停线程来捕获其上下文。线程的上下文实际上就是其寄存器的状态。我们特别感兴趣的寄存器是EIP / RIP(有时称为IP指令指针)。

由于线程被暂停,所以我们可以更改EIP / RIP的值,并强制它继续在不同的路径(我们的代码洞)中执行。

ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
DWORD64 oldIP = ctx.Rip;
ctx.Rip = (DWORD64)stub;
ctx.ContextFlags = CONTEXT_CONTROL;
WriteProcessMemory(hProcess, (void *)stub, &sc, stubLen, NULL); // write code cave
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);

所以,我们暂停线程,捕获上下文,从中提取EIP / RIP。当我们注入的代码运行完成时,保存的这些数据将用来恢复现场。新的EIP / RIP设置为我们注入的代码位置。

然后我们使用返回地址、DLL路径名地址和LoadLibrary()地址对所有占位符进行填补。

一旦线程开始执行,我们的DLL将被加载,它一旦运行完成,将返回到被挂起的位置,并在那里恢复执行。

如果你想练习调试的话,这里有具体的操作指南。启动要注入的应用程序,如notepad.exe。运行injectAllTheThings_64.exe与x64dbg,如下所示。

t016f430e3e32eafac3.png

也就是说,我们可以使用以下命令行(具体视您的环境而定):

"C:\Users\rui\Documents\Visual Studio 2013\Projects\injectAllTheThings\bin\injectAllTheThings_64.exe" -t 6 notepad.exe "c:\Users\rui\Documents\Visual Studio 2013\Projects\injectAllTheThings\bin\dllmain_64.dll"

在调用WriteProcessMemory()处设置断点,如下所示。

t01211a0986f8be0fe4.png

        

当代码运行时,断点触发,这时要注意寄存器RDX的内存地址。至于为什么要关注RDX,可以阅读x64调用约定方面的资料。

t01cb123f3f25a95e8c.png

利用单步方式(F8)调用WriteProcessMemory(),启动x64dbg的另一个实例,并连接到'notepad.exe'。转到以前复制的地址(RDX中的地址),按“Ctrl + g”,您将看到我们的代码,如下所示。

t01515e1bf77a7c9c28.png

太棒了! 现在,请在这个shellcode的开头设置一个断点。转到被调试进程的injectAllTheThings,让它运行。正如在下面看到的,我们的断点触发,现在可以单步调试代码,进行仔细研究了。

t012b550b4dc04de29f.png

一旦调用LoadLibrary()了函数,就可以加载我们的DLL了。

t01549dce113bb97186.png

这真是太好了。

我们的shellcode将返回到之前保存的RIP的地址处,notepad.exe将恢复执行。

完整的源代码,请参阅t_suspendInjectResume.cpp。

反射型DLL注射

我还将Stephen Fewer(这种技术的先驱)代码引入了这个“injectAllTheThings”项目,此外,我还构建了一个反射型DLL项目使用这种技术。请注意,我们正在注入的DLL必须使用适当的include和options进行编译。

由于反射型DLL注入会将整个DLL复制到内存中,因此避免了使用进程注册DLL。我们已经完成了一切繁重的工作。要在DLL中加载内存时获取入口点,只需使用Stephen Fewer的代码即可。他的项目中包含的“LoadRemoteLibraryR()”函数为我们完成了这些工作。我们使用GetReflectiveLoaderOffset()来确定我们进程内存中的偏移量,然后使用该偏移加上目标/远程进程(我们写入DLL的位置)中的内存的基地址作为执行起始点。

有点太复杂了?是的,确实如此。下面是实现这一目标的4个主要步骤。

1.将DLL头写入内存

2.将每个节写入内存(通过解析节表)

3.检测import并加载所有其他已导入的DLL

4.调用DllMain入口点

与其他方法相比,这种技术提供了强大的隐蔽性,并在Metasploit中大量应用。

如果你想了解更多详情,请访问官方的GitHub信息库。此外,请务必阅读Stephen Fewer的文章

另外,最好读一下MemoryModule的作者Joachim Bauch写的一篇文章,讲述了如何从内存加载一个DLL,同时,这也是在没有LoadLibrary()的情况下手动加载Win32 / 64 DLL的好方法


代码


当然,还有一些复杂的注入方法,所以后面还会更新injectAllTheThings项目。我最近看到的最有趣的一些是:

DOUBLEPULSAR使用的一种方法

@zerosum0x0编写的,使用SetThreadContext()和NtContinue()的反射型DLL注入技术,此处提供了可用的代码

我上面描述的所有技术都是我在GitHub上提供的一个项目中已经实现的。此外,我还提供了每种技术所需的DLL。下表可以了解实际实现的内容以及使用方法。

http://p7.qhimg.com/t01d602d0cc9db1fbf2.png

不用说,为了安全起见,最好始终使用injectAllTheThings_32.exe注入32位进程或使用AllTheThings_64.exe注入64位进程。当然,您也可以使用injectAllTheThings_64.exe注入32位进程。其实我还没有实现这一点,但是我可能稍后会再试一次,你可以试着用WoW64鼓捣一下64位进程。Metasploit的smart_migrate基本上就是这种情况,具体请看这里

本文涉及的整个项目的所有代码,包括DLL,都可从GitHub下载。当然,如果您有兴致的话,也可以自己试着编译32位和64位代码。


本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:http://blog.deniable.org/blog/2017/07/16/inject-all-the-things

参与讨论,请先 | 注册 | 匿名评论
发布
用户评论
无任何评论