文章目录
-
- [厚朴 APK 搜索接口分析](#厚朴 APK 搜索接口分析)
-
- [1. APK 本体角色](#1. APK 本体角色)
- [2. JS Bridge 可调用的方法](#2. JS Bridge 可调用的方法)
- [3. 真正的搜索数据接口](#3. 真正的搜索数据接口)
- [4. 搜索请求体字段](#4. 搜索请求体字段)
- [5. H5 API 签名方式](#5. H5 API 签名方式)
- [6. 搜索调用链](#6. 搜索调用链)
- [7. 额外的代理搜索分支](#7. 额外的代理搜索分支)
- [8. 结论](#8. 结论)
- 总结
-
- [阶段一:WebSocket RPC 代理搜索逻辑还原](#阶段一:WebSocket RPC 代理搜索逻辑还原)
- [阶段二:登录接口与鉴权 Token (sign_token) 的获取](#阶段二:登录接口与鉴权 Token (sign_token) 的获取)
- [阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析](#阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析)
- 阶段四:前端反调试与环境检测绕过 (版本过旧问题)
- [阶段五:直连闲鱼主搜索接口及 Mtop 签名算法](#阶段五:直连闲鱼主搜索接口及 Mtop 签名算法)
- [阶段六:x5sec 滑块风控的云端打码接口调用](#阶段六:x5sec 滑块风控的云端打码接口调用)
- 阶段七:攻防转换------基于厚朴架构的防破解与反编译思路启示
厚朴 APK 搜索接口分析
1. APK 本体角色
- APK 入口是
WebView壳,不直接写死搜索接口。 - 原生入口加载地址:
http://119.45.152.46:8000 - 证据:
MainActivity在启动时创建WebView并加载远端页面。- 原生暴露 JS Bridge:
android.callNative(...)
原始文件:
D:\software\codex-project\apk\houpu-decompiled\sources\com\taobao\fetchidlefish\MainActivity.javaD:\software\codex-project\apk\houpu-decompiled\sources\com\taobao\fetchidlefish\a.javaD:\software\codex-project\apk\houpu-decompiled\sources\defpackage\u3.java
2. JS Bridge 可调用的方法
远端前端通过 android.callNative 可调用这些原生方法:
postjsonhttpgetopen1readFilewriteFileclearckdelFileappVersion
其中真正用于搜索请求的是:
postjsonhttpget
3. 真正的搜索数据接口
远端前端脚本 remote-index.js 中,搜索封装函数是:
async function CE(...)
它最终会调用两个闲鱼搜索 API 之一:
mtop.taobao.idlemtopsearch.pc.searchmtop.taobao.idlemtopsearch.wx.search
请求统一走:
https://h5api.m.goofish.com/h5/<api>/<version>/
其中:
-
PC 搜索:
- API:
mtop.taobao.idlemtopsearch.pc.search - 版本:
1.0 appKey=34839810- 方法:
POST Referer=https://www.goofish.com/Origin=https://www.goofish.com
- API:
-
WX 搜索:
- API:
mtop.taobao.idlemtopsearch.wx.search - 版本:
1.0 appKey=12574478- 方法:
POST Referer=https://2.taobao.com/Origin=https://2.taobao.com
- API:
4. 搜索请求体字段
CE(...) 里可确认的核心参数:
keywordpageNumberrowsPerPagesortFieldsortValuepropValueStrsearchReqFromPage
可选过滤字段:
priceRangepublishDaysdisableHierarchicalSortprovincecityareaextraFilterValue.divisionList
PC 搜索请求体典型字段:
keywordpageNumber: 1fromFilter: truerowsPerPagesortField: create | modifysortValue: descsearchReqFromPage: "xyHome"searchTabType: "SEARCH_TAB_MAIN"plateform: "pc"propValueStr: {"searchFilter":"..."}
5. H5 API 签名方式
前端封装函数:
async function Xr(...)
关键逻辑:
- 从 Cookie 中提取
_m_h5_tk - 计算签名:
MD5(tokenPart + "&" + t + "&" + appKey + "&" + data) - 最终通过原生桥发起:
We("postjson", ...)- 或
We("httpget", ...)
6. 搜索调用链
调用链分两层:
MainActivity加载远端页面119.45.152.46:8000- 远端页面中的
We(...)调用android.callNative(...) - 原生
u3.java的postjson/httpget用 OkHttp 发出真实请求 - 搜索数据源落到 Goofish H5 API:
mtop.taobao.idlemtopsearch.pc.searchmtop.taobao.idlemtopsearch.wx.search
7. 额外的代理搜索分支
除了直接请求 Goofish 外,前端还有一个兜底搜索分支:
function YF(e, t)
当调用 YF("search", ...) 时,它不是普通 HTTP,而是把参数通过 WebSocket 发给服务端代理。
参数格式可见:
keyword#@##@0#@##@99999999#@##@sortField#@##@area#@##@rows#@##@days#@##@disableHierarchicalSort
所以这个应用里实际存在两种"搜索"路径:
- 直连闲鱼真实搜索接口:
idlemtopsearch.pc.search / wx.search - 自家服务端代理搜索:
YF("search", ...)通过 WebSocket 转发
8. 结论
如果你要找"真实搜索数据接口",结论是:
mtop.taobao.idlemtopsearch.pc.searchmtop.taobao.idlemtopsearch.wx.search
如果你要找"这个 APK 前端自己调用的搜索入口",则有两个:
- 直连:
CE(...) -> Xr(...) -> We("postjson"/"httpget") -> h5api.m.goofish.com - 代理:
YF("search", ...) -> WebSocket
总结
厚朴 APK (闲鱼搜索工具) 逆向与脱机协议深度分析全记录
文档说明:
本文档完整记录了针对"厚朴 APK"底层 H5 运行逻辑(remote-index.js)的逆向分析过程。从 WebSocket 代理搜索、登录注册鉴权、AES 报文加密、前端环境检测绕过,到直连闲鱼 Mtop 接口及 x5sec 滑块风控的处理,均提供了详细的原理解析与等效 Python 脱机代码。
- [阶段一:WebSocket RPC 代理搜索逻辑还原](#阶段一:WebSocket RPC 代理搜索逻辑还原)
- [阶段二:登录接口与鉴权 Token (sign_token) 的获取](#阶段二:登录接口与鉴权 Token (sign_token) 的获取)
- [阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析](#阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析)
- 阶段四:前端反调试与环境检测绕过 (版本过旧问题)
- [阶段五:直连闲鱼主搜索接口及 Mtop 签名算法](#阶段五:直连闲鱼主搜索接口及 Mtop 签名算法)
- [阶段六:x5sec 滑块风控的云端打码接口调用](#阶段六:x5sec 滑块风控的云端打码接口调用)
阶段一:WebSocket RPC 代理搜索逻辑还原
- 原理分析
代码中存在一个 YF("search", ...) 函数。厚朴并没有把所有的搜索请求直接发给闲鱼的 H5 API,而是为了规避风控或利用服务端的号池池(CK池),将搜索参数打包,通过 WebSocket 发送给自己的中控服务器(119.45.152.46)。
- 核心架构 :使用
Promise和Map关联请求与异步响应。 - 数据协议 :外层协议分隔符为
@#@@#,内层参数分隔符为#@##@。
- Python 等效实现
python
import asyncio
import websockets
import uuid
import json
class HoupuSearchClient:
def __init__(self, host, sign_token):
# 拼接端口 Trick:原 JS 中 hC="119.45.152.46:4545",hC+"1/" 实际请求了 45451 端口
self.ws_url = f"ws://{host}1/{sign_token}"
self.ws = None
self.futures = {}
async def connect(self):
print(f"[*] 正在连接中控服务器: {self.ws_url}")
self.ws = await websockets.connect(self.ws_url)
asyncio.create_task(self._listen())
print("[*] WebSocket 连接成功")
async def _listen(self):
try:
async for message in self.ws:
if message == "ping" or message.startswith("delay") or message in ["debug", "nodebug", "refreshrushck"]:
continue
parts = message.split("@#@@#")
if len(parts) == 3:
action, req_uuid, payload = parts
if action == "search" and req_uuid in self.futures:
future = self.futures.pop(req_uuid)
if not future.done():
future.set_result(payload)
except Exception as e:
print(f"[-] WebSocket 监听异常: {e}")
async def search(self, keyword, sort_field="modify", area="", rows=10, days=1, disable_sort=""):
if not self.ws:
raise Exception("WebSocket 未连接")
req_uuid = str(uuid.uuid4())
params_str = f"{keyword}#@##@0#@##@99999999#@##@{sort_field}#@##@{area}#@##@{rows}#@##@{days}#@##@{disable_sort}"
send_msg = f"search@#@@#{req_uuid}@#@@#{params_str}"
loop = asyncio.get_running_loop()
future = loop.create_future()
self.futures[req_uuid] = future
await self.ws.send(send_msg)
try:
result_raw = await asyncio.wait_for(future, timeout=3.0)
error_keywords = ["no server", "token", "you are too quick", "4455"]
if result_raw in error_keywords:
raise Exception(f"中控服务器拒绝请求,状态码: {result_raw}")
if not result_raw.startswith("{"):
raise Exception(f"返回数据异常: {result_raw}")
return json.loads(result_raw)
except asyncio.TimeoutError:
self.futures.pop(req_uuid, None)
raise TimeoutError("搜索请求超时")
async def main():
client = HoupuSearchClient("119.45.152.46:4545", "YOUR_ACTUAL_SIGN_TOKEN")
await client.connect()
result = await client.search(keyword="iPhone 13")
print(result)
if __name__ == "__main__":
# asyncio.run(main()) # 需要替换有效 Token 后运行
pass
阶段二:登录接口与鉴权 Token (sign_token) 的获取
- 原理分析
要连接 WebSocket 或使用作者的代理服务,必须具备合法的 sign_token。作者为了防抓包盗刷,加入了两层防护:
-
动态参数 ddsign:基于时间戳计算,防止重放攻击。
-
全局 AES 加密:将原本的 GET 请求打包为 JSON,利用 thedatayoucannotseethedatayoucan 作为密钥,使用 CryptoJS CBC 模式加密,然后作为 POST 的 body 发送。
-
Python 模拟登录代码
python
import time
import json
import hashlib
import base64
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class CryptoJS:
@staticmethod
def _derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = b''
while len(d) < key_length + iv_length:
d_i = hashlib.md5(d_i + password.encode('utf-8') + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+iv_length]
@staticmethod
def encrypt(text, passphrase):
salt = bytes([1, 2, 3, 4, 5, 6, 7, 8])
key, iv = CryptoJS._derive_key_and_iv(passphrase, salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted_bytes = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size))
return base64.b64encode(b"Salted__" + salt + encrypted_bytes).decode('utf-8')
@staticmethod
def decrypt(encrypted_text, passphrase):
encrypted_bytes = base64.b64decode(encrypted_text)
salt = encrypted_bytes[8:16]
ciphertext = encrypted_bytes[16:]
key, iv = CryptoJS._derive_key_and_iv(passphrase, salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ciphertext), AES.block_size).decode('utf-8')
def login_houpu(username, password, moneycheck):
host = "119.45.152.46:4545"
aes_key = "thedatayoucannotseethedatayoucan"
pw_md5 = hashlib.md5(password.encode()).hexdigest()
ddsign = int(time.time() * 1000 / 80000000 * 2 + 1024532)
target_url = f"/api/pro/login?username={username}&pw={pw_md5}&moneycheck={moneycheck}&ddsign={ddsign}"
payload_dict = {"url": target_url, "method": "GET"}
payload_json = json.dumps(payload_dict, separators=(',', ':'))
encrypted_body = CryptoJS.encrypt(payload_json, aes_key)
req_url = f"http://{host}/?secbody=1&ddsign={ddsign}"
headers = {
"User-Agent": "Mozilla/5.0",
"Content-Type": "text/plain;charset=UTF-8"
}
response = requests.post(req_url, data=encrypted_body, headers=headers)
if "secbody" in response.headers:
decrypted_res = CryptoJS.decrypt(response.text, aes_key)
res_json = json.loads(decrypted_res)
else:
res_json = response.json()
if res_json.get("code") == 0:
sign_token = res_json["data"]["sign"]
print(f"[+] 登录成功!获取到 sign_token: {sign_token}")
return sign_token
else:
raise Exception(f"登录失败: {res_json.get('message')}")
阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析
- 原理分析
- moneycheck 是什么? 这是在注册/登录时的第三个参数。前端没有任何生成算法,纯粹读取用户的界面输入。从命名和属性(数字)来看,它是作者用来验证授权的商业卡密 或支付流水号。
- 能否绕过? 无法从客户端绕过。 因为真正的核心搜索逻辑、淘宝签名计算都在云端(中控系统)。没有被中控系统数据库认可的 moneycheck,服务器就不会给你签发 sign_token,整个软件直接瘫痪。
- Python 模拟注册代码
利用与登录相同的通信协议,可以发出注册请求:
python
def register_houpu(username, password, moneycheck):
host = "119.45.152.46:4545"
aes_key = "thedatayoucannotseethedatayoucan"
pw_md5 = hashlib.md5(password.encode()).hexdigest()
ddsign = int(time.time() * 1000 / 80000000 * 2 + 1024532)
target_url = f"/api/reg?name={username}&pw={pw_md5}&moneycheck={moneycheck}&ddsign={ddsign}"
payload_dict = {"url": target_url, "method": "GET"}
payload_json = json.dumps(payload_dict, separators=(',', ':'))
encrypted_body = CryptoJS.encrypt(payload_json, aes_key)
req_url = f"http://{host}/?secbody=1&ddsign={ddsign}"
headers = {"User-Agent": "Mozilla/5.0", "Content-Type": "text/plain;charset=UTF-8"}
response = requests.post(req_url, data=encrypted_body, headers=headers)
if "secbody" in response.headers:
decrypted_res = CryptoJS.decrypt(response.text, aes_key)
res_json = json.loads(decrypted_res)
else:
res_json = response.json()
print(f"服务器消息: {res_json.get('message')}")
阶段四:前端反调试与环境检测绕过 (版本过旧问题)
- 现象描述
在电脑浏览器直接访问 http://119.45.152.46:8000/ 时,页面不显示登录框,而是提示"此版本过旧,请使用最新版本"。
- 原理分析
页面初始化时执行了这段逻辑:
javascript
We("appVersion","").then(F=>{
E.current.appVerCode = parseInt(F);
// qF 被硬编码为 83。如果当前不是原生安卓环境,We 方法兜底返回 "77"
E.current.appVerCode != qF && g("err")
})
检测到不是合法的原生 App,前端主动切换到 err 页面隐藏了正常功能。
-
绕过方案(Chrome 本地替换)
-
按 F12 打开开发者工具,进入 Sources (源代码)。
-
左侧打开 Overrides (替换) 选项卡,选择一个本地空文件夹并授权。
-
进入 Network (网络) ,刷新页面,右键点击 index.js 选择 Override content (保存并覆盖)。
-
在打开的代码编辑器中搜索 E.current.appVerCode!=qF&&g("err")。
-
将其删除或注释掉,Ctrl+S 保存。
-
刷新页面即可正常显示出登录/注册表单。
阶段五:直连闲鱼主搜索接口及 Mtop 签名算法
如果不使用作者的中控代理,而是客户端自己直接请求闲鱼的接口,底层调用的是阿里的 Mtop 网关 mtop.taobao.idlemtopsearch.pc.search。
- 核心难度
- 签名 (x-sign):MD5(_m_h5_tk前段 & 时间戳 & AppKey & 请求数据JSON)
- 风控 (x5sec):请求频繁会被 419 拦截,需要进行滑块验证。
- Python 完整还原代码 (包含风控检测逻辑)
python
import time
import json
import hashlib
import re
import requests
from urllib.parse import urlencode
class XianyuSearchClient:
def __init__(self, cookie_string):
self.cookie = cookie_string
self.app_key = "34839810" # PC 搜索 AppKey
self.api_name = "mtop.taobao.idlemtopsearch.pc.search"
self.api_version = "1.0"
self.headers = {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
"Origin": "https://www.goofish.com",
"Referer": "https://www.goofish.com/"
}
def _get_h5_token(self):
match = re.search(r'_m_h5_tk=([a-zA-Z0-9]+)_', self.cookie)
if match:
return match.group(1)
raise ValueError("Cookie 缺失 _m_h5_tk 字段!")
def _generate_sign(self, token, timestamp, data_json):
sign_str = f"{token}&{timestamp}&{self.app_key}&{data_json}"
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def update_x5sec_cookie(self, x5sec_value):
if 'x5sec=' in self.cookie:
self.cookie = re.sub(r'x5sec=[^;]+', f'x5sec={x5sec_value}', self.cookie)
else:
self.cookie += f"; x5sec={x5sec_value}"
print("[+] 风控票据 x5sec 已更新。")
def handle_risk_control(self, response_text, response_headers):
if "FAIL_SYS_USER_VALIDATE" in response_text or "ERROR::SM::" in response_text or '"code":419' in response_text:
print("[-] 触发阿里滑块验证码风控 (419 / 挤爆了)!")
slider_url = ""
try:
slider_url = json.loads(response_text).get("data", {}).get("url", "")
if not slider_url and "location" in response_headers:
slider_url = response_headers["location"]
except: pass
if slider_url:
print(f"[*] 请复制以下链接到浏览器进行滑块验证:\n {slider_url}")
print("[*] 滑过之后,抓包取出 Cookie 中的 x5sec 值,调用 update_x5sec_cookie 注入。")
return True
return False
def search(self, keyword, min_price="0", max_price="99999999", sort="modify", days="7", rows=10):
search_filter = f"priceRange:{min_price},{max_price};publishDays:{days};"
payload_dict = {
"pageNumber": 1,
"keyword": keyword,
"fromFilter": True,
"rowsPerPage": rows,
"sortValue": "desc",
"sortField": sort,
"customDistance": "", "gps": "", "customGps": "",
"propValueStr": json.dumps({"searchFilter": search_filter}),
"searchReqFromPage": "pcSearch",
"userPositionJson": "{}"
}
data_json = json.dumps(payload_dict, separators=(',', ':'))
timestamp = str(int(time.time() * 1000))
token = self._get_h5_token()
sign = self._generate_sign(token, timestamp, data_json)
params = {
"jsv": "2.7.2", "appKey": self.app_key, "t": timestamp,
"sign": sign, "api": self.api_name, "v": self.api_version,
"type": "originaljson", "dataType": "json"
}
url = f"https://h5api.m.goofish.com/h5/{self.api_name}/{self.api_version}/?" + urlencode(params)
headers = self.headers.copy()
headers["Cookie"] = self.cookie
body = {"data": data_json}
print(f"[*] 正在搜索关键词: '{keyword}'...")
response = requests.post(url, headers=headers, data=body, timeout=5)
if self.handle_risk_control(response.text, response.headers):
return None
res_data = response.json()
if "data" in res_data and "resultList" in res_data["data"]:
return res_data["data"]["resultList"]
return None
阶段六:x5sec 滑块风控的云端打码接口调用
- 原理分析
客户端本身并不具备破解滑块的能力。它在碰到上文的 location 拦截链接时,会将自己的用户名、包含淘宝信息的 Cookie、拦截 URL 发送到中控服务器 /api/slidev2/put,由服务端的打码集群去执行滑动,然后客户端每隔 3 秒通过 /api/x5sec/get 查询滑块票据 x5sec。
- Python 模拟云端代打码调用
前提条件:你必须花钱购买了作者的系统账号,拥有合法的 sign_token 才能调用此接口。
python
import asyncio
class HoupuSliderSolver:
def __init__(self, username, sign_token):
self.host = "119.45.152.46:4545"
self.username = username
self.sign_token = sign_token
self.aes_key = "thedatayoucannotseethedatayoucan"
self.user_agent = "Mozilla/5.0"
def _send_encrypted_request(self, target_url):
ddsign = int(time.time() * 1000 / 80000000 * 2 + 1024532)
target_url += f"&sign={self.sign_token}&ddsign={ddsign}" if "?" in target_url else f"?sign={self.sign_token}&ddsign={ddsign}"
payload = json.dumps({"url": target_url, "method": "GET"}, separators=(',', ':'))
encrypted_body = CryptoJS.encrypt(payload, self.aes_key) # 复用前文的 CryptoJS
req_url = f"http://{self.host}/?secbody=1&sign={self.sign_token}&ddsign={ddsign}"
headers = {"User-Agent": self.user_agent, "Content-Type": "text/plain;charset=UTF-8"}
res = requests.post(req_url, data=encrypted_body, headers=headers)
return res.json() # 假设后端直接返回或已解密
async def solve_slider(self, slider_url, tb_cookie, ck_id="1"):
# 拼接 location 参数: url||cookie||UA||id
location_raw = f"{slider_url}||{tb_cookie}||{self.user_agent}||{ck_id}"
location_encode = urllib.parse.quote(location_raw)
task_user = f"singlebrush_z_{self.username}_{ck_id}"
print("[*] 正在向中控提交打码任务...")
put_url = f"/api/slidev2/put?location={location_encode}&username={task_user}"
self._send_encrypted_request(put_url)
get_url = f"/api/x5sec/get?username={task_user}"
for i in range(15):
print(f"[*] 正在查询打码结果... (第 {i+1}/15 次)")
await asyncio.sleep(3)
get_res = self._send_encrypted_request(get_url)
if get_res.get("code") == 0:
print("[+] 打码成功!")
return get_res.get("data")
print("[-] 打码超时")
return None
阶段七:攻防转换------基于厚朴架构的防破解与反编译思路启示
在逆向分析完"厚朴"工具之后,我们可以清楚地看到作者在设计这套工具时,采取了一系列防护措施来保护其商业利益(防止白嫖、防止逆向提取核心接口)。
我们可以从防守者的角度出发,总结作者"做对了什么",以及"哪里存在短板",并探讨如何构建一个真正难以被破解的安全架构。
-
作者的防御亮点(做对了什么?)
-
客户端"空壳化"(动态下发逻辑)
- 策略: 原生 Android (Java/Kotlin) 中几乎没有任何业务逻辑,仅保留 WebView 和 JSBridge。核心业务逻辑通过 http://119.45.152.46:8000/index.js 动态加载。
- 优势: 使得传统的 APK 反编译(如 JADX、Apktool)毫无用处。逆向者只能通过抓包获取 H5 源码,极大地增加了分析的时间成本,且作者可以随时热更新逻辑。
-
核心资源"云端化"(SaaS 模式)
- 策略: 将最难突破的"闲鱼滑块 x5sec 打码"、"号池资源"完全放在服务器端。客户端只负责提交参数。
- 优势: 彻底杜绝了本地破解的可能性。逆向者即使看懂了所有 JS 代码,也没有服务器的控制权,不花钱买卡密(moneycheck)就无法调用云端资源。
-
接口加密与防重放
- 策略: 拦截所有的 HTTP 请求,将 URL 和参数转为 JSON,使用 AES-CBC 进行加密,并在请求参数中追加基于时间戳的动态签名 ddsign。
- 优势: 有效防止了简单的抓包和重放攻击(Replay Attack)。
-
防御的致命短板(为何被轻易逆向?)
虽然架构设计得不错,但在具体实施细节上,防线被轻易击穿了:
-
前端 JS "零混淆"
- 作者直接将打包后的 React 产物暴露在公网,没有任何控制流平坦化(Control Flow Flattening)、字符串加密等安全混淆处理。变量名、明文接口地址清晰可见。
-
"皇帝的新衣"式加密密钥
- AES 加密是对称加密,作者将密钥 "thedatayoucannotseethedatayoucan" 直接硬编码在明文的 JS 代码中,这使得原本严密的报文加密变成了"掩耳盗铃",逆向者只需全局搜索关键字即可瞬间提取密钥。
-
脆弱的环境检测
- 作者通过调用原生方法 appVersion 来判断是否在 App 内运行,这只是防住了普通的小白用户。稍微懂点的开发者通过 Chrome 的"本地替换(Local Overrides)"功能,一行代码就能把这个环境检测给删掉。
-
进阶:如何构建无法被轻易破解的架构?
如果要在厚朴的基础上升级防线,让逆向工程师"知难而退",应当采用以下架构设计:
方案一:JS 强混淆与 vmp (虚拟化保护)
- 由于核心逻辑在 JS 中,必须对 index.js 使用高级混淆器(如 Jscrambler 或 OLLVM 理念的 JS 混淆工具)。
- 隐藏关键的 API 路径、重命名变量,并对字符串进行动态解密。
方案二:密钥动态下发与非对称加密 (RSA + AES)
- 绝对不要在客户端硬编码 AES 密钥。
- 客户端启动时生成随机 AES 密钥,使用服务器的 RSA 公钥进行加密发送给服务器(密钥协商)。
- 服务器解密后,双方再使用该随机 AES 密钥进行后续通讯。即使 JS 被反编译,由于每次运行密钥都在变,且无法反推服务器私钥,通信数据绝对安全。
方案三:原生加固与 NDK (C++) 防护
- 将环境检测、加解密算法、设备指纹生成等逻辑,从 JS 抽离出来,下沉到 Android 的 .so 动态库(C/C++)中。
- JS 仅调用 Native 层暴露出的高度混淆接口。C++ 逆向难度呈指数级增加。
方案四:App 完整性校验与设备指纹
- 抛弃简单的 appVersion 检测。使用强设备指纹(Device Fingerprinting)收集设备硬件特征,并在 Native 层校验 APK 的签名哈希值(Signature Check)。
- 在服务端引入 JA3 指纹校验,拒绝非移动端发出的 HTTPS 握手请求。