初步分析

用于入门学习,选了一个没加壳的学校的课程表app

先用小黄鸟Reqable抓包,登录入口是教务在线账号
登录包,xh是登录输入的学号的明文,两个加密参数encoded,encoded2,一个空的info

1
2
3
4
5
6
7
POST /anydoor/data/usc HTTP/1.1
Content-Type: application/json
Content-Length: 315
Host: api.douyeblog.top


{"xh":"20214111377","method":"login","encoded":"MjAyMTQxMTEzNzc=%%%MTIzNDU2","encoded2":"7d343122e910b4294297f72ca034f817b9d9d76c19001d0a0ef80afde3d40ce00a8d5c0a0771c5a350dcacef4008b3a26ffd4815fa33c127ba23f6b0216aad35bce39d4252cf4ff6362e6d57595b775a55c0452ec411afd7d92ca433d88d2e412151057a2e6c39df62","info":""}

response

1
{"code":200,"msg":"ok","data":{"info":"ade88eaf045fd2c2478d1541a1304643ff58e377f06031511fae14e92f0bfddd559adacdac176c8b71c9bbc9b84d402532d3a836cb3d29eddc70e4026e92134af3f69f854bce4f842abdccc303d823faafde0899d597bcff110c0d22816c84865a8fce6b6d991f7a16224f57f4af0d3f8d72d924c1ff1194c0bbd9c1a5483088062f5acd794bc23ea9188e1247d4ccd92a83ad9d3c09682fc327c762f0ae7c18e3a5d4e5d4a73417184d0f71b5bd67c5374f67e9f1d4dc37"}}

登录成功后信息获取

1
2
3
4
5
6
7
8
POST /anydoor/data/usc HTTP/1.1
user-agent: Mozilla/5.0 (Linux; Android 10; MI 8 Build/QKQ1.190828.002; wv)
Content-Type: application/json
Content-Length: 682
Host: api.douyeblog.top


{"xh":"20214111377","encoded":"MjAyMTQxMTEzNzc=%%%MTIzNDU2","encoded2":"9bc1711a21b398a50b779a2eaa19381889728faa9db2e6eb0333edf448809f00381b69e590864efaae9c87f414a44d33ff98eb369c52bc0638b78c0fcd20339dac0c3567b31ff3d6c7dc2d00efea1840d58f097cdecf0d04989c270332e02656355936912d650e1666","method":"info","info":"35e4d336ecb1f5f4095a6cf86f244d960e5b071862a00df65a3d53dd52c19ed496a1eed213d259fc892feced18526707b32f16e11b0913a97692cd65f9b73babf86c5f99e46166f8a42e1796172522bd5d4fa9a64220a7051d46d77db041d9fbcf1b0f415cb5a02bd6a8377aab4ca6d5f5e7bafce724157cb1d922f615193d36f4bf43370c8de1be063b1b97e9cf6852dc25eed7a1cf13b4cb9ec321c975fe7d465ea6a4ee9ec3f3e4aad1aeda65761ff88174932d49f0ae"}

返回具体信息

1
{"code":200,"msg":"ok","data":{"stuInfo":{"学生姓名:":"xxx","学生编号:":"20214111377","所属院系:":"xxx学院/xxx学院","专业名称:":"xxxxxx","班级名称:":"xxxxxxx"},"nowWeek":"10","allWeek":"19"}}

jdax看一下反编译结果,dex乱码不可读,其他地方也找不到任何业务逻辑

解包后有一个assets文件夹,应该是uniapp框架,放jdax-mcp跑一下基本架构信息,plugins里面装一个JADX-AI-MCP Plugin,起一个mcp服务就能用

uniapp

DCloud 5+ App ,uni-app 旧版框架,DCloud的标准入口类io.dcloud.PandoraEntry,旧版的APP架构就是HTML5 + JS直接打包
是一个使用Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序、快应用等多个平台

1
业务逻辑都封装在assets/apps/UNI(__UNI__XXXXXXX)的app-service.js,非常大,我的jadx打开会卡死。找参数的时候就看这个文件夹里面的js文件就行了

