Skip to main content

iOS应用保护最佳实践

iOS程序介绍#

iOS程序是运行在苹果公司开发的iOS操作系统上的应用程序,iOS应用的可执行程序是MachO文件格式。

温馨提示
  • 为了方便对应用进行签名,建议在 macOS系统上对 iOS程序加密

安全性问题#

虽然从AppStore下载安装的iOS App都是苹果加壳后的,但是苹果对程序内的可执行文件只进行了简单的加密处理,随着市面上的脱壳工具(比如Clutch等)逐渐成熟,可以通过解密算法对iOS app进行脱壳处理,进而使用class-dump工具导出App的MachO文件所有头文件、Hopper或ida等工具分析App的MachO文件代码,并将代码高度还原,对于高安全性需要的场景,不经过保护很容易被逆向破解。

功能介绍#

Virbox Protector工具(简称加壳工具)通过对iOS app或ipa程序进行加固,支持函数级保护和基础保护,可以有效的防逆向、防调试、防篡改以及防二次打包等操作。

基础功能#

内存校验#

内存校验,可以在程序加载时校验自身完整性,如果发现程序被篡改,则会退出进程。

如果需要在运行时动态进行校验,可以使用SDK标签在代码中调用。

OC名称混淆#

OC名称混淆,可以将类名混淆成无意义的字符串名称,可以无法直观的找到类名的调用关系。

注意:1)该功能目前只能混淆类名,不能混淆方法名,调用到其他资源的类名也不会进行混淆。2)若程序代码里涉及到跨模块反射调用这种情况,则OC名称混淆可能会导致程序界面功能无法正常使用的情况。

原程序的类名反编译,如图所1示:

OC名称混淆后类名反编译,如图所示:

调试器检测#

调试是逆向分析时的重要手段,可以在庞大的二进制指令中迅速定位到相关的逻辑。

调测调试器,可以检测当前模块的进程是否被 IDA Pro/lldb等工具调试,被调试则退出阻止运行。

移除调试信息#

IPA程序中可执行程序有时会包含Debug节,静态符号表,其中包含了函数名、函数地址等信息,对外发布时如果携带会降低程序的安全性。

移除调试信息,会将程序中的 .debug 节,静态符号表移除。

原程序的符号,如图所示:

勾选移除调试信息后程序的符号,如图所示:

签名校验#

校验ipa中的开发者签名证书(Team id),防止ipa被第三方二次打包重签名。

1)若想使用签名校验功能,则必须要启用签名;2)若启用签名,则签名校验选项可选可不选。

签名设置#

若勾选启用签名,签名证书选择和Xcode编译xcarchive时的证书一致,则程序保护生成的app默认已签名;

若不勾选启用签名,则程序保护生成的app默认不签名,需自行在对加固后的app手动签名(比如使用codesign、iOS App Signer工具等);

函数功能#

代码混淆#

代码混淆是将函数中原始的指令,通过等价变换、立即数加密、间接跳转、虚假分支、花指令加扰、指令切片等手段,将原始指令转换为难以阅读的随机的指令片段。

原程序反编译效果,如图所示:

代码混淆后反编译效果,如图所示:

代码虚拟化#

代码虚拟化,是保护过程中将函数中原始的汇编指令,转换为自定义的虚拟指令,运行时在自定义的虚拟机中执行,模拟了汇编指令中的内存访问、条件判断、寄存器状态等。

代码虚拟化后反编译效果,如图所示:

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标签支持的功能

  • 常量数据加密(包括字符串、密钥等敏感数据)
  • 代码混淆
  • 代码虚拟化
  • 安全退出
  • 内存校验
  • 调试器检测
  • 虚拟机检测

注意:

1.若一段代码两个标签同时先后顺序写,则在前面的标签不会生效,在后面的标签生效。

2.若只写Begin不写end,加壳工具解析时发现未闭合的标签则默认在保护日志中进行提示。

函数标签#

函数标签可以标记整个函数,在保护时可以保护整个函数,包括编译生成的函数入口指令。

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;}

字符串和数据加密#

如果程序中包含了涉及安全性的问题的敏感字符串或数据(如密钥,证书等),很容易被扫出来通过反编译工具交叉引用分析直接定位到关联的代码,因此建议对敏感数据进行加密,可以使用 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;}
注意
  1. 由于编译器编译时优化可能将重复字符串给优化掉了,所以代码中使用字符串和数据加密的标签时,有可能会导致程序运行出现乱码问题;
  2. XXXXXFunction 函数不会受到编译器优化的影响;
  3. 函数中Begin 和 End 标签可能会受到编译器优化的影响,比如可能内联嵌套导致出现一些未知的问题;

