SkillAgentSearch skills...

IOSAppHook

专注于非越狱环境下iOS应用逆向研究,从dylib注入,应用重签名到App Hook

Install / Use

/learn @Urinx/IOSAppHook
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

iOSAppHook star this repo fork this repo platform download

专注于非越狱环境下iOS应用逆向研究,从dylib注入,应用重签名到App Hook。

注意!本文所有操作均在以下环境下成功进行,不同平台或环境可能存在某些问题,欢迎大家在issue中提出问题以及相互讨论。

Mac OS X 10.11.6 (15G12a) - macOS Sierra 10.12.4 (16E144f)<br> Xcode 7.3.1 (7D1014) - 8.1 (8B62) <br> iPhone 5s, iOS 9.3.3 (13G21) - iOS 10.3 (14E5230e) <br> 免费开发者账号 <br> 示例App:微信 6.3.19.18 - 6.5.4

目录

前言

提到非越狱环境下App Hook大家早就已经耳熟能详,已经有很多大神研究过,这方面相关的资料和文章也能搜到很多。我最早是看到乌云知识库上蒸米的文章才对这方面有所了解,当时就想试试,整个过程看似简单(大神总是一笔带过),然而当自己真正开始动手时一路上遇到各种问题(一脸懵逼),在iOSRE论坛上也看到大家遇到的各种问题,其实阻扰大家的主要是一些环境的搭建以及相关配置没设置好,结果导致dylib编译过程各种错误,重签名不成功,各种闪退等。所以本文里的每一步操作都会很详细的交代,确保大家都能操作成功。

应用脱壳

我们知道,App Store里的应用都是加密了的,直接拿上来撸是不正确的,所以在此之前一般会有这么一个砸壳的过程,其中用到的砸壳工具就是dumpdecrypted,其原理是让app预先加载一个解密的dumpdecrypted.dylib,然后在程序运行后,将代码动态解密,最后在内存中dump出来整个程序。然而砸壳是在越狱的环境下进行的,鉴于本文主要关注点在非越狱环境下,再者我手里也没有越狱设备(有就不会这么蛋疼了)。

所以这里我们选择的是直接从PP助手等各种xx助手里面下载,注意的是这里下载的是越狱应用(不是正版应用),也就是所谓的脱过壳的应用。

为了谨慎起见,这里我们还需要确认一下从xx助手里下载的应用是否已解密,毕竟有好多应用是只有部分架构被解密,还有就是Watch App以及一些扩展依然加密了,所以最好还是确认一下,否则的话,就算hook成功,签名成功,安装成功,app还是会闪退。

首先,找到应用对应的二进制文件,查看包含哪些架构:

> file WeChat.app/WeChat
WeChat.app/WeChat: Mach-O universal binary with 2 architectures
WeChat.app/WeChat (for architecture armv7):	Mach-O executable arm
WeChat.app/WeChat (for architecture arm64):	Mach-O 64-bit executable

可以看到微信包含两个架构,armv7和arm64。关于架构与设备之间的对应关系可以从iOS Support Matrix上查看。理论上只要把最老的架构解密就可以了,因为新的cpu会兼容老的架构。

otool可以输出app的load commands,然后通过查看cryptid这个标志位来判断app是否被加密。1代表加密了,0代表被解密了:

> otool -l WeChat.app/WeChat | grep -B 2 crypt
          cmd LC_ENCRYPTION_INFO
      cmdsize 20
     cryptoff 16384
    cryptsize 40534016
      cryptid 0
--
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 43663360
      cryptid 0

可以看到微信已经被解密了,第一个对应的是较老的armv7架构,后者则是arm64架构。

鉴于微信是一个多targets的应用,包含一个Watch App和一个分享扩展。所以同理,我们还需要依次确认以下二进制文件,这里就跳过了。

WeChat.app/Watch/WeChatWatchNative.app/WeChatWatchNative
WeChat.app/Watch/WeChatWatchNative.app/PlugIns/WeChatWatchNativeExtension.appex/WeChatWatchNativeExtension
WeChat.app/PlugIns/WeChatShareExtensionNew.appex/WeChatShareExtensionNew

应用重签名

