当前位置: 首页 > article >正文

Android APP通信协议逆向:AES+Base64+Protobuf加密还原实战

1. 这不是“破解”而是对通信协议的工程化还原2021年4月那会儿我接到一个需求某智网APP在登录、设备控制、状态上报等关键链路中所有HTTP/HTTPS请求体和响应体都是密文看不到明文字段连基础的接口字段名都抓不到。当时团队里有人直接说“加了壳混淆自研加密逆向成本太高建议放弃”但实际拆解后发现所谓“某智网加密数据”根本不是靠高强度密码学算法筑墙而是一套典型的客户端预置密钥 轻量级混淆 多层嵌套编码组合拳。它不防懂行的人只拦住伸手就点Fiddler抓包的初级排查者。关键词——APP逆向、某智网、加密数据、Android、JNI、AES、Base64、Protobuf——这些不是堆砌术语而是真实拆解过程中必须逐层触达的技术锚点。这篇文章不讲“如何绕过安全检测”也不教“怎么脱壳”而是聚焦在已获取可调试APK的前提下如何系统性地定位、提取、验证并复现其加密逻辑。适合两类人一是刚接触IoT类APP逆向的安卓开发或测试工程师手里有APK但卡在“全是乱码”这一步二是已有逆向经验但面对非标准加密流程时缺乏结构化分析路径的老手。我会把整个过程拆成四段不可跳过的硬核环节从网络层密文定位开始到Java层加密入口识别再到JNI层密钥与算法还原最后落地为可独立运行的解密脚本。每一步都附带我当时踩坑的真实日志片段、反编译工具的关键配置参数以及为什么必须用这个工具而不是另一个的底层原因——比如为什么JADX比JEB更适合看这一版的混淆代码为什么在IDA里搜索AES_encrypt毫无意义但搜sub_8A3C却能一击命中。2. 网络层密文定位先确认“哪里被加密”再决定“怎么解”2.1 抓包不是目的定位加密边界才是核心很多人一上来就开Wireshark或Charles看到一堆POST /api/v1/device/control就急着导出body结果发现全是Base64字符串然后卡住。问题不在抓包工具而在没搞清加密发生的精确位置。某智网APP的加密不是全局统一处理而是分场景、分模块、甚至分字段粒度的。我们实测发现登录请求/auth/login的password字段是单独AES加密后拼入JSON的设备控制指令/device/command的整个payload字段是Protobuf序列化后再AES加密的而设备心跳上报/device/heartbeat的data字段却是先AES加密再Base64编码最后用固定字符串X-Enc-做前缀混淆。这意味着如果你只盯着/device/command抓包会误以为所有接口都走ProtobufAES但实际登录密码压根没走Protobuf。所以第一步必须建立接口-加密模式映射表。我们用Frida Hook了OkHttp的RequestBody.create()方法在每次网络请求发出前打印URL、原始body类型String/ByteArray、body长度并用hexdump输出前32字节。脚本核心片段如下Java.perform(function () { var RequestBody Java.use(okhttp3.RequestBody); RequestBody.create.overload(okhttp3.MediaType, java.lang.String).implementation function (mediaType, bodyStr) { console.log([REQ] URL: this.url() | BodyLen: bodyStr.length | Hex: hexdump(bodyStr.substring(0, 32))); return this.create(mediaType, bodyStr); }; });提示不要用console.log(bodyStr)直接打明文因为此时bodyStr已经是加密后的字符串打出来就是一串Base64。必须用hexdump看原始字节才能判断是否经过编码。实测抓取20个接口后我们归纳出三类密文特征接口路径密文长度特征Base64特征是否含Protobuf魔数/auth/login长度恒为32/48/64字节AES-CBC块对齐标准Base64字符集无补位否/device/command长度不规则如137、205字节末尾有补位是开头0x08 0x01/device/heartbeat长度恒为原始长度2开头为X-Enc-后续为Base64否这个表直接决定了后续逆向的优先级先攻/auth/login因为它的加密最简单无Protobuf嵌套且密钥大概率硬编码在Java层/device/command留到最后因为Protobuf schema需要额外逆向。2.2 关键验证用明文构造法反推加密入口光看密文特征还不够必须验证。我们写了一个Python脚本模拟登录请求先用明文密码123456手动构造一个未加密的JSON体然后发给服务端必然失败接着我们把这个JSON体喂给APP用Frida Hook住加密后返回的密文记录下来最后用这个密文替换我们脚本里的body重发请求——成功了。这证明加密逻辑完全在客户端服务端只认密文。更重要的是这个过程帮我们锁定了加密函数的输入输出边界输入是纯字符串如{password:123456}输出是Base64字符串如U2FsdGVkX1...。有了这个确定性边界下一步就能精准反编译定位Java层调用点。2.3 工具链选择为什么JADX比JEB更适配这一版混淆这一版某智网APP用了ProGuard深度混淆类名全为a.b.c方法名是a()、b()但字符串常量没加密。JEB虽然反编译质量高但在处理大量invoke-static跳转时会把加密逻辑分散到多个匿名内部类里阅读路径断裂。而JADX有个关键优势它默认开启--deobf反混淆且支持--string-decrypt插件需手动启用。我们用以下命令启动jadx -d ./output --deobf --string-decrypt --no-replace-consts app-debug.apk其中--string-decrypt会自动识别并还原被String.valueOf()、new String()等包装的加密字符串这对找密钥至关重要。实测中JADX成功把a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l......## 1. 这不是“破解”而是对通信协议的工程化还原2021年4月那会儿我接到一个需求某智网APP在登录、设备控制、状态上报等关键链路中所有HTTP/HTTPS请求体和响应体都是密文看不到明文字段连基础的接口字段名都抓不到。当时团队里有人直接说“加了壳混淆自研加密逆向成本太高建议放弃”但实际拆解后发现所谓“某智网加密数据”根本不是靠高强度密码学算法筑墙而是一套典型的客户端预置密钥 轻量级混淆 多层嵌套编码组合拳。它不防懂行的人只拦住伸手就点Fiddler抓包的初级排查者。关键词——APP逆向、某智网、加密数据、Android、JNI、AES、Base64、Protobuf——这些不是堆砌术语而是真实拆解过程中必须逐层触达的技术锚点。这篇文章不讲“如何绕过安全检测”也不教“怎么脱壳”而是聚焦在已获取可调试APK的前提下如何系统性地定位、提取、验证并复现其加密逻辑。适合两类人一是刚接触IoT类APP逆向的安卓开发或测试工程师手里有APK但卡在“全是乱码”这一步二是已有逆向经验但面对非标准加密流程时缺乏结构化分析路径的老手。我会把整个过程拆成四段不可跳过的硬核环节从网络层密文定位开始到Java层加密入口识别再到JNI层密钥与算法还原最后落地为可独立运行的解密脚本。每一步都附带我当时踩坑的真实日志片段、反编译工具的关键配置参数以及为什么必须用这个工具而不是另一个的底层原因——比如为什么JADX比JEB更适合看这一版的混淆代码为什么在IDA里搜索AES_encrypt毫无意义但搜sub_8A3C却能一击命中。2. 网络层密文定位先确认“哪里被加密”再决定“怎么解”2.1 抓包不是目的定位加密边界才是核心很多人一上来就开Wireshark或Charles看到一堆POST /api/v1/device/control就急着导出body结果发现全是Base64字符串然后卡住。问题不在抓包工具而在没搞清加密发生的精确位置。某智网APP的加密不是全局统一处理而是分场景、分模块、甚至分字段粒度的。我们实测发现登录请求/auth/login的password字段是单独AES加密后拼入JSON的设备控制指令/device/command的整个payload字段是Protobuf序列化后再AES加密的而设备心跳上报/device/heartbeat的data字段却是先AES加密再Base64编码最后用固定字符串X-Enc-做前缀混淆。这意味着如果你只盯着/device/command抓包会误以为所有接口都走ProtobufAES但实际登录密码压根没走Protobuf。所以第一步必须建立接口-加密模式映射表。我们用Frida Hook了OkHttp的RequestBody.create()方法在每次网络请求发出前打印URL、原始body类型String/ByteArray、body长度并用hexdump输出前32字节。脚本核心片段如下Java.perform(function () { var RequestBody Java.use(okhttp3.RequestBody); RequestBody.create.overload(okhttp3.MediaType, java.lang.String).implementation function (mediaType, bodyStr) { console.log([REQ] URL: this.url() | BodyLen: bodyStr.length | Hex: hexdump(bodyStr.substring(0, 32))); return this.create(mediaType, bodyStr); }; });提示不要用console.log(bodyStr)直接打明文因为此时bodyStr已经是加密后的字符串打出来就是一串Base64。必须用hexdump看原始字节才能判断是否经过编码。实测抓取20个接口后我们归纳出三类密文特征接口路径密文长度特征Base64特征是否含Protobuf魔数/auth/login长度恒为32/48/64字节AES-CBC块对齐标准Base64字符集无补位否/device/command长度不规则如137、205字节末尾有补位是开头0x08 0x01/device/heartbeat长度恒为原始长度2开头为X-Enc-后续为Base64否这个表直接决定了后续逆向的优先级先攻/auth/login因为它的加密最简单无Protobuf嵌套且密钥大概率硬编码在Java层/device/command留到最后因为Protobuf schema需要额外逆向。2.2 关键验证用明文构造法反推加密入口光看密文特征还不够必须验证。我们写了一个Python脚本模拟登录请求先用明文密码123456手动构造一个未加密的JSON体然后发给服务端必然失败接着我们把这个JSON体喂给APP用Frida Hook住加密后返回的密文记录下来最后用这个密文替换我们脚本里的body重发请求——成功了。这证明加密逻辑完全在客户端服务端只认密文。更重要的是这个过程帮我们锁定了加密函数的输入输出边界输入是纯字符串如{password:123456}输出是Base64字符串如U2FsdGVkX1...。有了这个确定性边界下一步就能精准反编译定位Java层调用点。2.3 工具链选择为什么JADX比JEB更适配这一版混淆这一版某智网APP用了ProGuard深度混淆类名全为a.b.c方法名是a()、b()但字符串常量没加密。JEB虽然反编译质量高但在处理大量invoke-static跳转时会把加密逻辑分散到多个匿名内部类里阅读路径断裂。而JADX有个关键优势它默认开启--deobf反混淆且支持--string-decrypt插件需手动启用。我们用以下命令启动jadx -d ./output --deobf --string-decrypt --no-replace-consts app-debug.apk其中--string-decrypt会自动识别并还原被String.valueOf()、new String()等包装的加密字符串这对找密钥至关重要。实测中JADX成功把a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l......这种超长类名自动映射为可读的NetworkUtils、CryptoHelper等。而JEB需要手动配置反混淆规则耗时且易漏。这个细节差异直接让Java层定位从3小时缩短到40分钟。3. Java层加密入口识别密钥在哪算法是啥3.1 从网络请求链路倒推Hook OkHttp Call.enqueue()是最短路径既然已知加密发生在RequestBody.create()之前那加密函数必然在OkHttp的Call.enqueue()调用栈里。我们不用静态分析大海捞针而是用Frida动态Hookenqueue()打印完整调用栈Java.perform(function () { var Call Java.use(okhttp3.Call); Call.enqueue.overload(okhttp3.Callback).implementation function (callback) { console.log([CALL] Stack: Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); return this.enqueue(callback); }; });运行APP触发登录日志中立刻出现关键线索at com.xxx.network.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m............encrypt(...)这个超长类名被JADX反混淆后对应com.xxx.network.CryptoHelper.encrypt(String)。这就是我们要找的入口打开JADX搜索encrypt立刻定位到public static String encrypt(String str) { try { byte[] bArr str.getBytes(UTF-8); byte[] bArr2 new byte[16]; System.arraycopy(a, 0, bArr2, 0, 16); // 密钥来自静态数组a byte[] bArr3 new byte[16]; System.arraycopy(b, 0, bArr3, 0, 16); // IV来自静态数组b SecretKeySpec secretKeySpec new SecretKeySpec(bArr2, AES); IvParameterSpec ivParameterSpec new IvParameterSpec(bArr3); Cipher instance Cipher.getInstance(AES/CBC/PKCS5Padding); instance.init(1, secretKeySpec, ivParameterSpec); return Base64.encodeToString(instance.doFinal(bArr), 2); } catch (Exception e) { e.printStackTrace(); return ; } }注意Cipher.getInstance(AES/CBC/PKCS5Padding)中的2是Base64.NO_WRAP标志位不是乱码。很多初学者看到2就懵其实这是Android Base64编码的常量。3.2 密钥提取静态数组a和b在哪为什么不能直接看smali密钥在静态数组a和b里但JADX里只显示a和b没显示值。这是因为ProGuard把数组初始化逻辑拆到了clinit类初始化方法里。我们切到smali目录搜索CryptoHelper找到CryptoHelper.smali然后搜.method static constructor clinit.method static constructor clinit()V .registers 3 const/16 v0, 0x10 new-array v0, v0, [B fill-array-data v0, :array_0 sput-object v0, Lcom/xxx/network/CryptoHelper;-a:[B ... :array_0 .array-data 1 0x31t 0x32t 0x33t 0x34t 0x35t 0x36t 0x37t 0x38t 0x39t 0x30t 0x61t 0x62t 0x63t 0x64t 0x65t 0x66t .end array-data .end method0x31t就是ASCII的10x32t是2……所以a数组就是1234567890abcdef——一个标准的16字节AES密钥。同理b数组是fedcba9876543210注意顺序。这里有个关键经验永远不要相信JADX反编译出的“密钥变量名”必须回smali看.array-data。因为JADX有时会把fill-array-data误判为其他操作导致密钥显示为空或错误。3.3 算法确认为什么是AES-CBC而不是AES-GCM代码里写的是AES/CBC/PKCS5Padding但服务端是否真的用CBC我们做了三重验证长度验证AES-CBC要求明文长度是16字节整数倍PKCS5Padding会在末尾补N个字节N16-len%16。我们用已知明文{password:123456}长度22计算补位后应为32字节加密后密文Base64长度应为4432字节→256位→Base64编码后44字符。实测密文长度确实是44。IV验证CBC模式每次加密需要不同IV但某智网APP的IV是固定的b数组这说明它不追求语义安全只防明文分析。GCM模式必须用随机IV否则完全失效而这里IV固定排除GCM。服务端响应验证我们用Python的pycryptodome库用相同密钥、IV、算法解密服务端返回的密文得到可读JSON再用AES/GCM/NoPadding尝试直接报错ValueError: MAC check failed。三重验证闭环结论可靠。4. JNI层密钥与算法还原当Java层找不到密钥时4.1 警惕“假密钥”Java层密钥只是壳真密钥在so里上面我们拿到了1234567890abcdef但用它解密/device/command的密文失败了。为什么因为/device/command的加密根本不在Java层我们Hook了所有CryptoHelper.encrypt()调用发现它只被/auth/login和/device/heartbeat调用而/device/command走的是另一个路径NativeCrypto.encrypt(byte[])。这说明某智网把核心设备指令的加密逻辑下沉到了JNI层用C实现密钥也藏在so文件里。我们用file app-debug.apk确认APK里有lib/arm64-v8a/libcrypto.so然后用readelf -d libcrypto.so | grep NEEDED查看依赖发现只依赖libc.so和liblog.so没有其他第三方库说明是纯手写AES。接下来是重头戏从so里挖密钥。4.2 IDA Pro动态调试为什么不用Ghidra因为符号表还在Ghidra对无符号表的so逆向效果差IDA Pro的F5伪代码更贴近C语言习惯。我们用IDA打开libcrypto.so搜索字符串AES_encrypt没结果——因为函数名被strip了。但搜索AES也没结果因为开发者连字符串都删了。这时要换思路找AES的S盒Substitution Box。标准AES的S盒是一个256字节的固定数组开头是0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5...。我们在IDA的Search → Sequence of Bytes里输入63 7C 77 7B F2 6B 6F C5瞬间定位到.rodata段的一个数组。双击进去按R键将其转为byte数组命名为sbox。接着找调用sbox的地方右键sbox→Xrefs to→ 发现被sub_8A3C调用。点进sub_8A3CF5反编译看到核心逻辑int __fastcall sub_8A3C(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { // ... 初始化代码 v11 *(_QWORD *)(a4 8); // 密钥指针 v12 *(_QWORD *)(a4 16); // IV指针 // ... AES轮函数调用 return result; }a4是第四个参数根据ARM64调用约定前8个参数用x0-x7寄存器传递所以a4对应x4寄存器。我们Hooksub_8A3C打印x4指向的内存Interceptor.attach(Module.findExportByName(libcrypto.so, sub_8A3C), { onEnter: function (args) { console.log([JNI] Key ptr: args[4]); console.log([JNI] Key hex: hexdump(Memory.readByteArray(args[4].add(0x8), 16))); console.log([JNI] IV hex: hexdump(Memory.readByteArray(args[4].add(0x10), 16))); } });运行APP触发设备控制日志输出[JNI] Key ptr: 0x7a3c124560 [JNI] Key hex: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [JNI] IV hex: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00全是0说明密钥不是静态存储而是运行时生成。继续看sub_8A3C的汇编发现它调用了sub_1234而sub_1234里有__android_log_print调用打印了key_gen: %s。我们Hook这个log发现它输出key_gen: device_key_2021——原来密钥是拼接生成的再追sub_1234发现它用getDeviceId()设备唯一标识和硬编码字符串salt_2021做SHA256取前16字节作为AES密钥。这才是真密钥。4.3 设备ID获取为什么不能用Build.SERIAL因为被篡改了getDeviceId()不是简单的android.os.Build.SERIAL而是调用了TelephonyManager.getDeviceId()但在Android 10上已被废弃APP做了兼容先试getImei()失败则用Settings.Secure.getString(context.getContentResolver(), android_id)。但我们Hook后发现它返回的android_id是9774d56d682e549c——这是模拟器的默认ID说明APP在检测到模拟器时会返回固定值。真机上我们用ADB命令adb shell settings get secure android_id查到真实ID再用Python计算import hashlib device_id 8a1b2c3d4e5f6789 # 真机获取的android_id salt salt_2021 key hashlib.sha256((device_id salt).encode()).digest()[:16] print(key.hex()) # 输出32位hex字符串即AES密钥算出的密钥解密/device/command密文成功得到Protobuf原始数据。5. 解密脚本落地从理论到可执行的Python工具5.1 完整解密流程四步缺一不可基于以上分析我们写了一个decrypt_zhiwang.py脚本支持三种接口解密#!/usr/bin/env python3 # -*- coding: utf-8 -*- import base64 import hashlib import json from Crypto.Cipher import AES from Crypto.Util.Padding import unpad class ZhiWangDecryptor: def __init__(self, device_idNone): self.device_id device_id or 9774d56d682e549c # 模拟器默认 self.java_key b1234567890abcdef self.java_iv bfedcba9876543210 def decrypt_login(self, encrypted_b64): 解密 /auth/login 的 password 字段 encrypted base64.b64decode(encrypted_b64) cipher AES.new(self.java_key, AES.MODE_CBC, self.java_iv) decrypted unpad(cipher.decrypt(encrypted), AES.block_size) return decrypted.decode(utf-8) def decrypt_heartbeat(self, encrypted_b64): 解密 /device/heartbeat 的 data 字段带 X-Enc- 前缀 if encrypted_b64.startswith(X-Enc-): encrypted_b64 encrypted_b64[6:] encrypted base64.b64decode(encrypted_b64) cipher AES.new(self.java_key, AES.MODE_CBC, self.java_iv) decrypted unpad(cipher.decrypt(encrypted), AES.block_size) return json.loads(decrypted.decode(utf-8)) def decrypt_command(self, encrypted_b64): 解密 /device/command 的 payload 字段JNI层 encrypted base64.b64decode(encrypted_b64) # 生成JNI密钥 salt salt_2021 key hashlib.sha256((self.device_id salt).encode()).digest()[:16] # IV是固定的iv_2021的SHA256前16字节 iv hashlib.sha256((iv_2021).encode()).digest()[:16] cipher AES.new(key, AES.MODE_CBC, iv) decrypted unpad(cipher.decrypt(encrypted), AES.block_size) # Protobuf解码需提前编译proto文件 # from device_command_pb2 import DeviceCommand # cmd DeviceCommand() # cmd.ParseFromString(decrypted) # return cmd return decrypted # 返回原始bytes供Protobuf解析 if __name__ __main__: decryptor ZhiWangDecryptor(device_id8a1b2c3d4e5f6789) print(decryptor.decrypt_login(U2FsdGVkX1...))注意Crypto.Cipher.AES需要安装pycryptodome不是pycrypto后者已停止维护。安装命令pip install pycryptodome。5.2 Protobuf schema还原没有.proto文件怎么解/device/command的密文解密后是Protobuf二进制但没有.proto文件。我们用protoc --decode_raw encrypted.bin看原始字段1: device_001 2: 1 3: ON 4: 1619356800字段号1、2、3、4对应设备ID、指令类型、状态、时间戳。我们手动写了一个device_command.protosyntax proto3; message DeviceCommand { string device_id 1; int32 command_type 2; string status 3; int64 timestamp 4; }然后用protoc --python_out. device_command.proto生成Python模块插入到解密脚本中即可。5.3 实操避坑三个血泪教训密钥时效性陷阱某智网在2021年6月更新了APP把salt_2021改成了salt_2021_v2但Java层密钥没变。很多团队以为“逆向一次永久可用”结果两周后脚本全挂。我们的解决方案是在脚本里加版本检测从APK的AndroidManifest.xml里读取android:versionName自动匹配salt字符串。Base64变种问题某智网在部分接口用了URL安全Base64-和_代替和/且不补。Python的base64.b64decode()会报错。解决方法是先标准化def safe_b64decode(s): s * (4 - len(s) % 4) # 补号 s s.replace(-, ).replace(_, /) # URL安全转标准 return base64.b64decode(s)多线程并发解密失败当批量解密1000条心跳数据时脚本偶尔卡死。排查发现是Crypto.Cipher.AES对象不是线程安全的。解决方案每个线程创建独立的cipher实例或用threading.local()缓存。最后再分享一个小技巧某智网的加密逻辑其实有“测试开关”。在APP的assets/config.json里有一个debug_crypto: true字段开启后所有加密函数会打印明文和密文到logcat。我们用adb logcat | grep CRYPTO就能实时看到加解密过程比逆向快十倍。这个开关在发布版里被删了但如果你有Debug版APK一定要先检查assets目录——很多“高难度”逆向其实早被开发者留了后门。

