iOS安全与逆向-判断APP被重签名的一种方案

2,353 阅读6分钟

之前看到一篇防逆向的文章,大概就是去检测包里是否有embedded.mobileprovision,然后解析描述文件的application-identifier来对比看是否包被重签名了

于是我就自己测试了一下,本文就整理一下如何去检测,以及如果去反检测(分析一种简单场景-将检测的函数给hook掉)

1.检测是否被重签名

实现的思路就是:

  • 我们通过开发证书去重签名一个应用的时候,会有一个embeded.mobileprovision文件来描述应用的信息、可以安装的设备信息等
  • 当我们去重签名的时候,会生成一个embeded.mobileprovision来签名砸壳之后的包,这个描述文件中则包含着签名的证书的teamId以及壳APP的identifier等信息
  • 在程序启动的时候去检测是否有描述文件,然后获取到关键的信息,跟原始的信息进行比对,不一致则程序退出

一般我们开发调试以及逆向的人重签名均有这个描述文件

代码也比较简单,就是解析描述文件,然后拿到application-identifier跟已知的签名信息比对,不一致则退出程序

代码实现如下:

void checkCodeSign(NSString *identifier, NSString *teamId) {
#if defined __x86_64__ || __i386__ // 模拟器不需要生成embeded.mobileprovision文件来做真机调试的配置
    // do nothing
#else
    // 描述文件路径
    NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:embeddedPath]) {
        return;
    }
    // 读取application-identifier  注意描述文件的编码要使用:NSASCIIStringEncoding
    NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
    NSArray<NSString *> *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    for (int i = 0; i < embeddedProvisioningLines.count; i++) {
        if ([embeddedProvisioningLines[i] rangeOfString:@"application-identifier"].location != NSNotFound) {
            NSString *identifierString = embeddedProvisioningLines[i + 1]; // 类似:<string>L2ZY2L7GYS.com.xx.xxx</string>
            NSRange fromRange = [identifierString rangeOfString:@"<string>"];
            NSInteger fromPosition = fromRange.location + fromRange.length;
            NSInteger toPosition = [identifierString rangeOfString:@"</string>"].location;
            NSRange range;
            range.location = fromPosition;
            range.length = toPosition - fromPosition;
            NSString *fullIdentifier = [identifierString substringWithRange:range];
            NSScanner *scanner = [NSScanner scannerWithString:fullIdentifier];
            NSString *teamIdString;
            [scanner scanUpToString:@"." intoString:&teamIdString];
            NSRange teamIdRange = [fullIdentifier rangeOfString:teamIdString];
            NSString *appIdentifier = [fullIdentifier substringFromIndex:teamIdRange.length + 1];
            // 对比签名teamID或者identifier信息
            if (![appIdentifier isEqualToString:identifier] || ![teamId isEqualToString:teamIdString]) {
                // exit(0)
                asm(
                    "mov X0,#0\n"
                    "mov w16,#1\n"
                    "svc #0x80"
                    );
            }
            break;
        }
    }
#endif
}

2.逆向hook检测的函数

针对检测函数的类型有不同的hook方式

  • OC方法 - 直接使用runtime的method-swizzle
  • 动态库的C方法 - fishhook去rebind symbols
  • 静态的C方法 - Dobby去静态hook

本文主要是测试一下Dobby去静态hook,对于OC的函数以及动态库的C方法比较简单,就不多说了; 如果发现你逆向的app有一些检测,那么一般的思路就是绕过检测,接下来就使用Dobby来看如果绕过上面说的checkCodeSign检测

2.1 Dobby

由于checkCodeSign函数是个静态的C函数,它没有动态符号同时也不走OC的消息机制,没法通过runtime的方式以及fishhook rebind符号的方式去hook它,不过还好有Dobby这个工具支持静态的hook

你可以直接下载最新的release包,也可以自己去编译生成包;如何编译生成包直接参照Getting Started With iOS去生成一个工程,然后Xcode编译一下framework

通过cmake来生成对应平台的工程

cmake .. -G Xcode \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake \
-DPLATFORM=OS64 -DARCHS="arm64" -DCMAKE_SYSTEM_PROCESSOR=arm64 \
-DENABLE_BITCODE=0 -DENABLE_ARC=0 -DENABLE_VISIBILITY=1 -DDEPLOYMENT_TARGET=9.3 \
-DDynamicBinaryInstrument=ON -DNearBranch=ON -DPlugin.SymbolResolver=ON -DPlugin.Darwin.HideLibrary=ON -DPlugin.Darwin.Obj

该工具提供了很对配置参数,比如架构、是否支持bitcode等,如果你的项目支持bitcode那么就-DENABLE_BITCODE=1

这里需要注意下cmake .. 是在build目录下执行的,如果你跟我一样有创建子目录,那么需要修改下命令

图片.png

我这里是多了一级子目录,我在iOS_arm64目录执行的cmake,那么就将cmake ..修改为cmake ../..就能找到cmake配置目录了

执行完后在build目录下就有对于的工程文件了

图片.png

通过Xcode来打包Framework

图片.png

运行一下,然后将打包好的Framework拷贝出来就可以了

2.2 使用Dobby静态hook函数

第一次用Dobby,就先试了下直接在主APP中去hook试试效果

先看看hook函数的定义

