某 LNG 巡检 App 登录协议逆向实战:双因子设备指纹、RSA 密码加密与 Hook 定位

免责声明:本文仅用于安全研究、协议分析与技术交流学习,所涉对象为作者在授权范围内持有的账号与设备。文中软件名称统一以「某 App」代替。请勿将本文任何内容用于未授权访问、数据爬取、绕过风控或其他侵犯他人权益与违反法律法规的行为。因滥用造成的一切后果由使用者自行承担。阅读即视为同意以上条款。

一、要解决的问题

某 App 是一款能源行业的移动端应用,主功能之一是 LNG 储配站的巡检任务管理。在做接口协议分析时,遇到三个典型的工程化难点,也是本文的核心:

  1. 登录为什么有时要短信验证码、有时不要?同一个账号在不同设备上行为不一致,需要定位服务端的判定依据。
  2. 登录密码不是明文提交,请求体里是一段 Base64 密文,需要还原加密算法、Padding 和编码细节,否则无法脱离 App 自行登录复现。
  3. 业务接口分散在宿主 App + 内嵌 uni-app 小程序两套体系,token 还要二次交换,需要厘清调用链才能复现任务列表这类核心接口。

下文按「静态反编译定位 → 关键算法还原 → 动态 Hook 验证」的顺序展开,每一步都给出实际用到的命令与判断依据。

二、反编译:从 APK 到可读代码

2.1 基础信息提取

拿到 app.apk 后,先看清楚它的结构和加固情况:

# 解出 manifest、资源、smali,-r 跳过资源回编、-s 可选跳过 dex(这里需要 dex 所以不加 -s)
apktool d app.apk -o apktool_out

# 单独看签名与包信息
aapt dump badging app.apk | grep -E "package|launchable-activity"
unzip -l app.apk | grep -E "classes.*dex|\.so"

反编译产物的实际目录结构如下,注意这是一个多 dex 应用:

apktool_out/
├── AndroidManifest.xml
├── apktool.yml
├── assets/                 # 内含 uni-app 小程序包
├── lib/                    # native .so
├── smali/                  # classes.dex
├── smali_classes2/         # classes2.dex
├── smali_classes3/
└── smali_classes4/

难点 1:多 dex 导致类不在预期目录。 一个类到底落在 smali 还是 smali_classes4,取决于打包时的 dex 分包结果,不能只在 smali/ 里找。跨全部 dex 目录搜索是基本功。

2.2 跨 dex 全局搜索关键词

逆向定位的第一招永远是「按字符串/类名全局搜索」。登录相关的入口,可以从 Activity 名、接口路径、字段名三个维度切入:

# 按业务关键词搜登录入口
grep -rn "LoginActivity" apktool_out/smali*

# 按接口路径搜网络层(路径片段往往是常量,最稳)
grep -rn "getAppToken" apktool_out/smali*
grep -rn "mobileDoubleFactorVerify" apktool_out/smali*

# 按字段名搜请求体组装位置
grep -rn "verificationCode" apktool_out/smali*
grep -rn "imageCode" apktool_out/smali*

通过这种方式,本案例定位到的关键 smali 类是:

com/某/module/login/LoginActivity.smali        # 登录页与预检
com/某/util/NetworkUtil.smali                  # 业务网络封装
com/某commonlib/BaseNetworkUtil.smali          # 网关头注入
com/某/widget/SmsVerifyCodeDialog.smali        # 短信验证码弹窗

2.3 配合 jadx 看伪代码

smali 适合精确定位和后续插桩,但读逻辑还是 Java 伪代码快。两者配合:

# 生成可读 Java(-d 输出目录,--show-bad-code 容忍反编译失败的方法)
jadx -d jadx_out app.apk

# 只想快速看某个类,用图形界面交叉引用跳转
jadx-gui app.apk

实践经验:用 jadx 读懂逻辑、记下方法签名,再回到 smali 里按签名做 Hook 或插桩,定位效率最高。

三、难点拆解一:登录为什么时要时不要验证码

3.1 静态结论:判定发生在「预检」而非「登录」

LoginActivity 的预检方法可以看到,它在真正登录前先打了一个 mobileDoubleFactorVerify,请求体只有三个字段:

