基于Objection的跨平台移动安全测试脚本实战指南
1. 项目概述为什么我们需要一套跨平台的移动安全测试脚本在移动应用安全测试的日常工作中一个让很多安全研究员和渗透测试工程师头疼的问题是iOS和Android是两个截然不同的世界。iOS有它封闭的沙盒、严格的代码签名和独特的运行时环境Android则以其开放性、碎片化的系统和多样的API版本著称。这意味着当我们拿到一个需要同时测试iOS和Android版本的应用时往往需要准备两套完全不同的工具链、命令集和测试思路。你可能刚在Android上用Frida hook完一个Java方法切换到iOS时就得重新研究Objective-C的Runtime或者Swift的objc动态派发机制不仅效率低下还容易在切换中遗漏测试点。这就是“跨平台移动安全测试”这个命题的核心价值所在。它追求的并非一个能“通吃”所有底层细节的万能工具——这在技术上几乎不可能——而是希望建立一套统一的、高层的操作抽象层和工作流。通过这套工作流测试人员可以用相似的逻辑和命令去完成在两个平台上本质相同但实现各异的安全测试任务比如动态插桩、内存搜索、数据存储分析和网络流量拦截。而objection正是实现这一愿景的绝佳起点。它基于强大的动态插桩框架Frida但提供了一个更友好、更统一的命令行界面。然而原生的objection虽然统一了入口但在实际针对iOS和Android的深入测试时其命令参数、模块功能依然存在差异直接编写“一套脚本”往往会遇到兼容性问题。因此本实战指南的核心就是分享如何基于objection通过封装、适配和条件判断打造一套真正能“一次编写双端运行”的自动化安全测试脚本从而将测试效率提升一倍并保证测试覆盖的一致性。2. 核心思路与架构设计抽象共同点隔离差异点要设计一套跨平台的脚本不能蛮干地试图用同一行代码去调用两个平台的不同底层机制。正确的思路是“分而治之”进行合理的架构设计。2.1 识别跨平台测试的通用核心动作首先我们需要剥离平台特性找到在iOS和Android安全测试中那些目标一致的操作。经过归纳主要包括以下几类应用生命周期控制启动、关闭、切换到前台/后台。虽然底层命令不同ios launchvsandroid intent launch但脚本需要提供的“启动应用”这个接口是一致的。内存操作搜索内存中的字符串、类实例、修改值。Frida的API本身是跨平台的但搜索的类名、方法签名截然不同。我们的脚本需要提供统一的搜索接口内部根据平台调用不同的Frida脚本片段。文件系统访问列举应用沙箱文件、下载/上传文件。iOS和Android的应用沙箱路径、访问权限模型不同但“列出Documents目录”这个需求是通用的。密钥与数据存储探查检查KeychainiOS、SharedPreferences/KeystoreAndroid、数据库文件等。这是差异最大的部分之一需要完全不同的模块来处理。网络流量监控虽然通常由独立的代理工具如Burp Suite完成但脚本可以统一完成证书绑定SSL Pinning绕过、代理设置等前置工作。Hook管理加载自定义Frida脚本、监控特定方法的调用。这是Frida的核心能力相对最容易统一但需要注意Objective-C与Java/Swift方法签名的转换。2.2 设计脚本的适配层架构基于以上分析一个可行的脚本架构如下你的跨平台测试脚本 (e.g., cross_mobile_test.py) ├── 平台检测模块 ├── 统一命令接口层 (如start_app(), search_memory()) │ ├── iOS命令实现层 (调用 objection -g iOS_BundleID explore 及子命令) │ └── Android命令实现层 (调用 objection -g Android_PackageName explore 及子命令) ├── 配置管理模块 (分离iOS/Android的配置项BundleID/PackageName、设备ID、Hook脚本路径等) └── 报告生成模块 (统一输出格式如JSON或HTML标注来源平台)这个架构的关键在于统一接口层。它对外暴露一套简单的函数如test_data_storage()。在这个函数内部它首先调用平台检测模块然后根据结果将请求“路由”到对应的iOS或Android实现函数中去。而iOS/Android的实现函数则封装了各自平台原生的objection命令或Frida JavaScript脚本。2.3 工具选型与依赖管理除了objection和Frida这两个基石为了构建健壮的脚本我们还需要考虑脚本语言Python是首选。因为它不仅是objection本身的使用语言而且拥有丰富的库用于处理子进程调用objection命令、解析JSON输出objection命令结果、以及编写清晰的胶水逻辑。设备连接脚本需要能自动识别连接的设备。对于Android我们可以依赖adb devices对于iOS则需要idevice_id来自libimobiledevice套件。脚本的初始化阶段应包含设备检测和选择逻辑。依赖封装强烈建议使用Docker。可以构建两个镜像一个包含Android SDK、adb、objection、frida-tools另一个包含iOS的objection、frida-tools、libimobiledevice。或者更实用的是在一个镜像内整合所有工具通过脚本逻辑来切换使用路径。这能保证测试环境的一致性避免“在我机器上好好的”这类问题。注意iOS测试严重依赖证书和预置描述文件。自动化脚本通常需要在已越狱或具备开发者证书的设备上运行。对于企业签名的应用测试也需要提前将证书信任到设备上。这部分很难做到全自动化通常是脚本执行前的手动准备步骤但脚本应包含检查机制在证书无效时报错。3. 实战脚本拆解从平台检测到核心功能实现下面我们以一个名为mobile_sec_audit.py的脚本为例拆解关键部分的实现。假设我们的目标是自动化完成应用启动、内存关键词搜索、检查明文存储和生成报告。3.1 平台检测与环境初始化这是脚本的基石必须在所有操作之前完成。import subprocess import json import sys from enum import Enum class Platform(Enum): IOS ios ANDROID android UNKNOWN unknown def detect_platform(): 检测当前连接的设备平台。 优先检测iOS设备因为adb也可能在连接iPhone时返回某些驱动情况但idevice_id更精确。 # 检测iOS设备 try: result subprocess.run([idevice_id, -l], capture_outputTrue, textTrue, timeout5) if result.stdout.strip(): return Platform.IOS, result.stdout.strip().split(\n)[0] # 返回第一个设备UDID except (subprocess.CalledProcessError, FileNotFoundError): pass # idevice_id 未安装或无iOS设备 # 检测Android设备 try: result subprocess.run([adb, devices], capture_outputTrue, textTrue, timeout5) lines result.stdout.strip().split(\n) if len(lines) 1 and device in lines[1]: # 格式: List of devices attached\nemulator-5554 device device_id lines[1].split(\t)[0] return Platform.ANDROID, device_id except (subprocess.CalledProcessError, FileNotFoundError): pass print([!] 未检测到已连接的iOS或Android设备。请确保) print( - iOS: 安装libimobiledevice设备已通过USB连接并信任。) print( - Android: 安装adb设备已开启USB调试并授权。) return Platform.UNKNOWN, None # 初始化 platform, device_id detect_platform() if platform Platform.UNKNOWN: sys.exit(1) # 加载平台特定配置 config {} if platform Platform.IOS: config[bundle_id] com.example.target.ios config[frida_script_dir] ./hooks/ios/ elif platform Platform.ANDROID: config[package_name] com.example.target.android config[frida_script_dir] ./hooks/android/实操心得设备检测的顺序很重要。在一些混合环境中可能同时连接了iOS和Android设备。更完善的脚本应该列出所有设备让用户选择或者通过命令行参数--platform ios --device-id xxxx直接指定避免自动检测的歧义。3.2 封装统一的Objection命令执行器直接拼接命令字符串容易出错且不安全封装一个执行器是必要的。def run_objection_cmd(cmd_args, platform, device_id, target_id): 执行objection命令的通用包装函数。 :param cmd_args: 命令参数列表如 [memory, search, --string, password] :param platform: Platform枚举 :param device_id: 设备ID :param target_id: 应用标识Bundle ID 或 Package Name base_cmd [objection] if platform Platform.IOS: base_cmd.extend([-g, target_id, -N, -h, localhost, -p, 27042]) # 常用连接参数 # 注意iOS可能需要先通过 frida-ps -U 确认注入方式对于非越狱设备这里假设使用Developer Disk Image附加 else: # Android base_cmd.extend([-g, target_id, -U]) # -U 表示连接USB设备 base_cmd.extend(cmd_args) print(f[*] 执行命令: { .join(base_cmd)}) try: result subprocess.run(base_cmd, capture_outputTrue, textTrue, timeout60) if result.returncode ! 0: print(f[!] 命令执行失败stderr: {result.stderr[:200]}) # 只打印前200字符 return None return result.stdout except subprocess.TimeoutExpired: print([!] 命令执行超时) return None注意事项objection连接iOS应用时情况比Android复杂。对于已越狱设备通常可以直接附加。对于非越狱设备需要提前通过frida的-f参数以“注入”模式启动应用这需要开发者证书或企业证书。我们的run_objection_cmd函数目前处理的是最常见的“附加”场景。在实际脚本中你可能需要根据设备状态是否越狱和测试需求动态构建不同的objection连接命令。3.3 实现跨平台的内存搜索功能内存搜索是动态分析的核心。虽然objection提供了memory search命令但其搜索语法和对象在不同平台差异很大。我们需要在统一接口下处理这些差异。def unified_memory_search(search_pattern, pattern_typestring): 统一的内存搜索接口。 :param search_pattern: 要搜索的模式如字符串或十六进制。 :param pattern_type: string 或 hex target_id config.get(bundle_id) if platform Platform.IOS else config.get(package_name) if platform Platform.IOS: # iOS下objection memory search 对字符串支持较好 if pattern_type string: cmd [memory, search, --string, search_pattern, --offsets-only] else: # hex # 需要将十六进制字符串格式化为 objection 接受的格式如 “41 42 43” hex_spaced .join(search_pattern[i:i2] for i in range(0, len(search_pattern), 2)) cmd [memory, search, hex_spaced, --offsets-only] else: # Android # Android的memory search命令格式略有不同且对字符串搜索可能需要指定大小端 if pattern_type string: # 注意Android下字符串搜索可能需指定编码这里使用默认 cmd [android, memory, search, --string, search_pattern] else: cmd [android, memory, search, search_pattern] # hex output run_objection_cmd(cmd, platform, device_id, target_id) # 解析输出提取内存地址 addresses [] if output: for line in output.split(\n): if 0x in line.lower(): # 简单提取十六进制地址实际解析可能需要更复杂的正则 import re found re.findall(r(0x[0-9a-fA-F]), line) addresses.extend(found) return addresses # 使用示例 print([*] 在内存中搜索关键词 admin...) found_addrs unified_memory_search(admin, pattern_typestring) print(f[] 找到 {len(found_addrs)} 个潜在地址: {found_addrs[:5]}) # 只打印前5个核心细节解析这里的关键差异在于objection命令本身。在iOS上内存搜索是全局命令memory search而在Android上则是android memory search子命令。我们的封装函数在内部处理了这个差异。此外搜索结果的解析也需要小心。objection的输出格式是给人看的不是给机器看的。上面简单的正则提取可能漏掉一些情况生产脚本需要编写更健壮的解析器或者直接使用--json参数如果objection版本支持来获取结构化数据。3.4 检查数据存储Keychain vs. SharedPreferences这是平台差异最大的部分之一必须分别实现。def check_sensitive_data_storage(): 检查常见的敏感数据存储位置。 findings [] target_id config.get(bundle_id) if platform Platform.IOS else config.get(package_name) if platform Platform.IOS: print([*] 检查iOS Keychain条目...) # objection 的 ios keychain dump 会尝试转储所有可访问的Keychain条目 cmd [ios, keychain, dump] output run_objection_cmd(cmd, platform, device_id, target_id) if output and No clear text passwords not in output: # 这里需要解析keychain输出寻找敏感信息 # 简化处理查找包含“password”、“token”、“key”等关键词的行 sensitive_keywords [password, pwd, token, secret, key, auth] for line in output.split(\n): if any(kw in line.lower() for kw in sensitive_keywords): findings.append(fKeychain潜在敏感信息: {line.strip()}) print([*] 检查NSUserDefaults...) cmd [ios, nsuserdefaults, get] output run_objection_cmd(cmd, platform, device_id, target_id) # 解析NSUserDefaults的输出寻找敏感数据 else: # Android print([*] 检查Android SharedPreferences...) # objection 的 android hooking list activities 等命令可以找到类但直接dump shared_prefs文件更直接 # 这里演示通过执行一个内置的Frida脚本片段来枚举SharedPreferences文件 frida_script Java.perform(function() { var context Java.use(android.app.ActivityThread).currentApplication().getApplicationContext(); var prefsDir context.getFilesDir().getParent() /shared_prefs/; var File Java.use(java.io.File); var dir File.$new(prefsDir); var files dir.listFiles(); if (files) { for (var i 0; i files.length; i) { console.log([SharedPrefs File] files[i].getName()); } } }); # 将脚本写入临时文件并让objection加载 import tempfile with tempfile.NamedTemporaryFile(modew, suffix.js, deleteFalse) as f: f.write(frida_script) temp_script_path f.name cmd [script, load, temp_script_path] output run_objection_cmd(cmd, platform, device_id, target_id) # 解析output获取shared_prefs文件列表然后可以进一步用 android file download 下载分析 print([*] 检查内部存储文件...) cmd [android, file, ls, /data/data/ target_id /] output run_objection_cmd(cmd, platform, device_id, target_id) # 分析文件列表寻找.db, .xml等可能包含敏感数据的文件 return findings避坑技巧对于Android的SharedPreferences直接通过objection的android sqlite命令可能无法直接读取.xml文件。更可靠的方法是1使用上面的Frida脚本列出文件2使用android file download命令将/data/data/package/shared_prefs/目录下的文件下载到本地3使用Python的xml.etree.ElementTree或plistlib对于某些格式进行解析。记住永远不要假设存储是安全的即使文件权限是600在已Root或已越狱的设备上我们都能访问。4. 脚本的进阶整合与自动化工作流基础功能封装好后我们可以将它们串联成一个完整的自动化测试流程。4.1 构建可配置的测试流水线我们可以设计一个JSON或YAML配置文件来定义测试套件// config_audit.json { ios: { bundle_id: com.example.app.ios, actions: [ {type: launch_app}, {type: memory_search, pattern: API_KEY, pattern_type: string}, {type: keychain_dump}, {type: nsuserdefaults_get} ] }, android: { package_name: com.example.app.android, actions: [ {type: launch_app}, {type: memory_search, pattern: password, pattern_type: string}, {type: shared_prefs_enum}, {type: sqlite_dump, db_path: databases/user.db} ] } }主脚本读取配置根据检测到的平台选择对应的动作列表然后遍历执行每个动作并收集结果。4.2 集成自定义Frida脚本进行深度Hookobjection内置的hook命令如android hooking watch class适合探索但对于复杂的、需要自定义逻辑的Hook必须集成自定义Frida脚本。def run_custom_hook(hook_script_name): 加载并运行位于平台特定目录下的自定义Frida脚本。 script_path os.path.join(config[frida_script_dir], hook_script_name) if not os.path.exists(script_path): print(f[!] 钩子脚本不存在: {script_path}) return cmd [script, load, script_path] output run_objection_cmd(cmd, platform, device_id, config.get(bundle_id, config.get(package_name))) # 处理脚本输出可能包含拦截到的数据、函数调用参数等 # 这里可以将输出重定向到日志文件或进行实时分析 log_hook_output(hook_script_name, output)实操心得编写跨平台的Frida脚本本身也是一个挑战。你需要用Process.platform来判断当前运行环境然后为iOSObjC和AndroidJava分别编写Hook代码。更好的做法是维护两套独立的脚本./hooks/ios/和./hooks/android/由主脚本根据平台选择加载。这虽然增加了维护点但保证了脚本的清晰和可维护性。4.3 结果收集与报告生成自动化测试的价值在于可重复和可审计。必须将每一步的结果记录下来。import json from datetime import datetime class AuditReporter: def __init__(self): self.report { metadata: { platform: platform.value, device_id: device_id, target_id: config.get(bundle_id, config.get(package_name)), timestamp: datetime.now().isoformat() }, findings: [], logs: [] } def add_finding(self, category, description, severityINFO, evidenceNone): 添加一条发现。 self.report[findings].append({ category: category, description: description, severity: severity, # INFO, LOW, MEDIUM, HIGH, CRITICAL evidence: evidence, timestamp: datetime.now().isoformat() }) def add_log(self, message): 添加一条日志。 self.report[logs].append({ time: datetime.now().isoformat(), message: message }) print(message) # 同时打印到控制台 def save_report(self, filenameNone): 保存报告为JSON文件。 if not filename: filename faudit_report_{self.report[metadata][target_id]}_{datetime.now().strftime(%Y%m%d_%H%M%S)}.json with open(filename, w, encodingutf-8) as f: json.dump(self.report, f, indent2, ensure_asciiFalse) print(f[] 报告已保存至: {filename}) return filename # 在脚本中使用 reporter AuditReporter() reporter.add_log(f开始对 {config.get(bundle_id, config.get(package_name))} 进行安全测试) addresses unified_memory_search(secretToken) if addresses: reporter.add_finding(内存安全, 在内存中发现疑似密钥字符串, MEDIUM, evidence{addresses: addresses[:3]}) reporter.save_report()报告的价值结构化的JSON报告可以被导入到缺陷跟踪系统如JIRA、或通过其他工具如jq进行筛选和统计。你也可以很容易地在此基础上扩展生成HTML或PDF格式的、更易读的报告。5. 常见问题、排查技巧与优化建议在实际运行这套脚本时你肯定会遇到各种问题。下面是一些典型问题及其解决思路。5.1 设备连接与注入失败问题objection连接超时或报错“Failed to start: Unable to connect to the frida-server”。排查确认Frida-server运行Android上adb shell进入设备执行ps | grep frida-server。iOS上如果是越狱设备确保frida-server已通过Cydia等安装并运行。确认设备连接adb devices或idevice_id -l是否能列出设备确认端口frida-server默认监听27042端口。确保没有防火墙或网络规则阻止。应用状态对于“附加”模式目标应用必须已经在运行。对于“注入”模式-f需要确保证书有效。技巧写一个简单的连接测试函数在脚本开头执行例如用frida-ps -U来列出进程比直接运行完整的objection命令更快发现问题。5.2 Objection命令输出解析错误问题脚本在解析objection命令输出时崩溃或者提取不到正确信息。排查版本差异不同版本的objection其命令输出格式可能有细微差别。始终在脚本中注明测试通过的objection版本号。使用JSON输出如果objection命令支持--json参数如objection -g com.app explore --json务必使用它。解析JSON比解析纯文本稳定得多。防御性编程在解析文本时多用try...except对正则匹配结果进行判空if match:。假设输出是不稳定的。技巧在开发阶段将关键的objection命令的原始输出stdout和stderr保存到临时日志文件中便于出错时复查。5.3 跨平台Hook脚本的兼容性问题为iOS写的Frida脚本在Android上加载时报语法错误或找不到类。解决方案完全隔离如前所述维护两套脚本是最清晰的。条件编译在单个脚本内使用Frida的Process.platform进行判断。// 在同一个 .js 文件中 if (Process.platform darwin) { // iOS // Objective-C Hook 代码 var className ObjC.classes[NSString]; } else if (Process.platform linux) { // Android // Java Hook 代码 var className Java.use(java.lang.String); }心得即使使用条件编译iOS和Android的API也完全不同代码会变得冗长。除非Hook逻辑非常简单且高度相似否则推荐隔离方案。5.4 性能与稳定性优化问题脚本运行缓慢或者长时间运行后objection会话断开。优化建议会话复用不要为每个命令都重新启动一个objection进程。我们的run_objection_cmd函数目前是独立的实际可以优化为启动一个objection的REPL交互式会话并通过管道向其发送命令。这可以使用pexpect库来实现能极大提升速度并保持上下文。超时控制为每个命令设置合理的超时时间。像memory search这种可能很慢的操作超时可以设长一点如120秒简单的file ls则可以短一些。错误重试对于网络波动或设备临时无响应导致的失败可以实现简单的重试机制例如最多重试3次每次间隔2秒。资源清理脚本结束时确保正确断开与objection和Frida的连接避免僵尸进程。5.5 扩展性设计如何添加新的测试模块一个好的脚本框架应该易于扩展。当你想增加一个新的测试功能比如检查剪贴板滥用时应该怎么做在统一接口层在mobile_sec_audit.py中增加一个新的函数例如check_clipboard_access()。实现平台特定代码在函数内部根据平台调用不同的实现。def check_clipboard_access(): if platform Platform.IOS: return _check_ios_clipboard() else: return _check_android_clipboard()编写平台实现分别实现_check_ios_clipboard()和_check_android_clipboard()。它们内部会封装调用objection命令或运行Frida脚本的逻辑。更新配置与流水线将新的检查动作添加到JSON配置文件的actions列表中。集成到报告确保检查结果能通过reporter.add_finding()添加到最终报告中。通过这种方式你的跨平台测试脚本就能像一个不断成长的“武器库”随着你的经验积累逐渐覆盖更多的移动安全测试场景。从最初的内存搜索和文件枚举到后来的网络API分析、加密函数识别、随机数检测等都可以通过这个框架集成进来最终形成你个人或团队独有的、高效能的移动应用自动化安全审计平台。