首页
社区
课程
招聘
[原创]I Pack You加密壳:实现页粒度的动态解密和惰性加密
发表于: 1天前 509

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

1天前
509

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

上一篇文章:I Pack You:实现基本的软件壳框架

搭建好基本框架之后,我又向框架中添加了一些基础的静态反调试功能,这篇文章主要记录一下如何实现页粒度的动态解密与惰性加密。

需求与可行性分析

之前我们是在stub中一次性解密整个text段,这种方式很难对抗动态分析,因为解密之后内存中的代码就是明文了,很容易被dump下来。

为了增强壳的强度,我需要分多次解密text段,同时对使用较少的代码片段重新加密,待到其需要使用时在解密。这样一来,内存中很难出现完整的代码明文,对被加壳软件的保护力度得到了提升。

为了实现动态解密,我需要一种机制:在遇到加密的代码时暂停执行程序,将执行权转交到解密过程中。

操作系统的分页机制与Windows VEH很好地满足了我们的需求。我们将代码的状态(加密或解密)与内存的非法访问关联起来,通过VEH处理相关异常,实现动态解密和加密的功能。

采用VEH是因为它较于SEH更简单,不依赖于pdata等存储的信息,简化了stub的设计。

页粒度的动态解密

基本框架

基本思想是这样的:

  1. 将整个text进行分页,将每页的访问权限都设置为PAGE_NOACCESS
  2. 注册VEH,用于处理内存非法访问
  3. stub执行EP,触发内存非法访问异常,执行VEH回调
  4. VEH处理该异常,进行解密等相关操作,随后返回EXCEPTION_CONTINUE_EXECUTION
  5. 程序继续运行,若遇到内存非法访问则执行4

这是一个非常简单的策略,主要代码如下:

// 安装VEH
PVOID hHandler = g_MyAddVectoredExceptionHandler(1, MyVEH);

// 用于触发异常
DWORD dwOld = 0;
g_MyVirtualProtect((PVOID)sg_ullTextStart, g_param.dwTextSize, PAGE_NOACCESS, &dwOld);

// 执行原始入口点
if (g_param.dwEP && g_ImageBase)
{
    typedef void (*FUNC)();
    // 必然引发访问异常,在VEH中动态解密
    FUNC ep = (FUNC)(g_param.dwEP + g_ImageBase);
    ep();
}

上面的代码为动态解密搭建好了框架,MyVEH函数需要判断异常的类型是否为内存的非法访问,同时还要判断发生异常的地址是否位于text段中:

