Native程序保护最佳实践
#
Native程序介绍Native程序,这里是指操作系统原生的二进制格式的程序,如Windows PE格式,Linux/Android下的ELF格式以及 macOS/iOS下的MachO格式。
Native程序常见扩展名
操作系统(格式) | 主程序 | 动态库/共享库 | 驱动程序 |
---|---|---|---|
Windows (PE) | .exe | .dll | .sys |
Linux/Android (ELF) | / | .so | .ko |
macOS/iOS (MachO) | / | .dylib | / |
Harmony(ELF) | / | .so | / |
Native 程序一般由 C/C++/Objective-C/Swift/Delphi/Go 等语言开发,具有资源消耗小,运行效率高等特点,运行在 PC、服务器、移动端、IoT等设备上。
#
Native程序的安全性问题虽然经过编译生成的Native程序一般不会存在原始的函数名、变量名,逆向分析难度较高,但由于相应的反编译工具也成熟强大,依然可以反编译为类C伪代码,再加上特征库定位、交叉引用分析等高级功能,仍然可以将代码高度还原,对于高安全性需要的场景,不经过保护很容易被逆向破解。
测试程序举例(Android ARM架构):
源代码
反编译效果
虽然函数中变量名等信息丢失,通过符号表(ELF)、导入/导出函数、字符串等信息的辅助分析,函数依然可以反编译为易读的代码。
#
功能介绍Virbox Protector 对Native程序支持整体保护和函数级的保护,可以起到防逆向、防调试、防篡改以及运行时环境检测等功能。
#
基础保护基础保护,是对程序整体的保护,指通过一系列技术和策略来确保软件应用的安全性、完整性和可用性,对程序的性能影响小,操作简单,可以达到防反编译、防篡改、防调试等效果。
#
压缩压缩,是将程序中的代码、数据以及一些格式相关的程序数据(如导入表、重定位表、部分资源信息)进行打包、压缩和加密,再将程序入口替换为壳代码,运行时由壳代码将代码和数据解密并还原,并修复一些必须的程序数据,再跳转到原始的程序入口(OEP) 执行。
压缩加密了整个代码段和数据段(ELF程序不会加密可写数据段),可以防止程序被静态反汇编、反编译,一定程度上起到防止文件补丁的效果。
#
内存校验内存校验,可以在程序加载时校验自身完整性,如果发现程序被篡改,则会退出进程。
在加壳保护时,Virbox Protector会抽取整个程序中可校验的内存块并拆分,⽣成校验表,程序运⾏时在程序入⼝,壳代码会对每个要校验的内存块进⾏校验,以验证其完整性,如果校验失败,则会清场退出(内存校验表和校验逻辑本⾝经过了⾃保护,以保其安全性)。
如果需要在运行时动态进行校验,可以使用SDK标签
在代码中调用,则每次调⽤标签VBProtectVerifyImage时都会进⾏校验。
#
内存保护(ELF)程序启动后,防止被调试器附加调试,防止程序被Dump内存。
#
导入表保护(PE&ELF)导入表描述了程序的依赖库、依赖函数信息和导入地址,是模块间调用的边界,很容易暴露代码逻辑。
经过导入表保护后,原始的导入表被清除,模块间调用替换为壳的修复代码。
导入表保护可以去除程序中的导入函数和变量等敏感信息,防止反编译工具交叉引用分析模块边界。并将原始的 IAT(PE)或GOT表(ELF)中的地址替换为壳的修复函数,运行时不会还原,防止运行时被直接修复脱壳。
#
资源节加密(PE)Windows PE程序可以嵌入资源,一般存放界面布局、多语言字符串、图标等,开发者可能会将一些敏感数据以资源的形式嵌入到程序中,资源位于程序的资源节中(一般为 .rsrc节),很容易被提取和篡改。
资源节加密,可以将整个资源节加密,只保留图标和版本号等必要的信息,可以防止资源的提取和篡改。
#
附加数据加密(PE)附加数据是“拼接”到程序结尾的数据,某些音视频、PPT播放器等程序,会将资源、数据库等以附加数据的形式拼接到程序结尾。
附加数据加密,可以将附加数据加密,防止重要资源被提取。
#
移除调试信息(ELF)ELF程序中有时会包含Debug节,静态符号表,其中包含了函数名、函数地址等信息,对外发布时如果携带会降低程序的安全性。
移除调试信息,会将ELF程序中的 .debug 节,静态符号表移除。
#
调试器检测调试是逆向分析时的重要手段,可以在庞大的二进制指令中迅速定位到相关的逻辑。
调测调试器,可以检测当前模块的进程是否被 x64dbg/OllyDbg/IDA Pro/Windbg 等工具调试,被调试则退出阻止运行。
#
虚拟机检测(PE)检测程序是否在 VMWare, Virtual Box 等虚拟机中运行,检测到则退出阻止运行。
#
函数级保护函数保护以函数为粒度,提供了精细化,安全性高的针对性保护,适用于加密方案、客户端/服务端认证,通信加密等高安全性要求的场景。
Virbox Protector 对 Native程序的函数级保护,支持代码加密、代码混淆、代码虚拟化三种保护方式。其中性能损耗:
代码虚拟化 > 代码混淆 > 代码加密
安全性:
代码虚拟化 > 代码混淆 > 代码加密
#
代码加密代码加密是通过自修改代码(SMC)的方式,在函数被调用时解密自身,再跳转到正确的函数指令中执行。
代码加密后在运行时执行还是原始指令,因此几乎无性能影响,可以防脱壳,防止静态反编译。
保护前:
保护后:
#
代码混淆代码混淆是将函数中原始的指令,通过等价变换、立即数加密、间接跳转、虚假分支、花指令加扰、指令切片等手段,将原始指令转换为难以阅读的随机的指令片段。
功能特性
- 防止函数被反编译为可阅读函数,提升逆向分析的难度。
- 运行时也不会还原,干扰静态分析和动态调试分析。
- 函数体被拆分为无数个随机分布的指令块,破坏函数边界。
- 对内存访问和跳转指令进行转换,破坏反编译工具的交叉引用分析。
- 使函数没有指令特征,防止被特征定位。
- 混淆的指令中包含暗桩,可检测调试器Run Trace追踪。
保护前:
保护后:
#
代码虚拟化代码虚拟化,是保护过程中将函数中原始的汇编指令,转换为自定义的虚拟指令,运行时在自定义的虚拟机中执行,模拟了汇编指令中的内存访问、条件判断、寄存器状态等。
功能特性
- 虚拟指令每次保护时都是随机生成,需要逆向虚拟机代码才能理解虚拟指令对应的真实操作。
- 针对算法类的代码保护效果更好(函数调用较少的代码)。
- 虚拟机代码经过再度混淆,加强了自身安全性。
保护前:
保护后:
虚拟化后会在程序中插入解释器的汇编指令,还有虚拟机入口指令(用于加载虚拟指令,设置状态等)等,这些指令还会再度进行混淆,上图中只有部分入口跳转指令。
#
SDK标签Virbox Protector 还支持SDK标签的方式对程序保护,可以精细地控制要保护的函数或代码片段,也可以用于加密数据或字符串以及校验内存完整性。SDK标签支持C/C++/Objective-C/Swift等语言的调用集成,可以在安装目录 <install_dir>/example/sdk
目录下查看使用示例。
Virbox Protector 支持静态库标签和动态库标签。
静态库标签
静态库SDK位于 <install_dir>/example/sdk/lib
下,目录结构为:
├─a32├─a64├─fat├─framework│ ├─virbox_iOS.framework│ │ ├─Headers│ │ └─Modules│ └─virbox_macOS.framework│ ├─Headers│ ├─Modules│ └─Resources├─x64└─x86
x86: x86静态库x64: x64静态库a32: arm32静态库a64: arm64静态库fat: macOS/iOS fat格式静态库framework: macOS/iOS fat格式 framework
静态库以 virbox_\<sysabi> 命名。
virbox_windows.lib # Windows静态库virbox_wdk.lib # Windows驱动程序静态库libvirbox_android.a # Android静态库...
动态库标签
SDK标签会使程序依赖virbox32或virbox64动态库,程序被保护后会清除清除该依赖。
动态库SDK位于 <install_dir>/example/sdk
下,以系统名命名,如 Windows 动态库位于 <install_dir>/example/sdk/windows
下。
SDK标签技术原理
SDK标签本身并不包含加解密或者保护的功能,而是以特征的方式标记代码位置。使用 Virbox Protector 对程序保护时才真正对标记的代码保护、对标记的字符串等数据加密。
SDK标签支持的功能
- 常量数据加密(包括字符串、密钥等敏感数据)
- 代码混淆
- 代码虚拟化
- 安全退出
- 内存校验
- 调试器检测
- 虚拟机检测
#
函数标签函数标签可以标记整个函数,在保护时可以保护整个函数,包括编译生成的函数入口指令。
VBMutateFunction
混淆当前函数
VBVirtualizeFunction
虚拟化当前函数
代码示例:
int add(int x, int y){ int ret = 0; VBMutateFunction("add#"); ret = x + y; printf("x+y=%d\n", ret); return ret;}
int sub(int x, int y){ int ret = 0; VBVirtualizeFunction("sub#"); ret = x - y; printf("x-y=%d\n", ret); return ret;}
#
代码块标签代码块标签即 Begin&End
标签,可以标记函数中的代码片段,在保护时仅保护Begin和End之间的代码。
建议尽可能使用函数标签,由于编译器内联优化、分支优化等原因,代码块标签的稳定性不如函数标签,且函数标签可以保护函数头,安全性更高。
Begin标签
VBProtectBegin
代码虚拟化开始
VBVirtualizeBegin
代码虚拟化结束(同VBProtectBegin
)
VBMutateBegin
代码混淆开始
VBSnippetBegin
代码碎片化开始,仅支持 Virbox Protector LM (Pro)版。
End标签
VBProtectEnd
代码保护结束,与Begin标签配对使用。
代码示例:
int foo(int x){ if (x == 0) { VBVirtualizeBegin("foo_1#"); // do something... VBProtectEnd(); } else { VBMutateBegin("foo_2"); // do something... VBProtectEnd(); } return 0;}
#
高级数据加密(LM)VBProtectDecrypt
VBProtectDecrypt标签仅供 Virbox Protector LM(许可)版本使用。
VBProtectDecrypt 使用高安全性算法(AES256)加密数据,数据长度必须为16的整数倍。
void *VB_API_CALL VBProtectDecrypt(void *dst, const void *src, int size);
示例:
static const char g_my_key[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 };
int test_decrypt(){ char buf[sizeof(g_mey_key)]; VBProtectDecrypt(buf, g_my_key, sizeof(g_my_key)); // do something... return 0;}
#
字符串和数据加密如果程序中包含了涉及安全性的问题的敏感字符串或数据(如密钥,证书等),很容易被扫出来通过反编译工具交叉引用分析直接定位到关联的代码,因此建议对敏感数据进行加密,可以使用 SDK标签 标记要加密的数据或字符串,标记后可以被 Virbox Protector
保护时识别,在每次保护时会随机生成密钥加密,保证每次保护后的密文不同。
VBDecryptData
void* VB_API_CALL VBDecryptData(const void *data, int size);
VBDecryptData可以加密任意长度的常量数据。
VBDecryptStringA
VBDecryptStringA 是对VBDecryptData的封装,用于加密字符串,尽可能的避免编译链接的优化导致的异常问题。
VBDecryptStringW
VBDecryptStringW 一般用于加密Windows下宽字符串。
VBFreeData
VBFreeData用于释放以上三个接口返回的数据或字符串。可以防止内存搜索工具搜索到该数据。
为了方便开发者调用,Virbox Protector 另外提供了无需释放的数据解密接口,仅在第一次使用字符串时解密到堆内存,在模块卸载时自动释放。
VBDecryptDataOnce
同 VBDecryptData,无需释放。
VBDecryptStringOnceA
同 VBDecryptStringA,无需释放。
VBDecryptStringOnceW
同 VBDecryptStringW,无需释放。
代码示例:
int test_encrypt_key(){ // 加密静态常量 static const unsigned char s_key[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 }; unsigned char* key = VBDecryptData(s_key, sizeof(s_key)); for (int i = 0; i != sizeof(s_key); ++i) { printf("%02x ", key[i]); } printf("\n"); // 释放内存 VBFreeData(key); // 加密 hello字符串 printf(VBDecryptStringOnceA("hello")); return 0;}
注意
- 由于编译器编译时优化可能将重复字符串给优化掉了,所以代码中使用字符串和数据加密的标签时,有可能会导致程序运行出现乱码问题;
- XXXXXFunction 函数不会受到编译器优化的影响;
- 函数中Begin 和 End 标签可能会受到编译器优化的影响,比如可能内联嵌套导致出现一些未知的问题;
#
环境检测与安全退出#
安全退出VBSafeExit
如果程序发现一些恶意行为,调用ExitProcess
、 exit
、kill
、abort
等函数容易被破解者追踪到调用位置并绕过。VBSafeExit标签可动销毁调用位置的栈帧,常用寄存器、返回地址等信息,让破解者难以追踪到调用位置。
#
内存校验Virbox Protector 加密选项中的内存校验,仅在程序启动时校验一次,如果破解者通过动态内存补丁的方式破解,可能会绕过检测,开发者可以通过 SDK 标签调用 VBProtectVerifyImage
(自动安全退出)或 VBVerifyImage
(不退出,返回校验结果)进行校验,将校验时机与业务代码混合,提升校验的安全性。
VBProtectVerifyImage
VBProtectVerifyImage
可以校验当前模块的内存完整性,检查程序是否被篡改(包括静态补丁和动态内存补丁),如果发现篡改则自动安全退出。
VBVerfiyImage
VBVerifyImage
可以校验当前模块的内存完整性,检查程序是否被篡改(包括静态补丁和动态内存补丁),如果发现篡改则返回非0值。
#
调试器检测VBDetectDebugger
int VB_API_CALL VBDetectDebugger(void);
检测程序是否正在被调试器进行调试,发现则返回非0值。
#
虚拟机检测VBDetectVirtualMachine
int VB_API_CALL VBDetectVirtualMachine(void);
检测程序是否正在VMware, Virtual Box等虚拟机中运行,发现则返回非0值。(仅支持Windows)
#
使用示例int check(){ // 使用了检测的函数要加上混淆或虚拟化标签才更安全 VBMutateFunction("check#"); int code = 0; if (VBVerifyImage() != 0) { // 发现内存被篡改 code = 1; } if (VBDetectDebugger() != 0) { // 发现调试器 code |= 2; } if (VBDetectVirtualMachine() != 0) { // 发现在虚拟机中运行 code |= 4; } if (code != 0) { // 清场退出 VBSafeEixt(0xdead0000 | code); } return 0;}
#
插件#
DSProtector(Windows&Linux)DSProtector 插件可以用于加密程序使用到的只读文件
,如脚本语言、音视频、图片、只读配置文件、数据库等,防止敏感文件被直接窃取。
#
支持范围#
操作系统与CPU架构支持操作系统 | x86 | x64 | arm32 | arm64 |
---|---|---|---|---|
Windows | ✔️ | ✔️ | ✖️ | ✖️ |
Linux | ✔️ | ✔️ | ✔️ | ✔️ |
macOS | ✖️ | ✔️ | ✖️ | ✔️ |
Android | ✔️ | ✔️ | ✔️ | ✔️ |
iOS | ✖️ | ✖️ | ✖️ | ✔️ |
#
功能与操作系统支持保护选项 | Windows | Linux | macOS | Android | iOS |
---|---|---|---|---|---|
压缩 | ✔️ | ✔️ | ✖️ | ✔️ | ✖️ |
内存校验 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
内存保护 | ✖️ | ✔️ | ✖️ | ✔️ | ✖️ |
导入表保护 | ✔️ | ✔️ | ✖️ | ✔️ | ✖️ |
附加数据加密 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
调试器检测 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
虚拟机检测 | ✔️ | ✖️ | ✖️ | ✖️ | ✖️ |
移除调试信息 | ✖️ | ✔️ | ✖️ | ✔️ | ✖️ |
[E] 代码加密 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
[M] 代码混淆 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
[V] 代码虚拟化 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
#
自动化集成#
命令行工具Virbox Protector
支持命令行选项,可以指定基础保护选项和函数选项,方便自动化集成。
命令行工具 virboxprotector_con
的默认路径位于:
Windows:C:\Program Files\senseshield\Virbox Protector 3\bin
Linux:/usr/share/virboxprotector/bin
macOS:/Applications/Virbox Protector 3.app/Contents/MacOS/bin
#
使用配置文件使用Virbox Protector工具界面进行保护,在被保护的程序所在目录会生成 .ssp
配置文件,然后调用virboxprotector_con
:
virboxprotector_con <input_file> -o <output_file>
virboxprotector_con
会自动查找 <input_file>.ssp 作为配置文件开始保护。
#
保护选项PE
选项 | 命令行 | 默认选项 |
---|---|---|
压缩 | --pack= | 1 |
内存校验 | --mem-check= | 0 |
导入表保护 | --imp-protect= | 0 |
资源节加密 | --res-sect-enc= | 1 |
附加数据加密 | --overlay-enc= | 1 |
调试器检测 | --detect-dbg= | 0 |
虚拟机检测 | --detect-vm= | 0 |
移除强名称签名 | --remove-sns= | 1 |
ELF
选项 | 命令行 | 默认选项 |
---|---|---|
压缩 | --pack= | 1 |
内存校验 | --mem-check= | 0 |
导入表保护 | --imp-protect= | 0 |
调试器检测 | --detect-dbg= | 0 |
剥离符号表 | --strip-dbginfo= | 1 |
MachO
选项 | 命令行 | 默认选项 |
---|---|---|
内存校验 | --mem-check= | 1 |
调试器检测 | --detect-dbg= | 0 |
#
函数选项选项 | 命令行 |
---|---|
忽略不支持的函数 | --ignore-unsupported=<value> |
代码加密 | -e |
代码混淆 | -m |
代码虚拟化 | -v |
支持指定函数名称或规则保护,使用 ;
号隔开,支持通配符 *
,举例:
-m "function1;function2" -v "function3;function4" -e "test*" --ignore-unsupported=1
#
举例对Windows主程序保护:
virboxprotector_con test.exe --pack=1 --imp-protect=1 --mem-check=1 --res-sect-enc=1 --detect-dbg=1 -o protected/test.exe
对Linux程序保护:
virboxprotector_con libhello.so --pack=1 --mem-check=1 --detect-dbg=0 -o protected/libhello.so
对Linux程序保护(同时生成调试版和发布版):
#生成保留符号的版本用于故障分析virboxprotector_con libhello.so --pack=1 --mem-check=1 --strip-dbginfo=0 -o protected/libhello_with_sym.so
#去掉符号表对外发布virboxprotector_con -strip protected/libhello_with_sym.so -o protected/libhello.so
对macOS程序保护:
virboxprotector_con libtest.dylib --detect-dbg=1 --mem-check=1 -e "*" -o protected/libtest.dylib
#
保护指引#
普通的保护方式如果对安全性没有较高的要求,一般不需要函数级别的保护,只需要调整基础保护选项即可实现防反编译和防内存Dump脱壳,无需复杂的配置选项,直接用命令行工具即可完成保护。
保护选项 | 保护建议 | 保护效果 |
---|---|---|
压缩 | 勾选 | 压缩可以加密整个代码段,隐藏一些原程序的结构信息(导入表、重定位等),防止直接反编译。 |
内存校验 | 勾选 | 防止程序被修改。 |
导入表保护 | 勾选 | 加密导入表,防止脱壳,防止查看API引用。 |
资源节加密 | 勾选 | 加密PE资源节(.rsrc),防止资源信息被提取。 |
附加数据加密 | 有附加数据则勾选 | 部分打包工具会生成附加数据,此功能会将程序结尾的附加数据加密,防止提取。 |
调试器检测 | 每个进程只需要保护一个模块(如主程序exe); 如果模块做为SDK发布供第三方调用,则不勾选 | 检测调试器,发现程序被调试则退出。 |
去除调试信息 | 建议勾选; 如果要保留调试信息,请保护完毕调用命令行 -strip 后将调试信息移除再对外发布 | 去除 .debug 节,去除静态符号表。 |
[E] 代码加密 | 使用默认选项(仅加密入口函数) | 加密指定的函数 |
[M] 代码混淆 | 无需选择 | 混淆函数的指令 |
[V] 代码虚拟化 | 无需选择 | 虚拟化函数的指令 |
#
高安全性保护如果对安全性有较高要求,需要使用代码混淆
和代码虚拟化
对关键函数进行保护,比如授权验证,加密算法及流程,协议封装流程等重要逻辑。
#
保护前准备- 准备静态分析(反汇编/反编译)工具,用于保护效果的查看和评估。
- 原程序对应的符号文件:Windows 平台的 .pdb/.map文件,Linux 平台需要保留符号表(编译选项加上
-g
即可),如果使用SDK标签的方式则无需符号。
静态分析工具推荐:
IDA Pro(收费)
Ghidra(免费开源):Releases · NationalSecurityAgency/ghidra (github.com)
#
加密方案和安全性建议对于Native程序,安全性可以简单用以下公式来表述:
安全性 = 加密方案 x 壳的强度
要防高手逆向,需要逼迫对方分析虚拟机才能提高破解成本。在选用一款强壳的前提下,加强加密方案才是提升安全性的核心。
加密方案
简单可以理解为cracker完成破解,所需要逆向或者篡改的代码量。
壳的强度
对代码的保护效果,重点在混淆
和虚拟化
的强度。如果保护后容易被分析,或者能被轻易地反混淆甚至反虚拟化,说明壳的强度不足。
安全意识和注意事项
由于每个程序都有不同的功能和安全性需求,例如:
- 保护通讯协议防止机器人外挂造成业务入侵
- 保护授权验证防止被cracker 破解损害开发者权益
- 保护核心算法防止被竞品抄袭
- 保护加密解流程防止核心数据文件被解密
作为保护工具无法识别出哪些函数需要保护,只能提供一些注意事项作为参考:
去掉所有不需要导出的符号,别被 cracker 看到函数名就知道函数的意义。
扫描敏感的字符串引用,防止被cracker直接搜索字符串定位到核心代码(可以使用 IDA,Shift + F12查看)。
对于运行时库,如 C/C++运行时库,尽可能静态依赖,对于安全敏感的部分代码,可以使用自己实现的函数,例如 memcpy, strcpy, strcmp 等,防止被 cracker 追踪到敏感代码(标准库中的函数容易被特征定位)。
字符串虽然可以用SDK加密,可以防内存搜索,但在字符串被使用的时候,需要传递别给的函数或模块,这个函数间的边界如果被定位到,是可以调试出明文的,所以能不用就不用。
虚拟化可以让一个函数变为黑盒,但函数之间的边界问题无法解决。举例:
bool verify() { // do something}
这个函数可以虚拟化让人无法明白内部逻辑,但如果被猜到它的功能,可以用一两条汇编将其 return true。不需要分析该函数的逻辑,这种情况加不加虚拟化影响不大。
安全验证逻辑,最好与业务功能融合在一起,不能被cracker直接把安全代码抹掉程序还能正常运行,建议将安全代码 inline 到业务代码。(在使用SDK标签的时候也要注意这点)
标准密码学算法,是最容易被特征定位的代码,尽可能使用魔改算法或去掉特征,性能要求不高可以再加上混淆。
注:要以 cracker 的角度去思考破解点在哪里。如果程序已经被破解,可以使用一些二进制对比工具查看patch 点在哪里,方案漏洞在哪里,然后进行修补。
#
去除导出函数gcc/clang 编译生成的动态库,函数/变量是默认导出的,这会极大地降低程序安全性,可以修改编译选项去除不需要导出的函数。C/C++程序配置导出函数的方法如下:
#
Linux&Android去除导出函数CMakeLists.txt配置:
set_target_properties(${MyTarget} PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/symver.txt")
在CMakeLists.txt旁边创建symver.txt文件,内容示例如下:
{global:JNI_OnLoad;MyFunction1;MyFunction2;local:*;};
以上配置可以使用so库仅导出 JNI_OnLoad, MyFunction1, MyFunction2 三个函数,可以根据需要设置自己的导出函数。
#
macOS&iOS去除导出函数CMakeLists.txt配置:
set_target_properties(${MyTarget} PROPERTIES LINK_FLAGS "-Wl,-exported_symbols_list ${CMAKE_CURRENT_SOURCE_DIR}/export.txt")
export.txt示例:
_JNI_OnLoad_MyFunction1_MyFunction2
#
GO语言Linux&Android程序去除导出函数go build 编译选项:
go build -ldflags "-extldflags=-Wl,--version-script=symver.txt"
symver.txt文件,内容如下:
{global:empty;local:*;};
#
基础保护设置压缩
压缩可以防静态反编译,在保护方案调整完成前,建议不要勾选,方便评估保护效果。
注:勾选压缩会稍微降低内存校验的安全性。
内存校验
建议勾选,或者使用SDK标签进行内存校验。
导入表保护
建议勾选,防止脱壳,防止分析工具查看模块间函数调用。
调试器检测
如果程序是供三方调用的动态库,不要勾选。每个进程只需一个模块勾选。
保护选项 | 效果 |
---|---|
压缩 | 压缩可以加密整个代码段,隐藏一些原程序的结构信息(导入表、重定位等),防止直接反编译。 |
内存校验 | 防止程序被修改。 |
导入表保护 | 加密导入表,防止脱壳,防止查看API引用。 |
资源节加密 | 加密PE资源节(.rsrc),防止资源信息被提取。 |
附加数据加密 | 部分打包工具会生成附加数据,此功能会将程序结尾的附加数据加密,防止提取。 |
调试器检测 | 检测调试器,发现程序被调试则退出。 |
去除调试信息 | 去除 .debug 节,去除静态符号表。 |
[E] 代码加密 | 加密指定的函数 |
[M] 代码混淆 | 混淆函数的指令 |
[V] 代码虚拟化 | 虚拟化函数的指令 |
#
函数级保护对于高安全性需求的场景,需要用到代码混淆
和代码虚拟化
,这里着重介绍代码混淆
和代码虚拟化
的使用场景。
对于破解者来说,最核心的步骤是定位代码
,直接逆向关键代码逻辑
或修改代码
完成破解。例如对于协议分析,一般来说只需要逆向其中的协议、加解密或签名算法,不需要对程序修改即可达到目的(用于直接制作机器人或写脚本模拟封包)。对于功能的破解,往往需要对原程序的验证逻辑打补丁进行篡改,或者对认证数据进行填充。
代码混淆
用于模糊函数边界,拆散函数的分布(对函数切片并随机分配),对一些指令做等价变换,加密指令中的立即数,增加随机花指令改变指令原有的特征,还可以使得静态分析工具的交叉引用功能失效。简而言之,就是可以起到“一眼望去不知道这个函数是干什么的”,还可以让分析工具“无法直接搜索特征和交叉引用”,以及”让分析工具的反编译功能失效“,往往需要破解者动态跟踪汇编指令去理解函数。
交叉引用:是指静态分析工具直接搜索函数和数据(包括全局变量、字符串等)之间的引用关系,快速定位相关的代码。
函数特征:通过常用库(包括运行时库、密码学库)里包含的特定指令序列或立即数,可以直接定位函数。
代码混淆对性能的有一定的影响,常用于保护:
- 核心代码及其调用关系的上下游函数。
- 密码学库函数的入口和特征位置(如AES SBox、ECC曲线特征)。
- 协议的封装流程。
- 重要的函数边界。
代码虚拟化
代码虚拟化,是将函数原本的汇编指令,转换自定义的虚拟机指令。函数被虚拟化保护后,可以将其看成一个黑盒,代码的核心逻辑是由对应的虚拟机解释执行的,安全性极高。如果一个加密方案可以逼迫破解者去分析虚拟化的代码,说明这个方案是非常成功的,需要分析的代码(破解点)越多,破解的难度就越大
。
代码虚拟化对程序的性能影响较大,不可大面积使用,常用于以下几类函数:
- 对安全性要求较高的一些自定义算法。
- 可能被篡改的函数(如关键的验证逻辑)。
#
使用SDK标签使用SDK标签可以标记函数进行混淆或虚拟化保护,也可以解决以下安全性问题:
防止敏感字符串被定位
可以使用 VBDecryptStringA 标签加密字符串,使用 VBDecryptData 加密数据。
防止被注入或跨进程打内存补丁
可以在代码中使用 VBProtectVerifyImage
或 VBVerifyImage
校验内存完整性,防止代码被篡改。
注:对使用VBDecryptStringA 和 VBProtectVerifyImage 标签的函数再加上混淆或虚拟化更安全。
#
对外发布注意事项- 对外发布的 Windows 系统的程序,不可携带符号文件(.pdb/.map)
- 对于Linux/Android程序,需要去除符号表(请务必勾选
移除调试信息
) - 确认打包后的程序不依赖virbox32/virbox64动态库(已被保护)
#
常见问题#
不支持的函数Native程序中的函数是通过反汇编以及引用分析解析出来的,由于编译器优化、代码复杂度、保护技术等原因,有些函数是无法被保护的,这类函数在保护过程中日志窗口会有提示,这里列出一些常见的不支持的函数和原因。
函数太小
原因:
- 函数过于简单,指令字节数太少,无法容纳基本的跳转指令(具体大小要求不同CPU架构会有不同)。
解决办法:
- 将函数声明为内联函数
- 使用SDK标签
未分配的栈
原因:
- 函数简单,没有调用任何函数,编译器优化后的指令直接使用未分配栈空间(常见于GCC和Clang),由于混淆和虚拟化都是基于栈的,保护后会导致数据访问错误。
解决办法:
- 使用SDK标签,防止编译器优化
- 加入编译选项
-mno-red-zone
结构化异常处理函数
原因:
- 由于Windows异常处理的技术细节,代码在混淆和虚拟化后会导致
栈展开
过程出现错误,仅PE程序有此问题。
解决办法:
- 不使用异常处理
- 不保护
分析失败
原因:
- 在反汇编分析过程时逻辑出现错误,未正确分析函数的基本块,非常复杂的代码可能会出现
解决办法:
- 提交工单和问题样本,由产品开发人员解决
#
运行崩溃原因:
- 函数分析不正确,未正确识别外部调用。
解决办法:
- 请勿选择没有函数名,或不知道函数作用的函数进行保护。
- 提交工单和问题样本,由产品开发人员解决。
#
没有函数名原因:
- 没有pdb文件(PE)
- 程序经过strip,或使用 Release编译(ELF)
解决办法:
- 将pdb文件放在被保护程序同一目录下 (PE)
- 编译选项加入
-g
(ELF) - 使用SDK标签
#
杀毒软件误报原因:
- 程序被加密后,无法扫描特征,一般病毒和木马也借此逃避杀毒软件,因此杀毒软件常将加密后的程序直接定义为可疑文件。(一般Windows下的程序容易出现)
解决办法:
- 使用数字签名,可以大幅减轻杀毒软件误报
- 在杀软平台提交审核
#
其它问题产品BUG反馈可以在 Virbox Protector工单系统 提交,也可直接联系客服人员。