厚朴 APK 搜索接口分析

文章目录

    • [厚朴 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.java
  • D:\software\codex-project\apk\houpu-decompiled\sources\com\taobao\fetchidlefish\a.java
  • D:\software\codex-project\apk\houpu-decompiled\sources\defpackage\u3.java

2. JS Bridge 可调用的方法

远端前端通过 android.callNative 可调用这些原生方法:

  • postjson
  • httpget
  • open1
  • readFile
  • writeFile
  • clearck
  • delFile
  • appVersion

其中真正用于搜索请求的是:

  • postjson
  • httpget

3. 真正的搜索数据接口

远端前端脚本 remote-index.js 中,搜索封装函数是:

  • async function CE(...)

它最终会调用两个闲鱼搜索 API 之一:

  1. mtop.taobao.idlemtopsearch.pc.search
  2. mtop.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
  • WX 搜索:

    • API:mtop.taobao.idlemtopsearch.wx.search
    • 版本:1.0
    • appKey=12574478
    • 方法:POST
    • Referer=https://2.taobao.com/
    • Origin=https://2.taobao.com

4. 搜索请求体字段

CE(...) 里可确认的核心参数:

  • keyword
  • pageNumber
  • rowsPerPage
  • sortField
  • sortValue
  • propValueStr
  • searchReqFromPage

可选过滤字段:

  • priceRange
  • publishDays
  • disableHierarchicalSort
  • province
  • city
  • area
  • extraFilterValue.divisionList

PC 搜索请求体典型字段:

  • keyword
  • pageNumber: 1
  • fromFilter: true
  • rowsPerPage
  • sortField: create | modify
  • sortValue: desc
  • searchReqFromPage: "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. 搜索调用链

调用链分两层:

  1. MainActivity 加载远端页面 119.45.152.46:8000
  2. 远端页面中的 We(...) 调用 android.callNative(...)
  3. 原生 u3.javapostjson/httpget 用 OkHttp 发出真实请求
  4. 搜索数据源落到 Goofish H5 API:
    • mtop.taobao.idlemtopsearch.pc.search
    • mtop.taobao.idlemtopsearch.wx.search

7. 额外的代理搜索分支

除了直接请求 Goofish 外,前端还有一个兜底搜索分支:

  • function YF(e, t)

当调用 YF("search", ...) 时,它不是普通 HTTP,而是把参数通过 WebSocket 发给服务端代理。

参数格式可见:

  • keyword#@##@0#@##@99999999#@##@sortField#@##@area#@##@rows#@##@days#@##@disableHierarchicalSort

所以这个应用里实际存在两种"搜索"路径:

  1. 直连闲鱼真实搜索接口:idlemtopsearch.pc.search / wx.search
  2. 自家服务端代理搜索:YF("search", ...) 通过 WebSocket 转发

8. 结论

如果你要找"真实搜索数据接口",结论是:

  • mtop.taobao.idlemtopsearch.pc.search
  • mtop.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 脱机代码。

  1. [阶段一:WebSocket RPC 代理搜索逻辑还原](#阶段一:WebSocket RPC 代理搜索逻辑还原)
  2. [阶段二:登录接口与鉴权 Token (sign_token) 的获取](#阶段二:登录接口与鉴权 Token (sign_token) 的获取)
  3. [阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析](#阶段三:注册接口与 REG_CODE (moneycheck) 的深度剖析)
  4. 阶段四:前端反调试与环境检测绕过 (版本过旧问题)
  5. [阶段五:直连闲鱼主搜索接口及 Mtop 签名算法](#阶段五:直连闲鱼主搜索接口及 Mtop 签名算法)
  6. [阶段六:x5sec 滑块风控的云端打码接口调用](#阶段六:x5sec 滑块风控的云端打码接口调用)

阶段一:WebSocket RPC 代理搜索逻辑还原

  1. 原理分析

代码中存在一个 YF("search", ...) 函数。厚朴并没有把所有的搜索请求直接发给闲鱼的 H5 API,而是为了规避风控或利用服务端的号池池(CK池),将搜索参数打包,通过 WebSocket 发送给自己的中控服务器(119.45.152.46)。

  • 核心架构 :使用 PromiseMap 关联请求与异步响应。
  • 数据协议 :外层协议分隔符为 @#@@#,内层参数分隔符为 #@##@
  1. 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) 的获取

  1. 原理分析

要连接 WebSocket 或使用作者的代理服务,必须具备合法的 sign_token。作者为了防抓包盗刷,加入了两层防护

  1. 动态参数 ddsign:基于时间戳计算,防止重放攻击。

  2. 全局 AES 加密:将原本的 GET 请求打包为 JSON,利用 thedatayoucannotseethedatayoucan 作为密钥,使用 CryptoJS CBC 模式加密,然后作为 POST 的 body 发送。

  3. 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) 的深度剖析

  1. 原理分析
  • moneycheck 是什么? 这是在注册/登录时的第三个参数。前端没有任何生成算法,纯粹读取用户的界面输入。从命名和属性(数字)来看,它是作者用来验证授权的商业卡密支付流水号
  • 能否绕过? 无法从客户端绕过。 因为真正的核心搜索逻辑、淘宝签名计算都在云端(中控系统)。没有被中控系统数据库认可的 moneycheck,服务器就不会给你签发 sign_token,整个软件直接瘫痪。
  1. 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')}")

阶段四:前端反调试与环境检测绕过 (版本过旧问题)

  1. 现象描述

在电脑浏览器直接访问 http://119.45.152.46:8000/ 时,页面不显示登录框,而是提示"此版本过旧,请使用最新版本"。

  1. 原理分析

页面初始化时执行了这段逻辑:

javascript 复制代码
We("appVersion","").then(F=>{
    E.current.appVerCode = parseInt(F);
    // qF 被硬编码为 83。如果当前不是原生安卓环境,We 方法兜底返回 "77"
    E.current.appVerCode != qF && g("err") 
})

检测到不是合法的原生 App,前端主动切换到 err 页面隐藏了正常功能。

  1. 绕过方案(Chrome 本地替换)

  2. 按 F12 打开开发者工具,进入 Sources (源代码)

  3. 左侧打开 Overrides (替换) 选项卡,选择一个本地空文件夹并授权。

  4. 进入 Network (网络) ,刷新页面,右键点击 index.js 选择 Override content (保存并覆盖)

  5. 在打开的代码编辑器中搜索 E.current.appVerCode!=qF&&g("err")。

  6. 将其删除或注释掉,Ctrl+S 保存。

  7. 刷新页面即可正常显示出登录/注册表单。

阶段五:直连闲鱼主搜索接口及 Mtop 签名算法

如果不使用作者的中控代理,而是客户端自己直接请求闲鱼的接口,底层调用的是阿里的 Mtop 网关 mtop.taobao.idlemtopsearch.pc.search。

  1. 核心难度
  • 签名 (x-sign):MD5(_m_h5_tk前段 & 时间戳 & AppKey & 请求数据JSON)
  • 风控 (x5sec):请求频繁会被 419 拦截,需要进行滑块验证。
  1. 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 滑块风控的云端打码接口调用

  1. 原理分析

客户端本身并不具备破解滑块的能力。它在碰到上文的 location 拦截链接时,会将自己的用户名、包含淘宝信息的 Cookie、拦截 URL 发送到中控服务器 /api/slidev2/put,由服务端的打码集群去执行滑动,然后客户端每隔 3 秒通过 /api/x5sec/get 查询滑块票据 x5sec。

  1. 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

阶段七:攻防转换------基于厚朴架构的防破解与反编译思路启示

在逆向分析完"厚朴"工具之后,我们可以清楚地看到作者在设计这套工具时,采取了一系列防护措施来保护其商业利益(防止白嫖、防止逆向提取核心接口)。

我们可以从防守者的角度出发,总结作者"做对了什么",以及"哪里存在短板",并探讨如何构建一个真正难以被破解的安全架构。

  1. 作者的防御亮点(做对了什么?)

  2. 客户端"空壳化"(动态下发逻辑)

    • 策略: 原生 Android (Java/Kotlin) 中几乎没有任何业务逻辑,仅保留 WebView 和 JSBridge。核心业务逻辑通过 http://119.45.152.46:8000/index.js 动态加载。
    • 优势: 使得传统的 APK 反编译(如 JADX、Apktool)毫无用处。逆向者只能通过抓包获取 H5 源码,极大地增加了分析的时间成本,且作者可以随时热更新逻辑。
  3. 核心资源"云端化"(SaaS 模式)

    • 策略: 将最难突破的"闲鱼滑块 x5sec 打码"、"号池资源"完全放在服务器端。客户端只负责提交参数。
    • 优势: 彻底杜绝了本地破解的可能性。逆向者即使看懂了所有 JS 代码,也没有服务器的控制权,不花钱买卡密(moneycheck)就无法调用云端资源。
  4. 接口加密与防重放

    • 策略: 拦截所有的 HTTP 请求,将 URL 和参数转为 JSON,使用 AES-CBC 进行加密,并在请求参数中追加基于时间戳的动态签名 ddsign。
    • 优势: 有效防止了简单的抓包和重放攻击(Replay Attack)。
  5. 防御的致命短板(为何被轻易逆向?)

虽然架构设计得不错,但在具体实施细节上,防线被轻易击穿了:

  1. 前端 JS "零混淆"

    • 作者直接将打包后的 React 产物暴露在公网,没有任何控制流平坦化(Control Flow Flattening)、字符串加密等安全混淆处理。变量名、明文接口地址清晰可见。
  2. "皇帝的新衣"式加密密钥

    • AES 加密是对称加密,作者将密钥 "thedatayoucannotseethedatayoucan" 直接硬编码在明文的 JS 代码中,这使得原本严密的报文加密变成了"掩耳盗铃",逆向者只需全局搜索关键字即可瞬间提取密钥。
  3. 脆弱的环境检测

    • 作者通过调用原生方法 appVersion 来判断是否在 App 内运行,这只是防住了普通的小白用户。稍微懂点的开发者通过 Chrome 的"本地替换(Local Overrides)"功能,一行代码就能把这个环境检测给删掉。
  4. 进阶:如何构建无法被轻易破解的架构?

如果要在厚朴的基础上升级防线,让逆向工程师"知难而退",应当采用以下架构设计:

方案一: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 握手请求。
相关推荐
山川湖海2 小时前
AI时代快速学编程语言的陷阱(以Python为例)
大数据·人工智能·python
H Journey2 小时前
Supervisor 进程管理工具介绍
python·supervisor·linux 运维
春日见2 小时前
5分钟入门强化学习之动态规划算法与实现
大数据·人工智能·python·算法·机器学习·计算机视觉
DeniuHe3 小时前
sklearn 中所有交叉验证数据集划分方式完整总结
人工智能·python·sklearn
DeniuHe3 小时前
sklearn中不同交叉验证方法的场景适配
人工智能·python·sklearn
隐于花海,等待花开3 小时前
16.Python 常用第三方库概览 深度解析
python
我材不敲代码3 小时前
Python 函数核心:位置参数与关键字参数详解
java·前端·python
风落无尘4 小时前
第十一章《对齐与安全》 完整学习资料
python·安全·机器学习
Kratzdisteln4 小时前
【无标题】
前端·python