热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从概念到实际应用:详细讲解用户级API监控和代码注入检测方法

 一、前言在当今,网络犯罪分子致力于开发恶意软件,并用于感染主机以执行特定的活动。考虑到目前反病毒软件能力的不断提升,这些恶意软件也必须保持较强的存活能力,必须要在暗中进行操作,以避免被反病毒软件和系

 

一、前言

在当今,网络犯罪分子致力于开发恶意软件,并用于感染主机以执行特定的活动。考虑到目前反病毒软件能力的不断提升,这些恶意软件也必须保持较强的存活能力,必须要在暗中进行操作,以避免被反病毒软件和系统管理员觉察。在众多方法之中,代码注入是一个保证隐蔽性的较好方法。
本文主要描述恶意软件及其与Windows应用程序编程接口(WinAPI)交互的相关研究成果,详细介绍恶意软件是如何将恶意Payload植入其他进程,以及如何通过监控Windows操作系统的通信来检测此类行为。关于监控API调用的这一概念,在本文中也将会通过对特定函数进行Hook的过程来向大家展现,并且该概念将用于实现代码注入。
由于时间有限,我们以非常快的速度完成了该项目的研究,因此如果本文中有任何疏漏或错误之处,希望大家能够谅解,并期待各位的指正。此外,本文附带的代码可能会存在一些未知的设计缺陷,请各位自行参考。

 

二、相关概念



2.1 Inline Hooking

内联挂钩(Inline Hooking)是通过热补丁(Hotpatching)的方式绕开代码流的行为。在这里,热补丁是指在可执行映像运行期间对代码进行修改。之所以使用内联挂钩的方式,是为了能够捕获程序在何时调用函数的实例,以进行监控或实现调用。以下是一次函数调用正常执行的过程:

Normal Execution of a Function Call
+---------+ +----------+
| Program | ----------------------- calls function -----------------------------> | Function | | execution
+---------+ | . | | of
| . | | function
| . | |
| | v
+----------+

与执行一个挂钩后的函数相比:

Execution of a Hooked Function Call
+---------+ +--------------+ + -------> +----------+
| Program | -- calls function --> | Intermediate | | execution | | Function | | execution
+---------+ | Function | | of calls | . | | of
| . | | intermediate normal | . | | function
| . | | function function | . | |
| . | v | | | v
+--------------+ ------------------+ +----------+

该过程可以分为三个步骤。我们在这里,以WinAPI函数MessageBox为例,演示此过程。
(1) 对函数进行挂钩
想要挂钩(Hook)函数,我们首先需要一个中间函数,它必须能够复制目标函数的参数。在MSDN中对MessageBox的定义如下:

int WINAPI MessageBox(
_In_opt_ HWND hWnd,
_In_opt_ LPCTSTR lpText,
_In_opt_ LPCTSTR lpCaption,
_In_ UINT uType
);

所以,我们的中间函数可以这样定义:

int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
// our code in here
}

一旦存在,执行流就会重定向到代码中的特定位置。如果想要真正对MessageBox函数进行挂钩,我们可以修补代码的前几个字节(请注意,必须要对原始字节进行记录,以便在中间函数完成时能够恢复该函数)。以下是该函数的原始汇编指令,如该函数的相应模块user32.dll中所示:

; MessageBox
8B FF mov edi, edi
55 push ebp
8B EC mov ebp, esp

与挂钩后的函数相比:

; MessageBox
68 xx xx xx xx push ; our intermediate function
C3 ret

在这里,我选择了PUSH和RET的组合,而没有选择JMP,这是基于我之前的经验,前者会有更强的隐蔽性。其中的xx xx xx xx表示HookedMessageBox的小字节序顺序地址(Little-Endian Byte-Order Address)。
(2) 捕获函数调用
当程序调用MessageBox时,它将执行PUSH-RET并会成功跳入HookedMessageBox函数。一旦完成,程序就会完全控制参数和调用本身。如果想替换在消息对话框中所显示的文本,可以在HookedMessageBox中定义如下内容:

int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
TCHAR szMyText[] = TEXT("This function has been hooked!");
}

其中,szMyText可以用来替换MessageBox的LPCTSTR lpText参数。
(3) 恢复正常执行
要转发此参数,执行过程需要回到原始的MessageBox,以让操作系统可以显示该对话框。如果现在再次调用MessageBox,会导致无限递归(死循环),因此我们必须要恢复原始字节(如前所述)。

