本文记录分析福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,但是刷码时依然显示自己的名字。