Logo

在iOS Simulator上运行拆壳的ipa应用

September 5, 2025
已知:编译时虚拟机用的target是arm64-apple-ios-simulator,真机是arm64-apple-ios。那么他们有什么区别?
即答:没区别。

于是就有了这篇文章。

对于一个脱壳下来的ipa包,他就是一个纯粹的zip。所以第一步就是直接unzip。

结构如下:Payload/(Target.app)/…

然后可以通过vtool看到,这个包里的二进制都是给iOS使用的,

$ vtool -arch arm64 -show Payload/Twitter.app/Twitter
Payload/Twitter.app/Twitter (architecture arm64):
Load command 11
      cmd LC_BUILD_VERSION
  cmdsize 32
 platform IOS
    minos 15.0
      sdk 18.1
   ntools 1
     tool LD
  version 1115.7.3
Load command 12
      cmd LC_SOURCE_VERSION
  cmdsize 16
  version 0.0

这是一个给最小15.0的iOS使用的包。

同样我们也能发现vtool里有个命令:vtool -arch arm64 -set-build-version iossim 17.0 17.0 -replace -output xxx xxx

于是尝试一下:

$ vtool -arch arm64 -set-build-version iossim 17.0 17.0 -replace -output Payload/Twitter.app/Twitter Payload/Twitter.app/Twitter
/Applications/Xcode-16.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/vtool warning: code signature will be invalid for Payload/Twitter.app/Twitter
$ vtool -arch arm64 -show Payload/Twitter.app/Twitter
Payload/Twitter.app/Twitter (architecture arm64):
Load command 11
      cmd LC_SOURCE_VERSION
  cmdsize 16
  version 0.0
Load command 94
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform IOSSIMULATOR
    minos 17.0
      sdk 17.0
   ntools 0

计划通。可以看到这里变成了最小17.0,使用17.0sdk的给模拟器使用的包。

理论上,我们只需要把所有的满足条件的Mach-O全部换掉即可?

于是成功运行了,吗?

如果你直接运行,会闪退的。原因是你有mobile provisioning文件和一个烂掉的签名。

于是下一步是删掉签名,重新签名。并且比较有意思的是,Simulator并不限制签名,所以ad-hoc即可使用(所有权限!包括本来需要entitlements才能用的)

$ codesign --remove file 
$ codesign -s - -- force --deep file

这一步需要对每一个二进制,以及外面的包(app文件夹)做。

做完了之后用xcrun simctl 安装上就好了。搓了一个python脚本。

import sys
import os
import subprocess
import shutil
import tempfile

def system(cmd):
    ret = os.system(cmd)
    if ret != 0:
        print(f"[-] Command failed: {cmd}")
        sys.exit(1)

def get_booted_simulator_info():
    """Get UDID and version of currently booted iOS simulator"""
    try:
        result = subprocess.run(['xcrun', 'simctl', 'list'], capture_output=True, text=True, check=True)
        lines = result.stdout.split('\n')
        
        current_version = None
        
        # Parse the output to find version sections and booted devices
        for i, line in enumerate(lines):
            # Track current iOS version section
            if line.startswith('-- iOS '):
                version_match = line.split('-- iOS ')[1].split(' --')[0]
                current_version = version_match
                continue
            
            # Look for booted device in current version section
            if 'Booted' in line and current_version:
                # Extract device name and UDID
                line = line.strip()
                device_name = line.split(' (')[0]
                
                # Extract UDID from line like "iPhone 15 Pro (12345678-1234-1234-1234-123456789012) (Booted)"
                start = line.find('(') + 1
                end = line.find(')', start)
                if start > 0 and end > start:
                    udid = line[start:end]
                    return {
                        'udid': udid,
                        'device_name': device_name,
                        'ios_version': current_version
                    }
        
        return None
    except Exception as e:
        print(f"[!] Error getting booted simulator: {e}")
        return None

def resign(file_path):
    """Resign a Mach-O file using codesign"""
    try:
        subprocess.run(['codesign', '--remove', file_path], stderr=subprocess.DEVNULL)
        subprocess.run(['codesign', '-s', '-', '--force', '--deep', file_path], check=True)
        os.chmod(file_path, 0o777)
    except Exception as e:
        print(f"[!] Error resigning {file_path}: {e}")

def get_import_libs(file_path):
    """Get list of imported libraries from a Mach-O file using otool"""
    try:
        result = subprocess.run(['otool', '-L', file_path], capture_output=True, text=True, check=True)
        lines = result.stdout.split('\n')[1:]  # Skip the first line which is the file name
        libs = []
        for line in lines:
            line = line.strip()
            if line:
                lib_name = line.split(' (')[0]
                libs.append(lib_name)
        return libs
    except Exception as e:
        print(f"[!] Error getting import libs for {file_path}: {e}")
        return []

