跳至主要内容

iOS 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进行安装。
  • 参考

评论

此博客中的热门博文

Resolving errSecInternalComponent errors during code signing

原文链接 One code signing issue I commonly see, both here on DevForums and in my Day Job™ with DTS, is that the codesign command fails with errSecInternalComponent. This issue crops up in a wide variety of circumstances and the correct fix depends on the specific problem. This post is my attempt to clarify the potential causes of this error and help folks resolve it. If you have any questions or comments about this, please start a new thread, tagging it with Code Signing so that I see it. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Resolving errSecInternalComponent errors during code signing In some circumstances the codesign command might fail with the error errSecInternalComponent. For example: % codesign -s "Apple Development" "MyTrue" MyTrue: errSecInternalComponent This typically affects folks who are signing code in a nonstandard environm...

iOS:检测使用VPN或Proxy

参考链接: https://www.jianshu.com/p/c3b950dbf86a https://gist.github.com/PramodJoshi/4faad4c91f7dcb4eb9b06be8390c01db http://noodlecode.net/2018/04/check-if-ios-app-is-connected-to-vpn 第一种方法 需要导入框架CFNetwork 然后,这个方法是mrc的:需要添加-fno-objc-arc的flag 代码如下: + ( BOOL )getProxyStatus { NSDictionary *proxySettings = NSMakeCollectable ([( NSDictionary *) CFNetworkCopySystemProxySettings () autorelease]); NSArray *proxies = NSMakeCollectable ([( NSArray *) CFNetworkCopyProxiesForURL (( CFURLRef )[ NSURL URLWithString: @"http://www.google.com" ], ( CFDictionaryRef )proxySettings) autorelease]); NSDictionary *settings = [proxies objectAtIndex: 0 ]; NSLog ( @"host=%@" , [settings objectForKey:( NSString *)kCFProxyHostNameKey]); NSLog ( @"port=%@" , [settings objectForKey:( NSString *)kCFProxyPortNumberKey]); NSLog ( @"type=%@" , [settings objectForKey:( NSString *)kCFProxyTypeKey]); if ([[settings object...

去广告DNS设置,国内ADGuard DNS方案,手机电脑iOS去广告,保护隐私

 原文链接 之前分享过使用mac系统搭建adguard home,这几个月用下来零零散散基本上也被弃用了。主要原因是因为需要保持电脑一直开机。但是我的电脑是笔记本,存在移动各个地域的情况,也就是说只能够屏蔽电脑自身,对于手机而言不太现实。今天偶然发现dnspod推出了高级版的公共解析。dnspod背靠腾讯云,肯定是合法合规的公共解析服务,这个高级版用起来不错。 国内自己搭建解析服务是违法行为,所以这也是为什么使用dnspod的原因。 后台截图 开始使用 首先我们先进入dnspod的公共解析页面,点击开始使用。 专业版公共解析 dnspod会提供几种预设,我们选择「开发者」即可 开发者 然后你就成功的申请到自己个人使用的dns了! 更新拦截规则 我们可以将常见的广告过滤规则加入到dns中。我们在顶部选项卡中选择「拦截规则」。 拦截规则设置 打开adguard adguard 绑定iOS设备 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 绑定macOS 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 mac需要在「系统偏好设置」的「网络」中查看是否正在运行。 代理 如果没有运行需要点击「···」来启动服务。 启动服务 绑定路由器 找到自己路由器的DHCP设置,修改dns,然后记得绑定自己的ip。 修改dns 绑定ip 费用 目前有300万次/月的免费额度,但没有超出之后的价格。300万次一个人比较难用完,可以放心使用。 我个人使用iOS设备两台、智能家居、电脑两台,日均请求数大致2万/日。 判断是否搭建成功 可以通过查看日志的方式,日志大概有半小时到一小时的延迟,请耐心等待。