在第二部分我们将要进行的是应用重签名,注意这里并不是按照整个操作流程的顺序来讲,而是跳过编译dylib,因为我觉得如果现在你没有把重签名拿下的话,写tweak写hook都是白搭。所以从这里在开始,我们不需要对App进行任何修改和处理,仅仅对其进行重签名,然后将其安装到设备上能够正常的运行。再次提醒的是重签名用的是脱壳后的App,加密的App重签名成功安到设备上也会闪退。

关于iOS应用重签名的文章有很多,签名方法都是一样,个别地方会有些小出入,然后按照里面给出的步骤手动一步一步的来操作,不知道是由于免费的开发者证书的原因还是哪一步漏掉了或者是什么其它的原因,总之就是不成功。就在一筹莫展的时候,偶然发现了一个名为iOS App Signer的Mac上的应用,这款重签名工具能够用免费的开发者账号重签名应用。我试了下,用这个工具重签名了一个应用,并且成功安装到手机上,尽管打开时闪退,但至少总算能安装到设备上去了。

看到签名成功心里一阵高兴,并且由于该应用开源,于是就到Github上阅读源码看其具体的实现。该应用是用Swift语音所写,代码量也不多,阅读起来没有问题。由于整个操作都是在终端下进行的,从终端到图形界面来回切换实在是麻烦,所以在其代码基础上稍作修改写了一个Command Line Tool工具在命令行下使用。

下面就来具体交代下这个重签名工具到底做了什么。

1. 获取到本地上的开发者签名证书和所有的Provisioning文件

获取本机上的Provisioning文件主要是调用了populateProvisioningProfiles()方法,该方法调用了ProvisioningProfile.getProfiles()将结果封装到ProvisioningProfile结构体中以数组的形式返回,并对其结果做了一个刷选,去掉了那些过期的证书。

struct ProvisioningProfile {
    ...
    static func getProfiles() -> [ProvisioningProfile] {
        var output: [ProvisioningProfile] = []
        
        let fileManager = NSFileManager()
        if let libraryDirectory = fileManager.URLsForDirectory(.LibraryDirectory, inDomains: .UserDomainMask).first, libraryPath = libraryDirectory.path {
            let provisioningProfilesPath = libraryPath.stringByAppendingPathComponent("MobileDevice/Provisioning Profiles") as NSString
            
            if let provisioningProfiles = try? fileManager.contentsOfDirectoryAtPath(provisioningProfilesPath as String) {
                for provFile in provisioningProfiles {
                    if provFile.pathExtension == "mobileprovision" {
                        let profileFilename = provisioningProfilesPath.stringByAppendingPathComponent(provFile)
                        if let profile = ProvisioningProfile(filename: profileFilename) {
                            output.append(profile)
                        }
                    }
                }
            }
        }
        return output
    }
    ...
}

简单点用shell表示就是,先列出所有的~/Library/MobileDevice/Provisioning Profiles路径下所有的后缀为.mobileprovision的文件,然后依次获取文件的信息(plist格式)。

security cms -D -i "~/Library/MobileDevice/Provisioning Profiles/xxx.mobileprovision"

获取开发者签名证书:

func populateCodesigningCerts() {
    var output: [String] = []
    
    let securityResult = NSTask().execute(securityPath, workingDirectory: nil, arguments: ["find-identity","-v","-p","codesigning"])
    if securityResult.output.characters.count >= 1 {
        let rawResult = securityResult.output.componentsSeparatedByString("\"")
        
        for index in 0.stride(through: rawResult.count - 2, by: 2) {
            if !(rawResult.count - 1 < index + 1) {
                output.append(rawResult[index+1])
            }
        }
    }
    self.codesigningCerts = output
    
    Log("Found \(output.count) Codesigning Certificates")
}

以上代码相当于:

> security find-identity -v -p codesigning
1) 1234567890123456789012345678901234567890 "iPhone Developer: XXX (xxxxxxxxxx)"
2) 1234567890123456789012345678901234567890 "Mac Developer: XXX (xxxxxxxxxx)"

2. 一些准备工作

创建临时目录,makeTempFolder()方法:

> mktemp -d -t com.eular.test
/var/folders/qr/8_n21zhd4f993khcsh_qll000000gp/T/com.eular.test.6aHPpdBZ

处理不同格式的输入文件,包括debipaappxcarchive