{
  "meId": "<deviceId>",
  "loginCode": "<username>",
  "domainName": "mportal.crcgas.com"
}

请求头部分由 BaseNetworkUtil 统一注入:

X-Apig-AppCode: 1a52f7e8e19b4490a2953da9bcf0e9544a5739e9c850421898d897f1e21f9ca4
AuthType: APP

服务端返回后,客户端的分支判定逻辑是:

预检返回客户端行为
verifyResult == false直接账号密码登录,不弹任何验证码
verifyResult == true && authMethod == "captcha"弹图形验证码,带 imageId/imageCode 登录
verifyResult == true(其他)走短信弹窗,带 verificationCode 登录

关键点:password 没有参与预检,预检的唯一可变输入就是 meId(设备 ID)。

3.2 设备指纹 meId 的来源

继续跟 meId 的取值,定位到设备 ID 工具类。其逻辑可归纳为:

  • Android 10+ 且有 READ_PHONE_STATE 权限:使用本地持久化的 device_id首次生成值为 System.currentTimeMillis() + random(0..8)
  • 低版本有权限:优先 TelephonyManager.getDeviceId()
  • 无权限:返回固定字符串 Device ID without permission

这个值会被持久化在应用数据库 default_databaseKEY_VALUE_PAIR 表中,键名 device_id

难点 2 的本质:meId 不是普通展示字段,而是服务端风控里的「可信设备」凭据。 一旦某个 meId 在某账号上验证成功过,服务端会把它标记为可信,后续预检直接 verifyResult=false,所以「老设备不要验证码」;换设备/重置后 meId 变化,被当作新设备,于是要短信。这解释了「时要时不要」的根因——它由服务端按设备指纹决定,客户端无法单方面绕过。

工程上能做的正确处理只有两件事:

  • 让已验证设备复用同一份持久化 device_id,避免无谓地触发二次验证;
  • 在确实被要求验证码时,把图形/短信验证码流程走通,而不是当作失败。

四、难点拆解二:RSA 密码加密还原

4.1 静态定位加密点

登录请求体里的 password 是密文。在网络层方法里向上回溯,会发现密码在交给 login() 之前先过了一层加密工具:

RSAEncrypt.encryptByPublicKey(password)

读该工具类,提取出完整的加密参数(这是脱离 App 复现登录的核心,必须 100% 对齐):

  • 公钥(X509/SPKI,Base64 DER)

    MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIsDNTSUWkTF+tfRwZoTUCGYVt+V1uFAJxW+qg/f92N+i3Csw0p9boaid+XssOKZGnwxeHQPeO6l9FWAc8zWr2sCAwEAAQ==
    
  • CipherRSA/ECB/PKCS1Padding

  • 输出编码:Android Base64.NO_WRAP(无换行)

  • 提交前处理:把密文里的 + 全部替换为 %2B

难点 3:易踩的三个坑。 ① PKCS1Padding 每次加密结果不同(含随机填充),不能用「固定密文比对」来验证算法对错,要用「能否成功登录」来验证;② Android 默认 Base64.NO_WRAP,若用带换行的标准 Base64 会导致服务端解析失败;③ + → %2B 这步是业务自定义的 URL 安全处理,漏了会偶发失败(仅当密文恰好含 + 时复现,极具迷惑性)。

4.2 还原实现(可复现模板)

下面给出两种等价实现,参数严格对齐上面的静态结论。公钥与算法来自反编译事实,可直接使用;账号密码请按你自己授权的凭据补齐。

Node.js 版:

import { constants, publicEncrypt } from "node:crypto";

// 来自反编译的登录公钥(X509/SPKI, DER, base64)
const PUB_DER_B64 =
  "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIsDNTSUWkTF+tfRwZoTUCGYVt+V1uFAJxW+qg/f92N+i3Csw0p9boaid+XssOKZGnwxeHQPeO6l9FWAc8zWr2sCAwEAAQ==";

export function encryptPasswordForLogin(password) {
  if (!password) throw new Error("password is required");
  const encrypted = publicEncrypt(
    {
      key: Buffer.from(PUB_DER_B64, "base64"),
      format: "der",
      type: "spki",
      padding: constants.RSA_PKCS1_PADDING, // 对齐 PKCS1Padding
    },
    Buffer.from(String(password))
  );
  // 对齐 Android Base64.NO_WRAP + 业务自定义的 +→%2B
  return encrypted.toString("base64").replaceAll("+", "%2B");
}