LONG MyVEH(PEXCEPTION_POINTERS pExceptionInfo)
{
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) // 是否为访问异常
    {
        void *faultAddr = (void *)pExceptionInfo->ExceptionRecord->ExceptionAddress;

        // 发生异常的地址是否在text节区中
        if ((ULONGLONG)faultAddr >= sg_ullTextStart && (ULONGLONG)faultAddr < sg_ullTextEnd)
        {
            // 解密
            DecryptTextPage(faultAddr);
        }

        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

解密流程由DecryptTextPage实现,在解密之后,我们需要把页面的权限设置为可读可执行:

DWORD dwOld = 0;
BOOL res = g_MyVirtualProtect((PVOID)faultAddr, g_param.dwTextSize, PAGE_EXECUTE_READWRITE, &dwOld);
if (!res)
{
    g_MyExitProcess(1);
}

// 解密text
PBYTE ch = (PBYTE)sg_ullTextStart;
for (size_t i = 0; i < g_param.dwTextSize; i++)
{
    ch[i] ^= KEY;
}

res = g_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处理到达一定次数后,我们就对它的下一页进行解密并设置可执行权限。这样就跳出了死循环。

将上面的解决方案组合起来,我设计了一个结构体管理页面信息:

struct PAGE_INFO
{
    ULONGLONG ullPageBase;
    bool bHeadTag; // 头部15字节是否已解密
    bool bDecrypted; // 页是否解密
    
    // 当前页错误的次数,如果是3次以上,说明有指令跨页了,此时解密下一页并给与其执行权限
    WORD wFaultCount;
};

每页对应一个结构体对象,这些对象通过数组管理。

同时更新我们的解密流程:

static void DecryptTextPage(PVOID faultAddr)
{
    // 计算异常地址所在的页起始地址
    ULONGLONG ullFaultPageStart = (ULONGLONG)faultAddr & ~(PAGE_SIZE - 1);
    ULONGLONG ullFaultPageEnd = (ULONGLONG)ullFaultPageStart + PAGE_SIZE - 1;
    ULONGLONG ullNextPageStart = ullFaultPageEnd + 1;
    ULONGLONG ullNextPageEnd = ullNextPageStart + PAGE_SIZE; // 注意处理最后一个页面的情况

    // 定位页对应的PAGE_INFO
    DWORD dwIndex = 0;
    bool finded = false;
    for (dwIndex; dwIndex < sg_dwTextPageNums; dwIndex++) // 这里的查找可以优化,因为ullPageBase是递增的
    {
        if (sg_pPageInfo[dwIndex].ullPageBase == ullFaultPageStart)
        {
            finded = true;
            break;
        }
    }

    sg_pPageInfo[dwIndex].wFaultCount++;
    // =======================这里说明有指令跨页了
    if (sg_pPageInfo[dwIndex].wFaultCount > 3)
    {
        DecryptTextPage((PVOID)sg_pPageInfo[dwIndex + 1].ullPageBase);
        sg_pPageInfo[dwIndex].wFaultCount = 0; // 记得清零,否则所有页都要不断进行扩展解密
        return;
    }

    // =======================更新活跃度,当前页和下一页都更新
    sg_pPageInfo[dwIndex].dwLastDecryptTime = g_MyGetTickCount();
    sg_pPageInfo[dwIndex + 1].dwLastDecryptTime = g_MyGetTickCount();

    // 修改页面权限
    DWORD dwOld = 0;
    BOOL res = g_MyVirtualProtect((PVOID)ullFaultPageStart, PAGE_SIZE, PAGE_EXECUTE_READWRITE, &dwOld);
    if (!res)
    {
        g_MyExitProcess(1);
    }

    // 开始解密流程
    if (!sg_pPageInfo[dwIndex].bDecrypted) // 当前页未解密
    {
        // 解密text
        PBYTE ch = (PBYTE)ullFaultPageStart;
        if (sg_pPageInfo[dwIndex].bHeadTag) // 当前头部已解密,跳过头部
        {
            ch += HEAD_SIZE; // 调整指针跳过头部
            for (size_t i = 0; i < PAGE_SIZE - HEAD_SIZE; i++)
            {
                ch[i] ^= KEY;
            }
        }
        else // 当前头部未解密,则解密当前整个页
        {
            for (size_t i = 0; i < PAGE_SIZE; i++)
            {
                ch[i] ^= KEY;
            }
            sg_pPageInfo[dwIndex].bHeadTag = true;
        }
        sg_pPageInfo[dwIndex].bDecrypted = true;
    }

    // 修改为正常权限
    res = g_MyVirtualProtect((PVOID)ullFaultPageStart, PAGE_SIZE, PAGE_EXECUTE_READ, &dwOld);
    if (!res)
    {
        g_MyExitProcess(1);
    }

    // 检查下一页面是否还在text中
    if (ullNextPageStart >= sg_ullTextStart && ullNextPageEnd <= sg_ullTextEnd)
    {
        res = g_MyVirtualProtect((PVOID)ullNextPageStart, PAGE_SIZE, PAGE_EXECUTE_READWRITE, &dwOld);
        if (!res)
        {
            g_MyExitProcess(1);
        }

        // 检查下一页面头部是否解密
        if (!sg_pPageInfo[dwIndex + 1].bHeadTag) // 下一页头部未解密,进行解密
        {
            PBYTE c = (PBYTE)ullNextPageStart;
            for (size_t i = 0; i < HEAD_SIZE; i++)
            {
                c[i] ^= KEY;
            }
            sg_pPageInfo[dwIndex + 1].bHeadTag = true; // 设置头部标记
        }

        res = g_MyVirtualProtect((PVOID)ullNextPageStart, PAGE_SIZE, PAGE_READONLY, &dwOld);
        if (!res)
        {
            g_MyExitProcess(1);
        }
    }
}

这样一来,就解决了跨页指令产生的问题。

惰性加密

有了页粒度的动态解密框架之后,惰性加密就很好实现了。

我们可以新开一个线程,结合计时器内核对象实现定期或不定期的加密,通过关键段或其它的同步机制避免解密流程的竞争。

还有更简单的,在页信息结构体中新增一个字段dwLastDecryptTime:

struct PAGE_INFO
{
    ULONGLONG ullPageBase;
    bool bHeadTag; // 头部15字节是否已解密
    bool bDecrypted;
    
    // 当前页错误的次数,如果是3次以上,说明有指令跨页了,此时解密下一页并给与其执行权限
    WORD wFaultCount;

    // 最后一次解密时间
    DWORD dwLastDecryptTime;
};

在每次加密前都扫描一次页信息数组,加密活跃值最小的若干页面:

// 遍历页面信息数组,如果页面的活跃度小于设定的阈值,则加密该页面
static void EncryptTextPage(PVOID pCurrentPage)
{
    // 以毫秒为单位
    const DWORD dwThreshold = 7000;
    DWORD dwCurentTime = g_MyGetTickCount();
    for (size_t i = 0; i < sg_dwTextPageNums; i++)
    {
        if (sg_pPageInfo[i].ullPageBase != (ULONGLONG)pCurrentPage)
        {
            if (dwCurentTime - sg_pPageInfo[i].dwLastDecryptTime <= dwThreshold)
            {
                // 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 i = 0; i < PAGE_SIZE; i++)
                    {
                        ch[i] ^= KEY;
                    }
                    sg_pPageInfo[i].bHeadTag = false;
                }
                else // 头部未解密,则头部不需要加密
                {
                    ch += HEAD_SIZE; // 调整指针跳过头部
                    for (size_t i = 0; i < PAGE_SIZE - HEAD_SIZE; i++)
                    {
                        ch[i] ^= KEY;
                    }
                }
                sg_pPageInfo[i].bDecrypted = false;

                DWORD dwOld = 0;
                g_MyVirtualProtect((PVOID)sg_pPageInfo[i].ullPageBase, PAGE_SIZE, PAGE_NOACCESS, &dwOld);
            }
        }
    }
}

这种方式要求设置一个合理的阈值。

效果演示

运行被加壳程序一段时间后,检查text段,发现存在未解密的页面:

图片描述


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 3769
活跃值: (2334)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
6ceK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6b7L8%4c8S2N6r3!0a6k6V1c8G2L8$3#2Q4x3V1k6F1L8%4c8Q4x3X3c8T1P5h3k6J5L8$3^5`.
1天前
1
雪    币: 674
活跃值: (285)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
Saileaxh 415K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6b7L8%4c8S2N6r3!0a6k6V1c8G2L8$3#2Q4x3V1k6F1L8%4c8Q4x3X3c8T1P5h3k6J5L8$3^5`.
这个项目我之前没听说过。用page和VEH是我自己想出来的,还有很多不足。这个项目给了我很好的参考,感谢分享,祝君好运
1天前
1
游客
登录 | 注册 方可回帖
返回