返回列表 发新帖

偶然发现的签名校验思路

[复制链接]

7

主题

350

回帖

2126

积分

大学生

Rank: 5Rank: 5

金币
724
好评
4
信誉
100

MT论坛新人

发表于 1 小时前 来自手机  | 显示全部楼层 | 阅读模式  来自 河北
本帖最后由 岁安辞 于 2026-9-15 01:14 编辑

可能是新的思路基于 Android Key Attestation 的签名检验

本文AI生成大佬轻喷


最近在研究其他检测点的时候,偶然发现了一个挺有意思的东西:

Android Key Attestation 生成的证书里面居然有应用签名相关的信息。

当时我就在想:

«这玩意是不是也能拿来做 APK 签名检测?»

然后实际测了一下……

还真可以。

所以就记录一下这个思路,纯属个人瞎折腾,欢迎大佬指点。



一、传统的签名检测

我们平时检测 APK 签名,基本就是:

当前 APP
   ↓
PackageManager
   ↓
SigningInfo
   ↓
获取签名证书
   ↓
计算 SHA-256
   ↓
和官方签名比较

这种方法大家应该都比较熟了。

而我这次想到的玩法不太一样。

我不直接去问:

«“PackageManager,你告诉我这个 APP 签名是什么。”»

而是:

«“Android Keystore,你给我生成一个带 Attestation 的密钥,然后我看看你证书里面记录的 APP 身份信息。”»

大概就是:

当前 APP
   ↓
AndroidKeyStore
   ↓
生成 Attestation Key
   ↓
拿到证书链
   ↓
解析 Key Attestation
   ↓
找到 AttestationApplicationId
   ↓
拿 signatureDigests
   ↓
和官方 SHA-256 比较



二、关键东西:AttestationApplicationId

这里最关键的就是这个 OID:

1.3.6.1.4.1.11129.2.1.17

这是 Android Key Attestation 的扩展。

里面有一堆 KeyDescription、AuthorizationList 之类的东西。

其中又能找到:

AttestationApplicationId

里面有:

packageName
signatureDigests

而 "signatureDigests" 就是我们真正想要的东西。

简单理解就是:

AttestationApplicationId
        ↓
signatureDigests
        ↓
APP 签名摘要

所以我就想着:

那直接拿这个东西和官方签名 SHA-256 比一下不就完了?