def main():
    if len(sys.argv) not in [2, 3]:
        print("Usage: python install.py <filename> [udid]")
        sys.exit(1)
    
    # Create temporary working directory
    temp_dir = tempfile.mkdtemp()
    working_location = temp_dir
    # working_location = "."
    original_cwd = os.getcwd()
    
    print(f"[i] working location: {working_location}")
    
    # Change to temporary directory
    os.chdir(working_location)
    
    target_app = sys.argv[1]
    
    # Get UDID: use provided one or auto-detect booted simulator
    if len(sys.argv) == 3:
        udid = sys.argv[2]
        print(f"[i] using provided UDID: {udid}")
    else:
        print("[*] detecting booted iOS simulator...")
        simulator_info = get_booted_simulator_info()
        if not simulator_info:
            print("[!] No booted iOS simulator found. Please boot a simulator first or provide UDID manually.")
            sys.exit(1)
        
        udid = simulator_info['udid']
        print(f"[i] found booted simulator: {simulator_info['device_name']}")
        print(f"[i] iOS version: {simulator_info['ios_version']}")
        print(f"[i] UDID: {udid}")
    
    # Convert to absolute path if it's relative
    if not os.path.isabs(target_app):
        target_app = os.path.join(original_cwd, target_app)
    
    print(f"[i] target device UDID: {udid}")
    
    # Handle IPA file extraction
    if target_app.endswith('.ipa'):
        print("[*] extracting application bundle from ipa file")
        system(f"unzip -qq '{target_app}'")
        
        # Find .app file in Payload directory
        payload_files = os.listdir('Payload')
        app_files = [f for f in payload_files if f.endswith('.app')]
        
        if len(app_files) == 0:
            print("ERROR: no .app file found in ipa")
            sys.exit(1)
        elif len(app_files) > 1:
            print("ERROR: multiple .app files found in ipa file")
            sys.exit(1)
        
        # Copy app bundle and clean up
        shutil.copytree(f'Payload/{app_files[0]}', app_files[0])
        shutil.rmtree('Payload')
        target_app = app_files[0]
        
        print("[*] application bundle extracted")
        print(f"[*] app bundle will be set to: {target_app}")
    
    # Verify target app exists
    if not os.path.isdir(target_app):
        print("[E] please specify a valid application bundle location")
        print("    usage: <application_bundle_location>/<ipa_file_location>")
        sys.exit(1)
    
    print("[*] preparing environment...")
    
    # Remove quarantine attributes and clean up
    system(f"xattr -r -d com.apple.quarantine '{target_app}' 2>/dev/null || true")
    
    files_to_remove = [
        f"{target_app}/embedded.mobileprovision",
        f"{target_app}/_CodeSignature",
        f"{target_app}/PlugIns",
        f"{target_app}/SC_Info"
    ]
    
    for file_path in files_to_remove:
        if os.path.exists(file_path):
            print(f"[*] removing {file_path}...")
            if os.path.isdir(file_path):
                shutil.rmtree(file_path)
            else:
                os.remove(file_path)
    
    print(f"[i] will make patch directly inside {target_app}")
    print("[*] scanning files...")
    
    # Process Mach-O files
    processed = 0
    for root, _, files in os.walk(target_app):
        for file in files:
            file_path = os.path.join(root, file)
            if os.path.isfile(file_path):
                # Check if file is Mach-O
                try:
                    file_output = subprocess.run(['file', file_path], capture_output=True, text=True)
                    if 'Mach-O' in file_output.stdout:
                        print(f"[*] processing {file_path}...")
                        
                        # Use vtool to modify build version
                        # system(f"vtool -arch arm64e -set-build-version iossim 17.0 17.0 -replace -output '{file_path}' '{file_path}'")
                        subprocess.run(['vtool', '-arch', 'arm64', '-set-build-version', 'iossim', '17.0', '17.0', '-replace', '-output', file_path, file_path], check=True)
                        resign(file_path)
                        os.chmod(file_path, 0o777)
                        
                        processed += 1
                except Exception as e:
                    print(f"[!] Error processing {file_path}: {e}")
                    continue
    
    if processed == 0:
        print("[!] no mach object was found nor processed")
        sys.exit(1)
    
    print(f"[i] patch was made to {processed} files")
    # Remove import of .prelib use otool
    print("[*] removing .prelib imports...")
    system(f"install_name_tool -change @loader_path/.prelib /usr/lib/libSystem.B.dylib '{target_app}/{target_app.split(".app")[0]}' || true")
    resign(f"{target_app}/{target_app.split(".app")[0]}")
    # system(f"codesign --remove '{target_app}/{target_app.split('.app')[0]}' 2>/dev/null || true")
    # system(f"codesign -s - --force --deep '{target_app}/{target_app.split('.app')[0]}'")
    # Final codesign of the entire app bundle
    system(f"codesign -s - --force --deep '{target_app}'")
    
    print("[*] verify code sign...")
    system(f"codesign --verify --deep '{target_app}'")
    
    # Get bundle ID for app installation
    try:
        bundle_id_output = subprocess.run(
            ["plutil", "-extract", "CFBundleIdentifier", "xml1", "-o", "-", f"{target_app}/Info.plist"],
            capture_output=True, text=True, check=True
        )
        bundle_id = bundle_id_output.stdout.strip()
        if "<string>" in bundle_id and "</string>" in bundle_id:
            bundle_id = bundle_id.split("<string>")[1].split("</string>")[0]
        print(f"[+] Bundle ID: {bundle_id}")
    except Exception as e:
        print(f"[!] Could not extract bundle ID: {e}")
        bundle_id = None
    
    # Install app to simulator
    print(f"[*] installing app from {target_app}")
    # cp temp to current path
    system(f"xcrun simctl install {udid} '{target_app}'")
    
    # Grant permissions if bundle ID was extracted successfully
    if bundle_id:
        print(f"[*] granting permissions to {bundle_id}")
        system(f"xcrun simctl privacy {udid} grant all '{bundle_id}'")
    else:
        print("[!] Skipping permission grant due to missing bundle ID")
    
    print("[*] done")
    
    # Clean up temporary directory
    os.chdir(original_cwd)
    try:
        shutil.rmtree(working_location)
        print(f"[i] cleaned up temporary directory: {working_location}")
    except Exception as e:
        print(f"[!] Failed to clean up temporary directory: {e}")

if __name__ == "__main__":
    main()

全剧终。

当然如果你想问有些程序为什么运行不了可以别问我…因为我也有很多程序运行不了…想要直接查看日志的话,可以这样看:

$ xcrun simctl launch --console booted bundleId

然后就可以看到报错了

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。