前言
本文主要是逆向的APP层的。Web层的也是一样的 不同的是wasm 。
实际上调用也是非常简单。这里就简单说一下
from wasmtime import Linker, Module, Store
将wasm的方法导出 直接再封装计算下 就可以了。 算法应该都是一样的。这里不多说了
直接看APP流程。
这里逆向什么参数我就不说了每个请求头里都挂着一个 x-ds-pow-response
解码之后长这个样子
1 2 3 4 5 6 7 8
| { "algorithm": "DeepSeekHashV1", "challenge": "xxxx", "salt": "xxx", "signature": "xxxx", "answer": 1234123, "target_path": "/api/v0/chat/completion" }
|
其中只有answer是生成的
定位点

实际上和web 走的应该是同一个算法。web上走的是wasm
这里只分析算法 不研究流程。
Frida
1 2 3 4 5 6 7 8 9
| Java.perform(function () { var P = Java.use("com.deepseek.crypto.PowCalculator"); P.nativeCalculateDeepSeekHashV1Pow.implementation = function (base, challenge, difficulty) { var r = this.nativeCalculateDeepSeekHashV1Pow(base, challenge, difficulty); console.log("[POW] base=" + base + " challenge=" + challenge + " diff=" + difficulty + " => " + r); return r; }; console.log("[POW] hook installed, waiting for prefetch..."); });
|
如上 hook 没数据 原因应该是Java层应该是走了缓存信息了。PoW 是从缓存池里取的预取结果。
所以这里 我们简单点
直接 hook native 层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Java.perform(function () { var addr = Module.findExportByName("librscrypto.so", "Java_com_deepseek_crypto_PowCalculator_nativeCalculateDeepSeekHashV1Pow"); console.log("native addr = " + addr); Interceptor.attach(addr, { onEnter: function (args) { this.base = Java.vm.getEnv().getStringUtfChars(args[2], null).readUtf8String(); this.chal = Java.vm.getEnv().getStringUtfChars(args[3], null).readUtf8String(); this.diff = args[4].toInt32(); }, onLeave: function (retval) { console.log("[NATIVE POW] base=" + this.base + " challenge=" + this.chal + " diff=" + this.diff + " => " + retval); } }); });
|
这里直接获取到了传参数据了

So
重点还是看下So层的数据
可谓是非常简单了
1
| 0x18E40 Java_com_deepseek_crypto_PowCalculator_nativeCalculateDeepSeekHashV1Pow
|

入口进入sub_1AB74 调用 get_string
1
| v11 = sub_19BC0(challenge_ptr, challenge_len, base_ptr, base_len, difficulty);
|

注意参数顺序,JNI 进来是 (base, challenge, difficulty),到内层变成了 (challenge, base, difficulty),base 和 challenge 换了位置
乍一看
这个应该是sha3 至少是 编译时链接了 Rust 的 sha3 库。

看得到一堆 veorq_s8(NEON 异或)、一个吸收循环、block 大小 0x88(136),还有个常量 0x06
rate=136 正是 SHA3-256 的 rate(capacity 512),padding 字节 0x06 是 SHA3 的域分隔符.
由此可以判断诗歌 sha3-256

这段So主要
- 一上来
bytes.fromhex(challenge),把 64 个 hex 字符解成 32 字节,这是比对目标 target。
- 主循环
nonce 从 0 数到 difficulty,每次把 base + str(nonce) 喂进哈希。
- 哈希出来的 32 字节跟
target 逐字节全等就返回当前 nonce,数到头没找到返回 -1。
但是后面实际走了 标准 sha3
发现得到的值对应不上
基础值是:
1 2 3 4
| base = 4a5a3ebe482ca78553f1_1781258202446_ challenge = 8868f19fb77e23cf1fc41d1af76a552bce58a24517d7e0920e9848c2013eba7c difficulty = 144000 answer = 102773
|
然后反推
1
| hashlib.sha3_256(base + "102773")
|
对不上

这是 Rust keccak crate 的通用置换,圈数是参数传进来的。
标准 SHA3-256 必须是 24 圈,而这里传的是 23。
光少一圈还不够,轮常量(round constant)的取法也被动了。轮常量表在 0x8E78,dump 出来是标准的 24 个 Keccak RC。但 so 里访问它的方式是:
1 2
| v3 = -8 * 23; RC_ptr = unk_8E78 + v3 + 192;
|
+8 字节,正好是跳过第一个 8 字节常量 RC[0],从 RC[1] 开始用,一直用到 RC[23]。
最后验证确实是23圈
SHA3_256_魔改(base + “102773”) == challenge 一致
最后有个简单方法
1 2 3
| var fn = new NativeFunction(soBase.add(0x19BC0), 'int64', ['pointer', 'uint32', 'pointer', 'uint32', 'int64']); var r = fn(chBuf, 64, baBuf, baLen, int64(144000));
|
直接调用
结果
最后结果如下