于是就有了下面这个东西。

  1. package com.test;

  2. import android.security.keystore.KeyGenParameterSpec;
  3. import android.security.keystore.KeyProperties;

  4. import java.security.KeyPairGenerator;
  5. import java.security.KeyStore;
  6. import java.security.ProviderException;
  7. import java.security.cert.Certificate;
  8. import java.security.cert.X509Certificate;
  9. import java.security.spec.ECGenParameterSpec;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.UUID;

  13. public class Detection_Tampering {

  14.     private static final String KEY_ALIAS = "AndroidKeyStore_check";
  15.     private static final String KS = "AndroidKeyStore";
  16.     private static final String ATTEST_OID =
  17.             "1.3.6.1.4.1.11129.2.1.17";

  18.     /**
  19.      * @param expectedSha256 预期签名 SHA-256 (Hex)
  20.      * @return Normal | Tampering with signatures | Unsupported devices
  21.      */
  22.     public static String check(String expectedSha256) {
  23.         try {

  24.             // 1. 生成带 Challenge 的硬件密钥
  25.             KeyPairGenerator kpg =
  26.                     KeyPairGenerator.getInstance(
  27.                             KeyProperties.KEY_ALGORITHM_EC,
  28.                             KS);

  29.             KeyGenParameterSpec spec =
  30.                     new KeyGenParameterSpec.Builder(
  31.                             KEY_ALIAS,
  32.                             KeyProperties.PURPOSE_SIGN
  33.                                     | KeyProperties.PURPOSE_VERIFY)
  34.                             .setAlgorithmParameterSpec(
  35.                                     new ECGenParameterSpec("secp256r1"))
  36.                             .setDigests(
  37.                                     KeyProperties.DIGEST_SHA256)
  38.                             .setAttestationChallenge(
  39.                                     UUID.randomUUID()
  40.                                             .toString()
  41.                                             .getBytes())
  42.                             .build();

  43.             kpg.initialize(spec);
  44.             kpg.generateKeyPair();

  45.             // 2. 获取证书链
  46.             KeyStore ks =
  47.                     KeyStore.getInstance(KS);

  48.             ks.load(null);

  49.             Certificate[] chain =
  50.                     ks.getCertificateChain(KEY_ALIAS);

  51.             if (chain == null || chain.length == 0) {
  52.                 clean();
  53.                 return "Unsupported devices";
  54.             }

  55.             X509Certificate leaf =
  56.                     (X509Certificate) chain[0];

  57.             byte[] ext =
  58.                     leaf.getExtensionValue(
  59.                             ATTEST_OID);

  60.             if (ext == null) {
  61.                 clean();
  62.                 return "Unsupported devices";
  63.             }

  64.             // 3. OCTET STRING -> KeyDescription
  65.             byte[] kd =
  66.                     octetContent(ext, 0);

  67.             Element kdSeq =
  68.                     read(kd, 0);

  69.             if (kdSeq.tagNo != 16) {
  70.                 clean();
  71.                 return "Unsupported devices";
  72.             }

  73.             // 4. 获取 KeyDescription 顶层字段
  74.             List<Element> top =
  75.                     children(
  76.                             kdSeq.value(),
  77.                             0,
  78.                             kdSeq.value().length);

  79.             int last1 = -1;
  80.             int last2 = -1;

  81.             for (int i = 0; i < top.size(); i++) {

  82.                 if (top.get(i).tagNo == 16) {

  83.                     last2 = last1;
  84.                     last1 = i;
  85.                 }
  86.             }

  87.             if (last1 < 0) {
  88.                 clean();
  89.                 return "Unsupported devices";
  90.             }

  91.             Element sw =
  92.                     top.get(
  93.                             last2 >= 0
  94.                                     ? last2
  95.                                     : last1);

  96.             Element hw =
  97.                     top.get(last1);

  98.             // 5. 查找 Tag 709
  99.             byte[] appId =
  100.                     findTag709(sw.value());

  101.             if (appId == null) {
  102.                 appId =
  103.                         findTag709(hw.value());
  104.             }

  105.             if (appId == null) {
  106.                 clean();
  107.                 return "Unsupported devices";
  108.             }

  109.             // 6. 解析 signatureDigests
  110.             List<byte[]> sigs =
  111.                     parseAppIdSigs(appId);

  112.             if (sigs.isEmpty()) {
  113.                 clean();
  114.                 return "Unsupported devices";
  115.             }

  116.             if (expectedSha256 == null) {
  117.                 clean();
  118.                 return "Unsupported devices";
  119.             }

  120.             String exp =
  121.                     expectedSha256
  122.                             .trim()
  123.                             .toUpperCase();

  124.             // 7. 比较签名
  125.             for (byte[] s : sigs) {

  126.                 if (exp.equals(toHex(s))) {

  127.                     clean();
  128.                     return "Normal";
  129.                 }
  130.             }

  131.             clean();
  132.             return "Tampering with signatures";

  133.         } catch (ProviderException e) {

  134.             clean();
  135.             return "Unsupported devices";

  136.         } catch (Exception e) {

  137.             clean();
  138.             return "Unsupported devices";
  139.         }
  140.     }

  141.     /* ==================== DER 解析 ==================== */

  142.     static final class Element {

  143.         int tagNo;
  144.         int tagClass;
  145.         boolean constructed;
  146.         int content;
  147.         int len;
  148.         int end;
  149.         byte[] raw;

  150.         Element(
  151.                 int tagNo,
  152.                 int tagClass,
  153.                 boolean constructed,
  154.                 int content,
  155.                 int len,
  156.                 int start,
  157.                 byte[] raw) {

  158.             this.tagNo = tagNo;
  159.             this.tagClass = tagClass;
  160.             this.constructed = constructed;
  161.             this.content = content;
  162.             this.len = len;
  163.             this.end = content + len;
  164.             this.raw = raw;
  165.         }

  166.         byte[] value() {

  167.             if (len <= 0) {
  168.                 return new byte[0];
  169.             }

  170.             byte[] v =
  171.                     new byte[len];

  172.             System.arraycopy(
  173.                     raw,
  174.                     content,
  175.                     v,
  176.                     0,
  177.                     len);

  178.             return v;
  179.         }
  180.     }

  181.     private static Element read(
  182.             byte[] d,
  183.             int off) throws Exception {

  184.         if (off >= d.length) {
  185.             throw new Exception("DER EOF");
  186.         }

  187.         int first =
  188.                 d[off] & 0xFF;

  189.         int tagClass =
  190.                 first & 0xC0;

  191.         boolean constructed =
  192.                 (first & 0x20) != 0;

  193.         int tagNo =
  194.                 first & 0x1F;

  195.         int p =
  196.                 off + 1;

  197.         // 多字节 Tag
  198.         if (tagNo == 0x1F) {

  199.             tagNo = 0;

  200.             while (p < d.length) {

  201.                 int b =
  202.                         d[p++] & 0xFF;

  203.                 tagNo =
  204.                         (tagNo << 7)
  205.                                 | (b & 0x7F);

  206.                 if ((b & 0x80) == 0) {
  207.                     break;
  208.                 }
  209.             }
  210.         }

  211.         // 长度
  212.         if (p >= d.length) {
  213.             throw new Exception(
  214.                     "DER len EOF");
  215.         }

  216.         int lenByte =
  217.                 d[p++] & 0xFF;

  218.         int len;

  219.         if (lenByte < 0x80) {

  220.             len = lenByte;

  221.         } else if (lenByte == 0x80) {

  222.             throw new Exception(
  223.                     "Indefinite length not supported");

  224.         } else {

  225.             int n =
  226.                     lenByte & 0x7F;

  227.             if (n > 4) {
  228.                 throw new Exception(
  229.                         "Length too large");
  230.             }

  231.             len = 0;

  232.             for (int i = 0; i < n; i++) {

  233.                 if (p >= d.length) {
  234.                     throw new Exception(
  235.                             "DER len overflow");
  236.                 }

  237.                 len =
  238.                         (len << 8)
  239.                                 | (d[p++] & 0xFF);
  240.             }
  241.         }

  242.         if (p + len > d.length) {
  243.             throw new Exception(
  244.                     "DER content overflow");
  245.         }

  246.         return new Element(
  247.                 tagNo,
  248.                 tagClass,
  249.                 constructed,
  250.                 p,
  251.                 len,
  252.                 off,
  253.                 d);
  254.     }

  255.     private static byte[] octetContent(
  256.             byte[] d,
  257.             int off) throws Exception {

  258.         Element e =
  259.                 read(d, off);

  260.         if (e.tagNo != 4) {
  261.             throw new Exception(
  262.                     "Expected OCTET STRING");
  263.         }

  264.         return e.value();
  265.     }

  266.     private static List<Element> children(
  267.             byte[] d,
  268.             int from,
  269.             int to) throws Exception {

  270.         List<Element> list =
  271.                 new ArrayList<Element>();

  272.         int off = from;

  273.         while (off < to) {

  274.             Element e =
  275.                     read(d, off);

  276.             list.add(e);

  277.             off = e.end;
  278.         }

  279.         return list;
  280.     }

  281.     private static byte[] findTag709(
  282.             byte[] auth) {

  283.         try {

  284.             int off = 0;

  285.             while (off < auth.length) {

  286.                 Element e =
  287.                         read(auth, off);

  288.                 if (e.tagClass == 0x80
  289.                         && e.tagNo == 709) {

  290.                     byte[] v =
  291.                             e.value();

  292.                     try {

  293.                         Element inner =
  294.                                 read(v, 0);

  295.                         if (inner.tagNo == 4) {
  296.                             return inner.value();
  297.                         }

  298.                     } catch (Exception ignore) {
  299.                     }

  300.                     return v;
  301.                 }

  302.                 off = e.end;
  303.             }

  304.         } catch (Exception ignore) {
  305.         }

  306.         return null;
  307.     }

  308.     private static List<byte[]> parseAppIdSigs(
  309.             byte[] appId) throws Exception {

  310.         List<byte[]> out =
  311.                 new ArrayList<byte[]>();

  312.         Element seq =
  313.                 read(appId, 0);

  314.         if (seq.tagNo != 16) {
  315.             return out;
  316.         }

  317.         List<Element> kids =
  318.                 children(
  319.                         seq.value(),
  320.                         0,
  321.                         seq.value().length);

  322.         for (Element k : kids) {

  323.             if (k.tagNo == 17) {

  324.                 List<Element> sigs =
  325.                         children(
  326.                                 k.value(),
  327.                                 0,
  328.                                 k.value().length);

  329.                 for (Element s : sigs) {

  330.                     if (s.tagNo == 4) {
  331.                         out.add(s.value());
  332.                     }
  333.                 }
  334.             }
  335.         }

  336.         return out;
  337.     }

  338.     private static String toHex(byte[] b) {

  339.         if (b == null) {
  340.             return "";
  341.         }

  342.         StringBuilder sb =
  343.                 new StringBuilder();

  344.         for (byte x : b) {

  345.             sb.append(
  346.                     String.format(
  347.                             "%02X",
  348.                             x));
  349.         }

  350.         return sb.toString();
  351.     }

  352.     private static void clean() {

  353.         try {

  354.             KeyStore ks =
  355.                     KeyStore.getInstance(KS);

  356.             ks.load(null);

  357.             if (ks.containsAlias(KEY_ALIAS)) {

  358.                 ks.deleteEntry(
  359.                         KEY_ALIAS);
  360.             }

  361.         } catch (Exception ignored) {
  362.         }
  363.     }
  364. }