环境检测与安全退出#

macOS和iOS的SDK标签功能选项,如下表所示:

接口描述
VBProtectVerifyImage()内存校验可以校验当前模块的内存完整性,失败则自动退出进程
VBVerifyImage()内存校验可以校验当前模块的内存完整性,失败则返回非0值
VBDetectDebugger()检测调试器,发现则返回非0值
VBVerifySignature()签名校验,仅支持iOS app主模块
VBSafeExit检测到非0值就退出程序,销毁当前栈帧

注意:内存校验的实现不能是跨模块的,只校验自己的模块,所以主模块要检测,只能主模块来调SDk,否则内存校验sdk不会生效。

使用示例#

  • 下面简述iOS应用使用OC语言时,通过调用动态库使用sdk标签的操作流程:

1)将virbox.h放到工程中,使用Xcode打开工程后,将virbox.h添加到工程中;

2)选中TARGETS->Build Settings->Architectures,查看项目的架构是arm64还是x64还是Fat格式的;

3)从Virbox Protector工具的安装目录sdk/darwin下找到对应架构的libvirbox64.dylib,选中TARGETS->Build Phases->Link Binary选项,添加libvirbox64.dylib;

4)在代码中引用virbox.h,并添加标签;

代码参考示例,:

#import "virbox.h"-(void)readLoad{    //可以根据自己需求选择其他标签    VBMutateBegin("load_test"); //代码混淆    for (int i=0; i<array.count; i++) {        News * news=[[News alloc]init];        news.content=array[i];        [_newsArray addObject:news];    }    VBProtectEnd();    return NO;}
- (BOOL)isImageSetFolder:(NSString *)folder{    //可以根据自己需求选择其他标签    VBVirtualizeFunction("reset_test"); //代码虚拟化    if ([folder hasSuffix:kSuffixImageSet]        || [folder hasSuffix:kSuffixAppIcon]        || [folder hasSuffix:kSuffixLaunchImage]) {        return YES;    }    return NO;}
  • 下面简述iOS应用使用swift语言时,通过调用动态库使用sdk标签的操作流程:

1)将virbox.h放到工程中,使用Xcode打开工程后,将virbox.h添加到工程中,并将virbox.h设置为桥接文件;

TARGETS->Build Settings->Swift Compiler->Object-C Bridging Header->项目名/virbox.h

2)选中TARGETS->Build Settings->Architectures,查看项目的架构是arm64还是x64还是Fat格式的;

3)从Virbox Protector工具的安装目录sdk/darwin下找到对应架构的libvirbox64.dylib,选中TARGETS->Build Phases->Link Binary选项,添加libvirbox64.dylib;

4)在代码中直接添加标签,且需要给标签添加变量名;

代码参考示例:

struct ContentView: View {    var body: some View {        //可以根据自己需求选择其他标签        var body_begin = VBMutateBegin("body_test")//代码混淆                    let columns = [        GridItem(.flexible()),        GridItem(.flexible()),        GridItem(.flexible()),        GridItem(.flexible())]        var body_end = VBProtectEnd()//代码段结束    }}
struct RdioApp: App {    var body: some Scene {        //可以根据自己需求选择其他标签        var myva = VBVirtualizeFunction("test") //代码虚拟化        HStack(spacing: 24) {                Image(systemName: "backward.fill")                    .font(.title3)                Image(systemName: "play.fill")                    .font(.title)                Image(systemName: "forward.fill")                    .font(.title3)            }    }}

3.程序运行

1)编译后的app直接运行需要依赖libvirbox64.dylib,即将对应app程序架构的libvirbox64.dylib拷贝到/usr/local/lib/libvirbox64.dylib,然后再运行编译后的app,即可正常运行;

如果不拷贝则会报以下错误:

注:若是arm架构和FAT格式的libvirbox64.dylib,需要先对其进行签名后,再进行拷贝。

2)将编译后的app拖入到加壳工具中,函数选项即可显示标记的函数;

对文件进行保护,则保护后的程序即可运行,不会依赖libvirbox64.dylib

操作指引#

根据自己环境要求,界面操作和命令行操作方式任选其一即可。

编译app#

1.修改编译选项,编译的xcarchive包内带有dSYM文件,如图所示:

操作:Xcode->TARGETS->Build Setting->Build Options->Debug Information Format选择DWARF with dSYM File选项;

目的:加壳工具解析app时可以解析出函数名称,否则,函数将会只会显示地址;

2.加壳工具暂不支持bitcode,Xcode15编辑器已经没有bitcode选项,所以若是Xcode15以上的编译器则可忽略该操作;

若是Xcode15以下的编译器,则编译程序时需关闭bitcode的编译选项,操作Xcode->TARGETS->Build Setting->Build Options->Enable Bitcode->no关闭bitcode;

3.以上选项配置完后,选择Xcode->Prodcut->Archive进行编译程序,如图所示;

4.编译成功后进入到Archives页面;

5.选中编译好的程序,右键在Finder里打开该程序;

6.找到编译好的xcarchive,右键显示包内容;

7.进入包内容后,在Products\Applications目录下为待保护的app程序。

界面操作#

1.将app和ipa程序拖入加壳工具界面;

2.点击按钮函数选项->添加函数可以选择要保护的函数;

3.可以选择想要保护的函数,将其设置为代码混淆或代码虚拟化;

注意

1.若是使用OC或Swift语言编译的iOS程序,则其本身自带了符号,所以有无dSYM文件,加壳工具均可识别函数名;

2.若是iOS程序中调用了C/C++语言编译的库,则编译后的app或ipa文件里不带符号,此时若想显示C/C++库里的函数名,则需要dSYM文件;

4.在加密选项处选择所需的功能并勾选,启用签名选择证书,点击保护选中项目,等待保护完成即可。

5.保护完成后,默认在protected目录下生成加固后的程序。

