在论坛一直混迹也不短时间了,看了很多人的技术文章,总想着自己有一天也能写些东西。遂把这几个月做的一款壳分享一下,若能指点一二,小生感激不尽。因为本人写壳经验不长,其中参考了论坛上不少人的文章,前期感觉发出来有点见不得人哈,后来想想总有人需要的,这里算是做一个整合和优化,并且提出了一些新的保护思路。
1.本系统提供了加壳 和 API 监控
2.加壳模块的基本功能包括压缩引擎(aplib、JCALG1)、IAT转储、HOOT-IAT、重定位转储
3.加壳模块的附加功能包括反调试、反dump、反OD、混淆函数和校验(内存校验和文件校验)
4.API监控模块可以记录选中的DLL的API调用信息(调用地址、调用模块、API名称和调用次数)和API 调用参数信息
5.新的思路:利用API的调用频率作为触发概率的参考条件,代码混淆模块对频繁调用的API有更大的概率实现代码混淆,反之亦然
6.利用已有的花指令模板来实现代码混淆
7.提供了针对普通用户的三套方案(性能、安全和均衡)
8.可以对EXE和DLL进行加壳
对部分程序进行测试,加壳的成功率和兼容性表现良好。结合了API监控的代码混淆属于测试功能,只能对特定程序进行测试,当前的意义仅仅为提供了一个新的保护思路。下面给出系统的界面效果。
接下来就该系统各个部分的功能做一个粗略但求精简的讲解。第一部分讨论加壳中各模块细节,第二部分讲述 API 监控 以及 第三部分为代码混淆。
该小节主要讲述IAT转储、重定位表转储、HOOK-IAT 和 压缩引擎以及压缩过程。
IAT转储是将导入表实现一个结构的简化并存储到指定被保护的区域。此处直接使用了《加密与解密》第三版提供的转储结构。结构如下图。
转储代码的细节提供在项目文件中,有需要的读者可自行阅览。
重定位表亦使用但是改进了第三版中提供的结构。因为在原版的结构中,只用了1个byte去存储偏移量,但是在实际当中,某些重定位项之间的偏移会大于0xff。由于内存对齐长度为0x1000,而每一重定位项之间的偏移小于0xfff,所以在一份内存页当中2个byte适合用于转储。值得注意的,在测试某些样本中,两个区段的重定位项的偏移可能会大于0xfff,所以每一个内存页都要保存相同的转储结构。
最后,保存的形式如下图。这样就可以清晰地区分每一个需要重定位的内存页了。除此之外,在进行修复每一页时,都要使用VirtualProtect修改内存中该页的属性,使其变为可写,否则会触发中断崩溃。
4.1.3 压缩引擎
压缩后的样本区段结构和upx是一样的。本系统使用了比较常见的压缩库,包括aplib和JCALG1,前者效率均衡,对PE文件的处理较好,后者对含有较多资源的程序表现更佳。下面给出两个压缩库的使用过程。值得一提,若是遇到TLS表的话,理论上可以压缩,但是只能在TLS表之后压缩,这种压缩策略不仅使操作复杂化,而且效率也不高,故而本系统对含有TLS的程序不进行压缩处理。(注:发现JCALG1不能压缩过大的文件,问题还未解决)
下面给出压缩与解压后的区段结构。pack0是占位区段,用于存放解压后的数据;pack1保存着压缩区段后的数据;pack2保存着外壳代码。rsrc保存着转储后的小部分关键资源(例如Icons、Dialogs和Group Icons)。
4.1.4 其他相关保护技术
1.文件校验
对程序文件的数据进行CRC32的计算,计算的值保存到PE标识的前4个byte中。
VOID COperationPE::CalAndSaveCRC(DWORD dwFileSize)
{
DWORD dwCrc32; //计算的值
//1. 生成CRC32表格
if(m_bCRC32Table == FALSE)
MakeCRC32Table();
//2. 计算PE头之后的数据
dwCrc32 = CalcuCRC((UCHAR*)(m_pDosHeader->e_lfanew + m_dwFileDataAddr), dwFileSize - m_pDosHeader->e_lfanew);
//3. 将该CRC32值写进PE头标识前4个字节
*(PDWORD)((DWORD)m_pNtHeader - 4) = dwCrc32;
}
2.内存校验
对未压缩的和加密的代码段进行CRC32的计算,如果存在重定位表,则不进行计算,因为重定位修复时会修正全局变量的绝对地址,导致前后计算的数值不一致,从而校验失败。
BOOL CodeMemVerification()
{
DWORD dwCodeBase;
DWORD dwCodeSize;
DWORD dwCRC32;
dwCodeBase = g_stcShellData.dwCodeBase;
dwCodeSize = g_stcShellData.dwCodeSize;
dwCRC32 = CalcuCRC( (UCHAR*) (g_dwImageBase + dwCodeBase), dwCodeSize);
//如果有重定位修复,则不进行验证,因为修复全局变量会改变代码段,所以CRC32的计算会出错
if(g_stcShellData.stcPERelocDir.VirtualAddress == 0)
// if(g_stcShellData.stcIATDir.VirtualAddress == 0)
if (dwCRC32 != g_stcShellData.dwCodeMemCRC32) return FALSE;
return TRUE;
}
3.反Dump
这是一个非常常见的保护手段,通过修改PEB结构来使某些工具无法获取到加壳程序的进程信息。特别是使用了某些直接获取PEB结构信息的Windows API的工具。借鉴网上通用的某些代码,但是发现PEB中有些信息的删除会导致程序异常,故而将其精简后得到最终代码,效果如下图。
可见原来进程的信息被替换了ntdll.dll,而且进程模块的基本信息(基址和大小)都被抹去。
4.混淆函数
相当于一个花指令黑盒,把运行地址作为参数传入该函数,最后在花指令结束后便跳转回目标代码处执行。
5.反OD
原理很简单,由于用OD载入程序时,内存中会产生某些特征字符串,故而可以通过搜索这些信息来进行判读自身是否被OD载入调试。
由于在x86程序的虚拟内存中,有2G只分配给用户空间的,所以只需要对该区域进行扫描,经过测试,算法的时间复杂度对壳的性能影响相当大,所以选择优秀的算法非常重要。在本系统中,选择KMP做为字符串匹配算法。
6.反调试
简单的方法有很多,可以通过调用Windows API来查看,也可以自己去PEB结构里面查,本系统采用后者,这种保护手段早已经过时,在这里只是做为一个壳保护功能的完善罢了。
4.2 API监控
这里直接参考SoftSnoop的代码,并且对其的通信模式与hook方法进行修改。本系统直接使用detour库进行inline hook,这个库的健壮性不是盖的,毕竟是微软自己人做的东西。其次,除了商用软件,发现目前开源的抑或网友制作的工具,稳定性都不太好,故而就萌生一份将其优化与增加适用性的想法。最后,这个模块主要是为代码乱序提供一个API调用频率的信息。
首先给出该模块与SoftSnoop区别的细节:
1)通信模式改变,由于本系统是用Qt去事先界面,所以监控dll与UI之间的通信不能在使用原来的消息机制,而是改成了命名管道。
2)hook方法更变,原来是对某个模块所有的api进行hook,后来测试发现,有些api不可以hook,原因是Windows中有些涉及到窗口的API是回调函数,如果对这些API进行hook,那也会导致一个不可想象的调用膨胀,以至于崩溃。所以,本系统改变策略,只对提前约定好的API进行hook并解析,这样既可以获取关键的信息,也可以提高程序性能。
4.2.1 流程与原理
下面给出遍历进程模块的一个流程。
接下来给出命名管道的通信模式。指令的功能包括,遍历某个模块的api名称,其次还可以对某个规定好的api进行hook。
4.2.2 监控日志
接下来就是api监控结束后,除了会生成一份简陋的txt格式的日志文件,如下图。
还会生成一个给代码乱序引擎使用的一个日志。日志包含约定记录的api与其调用次数,而它的存储格式如下。
用二进制工具查看。
4.2.3 添加规则
添加的规则方式如
SoftSnoop提供的规则文件是一样的。当初因为作这款作品是有一些目的,所以在这一块就没有比较完善,其实我当初的设想是把规则做成一个可视化,更方便使用。这里给出一个kernel32模块约定监控的api的例子。
4.3 代码乱序引擎
这里首先要感谢玩命提供的代码乱序分析框架,这个模块的本意是想实现一个代码虚拟化,但是由于能力不足,时间不够,百忙之中没有办法实现,故而先通过开发一个代码乱序来测试结合api监控的效果。
4.3.1 总体设计
1.反汇编引擎 - udis86
2.链式存储指令信息 - 记录每一个需要修改的指令的信息
这里直接使用了玩命提供的结构体,开始觉得信息很多,写到后面觉得这些信息都很重要。
typedef struct _Code_Flow_Node
{
struct _Code_Flow_Node *pNext; //下一个节点
BOOL bGoDown; //是否向下跳
DWORD dwBits; //跳转范围
DWORD dwType; //指令类型
BOOL bFar; //是否是远跳
DWORD dwMemoryAddress; //当前内存地址
LPBYTE pFileAddress; //当前文件地址
DWORD dwGotoMemoryAddress; //跳转后的内存地址
LPBYTE pGotoFileAddress; //跳转后的文件地址
DWORD dwInsLen; //指令长度
pImport_Node pImpNode; //在IAT中的节点信息
DWORD dwFunIndex; //节点函数表的索引
DWORD dwFinalMemoryAddress; //花指令的内存地址
DWORD dwFinalFileAddress; //花指令的文件地址
BOOL bConfused; //是否乱序
union
{
BYTE bOffset;
WORD wOffset;
DWORD dwOffset;
};//偏移
}Code_Flow_Node, *pCode_Flow_Node;
3.花指令模板
这里仅仅提供了2套花指令模板作为测试,当初想寻找一个自动花指令的生成代码,但是测试之后发现效果都不好,遂放弃。乱序后的指令会跳转到一个充满花指令的区域执行。
4.导入表-函数检测
众所周知,静态链接直接将功能代码放入代码段,则调用与跳转一般为短跳转,就会与普通的跳转调用指令产生混淆,无法分辨出这是否在调用一个函数。故而这里设计的理念就是找出调用动态链接库的函数的跳转,然后将其记录下来。
涉及的原理:
- Windows PE导入表的结构
- 编译器如何实现对动态链接库函数的调用
- 反汇编引擎及IA-32架构的OPCODE
由下图可知,若某主调函数想调用动态链接库的函数,则一般在实际流程中先Call进入到指定的远跳转区域。再由远跳转FF25(JMP)到DLL的代码中,或直接FF15(CALL)。而FF15(JMP)指令会读取IAT中的数据作为绝对地址去跳转。
可知,反汇编引擎的工作就是找到FF15(JMP)。所以,整个分析由几步构成。
第一,解析到Call,记录下来;
第二,检测Call到的下一个地址是否是远跳转指令FF15;
第三,若是的话,提取出该偏移量,匹配IAT,获得被调用的函数名与相应的动态库名。
5.调用样本数据 与 随机概率 - Api调用次数
由于我们并不希望每个跳转都被乱序化,这样不仅保护效果没有针对性,还消耗了大量空间去存放花指令。所以根据这个想法,调用次数占总次数的比例越大,则被乱序化的可能性就越高。例如下表。
API |
Count |
CreateFile |
x1 |
VirtualAlloc |
x2 |
....... |
xn |
计算出每个API的调用概率:
随机概率处理的情况:
- 无函数识别及调用样本数据 --> 1/4
- 该函数未产生调用信息 --> 1/3
- 该函数产生调用信息 --> 1/3 + Pxi
6.区段信息及结构
7.花指令平均长度估计区段大小
由于存放花指令的区段是需要事先计算的,否则就导致空间不够使用的情况。
值得注意,有些程序的调用次数相当巨大,会产生大量的结点信息,故而,这里对100个以上的结点数,直接视为100个。这100平均长度的空间用完则不再处理后序的指令了。
4.3.2 测试效果
这里用汇编写了一个switch - case ,循环次数随机地,在每个case中调用一个Windows API的程序。下图是switch - case 的结构。
下图是代码乱序前的部分情况。
下图是代码乱序后的情况。
5.参考资料
[1] SoftSnoop 1.3.2 + Source(增加了中文版和说明文档) -
https://bbs.pediy.com/thread-55974.htm
重定位表亦使用但是改进了第三版中提供的结构。因为在原版的结构中,只用了1个byte去存储偏移量,但是在实际当中,某些重定位项之间的偏移会大于0xff。由于内存对齐长度为0x1000,而每一重定位项之间的偏移小于0xfff,所以在一份内存页当中2个byte适合用于转储。值得注意的,在测试某些样本中,两个区段的重定位项的偏移可能会大于0xfff,所以每一个内存页都要保存相同的转储结构。
重定位表亦使用但是改进了第三版中提供的结构。因为在原版的结构中,只用了1个byte去存储偏移量,但是在实际当中,某些重定位项之间的偏移会大于0xff。由于内存对齐长度为0x1000,而每一重定位项之间的偏移小于0xfff,所以在一份内存页当中2个byte适合用于转储。值得注意的,在测试某些样本中,两个区段的重定位项的偏移可能会大于0xfff,所以每一个内存页都要保存相同的转储结构。
最后,保存的形式如下图。这样就可以清晰地区分每一个需要重定位的内存页了。除此之外,在进行修复每一页时,都要使用VirtualProtect修改内存中该页的属性,使其变为可写,否则会触发中断崩溃。
压缩后的样本区段结构和upx是一样的。本系统使用了比较常见的压缩库,包括aplib和JCALG1,前者效率均衡,对PE文件的处理较好,后者对含有较多资源的程序表现更佳。下面给出两个压缩库的使用过程。值得一提,若是遇到TLS表的话,理论上可以压缩,但是只能在TLS表之后压缩,这种压缩策略不仅使操作复杂化,而且效率也不高,故而本系统对含有TLS的程序不进行压缩处理。(注:发现JCALG1不能压缩过大的文件,问题还未解决)
下面给出压缩与解压后的区段结构。pack0是占位区段,用于存放解压后的数据;pack1保存着压缩区段后的数据;pack2保存着外壳代码。rsrc保存着转储后的小部分关键资源(例如Icons、Dialogs和Group Icons)。
1.文件校验
对程序文件的数据进行CRC32的计算,计算的值保存到PE标识的前4个byte中。
VOID COperationPE::CalAndSaveCRC(DWORD dwFileSize)
{
DWORD dwCrc32; //计算的值
//1. 生成CRC32表格
if(m_bCRC32Table == FALSE)
MakeCRC32Table();
//2. 计算PE头之后的数据
dwCrc32 = CalcuCRC((UCHAR*)(m_pDosHeader->e_lfanew + m_dwFileDataAddr), dwFileSize - m_pDosHeader->e_lfanew);
//3. 将该CRC32值写进PE头标识前4个字节
*(PDWORD)((DWORD)m_pNtHeader - 4) = dwCrc32;
}
2.内存校验
对未压缩的和加密的代码段进行CRC32的计算,如果存在重定位表,则不进行计算,因为重定位修复时会修正全局变量的绝对地址,导致前后计算的数值不一致,从而校验失败。
BOOL CodeMemVerification()
{
DWORD dwCodeBase;
DWORD dwCodeSize;
DWORD dwCRC32;
dwCodeBase = g_stcShellData.dwCodeBase;
dwCodeSize = g_stcShellData.dwCodeSize;
dwCRC32 = CalcuCRC( (UCHAR*) (g_dwImageBase + dwCodeBase), dwCodeSize);
//如果有重定位修复,则不进行验证,因为修复全局变量会改变代码段,所以CRC32的计算会出错
if(g_stcShellData.stcPERelocDir.VirtualAddress == 0)
// if(g_stcShellData.stcIATDir.VirtualAddress == 0)
if (dwCRC32 != g_stcShellData.dwCodeMemCRC32) return FALSE;
return TRUE;
}
3.反Dump
对程序文件的数据进行CRC32的计算,计算的值保存到PE标识的前4个byte中。
VOID COperationPE::CalAndSaveCRC(DWORD dwFileSize)
{
DWORD dwCrc32; //计算的值
//1. 生成CRC32表格
if(m_bCRC32Table == FALSE)
MakeCRC32Table();
//2. 计算PE头之后的数据
dwCrc32 = CalcuCRC((UCHAR*)(m_pDosHeader->e_lfanew + m_dwFileDataAddr), dwFileSize - m_pDosHeader->e_lfanew);
//3. 将该CRC32值写进PE头标识前4个字节
*(PDWORD)((DWORD)m_pNtHeader - 4) = dwCrc32;
}
VOID COperationPE::CalAndSaveCRC(DWORD dwFileSize)
{
DWORD dwCrc32; //计算的值
//1. 生成CRC32表格
if(m_bCRC32Table == FALSE)
MakeCRC32Table();
//2. 计算PE头之后的数据
dwCrc32 = CalcuCRC((UCHAR*)(m_pDosHeader->e_lfanew + m_dwFileDataAddr), dwFileSize - m_pDosHeader->e_lfanew);
//3. 将该CRC32值写进PE头标识前4个字节
*(PDWORD)((DWORD)m_pNtHeader - 4) = dwCrc32;
}
2.内存校验
对未压缩的和加密的代码段进行CRC32的计算,如果存在重定位表,则不进行计算,因为重定位修复时会修正全局变量的绝对地址,导致前后计算的数值不一致,从而校验失败。
BOOL CodeMemVerification()
{
DWORD dwCodeBase;
DWORD dwCodeSize;
DWORD dwCRC32;
dwCodeBase = g_stcShellData.dwCodeBase;
dwCodeSize = g_stcShellData.dwCodeSize;
dwCRC32 = CalcuCRC( (UCHAR*) (g_dwImageBase + dwCodeBase), dwCodeSize);
//如果有重定位修复,则不进行验证,因为修复全局变量会改变代码段,所以CRC32的计算会出错
if(g_stcShellData.stcPERelocDir.VirtualAddress == 0)
// if(g_stcShellData.stcIATDir.VirtualAddress == 0)
if (dwCRC32 != g_stcShellData.dwCodeMemCRC32) return FALSE;
return TRUE;
}
BOOL CodeMemVerification()
{
DWORD dwCodeBase;
DWORD dwCodeSize;
DWORD dwCRC32;
dwCodeBase = g_stcShellData.dwCodeBase;
dwCodeSize = g_stcShellData.dwCodeSize;
dwCRC32 = CalcuCRC( (UCHAR*) (g_dwImageBase + dwCodeBase), dwCodeSize);
//如果有重定位修复,则不进行验证,因为修复全局变量会改变代码段,所以CRC32的计算会出错
if(g_stcShellData.stcPERelocDir.VirtualAddress == 0)
// if(g_stcShellData.stcIATDir.VirtualAddress == 0)
if (dwCRC32 != g_stcShellData.dwCodeMemCRC32) return FALSE;
return TRUE;
}
这是一个非常常见的保护手段,通过修改PEB结构来使某些工具无法获取到加壳程序的进程信息。特别是使用了某些直接获取PEB结构信息的Windows API的工具。借鉴网上通用的某些代码,但是发现PEB中有些信息的删除会导致程序异常,故而将其精简后得到最终代码,效果如下图。
可见原来进程的信息被替换了ntdll.dll,而且进程模块的基本信息(基址和大小)都被抹去。
4.混淆函数
可见原来进程的信息被替换了ntdll.dll,而且进程模块的基本信息(基址和大小)都被抹去。
相当于一个花指令黑盒,把运行地址作为参数传入该函数,最后在花指令结束后便跳转回目标代码处执行。
5.反OD
原理很简单,由于用OD载入程序时,内存中会产生某些特征字符串,故而可以通过搜索这些信息来进行判读自身是否被OD载入调试。
由于在x86程序的虚拟内存中,有2G只分配给用户空间的,所以只需要对该区域进行扫描,经过测试,算法的时间复杂度对壳的性能影响相当大,所以选择优秀的算法非常重要。在本系统中,选择KMP做为字符串匹配算法。
6.反调试
简单的方法有很多,可以通过调用Windows API来查看,也可以自己去PEB结构里面查,本系统采用后者,这种保护手段早已经过时,在这里只是做为一个壳保护功能的完善罢了。
这里直接参考SoftSnoop的代码,并且对其的通信模式与hook方法进行修改。本系统直接使用detour库进行inline hook,这个库的健壮性不是盖的,毕竟是微软自己人做的东西。其次,除了商用软件,发现目前开源的抑或网友制作的工具,稳定性都不太好,故而就萌生一份将其优化与增加适用性的想法。最后,这个模块主要是为代码乱序提供一个API调用频率的信息。
首先给出该模块与SoftSnoop区别的细节:
1)通信模式改变,由于本系统是用Qt去事先界面,所以监控dll与UI之间的通信不能在使用原来的消息机制,而是改成了命名管道。
2)hook方法更变,原来是对某个模块所有的api进行hook,后来测试发现,有些api不可以hook,原因是Windows中有些涉及到窗口的API是回调函数,如果对这些API进行hook,那也会导致一个不可想象的调用膨胀,以至于崩溃。所以,本系统改变策略,只对提前约定好的API进行hook并解析,这样既可以获取关键的信息,也可以提高程序性能。
4.2.1 流程与原理
下面给出遍历进程模块的一个流程。
接下来给出命名管道的通信模式。指令的功能包括,遍历某个模块的api名称,其次还可以对某个规定好的api进行hook。
接下来就是api监控结束后,除了会生成一份简陋的txt格式的日志文件,如下图。
还会生成一个给代码乱序引擎使用的一个日志。日志包含约定记录的api与其调用次数,而它的存储格式如下。
用二进制工具查看。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2019-3-31 21:10
被KitTraumen编辑
,原因: