我几乎没做逆向:一次把抓包、help_tool、AI 提示词和 Codex 串起来的实战

这篇文章想记录的,不只是“某河南阅卷平台”的登录加密是怎么被还原出来的,更想记录另一件事:

我自己几乎没有手工做逆向分析,只是把几个线索交给 AI,然后让 Codex 直接接管电脑,把后面的事一路做完。

先说明两点:

  1. 平台名称、域名、手机号、密码、密钥等敏感信息均已脱敏。
  2. 文中的脚本是教学版脱敏脚本,域名统一替换为 https://app.test.com,不针对任何真实线上服务。

一、我自己实际只做了四件事

如果把整个过程压缩一下,我自己真正动手做的事其实非常少:

  1. 打开模拟器,把 App 启动起来。
  2. 用抓包软件拿到登录请求包。
  3. 用开源项目 help_tool 先跑出“可能的加密方式”和“候选 key”。
  4. 让另一个通用 AI 根据这些线索,帮我生成一段适合发给 Codex 的执行型提示词。

到这里为止,我还没有自己去点 jadx、没有自己翻 smali、也没有自己连 adb 看内存。

后面的工作,基本都是 Codex 自己完成的。


二、help_tool 给我的不是答案,而是“第一推动力”

我一开始并不知道 userPwd 到底是怎么来的,只知道抓包里有一个看起来像 Base64 的字段。

这时候 help_tool 给我的价值,不是直接还原出最终算法,而是给了我一个很重要的起点:

换句话说,help_tool 没有帮我直接得出最终结论,但它帮我把一个本来模糊的问题,压缩成了一个适合交给 AI 执行的任务。

这个阶段最关键的收获不是“我已经知道答案了”,而是:

我已经拥有了一组足够具体、足够结构化的输入,可以让 AI 去接手。


三、我交给 Codex 的,不是一个问题,而是一份任务单

接下来我没有自己继续分析,而是让另一个 AI 帮我整理了一段执行提示词,然后直接发给 Codex。

这段提示词的核心思路很简单:

下面是我当时交给 Codex 的任务单结构。出于脱敏需要,我把候选 key 做了替换,但格式和思路保持一致:

任务目标:

对当前目录中的 APK 文件进行静态分析,定位登录接口中 userPwd 的加密生成逻辑。

已知信息:

1. 登录接口:
   POST /api/v2/user/Login

2. 加密结果:
   userPwd 为 Base64 字符串,长度约 44 字符(解码后 32 bytes)

3. 已知候选 key:
   "REDACTED_CANDIDATE_KEY"

4. 高概率算法:
   AES-128 / ECB / ZeroPadding

5. 已存在中间值(32字节),说明密码在 AES 前被处理过(可能是 SHA256 或自定义逻辑)

---

执行步骤:

1. 工具准备(如本地不存在):
   - 从 GitHub 下载并使用 jadx
   - 从 GitHub 下载并使用 apktool

2. 反编译 APK:
   - 使用 jadx 打开 APK
   - 同时使用 apktool 解包

3. 全局搜索关键字:
   优先级1:
   - "REDACTED_CANDIDATE_KEY"
   - "userPwd"
   - "/api/v2/user/Login"

   优先级2:
   - "Cipher.getInstance"
   - "SecretKeySpec"
   - "doFinal"
   - "Base64.encode"

   优先级3:
   - "MessageDigest"
   - "SHA"
   - "MD5"
   - "digest"

4. 重点定位:
   找到如下结构代码:

   Cipher cipher = Cipher.getInstance(...)
   SecretKeySpec key = new SecretKeySpec(...)
   cipher.doFinal(...)

5. 向上追踪调用链:
   必须还原完整流程:

   password -> ??? -> AES -> Base64

   重点找出:
   “??? 这一步的实现代码”

6. 输出结果要求:
   ① 加密算法完整描述
   ② key 来源
   ③ 中间层处理逻辑
   ④ 完整 Java 调用链
   ⑤ 对应的 Python 还原代码(完整可运行)

注意事项:

- 不要根据密文“猜算法”
- 必须以代码中的 Cipher.getInstance 为最终依据
- 多算法匹配(DES/SM4)属于误导结果,应忽略

