某初滑块逆向分析

查看 40|回复 4
作者:buluo533   
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责
某初滑块滑块,难度适中,食用地址(好像更新了代码,更简单了,我会给出复杂一点的版本):
aHR0cHM6Ly9hcHAueWljaHVhcHAuY24vaW5kZXgvaW52aXRlL2luZGV4Lw==
工具站:
"

https://babel.nodejs.cn/docs/babel-core
    本文主要是对滑块的思路进行分析讲解,同样分析简单ob混淆的ast还原以及滑块加密逻辑
                           


image.png (307.03 KB, 下载次数: 3)
下载附件
滑块情况
2025-8-11 16:44 上传

一、流程分析
  


image.png (20.77 KB, 下载次数: 3)
下载附件
debuggger
2025-8-11 16:45 上传

              开局一个debugger,掏出多年的hook脚本,直接注入过掉(百度一搜就有,或者自己写一个)
              
           let AAA = Function.prototype.constructor
Function.prototype.constructor = function (x) {
    console.log(x)
    if (X != "debugger") {
        return AAA(x)
    }
    ;
    return AAA.call(this, x)
}
           解决掉debugger我们就可以按照之前的经验来分析,我们需要进行滑动,来看需要处理的请求接口信息
        


image.png (95.67 KB, 下载次数: 3)
下载附件
滑动接口
2025-8-11 16:49 上传

         很明显,啥都没有,还反手送我一个debugger,还有一串混淆的代码,很明显,这个js代码是我们要处理的,但是我们没有看到校验的逻辑。我们就可以猜测了,因为之前还有假的滑块,先排除一下,会不会是cookie校验
      


image.png (29.68 KB, 下载次数: 2)
下载附件
cookie前
2025-8-11 16:56 上传

      


image.png (28.01 KB, 下载次数: 3)
下载附件
cookie信息
2025-8-11 16:52 上传

       我们对比一下滑动前和滑动后的参数变化,发现有一个异常的参数
[color=]guard_ret
,简单注入hook一下
      
(function () {
    'use strict';
    var cookie_cache = document.cookie;
    Object.defineProperty(document, 'cookie', {
        get: function () {
            return cookie_cache;
        },
        set: function (val) {
            console.log('Setting cookie', val);
            if (val.indexOf('guard_ret') != -1) {
                debugger;
            }
            var cookie = val.split(";")[0];
            var ncookie = cookie.split("=");
            var flag = false;
            var cache = cookie_cache.split("; ");
            cache = cache.map(function (a) {
                if (a.split("=")[0] === ncookie[0]) {
                    flag = true;
                    return cookie;
                }
                return a;
            })
            cookie_cache = cache.join("; ");
            if (!flag) {
                cookie_cache += cookie + "; ";
            }
            return cookie_cache;
        }
    });
})();
            


image.png (80.26 KB, 下载次数: 4)
下载附件
cookie堆栈
2025-8-11 17:00 上传

           这里就是成功断住了,我们往上看看堆栈,分析产生
      


image.png (108.16 KB, 下载次数: 3)
下载附件
拼接cookie
2025-8-11 17:01 上传

           我们发现这个js文件就是我们返回的那一个js文件,混淆很难看,直接ast一把梭哈
二、ast还原混淆代码逻辑分析
   拿到混淆代码先总览一边,看看还原思路。
      


image.png (662.4 KB, 下载次数: 2)
下载附件
混淆逻辑
2025-8-11 17:49 上传

                    很明显一个编码混淆。
                     


image.png (319.25 KB, 下载次数: 1)
下载附件
赋值调用
2025-8-11 18:12 上传

                    然后发现有很多_0xb0e1函数的一个赋值调用,符合ob混淆的特征,是一个解密函数,然后就是找准大数组和数组移位函数
               


image.png (576.97 KB, 下载次数: 4)
下载附件
大数组逻辑
2025-8-11 18:13 上传

              
                找准了大数组和解密函数,接下来就是开始写ast代码,先搭一个模板,理一下思路
                (1)将三要素,大数组,数组移位函数,解密函数,存进内存
                (2)编码混淆还原
                (3)解密函数调用还原
                (4)ob混淆特征大对象还原
                 一步一步来,找到对应的三要素位置,直接提取出来,存进内存。
               


