在软件开发的今天,特别是在逆向工程这一领域,对软件包进行重签名检测和反检测,无疑是一项既具挑战性又充满乐趣的任务。一方面,这是为了确保软件的安全性而设计的检测系统;另一方面,逆向工程师们则不断尝试各种反检测方法。这两者之间的较量,无疑非常引人入胜。
包重签名检测原理
检查包内是否含有特定符号,比如“.”,这样的动作算是一种特别的检查起点。接着,分析描述文件中的“-”,通过比较签名数据来确认包是否被重新签名。在软件开发过程中,开发人员、调试员和逆向工程师在处理重签名时,都会用到描述文件。这种检测方法就像是一道门禁,只让拥有特定签名的“访客”进入。此外,相关代码编写简洁明了,遵循着读取文件、获取签名信息、进行比对等几个基本步骤,一旦发现信息不匹配,程序便会立即停止运行。
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,#0n"
"mov w16,#1n"
"svc #0x80"
);
}
break;
}
}
#endif
}
在具体的项目操作中,尤其是中小型的iOS应用开发项目,若缺乏此类检测手段,将面临众多安全威胁。这些威胁可能包括公司知识产权遭受侵犯,用户隐私数据泄露等问题。因此,建立有效的检测机制显得尤为关键。
检测函数被hook的风险
检测函数种类繁多,相应地,其hook方法各异,这也给检测工作带来了潜在风险。例如,对于静态的C语言函数,若它们不涉及动态符号且未采用OC消息机制,常规的hook方法就不再适用。此时,我们只能借助特定的工具,如Dobby,来进行静态hook。若这些检测函数轻易被hook,那就像城堡的大门被不法之徒悄无声息地找到钥匙打开,相当危险。
换个角度来想,若检测函数遭受了hook,那么原本稳固的安全防线便如同破碎的墙垣,软件的整体安全性遭受了严重破坏。这种情况不仅会损害软件厂商的利益,还可能威胁到成千上万用户的数据安全。
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
Dobby静态hook的应用
Dobby工具对静态hook操作,特别是在处理OC函数和动态库中的C方法时,其原理较为直观,故无需详述。然而,对于某些特定的静态C函数,Dobby工具则展现出其独特价值。我们能够利用它,在特定项目中实施复杂的hook任务。比如,在某个与金融信息保护相关的iOS应用开发过程中,若需对特定检测函数实施静态hook,Dobby便成为一款出色的选择。
int DobbyHook(void *function_address, void *replace_call, void **origin_call);
实际操作中,要用好Dobby,必须按照既定步骤进行,这和操作精密仪器一样,要求非常严格。必须精确调整各项参数,以防操作不当造成钩子失效或触发其他系统风险。
编译生成包的注意事项
在编译生成包的过程中,正确操作同样不可忽视。我们可以模仿“WithiOS”的步骤创建一个工程,并使用Xcode进行编译。然而,在这个过程中,必须特别留意“cmake”命令的执行目录。比如,若存在子目录,直接在build目录下执行“cmake..”可能无法成功。以我自己的操作为例,多了一层子目录,这时必须将“cmake..”改为“cmake../..”,以确保正确定位到cmake配置目录。这虽是小事,但若疏忽,可能导致编译过程中出现错误,进而影响后续步骤。
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__);
}
在众多开发任务中,新手开发者往往因忽视这些小细节,耗费不少时间寻找错误,这无疑会减缓项目的推进速度。
函数指针存储原始实现
RuntimeLearning[8921:1837017] hookCheckCodeSign
我们可以设定一个新函数以及一个函数指针,以保存原始实现代码的指针位置。这个过程在编写代码时并不繁琐。尽管这个操作听起来较为复杂,但通过查看具体的代码示例,我们便能较为直观地理解它。这一做法相当于为原始函数配备了一个“备用方案”,在特定条件下,它能够对原始函数进行修复或替换等操作。
在复杂的软件逻辑里,若缺乏对原始实现指针地址的存储操作,一旦目标函数遭到改动或损坏,想要恢复其原始功能将会极其不易。
注入方式hook主包函数
在研究他人APP时,若要进行重签名和代码注入,会发现主包源码中无法直接编写hook代码。这时,动态注入技术变得尤为关键。这种技术能够修改主程序的MachO文件,并将我们编写的动态库嵌入其中。以获取目标函数地址为例,由于iOS的安全性措施,如ASLR可能导致函数地址发生偏移,因此必须准确计算出函数的真实地址,才能确保注入的正确性。
在APP逆向分析的实际操作中,这种注入手段能让我们轻松突破众多安全防护,从而更深入地审视程序功能,或发掘潜在的安全风险。
在此,我想向众多读者请教,在你们进行软件反向工程或进行安全防御的过程中,是否遇到过什么有趣的故事?期待大家的留言、点赞以及文章的转发。
#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