Android/Kotlin 版(与原 App 等价,用于交叉验证):

import android.util.Base64
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher

object RsaCrypto {
    private const val PUB_DER_B64 =
        "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIsDNTSUWkTF+tfRwZoTUCGYVt+V1uFAJxW+qg/f92N+i3Csw0p9boaid+XssOKZGnwxeHQPeO6l9FWAc8zWr2sCAwEAAQ=="

    fun encryptPasswordForLogin(password: String): String {
        require(password.isNotEmpty()) { "password is required" }
        val keyBytes = Base64.decode(PUB_DER_B64, Base64.DEFAULT)
        val publicKey = KeyFactory.getInstance("RSA")
            .generatePublic(X509EncodedKeySpec(keyBytes))
        val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        val out = cipher.doFinal(password.toByteArray(Charsets.UTF_8))
        return Base64.encodeToString(out, Base64.NO_WRAP).replace("+", "%2B")
    }
}

4.3 登录请求组装

还原后的账号密码主流程(仅当预检 verifyResult=false 时):

POST https://openapi.crcgas.com/mobile/cas/getAppToken?service=mportal.crcgas.com&clientSecret=28f37e3802e546f5ac453fa22af78508
Headers:
  X-Apig-AppCode: 1a52f7e8e19b4490a2953da9bcf0e9544a5739e9c850421898d897f1e21f9ca4
Body:
  {
    "username": "<username>",
    "password": "<上面还原的 RSA 密文>",
    "meId": "<deviceId>"
  }

captcha 分支额外加 imageId/imageCode,短信分支额外加 verificationCode,二者互斥。

五、动态验证:Frida Hook 思路

静态还原后必须用动态 Hook 交叉验证「我算的密文 / 我猜的字段,和 App 实际发出去的是否一致」。

5.1 Hook 加密函数,核对明文与密文

直接 Hook 那一层加密方法,打印入参(明文)和返回(密文),与自己实现的结果比对:

// frida -U -f <pkg> -l hook.js
Java.perform(function () {
  var Enc = Java.use("com.某.util.RSAEncrypt");
  // 方法名按反编译实际签名替换
  Enc.encryptByPublicKey.overload("java.lang.String").implementation = function (plain) {
    var ret = this.encryptByPublicKey(plain);
    console.log("[RSA] plain =", plain);
    console.log("[RSA] cipher=", ret);
    return ret; // 不改行为,仅观测
  };
});

5.2 Hook 网络层,dump 真实请求体

为了确认请求体字段(尤其是 meId、各验证码字段在不同分支下的取舍),Hook 统一网络封装:

Java.perform(function () {
  var Net = Java.use("com.某.util.NetworkUtil");
  // 以 login 方法为例,按实际重载签名调整
  Net.login.overload(/* ...args... */).implementation = function () {
    console.log("[login] args =", JSON.stringify([].slice.call(arguments)));
    return this.login.apply(this, arguments);
  };
});

如果业务层方法太多,更通用的做法是 Hook OkHttp 的 RequestBody/Interceptor,统一抓全量出网请求。

5.3 解决 native .so 加载时机

当关键逻辑在 lib/ 下的 native 层、或类在 App 启动较晚才加载时,过早 Hook 会找不到符号。两个常用对策:

// 对策 A:等目标 so 加载后再 hook native 导出
var pending = Module.findExportByName("libtarget.so", "Java_xxx");
if (!pending) {
  // 用 dlopen 拦截,等 so 真正加载
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) { this.path = args[0].readCString(); },
    onLeave: function () {
      if (this.path && this.path.indexOf("libtarget.so") >= 0) {
        // 此处再做 native hook
      }
    },
  });
}
// 对策 B:Java 层用 spawn 模式 + Java.perform 内做类存在性判断
Java.perform(function () {
  try { Java.use("com.某.module.login.LoginActivity"); }
  catch (e) { console.log("class not loaded yet"); }
});

若目标做了 Frida 检测,可优先考虑 spawn 模式、改默认端口、或使用 Gadget 注入等方式,本文不展开对抗细节。

