之前看到一篇防逆向的文章,大概就是去检测包里是否有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 *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]; // 类似:L2ZY2L7GYS.com.xx.xxx
NSRange fromRange = [identifierString rangeOfString:@""];
NSInteger fromPosition = fromRange.location + fromRange.length;
NSInteger toPosition = [identifierString rangeOfString:@" "].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目录下执行的,如果你跟我一样有创建子目录,那么需要修改下命令
我这里是多了一级子目录,我在iOS_arm64
目录执行的cmake,那么就将cmake ..
修改为cmake ../..
就能找到cmake配置目录了
执行完后在build目录下就有对于的工程文件了
通过Xcode来打包Framework
运行一下,然后将打包好的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
新建了一个动态库HCInject
target,然后编写代码:
由于安全性的需要,iOS在加载代码镜像到内存的时候会随机生成一个镜像文件的起始地址,函数的真实地址则是这个随机值ASLR+函数对于该镜像的offset值
我这里hook的是主程序中的函数,所以_dyld_get_image_vmaddr_slide(0)
的参数传的是0
lldb
调试下也可以直接通过image list
来查看所有加载的镜像的ASLR
最终实现代码如下:
#import "HCHook.h"
#import
#import
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
菜单下添加一个执行脚本的工作流
执行脚本
拷贝库文件到APP的Frameworks目录
运行查看效果:
嗯,成功了;至此已经实现了通过注入代码的方式去hook主程序中的C函数了
3. 总结
- 通过一个APP加固的小方案以及如何去逆向它,学习了一下Dobby工具的使用
- APP没有绝对的安全,安全是相对的,做的一切加固的方案,只是为了增加逆向的成本