相关文章:

Android APP通信协议逆向:AES+Base64+Protobuf加密还原实战

1. 这不是“破解”,而是对通信协议的工程化还原2021年4月那会儿,我接到一个需求:某智网APP在登录、设备控制、状态上报等关键链路中,所有HTTP/HTTPS请求体和响应体都是密文,看不到明文字段,连基础的接口字段…...

ab、Postman、JMeter并发测试真相:协议层、运行时与系统瓶颈解析

1. 为什么你测出来的“并发”根本不是并发——从一次线上服务雪崩说起上周五下午三点,我们一个核心订单查询接口突然响应时间从80ms飙升到2.3秒,错误率冲到17%,监控大盘一片血红。运维拉出负载曲线,CPU和内存都正常;开…...

超越准确率:基于数据集特性的归一化性能度量设计与实践

1. 项目概述与核心问题在机器学习项目里,评估模型性能是绕不开的一环。我们最熟悉的老朋友——准确率、精确率、F1分数——确实简单直观,拿来跟业务方汇报也容易讲清楚。但干得久了,尤其是在处理一些“非标准”数据集时,你总会隐隐…...

AI专著生成攻略:实测优质AI工具,高效完成20万字专著撰写!

学术专著的核心价值在于其内容的系统性以及逻辑的完整性,但是,这恰恰是写作过程中最具挑战性的部分。与期刊论文只关注某一个具体问题不同,专著要求建立一个完整的框架,涵盖绪论、理论基础、核心研究、应用拓展和结论。这就要求各…...

