之前看到一篇防逆向的文章,大概就是去檢測包里是否有embedded.mobileprovision,然后解析描述文件的application-identifier來對比看是否包被重簽名了
于是我就自己測試了一下,本文就整理一下如何去檢測,以及如果去反檢測(分析一種簡單場景-將檢測的函數(shù)給hook掉)
1.檢測是否被重簽名
實(shí)現(xiàn)的思路就是:
- 我們通過開發(fā)證書去重簽名一個應(yīng)用的時候,會有一個embeded.mobileprovision文件來描述應(yīng)用的信息、可以安裝的設(shè)備信息等
- 當(dāng)我們?nèi)ブ睾灻臅r候,會生成一個embeded.mobileprovision來簽名砸殼之后的包,這個描述文件中則包含著簽名的證書的teamId以及殼APP的identifier等信息
- 在程序啟動的時候去檢測是否有描述文件,然后獲取到關(guān)鍵的信息,跟原始的信息進(jìn)行比對,不一致則程序退出
一般我們開發(fā)調(diào)試以及逆向的人重簽名均有這個描述文件
代碼也比較簡單,就是解析描述文件,然后拿到application-identifier
跟已知的簽名信息比對,不一致則退出程序
代碼實(shí)現(xiàn)如下:
void checkCodeSign(NSString *identifier, NSString *teamId) {
#if defined __x86_64__ || __i386__ // 模擬器不需要生成embeded.mobileprovision文件來做真機(jī)調(diào)試的配置
// 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檢測的函數(shù)
針對檢測函數(shù)的類型有不同的hook方式
- OC方法 - 直接使用runtime的method-swizzle
- 動態(tài)庫的C方法 - fishhook去rebind symbols
- 靜態(tài)的C方法 - Dobby去靜態(tài)hook
本文主要是測試一下Dobby去靜態(tài)hook,對于OC的函數(shù)以及動態(tài)庫的C方法比較簡單,就不多說了;
如果發(fā)現(xiàn)你逆向的app有一些檢測,那么一般的思路就是繞過檢測,接下來就使用Dobby來看如果繞過上面說的checkCodeSign
檢測
2.1 Dobby
由于checkCodeSign
函數(shù)是個靜態(tài)的C函數(shù),它沒有動態(tài)符號同時也不走OC的消息機(jī)制,沒法通過runtime的方式以及fishhook rebind符號的方式去hook它,不過還好有Dobby這個工具支持靜態(tài)的hook
你可以直接下載最新的release包,也可以自己去編譯生成包;如何編譯生成包直接參照Getting Started With iOS去生成一個工程,然后Xcode編譯一下framework
通過cmake來生成對應(yīng)平臺的工程
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
該工具提供了很對配置參數(shù),比如架構(gòu)、是否支持bitcode等,如果你的項目支持bitcode那么就
-DENABLE_BITCODE=1
這里需要注意下cmake .. 是在build目錄下執(zhí)行的,如果你跟我一樣有創(chuàng)建子目錄,那么需要修改下命令
我這里是多了一級子目錄,我在iOS_arm64
目錄執(zhí)行的cmake,那么就將cmake ..
修改為cmake ../..
就能找到cmake配置目錄了
執(zhí)行完后在build目錄下就有對于的工程文件了
通過Xcode來打包Framework
運(yùn)行一下,然后將打包好的Framework拷貝出來就可以了
2.2 使用Dobby靜態(tài)hook函數(shù)
第一次用Dobby,就先試了下直接在主APP中去hook試試效果
先看看hook函數(shù)的定義
int DobbyHook(void *function_address, void *replace_call, void **origin_call);
三個參數(shù):
function_address -- 需要hook的函數(shù)的地址
replace_call -- 新函數(shù),也就是我們hook的實(shí)現(xiàn)
origin_call -- 用來保存原有的函數(shù)實(shí)現(xiàn)的指針地址
那么我們就定義一個新函數(shù)hookCheckCodeSign
,一個函數(shù)指針*originCheckCodeSign
用來存儲原始實(shí)現(xiàn)的指針地址,實(shí)現(xiàn)起來也簡單,代碼如下:
void checkCodeSign(NSString *identifier, NSString *teamId); // 原來的方法
void (*originCheckCodeSign)(NSString *identifier, NSString *teamId); // 保留原始的方法實(shí)現(xiàn)的指針地址
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 可以獲取到,也可以在導(dǎo)出ipa包的plist中查看
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
}
void hookCheckCodeSign(NSString *identifier, NSString *teamId) {
// do nothing
NSLog(@"%s", __FUNCTION__);
}
運(yùn)行查看效果,hook成功了執(zhí)行了hook之后的函數(shù)
RuntimeLearning[8921:1837017] hookCheckCodeSign
2.3 注入的方式hook主包中的函數(shù)
我們分析別人的APP,重簽名做代碼注入的時候,顯然是沒法直接在主包的源碼中去寫hook的代碼的,可以通過動態(tài)注入的方式,注入代碼來達(dá)到修改源程序的功能
注入的思路:
- 創(chuàng)建一個動態(tài)庫編寫注入代碼
- 將動態(tài)庫注入到主APP的macho中
- 在合適的時機(jī)執(zhí)行注入的代碼;關(guān)于這個合適的時機(jī)可以利用
_objc_init
的流程來在程序初始化的時候去執(zhí)行注入的代碼邏輯
代碼注入時機(jī):
- C全局初始化方法
__attribute__((constructor(1)))
- 庫的load函數(shù)中
這些方法都會在main之前調(diào)用,可以保證我們在程序執(zhí)行檢查函數(shù)之前就將其hook掉
2.3.1 編寫注入代碼
準(zhǔn)備工作:
通過MachOView
去獲取需要hook的函數(shù)的地址(偏移地址) 0x1000164A0
新建了一個動態(tài)庫HCInject
target,然后編寫代碼:
由于安全性的需要,iOS在加載代碼鏡像到內(nèi)存的時候會隨機(jī)生成一個鏡像文件的起始地址,函數(shù)的真實(shí)地址則是這個隨機(jī)值A(chǔ)SLR+函數(shù)對于該鏡像的offset值
我這里hook的是主程序中的函數(shù),所以_dyld_get_image_vmaddr_slide(0)
的參數(shù)傳的是0
lldb
調(diào)試下也可以直接通過image list
來查看所有加載的鏡像的ASLR
最終實(shí)現(xiàn)代碼如下:
#import "HCHook.h"
#import <Dobby/dobby.h>
#import <mach-o/dyld.h>
void (*originCheckCodeSign)(NSString *identifier, NSString *teamId); // 保留原始的方法實(shí)現(xiàn)的指針地址
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函數(shù)在主程序
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文件,我習(xí)慣使用yololib
工具去修改MachO的Load Commonds
將我們寫的動態(tài)庫注入進(jìn)去,這里我直接使用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\<`
#上可執(zhí)行權(quán)限
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
菜單下添加一個執(zhí)行腳本的工作流
執(zhí)行腳本
拷貝庫文件到APP的Frameworks目錄
運(yùn)行查看效果:
嗯,成功了;至此已經(jīng)實(shí)現(xiàn)了通過注入代碼的方式去hook主程序中的C函數(shù)了
3. 總結(jié)
- 通過一個APP加固的小方案以及如何去逆向它,學(xué)習(xí)了一下Dobby工具的使用
- APP沒有絕對的安全,安全是相對的,做的一切加固的方案,只是為了增加逆向的成本