int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
TCHAR szMyText[] = TEXT("This function has been hooked!");
// restore the original bytes of MessageBox
// ...
// continue to MessageBox with the replaced parameter and return the return value to the program
return MessageBox(hWnd, szMyText, lpCaption, uType);
}

如果需要拒绝调用MessageBox,方法也非常简单,就像返回一个值一样,但这个值最好是在文档中定义过的。例如,想要从”是/否”对话框中返回”否”选项的值,其中间函数可以是:

int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
return IDNO; // IDNO defined as 7
}


2.2 API监控

在了解函数挂钩以后,我们紧接着讲解API监控的概念。我们有能力获得对函数调用的控制,同时也可以对所有参数进行监控,也就是标题所说的API监控。但是,还存在一个小问题,是由不同级别的API调用可用性导致的,尽管这些调用是唯一的,但在较低级别会使用相同的一组API。这就称为函数包装(Function Wrapping),定义为用于调用次级子程序的子程序。让我们回到MessageBox的示例,其中有两个定义的函数:MessageBoxA用于包含ASCII字符的参数,MessageBoxW用于包含宽字符的参数。实际上,如果我们要对MessageBox进行挂钩,就必须要同时修补MessageBoxA和MessageBoxW。要解决这一问题,我们需要尽可能在函数调用层次(Call Hierarchy)结构的最低公共点位置进行挂钩。

+---------+
| Program |
+---------+
/
| |
+------------+ +------------+
| Function A | | Function B |
+------------+ +------------+
| |
+-------------------------------+
| user32.dll, kernel32.dll, ... |
+-------------------------------+
+---------+ +-------- hook -----------------> |
| API | <---- + +-------------------------------------+
| Monitor | <-----+ | ntdll.dll |
+---------+ | +-------------------------------------+
+-------- hook -----------------> | User mode
-----------------------------------------------------
Kernel mode

以下是MessageBox调用层次结构:

MessageBoxA:
user32!MessageBoxA -> user32!MessageBoxExA -> user32!MessageBoxTimeoutA -> user32!MessageBoxTimeoutW
MessageBoxW:
user32!MessageBoxW -> user32!MessageBoxExW -> user32!MessageBoxTimeoutW

上述的调用层次,都将汇入MessageBoxTimeoutW,这是一个挂钩的绝佳位置。在这里要特别指出,对于具有更深层次结构的函数,由于函数参数的复杂程度有所增加,对较低位置的值进行挂钩可能会产生一些问题。MessageBoxTimeoutW是一个没有出现在文档中的WinAPI函数(Undocumented WinAPI Function),其定义如下:

int WINAPI MessageBoxTimeoutW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType,
WORD wLanguageId,
DWORD dwMilliseconds
);

其用法如下:

int WINAPI MessageBoxTimeoutW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds) {
std::wofstream logfile; // declare wide stream because of wide parameters
logfile.open(L"log.txt", std::ios::out | std::ios::app);
logfile < logfile < logfile < logfile.close();
// restore the original bytes
// ...
// pass execution to the normal function and save the return value
int ret = MessageBoxTimeoutW(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds);
// rehook the function for next calls
// ...
return ret; // return the value of the original function
}

当挂钩被放入MessageBoxTimeoutW中之后,MessageBoxA和MessageBoxW就都可以被捕获了。


2.3 代码注入入门

就本文而言,我们所讲解的”代码注入”是指将可执行代码插入到外部进程中。在WinAPI中,允许了一些功能,从而让代码注入成为了可能。在某些函数的共同作用下,我们可以访问现有进程,向其中写入数据,并且在其上下文中进行远程执行。在本节中,我们将介绍研究中涉及到的相关代码注入技术。


2.3.1 DLL注入

关于代码注入,代码可以是各种形式,其中之一就是动态链接库(DLL)。DLL是用来为可执行程序提供扩展功能的库,通过导出子程序的方式我们就可以使用该程序。下面是一个DLL示例,下文中都将以此为例:

extern "C" void __declspec(dllexport) Demo() {
::MessageBox(nullptr, TEXT("This is a demo!"), TEXT("Demo"), MB_OK);
}
bool APIENTRY DllMain(HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH)
::CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)Demo, nullptr, 0, nullptr);
return true;
}