最终目标:

生成可用于 Python 模拟登录请求的完整加密函数。

从现在回头看,这段提示词真正有价值的地方,不是写得多“高级”,而是写得足够像一个可以执行的工单。


四、从这一刻开始,基本就是 Codex 在自己操作电脑

把任务单发出去以后,后面的流程几乎都是 Codex 自己推进的。

我印象最深的是,它不是只做“文字分析”,而是真正在电脑上把一整条链跑通了。

大致流程是这样的:

1. 它先自己准备工具

Codex 先检查本地环境,然后自己下载并解压:

接着直接在当前目录开始反编译 APK、解包资源和 smali。

2. 它先按提示词的优先级去搜

最开始它就老老实实按任务单去做:

很快,它确认了登录接口和 LoginParams.userPwd 的赋值点,但真正的算法逻辑并没有在普通反编译结果里直接出现。

3. 它自己判断出“静态分析被壳挡住了”

这是我觉得很像“会干活的 AI”的地方。

Codex 没有在静态 dex 上死磕,而是通过反编译结果判断出:

也就是说,它自己从“继续搜关键词”切换成了“想办法拿运行时真实代码”。

4. 它开始要求一个新前提:root 环境

这时我做的唯一一次配合,就是告诉它:

然后 Codex 就开始自己用 adb 干活了。

5. 它自己连 adb,自己看进程,自己 dump dex

后面这一段,基本已经不是“我和 AI 聊天”,而是“AI 在替我操作机器”了。

它依次做了这些事:

说实话,这一步如果让我自己手工做,也不是不能做,但我大概率会在中途频繁切换工具和思路。

而 Codex 的优势是,它能一直保持问题上下文不丢。


五、真正决定胜负的,不是“找到 AES”,而是继续追到了运行时 key

恢复出真实 dex 之后,Codex 很快就把登录调用链串起来了:

登录按钮
-> BaseLoginActivity.h0()
-> e3.d.h(userNo, password)
-> LoginParams.userPwd = x5.a.b(password, j.a)
-> POST /api/v2/user/Login

再往下追,它找到了真正的算法代码:

Cipher.getInstance("AES/CBC/PKCS5Padding")

到这里,最开始 help_tool 给出的“可能是 AES”这一点,算是被代码证实了。
但更有意思的是,help_tool 提供的候选方向并不等于最终答案。

因为 Codex 继续往后挖,发现还有一层更关键的事实:

这就是为什么有一条客户端样本密文,一开始怎么都解不开。

不是因为算法错了,而是因为如果你只看到静态 key,就会拿错钥匙。

这一步我自己完全没有手工定位,是 Codex 继续沿着调用链、初始化逻辑和运行时赋值一路追下去,最后把这个坑找出来的。


六、最终还原出来的真实流程

最后被还原出来的流程是:

password
-> UTF-8 bytes
-> AES/CBC/PKCS5Padding
   key = SHA-256(runtime_key_utf8)[:16]
   iv  = random 16 bytes
-> IV || ciphertext
-> Base64(NO_WRAP)

这里有两个特别值得记住的点:

1. 真正做 SHA-256 的不是密码,而是 key

一开始根据抓包去猜,很容易怀疑是:

password -> SHA256 -> AES

但代码证据告诉我们不是。

真实逻辑是:

runtime_key -> SHA256 -> 前16字节 -> AES key
password -> 直接 UTF-8 -> AES/CBC/PKCS5Padding

2. 同一个密码每次加密结果都不一样

因为 IV 是随机 16 字节,所以:

这也是为什么如果只看几条抓包,很容易误判。


七、这次最让我有感触的地方:我几乎没有“亲手逆向”

如果要非常直白地总结这次过程,那就是:

我不是靠自己一点点手工翻代码,把算法抠出来的。
我是先用 help_tool 给 AI 一个起点,再用提示词把问题打包,然后让 Codex 去接管操作。

我自己做的更像是:

真正把这些变成结果的,是 Codex 后面这一连串自动化动作:

如果说过去大家理解 AI 更像“会回答问题的聊天工具”,那这次给我的感觉更像是:

只要你的任务描述足够具体,AI 就可以开始扮演一个能持续执行、能自己换路线、还能自己落脚本的操作者。