deb -> ar -x xxx.deb -> tar -xf xxx.tar -> mv Applications/ -> Payload/
ipa -> unzip -> Payload/
app -> copy -> Payload/
xcarchive -> copy .xcarchive/Products/Applications/ -> Payload/

3. 重签名

首先,复制provisioning文件到app目录里:

cp xxx.mobileprovision Payload/xxx.app/embedded.mobileprovision

根据provisioning文件导出entitlements.plist:

if provisioningFile != nil || useAppBundleProfile {
    print("Parsing entitlements")
                    
    if let profile = ProvisioningProfile(filename: useAppBundleProfile ? appBundleProvisioningFilePath : provisioningFile!){
        if let entitlements = profile.getEntitlementsPlist(tempFolder) {
            Log("–––––––––––––––––––––––\n\(entitlements)")
            Log("–––––––––––––––––––––––")
            do {
                try entitlements.writeToFile(entitlementsPlist, atomically: false, encoding: NSUTF8StringEncoding)
                Log("Saved entitlements to \(entitlementsPlist)")
            } catch let error as NSError {
                Log("Error writing entitlements.plist, \(error.localizedDescription)")
            }
        } else {
            Log("Unable to read entitlements from provisioning profile")
            warnings += 1
        }
        if profile.appID != "*" && (newApplicationID != "" && newApplicationID != profile.appID) {
            Log("Unable to change App ID to \(newApplicationID), provisioning profile won't allow it")
            cleanup(tempFolder); return
        }
    } else {
        Log("Unable to parse provisioning profile, it may be corrupt")
        warnings += 1
    }
    
}

修复可执行文件的权限:

if let bundleExecutable = getPlistKey(appBundleInfoPlist, keyName: "CFBundleExecutable"){
    NSTask().execute(chmodPath, workingDirectory: nil, arguments: ["755", appBundlePath.stringByAppendingPathComponent(bundleExecutable)])
}

替换所有Info.plist里的CFBundleIdentifier

if let oldAppID = getPlistKey(appBundleInfoPlist, keyName: "CFBundleIdentifier") {
                        
    func changeAppexID(appexFile: String){
        
        func shortName(file: String, payloadDirectory: String) -> String {
            return file.substringFromIndex(payloadDirectory.endIndex)
        }
        
        let appexPlist = appexFile.stringByAppendingPathComponent("Info.plist")
        if let appexBundleID = getPlistKey(appexPlist, keyName: "CFBundleIdentifier"){
            let newAppexID = "\(newApplicationID)\(appexBundleID.substringFromIndex(oldAppID.endIndex))"
            print("Changing \(shortName(appexFile, payloadDirectory: payloadDirectory)) id to \(newAppexID)")
            
            setPlistKey(appexPlist, keyName: "CFBundleIdentifier", value: newAppexID)
        }
        if NSTask().execute(defaultsPath, workingDirectory: nil, arguments: ["read", appexPlist,"WKCompanionAppBundleIdentifier"]).status == 0 {
            setPlistKey(appexPlist, keyName: "WKCompanionAppBundleIdentifier", value: newApplicationID)
        }
        recursiveDirectorySearch(appexFile, extensions: ["app"], found: changeAppexID)
    }
    
    recursiveDirectorySearch(appBundlePath, extensions: ["appex"], found: changeAppexID)
}
...

然后对所有的dylibso0vispvrframeworkappexapp以及egg文件用codesign命令进行签名。代码略长,此处不写。

4. 打包

最后将上述目录用zip打包成ipa文件就可以了。

5. 安装ipa文件

这里用到的是mobiledevice工具,执行下列命令安装ipa文件到手机上:

./mobiledevice install_app xxx.ipa

当然,你也可以使用ideviceinstaller工具。

./ideviceinstaller -i xxx.ipa

总之,上述步骤较多,主要集中在前4个步骤上,不建议自己操作,你可以选择使用图形界面的iOS App Signer应用,也可以使用我提供的根据其开源代码写的命令行工具,AppResign,你可以直接下载编译好的二进制文件

使用方法

Related Skills

View on GitHub
GitHub Stars2.5k
CategoryDevelopment
Updated9d ago
Forks452

Languages

Swift

Security Score

100/100

Audited on Mar 17, 2026

No findings