1、概述
我们做iOS的开发者对应用本身的安全问题总不是那么上心,总以为Apple自身的加密签名机制足够安全了,我们除了关心业务网络安全,对自身App包的安全重视度总是不够。然而一旦我们的包被有心人破解,那么无论是对自身业务还是用户数据都是致命的。真正开始研究逆向,才发现我们App是如此的不堪一击。所以研究逆向其实是研究Hacker
是如何攻破我们的应用,从而更好地保护自身应用安全。
研究逆向,首当其冲的就是重签名,重签名就是用自己的证书签名别人的应用,从而进行分发。重签名说白了就是套壳别人的应用,挺火的微信双开,其实就是对微信进行了重签名。而重签名首先要做的就是对应用砸壳
2、手机越狱与应用砸壳
我们从App store 下载的应用是经过苹果加密签名过的,是无法进行重签名的。所以重签名首先要做的就是对应用进行砸壳,而砸壳需要你有一台越狱手机
2.1、手机越狱
目前主流的越狱平台有PP助手、爱思助手做到比较好,提供一键越狱功能。下载爱思助手电脑版,选择【工具箱】-【一键越狱】,此时会匹配你当前手机版本的越狱工具,选择工具进行越狱
我使用的是 iphone SE,系统版本是14.4,按照提示完成越狱就OK了
需要注意,这种越狱方式是不完美的越狱,当你重启手机后会重新变回非越狱状态,需要重新走一遍越狱流程。不过对于我们研究逆向足够了。
还没完,越狱完成后需要安装一个插件:Apple File Conduit"2"
,安装这个插件的目的是,让我们的电脑端能访问设备的根文件目录,安装方式也很简单,可以参考这篇文章Apple File Conduit"2"安装,这里不再赘述。当我们安装完成后,爱思助手连接手机,可以看到在【文件管理】中多了个栏目【文件系统(越狱)】,这就是越狱状态下的系统根目录
2.2、ipa应用砸壳
首先了解什么是应用砸壳:我们提交到App Store发布的App都是经过Apple加密的,这样可以确保安装到我们手机的应用都是苹果审核授权的,当然通过企业级证书或者开发者证书生成的App是不需要砸壳的。对于App Store加密的应用,我们无法通过Hopper等反编译静态分析,也无法class-dump,在逆向分析过程中,需要对加密的二进制文件进行解密才可以进行静态分析,这一过程就是大家熟知的砸壳。 砸壳主要有两种方式:
静态砸壳
:静态砸壳就是在已经掌握和了解到了壳应用的加密算法和逻辑后,在不运行壳应用程序的前提下将壳应用程序进行解密处理。静态脱壳的方法难度大,而且加密方发现应用被破解后,就可能会改用更加高级和复杂的加密技术动态砸壳
:动态砸壳就是从运行在进程内存空间中的可执行程序映像(image)入手,来将内存中的内容进行转储(dump)处理来实现脱壳处理。这种方法实现起来相对简单,且不必关心使用的是何种加密技术。所以目前市面上的砸壳工具都是基于动态砸壳进行的。动态砸壳有多种工具:Clutch
、dumpdecrypted
、CrackerXI App
,前两种对系统要求较严格,而且年久失修很容易砸壳失败,我们直接介绍最方便的第三种方式:利用CrackerXI App
进行砸壳
2.2.1、在Cydia
中下载CrackerXI App
Cydia
类似于越狱前的App Store,越狱后我们所有的软件都是通过Cydia
来进行安装。打开Cydia
后,选择【软件源】-右上角【编辑】-点击【添加】,输入http://apt.wxhbts.com/
,【添加源】等待添加完成。添加软件源完成后,搜索CrackerXI App
,点击安装完成后【重启springboard】,回到桌面,可以看到桌面上安装完成了CrackerXI+
App
2.2.2、砸壳
提前在App Store 下载好你要砸壳的App,打开CrackerXI+
,选择【AppList】然后点击你要砸壳的应用,在弹框中选择【YES,Full IPA】,此时会打开我们要砸壳的应用进行砸壳,完成后会看到一个砸壳后的文件地址/var/mobile/Documents/CrackerXI/*****.ipa
,这个文件就是我们砸壳后的ipa包
这个路径怎么查找相信聪明的你已经知道了,去爱思助手-【文件管理】-【文件系统(越狱)】查找到这个ipa,导出到桌面目录就完成了
2.2.3、验证
完成上面的操作后,我们需要验证一下拿到的ipa是否是被砸壳的。把.ipa
包扩展名改为.zip
,解压后得到Payload
文件夹,右键【显示包内容】- 找到可执行文件
终端输入
otool -l 执行文件名 | grep crypt
复制代码
可以看到cryptid
的值为0,说明砸壳成功
3、重签名
3.1、概述
Apple 应用的分发一般有以下几种方式:
- 最常用的是从App store下载应用。这种下发方式不受设备数的限制,只要上线App store 的应用,都会被Apple进行签名加密
- 第二种方式是申请企业账号,把我们的应用通过企业账号进行签名,从而绕过App store恼人的审核机制,达到分发应用的目的。这种方法分发数目也不受限制
- 第三种是通过
TestFlight
进行测试版本的分发,他分为内部测试人员与外部测试人员。通过分发外部测试人员,最多能给1万名用户进行分发安装 - 第四种开发人员通过添加设备ID安装应用,最多可以注册100台设备
不同的开发者账号对应着App不同的分发方式,我们申请完成开发者账号后,创建应用Id,然后创建其对应证书,描述文件等一系列动作,实际已经决定了它的分发方式了。可以这样理解:每一个应用Id后面对应一套证书,这套证书决定了你应用的分发方式。重签名就是为当前的应用换一套应用Id与证书,从而达到分发应用的目的。现实的需求是,如果你上线App Store的应用,想通过企业账号的形式进行分发,而你又没有源码,或是想探究一下应用双开,那么重签名就派上用场了
学习重签名之前,需要注意几点:
- 第一,安装包中的可执行文件必须是进行过脱壳操作的,重签名才会生效,不然会安装失败
- 第二,重签名所需要的mobileprovision文件必须是付费开发者账号申请的才可以,免费开发者账号无法进行重签名。
- 第三,.app包中的所有动态库(.framework,.dylib)、AppExtension(PlugIns文件夹,拓展名是appex)、WatchApp(Watch文件夹)等都需要进行重签名操作
3.2、重签名
3.2.1、新建同名的工程文件
注意这里的同名并不是Bundle Identifier 相同,而是跟你砸壳解压ipa文件,Payload里面的包相同的名称
还需要注意,如果你重签名的工程中是通过AppDelegate
来监听App的生命周期的话,那么需要在新版的Xcode中移除SceneDelegate
这个类,重新使用AppDelegate
来监听App的生命周期。重新配置完成工程后,真机运行,把描述文件安装到手机里
3.2.2、替换编译的App包
找到我们砸壳过的Payload文件夹中包,对我们编译的包进行替换
3.2.3、对二进制文件中的FrameWork进行重签名
其实在这一步之前还需要对包内的
PlugIns
插件以及Watch
相关组件进行删除,我们逆向包里面没有这些组件,所以省略了。
进入Framework 文件夹,利用CodeSign
对Framework进行证书签名,注意要对所有FrameWork进行重签名。
codesign -fs "复制的你自己的证书名字" 要重签的FrameWork名称
证书的名字就是你真机测试的证书的名称,如果不知道可以去钥匙串中查看
iPhone Developer: *** ** (********)
就是证书名称
3.2.4、重签名后运行
这样我们就在没有源码的情况下,完成了对应用的重签名
3.3、完全重签名流程
重签是把已发布/未发布的包重新签名为自己的证书和签名,关键就是 替换ipa内的证书和描述文件。主要通过codesign
命令完成
-
查看ipa包是否加壳,只有未加壳的包才可以重签名
$ otool -l 「Mach-O文件」 | grep crypt // 输出cryptid为0代表已经砸壳,即解密,为1或者2表示以第1类或者第2类加密方案加密
-
查看本地证书列表并记录下要用来签名的证书名,例如"iPhone Distribution: XXXXX (XXX)"
$ security find-identity -v -p codesigning
-
新建Xcode工程,用第1步证书编译后生成新App,将App包里
embedded.mobileprovision
文件取出替换ipa包中的文件 -
删除ipa包内部可被重签名的插件(PlugIns目录下)
-
将ipa包内的所有Framework重签名(Frameworks目录下)
$ codesign -fs "iPhone Distribution: XXXXX (XXX)" xxx.framework
-
查看Mach-O文件是否有系统权限,若没有则添加权限
$ chmod +x 「Mach-O文件」
-
将ipa包内info.plist的BundleId修改为第1步中工程的BundleId
-
用命令查看embedded.mobileprovision文件,找到其中的entitlements字段,并且复制entitlements字段和其中的内容
$ security cms -D -i 「embedded文件路径」
-
新建entitlements.plist文件,将复制内容拷贝到文件中,然后将entitlements.plist复制到ipa的同级目录下
-
对App进行重签名,并压缩成新的ipa包
//重签名 $ codesign -fs "iPhone Distribution: XXXXX (XXX)" --no-strict --entitlements=entitlements.plist //压缩ipa包 $ zip -r 「输出的文件名(.ipa)」 Payload/
-
将ipa包安装到手机,若能同时存在两个应用且能正常运行则表示重签名成功
3.4、使用Shell脚本文件重签名
利用shell
脚本进行重签名的原理,跟上面的签名原理相同,只不过把重签步骤给脚本化了
- 创建空工程(工程名随便),并且进行真机运行
- 在工程根目录下创建APP文件夹,在文件中放入我们砸壳后的ipa包
- 在工程的
Build Phases
中添加脚本 - 在
Run Script
中添加脚本
运行后完成对其重签名# Type a script or drag a script file from your workspace to insert its path. # Type a script or drag a script file from your workspace to insert its path. # ${SRCROOT} 为工程文件所在的目录 TEMP_PATH="${SRCROOT}/Temp" #资源文件夹,放三方APP的 ASSETS_PATH="${SRCROOT}/APP" #ipa包路径 TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa" #新建Temp文件夹 rm -rf "$TEMP_PATH" mkdir -p "$TEMP_PATH" # -------------------------------------- # 1. 解压IPA 到Temp下 unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH" # 拿到解压的临时APP的路径 TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1") # 这里显示打印一下 TEMP_APP_PATH变量 echo "TEMP_APP_PATH: $TEMP_APP_PATH" # ------------------------------------- # 2. 把解压出来的.app拷贝进去 #BUILT_PRODUCTS_DIR 工程生成的APP包路径 #TARGET_NAME target名称 TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app" echo "TARGET_APP_PATH: $TARGET_APP_PATH" rm -rf "$TARGET_APP_PATH" mkdir -p "$TARGET_APP_PATH" cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH/" # ------------------------------------- # 3. 为了是重签过程简化,移走extension和watchAPP. 此外个人免费的证书没办法签extension echo "Removing AppExtensions" rm -rf "$TARGET_APP_PATH/PlugIns" rm -rf "$TARGET_APP_PATH/Watch" # ------------------------------------- # 4. 更新 Info.plist 里的BundleId # 设置 "Set :KEY Value" "目标文件路径.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist" # 5.给可执行文件上权限 #添加ipa二进制的执行权限,否则xcode会告知无法运行 #这个操作是要找到第三方app包里的可执行文件名称,因为info.plist的 'Executable file' key对应的是可执行文件的名称 #我们grep 一下,然后取最后一行, 然后以cut 命令分割,取出想要的关键信息。存到APP_BINARY变量里 APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d>|cut -f1 -d<` #这个为二进制文件加上可执行权限 +X chmod +x "$TARGET_APP_PATH/$APP_BINARY" # ------------------------------------- # 6. 重签第三方app Frameworks下已存在的动态库 TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks" if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ]; then #遍历出所有动态库的路径 for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"* do echo "FRAMEWORK : $FRAMEWORK" #签名 /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK" done fi
4、如何防止应用被重签名
内容来源于「iOS安全防护之重签名防护和sysctl反调试」
4.1、校验描述文件信息
可以在启动时校验描述文件信息与打包时是否一致。例如判断组织单位:
先记录证书中的组织单位信息
在启动时检测是否一致
void checkCodesign(NSString *id){
// 描述文件路径
NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
// 读取application-identifier 注意描述文件的编码要使用:NSASCIIStringEncoding
NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
for (int i = 0; i < embeddedProvisioningLines.count; i++) {
if ([embeddedProvisioningLines[i] rangeOfString:@"application-identifier"].location != NSNotFound) {
NSInteger fromPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"<string>"].location+8;
NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"</string>"].location;
NSRange range;
range.location = fromPosition;
range.length = toPosition - fromPosition;
NSString *fullIdentifier = [embeddedProvisioningLines[i+1] substringWithRange:range];
NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
NSString *appIdentifier = [identifierComponents firstObject];
// 对比签名ID
if (![appIdentifier isEqual:id]) {
//exit
asm(
"mov X0,#0\n"
"mov w16,#1\n"
"svc #0x80"
);
}
break;
}
}
}
4.2、sysctl检测是否被调试
#import <sys/sysctl.h>
bool checkDebugger(){
//控制码
int name[4];//放字节码-查询信息
name[0] = CTL_KERN;//内核查看
name[1] = KERN_PROC;//查询进程
name[2] = KERN_PROC_PID; //通过进程id查进程
name[3] = getpid();//拿到自己进程的id
//查询结果
struct kinfo_proc info;//进程查询信息结果
size_t info_size = sizeof(info);//结构体大小
int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0);
assert(error == 0);//0就是没有错误
//结果解析 p_flag的第12位为1就是有调试
//p_flag 与 P_TRACED =0 就是有调试
return ((info.kp_proc.p_flag & P_TRACED) !=0);
}
...
...
//检测异常时退出
if (checkDebugger()) {
asm("mov X0,#0\n"
"mov w16,#1\n"
"svc #0x80"
);
}
...