当一个DLL被加载到进程并初始化时,加载程序将会调用Dllmain,并将fdwReason设置为DLL_PROCESS_ATTACH。在这个例子中,当它被加载到进程中时,会通过Demo子例程来显示一个消息框,其标题为”Demo”,文本内容为”This is a demo!”。要正确完成DLL的初始化,它必须返回True,否则就会被卸载。
2.3.1.1 CreateRemoteThread
通过CreateRemoteThread函数的DLL注入会利用此函数在另一个进程的虚拟空间中执行远程线程(Remote Thread)。如上所述,我们要执行DLL,只需要通过强制执行LoadLibrary函数来将其加载到进程中。以下代码可以完成此操作:

void injectDll(const HANDLE hProcess, const std::string dllPath) {
LPVOID lpBaseAddress = ::VirtualAllocEx(hProcess, nullptr, dllPath.length(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
::WriteProcessMemory(hProcess, lpBaseAddress, dllPath.c_str(), dllPath.length(), &dwWritten);
HMODULE hModule = ::GetModuleHandle(TEXT("kernel32.dll"));
LPVOID lpStartAddress = ::GetProcAddress(hModule, "LoadLibraryA"); // LoadLibraryA for ASCII string
::CreateRemoteThread(hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)lpStartAddress, lpBaseAddress, 0, nullptr);
}

在MSDN中,对LoadLibrary的定义如下:

HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);

该定义使用了一个参数,即要加载的所需库的路径名称。CreateRemoteThread函数允许将一个参数传递到与LoadLibrary的函数定义完全匹配的线程例程。其目标是在目标进程的虚拟地址空间中分配字符串参数,然后将分配的空间地址传递给CreateRemoteThread的参数中,以便调用LoadLibrary来加载DLL。
(1) 在目标进程中分配虚拟内存
使用VirtualAllocEx可以允许在选定的进程哪分配空间,一旦成功,会返回分配内存的起始地址。

Virtual Address Space of Target Process
+--------------------+
| |
VirtualAllocEx +--------------------+
Allocated memory ---> | Empty space |
+--------------------+
| |
+--------------------+
| Executable |
| Image |
+--------------------+
| |
| |
+--------------------+
| kernel32.dll |
+--------------------+
| |
+--------------------+

(2) 将DLL路径写入分配的内存
一旦内存被初始化,DLL的路径可以被注入到VirtualAllocEx使用WriteProcessMemory返回的分配内存中。

Virtual Address Space of Target Process
+--------------------+
| |
WriteProcessMemory +--------------------+
Inject DLL path ----> | "....myDll.dll" |
+--------------------+
| |
+--------------------+
| Executable |
| Image |
+--------------------+
| |
| |
+--------------------+
| kernel32.dll |
+--------------------+
| |
+--------------------+

(3) 获取LoadLibrary的地址
由于所有系统DLL都映射到所有进程的相同地址空间,因此LoadLibrary的地址不必直接从目标进程中检索,只需调用GetModuleHandle(TEXT(“kernel32.dll”))和GetProcAddress(hModule, “LoadLibraryA”)即可完成这项工作。
(4) 加载DLL
如果要加载DLL,就需要LoadLibrary的地址和DLL的路径。此时使用CreateRemoteThread函数,LoadLibrary会在目标进程的上下文中以DLL路径作为参数执行。

Virtual Address Space of Target Process
+--------------------+
| |
+--------------------+
+--------- | "....myDll.dll" |
| +--------------------+
| | |
| +--------------------+ <---+
| | myDll.dll | |
| +--------------------+ |
| | | | LoadLibrary
| +--------------------+ | loads
| | Executable | | and
| | Image | | initialises
| +--------------------+ | myDll.dll
| | | |
| | | |
CreateRemoteThread v +--------------------+ |
LoadLibraryA("....myDll.dll") --> | kernel32.dll | ----+
+--------------------+
| |
+--------------------+

2.3.1.2 SetWindowsHookEx
在Windows中,通过SetWindowsHookEx函数为开发人员提供了通过安装钩子来监视某些事件的功能。虽然这一功能经常被用于监控键盘输入并记录,但它其实也能用于注入DLL。以下代码演示了如何向其自身注入DLL:

int main() {
HMODULE hMod = ::LoadLibrary(DLL_PATH);
HOOKPROC lpfn = (HOOKPROC)::GetProcAddress(hMod, "Demo");
HHOOK hHook = ::SetWindowsHookEx(WH_GETMESSAGE, lpfn, hMod, ::GetCurrentThreadId());
::PostThreadMessageW(::GetCurrentThreadId(), WM_RBUTTONDOWN, (WPARAM)0, (LPARAM)0);
// message queue to capture events
MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0) > 0) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return 0;
}

MSDN中定义的SetWindowsHookEx如下:

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

在这里,需要一个HOOKPROC参数,该参数是触发特定挂钩事件时执行的用户定义的回调子例程。在这种情况下,事件是WH_GETMESSAGE,它负责处理消息队列中的消息。该代码最初将DLL加载到其自身的虚拟进程空间中,并且导出的Demo函数的地址将被获取并定义为调用SetWindowsHookEx中的回调函数。为了强制回调函数执行,使用了消息WM_RBUTTONDOWN调用PostThreadMessage,这样就能触发WH_GETMESSAGE钩子,从而显示消息框。
2.3.1.3 QueueUserAPC
使用QueueUserAPC的DLL注入与CreateRemoteThread类似,都是分配DLL路径并注入到目标进程的虚拟地址空间,然后在其上下文中强制调用LoadLibrary。

int injectDll(const std::string dllPath, const DWORD dwProcessId, const DWORD dwThreadId) {
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, false, dwProcessId);
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, false, dwThreadId);
LPVOID lpLoadLibraryParam = ::VirtualAllocEx(hProcess, nullptr, dllPath.length(), MEM_COMMIT, PAGE_READWRITE);
::WriteProcessMemory(hProcess, lpLoadLibraryParam, dllPath.data(), dllPath.length(), &dwWritten);
::QueueUserAPC((PAPCFUNC)::GetProcAddress(::GetModuleHandle(TEXT("kernel32.dll")), "LoadLibraryA"), hThread, (ULONG_PTR)lpLoadLibraryParam);
return 0;
}

其与CreateRemoteThread之间的一个主要区别是,QueueUserAPC能在可报警状态(Alertable State)下运行。由QueueUserAPC进行排队的异步过程仅在线程进入此状态时才进行处理。


2.3.2 Process Hollowing

Process Hollowing,也称RunPE,适用于逃避反病毒检测的一种常用方式。该方式允许将整个可执行文件的注入部分加载到目标进程中,并在其上下文中执行。通常我们可以在加密的应用程序中看到,与Payload相兼容的磁盘上文件会被选为主文件,并创建为进程,其主要可执行模块会被替换。该过程可以分为下述四个阶段。
(1) 创建主进程
为了注入Payload,引导程序必须会首先找到合适的主文件。如果Payload是.NET应用程序,那么主文件也必须是.NET应用程序。如果Payload是定义为使用控制台子系统的本地可执行文件,那么主文件也必须有相同的属性。这一原则在x86和x64程序时都要满足。一旦选择了主文件,随后便会使用CreateProcess(PATH_TO_HOST_EXE, …, CREATE_SUSPENDED, …)将其作为挂起的进程创建。

Executable Image of Host Process
+--- +--------------------+
| | PE |
| | Headers |
| +--------------------+
| | .text |
| +--------------------+
CreateProcess + | .data |
| +--------------------+
| | ... |
| +--------------------+
| | ... |
| +--------------------+
| | ... |
+--- +--------------------+

(2) 对主进程的Hollowing
为了让Payload能在注入后正常工作,必须要将其映射到虚拟地址空间,该虚拟地址空间要与在Payload PE头部的可选头中找到的ImageBase值相匹配。

typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; // <---- this is required later
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase; // <----
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // <---- size of the PE file as an image
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

这一点非常重要,因为绝对地址很可能完全依赖于其内存位置的代码。为了安全地映射可执行映像,必须从描述的ImageBase值开始的虚拟内存空间取消映射。由于许多可执行文件都会使用公共的基地址(通常是0x400000),因此主进程自己的可执行映像未映射的情况并不罕见。这一过程是通过NtUnmapViewOfSection(IMAGE_BASE, SIZE_OF_IMAGE)来完成的。

Executable Image of Host Process
+--- +--------------------+
| | |
| | |
| | |
| | |
| | |
NtUnmapViewOfSection + | |
| | |
| | |
| | |
| | |
| | |
| | |
+--- +--------------------+