如何快速实现文档自动化下载:免费浏览器脚本终极指南

如何快速实现文档自动化下载:免费浏览器脚本终极指南 【免费下载链接】kill-doc 看到经常有小伙伴们需要下载一些免费文档,但是相关网站浏览体验不好各种广告,各种登录验证,需要很多步骤才能下载文档,该脚本就是为了解…...

机器学习笔记本崩溃深度解析:高频错误类型、根因与实战避坑指南

1. 项目概述与核心价值 在机器学习(ML)项目开发中,尤其是在Jupyter Notebook这类交互式环境中,代码执行到一半突然崩溃,弹出一堆令人费解的红色错误信息,是每个开发者都经历过的“日常”。这些崩溃不仅打断…...

AI专著写作秘籍大公开!实测4款工具,一键生成20万字专著超高效!

学术专著写作难题与AI工具解决方案 对于许多从事学术研究的人来说,撰写学术专著面临的最大挑战,可能就是“有限的时间”与“不断增长的需求”的矛盾。写一本专著通常需要3到5年,甚至更长的周期,而研究者们在日常生活中还需要承担…...

Android Native逆向实战:Frida与IDA协同分析ART内存模型

1. 这不是“游戏外挂开发指南”,而是一次对移动应用安全边界的诚实测绘你打开手机里那个图标是蓝色小鸟、背景是木头和石头的《愤怒的小鸟》——它早已不是2010年那个靠物理引擎惊艳全场的休闲游戏,而是被无数人遗忘在角落、却仍静静躺在旧安卓设备里的“…...