八、博客版脱敏脚本

下面放的是脱敏版、教学版脚本:

1. 加密 / 解密辅助脚本

import base64
import hashlib
import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


BASE_URL = "https://app.test.com"
LOGIN_KEY = "REDACTED_RUNTIME_KEY"


def derive_aes_key(key_str: str = LOGIN_KEY) -> bytes:
    return hashlib.sha256(key_str.encode("utf-8")).digest()[:16]


def encrypt_user_pwd(password: str, key_str: str = LOGIN_KEY) -> str:
    aes_key = derive_aes_key(key_str)
    iv = os.urandom(16)
    cipher = AES.new(aes_key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(password.encode("utf-8"), AES.block_size))
    return base64.b64encode(iv + ciphertext).decode("ascii")


def decrypt_user_pwd(user_pwd_b64: str, key_str: str = LOGIN_KEY) -> str:
    raw = base64.b64decode(user_pwd_b64)
    iv = raw[:16]
    ciphertext = raw[16:]
    aes_key = derive_aes_key(key_str)
    cipher = AES.new(aes_key, AES.MODE_CBC, iv)
    return unpad(cipher.decrypt(ciphertext), AES.block_size).decode("utf-8")

2. 脱敏版模拟登录脚本

import json
import requests
import urllib3

from crypto_helper import encrypt_user_pwd


BASE_URL = "https://app.test.com"
LOGIN_API = f"{BASE_URL}/api/v2/user/Login"

HEADERS = {
    "User-Agent": "okhttp/3.12.13",
    "Accept-Encoding": "gzip",
    "content-type": "application/json; charset=UTF-8",
}

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def build_commondata():
    return json.dumps(
        {
            "appSelectType": 2,
            "clientConfig": "REDACTED_CLIENT_CONFIG",
            "deviceModel": "REDACTED_MODEL",
            "systemVersion": "REDACTED_SYSTEM_VERSION",
            "version": "REDACTED_VERSION",
            "wyClient": 1,
        },
        separators=(",", ":"),
    )


def login(phone: str, password: str):
    headers = dict(HEADERS)
    headers["commondata"] = build_commondata()

    payload = {
        "appSelectType": 2,
        "deviceModel": "REDACTED_MODEL",
        "systemVersion": "REDACTED_SYSTEM_VERSION",
        "userNo": phone,
        "userPwd": encrypt_user_pwd(password),
        "wyClient": 1,
    }

    with requests.Session() as session:
        session.trust_env = False
        response = session.post(
            LOGIN_API,
            headers=headers,
            json=payload,
            timeout=20,
            verify=False,
        )
        response.raise_for_status()
        return response.json()


if __name__ == "__main__":
    result = login("138****0000", "******")
    datas = result.get("datas") or {}
    print(
        json.dumps(
            {
                "token": datas.get("token"),
                "userName": datas.get("userName"),
                "unitName": datas.get("unitName"),
            },
            ensure_ascii=False,
            indent=2,
        )
    )

九、如果只用一句话总结这次流程

如果要我用最简单的一句话概括整个过程,那就是:

我先用抓包和 help_tool 把问题缩小到一个 AI 可执行的范围,再用另一段提示词把任务交给 Codex,然后几乎不自己分析,直接看它把整条链跑通。

对我来说,这不是“AI 帮我查了点资料”,而是:

AI 真的开始像一个会操作电脑、会换思路、会写脚本、会自己验证结果的执行者。

这也是这次经历里最让我兴奋的地方。


十、结尾

以前很多人说 AI 适合做“辅助”。
但这次我的感受更像是:

只要你前面把任务描述清楚,把已知线索组织好,把目标定义明确,后面的很多事其实已经不必再由你亲手完成。

我自己只是:

剩下的逆向、定位、验证、脚本化,基本都交给了 Codex。

如果以后再做类似的分析,我大概率还会沿用这套思路:

抓包 -> help_tool 给候选方向 -> 另一个 AI 生成执行型提示词 -> Codex 全程接管

从这个角度看,这篇文章其实不只是一次逆向复盘,更像是一次“AI 如何接管技术流程”的实战记录。

转载请注明出处