六、业务链路:宿主 App 与内嵌小程序的 token 交换

某 App 的巡检业务实际跑在内嵌的 uni-app 小程序里。反编译 assets/ 会看到小程序包,其请求走的是另一套网关:

apigHost: https://sms-openapi.crcgas.com/apis/
x-apig-appcode: 9fda8c7b6ff94e1ab063c98c101c2904aefd6062068342fd979eea2e9f8e1210

难点 4:token 不能直接复用。 普通登录 token 不能直接请求小程序网关,必须先做一次交换:

POST https://openapi.crcgas.com/mobile/exchangeToken?mobileAppId=2d2cc5f3f0614d64b677bdd8b4790da1
Headers:
  AuthType: APP
  Authorization: Bearer <普通登录 token>
  X-Apig-AppCode: 1a52f7e8e19b4490a2953da9bcf0e9544a5739e9c850421898d897f1e21f9ca4

交换得到的新 access_token 才能请求小程序接口。任务列表的岗位上下文 companyCode 来自:

POST https://sms-openapi.crcgas.com/apis/mgt/security/getLoginData   (body: {})
→ userBasicVo.currentPost.treeNodeId  即 companyCode

最终任务列表接口:

POST https://sms-openapi.crcgas.com/apis/factoryBusiness/station/patrol/task/findStationmgrPatrolTaskListPage
Body:
  {
    "pageNum": 1,
    "pageSize": 20,
    "condition": { "isHistory": false, "companyCode": "<treeNodeId>" }
  }

调用链小结:账密登录 → exchangeToken 换小程序 token → getLoginData 取 companyCode → 业务接口。漏掉中间任何一步,要么 401、要么缺岗位上下文返回空。

七、异常边界与上线前检查

复现/接入时,以下边界最容易出问题,建议逐项核对:

  • token 续期:登录返回里同时有 access_tokenrefresh_token(本案例 expires_in 约 15 天)。access_token 过期应优先用 refresh_token 续期接口换新,避免重新登录又触发设备风控/验证码。注意部分实现刷新响应不回 refresh_token,需保留旧值。
  • 验证码互斥:图形验证码只服务于「发送短信」一步,用后即失效;最终登录只认 verificationCode不要再带已消费的 imageId/imageCode,否则服务端校验失败。
  • Base64 与转义:RSA 密文务必无换行 + +→%2B;JSON 序列化注意字段顺序对部分老网关的影响。
  • 设备 ID 一致性:复现时固定一个已被服务端信任的 meId,不要每次随机生成,否则会反复进入短信分支。
  • 请求头完整性X-Apig-AppCodeAuthTypeAuthorization 缺一不可,且宿主网关与小程序网关的 AppCode 不同。

测试建议:

  • 用 Frida Hook 出的真实请求体作为「黄金样本」,对自己复现的请求逐字段 diff;
  • 验证 RSA 时以「能否换到 token」为准,不要比对密文;
  • 分别覆盖「可信设备直登」「captcha 分支」「短信分支」「token 过期续期」四条路径。

八、总结

本文以某 App 的登录与巡检链路为例,串起了一套通用的 Android 协议逆向方法论:

  1. 静态先行:apktool 多 dex 反编译 + 跨目录 grep 关键词 + jadx 读逻辑,快速定位入口与算法点;
  2. 算法还原讲究细节:RSA 的 Padding、Base64 换行、自定义转义任何一处错都会导致复现失败,且 PKCS1Padding 不能靠密文比对验证;
  3. 动态 Hook 收尾:Frida 观测明文/密文/请求体,解决 native so 加载时机,用真实数据校验静态结论;
  4. 业务链路要厘清:宿主 App 与内嵌小程序两套网关、token 二次交换,是能否跑通核心接口的关键。

逆向工程的价值在于理解系统、发现风险与做兼容性研究。请始终在授权范围内进行,并遵守相关法律法规与平台协议。

再次声明:本文仅供安全研究与技术学习,严禁用于任何未授权用途。