(3) 注入Payload
要注入Payload,必须手动解析PE文件,以将其从磁盘形式转换为映像形式。在使用VirtualAllocEx分配虚拟内存之后,要将PE头部直接复制到该基地址。

Executable Image of Host Process
+--- +--------------------+
| | PE |
| | Headers |
+--- +--------------------+
| | |
| | |
WriteProcessMemory + | |
| |
| |
| |
| |
| |
| |
+--------------------+

要将PE文件转换为映像,必须单独从文件偏移量中读取所有部分,然后使用WriteProcessMemory将其正确放置到相应的虚拟偏移量中。这一点在本文每一节中都有详细的讲解。

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // <---- virtual offset
DWORD SizeOfRawData;
DWORD PointerToRawData; // <---- file offset
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Executable Image of Host Process
+--------------------+
| PE |
| Headers |
+--- +--------------------+
| | .text |
+--- +--------------------+
WriteProcessMemory + | .data |
+--- +--------------------+
| | ... |
+---- +--------------------+
| | ... |
+---- +--------------------+
| | ... |
+---- +--------------------+

(4) 执行Payload
最后一步,就是将执行的起始地址指向Payload的AddressOfEntryPoint。由于进程的主线程已经被挂起,所以可以使用GetThreadContext来检索相关信息。其上下文结构被定义为:

typedef struct _CONTEXT
{
ULONG ContextFlags;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
FLOATING_SAVE_AREA FloatSave;
ULONG SegGs;
ULONG SegFs;
ULONG SegEs;
ULONG SegDs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Edx;
ULONG Ecx;
ULONG Eax; // <----
ULONG Ebp;
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG Esp;
ULONG SegSs;
UCHAR ExtendedRegisters[512];
} CONTEXT, *PCONTEXT;

要修改起始地址,必须将Eax成员更改为Payload的AddressOfEntryPoint的虚拟地址。简单来说,context.Eax = ImageBase + AddressOfEntryPoint。通过调用SetThreadContext,将修改的部分传入CONTEXT结构,即可将上述更改应用到进程的线程。接下来,我们只需调用ResumeThread,就可以使Payload执行。


2.3.3 Atom Bombing

Atom Bombing是一种代码注入技术,它利用Windows全局原子表(Windows’s Global Atom Table)进行全局数据存储。全局原子表的数据可以通过所有进程访问,这样就使我们的代码注入成为了可能。存储在表中的数据是以空字符结尾的C字符串类型,使用一个名为atom的16位整数键表示,类似于map数据结构。在MSDN中提供了一个用于添加数据的GlobalAddAtom函数,其定义如下:

ATOM WINAPI GlobalAddAtom(
_In_ LPCTSTR lpString
);

其中,lpString是要存储的数据。在成功调用时,会返回16位整数的原子。为了检索存储在全局原子表中的数据,MSDN提供了一个GlobalGetAtomName,其定义如下:

UINT WINAPI GlobalGetAtomName(
_In_ ATOM nAtom,
_Out_ LPTSTR lpBuffer,
_In_ int nSize
);

其功能是传入从GlobalAddAtom返回的标识原子,会将数据放入lpBuffer并返回不包含空终止符的字符串长度。
Atom Bombing通过迫使目标进程加载并执行放置在全局原子表中的代码来实现,该过程依赖于另一个至关重要的函数NtQueueApcThread,它是QueueUserAPC的最低级用户空间调用。我们之所以使用NtQueueApcThread而不是QueueUserAPC的原因在于,与GlobalGetAtomName相比,QueueUserAPC的APCProc只能接收一个不匹配的参数。

VOID CALLBACK APCProc( UINT WINAPI GlobalGetAtomName(
_In_ ATOM nAtom,
_In_ ULONG_PTR dwParam -> _Out_ LPTSTR lpBuffer,
_In_ int nSize
); );

然而,在NtQueueApcThread的底层实现中,实际上是允许三个参数的:

NTSTATUS NTAPI NtQueueApcThread( UINT WINAPI GlobalGetAtomName(
_In_ HANDLE ThreadHandle, // target process's thread
_In_ PIO_APC_ROUTINE ApcRoutine, // APCProc (GlobalGetAtomName)
_In_opt_ PVOID ApcRoutineContext, -> _In_ ATOM nAtom,
_In_opt_ PIO_STATUS_BLOCK ApcStatusBlock, _Out_ LPTSTR lpBuffer,
_In_opt_ ULONG ApcReserved _In_ int nSize
); );

