某蜂窝w_tsfp参数分析

查看 135|回复 9
作者:kylin1020   
某蜂窝w_tsfp参数分析
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除.
目标地址
aHR0cHM6Ly93d3cubWFmZW5nd28uY24v
参数来源分析
新建新的无痕窗口,打开devtools并浏览目标地址,浏览器停在了一处动态加载的debugger代码中, 该段js代码可以分析出由Function.constructor动态构造得到, 所以直接在console中hook掉debugger的构造函数,使其失效:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (65.69 KB, 下载次数: 0)
下载附件
2024-4-21 18:27 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (123.64 KB, 下载次数: 1)
下载附件
2024-4-21 18:27 上传

Function.prototype.original_constructor= Function.prototype.constructor;
Function.prototype.constructor=function(){
    if (arguments && typeof arguments[0]==="string"){
        if (arguments[0]==="debugger")
            return;
    }
        return Function.prototype.original_constructor.apply(this, arguments);
};


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (63.26 KB, 下载次数: 1)
下载附件
2024-4-22 10:10 上传

点击继续:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (80.55 KB, 下载次数: 0)
下载附件
2024-4-22 10:11 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (198.9 KB, 下载次数: 1)
下载附件
2024-4-22 10:11 上传

查看网络请求可以知道,请求了两次目标地址,其中第一次请求只返回了一个空的x-waf-captcha-referer参数


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (52.78 KB, 下载次数: 1)
下载附件
2024-4-22 10:12 上传

两次目标地址请求间加载了一个probe.js文件,之后发起第二次请求, 此时携带了一个w_tsfp参数并且成功返回网页内容.


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (245.52 KB, 下载次数: 0)
下载附件
2024-4-22 10:13 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (185.42 KB, 下载次数: 1)
下载附件
2024-4-22 10:14 上传

由此可以基本断定w_tsfp参数在probe.js中生成.
解混淆
打开probe.js, 在js文件加载入口函数处打上断点, 然后对其进行分析.


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (100.89 KB, 下载次数: 1)
下载附件
2024-4-22 10:14 上传

可以知道该js文件进行了一些常规混淆, 例如大量使用了字符串函数(指调用了某个函数返回特定字符串的函数,该函数目的是为了隐藏字符串), 控制流平坦化等.


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (173.1 KB, 下载次数: 1)
下载附件
2024-4-22 10:14 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (165.83 KB, 下载次数: 0)
下载附件
2024-4-22 10:15 上传

1.1 还原字符串函数
通过分析可以发现,probe.js任意一个字符串函数都源自a1i根字符串函数并且参数都是透传的,没有加上任何偏移:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (74.2 KB, 下载次数: 0)
下载附件
2024-4-22 10:15 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (54.85 KB, 下载次数: 0)
下载附件
2024-4-22 10:16 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (144.21 KB, 下载次数: 1)
下载附件
2024-4-22 10:16 上传

因此可以先使用babel遍历所有字符串函数,得到所有参数变化列表,之后在console中计算得到所有字符串值;根据这些字符串值再将原js中的字符串函数调用替换为对应字符串。
首先需要找到字符串函数调用的特征, 通过观察结构可以知道任何一个字符串函数调用都带有两个参数,其中第一个参数是一个十六进制数字,第二个参数是一个字符串,并且函数名长度很短, 总是2(除了a1i函数)。


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (212.37 KB, 下载次数: 1)
下载附件
2024-4-22 10:17 上传

