iOS证书签名机制&重签名&防止重签名

6,641 阅读6分钟

iOS签名机制

概念

相比安卓系统随便从哪里下载都能运行,系统安全存在许多隐患,例如盗版软件、病毒入侵、静默安装等等。苹果为了控制手机上的所有应用必须是经过授权的,规定正规应用只能在AppStore下载,同时对于调试阶段的安装、灰度测试和企业分发,苹果使用双重签名机制来控制应用权限并使其不被滥用。先介绍一些概念便于后续的理解。

非对称加密

非对称加密则两份密钥,分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。正常情况下,公钥可以对外公开,而私钥需私密保存。我们常用公钥加密私钥解密的方式来进行加密防止数据明文传输,用私钥加密公钥解密的方式来签名确保数据的完整性防止被篡改。

数字摘要

将任意长度的源文本通过Hash函数计算得到一窜固定长度的文本,要保证不同源文本计算得到的值各不相同,且不能被反推得到源文本。常用的Hash算法有MD5和SHA。

数字签名

又称公钥数字签名,是非对称加密与数字摘要的结合,用于验证数据的完整性及不可抵赖性。发送方用接收方的公钥对摘要进行加密后和报文一起发送,接受方接受后用相同的Hash算法对报文进行摘要计算,接着用自己的私钥解密得到发送方的摘要,如果两个摘要相同则接收方就能确认该报文是对方发出的且未被篡改。

其他

名称 作用
证书(cer) 内容是公钥或私钥,由其他机构对其签名组成的数据包。
Entitlements 包含了 App 权限开关列表。
CertificateSigningRequest 本地公钥。
p12 本地私钥,可以导入到其他电脑。
Provisioning Profile 包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

AppStore签名

将App提交审核后,苹果会用官方的私钥对我们提交的App重签名,用户下载到手机后,iOS设备内置的公钥会对App进行验证,验证成功即可正常使用。
所以App上传到AppStore后,就跟我们本地的证书/Provisioning Profile 都没有关系了,无论是否过期或被废除,都不会影响 AppStore 上的安装包。

Xcode安装&Adhoc&In-House

对于非AppStore安装的应用苹果采用了双重签名的方式,用到了两对密钥,Mac电脑密钥对L,苹果官方密钥对A。

  • 我们在进行开发时,会从Mac的keychain 里的 “从证书颁发机构请求证书生成”CertificateSigningRequest(含有Mac的公钥L信息)文件上传到苹果后台,苹果会用其私钥A对公钥L签名,并生成一份包含公钥L信息和苹果签名信息的开发/发布证书cer,当我们将cer下载到Mac后,keychain会把CertificateSigningRequest和证书关联起来。
  • 在苹果后台配置AppID,可用设备IDs(企业证书不需要)和Entitlements,用这些额外信息和证书再用私钥A签名,最后苹果将证书+额外信息+签名组成一个Provisioning Profile文件(mobileprovision后缀),下载到Mac上。
  • 在Mac上编译完一个App后,Mac会用私钥L对App签名,并将Provisioning Profile文件也打包到App中,文件名为 embedded.mobileprovision。
  • 安装时,苹果通过内置在手机中的公钥A验证embedded.mobileprovision中的签名是否正确,接着验证证书中的签名是否正确。
  • 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,验证证书是否过期,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等等。

如果别的 Mac 也要编译签名这个 App,可以将私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成.p12 文件,其他 Mac 导入了这个私钥后便可用同一套苹果证书及Provisioning Profile文件。 In-House安装不限制设备ID数。

重签名

完全重签名

重签是把已发布/未发布的包重新签名为自己的证书和签名,关键就是替换ipa内的证书和描述文件。主要通过codesign命令完成。

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

$ otool -l 「Mach-O文件」 | grep crypt
// 输出cryptid为0代表已经砸壳,即解密,为1或者2表示以第1类或者第2类加密方案加密。

1.1 查看本地证书列表并记录下要用来签名的证书名,例如"iPhone Distribution: XXXXX (XXX)"。

$ security find-identity -v -p codesigning

1.2 新建Xcode工程,用1.1证书编译后生成新App,将App包里embedded.mobileprovision文件取出替换ipa包中的文件。

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

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

$ codesign -fs "iPhone Distribution: XXXXX (XXX)"  xxx.framework

2.3 查看Mach-O文件是否有系统权限,若没有则添加权限。

$ chmod +x 「Mach-O文件」

3.1 将ipa包内info.plist的BundleId修改为1.1中工程的BundleId。

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

$ security cms -D -i 「embedded文件路径」

3.3 新建entitlements.plist文件,将复制内容拷贝到文件中,然后将entitlements.plist复制到ipa的同级目录下。

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

//重签名
$ codesign -fs "iPhone Distribution: XXXXX (XXX)" --no-strict --entitlements=entitlements.plist

//压缩ipa包
$ zip -r 「输出的文件名(.ipa)」 Payload/

4 将ipa包安装到手机,若能同时存在两个应用且能正常运行则表示重签名成功。

Shell脚本重签名

# ${SRCROOT} 它是工程文件所在的目录
TEMP_PATH="${SRCROOT}/Temp"
#资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
#目标ipa包路径
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夹
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"

#----------------------------------------
# 1. 解压IPA到Temp下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解压的临时的APP的路径
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路径是:$TEMP_APP_PATH"

#----------------------------------------
# 2. 将解压出来的.app拷贝进入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路径
# TARGET_NAME target名称
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路径:$TARGET_APP_PATH"

rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

#----------------------------------------
# 3. 删除extension和WatchAPP.个人证书没法签名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"

#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
#  设置:"Set : KEY Value" "目标文件路径"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"
#----------------------------------------

# 5. 给MachO文件上执行权限
# 拿到MachO文件的路径
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可执行权限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"

#----------------------------------------
# 6. 重签名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do

# 签名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi

如何防止应用被重签名

内容来源于「iOS安全防护之重签名防护和sysctl反调试」。

校验描述文件信息

可以在启动时校验描述文件信息与打包时是否一致。例如判断组织单位:
先记录证书中的组织单位信息。

在启动时检测是否一致。

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

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"
            );
       
    }
...

参考链接

iOS App 签名的原理
iOS逆向(3)-APP重签名
iOS安全防护之重签名防护和sysctl反调试

From:SimonYe