复制代码


三、目前的测试代码

代码我这里没有使用 BouncyCastle,而是自己简单写了个 DER 解析。

主要就是为了把 Attestation 里面的东西扒出来。




四、实际测试下来确实能用

简单说就是:

我拿正常 APK 测。

然后从 Attestation 里面拿到:

signatureDigests

再和我提前准备好的官方签名 SHA-256 比。

能匹配上:

Normal

换成其他签名之后:

Tampering with signatures

所以至少从实际测试结果来看:

这个思路是成立的。



五、但是马上就发现了一个问题

这东西最大的问题就是:

不是所有手机都能正常搞。

我实际测试发现,部分国产系统,还有一些欧加系设备,可能会因为各种奇奇怪怪的原因导致 TEE 出问题。

比如:

Key Attestation 生成失败

或者:

证书链为空

或者:

拿不到 Attestation 扩展

甚至有些情况看起来就像:

TEE 损坏 / 假死

这种情况下,这个检测点就直接没了。

所以不能看到:

Attestation 失败

就直接:

签名被篡改

因为人家可能只是 TEE 有问题。



六、所以我最后把结果分成了三个

目前代码里面是:

Normal

正常,Attestation 获取到了,而且签名摘要匹配。

Tampering with signatures

Attestation 正常获取到了,但是里面的签名摘要和预期的不一样。

