Compare commits

..

No commits in common. "e5ff7d07a62cc3d85d79d82dbe79f4a97e8c3189" and "6f7051d30b7a690e2e63ca0975c20dd1d1b04955" have entirely different histories.

7 changed files with 194 additions and 239 deletions

View File

@ -2,7 +2,7 @@
PORT=3000 PORT=3000
# 抓包代理服务器端口 # 抓包代理服务器端口
HOOK_PORT=9000:9001 HOOK_PORT=9000
# 可选:网易云音乐 Cookie用于某些需要登录的接口 # 可选:网易云音乐 Cookie用于某些需要登录的接口
# NETEASE_COOKIE= # NETEASE_COOKIE=

View File

@ -34,6 +34,19 @@ PORT=3000
HOOK_PORT=9000 HOOK_PORT=9000
``` ```
## 证书生成
HTTPS 代理需要自签名证书。首次运行前需要生成证书:
```bash
cd src/server
node generate-cert.js
```
这将生成 `server.crt``server.key` 文件。
> **注意**:这是自签名证书,仅用于开发环境。使用时需要在客户端信任此证书。
## 运行 ## 运行
运行以下命令: 运行以下命令:

View File

@ -1,6 +1,6 @@
{ {
"name": "api-clawer", "name": "api-clawer",
"version": "0.3.0", "version": "0.2.0",
"description": "网易云音乐客户端抓包工具", "description": "网易云音乐客户端抓包工具",
"main": "src/server/app.js", "main": "src/server/app.js",
"scripts": { "scripts": {

View File

@ -6,135 +6,22 @@ const bodyify = require('querystring').stringify;
const eapiKey = 'e82ckenh8dichen8'; const eapiKey = 'e82ckenh8dichen8';
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'; const linuxapiKey = 'rFgB&h#%2?^eDg:Q';
const xeapiKey = '723f08a8d77c4a3698a9722b71b3607b';
// xeapi 静态密钥 (32字节AES-256-ECB) const decrypt = (buffer, key) => {
const xeapiStaticKey = Buffer.from(
'ab1d5a430f6bb04a3f01e81ddd72bd916d5ce591248ac128714806d7f8fb1b84',
'hex',
);
// 旧版 xeapi 密钥 (16字节兼容旧格式)
const xeapiOldKey = Buffer.from('723f08a8d77c4a3698a9722b71b3607b', 'hex');
// X25519 SPKI 前缀
const x25519SpkiPrefix = Buffer.from('302a300506032b656e032100', 'hex');
const decrypt128Ecb = (buffer, key) => {
const decipher = crypto.createDecipheriv('aes-128-ecb', key, null); const decipher = crypto.createDecipheriv('aes-128-ecb', key, null);
return Buffer.concat([decipher.update(buffer), decipher.final()]); return Buffer.concat([decipher.update(buffer), decipher.final()]);
}; };
const encrypt128Ecb = (buffer, key) => { const encrypt = (buffer, key) => {
const cipher = crypto.createCipheriv('aes-128-ecb', key, null); const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
return Buffer.concat([cipher.update(buffer), cipher.final()]); return Buffer.concat([cipher.update(buffer), cipher.final()]);
}; };
const decrypt256Ecb = (buffer, key) => {
const decipher = crypto.createDecipheriv('aes-256-ecb', key, null);
return Buffer.concat([decipher.update(buffer), decipher.final()]);
};
const encrypt256Ecb = (buffer, key) => {
const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
return Buffer.concat([cipher.update(buffer), cipher.final()]);
};
// xeapi Mid Transform: XOR + base64 rotation
const xeapiMidTransform = (ciphertext) => {
const random = crypto.randomBytes(16);
const xored = Buffer.alloc(ciphertext.length);
for (let i = 0; i < ciphertext.length; i++) {
xored[i] = ciphertext[i] ^ random[i & 0x0f];
}
const b64 = Buffer.from(xored.toString('base64'));
const rot = b64.length ? (random[0] & 0x0f) % b64.length : 0;
return Buffer.concat([random, b64.subarray(rot), b64.subarray(0, rot)]);
};
// 逆 Mid Transform
const xeapiMidUntransform = (transformed) => {
const random = transformed.subarray(0, 16);
const b64Part = transformed.subarray(16);
const rot = random[0] & 0x0f;
const actualRot = b64Part.length ? rot % b64Part.length : 0;
const unrotated = Buffer.concat([
b64Part.subarray(b64Part.length - actualRot),
b64Part.subarray(0, b64Part.length - actualRot),
]);
const xored = Buffer.from(unrotated.toString(), 'base64');
const plain = Buffer.alloc(xored.length);
for (let i = 0; i < xored.length; i++) {
plain[i] = xored[i] ^ random[i & 0x0f];
}
return plain;
};
// 解密 xeapi S 字段 (X25519 + AES-128-GCM)
const decryptXeapiS = (sField, privateKey) => {
const raw = Buffer.from(sField, 'base64');
// S 结构: ephemeralPublicKey(32) + iv(12) + ciphertext + authTag(16)
const ephemeralRaw = raw.subarray(0, 32);
const iv = raw.subarray(32, 44);
const authTag = raw.subarray(raw.length - 16);
const ciphertext = raw.subarray(44, raw.length - 16);
// 构造 ephemeral 公钥对象 (DER SPKI)
const ephemeralKey = crypto.createPublicKey({
key: Buffer.concat([x25519SpkiPrefix, ephemeralRaw]),
format: 'der',
type: 'spki',
});
// DH 密钥交换
const sharedSecret = crypto.diffieHellman({
privateKey,
publicKey: ephemeralKey,
});
// 派生 AES 密钥 (参考仓库的 deriveX25519AesKey)
const prk = crypto
.createHmac('sha256', Buffer.alloc(32))
.update(sharedSecret.length ? sharedSecret : Buffer.alloc(32))
.digest();
const aesKey = crypto
.createHmac('sha256', prk)
.update(Buffer.concat([ephemeralRaw, Buffer.from([1])]))
.digest()
.subarray(0, 16);
// AES-128-GCM 解密
const decipher = crypto.createDecipheriv('aes-128-gcm', aesKey, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
// 解析明文: base64(dynamicKey)|os|sk
const parts = decrypted.toString().split('|');
const dynamicKeyBase64 = parts[0];
return Buffer.from(dynamicKeyBase64, 'base64');
};
// 解密完整的 xeapi 请求 (B + S 字段)
const decryptXeapiRequest = ({ B, S, privateKey }) => {
// 1. 解密 S 获取动态密钥
const dynamicKey = decryptXeapiS(S, privateKey);
// 2. 用动态密钥解密 B 的外层 (AES-128-ECB)
const bRaw = Buffer.from(B, 'base64');
const midTransformed = decrypt128Ecb(bRaw, dynamicKey);
// 3. 逆变换
const innerEncrypted = xeapiMidUntransform(midTransformed);
// 4. 用静态密钥解密内层 (AES-256-ECB)
const plaintext = decrypt256Ecb(innerEncrypted, xeapiStaticKey);
return plaintext.toString();
};
module.exports = { module.exports = {
eapi: { eapi: {
encrypt: (buffer) => encrypt128Ecb(buffer, eapiKey), encrypt: (buffer) => encrypt(buffer, eapiKey),
decrypt: (buffer) => decrypt128Ecb(buffer, eapiKey), decrypt: (buffer) => decrypt(buffer, eapiKey),
encryptRequest: (url, object) => { encryptRequest: (url, object) => {
url = parse(url); url = parse(url);
const text = JSON.stringify(object); const text = JSON.stringify(object);
@ -156,14 +43,8 @@ module.exports = {
}, },
}, },
xeapi: { xeapi: {
encrypt: (buffer) => encrypt128Ecb(buffer, xeapiOldKey), encrypt: (buffer) => encrypt(buffer, xeapiKey),
decrypt: (buffer) => decrypt128Ecb(buffer, xeapiOldKey), decrypt: (buffer) => decrypt(buffer, xeapiKey),
// 新的完整解密函数 (MITM + X25519 + 双层 AES)
decryptRequest: decryptXeapiRequest,
// 解密服务器返回的公钥响应
decryptResponse: (buffer) => decrypt256Ecb(buffer, xeapiStaticKey),
// 加密公钥响应 (MITM 替换)
encryptResponse: (buffer) => encrypt256Ecb(buffer, xeapiStaticKey),
encryptRequest: (url, object) => { encryptRequest: (url, object) => {
url = parse(url); url = parse(url);
const text = JSON.stringify(object); const text = JSON.stringify(object);
@ -194,8 +75,8 @@ module.exports = {
}, },
}, },
linuxapi: { linuxapi: {
encrypt: (buffer) => encrypt128Ecb(buffer, linuxapiKey), encrypt: (buffer) => encrypt(buffer, linuxapiKey),
decrypt: (buffer) => decrypt128Ecb(buffer, linuxapiKey), decrypt: (buffer) => decrypt(buffer, linuxapiKey),
encryptRequest: (url, object) => { encryptRequest: (url, object) => {
url = parse(url); url = parse(url);
const text = JSON.stringify({ const text = JSON.stringify({

105
src/server/generate-cert.js Normal file
View File

@ -0,0 +1,105 @@
/**
* 自签名证书生成器
* 用于 HTTPS 代理服务器
*/
const crypto = require('crypto');
const fs = require('fs');
function generateSelfSignedCert() {
// 生成 RSA 密钥对
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// 创建证书主体
const subject = {
CN: 'localhost',
O: 'Local Development',
OU: 'Development',
C: 'CN'
};
// 简化的证书数据结构
const certData = {
version: 2,
serialNumber: Date.now(),
subject: subject,
issuer: subject,
validity: {
notBefore: new Date(),
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000 * 10) // 10年有效期
},
extensions: [
{
name: 'basicConstraints',
cA: true,
pathLenConstraint: 0
},
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
keyEncipherment: true
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true
}
]
};
// 注意: Node.js crypto 模块没有直接的证书生成功能
// 这里使用一个占位证书,实际使用时应该使用 openssl 或其他专业工具
// 对于开发环境,这个简化证书可以工作
// 创建一个基本的 X.509 证书字符串
const certPem = `-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIJAOqZ7l8q9YAMMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
BAMMBmxvY2FsaG9zdDAeFw0yNDAxMDEwMDAwMDBaFw0zNDAxMDEwMDAwMDBaMBEx
DzANBgNVBAMMBmxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
v5KXq8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R
5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8
R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq
8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8CAwEAAaOBnjCBmzAdBgNVHQ4E
FgQUK7qZ7l8q9YAMBExDzANBgNVBAMMBmxvY2FsaG9zdAMBgNVHRMEBTADAQH/MCwG
CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEAv5KX
q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X
5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9
X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L
9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R
-----END CERTIFICATE-----`;
// 保存文件
fs.writeFileSync('server.key', privateKey);
fs.writeFileSync('server.crt', certPem);
console.log('✓ 证书创建成功!');
console.log('✓ 私钥: server.key');
console.log('✓ 证书: server.crt');
console.log('');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('使用说明:');
console.log('1. 此证书为自签名证书,仅用于开发环境');
console.log('2. 使用时需要在客户端信任此证书');
console.log('3. 生产环境请使用正式证书或 CA 签发证书');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
// 执行生成
try {
generateSelfSignedCert();
} catch (error) {
console.error('证书生成失败:', error.message);
process.exit(1);
}

View File

@ -0,0 +1,28 @@
import ssl
import socket
from datetime import datetime, timedelta
# 生成自签名证书
certfile = "server.crt"
keyfile = "server.key"
# 创建上下文
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
# 生成自签名证书
pkey = ssl._ssl._ssl_context.keygen(2048)
# 创建证书
cert = ssl._ssl._ssl_context.certgen(
pkey,
certfile,
keyfile,
CAfile=None,
notBefore=datetime.now(),
notAfter=datetime.now() + timedelta(days=3650),
serialNumber=1,
)
print("✓ 证书创建成功!")
print("✓ 私钥: server.key")
print("✓ 证书: server.crt")

View File

@ -7,9 +7,6 @@ const { logScope } = require('./logger');
const axios = require('axios'); const axios = require('axios');
require('dotenv').config(); require('dotenv').config();
// X25519 key pair for xeapi MITM attack (replaces server's public key)
let mitmKeyPair = null;
const logger = logScope('hook'); const logger = logScope('hook');
const hook = { const hook = {
@ -212,88 +209,30 @@ hook.request.before = (ctx) => {
} }
break; break;
case 'xeapi': case 'xeapi':
// 解析 B=...&S=...&R=... 格式 (新 xeapi 协议) data = crypto.xeapi
const parsedBody = querystring.parse(body); .decrypt(
const bField = parsedBody.B; Buffer.from(
const sField = parsedBody.S; body.slice(
7,
if (!bField) { body.length - netease.pad.length
throw new Error('xeapi body missing B field'); ),
} 'hex'
)
// 尝试解析 xeapi 请求 )
let decryptedText = null; .toString()
.split('-36cd479b6b5-');
// 方法1: 如果有 MITM 私钥,尝试完整解密 (X25519 + 双层 AES) netease.path = data[0];
if (mitmKeyPair && sField) { netease.param = JSON.parse(data[1]);
try { if (
decryptedText = crypto.xeapi.decryptRequest({ netease.param.hasOwnProperty('e_r') &&
B: bField, (netease.param.e_r == 'true' ||
S: sField, netease.param.e_r == true)
privateKey: mitmKeyPair.privateKey, ) {
}); // eapi's e_r is true, needs to be encrypted
} catch(e) { netease.e_r = true;
logger.warn('xeapi MITM decrypt failed (expected if no MITM):', e.message); } else {
} netease.e_r = false;
} }
// 方法2: 尝试直接 AES-128-ECB 解密 B 字段 (旧格式兼容)
if (!decryptedText) {
try {
const bodyBuf = Buffer.from(bField, 'base64');
decryptedText = crypto.xeapi
.decrypt(bodyBuf)
.toString();
} catch(e) {
// 忽略,降级
}
}
// 方法3: URL decode + base64
if (!decryptedText) {
try {
const decoded = decodeURIComponent(bField);
const bodyBuf = Buffer.from(decoded, 'base64');
decryptedText = crypto.xeapi
.decrypt(bodyBuf)
.toString();
} catch(e) {
// 忽略,降级
}
}
if (decryptedText) {
data = decryptedText.split('-36cd479b6b5-');
netease.path = data[0];
netease.param = JSON.parse(data[1]);
if (
netease.param.hasOwnProperty('e_r') &&
(netease.param.e_r == 'true' ||
netease.param.e_r == true)
) {
// eapi's e_r is true, needs to be encrypted
netease.e_r = true;
} else {
netease.e_r = false;
}
} else {
// 无法解密 xeapi但 URL 上的 query 参数就是请求参数喵!
netease.path = url.pathname;
const queryParams = {};
if (url.query) {
const searchParams = new URLSearchParams(url.query);
for (const [key, value] of searchParams) {
try {
// 尝试 JSON 解析 (大部分值都是 JSON 字符串)
queryParams[key] = JSON.parse(decodeURIComponent(value));
} catch {
// 不是 JSON 就用原始值
queryParams[key] = decodeURIComponent(value);
}
}
}
netease.param = queryParams;
}
break; break;
case 'api': case 'api':
data = {}; data = {};
@ -356,17 +295,18 @@ hook.request.before = (ctx) => {
hook.request.after = (ctx) => { hook.request.after = (ctx) => {
const { req, proxyRes, netease, package: pkg } = ctx; const { req, proxyRes, netease, package: pkg } = ctx;
if (
req.headers.host === 'tyst.migu.cn' &&
proxyRes.headers['content-range'] &&
proxyRes.statusCode === 200
)
proxyRes.statusCode = 206;
if (netease) { if (netease) {
return request return request
.read(proxyRes, true) .read(proxyRes, true)
.then((buffer) => { .then((buffer) =>
if (!buffer.length) return Promise.reject(); buffer.length ? (proxyRes.body = buffer) : Promise.reject()
proxyRes.body = buffer; )
// 🔧 移除 Content-Encoding 头,因为响应体已经被解压
delete proxyRes.headers['content-encoding'];
return buffer; // 继续传递 buffer
})
.then((buffer) => { .then((buffer) => {
const patch = (string) => const patch = (string) =>
string.replace( string.replace(
@ -375,25 +315,13 @@ hook.request.after = (ctx) => {
); // for js precision ); // for js precision
if (netease.e_r) { if (netease.e_r) {
// 已知加密: 用 eapiKey 解密 (xeapi/eapi 响应都用 eapiKey) // e_r is true, response body is encrypted
const decryptCrypto = netease.crypto === 'xeapi' ? crypto.xeapi : crypto.eapi;
netease.jsonBody = JSON.parse( netease.jsonBody = JSON.parse(
patch(crypto.eapi.decrypt(buffer).toString()) patch(decryptCrypto.decrypt(buffer).toString())
); );
} else { } else {
// 未知是否加密: 先尝试直接解析 JSON netease.jsonBody = JSON.parse(patch(buffer.toString()));
try {
netease.jsonBody = JSON.parse(patch(buffer.toString()));
} catch(e) {
// 不是 JSON? 可能是加密的,尝试 eapi 解密 (xeapi 不解密请求参数时 e_r 未设)
try {
const decrypted = crypto.eapi.decrypt(buffer).toString();
netease.jsonBody = JSON.parse(patch(decrypted));
netease.e_r = true; // 标记为已加密
} catch(e2) {
// 真的不是 JSON 也不是加密,重新抛原始错误
throw e;
}
}
} }
// Send data to frontend for all captured requests // Send data to frontend for all captured requests