吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 242|回复: 0
上一主题 下一主题
收起左侧

[Android 原创] 【含AIGC】逆向/学习墨西哥国家选举委员会身份证认证App

[复制链接]
跳转到指定楼层
楼主
看等非军事 发表于 2026-11-7 14:51 回帖奖励

给管理的备注:虽然看起来逆向外国国家机关的行为很刑,但是实际上这个App的验证方式是用非对称加密密钥对解密,本身就没有加壳的必要;在大学学过计算机安全第一课的人都学过,隐晦式安全并不能带来任何安全。因为这个App本身的设计就是安全的,加混淆实属画蛇添足,因此分享出来给各位。

在论坛逛了好久好久,第一次发逆向贴,虽然做了七八年软件开发,但是对CTF和脱壳一直是一窍不通。现在有了LLM的帮助,总算可以对自己感兴趣的程序进行逆向脱壳了。
我个人很不喜欢在论坛里发AIGC,读起来特别难受,但是因为这部分静态分析主要都是LLM做的,感觉只能分享LLM思路,但愿各位大佬批评斧正

人在墨西哥,不像中国,墨西哥的身份证里没有芯片,而是通过背面的两个二维码存储电子数据供机读的。墨西哥选举委员会有一个App可以读取二维码里的信息,辨别真伪和假证,而且不需要联网。

https://centralelectoral.ine.mx/wp-content/uploads/2026/06/CPV_muestra-tras-1024x647.png
举个例子,这是墨西哥选举委员会官网给的“证件样本”女士的证件背面;这个二维码是可以用的,扫出来确实能看到她的“个人信息”

直接扫这两个二维码读出来似乎是一堆二进制字符,因此猜测:

  1. 肯定是有编码解码的
  2. 墨西哥人不会傻到直接把照片和个人信息随便搞个编码就放进去,这样做不到打击假证;如果随便脱壳就能知道他们怎么加密信息,那设计这个软件的人可以蹲监狱了


看了看论坛里的帖子,决定从apk下手;先下载 GDA 4.11,把APK拖进去,显示没有检测到壳,代码里充斥着乱七八糟的英语、西班牙语、还有调用大概是中国买的ABI的拼音函数名……

我最开始想得很简单:界面既然能显示男、女,搜字符串,再顺着交叉引用往回找,总能找到数据从哪里来的。



找到 ParametrosScanner.getTexto(),里面长这样:

public String getTexto() {
    // ......
    String t35372426620 = ParametrosScanner.t353724266201u;
    // ……
    return t35372426620;
}

原来的反编译结果有几十行,但是跟返回值有关的实际上就这一句。继续找谁给 t353724266201u 赋值,找到:

public static void c617893634551k(String p0) {
    // ……
    ParametrosScanner.t353724266201u = p0;
    // ……
}

现在就像剥大葱一样,一层一层往前找,找得我眼睛都花了,一路找,最后停在com.hubox.Azf里的t355299517578u,c203954798102k()把它返回。它还有一个tt(String) 函数负责往这个字符串后面追加字符。

到这卡住了,dd(String)在哪里声明的呢?当时挠破头皮都没想明白,后来发现这个apk好像也不止是只有Java,于是去com/hubox/Azf.java找了一下,找到:
public native int dd(Activity activity);

static {
    System.loadLibrary("a");
}

既然是.so,那就没办法啦,打开我心爱的Ghidra,拖进去发现实在实在是看不懂;但是解密之道就在其中,只好请出我的救星蓝色大肥鱼 Deepseek 配合 GPT-6 一起帮我反编译。



大肥鱼在Ghidra反编译的代码里找了半天,最后给我总结出了这一整条链路:

ZXing Result.getRawBytes()
    ↓
Dqf.t51023434872u(Result[])
    ↓
Azf.t452356517317u(Activity, byte[])
    ↓
native aa():识别并保存两个二维码的数据
    ↓
native dd():RSA 运算、解析文本、还原照片
    ↓
JNI 回调 Azf.tt(),逐个追加字符
    ↓
Azf.c203954798102k()
    ↓