int DobbyHook(void *function_address, void *replace_call, void **origin_call);

三个参数:

function_address -- 需要hook的函数的地址 replace_call -- 新函数,也就是我们hook的实现 origin_call -- 用来保存原有的函数实现的指针地址

那么我们就定义一个新函数hookCheckCodeSign,一个函数指针*originCheckCodeSign用来存储原始实现的指针地址,实现起来也简单,代码如下:

void checkCodeSign(NSString *identifier, NSString *teamId); // 原来的方法
void (*originCheckCodeSign)(NSString *identifier, NSString *teamId); // 保留原始的方法实现的指针地址
void hookCheckCodeSign(NSString *identifier, NSString *teamId); // hook的方法

int main(int argc, char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        NSString * appDelegateClassName = NSStringFromClass([AppDelegate class]);
        BOOL isEncrypt = isEncrypted();
        NSLog(@"check is encrypt: %d", isEncrypt);
        // int DobbyHook(void *function_address, void *replace_call, void **origin_call);
        DobbyHook(checkCodeSign, hookCheckCodeSign, (void *)&originCheckCodeSign);
        checkCodeSign(@"hc.RuntimeLearning.demo", @"9D7EH8PVAX"); // security find-identity -v -p CodeSigning 可以获取到,也可以在导出ipa包的plist中查看
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }
}

void hookCheckCodeSign(NSString *identifier, NSString *teamId) {
    // do nothing
    NSLog(@"%s", __FUNCTION__);
}

运行查看效果,hook成功了执行了hook之后的函数

RuntimeLearning[8921:1837017] hookCheckCodeSign
2.3 注入的方式hook主包中的函数

我们分析别人的APP,重签名做代码注入的时候,显然是没法直接在主包的源码中去写hook的代码的,可以通过动态注入的方式,注入代码来达到修改源程序的功能

注入的思路:

  • 创建一个动态库编写注入代码
  • 将动态库注入到主APP的macho中
  • 在合适的时机执行注入的代码;关于这个合适的时机可以利用_objc_init的流程来在程序初始化的时候去执行注入的代码逻辑

代码注入时机:

  • C全局初始化方法__attribute__((constructor(1)))
  • 库的load函数中

这些方法都会在main之前调用,可以保证我们在程序执行检查函数之前就将其hook掉

2.3.1 编写注入代码

准备工作:

通过MachOView去获取需要hook的函数的地址(偏移地址) 0x1000164A0

图片.png

新建了一个动态库HCInjecttarget,然后编写代码:

由于安全性的需要,iOS在加载代码镜像到内存的时候会随机生成一个镜像文件的起始地址,函数的真实地址则是这个随机值ASLR+函数对于该镜像的offset值

我这里hook的是主程序中的函数,所以_dyld_get_image_vmaddr_slide(0)的参数传的是0

lldb调试下也可以直接通过image list来查看所有加载的镜像的ASLR 图片.png

最终实现代码如下:

#import "HCHook.h"
#import <Dobby/dobby.h>
#import <mach-o/dyld.h>

void (*originCheckCodeSign)(NSString *identifier, NSString *teamId); // 保留原始的方法实现的指针地址
void hookCheckCodeSign(NSString *identifier, NSString *teamId); // hook的方法

@implementation HCHook

+ (void)load {
    static uintptr_t checkCodeSignOffset = 0x1000164A0; // 这个偏移地址可以通过MachOView去查看
    uintptr_t mainASLR = _dyld_get_image_vmaddr_slide(0); // 获取主程序的aslr,因为checkCodeSign函数在主程序
    uintptr_t checkCodeSignAddress = mainASLR + checkCodeSignOffset;
    DobbyHook((void *)checkCodeSignAddress, hookCheckCodeSign, (void *)&originCheckCodeSign);
}

void hookCheckCodeSign(NSString *identifier, NSString *teamId) {
    // do nothing
    NSLog(@"%s", __FUNCTION__);
}

@end

2.3.2 注入代码到主程序

注入的思路就是修改主程序的MachO文件,我习惯使用yololib工具去修改MachO的Load Commonds将我们写的动态库注入进去,这里我直接使用shell脚本的方式,脚本如下:

TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路径:$TARGET_APP_PATH"
# 拿到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"
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
# 拷贝包到framework里面去
INJECT_FRAMEWORK_BUILD_PATH="$SRCROOT/HCInject.framework"
cp -rf "$INJECT_FRAMEWORK_BUILD_PATH" "$TARGET_APP_FRAMEWORKS_PATH"
#签名:如果不签名,那么dyld load的时候签名校验不过就会加载失败 报 image not found
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$TARGET_APP_FRAMEWORKS_PATH/HCInject.framework"
#注入:修改MachO文件
yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/HCInject.framework/HCInject"

在Xcode的Build Phases菜单下添加一个执行脚本的工作流 图片.png

执行脚本 图片.png

拷贝库文件到APP的Frameworks目录 图片.png

运行查看效果: 图片.png

嗯,成功了;至此已经实现了通过注入代码的方式去hook主程序中的C函数了

3. 总结

  • 通过一个APP加固的小方案以及如何去逆向它,学习了一下Dobby工具的使用
  • APP没有绝对的安全,安全是相对的,做的一切加固的方案,只是为了增加逆向的成本