资讯动态

APK签名欺骗与PMS Hook原理及防篡改对抗实战

发布时间:2026/9/20 3:17:54 来源:尧图企业网站定制
APK签名这块平时写业务代码的时候很少被正眼看待但凡是上架应用商店被拒、做安全加固、或者遇到应用被“二次打包”的情况你就知道签名校验到底有多重要了。这些年做应用安全对抗我见过太多把签名校验写在Java层然后被一行Hook就绕过的案例也见过PMS Hook这种直接把系统返回结果给“调包”的操作。这篇内容我打算把APK签名欺骗的原理、PMS Hook的实现思路以及我们防御方应该怎么去对抗一次性讲透。1. APK签名机制与签名欺骗的基本盘1.1 先搞懂APK签名到底是什么APK签名说白了就是给安装包盖一个数字印章。Android系统在安装APK时会校验这个印章确保应用确实是由某个开发者签发的、没有被第三方动过手脚。这个机制从Android 1.0就存在了但演进到今天已经分出了三代签名方案v1JAR签名对APK内每个文件逐一计算SHA1/SHA256摘要写入META-INF目录下的MANIFEST.MF、CERT.SF、CERT.RSA文件。安装时逐文件校验摘要一旦有文件被改动就校验失败。v2APK Signature Scheme v2Android 7.0引入对整个ZIP文件的数据块做摘要校验速度快、覆盖范围更全还能防住v1方案中“文件被删除却不报错”的问题。v3/v3.1在v2基础上支持密钥轮换应用可在不改变包名的情况下更换签名密钥。网上那些所谓“重签名APK”的操作用到的就是这一套机制的反面先反编译改完代码后重新打包再用自己的密钥给APK签名。可问题是签名一变系统记录的签名信息就和原来的不一致了。如果APK里没有任何签名校验逻辑那攻击者改完签名安装运行几乎毫无感知。但如果APK里做了签名比对游戏就开始了——攻击者要么把校验逻辑一并改掉要么在更底层直接骗过系统。1.2 攻击者的真实路径从反编译到重打包我在分析恶意样本时经常看到攻击者走的是这条路径用Apktool或jadx把目标APK反编译拿到smali代码或反编译的Java源码。定位到签名校验的代码段直接NOP掉或改成恒为true。重打包成新的APK用自签名密钥签名。安装运行观察是否还有新的校验点。这套路在今天依然有效因为不少应用只在启动时做了“一次性静态校验”——判断签名字节数组等于某个固定值。改掉一个if条件就万事大吉。像热门热搜词里经常出现的“termux重签名apk”相关操作本质上也属于这种二次打包流程。只要校验逻辑写在Java/Kotlin层、校验结果只用一次、而且校验值硬编码在代码里攻击者就有十足的把握把它处理掉。真正麻烦的是那些把签名校验下沉到Native层、或者把校验结果当成密钥去解密资源和服务端通讯的应用。这类应用单纯靠“改代码重打包”已经绕不过去了于是攻击者开始盯上系统层——这就是PMS Hook登场的背景。1.3 为什么说签名欺骗的终极战场在系统服务我们常说的“签名校验”在Android系统里其实是由PackageManagerServicePMS这个系统服务来执行的。PMS负责解析APK、提取签名、管理包信息和权限。当应用在Java层调用context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES)时发出去的其实是一趟跨进程Binder调用由PMS返回结果。问题来了如果整个校验链路都依赖系统返回的签名信息那么攻击者只需要让系统“说假话”就行了。他们不需要去改APK里的任何一行检查代码只需要Hook住PMS返回签名数据的环节把新签名替换成原始签名——应用侧怎么校验都是通过的。这就是PMS Hook的底层思路不和你较劲直接改裁判的判罚。2. PMS Hook的攻击路径与Hook点分析2.1 PMS到底存着哪些攻击者想要的数据我们先看看PMS相关的数据结构。当一个APK安装到系统后PMS会解析出以下关键信息PackageInfo包含包名、版本号、权限、签名数组等。Signature[]APK签名信息的数组也就是应用侧校验时最常获取的数据。ApplicationInfo应用的基础信息包括data目录、so库路径等。攻击者Hook PMS的核心目的无外乎两个第一篡改PackageInfo中返回的Signature[]让应用认为自己还是“原版签名”第二篡改PackageManager相关方法的返回值让系统层面的权限校验、包管理逻辑失效。我见过一些Xposed模块直接修改android.content.pm.PackageManager类的getPackageInfo方法在返回前用replaceFirst之类的反射操作把签名替换掉。这种做法在Android 7.0以下非常有效因为应用侧拿到的签名完全来自系统返回而系统返回的签名已经被模块“调包”了。2.2 攻击者常用的几个Hook点站在防御视角我们至少需要知道攻击者会碰哪些点位。根据我的经验这几个地方是重灾区Hook点作用风险等级PackageManager.getPackageInfo应用最常调用的签名获取入口篡改后签名比对直接“通过”极高PackageParser.collectCertificatesAPK解析阶段的签名收集篡改后可影响安装时的签名记录高PackageManagerService.checkSignatures系统内部签名比对方法篡改后应用与系统签名校验失效高Signature.toByteArray签名对象转字节数组篡改后所有基于字节数组的比对都失真极高context.getPackageManager().getInstallerPackageName获取安装来源包名可被篡改以绕过渠道校验中这几个点里getPackageInfo和Signature.toByteArray最经典。前者是应用侧主动拉取签名时必经之路后者则是在字节层面对签名的“最后一公里”做手脚。只要这两个方法被Hook住应用自己再去比较签名字节数组拿到的永远是攻击者伪造的“正确签名”。2.3 从Hook到欺骗一个典型的调用链分析为了讲清楚这条链子我用一个典型的“重签名后安装运行”场景来模拟攻击者的操作流程攻击者下载原版APK反编译后修改代码或植入恶意逻辑重新打包并自签名。安装时系统PMS解析新APK签名记录变成攻击者的签名。应用启动后业务代码调用getPackageInfo获取签名。此时如果系统里没有Hook环境拿到的就是新签名比对失败应用拒绝运行。攻击者此时在系统里加载Hook模块Xposed风格模块或基于Frida的注入脚本Hook住getPackageInfo或Signature.toByteArray。Hook模块在返回前检查调用者是谁如果发现是目标应用就把签名替换成从原版APK里提取出的原始签名字节数组。应用侧再次获取签名拿到的是被“洗白”的数据校验通过应用正常启动恶意逻辑成功潜伏。整个过程里应用代码一行没改校验逻辑原封不动但最终结果被外部环境欺骗了。这也是为什么我一直强调——本地签名校验必须有环境感知能力单纯比字节数组在Root/Hook环境下毫无信任基础。3. 防篡改对抗方案从代码层到框架层的层层设防3.1 防重打包的三道基础防线先说最基础的三道防线即便不引入复杂的加固方案这三样也建议每个应用都做第一道多签名方案强制校验。不要只判断v1或只判断v2而是把GET_SIGNATURES和GET_SIGNING_CERTIFICATES都拿一遍分别比较。Android 7.0之后api变了很多老App只支持GET_SIGNATURES攻击者只要把应用伪装成v1签名安装就能骗过部分校验逻辑。第二道校验值不以明文硬编码。很多应用直接把签名的SHA256哈希值写死在Java代码里攻击者搜索SHA-256或者找签名比较的if语句就能定位。建议把期望的哈希值拆成多段运行时动态拼接或者用简单的加密算法做一层变换至少提高静态分析的难度。第三道校验失败时不要原地退出而是“带病运行”。这是很多人的误区——校验失败直接System.exit(0)或者弹窗退出这种逻辑太好定位了攻击者改成不退出就完事。更狠的做法是校验失败后正常启动但某些核心功能静默降级、上报异常行为、或者在关键业务节点才真正触发限制。对面要测出这种“软性失败”逻辑成本高得多。3.2 Native层校验绕开Java层Hook的优势与局限众所周知Java层代码太容易被Hook和反编译所以很多团队把签名校验下沉到NativeJNI/NDK层去实现。思路是这样在C/C代码里自行解析APK文件头、提取证书数据或者读取/proc/self/status、/proc/self/maps等信息做完整性校验然后通过JNI将校验结果返回给Java层。这种方案确实能有效提升绕过门槛因为攻击者要Hook Native层的函数难度远大于Java层——Xposed对Native方法的Hook效果有限Frida虽然能在Native层做拦截但不是所有函数都能轻易Hook住。我自己在实测中发现只要Native校验逻辑写得稍微复杂一点比如涉及dlopen自解析、__attribute__((constructor))回调、Java层与Native层互验Frida脚本的编写成本就会指数级上升。但Native层不是万能的。它有明显的局限如果Native层校验的结果依然是一个boolean并且Java层拿到后直接决定是否退出那攻击者仍然可以Hook住JNI函数的返回值或者干脆在Java层把所有调用Native校验的地方都改成true。攻击者可以用Unidbg之类的框架模拟执行Native代码把校验逻辑“算”出来然后直接逆出结果。在高版本Android里读取自身APK路径越来越受限获取签名信息的方式本身也在收紧。所以我的建议是Native层校验应该是校验链路的加固点而不是终点。它最大的价值在于让“静态分析”和“运行时篡改”的难度同时提升但仅靠它是防不住铁了心的攻击者的。3.3 服务端协同校验让本地环境不再可信聊完本地侧的各种方案我想换个思路说说服务端校验。这才是目前对抗签名欺骗最有效的大杀器原因很简单攻击者的Hook能力再强也只能作用于本地环境骗不了服务端。具体做法是应用在登录、支付、拉取核心业务数据时通过安全通道把包信息上报到服务端。上报内容不止是包名和版本号要包含签名哈希、安装来源、应用启动时间等特征。服务端拿到这些数据后先跟数据库里备案过的合法签名的哈希比对。不一致直接拒绝请求。对高频请求做风控——如果同一个账号下出现大量不同签名、或者签名变化频繁直接拉黑设备或账号。服务端校验的进阶玩法是让签名参与加解密。比如客户端做某些关键数据解密时所需的AES密钥由签名哈希派生而来。本地如果被Hook伪造了签名那么派生出来的密钥是不对的解密出来的数据就是一堆乱码。这种“签名参与业务逻辑”的耦合方式比单点判断要扎实得多。3.4 环境对抗检测Root、Hook框架与调试器既然攻击者借助Xposed、Frida、Magisk这类框架来实施PMS Hook防御方就必须把这些环境给识别出来。我见过不少团队在这里走极端——要么完全不做检测要么检测到Root就坚决不让用两者其实都不可取。更合理的策略是检测到异常环境不直接阻断而是记录风险并降低信任等级。常见的检测维度包括Xposed检测遍历/system/lib下是否存在xposed相关文件检查ClassLoader中是否加载了de.robv.android.xposed相关类检查/proc/self/maps中是否有异常模块映射。Frida检测检查默认端口27042是否开放检测frida-server进程名或者在Native层扫描线程列表中是否有gum-js-loop线程。Frida的检测难度逐年增加但基础检测能拦住80%的脚本小子。Magisk检测检查/sbin/magisk、/data/adb/magisk等路径检测su二进制是否存在。注意Magisk的Hide功能会伪造这些路径检测时要结合多种特征综合判断。调试器检测检查TracerPid是否为0、Debug.isDebuggerConnected()是否为true以及/proc/self/status中的调试标志位。环境检测的用法我个人建议分三档检测到轻量风险比如开启USB调试只上报检测到Root/Magisk不直接阻断但敏感业务降级或增加额外风控验证检测到明显注入痕迹Xposed/Frida进程或模块特征则直接中断核心流程并上报。3.5 商业化加固与开源方案的选型参考聊到防篡改很多人会问一句到底自研还是上第三方加固我的看法是如果团队没有专职的安全研发优先选成熟的商业加固方案比如腾讯乐固、360加固、梆梆安全这类。商业方案在对抗成熟攻击链上确实有经验积累壳的强度、反调试、VMP虚拟化保护能力是多数自研方案比不了的。选商业加固需要注意几点注意兼容性加固后要跑全套回归测试特别是so库的加载、动态权限、热修复、插件化这些功能和加固壳冲突的概率不低。保留原始签名信息有些加固方案会修改签名获取逻辑导致服务端校验签名时拿到的值不对联调时得特别留意。不要裸奔上架很多加固厂商提供“企业版”或“金融版”功能更全包括强化反调试、防内存dump。省这点预算有时候得不偿失。如果确实要走自研路线我建议动手前先明确一个边界你对抗的是什么样的攻击者。如果是普通爱好者、脚本小子自研加上文提到的基础校验已经足够了如果是专业逆向团队、黑产团伙那还是得靠服务端校验风控模型来兜底本地自研做得多完美都只能拖慢速度挡不住最终被突破。4. 实测下来最容易翻车的几个坑与经验总结4.1 只校验v1签名导致的高版本绕过这是我见过最多的翻车现场。很多遗留代码只会用GET_SIGNATURES拿一个签名数组然后比字节这在Android 7.0以下没问题。但高版本APK如果同时存在v1和v2签名安装时系统默认优先采用v2签名。攻击者如果把v2签名剥离掉、只留v1或者反过来利用v1签名校验的兼容性问题就能让校验逻辑走到一个并未被篡改、但实际已经“非原版”的分支上。正确做法是在PackageManager的API层面分别读取GET_SIGNATURES返回旧签名和GET_SIGNING_CERTIFICATES返回新签名两边都验证。验证不是简单相等还要看证书链、签名算法、证书有效期这些属性。只比数组相等的做法骗过去不算难。4.2 单一校验点与硬编码哈希的组合陷阱另一个高频坑是把期望的签名哈希写死在代码里然后在一个Activity里做个if判断就结束。攻击者搜索签名字符串快速定位一行smali改掉就搞定。就算你把哈希做了分段、做了加密存储只要校验逻辑只有一处被定位后依然是几分钟的事。我推荐的做法是至少布置三个校验点分布在冷启动、核心页面进入、支付/交易等关键操作的前置逻辑里。校验结果不要只用来“退出”还可以作为后续某些数据的解密因子、日志上报的标记位、甚至UI层面的偶发异常现象装饰品。这样攻击者即使找到第一个校验点把它废掉后续校验触发时依然会暴露痕迹。4.3 服务端校验只验包名不验签名的疏漏有些产品虽然做了服务端校验但上报的只是包名和版本号。攻击者重打包时只要不改包名服务端那一关压根不会拒绝。这个问题在分发渠道和联运场景里尤其突出——包名相同的马甲包一抓一大把。正确的服务端校验至少要上报包名、签名哈希最好是v2/v3签名证书的SHA256、安装来源、设备指纹、APK文件的完整性摘要比如基于APK原始数据算出的CRC32或MD5。服务端对比时任何一个维度不一致都应当拒绝。另外上报通道要加密并且要防重放——攻击者如果抓包直接重放一个合法请求服务端也识别不了。加入时间戳、nonce、设备端一次性token才能把重放的风险压下去。4.4 环境检测误伤正常用户的问题环境检测不是做得越多越好。我见过把检测到TracerPid不为0就直接封号的结果用户开着开发者模式玩手游就被误杀了一大片。这种“宁可错杀不可放过”的策略在上线后往往会变成事故。更合理的做法是环境检测结果作为风控模型的输入特征之一而不是唯一的封禁依据。比如检测到设备已Root但签名校验通过、用户历史行为正常那就正常放行只是把风险等级从低提为中。检测到Root Frida注入 高频敏感操作再触发人工审核或加强二次验证。把安全判断从“二元对立”变成“连续评分”误杀率会大幅下降对抗强度也不会削弱太多。写到最后的一点个人体会回头再想想“APK签名欺骗PMS Hook与防篡改对抗”这套攻防游戏本质上是信任模型的较量。攻击者在赌“应用信任了不该信任的本地系统信息”防御方则要把这种信任一点一点收回来逼着攻击者在更多维度上露出破绽。签名校验从来不应该是一条孤立的安全防线它应该跟环境检测、服务端风控、业务逻辑深度绑定在一起。作为一名从逆向分析走过来、又回头做应用防护的开发者我最大的感受是没有绝对安全的方案只有不断叠加的成本。只要把攻击者绕过一个校验点的时间成本拉得足够高让他们的方案变得不划算就已经达到了绝大多数产品场景的安全诉求。所以与其焦虑漏洞永远堵不完不如踏踏实实把基础校验做全、把服务端协同做扎实剩下的交给时间去验证。

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

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

免费获取报价