因此可以根据这些特征找到所有字符串函数调用:
const parser = require("@babel/parser");
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const fs = require("fs");
const input_file = "probe.js";
const output_file = "probe.output.js";
const js_content = fs.readFileSync(input_file, "utf-8");
const ast = parser.parse(js_content);
const stringFuncItems = [];
traverse(ast, {
    CallExpression: {
        // 找出所有字符串函数调用并记录参数
        enter: function (path) {
            const { node } = path;
            // 只有两个参数的函数调用
            if (!types.isIdentifier(node.callee) || node.arguments.length !== 2) {
                return;
            }
            // 函数名长度不大于3
            if (node.callee.name.length > 3) {
                return;
            }
            const arg0 = node.arguments[0];
            const arg1 = node.arguments[1];
            // 第一个参数是数字,第二个参数是字符串
            if (!types.isNumericLiteral(arg0) || !types.isStringLiteral(arg1)) {
                return;
            }
            stringFuncItems.push([arg0.value, arg1.value]);
        }
    }
});
console.log(JSON.stringify(stringFuncItems));


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (174.37 KB, 下载次数: 0)
下载附件
2024-4-22 10:17 上传

之后将结果拷贝到console中,断点停在js文件函数入口处或任意一处含有字符串函数的地方, 准备调用实际字符串函数得到每个参数对应的字符串:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (285.02 KB, 下载次数: 0)
下载附件
2024-4-22 10:18 上传

words = {}
for (let [i, v] of items) {
    words[`${i}-${v}`] = a1i(i, v);
}
得到所有字符串:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (59.2 KB, 下载次数: 0)
下载附件
2024-4-22 10:18 上传

将所有字符串拷贝到代码中,准备开始替换:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (104.16 KB, 下载次数: 1)
下载附件
2024-4-22 10:19 上传

const parser = require("@babel/parser");
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const fs = require("fs");
const input_file = "probe.js";
const output_file = "probe.output.js";
const js_content = fs.readFileSync(input_file, "utf-8");
const ast = parser.parse(js_content);
const words = {
  // 这里仅是示例,实际为上一步得到的所有字符串
// 这里仅是示例,实际为上一步得到的所有字符串
};
traverse(ast, {
    CallExpression: {
        // 找出所有字符串函数调用并记录参数
        enter: function (path) {
            const { node } = path;
            // 只有两个参数的函数调用
            if (!types.isIdentifier(node.callee) || node.arguments.length !== 2) {
                return;
            }
            if (node.callee.name.length > 3) {
                return;
            }
            const arg0 = node.arguments[0];
            const arg1 = node.arguments[1];
            // 第一个参数是数字,第二个参数是字符串
            if (!types.isNumericLiteral(arg0) || !types.isStringLiteral(arg1)) {
                return;
            }
            // 得到字符串
            const value = words[`${arg0.value}-${arg1.value}`];
            if (!value) {
                return;
            }
            //替换
            path.replaceWith(types.stringLiteral(value));
            // 打印替换的结果
            console.log(`${generator(node).code} 替换为字符串: "${value}"`);
        }
    }
});
const {code} = generator(ast);
fs.writeFileSync(output_file, code);


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (110.63 KB, 下载次数: 1)
下载附件
2024-4-21 18:34 上传

查看probe.output.js文件可以知道,字符串函数已经替换完成:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (131.83 KB, 下载次数: 0)
下载附件
2024-4-21 18:34 上传

1.2 MemberExpression常量传播
还原字符串函数之后, 可以看到有很多访问变量某个属性的情况且这些属性的值是不变的,可能是字符串或一些数字, 这些MemberExpression表达式可以替换为对应的字符串或数字,使得理解代码逻辑更简单些:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (125.37 KB, 下载次数: 1)
下载附件
2024-4-21 18:34 上传

