I Pack You加密壳:实现页粒度的动态解密和惰性加密

发布时间:2026/5/25 1:39:28

I Pack You加密壳:实现页粒度的动态解密和惰性加密 I Pack You加密壳实现页粒度的动态解密和惰性加密上一篇文章I Pack You实现基本的软件壳框架搭建好基本框架之后我又向框架中添加了一些基础的静态反调试功能这篇文章主要记录一下如何实现页粒度的动态解密与惰性加密。需求与可行性分析之前我们是在stub中一次性解密整个text段这种方式很难对抗动态分析因为解密之后内存中的代码就是明文了很容易被dump下来。为了增强壳的强度我需要分多次解密text段同时对使用较少的代码片段重新加密待到其需要使用时在解密。这样一来内存中很难出现完整的代码明文对被加壳软件的保护力度得到了提升。为了实现动态解密我需要一种机制在遇到加密的代码时暂停执行程序将执行权转交到解密过程中。操作系统的分页机制与Windows VEH很好地满足了我们的需求。我们将代码的状态加密或解密与内存的非法访问关联起来通过VEH处理相关异常实现动态解密和加密的功能。采用VEH是因为它较于SEH更简单不依赖于pdata等存储的信息简化了stub的设计。页粒度的动态解密基本框架基本思想是这样的将整个text进行分页将每页的访问权限都设置为PAGE_NOACCESS注册VEH用于处理内存非法访问stub执行EP触发内存非法访问异常执行VEH回调VEH处理该异常进行解密等相关操作随后返回EXCEPTION_CONTINUE_EXECUTION程序继续运行若遇到内存非法访问则执行4这是一个非常简单的策略主要代码如下// 安装VEHPVOID hHandlerg_MyAddVectoredExceptionHandler(1,MyVEH);// 用于触发异常DWORD dwOld0;g_MyVirtualProtect((PVOID)sg_ullTextStart,g_param.dwTextSize,PAGE_NOACCESS,dwOld);// 执行原始入口点if(g_param.dwEPg_ImageBase){typedefvoid(*FUNC)();// 必然引发访问异常在VEH中动态解密FUNC ep(FUNC)(g_param.dwEPg_ImageBase);ep();}上面的代码为动态解密搭建好了框架MyVEH函数需要判断异常的类型是否为内存的非法访问同时还要判断发生异常的地址是否位于text段中LONGMyVEH(PEXCEPTION_POINTERS pExceptionInfo){if(pExceptionInfo-ExceptionRecord-ExceptionCodeEXCEPTION_ACCESS_VIOLATION)// 是否为访问异常{void*faultAddr(void*)pExceptionInfo-ExceptionRecord-ExceptionAddress;// 发生异常的地址是否在text节区中if((ULONGLONG)faultAddrsg_ullTextStart(ULONGLONG)faultAddrsg_ullTextEnd){// 解密DecryptTextPage(faultAddr);}returnEXCEPTION_CONTINUE_EXECUTION;}returnEXCEPTION_CONTINUE_SEARCH;}解密流程由DecryptTextPage实现在解密之后我们需要把页面的权限设置为可读可执行DWORD dwOld0;BOOL resg_MyVirtualProtect((PVOID)faultAddr,g_param.dwTextSize,PAGE_EXECUTE_READWRITE,dwOld);if(!res){g_MyExitProcess(1);}// 解密textPBYTE ch(PBYTE)sg_ullTextStart;for(size_t i0;ig_param.dwTextSize;i){ch[i]^KEY;}resg_MyVirtualProtect((PVOID)sg_ullTextStart,g_param.dwTextSize,PAGE_EXECUTE_READ,dwOld);if(!res){g_MyExitProcess(1);}难题指令跨页如果拿着上面的框架去给一个程序加壳那么有的程序能运行有的不能运行。通过调试VEH发现出错的地址都在页尾附近。这是因为如果刚好有一条指令它刚好占用了当前页页末和下一页页头部分的内存那么这条指令就会被划分为两部分已经解密的前半部分和加密的后半部分。如果前半部分刚好可以解释为一条新的指令那么在触发下一页的解密流程前CPU就会执行这条错误的指令。这带来了很严重的后果破坏了线程的上下文。CPU执行了一条错误的指令再次读取指令时即使下一页解密了但是读取到的字节已经不是原指令的起始字节了。解决指令跨页主要矛盾是页面大小的固定性和amd64处理器指令的不定长性。我们在解密的时候不能只解密发生错误的页面还要解密下一页面头部的若干字节这样即使有指令跨页那它也不会被分割为已解密的前半部分和未解密的后半部分。为此我们需要将第二页面的权限设置为可读的这样CPU就不会得到错误的指令。**但是当CPU尝试执行这条跨页指令时依然会遇到违法访问异常地址所在的页面仍旧是第一页面。也就是说VEH会反复检查第一页面是否解密发现已经解密了返回让程序继续执行程序执行跨页指令继续遇到访问违规异常VEH又尝试解密第一页面。**这样就会得到一个死循环。我们可以设计这样一个机制如果一个页面反复进入VEH处理到达一定次数后我们就对它的下一页进行解密并设置可执行权限。这样就跳出了死循环。将上面的解决方案组合起来我设计了一个结构体管理页面信息structPAGE_INFO{ULONGLONG ullPageBase;boolbHeadTag;// 头部15字节是否已解密boolbDecrypted;// 页是否解密// 当前页错误的次数如果是3次以上说明有指令跨页了此时解密下一页并给与其执行权限WORD wFaultCount;};每页对应一个结构体对象这些对象通过数组管理。同时更新我们的解密流程staticvoidDecryptTextPage(PVOID faultAddr){// 计算异常地址所在的页起始地址ULONGLONG ullFaultPageStart(ULONGLONG)faultAddr~(PAGE_SIZE-1);ULONGLONG ullFaultPageEnd(ULONGLONG)ullFaultPageStartPAGE_SIZE-1;ULONGLONG ullNextPageStartullFaultPageEnd1;ULONGLONG ullNextPageEndullNextPageStartPAGE_SIZE;// 注意处理最后一个页面的情况// 定位页对应的PAGE_INFODWORD dwIndex0;boolfindedfalse;for(dwIndex;dwIndexsg_dwTextPageNums;dwIndex)// 这里的查找可以优化因为ullPageBase是递增的{if(sg_pPageInfo[dwIndex].ullPageBaseullFaultPageStart){findedtrue;break;}}sg_pPageInfo[dwIndex].wFaultCount;// 这里说明有指令跨页了if(sg_pPageInfo[dwIndex].wFaultCount3){DecryptTextPage((PVOID)sg_pPageInfo[dwIndex1].ullPageBase);sg_pPageInfo[dwIndex].wFaultCount0;// 记得清零否则所有页都要不断进行扩展解密return;}// 更新活跃度当前页和下一页都更新sg_pPageInfo[dwIndex].dwLastDecryptTimeg_MyGetTickCount();sg_pPageInfo[dwIndex1].dwLastDecryptTimeg_MyGetTickCount();// 修改页面权限DWORD dwOld0;BOOL resg_MyVirtualProtect((PVOID)ullFaultPageStart,PAGE_SIZE,PAGE_EXECUTE_READWRITE,dwOld);if(!res){g_MyExitProcess(1);}// 开始解密流程if(!sg_pPageInfo[dwIndex].bDecrypted)// 当前页未解密{// 解密textPBYTE ch(PBYTE)ullFaultPageStart;if(sg_pPageInfo[dwIndex].bHeadTag)// 当前头部已解密跳过头部{chHEAD_SIZE;// 调整指针跳过头部for(size_t i0;iPAGE_SIZE-HEAD_SIZE;i){ch[i]^KEY;}}else// 当前头部未解密则解密当前整个页{for(size_t i0;iPAGE_SIZE;i){ch[i]^KEY;}sg_pPageInfo[dwIndex].bHeadTagtrue;}sg_pPageInfo[dwIndex].bDecryptedtrue;}// 修改为正常权限resg_MyVirtualProtect((PVOID)ullFaultPageStart,PAGE_SIZE,PAGE_EXECUTE_READ,dwOld);if(!res){g_MyExitProcess(1);}// 检查下一页面是否还在text中if(ullNextPageStartsg_ullTextStartullNextPageEndsg_ullTextEnd){resg_MyVirtualProtect((PVOID)ullNextPageStart,PAGE_SIZE,PAGE_EXECUTE_READWRITE,dwOld);if(!res){g_MyExitProcess(1);}// 检查下一页面头部是否解密if(!sg_pPageInfo[dwIndex1].bHeadTag)// 下一页头部未解密进行解密{PBYTE c(PBYTE)ullNextPageStart;for(size_t i0;iHEAD_SIZE;i){c[i]^KEY;}sg_pPageInfo[dwIndex1].bHeadTagtrue;// 设置头部标记}resg_MyVirtualProtect((PVOID)ullNextPageStart,PAGE_SIZE,PAGE_READONLY,dwOld);if(!res){g_MyExitProcess(1);}}}这样一来就解决了跨页指令产生的问题。惰性加密有了页粒度的动态解密框架之后惰性加密就很好实现了。我们可以新开一个线程结合计时器内核对象实现定期或不定期的加密通过关键段或其它的同步机制避免解密流程的竞争。还有更简单的在页信息结构体中新增一个字段dwLastDecryptTimestructPAGE_INFO{ULONGLONG ullPageBase;boolbHeadTag;// 头部15字节是否已解密boolbDecrypted;// 当前页错误的次数如果是3次以上说明有指令跨页了此时解密下一页并给与其执行权限WORD wFaultCount;// 最后一次解密时间DWORD dwLastDecryptTime;};在每次加密前都扫描一次页信息数组加密活跃值最小的若干页面// 遍历页面信息数组如果页面的活跃度小于设定的阈值则加密该页面staticvoidEncryptTextPage(PVOID pCurrentPage){// 以毫秒为单位constDWORD dwThreshold7000;DWORD dwCurentTimeg_MyGetTickCount();for(size_t i0;isg_dwTextPageNums;i){if(sg_pPageInfo[i].ullPageBase!(ULONGLONG)pCurrentPage){if(dwCurentTime-sg_pPageInfo[i].dwLastDecryptTimedwThreshold){// g_MyMessageBoxA(nullptr, AddressToString((PVOID)sg_pPageInfo[i].ullPageBase),// EncryptTextPage, MB_OK);PBYTE ch(PBYTE)sg_pPageInfo[i].ullPageBase;if(sg_pPageInfo[i].bHeadTag)// 头部已解密连同头部一起加密{for(size_t i0;iPAGE_SIZE;i){ch[i]^KEY;}sg_pPageInfo[i].bHeadTagfalse;}else// 头部未解密则头部不需要加密{chHEAD_SIZE;// 调整指针跳过头部for(size_t i0;iPAGE_SIZE-HEAD_SIZE;i){ch[i]^KEY;}}sg_pPageInfo[i].bDecryptedfalse;DWORD dwOld0;g_MyVirtualProtect((PVOID)sg_pPageInfo[i].ullPageBase,PAGE_SIZE,PAGE_NOACCESS,dwOld);}}}}这种方式要求设置一个合理的阈值。效果演示运行被加壳程序一段时间后检查text段发现存在未解密的页面

相关新闻