这个js文件没有混淆直接搜encoded

请求构造

1
2
3
4
5
6
7
8
data: {
xh: e.xh, // 学号
method: "login", // 方法名
encoded: t, // 编码后的学号+密码
encoded2: o, // SM2加密的密码
info: n // 存储的信息
}
看到"encoded":"MjAyMTQxMTEzNzc=%%%MTIzNDU2"

学号明文,两个部分都用的base64,账号和密码直接解码得到明文,用三个百分号拼起来
重点在encoded2,SM2加密,可以看到加密公钥,加密模式1,然后复制给encoded2
第二段PI函数里面的encoded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let t = "", o = e.pwd || getApp().$vm.$store.state.login.jiaowu.pwd;
//密码从参数 e.pwd 获取,如果没有则从全局状态管理中获取
o && (t = Al.sm2.doEncrypt(o, "04488a62410c17ca9800d18320ffc2343a122a0d1b63b166d1937a8d4243501ae2fdc15eb63f3e1997fcfa167c26d4c56c34338824737acb6ccd11d550fc6d9382", 1),
n.encoded2 = t)


第二段PI函数里面
let Pl = function(e) {
//ys() 是 Base64 编码函数
//Base64(学号) + "%%%" + Base64(密码)
let t = ys(e.xh) + "%%%" + ys(e.pwd), // encoded 字段
o = "";
try {
o = Al.sm2.doEncrypt(e.pwd, "04488a62410c17ca9800d18320ffc2343a122a0d1b63b166d1937a8d4243501ae2fdc15eb63f3e1997fcfa167c26d4c56c34338824737acb6ccd11d550fc6d9382", 1)
} catch(i) {}
// ...
data: { xh: n, encoded: t, encoded2: o, method: "info" }
}

info加密的逻辑,首次登录空值,之后读取本地的info加密输出,有服务器返回的和本地的info
接收之后用私钥解密之后用服务器公钥加密存本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 发送请求
const n = uni.getStorageSync("______info______"); // 从本地存储读取
data: { xh: e, method: "login", encoded: t, encoded2: o, info: n }

// 接收响应
success(e) {
if (200 === e.data.code) {
// 步骤1: 用私钥解密服务器返回的 info
t = Al.sm2.doDecrypt(
e.data.data.info,
"c31e904485ed4790f775c3f17e7bc1d8a7abf64385ceea058d009df90fa433b5",
// 私钥
1
)
//再用公钥重新加密
t = Al.sm2.doEncrypt(
t,
"04488a62410c17ca9800d18320ffc2343a122a0d1b63b166d1937a8d4243501ae2fdc15eb63f3e1997fcfa167c26d4c56c34338824737acb6ccd11d550fc6d9382", // 公钥
1
)//存到本地
uni.setStorageSync("______info______", t)

return { info: t }
}
}

直接写一个解密脚本,服务器响应返回的,用本地的SM2私钥就可以直接解密
解密结果是直接返回,JSESSIONID=CD1DF1E0E7830C6BF3056D06D21ED96B; Path=/jsxsd; HttpOnly, SERVERID=123; path=/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  const sm2 = require('sm-crypto').sm2;
const privateKey =

function decrypt(cipherText) {

try {

const result = sm2.doDecrypt(cipherText, privateKey, 1);
return result || null;
} catch (e) {
return null;
}
}
const serverInfo = 'e9dfac427392f980a88ded48b9f0d50599331f1657cfb5d86ba40c5e2fb940440ef7ec74aa342a1badd1f33456e425561f9a025df2c43263f731f33c9c80b2f700eb49e6769f5005ea9450c2a36bb2246ca4babb55cae783f3b698f51135028cb7ee981608a76d933fa8183935ef1c1a77c8e3511ef68dad19380175800eacc3f42b28d937661759b59ae32eb165269d017c3831ed44a980266e05217d72d8db85b110fc7b8709699c1c25500a45bcb65d7201b547880c12';
const result = decrypt(serverInfo);

