iOS-重签名

724 阅读10分钟

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)处理来实现脱壳处理。这种方法实现起来相对简单,且不必关心使用的是何种加密技术。所以目前市面上的砸壳工具都是基于动态砸壳进行的。动态砸壳有多种工具:ClutchdumpdecryptedCrackerXI 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命令完成

  1. 查看ipa包是否加壳,只有未加壳的包才可以重签名

    $ otool -l Mach-O文件 | grep crypt
    // 输出cryptid为0代表已经砸壳,即解密,为1或者2表示以第1类或者第2类加密方案加密
    
  2. 查看本地证书列表并记录下要用来签名的证书名,例如"iPhone Distribution: XXXXX (XXX)"

    $ security find-identity -v -p codesigning
    
  3. 新建Xcode工程,用第1步证书编译后生成新App,将App包里embedded.mobileprovision文件取出替换ipa包中的文件

  4. 删除ipa包内部可被重签名的插件(PlugIns目录下)

  5. 将ipa包内的所有Framework重签名(Frameworks目录下)

    $ codesign -fs "iPhone Distribution: XXXXX (XXX)"  xxx.framework
    
  6. 查看Mach-O文件是否有系统权限,若没有则添加权限

    $ chmod +x Mach-O文件
    
  7. 将ipa包内info.plist的BundleId修改为第1步中工程的BundleId

  8. 用命令查看embedded.mobileprovision文件,找到其中的entitlements字段,并且复制entitlements字段和其中的内容

    $ security cms -D -i embedded文件路径
    
  9. 新建entitlements.plist文件,将复制内容拷贝到文件中,然后将entitlements.plist复制到ipa的同级目录下

  10. 对App进行重签名,并压缩成新的ipa包

    //重签名
    $ codesign -fs "iPhone Distribution: XXXXX (XXX)" --no-strict --entitlements=entitlements.plist
    
    //压缩ipa包
    $ zip -r 输出的文件名(.ipa) Payload/
    
  11. 将ipa包安装到手机,若能同时存在两个应用且能正常运行则表示重签名成功

3.4、使用Shell脚本文件重签名

利用shell脚本进行重签名的原理,跟上面的签名原理相同,只不过把重签步骤给脚本化了

  1. 创建空工程(工程名随便),并且进行真机运行
  2. 在工程根目录下创建APP文件夹,在文件中放入我们砸壳后的ipa包
  3. 在工程的Build Phases中添加脚本
  4. 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. 解压IPATemp下
    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. 为了是重签过程简化,移走extensionwatchAPP. 此外个人免费的证书没办法签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_flagP_TRACED =0 就是有调试
    return ((info.kp_proc.p_flag & P_TRACED) !=0);
   
}
...
...
//检测异常时退出
if (checkDebugger()) {
        asm("mov X0,#0\n"
            "mov w16,#1\n"
            "svc #0x80"
            );
       
    }
...

参考:juejin.cn/post/703181…