iOS安全尝试记录
iOS安全尝试记录
防止IPA包被二次打包
Mach-O文件检测
通过检测SignerIdentity判断是Mach-O文件否被篡改。
原理是:SignerIdentity的值在info.plist中是不存在的,开发者不会加上去,苹果也不会,只是当ipa包被反编译后篡改文件再次打包,需要伪造SignerIdentity。所以只要被攻击篡改东西如果重新运行到手机上就会出现这个东西。
public static func isSecondIPA() -> Bool {
let bundle = Bundle.main
let info = bundle.infoDictionary
if info?["SignerIdentity"] != nil {
return true
}
return false
}
embedded.mobileprovision
校验
embeded.mobileprovision文件会在上架appStore后被苹果删除,因为该文件中包含了证书相关信息,ipa包上传到appstore后,苹果会验证证书的合法性,验证通过后会删除该文件,并对ipa包进行重签后上架appstore,这样才能保证开发者自己的证书过期后依然可以通过appstore进行下载。
但是如果ipa包被越狱手机二次签名后,embeded.mobileprovision文件会重新生成,这时则可以监测到文件是否被篡改来判断是否被二次签名。
embedded文件的hash校验
通过上架app store下载的应用中,没有这个文件,不能通过这个方式校验
if let embeddedPath = Bundle.main.path(forResource: "_CodeSignature/CodeResources", ofType: ""),
let dic = NSMutableDictionary.init(contentsOfFile: embeddedPath),let files = dic["files"] as? [String:Any],let hashData = files["embedded.mobileprovision"] as? Data{
let hashValue = hashData.base64EncodedString()
let localHashValue = getHashValue()
if hashValue != localHashValue{//文件被篡改,退出
Toast.showInfo(content: "检测到非法签名,程序即将退出",duration: 3)
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
exit(0) // (1)
}
}
}
embedded文件是否存在
如果存在embedded文件,意味着是二次打包
/// 检查是否二次签名
///
public static func checkCodeSign(_ provisionID: String) -> Bool {
// 描述文件路径
let embededPath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision")
guard let embededPath = embededPath,
FileManager.default.fileExists(atPath: embededPath) == true
else {
// 如果文件不存在,可能非二次签名
return false
}
// 读取application-identifier
guard let embeddedProvisioning = try? String.init(contentsOfFile: embededPath, encoding: .ascii) else {
return true
}
let embeddedProvisioningLines = embeddedProvisioning.components(separatedBy: CharacterSet.newlines)
for i in 0..<embeddedProvisioningLines.count {
let emStr = embeddedProvisioningLines[i]
guard let index = emStr.range(of: "application-identifier")?.lowerBound,
index.hashValue != NSNotFound
else {
continue
}
let positionStr = embeddedProvisioningLines[i + 1]
let fromPosition = positionStr.range(of: "<string>")?.upperBound ?? positionStr.startIndex
let toPosition = positionStr.range(of: "</string>")?.lowerBound ?? positionStr.endIndex
let range = Range(uncheckedBounds: (fromPosition, toPosition))
let fullIdentifier = positionStr[range]
let identifierComponents = fullIdentifier.components(separatedBy: ".")
let appIdentifier = identifierComponents.first
// 对比签名ID
if appIdentifier == provisionID {
// teamid一致,非二次签名
return false
} else {
return true
}
}
// 未检查到异常
return false
}
总结embedded.mobileprovision文件存在的情况:
不存在该文件:App Store下载的IPA、Cydia商店(越狱手机上的)下载的IPA。
存在该文件:Xcode打出来的IPA、企业证书分发的IPA、越狱手机上自己二次打包的IPA。
文件hash检测
通过对比文件的hash值,判断当前程序内的文件是否被篡改。
部分文章说,检测不同环境下不会变化的文件作为检测的文件。比如启动图、icon等,可以将hash值写在代码里,启动APP进行校验
但是我没有想到,为什么逆向工程的人员需要去改启动图。
越狱检测
通过检测是否存在越狱的文件、应用、是否可以打开越狱的app、是否可以直接访问其他文件等方式判断当前是否已经越狱
绝大部分app都没有对越狱进行校验
public static func isJailbreak() -> Bool {
guard TARGET_OS_SIMULATOR == 0 else {
return false
}
// 越狱检查
// CanOpenURL检查
let fakeURL = URL(string: "cydia://package/com.fake.package")
if UIApplication.shared.canOpenURL(fakeURL!) == true {
return true
}
// Cydia应用检查
var stat_info: stat
// 使用stat系列函数检测Cydia等工具
if stat("/Applications/Cydia.app", &stat_info) == 0 {
return true
}
// fork函数检查
// if fork()>=0 {
// return true
// }
// 注入动态库检查
if let env = getenv("DYLD_INSERT_LIBRARIES") {
return true
}
// 无法访问文件检查
let files = ["/Applications/Cydia.app",
"/Applications/limera1n.app",
"/Applications/greenpois0n.app",
"/Applications/blackra1n.app",
"/Applications/blacksn0w.app",
"/Applications/redsn0w.app",
"/Applications/Absinthe.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/"]
for fileName in files {
if FileManager.default.fileExists(atPath: fileName) {
return true
}
}
return false
}
反调试检测
通过检测调试的进程实现。发现pctrack调试进程,则结束程序
+ (void)antiDebug {
gx_disable_gdb();
gx_AntiDebugASM();
gx_AntiDebug_isatty();
}
/// ptrace反调试,阻止GDB依附
typedef int (*ptrace_ptr_t)(int _request, pid_t pid, caddr_t _addr, int _data);
#if !defined(PT_DENT_ATTACH)
#define PT_DENT_ATTACH 31
#endif
void gx_disable_gdb() {
void * handle = dlopen(0, RTLD_GLOBAL|RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENT_ATTACH, 0, 0, 0);
dlclose(handle);
}
/// 内联 svc + ptrace 实现和内联 svc + syscall + ptrace 实现
// 使用inline方式将函数在调用处强制展开,防止被hook和追踪符号
__attribute__((always_inline)) void gx_AntiDebugASM() {
#ifdef __arm__
asm volatile(
"mov r0,#31\n"
"mov r1,#0\n"
"mov r2,#0\n"
"mov r12,#26\n"
"svc #80\n"
);
#endif
#ifdef __arm64__
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n"
"svc #128\n"
);
#endif
}
/// isatty检测是否正在被调试
//需要头文件#include <unistd.h>
void gx_AntiDebug_isatty() {
if (isatty(1)) {
_exit(1);
} else {
}
}
代码混淆
通过命令工具将静态代码做重命名混淆
代理检测 - 防止抓包
通过访问baidu,判断是否有MITM
+ (BOOL)getProxyStatus {
NSDictionary *proxySettings = (__bridge NSDictionary *)(CFNetworkCopySystemProxySettings());
NSArray *proxies = (__bridge NSArray *)(CFNetworkCopyProxiesForURL((__bridge CFURLRef _Nonnull)([NSURL URLWithString:@"http://www.baidu.com"]), (__bridge CFDictionaryRef _Nonnull)(proxySettings)));
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 objectForKey:(NSString *)kCFProxyTypeKey] isEqualToString:@"kCFProxyTypeNone"]){
//没有设置代理
return NO;
}else{
//设置代理了
return YES;
}
}
参考
字符串替换