这是代码注入过程的可视化表示:

Atom bombing code injection
+--------------------+
| |
+--------------------+
| lpBuffer | <-+
| | |
+--------------------+ |
+---------+ | | | Calls
| Atom | +--------------------+ | GlobalGetAtomName
| Bombing | | Executable | | specifying
| Process | | Image | | arbitrary
+---------+ +--------------------+ | address space
| | | | and loads shellcode
| | | |
| NtQueueApcThread +--------------------+ |
+---------- GlobalGetAtomName ----> | ntdll.dll | --+
+--------------------+
| |
+--------------------+

以上,我们以简洁的方式介绍了Atom Bombing,但这些基础知识对于本文的研究已经足够了。如果你想了解更多关于Atom Bombing的只是,请参考enSilo的文章:https://blog.ensilo.com/atombombing-brand-new-code-injection-for-windows 。

 

三、UnRunPE

UnRunPE是我们的概念验证(PoC)工具,是为了将API监控的理论应用于实践之中而写的。该工具目的在于选定一个可执行文件,并创建挂起的进程,随后将DLL注入到该进程中,利用Process Hollowing技术来实现特定的功能。


3.1 代码注入检测

读完了我们代码注入的基础知识,Hollowing方法可以用下面的WinAPI调用链来描述:

CreateProcess
NtUnmapViewOfSection
VirtualAllocEx
WriteProcessMemory
GetThreadContext
SetThreadContext
ResumeThread

这些调用过程,不必按照特定顺序进行调用。例如,也可以在VirtualAllocEx之前调用GetThreadContext。然而,一些调用需要依赖之前的API调用,也不能将这些调用完全颠倒先后顺序。例如,SetThreadContext必须要在GetThreadContext或CreateProcess之前调用,否则目标进程就不会有Payload注入。该工具以上述内容为运行的基础,试图检测潜在的Process Hollowing过程。
根据API监控的理论,我们最好对较低的位置进行挂钩,但如果考虑到对恶意软件检测的场景,那么我们最理想的是在最低的位置挂钩。最厉害的恶意软件可能会尝试跳过更高级别的WinAPI函数,直接调用通常在ntdll.dll模块中所找到的调用层次结构中最低的函数。以下WinAPI函数是Process Hollowing的调用层次结构最低的函数:

NtCreateUserProcess
NtUnmapViewOfSection
NtAllocateVirtualMemory
NtWriteVirtualMemory
NtGetContextThread
NtSetContextThread
NtResumeThread


3.2 代码注入转储

当我们对必要的函数进行挂钩之后,就会执行目标进程,并记录每个已挂钩函数的参数,这样能跟踪Process Hollowing以及主进程的当前状态。最重要的两个钩子是NtWriteVirtualMemory和NtResumeThread,前者负责在代码注入后进行应用,后者负责执行。除了记录参数之外,UnRunPE还会尝试转储使用NtWriteVirtualMemory写入的字节,然后当到达NtResumeThread时,会尝试转储已经注入主进程的全部Payload。为了完成上述任务,它使用NtCreateUserProcess中记录的进程和线程句柄参数,以及NtUnmapViewOfSection记录的基地址和大小。在这里,如果使用NtAllocateVirtualMemory提供的参数可能更为合适,但实际过程中,由于某些暂不清楚的原因,如果挂钩该函数会在运行过程中出现一些错误。当Payload从NtResumeThread中转储出来后,它将会终止目标进程及其宿主进程,以防止注入的代码被真正执行。


3.3 UnRunPE示例

为了演示,我使用了之前创建的二进制木马文件,它包含主要可执行文件PEview.exe,同时隐藏了可执行文件PuTTY.exe。

 

四、实战:Dreadnought工具

Dreadnought是基于UnRunPE构建的PoC工具,用于更广泛的代码注入检测,也就是我们在本文2.3中讲解的全部代码注入。我们要实现全面的代码注入检测,就需要对工具进行一些强化。


4.1 代码注入检测方法