image.png (519.9 KB, 下载次数: 4)
下载附件
语法树
2025-8-11 18:14 上传

         
let parse = require("@babel/parser").parse;
let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
let fs = require("fs");
let types = require("@babel/types");
let jsc_code = fs.readFileSync("./混淆代码.js", encoding = "utf-8")
let ast = parse(jsc_code)
let init_ast = parse(jsc_code)
traverse(init_ast, {
    Program: function (path) {
        path.node.body = path.node.body.slice(0, 5)
        path.stop()
    }
})
let test_code = generator(init_ast, {minified: true}).code
eval(test_code)
           接下来做一个编码的还原,掏出俩件套
        traverse(ast, {
    StringLiteral({node}) {
        if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
            node.extra = undefined;
        }
    }
});
traverse(ast, {
    NumericLiteral({node}) {
        if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
            node.extra = undefined;
        }
    }
});
        完成这一步之后我们可以调用解密函数测试一下,有没有存进内存。
         


image.png (170.1 KB, 下载次数: 4)
下载附件
解密函数调用
2025-8-11 18:17 上传

         因为我们有很多赋值调用的过程,我们把赋值的函数名给收集起来,然后统一替换成解密函数_0xb0e1,执行替换结果,代码如下。
      
      
     let decode_name = []
traverse(ast, {
    VariableDeclarator(path) {
        const {id, init} = path.node;
        if (init && (init.type === 'Identifier' || init.type === 'FunctionDeclaration') ) {
            let name = id.name
            decode_name.push(name)
            path.remove()
        }
    }
});
traverse(ast, {
    CallExpression: {
        exit: function (path) {
            let {node} = path
            let func_name = node.callee.name
            if (!decode_name.includes(func_name)) {
                return
            }
            if (!node.arguments || node.arguments.length !== 1) {
                return;
            }
            let eval_path = path.toString().replace(func_name, "_0xb0e1")
            let result = eval(eval_path)
            path.replaceWith(types.valueToNode(result))
        }
    }
});
          这样就可以把所有解密函数调用的部分还原,可以看一下结果,还是非常的清晰
         


image.png (648.07 KB, 下载次数: 2)
下载附件
解密函数还原结果
2025-8-11 18:20 上传

            接下来就是处理最难的部分,ob混淆的大数组处理。我的大致思路如下
           1、收集所有的符合条件的对象
           2、分成两部分处理,一部分是函数和运算符的还原,一部分是字符串的还原
           3、通过函数调用和作用域去还原,比较特定,不通用
             第一部分是收集所有的符合条件的对象
         
         
            let obj_name = []
traverse(ast, {
    VariableDeclarator: function (path) {
        let {parentPath, node, scope} = path
        if (!parentPath.isVariableDeclaration()) {
            return;
        }
        let {id, init} = node
        if (!types.isObjectExpression(init)) {
            return;
        }
        if (!id.name && id.name === undefined) return;
        let name = id.name
        obj_name.push(name)
    }
})
           在最开始写代码建议使用某一个对象去测试,然后去处理函数和运算符的还原
       traverse(ast, {
    CallExpression: {
        exit(path) {
            if (path.get("callee.object").node && obj_name.includes(path.get("callee.object").node.name)) {
                let property = path.get("callee.property").node.value
                let argument_path_array = path.get("arguments")
                let name = path.get("callee.object").node.name
                path.scope.getBinding(name).scope.path.traverse({
                    ObjectExpression: function (path_Expression) {
                        let {node, parentPath} = path_Expression
                        if (!parentPath.isVariableDeclarator()) {
                            return
                        }
                        if (parentPath.node.id.name !== name) {
                            return;
                        }
                        let properties = node.properties
                        for (i of properties) {
                            if (i.key.value && i.key.value === property) {
                                let value = i.value
                                switch (value.type) {
                                    case 'FunctionExpression':
                                        let return_path = value.body.body[0].argument
                                        if (value.body.body[0].argument.type === 'BinaryExpression') {
                                            let operator = return_path.operator
                                            let left = argument_path_array[0].node
                                            let right = argument_path_array[1].node
                                            path.replaceInline(types.binaryExpression(operator, left, right))
                                        } else if (value.body.body[0].argument.type === 'CallExpression') {
                                            let func = argument_path_array[0].node
                                            if (!types.isCallExpression(path) && !path.arguments) {
                                                return;
                                            }
                                            let func_arguments = path.node.arguments.slice(1)
                                            path.replaceInline(types.callExpression(func, func_arguments))
                                        }
                                        break
                                }
                            }
                        }
                        path_Expression.scope.crawl();
                    }
                });
            }
        }
    }
})
           第三步是字符串对应的还原
           traverse(ast, {
    MemberExpression:
        function (path) {
            if (obj_name.includes(path.get("object.name").node) && path.get('property').isStringLiteral()) {
                let _string = path.get('property').node.value
                let name = path.get("object.name").node
                path.scope.getBinding(name).scope.path.traverse({
                    ObjectExpression: function (path_Expression) {
                        let {node, parentPath} = path_Expression
                        if (!parentPath.isVariableDeclarator()) {
                            return
                        }
                        if (parentPath.node.id.name !== name) {
                            return;
                        }
                        let properties = node.properties
                        for (i of properties) {
                            let key_value = i.key.value
                            let value = i.value
                            if (key_value === _string) {
                                switch (value.type) {
                                    case 'StringLiteral':
                                        let real_value = value.value
                                        path.replaceWith(types.valueToNode(real_value))
                                        break
                                }
                            }
                        }
                        path_Expression.scope.crawl();
                    }
                });
            }
        }
})
         
            总体还原思路是从函数调用入手,去匹配他的值和对象里的键这样去实现还原。现在更新的加密更加简单(需要处理定时器的逻辑),没有了对象的还原,简单写了一个,仅供参考
       traverse(ast, {
    CallExpression: {
        exit: function (path) {
            let {node} = path
            let func_name = node.callee.name
            if (func_name !== "_0x4de0") {
                return;
            }
            let result = eval(path.toString())
            path.replaceWith(types.valueToNode(result))
        }
    }
});
         剩下的就是处理垃圾代码的删除,就可以完成这个的还原。