// 尝试获取获取常量
function tryGetConstant(value, scope, binding, targetPropertyName) {
    // 字符串类型直接返回值
    if (types.isStringLiteral(value)) {
        return value.value;
    }
    // 数字类型直接返回值
    if (types.isNumericLiteral(value)) {
        return value.value;
    }
    // 变量的话查找其初始值
    if (types.isIdentifier(value)) {
        binding = scope.getBinding(value.name);
        if (!binding) {
            return;
        }
        return tryGetConstant(binding.path.node.init, binding.scope, binding);
    }
    // memberExpression的话需要其object属性的变量初始值,如果初始值中有对应property的属性,则可以直接拿其属性,否则
    // 找所有赋值语句,看赋值语句中有没有property属性
    if (types.isMemberExpression(value)) {
        if (!types.isIdentifier(value.object)) {
            return;
        }
        binding = scope.getBinding(value.object.name);
        if (!binding) {
            return;
        }
        const objectInit = binding.path.node.init;
        targetPropertyName = targetPropertyName || value.property.value || value.property.name;
        if (types.isObjectExpression(objectInit)) {
            const properties = objectInit.properties;
            for (const pro of properties) {
                const key = pro.key.name || pro.key.value;
                if (key === targetPropertyName) {
                    return tryGetConstant(pro.value, binding.scope, binding);
                }
            }
            // 从赋值语句中查找
            for (const ref of binding.referencePaths) {
                if (!types.isAssignmentExpression(ref.parentPath.parent)) {
                    continue;
                }
                const assign = ref.parentPath.parent;
                if (!types.isMemberExpression(assign.left)) {
                    continue;
                }
                if (assign.left.object !== value.object) {
                    continue;
                }
                const property = assign.left.property;
                if (types.isIdentifier(property) && property.name === targetPropertyName) {
                    return tryGetConstant(assign.right, binding.scope, binding);
                }
            }
        } else if (types.isIdentifier(objectInit)) {
            // object是变量的话,继续找
            binding = scope.getBinding(objectInit.name);
            if (!binding) {
                return;
            }
            return tryGetConstant(binding.path.node.init, binding.scope, binding, targetPropertyName);
        }
    }
}
// 常量传播
function MemberExpressionConstantPropagation(path) {
    const { node } = path;
    // 只需要类似于 A["xxx"]这样的member expression, 其中xxx是一个字符串, 例如 A["AaWYl"]
    if (!types.isIdentifier(node.object) || !types.isStringLiteral(node.property)) {
        return;
    }
  // 赋值语句排除,例如: A["xxx"] = "hello"; 这种语句不需要替换。
    if (types.isAssignmentExpression(path.parent) && node === path.parent.left) {
        return;
    }
    const binding = path.scope.getBinding(node.object.name);
    const value = tryGetConstant(node, path.scope, binding);
    if (!value) {
        return;
    }
    path.replaceWith(types.valueToNode(value));
    console.log(`常量传播: ${node.object.name}["${node.property.value}"] -> "${value}"`);
}
traverse(ast, {
    MemberExpression: {
        exit: [
            MemberExpressionConstantPropagation
        ]
    }
});


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (70.52 KB, 下载次数: 0)
下载附件
2024-4-21 18:35 上传

前后对比:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (121.74 KB, 下载次数: 0)
下载附件
2024-4-21 18:35 上传

1.3 消除一句话函数


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (64.6 KB, 下载次数: 0)
下载附件
2024-4-21 18:36 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (45.13 KB, 下载次数: 0)
下载附件
2024-4-21 18:36 上传

可以看到js代码中很多上图这种函数调用,其函数代码中只有一句return 语句是有用的(通常只有一行return语句),这些函数作用是隐藏各种运算表达式,例如:
B = {
                // 其他函数
      'eViYz': function (M, i) {
        return M(i);
      }
    };
// 函数调用
B["eViYz"](t, 0x0);
其中B["eViYz"](t, 0x0)应该简化为t(0x0)


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (257.74 KB, 下载次数: 0)
下载附件
2024-4-21 18:36 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (240.56 KB, 下载次数: 0)
下载附件
2024-4-21 18:36 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (30.77 KB, 下载次数: 0)
下载附件
2024-4-21 18:37 上传

