解析链家小程序 - 获取请求校验方式
数据需求,对链家小程序进行请求抓包,发现每次合法请求都存在 Authorization 授权码,想要成功模拟小程序发送请求,就必须破解得到Authorization 授权码的生成方式。
目录
- 1、获取链家微信小程序的 .wxapkg 包文件、解开 .wxapkg 程序包
- 2、了解.wxapkg文件结构
- 3、查看程序逻辑,实验生成Authorization
- 4、验证迭代、实现加密方式
1、获取链家小程序的 .wxapkg 包文件、解开 .wxapkg 程序包
原理:基于 android-sdk\platform-tools\adb工具,通过 root 权限获取小程序安装包 .wxapkg ,通过github 上的wechat-app-unpack 项目,逆向解压获取小程序源码。
获取小程序安装包
- 手机开启root权限,如何开启依据机型系统而定,这里使用网易MuMu模拟器, 默认开启root权限;
- 手机开启USB调试功能, 安装微信,打开链家小程序;
- 电脑安装 android-sdk;
- 获取小程序安装包:
> adb shell
# cd /data/data/com.tencent.mm/MicroMsg/70aa34178251376743797472a68c1c6a/appbrand/pkg
# ls
_-1261323258_6.wxapkg
_1079392110_3.wxapkg
_1123949441_106.wxapkg
# cp _-1261323258_6.wxapkg /sdcard/
# exit
> adb pull /sdcard/_-1261323258_6.wxapkg .
注意:
- 目录 /data/data/com.tencent.mm/MicroMsg/70aa34178251376743797472a68c1c6a/appbrand/pkg 中, 70aa34178251376743797472a68c1c6a 文件夹可能因不同的机器有所改变,在/data/data/com.tencent.mm/MicroMsg/ 目录下自行寻找一下,
- -12613232586.wxapkg 在我的机器上为 链家小程序的wxapkg文件,请大家自行寻找自己机器上的名字
由此,即可获取链家小程序安装包;
解开 .wxapkg 程序
这里 使用 python 版的程序解开.wxapkg 包, 将 -1261323258_6.wxapkg放到 unpack-wxapkg.py 同级目录下。
# python3 unpack-wxapkg.py _-1261323258_6.wxapkg
2、了解.wxapkg文件结构
- page-frame.html:
存放了我们的页面结构的文件,整个小程序的 WXML 都会处理后放入这个文件,比如你可以看下截图
- app-config.json
其实是我们小程序根目录的app.json,处理后就变成了app-config.json。格式化后的代码如下
- pages:页面样式存放目录,实际上是将我们的 wxml 处理后,将 wxss 放在这里。
- app-service.js:页面逻辑所在位置,使用js格式化工具格式化, 我们等下就是要解析这个文件
3、查看程序逻辑,实验生成Authorization
查找可能是 Authorization逻辑的代码段
将代码中折叠起来后发现,代码中载入了大量的模块,如下图所示,在这里看到 base64.js 、md5.js 、request.js 等模块,在载入模块语句内就是 这个模块的具体实现代码。
这里 猜测request.js 是对请求生成授权码的代码部分,展开utils/request.js 下的代码,将代码粘出来到另一个文件中,由上到下进行解析,只看下面注释部分,后面的代码部分 不重要,不想看代码的,可以直接跳到最后总结部分。
exports.request = function (i) {
return new Promise(function (s, u) {
/* url = 'https://wechat.lianjia.com/ershoufang/search?city_id=310000&condition=&query=&order=&offset=0&limit=10&sign='
Authorization = bGp3eGFwcDoxYmU3OThjZDg0ZWU4NzNmM2JhMzM0NTFhZTNkNWUwMA==
*/
// 这里主要进行的是一个相对URL转换为绝对URL的过程,这个不重要。
i.url = 0 != i.url.indexOf("http") ? "https://wechat.lianjia.com/" + i.url : i.url,
r().then(function (r) {
// 在这里 配置请求头信息
var c = o("lianjia_header", !0);
c["Lianjia-Session"] = o("session_id"),
c["Lianjia-Source"] = "ljwxapp",
c["OS-Version"] = r.platform + "-" + r.system,
c["Wx-Version"] = r.version,
c["Wxminiapp-SDK-Version"] = r.SDKVersion,
c["Lianjia-Wxminiapp-Version"] = .1,
c["Time-Stamp"] = +new Date,
i.data = i.data || {};
// 对请求URL进行 URL + city_id 进行URL格式编码,并从传进来的参数重取出sign, 这里sign 跳过
var f = encodeURIComponent(i.url) + i.data.city_id || "";
i.data.sign = o(f);
/* 这里定义了一个变量 l, 并空串,对参数key进行排序后组成一个字符串。
1、参数部分:
city=310000&condition=&query=&order=&offset=0&limit=10&sign=’,
2、解析成key-value 键值对:
{'city_id': '310000', 'condition': '', 'query': '', 'order': '', 'offset': '0', 'limit': '10', 'sign': ''},
3、从小到大排序后为:
{'city_id': '310000', 'condition': '', 'limit': '10', 'offset': '0', 'order': '', 'query': '', 'sign': ''}
4、用等号连接 key ,value值,并追加到 变量l 中为:
'city_id=310000condition=limit=10offset=0order=query=sign='
*/
var l = "";
Object.keys(i.data).sort().forEach(function (e) {
return l += e + "=" + ("object" == a(i.data[e]) ? JSON.stringify(i.data[e]) : i.data[e])
}),
// 这里对l 加上了一个后缀‘6e8566e348447383e16fdd1b233dbb49’,现在l = ‘city_id=310000condition=limit=10offset=0order=query=sign=6e8566e348447383e16fdd1b233dbb49’,
l += "6e8566e348447383e16fdd1b233dbb49",
// 获取变量 l 的MD5值:为 '1be798cd84ee873f3ba33451ae3d5e00'
l = e.default.hexMD5(l),
// 为 l 的MD5值加上前缀‘ljwxapp:’,现在l 为 ‘ljwxapp:1be798cd84ee873f3ba33451ae3d5e00’
l = "ljwxapp:" + l,
// 通过 后面代码中的t 对象中载入了 base64 模块,猜测这里对 l 进行了base64编码
c.Authorization = t.encode(l),
// 这里基本完成了 Authorization 的生成过程,后面我们去将生成过程总结一下。
i.header = Object.assign(i.header || {}, c),
i.method && "post" == i.method.toLocaleLowerCase() && (i.header["content-type"] = i.header["content-type"] || "application/x-www-form-urlencoded");
var p = i.success,
y = i.fail;
i.success = function (a) {
a.header["Lianjia-Uuid"] && d("lianjia_header", {
"Lianjia-Uuid": a.header["Lianjia-Uuid"]
}),
a.data && 20007 == a.data.error_code ? i.custom || (n(), console.log("用户未登录")) : a.data && 0 == a.data.error_code ? (a.data.data && 0 === a.data.data.has_new ? a.data.data = o(i.data.sign, !0) : a.data.data && 1 === a.data.data.has_new && a.data.data.sign && (i.data.sign && d(i.data.sign, ""), d(f, a.data.data.sign), d(a.data.data.sign, a.data.data)), s(a.data), p && p(a.data)) : (a.header["Lianjia-Uuid"] && d("lianjia_header", {
"Lianjia-Uuid": a.header["Lianjia-Uuid"]
}), u(a.data), y && y(a.data))
},
i.fail = function (a) {
u(a.data),
y && y(a.data)
},
wx.request(i)
})
})
};
// 这里是最后的 t 对象,不重要
var t = function (a) {
if (a && a.__esModule) return a;
var e = {};
if (null != a) for (var t in a) Object.prototype.hasOwnProperty.call(a, t) && (e[t] = a[t]);
return e.
default = a,
e
}(require("base64.js")),
n = require("./navToLogin.js"),
i = void 0,
r = function () {
return new Promise(function (a, e) {
i ? a(i) : wx.getSystemInfo({
success: function (e) {
a(i = e)
},
fail: function (a) {
e({})
}
})
})
},
o = function (a, e) {
try {
var t = wx.getStorageSync(a);
return e ? JSON.parse(t || "{}") : t || ""
} catch (a) {
return e ? {} : ""
}
},
d = function (e, t) {
wx.setStorage({
key: e,
data: "object" == (void 0 === t ? "undefined" : a(t)) ? JSON.stringify(t) : t
})
};
4、验证迭代、实现加密方式
将上面的注释部分提取下来,为以下过程, 这里以获取二手房信息为例
Authorization = 'bGp3eGFwcDoxYmU3OThjZDg0ZWU4NzNmM2JhMzM0NTFhZTNkNWUwMA=='
加密过程
1、获取参数部分,这里为 ‘city=310000&condition=&query=&order=&offset=0&limit=10&sign=’
2、对参数进行从大到小排序、并将key-value用等号连接起来,将所有的元素连接成为一个字符串 S
排序后:
{'city_id': '310000', 'condition': '', 'limit': '10', 'offset': '0', 'order': '', 'query': '', 'sign': ''}
key-value用等号连接起来,并连接所有元素:
S = 'city_id=310000condition=limit=10offset=0order=query=sign='
3、 添加后缀 "6e8566e348447383e16fdd1b233dbb49"到变量S中,获取S的MD5值。
4、为变量 S 的MD5值 添加前缀 ‘ljwxapp:’,并作base64 编码,生成为:‘bGp3eGFwcDoxYmU3OThjZDg0ZWU4NzNmM2JhMzM0NTFhZTNkNWUwMA==’的 Authorization
Python 实现代码如下
AUTHORIZATION_SIFFOX = "6e8566e348447383e16fdd1b233dbb49"
AUTHORIZATION_PREFIX = 'ljwxapp:'
def get_authorization(data):
"""
获取 authorization
:param data:
例子参数
{'city_id': '310000', 'condition': '', 'query': '', 'order': '',
'offset': '0', 'limit': '10', 'sign': ''}
:return:
例子 authorization 返回值
b'bGp3eGFwcDoxYmU3OThjZDg0ZWU4NzNmM2JhMzM0NTFhZTNkNWUwMA=='
"""
global AUTHORIZATION_SIFFOX
global AUTHORIZATION_PREFIX
l = ""
data_sort = dict_sort(data)
l += ''.join([key + '=' + str(data_sort[key]) for key in data_sort.keys()])
l += AUTHORIZATION_SIFFOX
l_md5 = hashlib.md5(l.encode()).hexdigest()
authorization_source = AUTHORIZATION_PREFIX+l_md5
authorization = base64.b64encode(authorization_source.encode())
return authorization.decode()
更详细的代码见我的gitHub项目 reverse_lianjia_wxapkg
Ant-Ferry/reverse_lianjia_wxapkggithub.com
参考文章:
微信小程序获取与解压:
微信小程序获取与解压流程 · Issue #200 · billfeller/billfeller.github.iogithub.com
.wxapkg文件结构:
WXAPKG 解包后如何提取页面逻辑?cloud.tencent.com
声明
此次解析 仅为 研究、学习,请勿用于商业用途。