福Star APP 门禁逆向思路

本文记录分析福Star APP - 身份码的逆向过程及分析思路。当然,本分析的结果大概是失败的,留待有志之士补完。

路由#

身份码的生成主要由三个路由组成,分别为 /member/getBizCardNo, /virtual-card/member/getCertificate/member/generateQRCode,相应的域均为 [https://fsdqrcode.app.fjnu.edu.cn]。

getBizCardNo#

根据观察,该接口用处不大。在实践时,该接口总是返回{"code":-100,"message":"数据异常或数据为空"},判断在实际场景中该接口无效,应使用getCertificate及generateQRCode。

getCertificate#

  • 接口路径:/virtual-card/member/getCertificate

  • 请求类型:POST

  • Header:userToken(用户token)、sno(学号)

  • Body:{"bizType":"1","pubKey":"..."}

  • 响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        {
    "code": 0,
    "message": "成功",
    "data": {
    "cert": {
    "platformSign": "...",
    "effectiveTime": ...,
    "vCardNo": "...", // generateQRCode中使用的vCardNo
    "vCardTypeSign": "...",
    "publicKey": "...", // 即请求中发送的publicKey
    "vCardTypeNo": "1002",
    "vCardTypePublicKey": "...",
    "version": "1.0",
    "algorithm": 2,
    "ts": ...
    },
    "vcardNo": null
    }
    }

此接口应为获取生成二维码所需的核心凭证。pubKey可以为任意内容,为空则会提示参数缺失。

逆向工作#

通过JADX对APP进行逆向。由于APP进行了加壳,使用Frida对代码进行了动态分析。最终得到以下结论:

  • 业务入口: KabawHomePageModel.updateCertification() 方法为了请求接口,需要 pubKey。
  • 获取公钥: 调用 VCertificationManager.getInstance().getVCardPublicKey() 来获取公钥字符串。
  • 生成密钥别名: getVCardPublicKey 方法内部首先调用 getVCodeKeyName() 方法,将 用户Token + 业务类型Value + 用户ID 拼接成一个唯一的密钥别名 (Key Alias)。
  • 路由分发: 接着,请求被交给 MsgSealRouter,它通过一个名为 AndroidRouter 的组件化路由框架,发起了一个路径为 “/getPublicKeyWithAlg” 的跨模块服务请求。
  • 服务实现: 一个带有 @RouterPath(“/getPublicKeyWithAlg”) 注解的类接收到该请求,并将处理逻辑层层转发,依次经过 TsbApiServer -> NativeApiServices.TsbServer。
  • JNI 接口: 最终,调用到达了 Java 层的终点——一个 native 方法:public static native String getPublicKeyWithAlg(int i, String str);。

最终指向 libmsgsealsdk.so 库中的 Java_com_msgseal_service_services_NativeApiServices_TsbServer_getPublicKeyWithAlg 函数,关于此处的工作没有再进行下去。

generateQRCode#

  • 接口路径:/member/generateQRCode

  • 请求类型:POST

  • Header:userToken(用户token)、sno(学号)

  • Body:

    1
    2
    3
    4
    5
    {
    "vCardNo":"...", // 上一步获取到的vCardNo
    "studNo":"...", // 学号
    "busNo":"..." // 学号
    }
  • 响应

    1
    2
    3
    4
    5
    6
    7
    {
    "code": 0,
    "message": "成功",
    "data": {
    "qrCode": "..."
    }
    }

客户端在获取到响应的qrCode后,将其转换为二维码显示在用户界面,也就是最终的身份码。

调试得出以下结论:

  • 请求体中的studNo和busNo可以随意修改,但服务器并不会取信,只会使用header中经过验证的sno。
  • vCardNo可以随意修改,但只有相应卡号存在于数据库中才能生成qrCode,否则会报错。
  • 对于同一用户,其对应的vCardNo值(18位文本型整数)始终不变。

QRCode 不同部分含义#

部分 (Part) 值 (Value) 来源/含义分析
应用/业务标识 FSQRFSTAE1010112 [确认] 固定前缀。FSQRFSTAE 可能是应用名缩写,1010112 是具体的业务类型代码。
学号/业务号 xxx [新发现/确认] 直接来源于请求 Body 中的 studNo 和 busNo。
未知代码 33 含义仍不明确,可能是固定的业务子类型、校验码或者版本标识。
虚拟卡号 xxxx [确认] 来源于请求 Body 中的 vCardNo。
分隔符 ** [确认] 固定分隔符。
学号/业务号 (重复) xxx [确认] 再次使用了 studNo / busNo。
分隔符 * [确认] 固定分隔符。
过期时间戳 20250922144011 [确认] 服务器生成的过期时间 (YYYYMMDDHHMMSS)。
有效时长 18000 [确认] 很可能是有效时长(18000秒 = 5小时)。
SM2 签名 [确认] 服务器对前面所有数据生成的 SM2 签名。
版本号 V1.0 [确认] 固定的版本号。

猜测的后端处理流程#

  • 基于userToken确认用户登录状态。
  • 使用userToken在数据库中验证学号studNo。
  • 对用户提交的vCardNo进行校验。
  • 拼接二维码字符串。
  • 签名并返回,得到qrCode。

细碎部分#

  • 两个接口均可进行重放。在userToken不失效的情况下一直有效。
  • /virtual-card/member/getCertificate 接口的作用似乎只是单纯为了获取到vCardNo,pubKey可以随意修改的操作推翻了我之前的部分设想。疑似为:客户端不知道或者不确定当前的 vCardNo,于是它调用 getCertificate 接口;服务器通过 userToken 识别用户,返回包含 vCardNo 的 cert 对象;客户端从 cert 对象中提取出 vCardNo,而 cert 对象中的其它信息在当下并未被直接使用。那么是否可以通过直接修改 /member/generateQRCode 接口的请求参数实现伪造?但经过测试,在调用 /member/generateQRCode 接口时,修改studNo、busNo不会影响到最终结果;修改vCardNo时可以正常生成带有不同vCardNo的qrCode,但是刷码时依然显示自己的名字。