还有:

Unsupported devices

Attestation 根本没法正常搞,或者解析不到需要的信息。

这个状态不能直接算“签名异常”。



七、这个方法和传统签名检测有什么区别?

我觉得这也是这个思路比较好玩的地方。

传统方法:

APP

PackageManager

SigningInfo

签名

而这个:

APP

AndroidKeyStore

KeyMint / TEE

Key Attestation

X509 Certificate

KeyDescription

AttestationApplicationId

signatureDigests

所以它至少算是:

从另一条路拿到了签名相关信息。

不是简单地再写一遍 "PackageManager"。



八、个人实测了一下过签

然后我也顺手拿一些目前见到的过签工具测试了一下。

目前我的测试结果是:

暂时都没成功过掉这个检测。

当然,这肯定不能说明:

«“这个检测无敌了。”»

毕竟我自己测试的东西也有限,测试环境也有限。

只能说:

至少我目前手里的几个东西没过去。

如果有大佬已经研究过这个方向,或者手里有什么好玩的东西,也欢迎自己试试。

说不定哪天就有人把它过了,然后我又得回来学习了

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

7

主题

350

回帖

2126

积分

大学生

Rank: 5Rank: 5

金币
724
好评
4
信誉
100

MT论坛新人

发表于 1 小时前 来自手机  | 显示全部楼层  来自 河北
纯Ai文大佬轻点喷
回复

使用道具 举报

0

主题

173

回帖

465

积分

初中生

Rank: 3Rank: 3

金币
122
好评
1
信誉
100
发表于 1 小时前 来自手机  | 显示全部楼层  来自 福建
感谢分享
回复

使用道具 举报

64

主题

1482

回帖

4438

积分

大学生

LZY·网络资源

Rank: 5Rank: 5

金币
1060
好评
9
信誉
101

MT论坛最佳新人MT论坛新人考神MT论坛帅哥MT论坛活跃会员

QQ
发表于 1 小时前 来自手机  | 显示全部楼层  来自 天津
谢谢分享,正好需要
回复

使用道具 举报

0

主题

3277

回帖

7609

积分

硕士生

Rank: 6Rank: 6

金币
3587
好评
0
信誉
100

MT论坛新人考神MT论坛帅哥

发表于 1 小时前 来自手机  | 显示全部楼层  来自 浙江
感谢分享。
回复

使用道具 举报

24

主题

2617

回帖

8024

积分

硕士生

Rank: 6Rank: 6

金币
1486
好评
24
信誉
102

MT论坛新人考神

发表于 1 小时前 来自手机  | 显示全部楼层  来自 海南
感谢分享
回复

使用道具 举报

1

主题

304

回帖

718

积分

初中生

Rank: 3Rank: 3

金币
160
好评
0
信誉
100
发表于 半小时前 来自手机  | 显示全部楼层  来自 广西
大佬牛逼
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表