App应用完整性校验:越狱检测、重签名检测,文件hash检测

App应用完整性校验

  • 大概做一下检测,以判断App是否被篡改,是否越狱等,做一些特殊处理。

  • 越狱检测

  • Mach-O文件检测

  • 重签名检测

  • 资源文件hash检测

越狱检测

  • 可以检测当前设备是否越狱,在关键性业务判断给出提示强制退出以免造成安全问题,这里的关键性业务可能是需要自己定义范围,比如牵扯到用户敏感信息等业务。
// 越狱检测
const char* jailbreak_tool_pathes[] = {
	"/Applications/Cydia.app",
	"/Library/MobileSubstrate/MobileSubstrate.dylib",
	"/bin/bash",
	"/usr/sbin/sshd",
	"/etc/apt"
};
#define ARRAY_SIZE(a) sizeof(a)/sizeof(a[0])
- (BOOL)isJailBroken
{
	if ([self isSimulator] == YES)
	{
		return NO;
	}

	for (int i=0; i<ARRAY_SIZE(jailbreak_tool_pathes); i++) {
		if ([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:jailbreak_tool_pathes[i]]]) {
			NSLog(@"The device is jail broken!");
			return YES;
		}
	}
	NSLog(@"The device is NOT jail broken!");
	return NO;
}


- (BOOL)isSimulator {
#if TARGET_OS_SIMULATOR
	return YES;
#else
	return NO;
#endif
}

Mach-O文件检测

  • 通过检测SignerIdentity判断是Mach-O文件否被篡改。原理是:SignerIdentity的值在info.plist中是不存在的,开发者不会加上去,苹果也不会,只是当ipa包被反编译后篡改文件再次打包,需要伪造SignerIdentity。所以只要被攻击篡改东西如果重新运行到手机上就会出现这个东西。
//判断Mach-O文件否被篡改
- (BOOL)checkMach_O
{
	NSBundle *bundle = [NSBundle mainBundle];
	NSDictionary *info = [bundle infoDictionary];
	if ([info objectForKey: @"SignerIdentity"] != nil) {
		//存在这个key,则说明被二次打包了
		return YES;
	}

	return NO;
}

重签名检测

  • 由于要篡改App必然重签名,至于为什么重签名,是因为苹果做了校验改动了任何东西校验失败是直接闪退的,其实原理也是校验文件的hash值。签名打包过程会出现这个embedded.mobileprovision文件,这个文件有teamID的一个东西我们可以校验是否是我们自己的团队的teamID来判断。或者判断BundleID 是否被修改。
/**
 重签名检测
@param provisionID 开发者的teamid
 */
- (BOOL)checkCodeSignWithProvisionID:(NSString *)provisionID
{
	// 描述文件路径
	NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
	if ([[NSFileManager defaultManager] fileExistsAtPath:embeddedPath]) {

		// 读取application-identifier
		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 objectAtIndex:i] rangeOfString:@"application-identifier"].location != NSNotFound) {

				NSInteger fromPosition = [[embeddedProvisioningLines objectAtIndex:i+1] rangeOfString:@"<string>"].location+8;

				NSInteger toPosition = [[embeddedProvisioningLines objectAtIndex:i+1] rangeOfString:@"</string>"].location;

				NSRange range;
				range.location = fromPosition;
				range.length = toPosition - fromPosition;

				NSString *fullIdentifier = [[embeddedProvisioningLines objectAtIndex:i+1] substringWithRange:range];

				//                NSLog(@"%@", fullIdentifier);

				NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
				NSString *appIdentifier = [identifierComponents firstObject];

				// 对比签名ID
				if (![appIdentifier isEqual:provisionID])
				{
					return NO;
				}
				else
				{
					return YES;
				}
			}
		}
	}
	return YES;
}

文件hash检测

  • 我们对Plist文件以及App 的icon资源文件做hash值校验。网上一些对_CodeSignature的CodeResources以及App二进制文件的校验做法有问题。因为Xcode打包过程不同环境造成的hash值不一样,可以看出不同环境打包过程造成的hash值不一样的选项。所以我们必须过滤掉变化的文件。检测Plist文件以及App Icon资源文件这些东西。
  • 也可以选择一些比较重要的资源进行hash校验
  • 比如启动图、icon等,可以将hash值写在代码里,启动APP进行校验