带着cookie请求信息的response直接返回的是明文

写一个脚本提取app-service.js里面的接口和路径,或者直接用小黄鸟点点点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const fs = require('fs');
const path = require('path');
const INPUT_FILE = path.join(__dirname, 'app-service.js');
const OUTPUT_FILE = path.join(__dirname, 'extracted_urls.json');
function extractUrlsAndApis(content) {
const httpUrls = new Set();
const domains = new Set();
const apiPaths = new Set();
const urlVariables = new Set();
const uniRequests = new Set();

const urlPattern = /https?:\/\/[^\s"'`<>\{\}\[\]]+\/?[^\s"'`<>\{\}\[\]]*/gi;
(content.match(urlPattern) || []).forEach(url => {
const cleanUrl = url.replace(/[),;\]\}]+$/g, '');
httpUrls.add(cleanUrl);
const domainMatch = cleanUrl.match(/https?:\/\/([^\/\:]+)/);
if (domainMatch) domains.add(domainMatch[1]);
});

const genericPathPattern = /["'`]((\/[a-zA-Z0-9_\-\.]+(?:\/[a-zA-Z0-9_\-\.:\$\{\}]+)*))["'`]/g;
let pathMatch;
while ((pathMatch = genericPathPattern.exec(content)) !== null) {
const p = pathMatch[1];
if (p.length > 3 && !p.includes('.css') && !p.includes('.html') && !p.includes('<')) {
apiPaths.add(p);
}
}
const concatPathPattern = /\+\s*["'`]([^"'`]+)["'`]/g;
let concatMatch;
while ((concatMatch = concatPathPattern.exec(content)) !== null) {
const p = concatMatch[1].trim();
if (p.startsWith('/') && p.length > 2) {
apiPaths.add(p);
}
}

const uniRequestPattern = /uni\.request\s*\(\s*\{[^}]*url\s*:\s*["'`]([^"'`]+)["'`]/gi;
let uniMatch;
while ((uniMatch = uniRequestPattern.exec(content)) !== null) {
uniRequests.add(uniMatch[1]);
}


const urlVarPattern = /(?:base\s*[_-]?url|api\s*[_-]?url|server\s*[_-]?url|host\s*[_-]?url|domain|gateway)\s*[:=]\s*["'`]([^"'`]+)["'`]/gi;
let varMatch;
while ((varMatch = urlVarPattern.exec(content)) !== null) {
urlVariables.add(varMatch[0]);
}

const ipPattern = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?/gi;
(content.match(ipPattern) || []).forEach(ip => {
const cleanIp = ip.replace(/[),;\]\}]+$/g, '');
httpUrls.add(cleanIp);
});

return {
httpUrls: [...httpUrls],
domains: [...domains],
apiPaths: [...apiPaths],
urlVariables: [...urlVariables],
uniRequests: [...uniRequests]
};
}

try {
if (!fs.existsSync(INPUT_FILE)) {
throw new Error(`找不到文件: ${INPUT_FILE}`);
}

const content = fs.readFileSync(INPUT_FILE, 'utf-8');
const results = extractUrlsAndApis(content);

fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2), 'utf-8');
} catch (error) {

console.error(error.message);
process.exit(1);
}

webview调试条件
Chrome WebView版本要大于手机端 WebView 版本 ,chrome更新到最新版
如果WebView下的一些信息都没显示出来则代表你没有调试权限限setWebContentsDebuggingEnabled 这个的值必须为true才有调试权限,一般都会把这个值设置成假,直接Hook掉就行,或者用插件 https://github.com/feix760/WebViewDebugHook
inspect 打开空白,就是需要挂个梯子下载离线包
运行在 WebView 里的框架先调试发包的方式,打开DevTools,inspect开始调试,Network 能抓到数据就是网页发包,不然就是java发包


还有一个xp的插件来平替 https://github.com/WankkoRee/WebViewPP
关于这个app没有什么功能点,提取出来的json里面的接口大都是直接去请求学校的一些开放的系统直接跳转。


访客数 0 访问量 0 运行 0