内容概要:本文介绍了一个基于Simulink的混合储能驱动永磁同步电机全系统仿真模型,涵盖了系统整体架构关键控制策略,重点实现了电流环的二阶滑模控制(STSMC)、有限集模型预测控制(FCS-MPC)和PI控制等多种先进控制方法。该模型集成了混合储能系统永磁同步电机驱动系统,能够模拟复杂工况下的动态响应、能量管理过程及多变量耦合特性,适用于高性能电机控制系统的设计、分析验证,尤其在新能源汽车、电动驱动系统和工业自动化等领域具有重要应用价值。; 适合人群:具备Simulink仿真基础、电力电子电机控制背景的高校研究生、科研人员及自动化、电气工程领域的研发工程师。; 使用场景及目标:①用于研究和对比不同电流控制策略(如STSMC、FCS-MPC、PI)在永磁同步电机系统中的动态性能、鲁棒性抗干扰能力;②支撑混合储能系统在电动驱动、新能源汽车、智能电网等领域的系统级仿真优化设计;③为先进控制算法的开发工程化落地提供高保真、模块化的仿真平台。; 阅读建议:建议结合Simulink模型相关控制理论进行对照学习,重点关注各功能模块之间的信号交互、控制逻辑设计及参数整定方法,可通过修改负载条件、切换控制模式等方式开展对比实验,深入理解系统动态行为控制效果差异。
软件概述 UG(Unigraphics NX)是一款由西门子(Siemens PLM Software)开发的交互式CAD/CAM/CAE系统。作为全球领先的产品工程解决方案,它集成了产品设计、工程仿真制造加工于一体。其功能强大且应用广泛,能够轻松实现各种复杂实体和造型的构造,为模具、汽车、航空航天及通用机械等行业提供了高性能的机械设计制图灵活性。 软件基础信息 • 支持系统: 64位 Windows 10、Windows 11 核心功能模块 一、创新设计:高效、灵活、无缝协同 全链路产品设计 涵盖从2D布局、3D建模、装配设计到图纸文档记录的各个环节,大幅提升设计吞吐量,缩短交付周期超35%。 强大的同步建模技术 打破数据壁垒,可无缝导入并直接修改来自其他CAD系统的几何模型,是跨平台协同设计的理想选择。 复杂装配管理 专为大型复杂产品打造,即使面对成千上万的零件也能从容应对,快速识别并解决数字样机中的干涉等问题。 集成设计验证 内置自动验证功能,实时监控设计是否符合公司及行业标准;结合PLM数据可视化合成,辅助工程师做出更明智的决策。 二、综合仿真(Simcenter 3D):精准预测,降低试错成本 极速前后处理 依托先进的几何引擎,将强大的分析命令几何编辑紧密集成,相比传统有限元工具,可缩短高达70%的仿真建模时间。 全方位结构分析 在同一环境中集成线性静力学、动态、疲劳及非线性分析,底层由业界顶尖的NX Nastran解算器提供支持,确保计算的高精度可靠性。 声学热管理分析 提供内外声学仿真以优化音质、降低噪音;具备一流的热传导仿真能力,帮助电子产品和工业机械实现最佳热管理方案。 多物理场耦合 简化了结构动力学、热传导、流体流动等复杂物理现象的模拟过程,消除外部数据传输错误,真实还原产品运行工况。 三、智能制造(CAM):打通从计划到车间的数字主线 全面的制造解决方案 提供从工装设计、CAM编程到机床控制器(如Sinumerik)的一体化支持,助力制定更科学的生产决策。 深度集成的PLM环境 借助Teamcenter实现数据和流程的统一管理,避免多数据库冲突,支持重用验证过的加工工艺刀具库。 车间级互联 通过DNC系统车间无缝对接,直接将加工数据和刀具清单下发至CNC机床,实现计划生产的紧密结合。 提质增效 优化NC编程刀具路径,提升表面精加工水平零件精度;减少人为错误,显著提高新机床部署成功率及制造资源利用率。 总结 UG NX 2023作为一款集成化的产品工程解决方案,通过其强大的设计、仿真和制造功能,为现代制造业提供了完整的数字化产品开发平台。无论是复杂产品的设计验证,还是精密制造的流程优化,UG NX 2023都能为工程师团队提供高效、可靠的解决方案,助力企业提升产品创新能力和市场竞争力。 适用领域 模具设计、汽车制造、航空航天、通用机械、消费电子等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值