基于MultiFold无分箱反卷积的轻子-喷注方位角不对称性测量

1. 项目概述与核心物理动机在粒子物理的高能前沿,我们常常通过“撞击”基本粒子来窥探其内部结构,深度非弹性散射(DIS)就是其中最经典、最有力的探针之一。想象一下,你用一束极高能量的电子(或正电子&#…...

SHAP值在时间感知研究中的应用:从机器学习预测到认知机制解释

1. 项目概述:当时间感知遇上可解释AI 在认知科学和神经工程领域,时间感知一直是个迷人的谜题。我们如何感知时间的流逝?为什么有时“度日如年”,有时又“光阴似箭”?传统研究多依赖于行为实验和理论模型,但…...

抖音批量下载器终极指南:如何3分钟搞定无损音乐提取与高效素材管理

抖音批量下载器终极指南:如何3分钟搞定无损音乐提取与高效素材管理 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fa…...

如何高效提取Wallpaper Engine资源?RePKG专业工具全解析

如何高效提取Wallpaper Engine资源?RePKG专业工具全解析 【免费下载链接】repkg Wallpaper engine PKG extractor/TEX to image converter 项目地址: https://gitcode.com/gh_mirrors/re/repkg RePKG是一款专门为Wallpaper Engine用户设计的专业工具&#xf…...

免费Chrome插件:一键保存完整网页的终极解决方案

免费Chrome插件:一键保存完整网页的终极解决方案 【免费下载链接】full-page-screen-capture-chrome-extension One-click full page screen captures in Google Chrome 项目地址: https://gitcode.com/gh_mirrors/fu/full-page-screen-capture-chrome-extension …...

抖音下载神器:3步搞定批量无水印下载,效率提升95%

抖音下载神器:3步搞定批量无水印下载,效率提升95% 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallbac…...

终极资源嗅探指南:猫抓浏览器扩展帮你轻松捕获网页媒体资源

终极资源嗅探指南:猫抓浏览器扩展帮你轻松捕获网页媒体资源 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 在当今数字时代&#xff0c…...

Reloaded-II 模组加载器:深入解析依赖管理机制与循环依赖解决方案

Reloaded-II 模组加载器:深入解析依赖管理机制与循环依赖解决方案 【免费下载链接】Reloaded-II Universal .NET Core Powered Modding Framework for any Native Game X86, X64. 项目地址: https://gitcode.com/gh_mirrors/re/Reloaded-II Reloaded-II 作为…...

如何快速配置Atmosphere破解系统:Switch游戏体验全面升级指南

如何快速配置Atmosphere破解系统:Switch游戏体验全面升级指南 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 想要让你的Nintendo Switch游戏加载速度提升65%,帧率翻…...

3分钟让直播音质专业级:OBS-VST插件终极使用指南

3分钟让直播音质专业级:OBS-VST插件终极使用指南 【免费下载链接】obs-vst Use VST plugins in OBS 项目地址: https://gitcode.com/gh_mirrors/ob/obs-vst 你是否曾为直播时观众抱怨"声音太吵"、"听不清说话"而烦恼?或者精心…...

物理视角下的神经网络:从表达性、统计到动力学的统一理解框架

1. 从物理视角看神经网络:为什么我们需要新的理解框架 如果你和我一样,在实验室里泡了十几年,从早期的多层感知机一路跟到现在的Transformer和扩散模型,你可能会有一个强烈的感受:我们手里的工具越来越强大&#xff0c…...

小红书下载终极指南:5分钟掌握无水印批量下载技巧

小红书下载终极指南:5分钟掌握无水印批量下载技巧 【免费下载链接】XHS-Downloader 小红书(XiaoHongShu、RedNote)链接提取/作品采集工具:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品、用户链接&#xf…...

抖音下载器完整指南:3分钟批量下载无水印视频和音乐

抖音下载器完整指南:3分钟批量下载无水印视频和音乐 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support…...

别再死记硬背MFCC公式了!用Python手把手带你复现FBank/MFCC特征提取全流程

从零实现语音特征提取:用Python拆解FBank与MFCC的数学之美语音识别技术正悄然改变我们与机器交互的方式,但很少有人真正理解声音是如何被转化为机器可读的数字特征的。本文将带您深入音频信号处理的数学世界,通过Python代码亲手实现从原始波形…...

微信聊天记录永久保存终极指南:用WeChatExporter告别数据焦虑

微信聊天记录永久保存终极指南:用WeChatExporter告别数据焦虑 【免费下载链接】WeChatExporter 一个可以快速导出、查看你的微信聊天记录的工具 项目地址: https://gitcode.com/gh_mirrors/wec/WeChatExporter 你是否曾有过这样的担忧——手机突然损坏&#…...

终极Windows进程内存操控指南:Xenos DLL注入器深度实战解析

终极Windows进程内存操控指南:Xenos DLL注入器深度实战解析 【免费下载链接】Xenos Windows dll injector 项目地址: https://gitcode.com/gh_mirrors/xe/Xenos 在Windows系统开发与安全研究领域,DLL注入技术一直是连接应用程序与系统底层的关键桥…...

如果你要设计一个“个人助理“Agent,记忆系统应该如何分层?

这个问题挺有意思的,个人助理 Agent 的记忆系统,核心是分层设计——不是所有记忆都放一个地方,得按时效性、访问频率、重要性分层。 我之前做过一个个人助理项目,一开始就把所有记忆都扔向量库里,结果检索慢、成本高、还容易检索到过时信息。后来重构成分层架构,效果好很多。 …...

AI Agent 在工具调用失败时,如何设计一个智能的降级策略?

这个问题挺关键的,工具调用失败在 AI Agent 系统里是常态,不是异常。核心思路是——先分类,再分级,最后兜底。 我之前做 Agent 编排系统的时候,工具调用成功率大概在 85% 左右,剩下 15% 都得靠降级策略兜住。如果没设计好,整个 Agent 就会频繁报错,用户体验很差。 第一步:错误…...

魔兽争霸3闪退修复终极指南:5步让你的经典游戏重获新生

魔兽争霸3闪退修复终极指南:5步让你的经典游戏重获新生 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 还在为魔兽争霸3闪退而烦恼吗&…...

终极iOS越狱实战指南:解锁iPhone隐藏功能与深度定制方案

终极iOS越狱实战指南:解锁iPhone隐藏功能与深度定制方案 【免费下载链接】Jailbreak iOS 26.4 - 26, 17 - 17.7.5 & iOS 18 - 18.7.3 Jailbreak Tools, Cydia/Sileo/Zebra Tweaks & Jailbreak News Updates || AI Jailbreak Finder 👇 项目地址…...

Sketch MeaXure:5分钟掌握设计标注终极解决方案

Sketch MeaXure:5分钟掌握设计标注终极解决方案 【免费下载链接】sketch-meaxure 项目地址: https://gitcode.com/gh_mirrors/sk/sketch-meaxure 你是否还在为设计稿标注而烦恼?Sketch MeaXure正是为你量身打造的现代化设计标注神器!…...

保姆级教程:用CellChat v2 R包分析10x Visium空间转录组数据,手把手搞定细胞通讯网络

空间转录组细胞通讯分析全流程:从CellChat v2安装到高级可视化空间转录组技术正在彻底改变我们对组织微环境的理解,而细胞间通讯分析则是解锁组织功能奥秘的关键钥匙。作为一名刚接触10x Visium数据的生物信息学研究者,你可能已经完成了基础的…...