三、加密逻辑分析
           


image.png (713.85 KB, 下载次数: 4)
下载附件
加密逻辑
2025-8-11 18:29 上传

             还原后的代码一目了然,可以看到清晰的逻辑,找一下 _0x2b7a59的一个生成逻辑
           const _0x167271 = _0x39eb8c["getBoundingClientRect"](),
        _0x4b9064 = _0x255abf['getBoundingClientRect'](),
        _0x3f4be5 = _0x167271["left"] - _0x4b9064['left'],
        _0x32d3e9 = _0x167271["top"] - _0x4b9064["top"];
      x = Math["round"](_0x3f4be5);
      y = Math["round"](_0x32d3e9);
      var _0x2c1619 = _0x5ce987("guard"),
        _0x564781 = _0x2c1619["substr"](0, 8),
        _0x1499fd = _0x5cc496(x["toString"]() + 'x' + y["toString"](), _0x564781),
        _0x2b7a59 = _0x40fb99(_0x1499fd);
      document["cookie"] = "guardret=" + _0x2b7a59;
        
function _0x5ce987(_0x17f493) {
      const _0x5b6ec3 = document['cookie']["split"](';');
      let _0x119d4b = null;
      for (let _0x4de2de of _0x5b6ec3) {
          const [_0x1cc261, _0xeec935] = _0x4de2de["trim"]()['split']('=');
          if (_0x1cc261 === _0x17f493) {
            _0x119d4b = _0xeec935
          }
        }
      return _0x119d4b;
    }
function _0x5cc496(_0x49fb4d, _0x45898e) {
    let _0x2f7fee = '';
    var _0x45898e = _0x45898e + "zVbhsiCROg";
    for (let _0x351fbd = 0; _0x351fbd
function _0x40fb99(_0x20cad9) {
        return btoa(_0x20cad9)
      }
     比较简单的一个逻辑,用python代码实现一下
def encrypt(data, key):
    result = []
    key += "zVbhsiCROg"
    key_length = len(key)
    for i in range(len(data)):   
        data_char_code = ord(data)
        key_char_code = ord(key[i % key_length])
        xor_result = data_char_code ^ key_char_code
        result.append(chr(xor_result))
    return ''.join(result)
   
四、总结
         这个站整体偏简单,没想到他竟然更新的更简单了,利用ast可以更快处理,还是用老版本做了一个示例,大佬们可以试试搞一下新版本,一把梭哈,cookie加密原理没有变{:1_932:} 给一个老版本混淆可以拿来练手

混淆代码.zip
(12.46 KB, 下载次数: 1)
2025-8-11 18:39 上传
点击文件名下载附件
下载积分: 吾爱币 -1 CB

   

下载次数, 函数

zhangyun_1   

不明觉厉,楼主编写辛苦了,更新后更简单的原因会不会是因为复杂影响了效率。
dandan946   

来了,大佬,多发,爱看
retorlin   

感谢大佬分享、谢谢
monica11   

感谢大佬分享
您需要登录后才可以回帖 登录 | 立即注册

返回顶部