声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
最近巴黎奥运会,很多平台都搞起了免费饮品免单的活动,当然雪王也不例外,小程序与 App 同为 webview 类型的程序,起初该平台只有一个 sign 加密,最近压力上来了,更新了一个新参数 type_1286 ,不少粉丝也被这个参数难住了,本文仅对雪王的这个新参数进行逆向分析,仅供学习交流。
逆向目标
- 某雪冰城小程序、某雪冰城 App
I+Wwj+eoi+W6jzovL+icnOmbquWGsOWfji9hb1RwNU1zT0tJMHRHWWM=
构建调试框架
APP 端调试 webview
APP 端的 webview 调试主要借助 XP 模块,或者 LSP。然后导入 webview 模块即可调试, 以 LSP 为例,模拟器装好面具以及 LSP 后,将 webview 模块导入并且选择指定 APP:
然后重启模拟器,在浏览器输入chrome://inspect/#devices
,若无设备加载出来,则在 cmd 控制台多输入几次 adb devices
直到设备加载出来:
然后点击 inspect 即可进入,出现正常的 F12 界面证明调试成功:
小程序端调试 webview
- 首先 USB 数据线连接手机进入调试模式;
- 首先微信访问
http://debugxweb.qq.com/?inspector=true
确定是否可以用(能打开就能用); - 微信上打开你需要调试的页面;
- 谷歌浏览器地址栏输入
chrome://inspect/#devices
等待一会儿 (浏览器需要具备翻强功能); - 点击对应网页或者小程序 inspect 即可出现调试栏,然后像正常调试页面即可
chrome://inspect/#devices
。
最后效果如 App 端开启调试的结果相同。
抓包分析
进入免单界面,点击领取,在开发者工具中即可查看到该接口,如下:
其主要是加密参数为 url 接口中的 type_1286,以及提交内容中的 sign,本文将重点讲 type_1286 参数的生成。
逆向分析
type_1286 参数
该参数为领取免单券接口的重要参数,我们在 App 或者小程序端输入口令点击确认,观察堆栈,从第一个堆栈进入:
然后我们在进入的地方下一个断点,然后继续点击确认,发现在此处断了下来,发现是一个大 OB 混淆,与普通的OB 还不太一样,经过分析可知 UL 参数即为我们需要逆向的参数:
var UL = UE['Fu'](this[oA(P7.a)][-0x190d + -0x20e9 + 0x39f7])
, UL = F0[oA(P7.A)](UH, UL, UV);
在俩个 UL 参数中下断点,再次刷新点击确认,成功在 UL 处断了下来,经过分析可知,第一个 UL 为一个 object 对象,然后将 UL,UV 参数传入UH中完成加密,生成最终的 url 如下:
我们进入 UH 函数进行分析,发现它是一个 && 用法,最终通过 M['F6'](L, N)
生成加密参数 type_128,也就是调用 F6 函数生成最终的加密参数:
我们进入 F6 函数,观察它的生成逻辑:
发现它也是被混淆的不成样子了,最后加密参数由以下代码块生成:
(g += N),
(N = F[UJ(mS.F)](F[UJ(mS.Y)](F[UJ(mS.U)](F[UJ(mS.a)](F[UJ(mS.A)](M[UJ(mS.D)](g), '|'), (-0xfbf + 0x9 * -0x189 + 0x16 * 0x158,
m['n'])()), '|'), new Date()[UJ(mS.o)]()), '|1'),
g = E['FU']['ua'](N, !(0xbb4 + 0x1a49 * -0x1 + 0xe95)),
N = {}),
(N[M['F7'](L[UJ(mS.i)])] = g,
L[UJ(mS.y)] = (-0x2ff + 0xbe5 + -0x8e6,
H['Fa'])(L[UJ(mS.y)], N),
(0x3 * 0xe5 + -0x614 + 0x1 * 0x365,
H['FY'])(L))
所以我们想要拿下这个参数就要将这个 F6 函数拿下,这里我们讲 2 种方法补环境和算法还原,如果细分第二种办法又可以分为两种。
补环境
关于补环境的话,我们直接将代码全部拿下,放到浏览器里跑一下试试:
没错,浏览器卡死了,甚至电脑的风扇都开始转个不停:
返回网页 js,我们发现在 F3 函数中存在格式化检测:
我们将代码压缩,放到 node 环境中执行一次,看看能不能正常报错:
发现我们的代码已经可以正常跑起来了,接下来就到缺啥补啥的环境了,还是老样子,将代理挂上:
memory = {
'Proxy': true,
'random': 0.5,
}
// Math.random = function(){return memory['random']};
memory.proxy = (function() {
memory.Object_sx = ['Date'];
memory.Function_sx = []//['Array', 'Object', 'Function', 'String', 'Number', 'RegExp', 'Symbol', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'Uint8Array'];
memory.setFun = [];
memory.getObjFun = [];
memory.color = {
'set': [3, 101, 100],
'get': [255, 140, 0],
'has': [220, 87, 18],
'apply': [107, 194, 53],
'ownKeys': [147, 224, 255],
'deleteProperty': [199, 21, 133],
'defineProperty': [179, 214, 110],
'construct': [200, 8, 82],
'getPrototypeOf': [255, 255, 255],
'object': [147, 224, 255],
'function': [147, 224, 255],
'number': [255, 224, 0],
'array': [147, 224, 0],
'string': [255, 224, 255],
'undefined': [255, 52, 4],
'boolean': [76, 180, 231],
};
memory.log = console.log;
memory.log_order = 0;
memory.proxy_Lock = 0;
// 文本样式
function styledText(text, styles) {
let styledText = text;
// RGB颜色
if (styles.color) {
styledText = `\x1b[38;2;${styles.color[0]};${styles.color[1]};${styles.color[2]}m${styledText}\x1b[0m`;
}
// 背景颜色
if (styles.bgColor) {
styledText = `\x1b[48;2;${styles.bgColor[0]};${styles.bgColor[1]};${styles.bgColor[2]}m${styledText}\x1b[0m`;
}
// 粗体
if (styles.bold) {
styledText = `\x1b[1m${styledText}\x1b[0m`;
}
// 斜体
if (styles.italic) {
styledText = `\x1b[3m${styledText}\x1b[0m`;
}
// 下划线
if (styles.underline) {
styledText = `\x1b[4m${styledText}\x1b[0m`;
}
// 返回带样式的文本
return styledText
}
// 文本填充
function limitStringTo(str, num) {
str = str.toString()
if (str.length >= num) {
return str + ' '
} else {
const spacesToAdd = num - str.length;
const padding = ' '.repeat(spacesToAdd);
// 创建填充空格的字符串
return str + padding;
}
}
// 进行代理
function new_obj_handel(target, target_name) {
if(memory.Proxy == false){return target};
let name = target_name.indexOf('.') != -1 ? target_name.split('.').slice(-1)[0]: target_name;
if (target['isProxy'] || memory.Object_sx.includes(name)) {
return target;
}else{
return new Proxy(target,my_obj_handler(target_name))
}
}
function new_fun_handel(target, target_name) {
if(memory.Proxy == false){return target}
let name = target_name.indexOf('.') != -1 ? target_name.split('.').slice(-1)[0]: target_name;
if (memory.Function_sx.includes(name)) {
return target;
}else{
return new Proxy(target,my_fun_handler(target_name))
}
}
// 获取数据类型
function get_value_type(value) {
if (Array.isArray(value)) {
return 'array'
}
if (value == undefined) {
return 'undefined'
}
return typeof value;
}
// 函数与对象的代理属性
function my_obj_handler(target_name) {
return {
set: function (obj, prop, value) {
if(memory['proxy_Lock']){
return Reflect.set(obj, prop, value);
};
const value_type = get_value_type(value);
const tg_name = `${target_name}.${prop.toString()}`;
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('setter',20) + limitStringTo(`hook->${tg_name};`,50)
// 如果设置到的属性是对象 --> 输出值对象
// 如果设置到的属性是方法 --> 输出值function
// 其他的就全部输出值
if (value && value_type === "object") {
memory.log(styledText(text, {
color: memory.color['set'],
}), styledText('value->',{
color: memory.color['set'],
}),value)
}
else if (value_type === "function") {
memory.setFun.push(tg_name)
memory.log(styledText(text , {
color: memory.color['set'],
}),styledText(`value->`, {
color: memory.color['set'],
}),styledText(`function`, {
color: memory.color[value_type],
}))
}
else {
memory.log(styledText(text, {
color: memory.color['set'],
}),styledText(`value->`, {
color: memory.color['set'],
}),styledText(`${value}`, {
color: memory.color[value_type],
}))
}
return Reflect.set(obj, prop, value);
},
get: function (obj, prop) {
if(memory['proxy_Lock']){
return Reflect.get(obj, prop)
};
if (prop === "isProxy") {
return true;
}
const value = Reflect.get(obj, prop);
const tg_name = `${target_name}.${prop.toString()}`;
const value_type = get_value_type(value);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('getter',20) + limitStringTo(`hook->${tg_name};`,50)
// 如果获取到的属性是对象 --> 对其getter和setter进行代理
// 如果获取到的属性是方法 --> 对其caller进行代理
// 其他的就全部输出值
if (value_type === 'object') {
if (memory.getObjFun.indexOf(tg_name) == -1){
memory.log(styledText(text, {
color: memory.color['get'],
}), styledText('value->',{
color: memory.color['get'],
}),value)
memory.getObjFun.push(tg_name)
}
return new_obj_handel(value,tg_name)
}
else if(value_type === "function"){
if (memory.getObjFun.indexOf(tg_name) == -1){
memory.log(styledText(text , {
color: memory.color['get'],
}),styledText(`value->`, {
color: memory.color['get'],
}),styledText(`function`, {
color: memory.color[value_type],
}))
memory.getObjFun.push(tg_name)
}
return new_fun_handel(value,tg_name);
}
else{
memory.log(styledText(text , {
color: memory.color['get'],
}),styledText( `value->` , {
color: memory.color['get'],
}),styledText( `${value}` , {
color: memory.color[value_type],
}))
return value
}
},
has: function(obj, prop) {
if(memory['proxy_Lock']){
return Reflect.has(obj, prop)
}
const value = Reflect.has(obj, prop);
const value_type = get_value_type(value);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('in',20) + limitStringTo(`hook->"${prop.toString()}" in ${target_name};`,50)
memory.log(styledText(text, {
color: memory.color['has'],
}), styledText(`value->`, {
color: memory.color['has'],
}), styledText(`${value}`, {
color: memory.color[value_type],
}))
return value;
},
ownKeys:function(obj){
if(memory['proxy_Lock']){
return Reflect.ownKeys(obj);
}
const value = Reflect.ownKeys(obj);
const value_type = get_value_type(value);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('ownKeys',20) + limitStringTo(`hook->${target_name};`,50)
memory.log(styledText(text, {
color: memory.color['ownKeys'],
}), styledText(`value->`, {
color: memory.color['ownKeys'],
}), styledText(`${value}`, {
color: memory.color[value_type],
}));
return value
},
deleteProperty:function(obj, prop) {
if(memory['proxy_Lock']){
return Reflect.deleteProperty(obj, prop);
}
const value = Reflect.deleteProperty(obj, prop);
const tg_name = `${target_name}.${prop.toString()}`;
const value_type = get_value_type(value);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('delete',20) + limitStringTo(`hook->${tg_name};`,50)
memory.log(styledText(text, {
color: memory.color['deleteProperty'],
}), styledText(`value->`, {
color: memory.color['deleteProperty'],
}), styledText(`${value}`, {
color: memory.color[value_type],
}));
return value;
},
defineProperty: function (target, property, descriptor) {
if(memory['proxy_Lock']){
return Reflect.defineProperty(target, property, descriptor);
};
const value = Reflect.defineProperty(target, property, descriptor);
const tg_name = `${target_name}.${property.toString()}`;
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('defineProperty',20) + limitStringTo(`hook->${tg_name};`,50)
memory.log(styledText(text, {
color: memory.color['defineProperty'],
}), styledText('value->',{
color: memory.color['defineProperty'],
}),descriptor)
return value;
},
getPrototypeOf(target) {
if(memory['proxy_Lock']){
return Reflect.getPrototypeOf(target);
}
var value = Reflect.getPrototypeOf(target);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('getPrototypeOf',20) + limitStringTo(`hook->${target_name};`,50)
memory.log(styledText(text, {
color: memory.color['getPrototypeOf'],
}), styledText('value->',{
color: memory.color['getPrototypeOf'],
}),value)
return value;
}
};
}
function my_fun_handler(target_name) {
return {
apply:function(target, thisArg, argumentsList){
if(memory['proxy_Lock']){
return Reflect.apply(target, thisArg, argumentsList);
};
if(memory.setFun.indexOf(target_name) != -1 || memory.setFun.includes(target_name.split('.')[0])){
// 扣的代码触发
var value = Reflect.apply(target, thisArg, argumentsList);
memory.setFun.push(`log_${memory['log_order'] + 1}`)
}
else{
// 补的环境触发的分支
memory['proxy_Lock'] = 1
var value = Reflect.apply(target, thisArg, argumentsList);
memory['proxy_Lock'] = 0
}
const value_type = get_value_type(value);
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('caller',20) + limitStringTo(`hook->log_${memory['log_order']} = ${target_name}();`,50);
memory.log(styledText(text, {
color: memory.color['apply'],
}),styledText('arguments->',{
color: memory.color['apply'],
}),argumentsList, styledText('returnValue->',{
color: memory.color[value_type],
}),value)
if(value_type == 'object'){
return new_obj_handel(value,`log_${memory['log_order']}`);
}
else if(value_type == 'function'){
return new_fun_handel(value,`log_${memory['log_order']}`);
}
return value;
},
construct: function (target, args, newTarget) {
if(memory['proxy_Lock']){
return Reflect.construct(target, args, newTarget)
}
if(memory.setFun.indexOf(target_name) != -1 || memory.setFun.includes(target_name.split('.')[0])){
var value = Reflect.construct(target, args, newTarget);
memory.setFun.push(`log_${memory['log_order'] + 1}`)
}
else{
memory['proxy_Lock'] = 1
var value = Reflect.construct(target, args, newTarget);
memory['proxy_Lock'] = 0
}
const text = limitStringTo(++memory['log_order'],5) + limitStringTo('new',20) + limitStringTo(`hook->log_${memory['log_order']} = new ${target_name}();`,50)
memory.log(styledText(text, {
color: memory.color['construct'],
}), styledText('arguments->',{
color: memory.color['construct'],
}),args, styledText('returnValue->',{
color: memory.color['construct'],
}),value);
return new_obj_handel(value, `log_${memory['log_order']}`);
},
}
}
// 返回进行对象代理
return new_obj_handel
}());
其中,检测最多的就是 createElement
校验了对标签的创建,以及标签下面的属于,检测较深但不严:
全部补完大概在 400 行代码左右,最后运行代码没有报错,那么我们的环境就已经补好了,如下:
那么我们应该如何调用加密函数呢?还记得刚刚的 F6 函数吗?我们将代码全部放到 Notepad 中,将代码进行改写,将 F6 函数导出:
然后将代码全部复制,放到在线 js 代码压缩网站中进行压缩,将压缩后的 js 代码放到我们刚刚补的环境下面,打印 console.log(window.kk)
看看我们的函数有没有导出:
最后将参数我们传入到加密函数中,不出所谓,打印出了正确的结果:
补环境的话需要考虑的地方很多,对某些节点检测甚至有 3-4 层的深度,全部拿下的话代码行数在 8000 行左右,下面我们用算法还原的方式将整个算法进行剖析。
算法还原 1
我们还是回到加密函数 F6 中,看看它具体是通过哪些步骤进行加密的:
首先将解密函数赋值给了 UJ,然后将 L 传入 H['FY'] 中进行取值,跟进 H['FY'] 看看它做了什么:
最终 var g = L["FW"] + L["hash"];
然后 g += N
,接着:
N = F[UJ(mS.F)](F[UJ(mS.Y)](F[UJ(mS.U)](F[UJ(mS.a)](F[UJ(mS.A)](M[UJ(mS.D)](g), '|'), (-0xfbf + 0x9 * -0x189 + 0x16 * 0x158,
m['n'])()), '|'), new Date()[UJ(mS.o)]()), '|1')
前面仍然是混淆的 + 函数,最后分析可得:
N = (((((sig(g) + '|') + 0) + '|') + new Date()["getTime"]()) + '|1')
然后调用 E['FU']['ua'](N, !(0xbb4 + 0x1a49 * -0x1 + 0xe95))
完成最后的加密:
所以分析可知,我们需要先拿下 sig 函数,进到 sig 中,发现其结构如下:
'sig': function(L) {
var Ub = Uc;
for (var N = 0xa52 + 0x1499 + 0x1 * -0x1eeb, g = F[Ub(mn.F)](encodeURIComponent, L), B = 0x129f + 0x7 * 0x3d + -0x144a; F[Ub(mn.Y)](B, g[Ub(mn.U)]); B++)
N = F[Ub(mn.a)](F[Ub(mn.A)](F[Ub(mn.D)](F[Ub(mn.o)](N, 0xb7a * -0x2 + 0x25c7 * 0x1 + -0xecc), N), -0x1bf6 + -0xb06 * -0x1 + 0x127e), g[Ub(mn.i)](B)),
N |= 0x6b * 0x35 + 0x1349 * 0x2 + -0x3cb9 * 0x1;
return N;
}
还原以后为:
function sig(L) {
for (var N = 0, g = encodeURIComponent(L), B = 0; B < g["length"]; B++)
N = ((((N << 7) - N) + 398) + g["charCodeAt"](B)),
N |= 0;
return N;
}
接着,我们再进入主加密函数 ua 中,结构如下,依旧是吃相极其难看的代码:
在代码中,我们看到了调用了解密函数,以及 uu 等函数调用,ua 分析后可得:
function ua(E, H) {
var W = ["3", "4", "2", "1", "0"]
, P = 0;
while (!![]) {
switch (W[P++]) {
case '0':
switch (M["length"] % 4) {
default:
case 0:
return M;
case 1:
return (M + "===");
case 2:
return (M + '==');
case 3:
return (M + '=');
}
case '1':
if (H)
return M;
continue;
case '2':
var M = uu(E, 6, function(L) {
return V["uGGDj"].charAt(L);
});
continue;
case '3':
var K = {};
K["uGGDj"] = "DGi0YA7BemWnQjCl4+bR3f8SKIF9tUz/xhr2oEOgPpac=61ZqwTudLkM5vHyNXsVJ";
var V = K;
continue;
case '4':
if (null === E)
return '';
continue;
}
break;
}
}
接着进入 uu 函数中,一如既往的吃相更难看的代码:
这就完了?不,还有:
前面几个函数我们都是手动替换的解密后的字符串,所以解密函数就没有扣,那看看这个函数,你再手动替换试试看,手估计要费掉,所以必须将解密函数 F3 拿下,然后它就可以调用 az 自主完成字符串的解密。当然解密函数就是本文难点之一。
前面我们说的算法分析可以粗略的分为俩种,其实就是解密函数的扣法可以分为俩种,我们进入解密函数 F3 中,观察其结构如下:
它接收两个参数 a 和 A,通过对 a 进行复杂的算术操作来计算一个新的索引值 n:
然后从数组 F 中取出该索引处的元素,并返回这个元素。数组 U 也在计算过程中被用来获取中间值。特定值的函数。
将 F3 分析后复现如下:
F3 = function(a, A) {
a = a - (-0x256c + -0x23 * -0x67 + -0x17f3 * -0x1);
var r = U[a];
var o = U[0x1b * 0xd3 + -0x2 * -0x1189 + 0xb77 * -0x5]
, n = a + o
, i = F[n];
r = i;
return r;
}
所以我们可以将 U 和 F 拿下,U 数组很好拿下,因为他就是偏移量之后的数据,但是 F 就不一样了,如果你在很早之前就将 F 拿下,那么可能会造成 F 缺失的问题,会导致解密函数某些值解密不成功,因为F是一直在增加的
所以我们如果想拿下完整的 F,就要在加密完成之前或者接近加密结束的时候进入 F3 中,将 F 全部控制台 copy 下来,这样整个解密函数将被彻底拿下:
在 uu 函数中同样存在 lw 和 F 。同时 F 函数中需要用到 UZ 函数,我们只需定义一个空的 UZ 函数即可:
function UZ(a) {
}
最后整个加密流程整合,即可出现正确的结果:
出现 OB 大数组所占的行数,整个算法也就几百行左右,比起之前的代码可谓是相当整洁。
算法还原 2
刚刚我们提到在扣解密函数的时候,可能会由于时机不正确,导致复制下来的 U 有所缺失,那么就会造成解密失败,如下:
所以我们在网页中找到该位置对应的 js 代码,将解密函数失败的 az(lw.V)
这种值全部找出来,然后我们放到自己定义的一个大对象 KKK 中,如下:
KKK = {
1719:"qMjri",
1093:"BEYhV",
267:"bdCZp",
1463:"HXHPX",
1016:"hzLDX",
1364:"aBzMZ",
507:"YFbJS",
1118:"pow",
1010:"Jwfww",
1483:"wZcNG",
1024:"UYeav",
927:"tLwiW",
230:"tQdqh",
1880:"dOdjo",
1614:"IlkJY",
410:"PvxjR",
1867:"tYdmC",
1670:"llrMA",
1577:"wEEdD",
1593:"uhTrx",
1860:"cbQeM",
1734:"rfswT",
594:"NUkoa",
877:"charAt",
}
然后我们用代理器给我们的解密函数挂上代理, 通过代理(Proxy)对象为 F3
对象添加一个自定义的函数调用处理器。它利用 KKK
这个映射对象,在某些条件下替代函数的返回值:
// 定义处理函数 kk_handler
const kk_handler = {
apply: function(target, thisArg, argumentsList) {
// 原始函数调用
let result = target(...argumentsList);
// 检查结果是否为 undefined 并且参数是否存在于 KKK
if (result === undefined && argumentsList in KKK) {
return KKK[argumentsList];
}
// 返回原始函数调用的结果
return result;
}
};
代码详细解释如下:
target
:被代理的目标函数,即F3
;thisArg
:如果原始函数是作为对象方法调用的,那么它的this
指向;argumentsList
:函数调用的参数列表;- 内部先调用目标函数并获取其返回值。如果返回值为
undefined
且参数列表存在于KKK
中,返回对应的映射值,否则返回原始返回值; - 通过
Proxy
包装F3
:javascript F3 = new Proxy(F3, kk_handler);
使用Proxy
构造函数创建新的代理对象F3
,将F3
和处理器 kk_handler关联起来。
最终就可以拦截解密函数,实现完整的解密,然后就同方法 1 调用加密函数即可完成 type_128 的加密,解决方法有很多种,遇到问题阅读相关 API 即可解决相关的问题,至此整个 type_128 参数就分析完毕了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。