//生成资源文件名及对应的hash的字典
-(NSDictionary *)getBundleFileHash {
	NSMutableDictionary * dicHash = [NSMutableDictionary dictionary];
    // 对这些文件进行校验
	NSArray *fileArr = @[
		@"launchGIF.gif",
		@"AppIcon29x29@2x.png",
		@"AppIcon29x29@3x.png",
		@"AppIcon40x40@2x.png",
		@"AppIcon40x40@3x.png",
		@"AppIcon60x60@2x.png",
		@"AppIcon60x60@3x.png",
		@"LaunchImage-568h@2x.png",
		@"LaunchImage-700-568h@2x.png",
		@"LaunchImage-700@2x.png",
		@"LaunchImage-800-667h@2x.png",
		@"LaunchImage-800-Portrait-736h@3x.png",
		@"LaunchImage-1100-Portrait-2436h@3x.png",
		@"LaunchImage@2x.png"
	];
	for (NSString * fileName in fileArr) {
//        对应的文件生成hash
		NSString * HashString = [MD5 getMD5WithFilePath:[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName]];
		NSLog(@"%@-%@",fileName,HashString);
		if (HashString != nil) {
			[[hashDict objectForKey:fileName] isEqualToString:HashString];
		}
	}
	//所有资源文件的hash就保存在这数组里
	return dicHash;
}
  • 使用MD5生成文件hash值
#import <CommonCrypto/CommonDigest.h>

@implementation MD5
+ (NSString*)getMD5WithData:(NSData *)data {
	if (!data) {
		return nil;//判断sourceString如果为空则直接返回nil。
	}
	//需要MD5变量并且初始化
	CC_MD5_CTX md5;
	CC_MD5_Init(&md5);
	//开始加密(第一个参数:对md5变量去地址,要为该变量指向的内存空间计算好数据,第二个参数:需要计算的源数据,第三个参数:源数据的长度)
	CC_MD5_Update(&md5, data.bytes, (CC_LONG)data.length);
	//声明一个无符号的字符数组,用来盛放转换好的数据
	unsigned char result[CC_MD5_DIGEST_LENGTH];
	//将数据放入result数组
	CC_MD5_Final(result, &md5);
	//将result中的字符拼接为OC语言中的字符串,以便我们使用。
	NSMutableString *resultString = [NSMutableString string];
	for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
		[resultString appendFormat:@"%02x",result[i]];
	}
//	NSLog(@"resultString=========%@",resultString);
	return resultString;
}

// 计算文件的hash
+(NSString*)getMD5WithFilePath:(NSString*)path {
	NSData *data = [NSData dataWithContentsOfFile:path];
	return [self getMD5WithData:data];
}
@end
  • app启动后检测

// 关键资源hash值检测
- (BOOL)hasFileChanged {
	NSArray *fileArr = @[
		@"launchGIF.gif",
		@"AppIcon29x29@2x.png",
		@"AppIcon29x29@3x.png",
		@"AppIcon40x40@2x.png",
		@"AppIcon40x40@3x.png",
		@"AppIcon60x60@2x.png",
		@"AppIcon60x60@3x.png",
		@"LaunchImage-568h@2x.png",
		@"LaunchImage-700-568h@2x.png",
		@"LaunchImage-700@2x.png",
		@"LaunchImage-800-667h@2x.png",
		@"LaunchImage-800-Portrait-736h@3x.png",
		@"LaunchImage-1100-Portrait-2436h@3x.png",
		@"LaunchImage@2x.png"
	];
	// 对一些文件进行hash检验
	NSDictionary *hashDict =  @{
		@"AppIcon29x29@2x.png":@"4b9692293a06d28865f9c4575db18616",
		@"AppIcon29x29@3x.png":@"395fbb12c1d1fdc6e7ba957148b0a2c1",
		@"AppIcon40x40@2x.png" :@"ddd599c7f7bd13c4e7bcd1ad63b9b014",
		@"AppIcon40x40@3x.png" :@"a62854365df9c784fbab913cfc0d0d24",
		@"AppIcon60x60@2x.png" :@"7076eaca82975a63be2d05b194cf1490",
		@"AppIcon60x60@3x.png" :@"07be79eb6fa2d4e5ae45b8d9e9b9074e",
		@"LaunchImage-1100-Portrait-2436h@3x.png" :@"f68d3a4ef5fecb9caebfc966c43088fe",
		@"LaunchImage-568h@2x.png":@"41d02d33a9f121b6513340f38f135ee3",
		@"LaunchImage-700-568h@2x.png":@"41d02d33a9f121b6513340f38f135ee3",
		@"LaunchImage-700@2x.png" :@"8dd40fd3ae453344dcc8de70813aa1c4",
		@"LaunchImage-800-667h@2x.png" :@"515af0633b2fc6e3fe69ddd251cc9411",
		@"LaunchImage-800-Portrait-736h@3x.png" :@"72bf97143d51f377acb048db25824eaa",
		@"LaunchImage@2x.png" :@"8dd40fd3ae453344dcc8de70813aa1c4",
		@"launchGIF.gif" :@"4c7985622b2987304a9a9ab89947e91d"
	};
	BOOL changed = NO;
	for (NSString * fileName in fileArr) {
		NSString * HashString = [MD5 getMD5WithFilePath:[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName]];
		NSLog(@"%@-%@",fileName,HashString);
		if (HashString != nil) {
			if([[hashDict objectForKey:fileName] isEqualToString:HashString]) {
				changed = NO;
			}else{
            return YES;
			}
		}else{
			return YES;
		}
	}
	return changed;
}

