资讯动态

iOS应用相似度评估:基于IPA结构与资源指纹的完整方案

发布时间:2026/10/8 19:22:32 来源:尧图企业网站定制
很多搞 iOS 开发、上架审核和移动安全分析的朋友应该都撞过同一堵墙两个应用看起来明显是“同一个东西”但要从技术上拿出可信的论证却没那么容易。界面像算不算代码像算不算资源文件一致能不能作为同源证据反过来更头疼——自己的原创应用因为风格和某个竞品相近被误判成马甲包或者仿冒应用申诉时除了写说明根本拿不出可复现的技术材料。我最近在做 iOS 应用相似度评估时把整套基于 IPA 结构调整和资源指纹变化的处理流程重新捋了一遍从解包、指纹计算、结构对比到实际整改全部走通。这篇文章就把这套方法完整写出来既要讲清楚“为什么要从这个角度切入”也会给出可以直接抄作业的脚本、命令和注意事项。动笔之前先把边界划清楚下文提到的方法定位是自有应用的合规优化、版权方向的取证评估、应用安全场景的风险识别。请只对你拥有源码或已获得授权的对象使用。技术本身是中立的但使用场景必须合规这一点比任何技术细节都重要。1. 相似度问题的本质先搞明白我们到底在比什么1.1 这不是一道是非题而是一道程度题iOS 应用相似度在实际业务里很少是“像”或“不像”的二选一更多时候是量化评估“相似度达到多少就该重点观察”“哪些维度重合可以佐证同源”“哪些差异只是换皮包装”。审核方的关注点、版权方的取证需求、安全侧的威胁判定虽然出发点不同但落到技术层面都会汇聚到同一个问题上两个 IPA 包的组成材料和排列方式重合度到底有多高。我习惯把应用相似度拆成三个可观测的维度来理解结构相似度IPA 解包后的目录层级、文件命名规则、资源分布方式是否高度一致。这能反映构建工程的组织习惯甚至出自同一套打包流程。内容相似度资源文件本体是否一致包括图片、音频、视频、配置文件、nib/storyboard 编译产物等。内容一致是“同源”最直接的证据。行为相似度运行后的界面跳转、接口调用、第三方 SDK 集成方式是否雷同。行为维度最接近用户感知但分析成本也最高。这三个维度不是孤立的。两个包如果资源指纹大面积重合通常结构也趋同行为上大概率存在承接关系。反过来如果已经有行为层面的怀疑用资源和结构维度去补证据是最快也最稳的路径。1.2 为什么是“结构调整”和“资源指纹变化”“处理相似度问题”可以有两种方向一是“识别相似度”也就是分析定位二是“调整相似度”也就是对自有应用做结构层面的整改。这两个方向正好对应的就是标题里的两个关键词——资源指纹负责量化内容重合度结构调整负责改变或识别结构层面的强特征。举个例子。两个应用都包含一张 1024x1024 的启动图内容完全一样那这张图的 SHA-256 也会完全一致。无论应用名称怎么改、图标怎么换这张图就是同源的“钉子”。反过来如果这是你自己的应用你发现它和某个旧版本或竞品之间存在大量无意遗留的相同资源那你需要做的就不是争论“像不像”而是通过结构调整把资源重新组织、替换、重命名把相似度真正降下来。所以这里面最关键的一点是资源和结构不是“看个大概”而是可以被精确计算和度量的对象。2. IPA 结构拆解先看清一个应用打包后到底有哪些组成2.1 IPA 就是一个 zip但内部远比想象中规整IPA 文件本质上是一个 zip 压缩包。拿到手第一步永远是解包工具不限命令行 unzip 就够用mkdir ipa_analysis cd ipa_analysis cp /path/to/YourApp.ipa . unzip YourApp.ipa -d extracted_ipa tree -L 2 extracted_ipa/解压后会看到典型结构通常长这样extracted_ipa/ └── Payload/ └── YourApp.app/ ├── YourApp # Mach-O 可执行文件 ├── Info.plist # 应用配置 ├── Assets.car # 编译后的资源包 ├── Frameworks/ # 动态库 ├── _CodeSignature/ # 签名信息 ├── Base.lproj/ ├── zh-Hans.lproj/ └── *.nib / *.storyboardc # 界面编译产物Payload 目录下一般只有一个 .app 包但也见过同时塞了多个 .app 的 IPA多见于壳工程或者聚合类应用。如果你在分析一个 IPA 时发现 Payload 下有多个 app这本身就是一个值得记录的结构特征。2.2 .app 包内部的关键成员把 .app 包看成一个小型文件系统里面每个成员的“长相”都带着工程痕迹成员作用在相似度分析中的价值Mach-O 可执行文件编译后的二进制主程序可提取字符串、符号表、SDK 版本信息是行为维度分析的基础Info.plist应用元数据Bundle ID、版本号、权限声明等是结构对比的“身份卡”Assets.car图片资源被编译后的统一资源包图片资源的集中地指纹计算的重点对象Frameworks工程依赖的动态库动态库的名称、架构、哈希可直接反映依赖关系是否同源_CodeSignature/CodeResources签名与资源校验清单列出所有文件的哈希值能直接看到应用登记了哪些资源.nib/.storyboardc界面布局编译产物路径和命名习惯体现开发工具链和代码组织方式.lproj多语言资源目录语言列表和文案内容本身就是一种指纹很多人会忽略_CodeSignature/CodeResources这个文件。它实际上是打包签名时生成的一份“资源清单”里面按文件路径记录了每个资源的 SHA-1 哈希。分析相似度时这份清单可以直接作为资源指纹的初步索引省去全盘遍历的时间。但它只覆盖签名时纳入的资源且不包含 Frameworks 内部文件所以只能作为辅助参考不能完全依赖。2.3 可量化的结构观测维度做结构对比时我通常会抽几个指标而不是人肉盯着目录看文件路径集合两个 .app 内所有相对路径的并集与交集。交集比例越高结构同源概率越大。目录深度与命名规律比如资源是按Resources/Images、Resources/Audio分类还是平铺在根目录。同一个团队的工程习惯基本稳定。文件大小分布同名文件大小完全一致的判断价值极高因为重命名容易但内容大小和哈希一起重合的概率很低。Info.plist 关键字段Bundle ID、CFBundleVersion、MinimumOSVersion、URL Scheme 等字段的组合本质上也是结构指纹的一部分。把上述指标记录下来作为后续对比的“结构指纹基线”。这一步做完才谈得上“通过结构调整来处理相似度”或“通过指纹变化来量化相似度”。3. 资源指纹怎么做从 SHA-256 到感知哈希3.1 精确指纹SHA-256 全量哈希资源指纹最基础也最可靠的做法是给包内每一个文件计算 SHA-256。只要文件内容有一个字节不同哈希就完全不同因此它适合精确匹配“同一份文件”的场景两个包如果存在大量 SHA-256 完全一致的文件基本可以认定这些文件来自同一来源。我写过一个简单的 Python 脚本专门用来给 IPA 解包目录建立指纹清单import os import hashlib import json def file_sha256(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def build_fingerprint(root_dir, excluded_dirsNone): excluded_dirs excluded_dirs or {_CodeSignature, Frameworks} fingerprint [] for current_root, dirs, files in os.walk(root_dir): dirs[:] [d for d in dirs if d not in excluded_dirs] for name in files: full_path os.path.join(current_root, name) rel_path os.path.relpath(full_path, root_dir) stat os.stat(full_path) fingerprint.append({ path: rel_path.replace(os.sep, /), size: stat.st_size, sha256: file_sha256(full_path) }) return fingerprint if __name__ __main__: fp build_fingerprint(extracted_ipa/Payload/YourApp.app) with open(fingerprint.json, w, encodingutf-8) as f: json.dump(fp, f, indent2)这个脚本会遍历 .app 包里的所有文件把相对路径、文件大小和 SHA-256 写进一个 JSON 文件。排除_CodeSignature和Frameworks是因为签名清单文件会在每次重新签名后变化不能作为资源同源判据Frameworks 则需要在单独的粒度上做对比因为第三方动态库的哈希往往是强特征不该混在一堆配置小文件里稀释权重。实际使用中我建议把指纹清单拆成两层一层是resources_fingerprint.json只包含图片、音频、视频、plist、nib 等资源类文件另一层是code_fingerprint.json包含 Mach-O 和动态库。两层分开的好处是后续算相似度时可以分别设置权重不至于让大量小配置文件主导结果。3.2 感知指纹pHash 解决“改了一点的图片”SHA-256 对“内容完全一致”的判断很准但对“看起来几乎一样、实际上只是被加了个水印、调了个亮度、或者重新压缩编码”的图片就无能为力了。这种情况需要感知哈希也就是 pHashPerceptual Hash。pHash 的核心思路是把图片缩成一个固定大小的灰度图再做离散余弦变换DCT取低频系数作为特征生成一串固定长度的哈希值。两张图片的哈希值之间用汉明距离衡量相似程度距离越小越像。我用 Python 实现过一版够用的做法from PIL import Image def phash(image_path, hash_size16): # 转为灰度图统一尺寸 img Image.open(image_path).convert(L) img img.resize((hash_size, hash_size), Image.LANCZOS) pixels list(img.getdata()) avg sum(pixels) / len(pixels) bits .join(1 if p avg else 0 for p in pixels) return int(bits, 2) def hamming_distance(h1, h2): return bin(h1 ^ h2).count(1)这段代码是简化版没有做 DCT只是比较了亮度均值。真正生产环境的 pHash 会用scipy.fftpack做 DCT取左上角低频区域再比较。不过即使简化到这个程度对“肉眼几乎一致”的两张图判断就已经很有效果了。依赖现成库的话ImageHash库可以直接用命令也更简单from PIL import Image import imagehash hash1 imagehash.phash(Image.open(icon_a.png)) hash2 imagehash.phash(Image.open(icon_b.png)) # 距离小于 10 通常可以视为同一张图的变种 print(hash1 - hash2)经验值两张图片的 pHash 汉明距离在 0 到 5 之间时可以认为视觉上高度相似5 到 10 之间存在较明显的后处理痕迹超过 10 一般就属于“风格相近但内容不同”了。这个阈值因图片内容复杂度而异建议对具体资源类型单独校准。pHash 不是银弹对纯色图标、大面积空白截图这类包含极少纹理的图片哈希距离会失真容易把完全不相关的单色图判成“高度相似”。所以在相似度评估模型里pHash 只作为补充证据不能单独作为判定依据。4. 相似度评估策略指纹对齐与评分模型4.1 文件级精确对比最快也最硬把两个解包目录的指纹清单准备好之后最直接的做法就是以 SHA-256 为键做对齐找出“哈希完全一致的文件”。import json def load_fp(path): with open(path, r, encodingutf-8) as f: return json.load(f) fp_a load_fp(app_a_fingerprint.json) fp_b load_fp(app_b_fingerprint.json) map_a {item[sha256]: item[path] for item in fp_a} map_b {item[sha256]: item[path] for item in fp_b} common_hashes set(map_a.keys()) set(map_b.keys()) print(完全一致的文件数:, len(common_hashes)) # 输出稍加分析的对比结果 for h in sorted(common_hashes, keylambda h: h)[:20]: print(map_a[h], 同名, map_b[h])这里有个细节值得注意路径不同但哈希完全相同是极强的同源信号。因为资源文件被移到不同目录并不影响内容哈希而两个独立开发的应用同时生成内容逐字节一致的文件概率非常低。反过来路径相同但哈希不同也可能是同一份资源被重新压缩或处理过的痕迹这需要用 pHash 做二次确认。哈希重合率的计算不能只拿一个数糊弄。我会分别算完全重合率 共同哈希数 / 包A总文件数反向完全重合率 共同哈希数 / 包B总文件数含名称重合率 路径一致且哈希一致的文件数 / 路径交集数三种比率可以组合使用规避“包A文件多导致重合率被稀释”或者“包B大量文件相同但包A只有少部分”这类不对称问题。4.2 感知级相似度处理“换皮”图片文件级对比只能抓到“没动过手脚”的资源但现实中的换皮操作会给图片加水印、调色、压缩、改后缀这时候就得靠感知级对齐来集合资源去重。我处理图片资源对比的流程是先从两个指纹清单里筛出图片文件按扩展名和大小过滤无用文件。逐一计算 pHash建立两组哈希列表。用汉明距离做两两比对找出距离小于阈值的图片对。把所有“高感知相似”的图片对数量汇总除以两组图片数量的较小值得到感知相似率。这个流程计算量不小几千张图做两两比对很容易跑成 O(n²)。我通常先按文件大小聚类只有大小差在 10% 以内的图片才进入 pHash 比对能省下大量无效计算。对音频、视频资源感知级对比复杂度高很多一般用时长、码率、采样率、轨道数等元数据做粗粒度判断很少直接做内容级感知匹配。在移动应用里音频视频资源往往是最容易被忽略但价值很高的指纹源如果两包的音频资源时长和编码参数高度一致同源嫌疑很大。4.3 结构级相似度比“路径像不像”更进一步资源指纹解决“内容是否一致”结构调整要解决的则是“结构是否同源”。结构相似度的量化我常用三种方式路径集合 Jaccard 相似度两组相对路径集合的交集大小除以并集大小。完全重命名的工程这个值会很低但路径骨架仍可能保留。目录深度分布对比统计每个目录下的文件数量分布绘制成向量后计算余弦相似度。高相似意味着文件组织习惯一致。文件名规则化后对比把文件名中的数字、UUID、版本号替换为占位符再比较规则化后的路径集合。这能识别“同一套命名规范”下的差异比如icon_1.png和icon_2.png在规则化后都变成icon_*.png。结构相似度不需要追求绝对精确它的价值在于提供“组织习惯是否同源”的参考维度。通常我会把结构相似度和资源重合度一起放进最终的评分模型。4.4 评分模型不迷信单一指标综合评估时我给每个维度分配权重。参考权重如下维度参考权重说明资源文件哈希重合率0.4内容同源的核心证据图片感知相似率0.2识别“换皮”后的视觉资源路径结构相似度0.25体现工程组织和打包习惯第三方库哈希重合率0.15动态库级同源特征最后的相似度分数是加权和范围 0 到 1。这里权重不是固定不变的如果分析对象是游戏类应用图片资源占大头感知相似率权重可以上调到 0.3如果是工具类应用资源和布局文件都很小权重就要向结构维度倾斜。真正做判断时我习惯先看哈希重合率高不高再看路径结构是否相似最后结合 pHash 做解释性验证而不仅仅是看一个总分数。5. 结构调整实操给自有应用“重新整理”的合规步骤5.1 先明确目标你到底想改什么如果分析后确认自己的应用存在容易被误判的相似度问题结构调整的目的不是“模仿谁”或“躲什么”而是让自有应用的资源组织和命名回到一个规范、独立的状态。常见目标包括清理从旧工程或共用组件里带出来的同名资源文件避免无意识重合。重新组织资源目录结构让工程布局更清晰、更符合当前业务。替换或重建 Asset Catalog让编译后的 Assets.car 不再保留旧包痕迹。动手前先给问题分类不要眉毛胡子一把抓。如果只是资源名和目录结构相近优先级从结构调整开始如果存在大量完全相同的历史资源文件必须先从资源替换和重做着手。5.2 重命名资源难点在于同步引用最基础的结构调整是重命名资源文件。比如把所有old_banner.png改成campaign_banner_2024.png这一步听起来简单但真正的坑在代码引用同步。iOS 工程里资源引用方式很多常见的有UIImage(named: old_banner)Bundle.main.path(forResource: old_banner, ofType: png)storyboard/xib 里直接通过资源名设置图片Assets.xcassets 里通过 setName 引用如果只用UIImage(named:)且资产名和文件名一致全局替换字符串还能勉强搞定。但要是代码里用字符串拼接生成资源名比如icon_ categoryName _normal就必须先梳理命名规则否则重命名后运行必崩。我踩过一次很深的坑工程里有上百个图片资源名是运行时拼接的当时只全局替换了显式字符串漏掉了拼接逻辑那一段结果测试时大量图标加载不出来回滚成本极高。所以建议先做引用扫描用ack或rg把资源名在代码里的所有出现位置枚举出来确认没有动态拼接再执行批量重命名。# 在工程目录里搜索资源名的引用位置 rg -l old_banner --type swift --type objc rg -l old_banner -g *.storyboard -g *.xib重命名后编译一次工程再用nm或strings检查 Mach-O 里是否还残留旧资源名字符串不漏过隐式引用。5.3 重新生成 Asset Catalog让编译产物脱离旧包痕迹资源指纹变化的一个高效入口是重新编译资源包。Assets.xcassets 里的图片经过actool编译后会生成 Assets.car这个文件内部的组织结构和资源索引是和图片文件路径强相关的。如果只是简单把图片文件换个名不重编 Assets.car指纹层面可能变化有限。重新生成 Assets.car 的典型命令如下xcrun actool Assets.xcassets \ --compile Resources/Assets.car \ --platform iphoneos \ --minimum-deployment-target 13.0 \ --app-icon AppIcon \ --launch-image LaunchImage重编之前建议先把 Assets.xcassets 内部的目录结构重新组织一下比如把通用图片从CommonImages调整到按业务模块划分的Modules/Home/Images把重复的旧命名清掉。还有一个实用技巧在 Asset Catalog 里给同一种资源同时提供 1x、2x、3x 三档尺寸不同的切片这样编译产物里的内部哈希会自然变化比简单改文件名带来的指纹变化幅度更大。5.4 目录层级与 Bundle 重组结构性更强的调整方式是改变资源所在层级。比如把一部分资源从主 .app 根目录下的Resources移动到独立子 bundle 中使用子 bundle 的应用场景通常适合组件化架构改动成本较高但带来的结构变化也更彻底修改前 Payload/YourApp.app/ Resources/ images/ banner.png 修改后 Payload/YourApp.app/ Frameworks/ YourResourceBundle.bundle/ images/ banner.png这里核心风险在于所有访问资源的代码都要同步改为通过内嵌 bundle 读取。以图片为例原来的UIImage(named:)要调整为let bundle Bundle(path: Bundle.main.path(forResource: YourResourceBundle, ofType: bundle)!) let image UIImage(named: banner.png, in: bundle, compatibleWith: nil)这种重组的收益不只是相似度特征变化还让资源边界更清晰后续维护也更好做。缺点是改动范围大需要预留充分测试时间。5.5 签名重置结构调整后的必修课无论做了哪种结构调整只要动了 .app 包里的内容原有的签名必然失效运行时会直接闪退装不上。模拟器上问题不明显真机上一装就报签名验证错误。重签的常用做法是先用codesign移除旧签名再重新签名。如果工程就是自己的 Xcode 工程直接在 Xcode 里重新 build 是更省事的方式不需要人工重签但如果想快速验证结构调整效果用手头可用的企业证书或开发证书做本地重签也能跑通。记住线上发布版本必须走 Xcode 完整构建流程任何手动重签都只用于本地分析验证不能作为面向用户分发的手段。6. 常见问题与排查技巧实录6.1 解包后 CodeResources 与文件列表对不上_CodeSignature/CodeResources里记录的资源清单可能和实际文件列表存在差异特别是当 IPA 经过二次签名或者额外注入文件后。遇到这种情况不要拿 CodeResources 直接当作实际文件清单它只能作为参考。我一般以os.walk遍历的结果为准CodeResources 用于核对“哪些文件曾被纳入签名”而不是“现在包里有什么”。6.2 指纹计算耗时过长全量对 .app 内所有文件做 SHA-256在资源特别多的游戏类应用上会跑很久。我通常做两个优化一是过滤目录只对Assets.car、图片目录、bundle、plist、nib 这些关键资源类型做全量哈希其余日志性小文件跳过或抽样二是用concurrent.futures做多进程并行计算机器配置允许时可以把十几分钟的任务压到两三分钟。资源指纹的价值在于“可重复对比”不在于“一次算尽”。6.3 结构调整后运行崩溃日志提示找不到资源这种情况绝大多数是重命名资源后引用未同步或者是资源被移到子 bundle 但代码仍用主 bundle 的读取方式。排查时先看崩溃栈里报的UIImage(named: nil)或bundle path not found然后立刻回溯所有对这个资源名的引用包括 storyboard、xib、代码硬编码、本地化文件。强烈建议在结构调整前先把资源名统一管理比如抽成常量枚举而不是散落在各处。6.4 哈希完全一致但路径完全不同怎么解释同一个资源出现在两个应用的不同路径下哈希一致的情况很能说明问题但也要想到一种合法可能两个应用都在不同时期使用过同样的第三方开源组件组件自带的资源就会完全相同。所以单纯看哈希一致不能直接下结论要结合“是否同时存在多个组件级资源一致”来判断。组件级资源通常以小文件居多如果一组第三方库的图标、初始化配置文件都完全一致那更合理的解释是“重复依赖同一个第三方库”而不是“应用整体同源”。6.5 pHash 误报率高pHash 对低纹理图片误报率高最典型的是大面积纯色图标。解决方式是加一个前置规则如果图片的颜色数低于阈值或者灰度直方图几乎只有单峰就直接跳过感知比对比只保留文件级哈希判断。更进一步可以在 pHash 之前先做一次尺度归一化避免“同一张图只因为缩放尺寸不同”产生额外的汉明距离。6.6 结构调整后相似度依然很高这种情况我遇到过几次排查下来问题多半出在 Mach-O 二进制本身。资源和目录结构都换了但二进制里的 SDK 信息、代码逻辑、第三方库引入方式和字符串特征依然高度近似导致综合评分不降反升。结构调整定位是“内容和组织层面的整改”涉及代码层面的相似度处理另有一套独立方案两者要分开执行不要指望单靠换资源文件名就能彻底改变整个评分。这几次实操下来我最大的体会是应用相似度处理不是一次性动作而是一个需要持续维护的分析框架。每次发版前跑一遍资源指纹清单把基线存档后续再遇到疑似同源的应用直接和新旧版本基线比对几分钟就能出报告。把这套流程固化到工程流水线里比每次临时补作业要靠谱得多。最后分享一个小技巧指纹清单文件记得带上应用版本号和构建号归档比如YourApp_2.3.0_build42_fingerprint.json。这样不仅方便对比历史版本在和多部门沟通相似度结论时也能快速定位到某个具体版本的具体资源状态省去很多来回确认的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