  • ***.app.ssp:指配置文件,主要存储Virbox Protector界面选择的函数和加密所选的选项,该配置文件和原程序在同目录且名称一致,即Virbox Protector再次保护程序时可不用重新配置;

  • protected/***.ipa:指加固后的app程序打包成ipa的包;

  • protected/***.app:指加固后生成的程序;

命令行操作#

命令行工具#

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

使用配置文件保护

使用工具界面进行保护,在被保护的程序旁边会生成 .ssp 文件,然后调用virboxprotector_con

virboxprotector_con <input_file> -o <output_file>

virboxprotector_con 会自动查找 <input_file>.ssp 作为配置文件开始保护。

无配置文件保护

如果没有配置文件,virboxprotector_con会使用默认参数保护,可以传入具体参数覆盖默认选项,参考命令行选项

以上两种方式任选其一即可。

命令行选项#

加密选项

选项参数默认选项
内存校验--mem-check=0
调试器检测--detect-dbg=0
OC名称混淆--objc-rename=0
签名校验(仅iOS)--sign-check=0
指定app内的frameworks--frameworks=<.framework>
移除调试信息--strip-dbginfo=1

举例

对ipa保护,勾选内存校验、调试器检测和移除调试信息选项,不启用签名,命令参考如下:virboxprotector_con test.ipa --mem-check=1 --detect-dbg=1 --strip-dbginfo=1 -o protector/test.ipa

签名选项

选项参数
启用签名--sign=
证书名称--identity=
ipa包输出--ipa=

举例

1.使用命令行查看系统上的证书security find-identity -v -p codesigning2.对ipa保护,勾选内存校验、调试器检测和签名校验选项,并启用签名,命令参考如下:virboxprotector_con test.ipa --mem-check=1 --detect-dbg=1 --sign-check=1 --sign=1 --identity="证书id" -o protector/test.ipa

1.macOS app保护,参考示例:

开启内存校验、调试器检测和移除调试信息功能,并启用签名,命令参考如下:virboxprotector_con test.app --mem-check=1 --detect-dbg=1 --strip-dbginfo=1 --sign=1 --identity="Apple Development:ceshi@123.com" -o protected/test.app

2.iOS app保护,参考示例:

开启内存校验、调试器检测和移除调试信息功能,并启用签名,输出ipa,命令参考如下:virboxprotector_con test.app --mem-check=1 --detect-dbg=1 --strip-dbginfo=1 --sign=1 --identity="Apple Development:ceshi@123.com" --ipa=protected/test.ipa

3.若app中的有framework框架,使用--frameworks=<.framework>参数保护app内的framework框架,其中<.framework>是指app内嵌套的framework的文件名,参考示例:

virboxprotector_con test.app --mem-check=1 --detect-dbg=1 --strip-dbginfo=1 --frameworks="test.framework" --sign=1 --identity="Apple Development:ceshi@123.com" --ipa=protected/test.ipa

函数选项

选项参数
代码虚拟化-v
代码混淆-m
忽略不支持的函数--ignore-unsupported=

支持指定函数名称或规则保护,使用 ;号隔开,支持通配符 *,举例:

-m "function1;function2" -v "function3;function4" --ignore-unsupported=1

举例

1)对指定的方法名进行混淆和虚拟化,参考示例:

virboxprotector_con test.app -v "-[ViewController setDetectResults:];-[ViewController timer]" -m "-[ViewController setTableView:];-[ViewController detectResults]" --detect-dbg=0 --sign-check=0 --sign=1 --identity="Apple Development: XXXX@qq.com" --ipa=protected/test.ipa

2)对某个类名进行混淆和虚拟化,参考示例:

virboxprotector_con AppProtectDemo.app -v "-[ViewController *]" -m "-[SecondViewController *]" --detect-dbg=0 --sign-check=0 --sign=1 --identity="Apple Development: XXXX@qq.com" --ipa=protected/test.ipa

上面命令表示:ViewController类名下的方法均会被虚拟化,SecondViewController类名下的方法均会被混淆。

注意

  1. 每个程序的函数名都不一样,根据自己的需求指定对应的函数名;
  2. 一个函数名不能同时指定代码混淆和代码虚拟化两种保护方式;
  3. 函数选项过多代码虚拟化后可能会影响程序性能,建议对关键函数进行设置代码虚拟化。

保护方案#

请根据自己程序的特性,可以在加固时勾选对应的选项。

选项性能影响稳定性和兼容性
内存校验启动时有轻微影响
调试器检测几乎无影响
OC名称混淆几乎无影响中(和程序有关)
签名校验(仅iOS)无影响
移除调试信息无影响

上架应用#

若加固后的程序需上架App Store,则可使用Xcode或Transporter工具进行提交(也可使用其他工具进行提交,以下不在列举)。

Xcode工具#

1.将xcarchive包里的原程序app删除或移动备份到其他位置;

2.再将protected/***.app移动到原程序app的位置上,并删除.ssp配置文件;

3.修改完后,在返回Archives页面,点击Distribute APP对程序进行提交。

Transporter工具#

1.将加固后生成的ipa直接拖入到Transporter工具中;

2.点击验证,等待程序验证完成;

3.程序验证完成后,可选择交付即可。

问题#

Xcode环境下加固app#

Xcode16以下版本

1.在Target->Build Phases->New Run Script Phases创建Run Script;

2.在Run Script添加如下脚本;

3.然后进行build,则默认在原app目录下生成一个protected文件夹,目录里即为加固后的app或ipa。

Xcode16以上版本

1.在Xcode 16版本创建新的项目;

2.在Edit Scheme->Build->Post-actions新建Run Script;

3.在Run Script添加脚本;

4.然后进行build,则默认在原app目录下生成一个protected文件夹,目录里即为加固后的app或ipa。

脚本示例,参考如下:

echo "开始执行加固命令,根据需求添加命令参数,参考如下""/Users/sense/Desktop/Virbox Protector 3 Trial.app/Contents/MacOS/bin/virboxprotector_con" "${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app" --detect-dbg=1 --sign-check=0 --sign=1 --identity="Apple Development: 27XXXXXX7@qq.com" --ipa="${TARGET_BUILD_DIR}/protected/${PRODUCT_NAME}.ipa"

非越狱手机上安装ipa#

【注】这种情况只在个人测试时才会出现这个问题,如果程序正常上架后,在app store里下载,就不会区分手机是越狱还是非越狱。

问题:若加壳时文件签名证书选择和Xcode编译xcarchive时的证书不一致,程序重新签名,如何在非越狱手机上安装程序?

解决1:重新签名时,证书用一致的。

解决2:

1、下载爱思助手,并打开

2、连接设备

3、点击爱思助手的工具箱,选择”IPA签名“,选择使用“Apple ID签名”

4、点击“添加Apple ID”,输入开发者账号,选择设备UDID,点击添加。

4、选择ipa文件,选择对应的Apple ID,点击开始签名

5、签名成功后,点击“打开已签名IPA位置”,可以看到重签后的IPA

6、在非越狱手机上可以安装成功(注:这种情况下签名的ipa只能在该设备ID上安装)。

崩溃信息#

若程序加壳后在手机上运行崩溃,需要将崩溃信息提供给Virbox人员,如何获取崩溃信息?

1、手机连接电脑,保证手机ip地址要和电脑IP地址在同一网段内。

2、打开Xcode ,选择Window->Devices and Simulators选项;

2、点击View Device Logs选项;

3、若手机ip地址要和电脑IP地址在同一网段内,在手机上运行的app崩溃后,日志会自动同步到此页面;

4、选中Type为Crash,点击右键Export log,将该日志保存到本地。

如何用命令给app签名#

1、使用该命令查询电脑上的证书

security find-identity -v -p codesigning

2、使用该命令对app进行签名

codesign -fs <证书信息>  ***.app

如果将app打包成ipa#

1、创建一个文件夹,名称为Payload;

2、将保护后的.app文件放入该文件夹中;

3、将Payload文件夹进行压缩(默认压缩为.zip);

4、将后缀名.zip改名为.ipa;