考虑到当前有很多种代码注入方法,因此必须对不同的代码注入技术进行区分。第一种方法是识别”触发”API调用,即负责远程执行Payload的API调用。共有四种类型:
段(Section):代码作为段被注入/代码被注入到段中。
进程:代码被注入到进程中。
代码:通用代码注入或Shellcode。
DLL:代码作为DLL被注入。

每一个触发API都列在相应的执行下面。当符合这些API中的任何一个时,Dreadought将会执行相应的代码转储方法,该方法与假设的注入类型相匹配,就类似于UnRunPE中Porcess Hollowing的方式。但是,这还远远不够,因为API调用仍然与可能会被混合,实现如箭头所示的功能。


4.2 启发式检测

为了让Dreadnought更准确地确定代码注入的方法,我们应该使用启发式检测。在开发过程中,我们应用了非常简单的启发式方法。在进程注入后,每次对API进行挂钩时,都会增加一个或多个存储在map数据结构中的相关代码注入类型的权重。由于它会跟踪每一个API调用,因此从一开始就有一个最高可能性的注入类型。一旦触发API被输入,它将识别并比较相关类型的权重,并进行调整。


4.3 Dreadnought实战



4.3.1 进程注入 – Process Hollowing


4.3.2 DLL注入 – SetWindowsHookEx


4.3.3 DLL注入 – QueueUserAPC


4.3.4 代码注入 – Atom Bombing



 

五、结论

本文对代码注入的技术进行了讲解,并帮助大家理解了其与WinAPI的交互过程。恶意软件利用注入的方式来绕过反病毒检测,我们可以使用Dreadnought来有效对这种行为进行检测。
然而,Dreadnought仍然存在一些局限性,它的启发式检测设计尚不完善,目前只能用于理论演示的目的,在实际使用中效果可能并不理想。因此,在操作系统正常运行期间,其可能会出现误报与漏报的情况。

 

六、PoC与相关源代码

https://github.com/NtRaiseHardError/UnRunPE
https://github.com/NtRaiseHardError/Dreadnought

 

七、参考

[1] https://www.blackhat.com/presentations/bh-usa-06/BH-US-06-Sotirov.pdf
[2] https://www.codeproject.com/Articles/7914/MessageBoxTimeout-API
[3] https://blog.ensilo.com/atombombing-brand-new-code-injection-for-windows
[4] http://struppigel.blogspot.com.au/2017/07/process-injection-info-graphic.html
[5] https://www.reactos.org/
[6] https://undocumented.ntinternals.net/
[7] http://ntcoder.com/category/undocumented-winapi/
[8] https://github.com/processhacker/processhacker
[9] https://www.youtube.com/channel/UCVFXrUwuWxNlm6UNZtBLJ-A
[10] https://www.youtube.com/channel/UC–DwaiMV-jtO-6EvmKOnqg


推荐阅读
  • 技术点:1、通过已知的网页路径获得流2、把流转换成字节数组3、把字节数组转换成String字符串显示在TextView控件中一、获得流publicstaticSt ... [详细]
  • 内存暴增排查分析
    一次偶然间,发现测试环境iis站点内存突然间暴增,平常都是300M,这次一下子暴增到8g于是就开始了接下来的分析发现Dictionary居然有1.78g懵逼windbg分析1.看看 ... [详细]
  • 为什么python是动态类型语言_Python 3.7.0 面向对象的动态类型语言
    代表Python开发社区和Python3.7发布团队,我们很高兴地宣布https:www.python.orgdownloadsreleasepython-370 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • JNI技术实践小结转自http:sett ... [详细]
  • 用户管理_用户管理的小项目
      之前学习链表数据结构的时候,写过(相信很多人都做过)dos窗口版的学生管理系统,通过输入数字来实现CURD学生的信息,顶多就是把数据写入文件来存储数据 ... [详细]
  • C#制作TextBox水印提示
    前言在使用C#的TextBox控件时,有时候会有以下需求:在用户没有输入文字时,TextBox有提示文字,如下图所示 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
  • d3dx9_26.dll极品飞车9修复工具下载及修复教程
    本文介绍了d3dx9_26.dll文件的修复工具下载和修复教程,解释了该dll文件的作用和安装方法,同时提供了其他dll文件下载安装的方法。文章涵盖了3d、windows、p2p、dll、visual studio等知识点,并由未来可期1212投稿。希望该技术和经验能帮到你解决dll文件相关技术问题。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
author-avatar
MySeptember
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有