ParametrosScanner.c617893634551k()
    ↓
getTexto()


以下主要是AIGC

先看liba.so里的Java_com_hubox_Azf_dd,找到字符串"tt"和方法签名"(Ljava/lang/String;)V",后面紧跟着CallVoidMethod。往上看是个按字符编号分支的switch,每次生成一个字符再调用tt(),这才接上前面找到的Java字符串。Ghidra没认出部分JNI调用,换LLM反编译出来是这种东西:

uVar16 = (**(code **)(*(long *)param_1 + 0x108))
    (param_1, uVar14, "tt", "(Ljava/lang/String;)V");
_JNIEnv::CallVoidMethod(
    (_jobject *)param_1, param_2, uVar16, puVar15
);

查JNI函数表,0x108对应GetMethodID,所以这两句就是查找tt(String)再调用它。分析其他地方也用这个办法:认出JNI和密码库调用,给变量补名字,参数不对就回去看ARM64汇编,再把内存复制整理成能读的流程。

从输入开始看,App取的是ZXing的Result.getRawBytes(),也就是扫出来的原始二进制。证件背面的两个二维码共同存一份记录,程序需要知道当前扫到的是哪一部分,才能按顺序拼接;这个编号就藏在原始数据里,直接通过位运算取得。aa()先取编号,再把后面的字节错开四位拼回来:

selector = ((raw[3] & 0x0f) << 4) | (raw[4] >> 4)
part = selector if selector < 2 else selector - 2

payload = bytes(
    ((raw[i + 4] << 4) | (raw[i + 5] >> 4)) & 0xff
    for i in range(len(raw) - 5)
)

selector为0或2时算第一部分,为1或3时算第二部分,其他值不接受。它只表示拼接位置,不是证件编号,也不能靠它判断两张码是不是来自同一张证件。后面的移位是把错开半个字节的数据重新对齐,没有涉及解密。
LLM选择用Java写个例程,用ZXing读取二维码图片(ZXingJS的getRawBytes不知为何不好用:编者注),把getRawBytes()保存到文件里。
两部分二维码数据收齐以后进入dd()。原代码是一堆内存分配和复制,整理以后如下,A、B分别是编号0、1的有效数据:

k = len(A) + len(B) - 1024

R = rsa_public_recover(A[k:] + B, outer_key)

m = 1024 - k
S = rsa_public_recover(A[:k] + R[:m], inner_key)

record = S + R[m:]

它不是两张码各解一次,而是先从两张码里拼出一个1024字节的RSA输入,做完第一轮,再拿恢复结果的一部分和之前留下的数据拼出第二个1024字节输入。目录里的样本两部分各856字节,因此第一轮是A后168字节加整个B,第二轮是A前688字节加第一轮结果前336字节,第二轮结果再接上第一轮剩余内容,才得到完整记录。

接着看RSA调用,找到的是:

rsa.ImportPublicKey(...);
rsa.DecryptBytes(input, false, output);

这里的false表示不用私钥,两处调用前的w2寄存器也确实被置零。App用公钥恢复数据,函数名虽然叫DecryptBytes,但里面没有签发证件用的私钥。两把公钥后来提出来,确认都是8192位,指数65537,1024字节刚好是一个RSA块,核心运算就是:

recovered = pow(
    int.from_bytes(block, "big"), 65537, n
).to_bytes(1024, "big")

n是公钥里的模数。算完还要去掉填充,也就是RSA块里用于格式和长度要求的额外字节,剩下的才是下一步要处理的数据;不是做完一次pow()就能直接看到姓名。

公钥没有明文摆在Java里,而是分散包装在libd.so、liba.so和Java表变换中。按Ghidra里的初始化流程往回追,APK内置公钥的解包顺序是:

Java表变换 → RSA公钥恢复 → Base64解码 → AES-256-CBC → 十六进制解码 → RSA公钥恢复 → 两把XML公钥