检测时机

  • 看具体需求,可以在启动时检测一次,或者在某些需要的地方进行检测,至于是退出APP还是提示用户,看需求了。
  #pragma mark - 安全检测

	if([self isJailBroken]) {
		NSLog(@"已越狱");
		DQAlertView * alert = [[DQAlertView alloc] initWithTitle:@"温馨提示" message:@"手机已越狱,清注意安全!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"知道了", nil];
		[alert show];
	}else{
		NSLog(@"未越狱");
	}
	if([self checkMach_O]) {
		NSLog(@"二次打包");
		DQAlertView * alert = [[DQAlertView alloc] initWithTitle:@"温馨提示" message:@"APP完整性被破坏,清注意安全!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"知道了", nil];
		[alert show];
	}else{
		NSLog(@"Mach_O 正常");
	}
	if([self checkCodeSignWithProvisionID:@"L76K5AFQYD"]) {
		NSLog(@"teamid 正常");
	}else{
		NSLog(@"APP被篡改-重签名");
		DQAlertView * alert = [[DQAlertView alloc] initWithTitle:@"温馨提示" message:@"APP被篡改,清注意安全!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"知道了", nil];
		[alert show];
	}
	if([self hasFileChanged]) {
		DQAlertView * alert = [[DQAlertView alloc] initWithTitle:@"温馨提示" message:@"APP被篡改,清注意安全!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"知道了", nil];
		[alert show];
	}else{
		NSLog(@"APP资源文件正常");
	}
  • 刚刚的代码添加了,还需要自己去重签名测试

APP重新签名打包ipa

  • 安装 fastlane

  • 第一步:用Xcode新建一个工程,Bundle identifier不要和手机中已有的的APP重复,然后用自己的证书打包出ipa文件。

  • 第二步:获取mobileprovision文件。将ipa文件后缀改为zip解压

  • 第三步:安装Homebrew,安装过可跳到第五步

    • 在终端先后执行下面2命令行安装,等待进度完毕
    • xcode-select --install
    • ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • 第四步:安装ruby

    • `在终端执行下面命令安装ruby,等待进度完毕(输完密码可能在较短时间无反应)
    • brew install ruby
  • 第五步:安装sigh脚本

    • 执行下面安装命令 sudo gem install fastlane

    • 附:Sigh脚本GitHub地址。

  • 第六步:使用sigh脚本开始重新签名

    • 1、在终端输入fastlane sigh resign ipa路径,回车
      • ipa路径=>把要签名的ipa文件(路径、包名不要有中文)拖到终端窗口上,即可快速获取
    • 2、填写Signing Identity:第一步中脚本会列出电脑中的证书,选择要用的证书的SHA-1即可
      • 如:27AF89640E0F32910815581CHB8L8281C71E8EEC8。完成后回车
    • 3、把项目的配置文件.mobileprovision文件(第二步中的文件)拖到终端窗口上,回车
    • 4、好了,resign脚本会自动更改bundel id,签名并重新打包。
      • 完成后提示Successfully signed,新生成的包会自动替换原有文件。
  • 第七步:安装重签名后的ipa文件

    • 最新的iTunes已经不能给iPhone安装APP了,所以我们可以使用各种助手或者iTools进行安装。
  • 参考

posted @ 2022-03-17 21:48  struggle_time  阅读(2237)  评论(0编辑  收藏  举报