通过观察其函数特征可以知道这种函数其callee一般是MemberExpression并且函数的body只有一行或只有两行,其中一行是完全没用的变量定义,据此可以筛选出这些一句话函数并进行替换:
function tryGetFunction(v, scope, targetName, binding) {
    if (types.isIdentifier(v)) {
        binding = scope.getBinding(v.name);
        if (!binding) {
            return ;
        }
        if (binding.kind === "param") {
            return;
        }
        return tryGetFunction(binding.path.node.init, binding.scope, targetName, binding);
    }
    if (types.isObjectExpression(v)) {
        const properties = v.properties;
        for (const pro of properties) {
            const key = pro.key.name || pro.key.value;
            if (key === targetName) {
                return [pro.value, scope];
            }
        }
        if (!binding) {
            return ;
        }
        // 从赋值语句中查找
        for (const ref of binding.referencePaths) {
            if (!types.isAssignmentExpression(ref.parentPath.parent)) {
                continue;
            }
            const assign = ref.parentPath.parent;
            if (!types.isMemberExpression(assign.left)) {
                continue;
            }
            if ((assign.left.property.name || assign.left.property.value) === targetName) {
                return [assign.right, ref.parentPath.parentPath.scope];
            }
        }
    }
}
function handleSimpleCallExpression(path) {
    const {node} = path;
    if (!types.isMemberExpression(node.callee) || !types.isIdentifier(node.callee.object)) {
        return;
    }
    const calleeBinding = path.scope.getBinding(node.callee.object.name);
    if (!calleeBinding) {
        return;
    }
    let func = null;
    let funcScope = null;
    const targetName = node.callee.property.name || node.callee.property.value;
    const info = tryGetFunction(calleeBinding.path.node.init, calleeBinding.scope, targetName, calleeBinding.scope.getBinding(node.callee.object.name));
    if (Array.isArray(info)) {
        func = info[0];
        funcScope = info[1];
    }
    if (func === null || funcScope === null) {
        return;
    }
    if (!types.isFunctionExpression(func)) {
        return;
    }
    let returnStatement = null;
    // 只有一个return语句
    if (func.body.body.length === 1 && types.isReturnStatement(func.body.body[0])) {
        returnStatement = func.body.body[0];
    } else if (func.body.body.length === 2) {
        // 可能有一个return语句, 一个var语句, 例如:
        // FhNDr: function (t, r) {
        //                       return ut["AaWYl"](t, r);
        //                       var n, e, o, i;
        //                     },
        // eljBu: function (t, r) {
        //                       return ut["UlYjQ"](t, r);
        //                       var n, e;
        //                     }
        if (types.isReturnStatement(func.body.body[0]) && types.isVariableDeclaration(func.body.body[1])) {
            returnStatement = func.body.body[0];
        } else if (types.isReturnStatement(func.body.body[1]) && types.isVariableDeclaration(func.body.body[0])) {
            returnStatement = func.body.body[1];
        }
    }
      if (!returnStatement) {
        return;
    }
    if (!types.isCallExpression(returnStatement.argument) && !types.isBinaryExpression(returnStatement.argument) && !types.isLogicalExpression(returnStatement.argument)) {
        return;
    }
}
traverse(ast, {
    CallExpression: {
        exit: [
            handleSimpleCallExpression
        ]
    }
});
开始遍历替换:
function handleSimpleCallExpression(path) {
  // 其余代码...
  // 寻找function的path
    let funcPath = null;
    funcScope.path.traverse({
        FunctionExpression: function (mpath) {
            const {node: mnode} = mpath;
            if (mnode === func) {
                funcPath = mpath;
                mpath.skip();
            }
        }
    });
    if (funcPath === null) {
        return;
    }
    // 得到传入参数节点和函数参数节点的对应关系, 准备将return表达式中的所有设计这些函数参数的替换为实际的传入参数.
    const paramToArgument = {};
    for (let i = 0; i  ${afterCode.slice(0, 30)}...`);
}


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (134.48 KB, 下载次数: 0)
下载附件
2024-4-21 18:37 上传

前后对比效果:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (102.28 KB, 下载次数: 0)
下载附件
2024-4-21 18:37 上传

然后再将BinaryExpression可以计算出常量的语句替换为常量,例如:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (39.67 KB, 下载次数: 0)
下载附件
2024-4-21 18:37 上传

1.4 BinaryExpression常量计算
针对一些可以直接得到结果的BinaryExpression表达式,使用babel path自带的evaluate计算得到值,在这里只应用字符串类型:
function BinaryExpressionConstantCalculation(path) {
    const { confident, value } = path.evaluate();
    if (!confident) {
        return;
    }
    // 只应用字符串类型
    if (typeof value !== "string") {
        return;
    }
    console.log(`常量计算: ${generator(path.node).code} -> ${value}`);
    path.replaceWith(types.valueToNode(value));
}


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (81.02 KB, 下载次数: 1)
下载附件
2024-4-21 18:38 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (60.01 KB, 下载次数: 0)
下载附件
2024-4-21 18:38 上传

此时阅读代码逻辑已经基本很清晰了,还有一些控制流混淆和一些无用代码没有使用babel 整理,但是都比较简单,基本不影响理解其代码逻辑,在此不展开了,感兴趣或想练手的朋友继续编写babel操作ast还原的代码。
参数分析
原本打算直接使用overwrite content替换为还原后的js代码进行断点分析,但是发现替换后会使得页面变为空白,不过这不影响,直接参照还原后的js找到对应的代码行进行断点分析即可。


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (41.99 KB, 下载次数: 1)
下载附件
2024-4-21 18:38 上传

代码中搜索关键词"w_tsfp"得到15处搜索结果,找出其中可能有生成逻辑的代码:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (145.22 KB, 下载次数: 1)
下载附件
2024-4-21 18:39 上传

经过查找发现其中一处有诸多参数生成逻辑,极有可能是w_tsfp参数的生成逻辑代码:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (131.99 KB, 下载次数: 1)
下载附件
2024-4-21 18:39 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (140.29 KB, 下载次数: 0)
下载附件
2024-4-21 18:40 上传

之后找到原js中对应的代码段打上断点,如果有经过这里,基本敲定是这里生成的w_tsfp:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (103.92 KB, 下载次数: 0)
下载附件
2024-4-21 18:40 上传

经过断点确认确实是这里,因此只需要分析这段代码的逻辑即可:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (161.79 KB, 下载次数: 0)
下载附件
2024-4-21 18:41 上传

2.1 function(G, C)函数
先来看function(G, C)这个函数


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (96.2 KB, 下载次数: 1)
下载附件
2024-4-21 18:41 上传

很容易看出来是一个rc4加密算法,G是key,C是value, 其中key是固定的:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (45.38 KB, 下载次数: 1)
下载附件
2024-4-21 18:41 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (29.28 KB, 下载次数: 1)
下载附件
2024-4-21 18:41 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (37.02 KB, 下载次数: 0)
下载附件
2024-4-21 18:42 上传

如果事先不知道rc4算法,导致不知道这段代码的做什么操作,也可以先将控制流手动还原下,把代码抠出来问下大模型即可:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (430.75 KB, 下载次数: 0)
下载附件
2024-4-21 18:42 上传

由于已知key,因此可以尝试将w_tsfp解密出来校验下是不是真的是rc4算法:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (155.08 KB, 下载次数: 1)
下载附件
2024-4-21 18:42 上传

2.2 basets/loadts/timestamp
这3个参数全部来自c = parseInt(new Date()["getTime"]()
2.3 fingerprint参数
核心代码, 其中c是2.2的timestamp
h = v(JSON["stringify"](window["pacus"]), c)
window["pacus"]可以在console中查看,有一大堆参数:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (106.94 KB, 下载次数: 1)
下载附件
2024-4-21 18:42 上传

手动在console中执行下,发现每次都生成不同的32位字符串:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (53.64 KB, 下载次数: 0)
下载附件
2024-4-21 18:43 上传

先分析下v函数, 代码从return开始往回看,追踪相关代码:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (189.41 KB, 下载次数: 1)
下载附件
2024-4-21 18:43 上传

可以明显看出是一个md5算法,其中四个幻数和MD5 每轮需要加的常数也都对上了,似乎没有魔改。继续往上看:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (112.88 KB, 下载次数: 1)
下载附件
2024-4-21 18:43 上传

实际参与运算的是R变量,R变量由Z + J得到, Z和J都是传入进来的参数。


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (41.62 KB, 下载次数: 1)
下载附件
2024-4-21 18:44 上传

不过如果Z和J如果是固定的值,则md5值应该是不会变得,但是console中每次运算都会返回一个新的32位,因此要找下这两个参数是不是有哪一步被变更了:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (82.06 KB, 下载次数: 0)
下载附件
2024-4-21 18:44 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (75.8 KB, 下载次数: 1)
下载附件
2024-4-21 18:44 上传

通过查看Z的引用可以看到当调用v函数时,如果传入的arguments没有第三个参数,则会给Z加上32位的随机字符串,因此在console中每次执行才会返回不同的32位字符串(只传入了两个参数),所以fingerprint实际上是随机的32位md5字符串。除此之后,还需要校验下md5有没有魔改,如果有魔改则需要找出魔改点并复现: 已知v函数传入三个参数可以不加32位随机字符串,因此可以在console中执行如下操作:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (37.97 KB, 下载次数: 0)
下载附件
2024-4-21 18:44 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (99.08 KB, 下载次数: 1)
下载附件
2024-4-21 18:44 上传

可以看到此时的结果是固定的,而且实际参与运算的只有前两个参数, 其md5值与"12"的md5值完全一致, 因此v函数md5算法没有魔改.
2.4 checksum参数
核心代码, 其中J是window["location"]["href"], h是fingerprint参数
A["checksum"] = v(J + h, new Date()["getTime"]())
由于传入的参数只有两个,因此checksum参数也是随机32位md5值
2.5 fingerprint和checksum二次加载分析
通过2.3和2.4的分析知道fingerprint和checksum都是随机的,这很令人疑惑. 经过继续查看网络请求可以知道,probe.js在第二次请求目标地址之后又重新加载了一次,此时的probe.js代码与第一次加载的probe.js稍有不同:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (104.42 KB, 下载次数: 0)
下载附件
2024-4-21 18:45 上传

第二次加载的probe.js混淆方式与第一次加载的大同小异,按照上述方法还原之后全文搜索checksum关键词:


1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (106.87 KB, 下载次数: 1)
下载附件
2024-4-21 18:45 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (191.07 KB, 下载次数: 1)
下载附件
2024-4-21 18:45 上传



1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (65.29 KB, 下载次数: 0)
下载附件
2024-4-21 18:45 上传

可以知道,其中:
L["checksum"] = z(P, L["timestamp"], I)
传入了三个参数其中P是uri, I是localStorage中存的fingerprint(为第一次生成的随机fingerprint值), v函数变为了z函数,z函数内部似乎也有一些不同,不过还是标准md5算法,只是多了一个参数N4(P和x是传入参数), 这个N4实际是fingerprint值.
感悟总结
probe.js混淆用到的都是很常见的方式,还原很容易,不过调试似乎有点麻烦。

下载次数, 下载附件

mufeng001   

蜂窝的找了两天了,谢谢楼主分享
Lty20000423   

很后悔没好好学前端
BonnieRan   

同学,底部的图片是不是开始那几张,忘记贴进去替换网络地址了?
kylin1020
OP
  

楼主这篇教程的ast解混淆逻辑清晰, 过程很详细,拿来练手label很合适
xixicoco   


Hmily 发表于 2024-4-22 08:59
同学,底部的图片是不是开始那几张,忘记贴进去替换网络地址了?

排版乱了,我今天再重新整理下
mufeng001   

楼主写的很详细,支持
kylin1020
OP
  

佬,那个_sn有办法算吗
mufeng001   


mufeng001 发表于 2024-4-22 23:04
佬,那个_sn有办法算吗

哪个sn,
mufeng001   


kylin1020 发表于 2024-4-22 23:05
哪个sn,

蜂窝的请求参数里有个_sn
您需要登录后才可以回帖 登录 | 立即注册

返回顶部