具体到这个APK,libd.so文件偏移0x34008的256字节先经过Java表变换,得到一把1024位包装公钥;再用它处理0x34110开始的9088字节,也就是71个128字节RSA块,恢复出8216字节Base64文本。继续按代码做AES和下一层RSA,最终得到2883字节字符串,中间用|隔开,两边各是一段1441字符的公钥XML。第一段放进clp[1]供二维码第二轮RSA使用,第二段放进clp[2]供第一轮使用。各层填充都能对上,导出的两把公钥也通过了OpenSSL检查。这里的偏移只针对本次分析的1.3版APK,换版本不能直接照抄。

绕了这么多层,最后藏的仍然是公钥(这是个好事啊!编者注)。拿到它可以恢复已有二维码里的内容,不能给任意姓名和照片生成有效二维码。另外,AES出现在公钥容器的解包流程里,扫描记录本身走的是前面两轮RSA。程序还有使用缓存或下发密钥的分支,这次恢复的是APK内置的那一组,没有检查运行中的安装实例用了哪组。

RSA处理完也不是JSON,文本和照片还得按记录里的长度字段取。文本每6位表示一个字符,从高位往低位读取,再查下面这张表,第一项是空格:

ALPHABET = " ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789:_-,;.\n\\ÁÉÍÓÚÜ^/()&|<>@'"

这就解释了为什么前面有一大串单字符分支:普通大写字母、数字,再加Ñ和西班牙语重音字母,一个字符只占6位。字符表共62项,6位却能表示64个值,剩下的编号62、63没有正常字符可查,应该不会在证件里出现。

记录里的位置计算也整理了出来,BE16表示把两个字节按大端顺序读成整数:

h = record[0]
text_length = BE16(record[h+66 : h+68])
text = record[h+68 : h+68+text_length]

q = h + 68 + text_length
r = q + BE16(record[q : q+2])

metadata = record[r+2 : r+6]
image_length = BE16(record[r+6 : r+8])
image = record[r+8 : r+8+image_length]

文本前面有一段头部,后面还有一段按长度跳过的数据,具体含义没有全部确认。metadata是程序另外取出的四个字节,目前只保留原值,不能因为一开始搜的是男、女,就硬说其中哪个字节代表性别。图片则省掉了部分固定内容,原程序往里面补23个固定字节,其中能看到RIFF、WEBP、VP8,还原成WebP后交给Java显示。目录里的样本最终解出1669字节记录,其中文本编码占130字节,解出173个字符。



以上主要是AIGC



于是,我让大肥鱼给我写了一个简单的Java + Python解码器复刻这个app,给进去二维码,输出二维码里的信息。简单来说,就是先用ZXing读取二维码原始数据,读取顺序位编号,然后用提取出来的公钥跑两轮RSA解密,解码6bit字符,最后把WebP从原始数据流里提出取出来。

我觉得墨西哥INE这个离线验证的思路挺好的,证件自己带数据,验证方拿公钥读取和验证,不依赖网络,也不需要发回服务器查假证。
但是,不知道写程序的人怎么想的,实现明显有点脱裤子放屁,公钥外面套表变换、RSA、AES、再套RSA,为了取两把本来就应该公开的公钥,绕了这么大一圈。
学过RSA的人都知道,直接明文存公钥完全没问题,拿到公钥也不会因此获得造假证的能力;把它藏起来增加的是分析成本,不会增加安全性。而现在有了LLM的大逆向时代,所有人只要有靠隐晦式安全防破解的想法,都应该去花10块钱买token给蓝色大肥鱼做点蓝队测试,让自己彻底死心。

这条公钥解包流程里的AES没有保密的必要,app既然能离线解开,算法和IV/Nonce就都得交到用户手上。ChatGPT说“真正该做扎实的是RSA的编码与验证、长度检查、两张码的数据绑定,以及解析失败时的处理”,但是只要找个没那么路边的开源RSA实现,应该也没有太大的实现难度。壳和混淆,我觉得都可以省掉,让大家直接读代码审计验证逻辑,反倒比把公钥藏七八层更有意义;代码公开不会自动保证没有问题,但至少方便别人发现问题。

免费评分

参与人数 2热心值 +2 收起 理由
larryliao + 1 我很赞同!
dechong + 1 谢谢@Thanks!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-11-9 21:27

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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