• jsvmp-某乎 x-zes-96 算法还原


    前言

    ​ 仅作学习交流,非商业用途,如侵删。

    ​ 记一次算法还原,手撕vmp的过程。

    网站链接

    aHR0cHM6Ly93d3cuemhpaHUuY29tL3NlYXJjaD9xPXB5dGhvbiZ0eXBlPWNvbnRlbnQ=

    1. 找到关键入口

    ​ 我们选择直接使用粗暴的搜索方法,要解密的 x-zes-96 在这个url header 里面。

    // 隐藏域名 防止帖子暴毙
    https://www.xxx.com/api/v4/search_v3?gk_version=gz-gaokao&t=general&q=python&correction=1&offset=0&limit=20&filter_fields=&lc_idx=0&show_all_topics=0&search_source=Normal
    
    • 1
    • 2

    ​ 直接搜 x-zse-96 找到入口,只有两个位置统统打上断点

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVMD3owe-1662087958898)(./_pic/image-20220902050912358.png)]

    2. 分析流程

    ​ 多打印两次看看 s 和 f()(s) 的值是不是值是否是不是动态的,防止走入误区。

    ​ 经过验证发现这是个标准md5可以使用标准库。那我们就没必要在这个上面浪费时间,我们主要分析x-zst-96所以跳过这个。

    在这里插入图片描述

    ​ 然后再来多打印两次看看 (0,F®.encrypt)(f()(s)) 的值是不是值是否是不是动态的,防止走入误区。

    ​ 是个动态的,我们先给它留意下,继续往下走。

    ​ F11 跟进去 F函数 就是这个 F®.encrypt)(f()(s) 所以直接 分析这个 encrypt: u.a

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEHe1HA2-1662087958901)(./_pic/image-20220902051935525.png)]

    ​ 打印 u.a 直接点进去 看到这个绿色的光 这就是导出方法。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FIWYR5lP-1662087958902)(./_pic/image-20220902052446287.png)]
    打上断点执行到这里,那这个就是生成加密的方法了,我们再重复运行两次试试验证猜想。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQxrewAH-1662087958902)(./_pic/image-20220902052821114.png)]
    ​ 我们继续跟试试看,这里new了一个 I 对象,我们进入这个I方法。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V7OzsbBo-1662087958903)(./_pic/image-20220902053211353.png)]
    ​ 这里会循环几万甚至几十万次,加入了大量的无用代码和逻辑。并且如果不熟悉ast的coder不要轻易尝试。

    ​ 接下来通过Debugger断点调试jsvmp就不太可行了,套用下渔哥的解释,本文末尾有参考链接。

    ​ jsvmp就是将js源代码首先编译为字节码,得到的这种字节码就变成只有操作码(opcode)和操作数(Operands),这是其中一个前端代码的保护技术。

    ​ 整体架构流程是服务器端通过对JavaScript代码词法分析 -> 语法分析 -> 语法树->生成AST->生成私有指令->生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,就开始一边解释,一边执行。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEYiSsxt-1662087958903)(./_pic/image-20220902053355451.png)]
    ​ 接下来就不继续通过Debugger调试了,既然可以在控制台通过调用 F®.encrypt)(f()(s) 来实现调用,这是一个webpack打包的项目那么我们找到它的加载器就可以调用,或者在这个模块内部没有如果通过加载器引用其它模块的话,把它直接变成全局的也是可以运行的。

    ​ 本次为了考虑到文章主要是算法还原,直接改成全局调用,有兴趣的可以通过加载器方法,不在此增加工作量了。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ey3JoRVo-1662087958904)(./_pic/image-20220902055256425.png)]
    ​ 新的某乎增加环境检测,要打开知乎界面调用,不然会走错误流程。请求结果是403。
    ​ 掐头去尾留中间,删除这个匿名函数头部和尾部还有导出函数 exports,直接放到浏览器调用。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-81QBtjbR-1662087958904)(./_pic/image-20220902060253402.png)]
    ​ 新手按箭头数字提示操作,老手直接跳过。
    ​ 出意外的报错了,我们点击报错点跟过去注释它。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6EqAPWts-1662087958905)(./_pic/image-20220902060844001.png)]
    ​ 运行成功,至此分析流程结束开始进入算法还原。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBZ1JQDK-1662087958905)(./_pic/image-20220902061008400.png)]

    3. 算法还原

    ​ 前面分析过得,l.prototype.O = function (A, C, s) { 在这个方法的大循环内生成算法,所以代码逻辑也肯定在这个里面,我们给它插桩打印日志看看。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-letQkpIJ-1662087958906)(./_pic/image-20220902063630797.png)]
    ​ 除了知道一个 S 看起来像时间戳之外,密密麻麻的基本没有有用信息。不过通过展开的case分支发现,频繁的出现了一个变量 this.C 和 this.C[this.c] 我们来打印它试试看。记得先清空之前的日志,在调用加密参数的前面加上 console.clear() 过滤掉生成算法之前走的其它初始化或无用逻辑。

    ​ 插桩如下代码到循环处

    console.log(`索引--> case ${this.T}: this.C-->${this.C}, this.C[this.c]-->${this.C[this.c]}`)
    
    • 1

    ​ 好消息我们看到了,this.C[this.c] 返回的结果和最后生成结果一致,证明我们的猜想是对的。全部复制到本地文本中分析。

    ​ 坏消息是密密麻麻的信息28000多行日志,我们从后往前来慢慢
    在这里插入图片描述​ 可以看到加密在这里不一样。

    Mh0gk2Mj76=d0Ccqp0v/F+0QSfx+V0SPzin6ig1fmzpTi8uQiDMrAQ81oT4FclX q
    Mh0gk2Mj76=d0Ccqp0v/F+0QSfx+V0SPzin6ig1fmzpTi8uQiDMrAQ81oT4FclXq
    
    • 1
    • 2

    ​ 明显有个算法拼接的过程 ,很难让人想到不是 + 拼接的,为了印证在网上继续找不过是拿着下面的去搜

    Mh0gk2Mj76=d0Ccqp0v/F+0QSfx+V0SPzin6ig1fmzpTi8uQiDMrAQ81oT4FclX
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWahyJVs-1662087958907)(./_pic/image-20220902065320125.png)]

    ​ 排除常规的标准算法,我们事先通过老版本也得知这是一个自定义算法,不用去猜想是不是aes des 这种。

    ​ 和猜想的一样的就是通过慢慢拼接起来的,这种一般按照经验都是循环生成的。我们可以看到字符串长度是48,开发程序员不可能一个一个去拼接,肯定是遵循某一种规律或者特定条件循环的,所以我们不忙着扣代码,直接往上找。同时先记住这个case168 两次生成最后两位都是同一个case 分支 改变的。我们继续网上找,找到加密拼接的头部

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqTvBNXB-1662087958907)(./_pic/image-20220902065625016.png)]

    ​ 我们直接搜算法开头的第一位M全匹配,可以看到case 168 和 465 都有可能是重点位置,为什么不是case 352呢?

    ​ 还记得开始的插桩

    console.log(`索引--> case ${this.T}: this.C-->${this.C}, this.C[this.c]-->${this.C[this.c]}`)
    
    • 1

    ​ 它是在 switch (this.T) 上面, this.C 的值 是上一次给 有效代码运算过后给 this.C 的有效赋值。我们去 case 168 下插桩试试看

                    case 168:
                        console.log(`case ${this.T}: this.C[${this.c}] = ${this.C[this.I]} + ${this.C[this.F]} --> ${this.C[this.I] + this.C[this.F]}`)
                        this.C[this.c] = this.C[this.I] + this.C[this.F];
                        this.T = 2 * this.T + 16;
                        break;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NL87iDCZ-1662087958908)(./_pic/image-20220902070604165.png)]

    ​ 和猜想的一模一样,是通过拼接完成的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrddOXvx-1662087958908)(_pic/image-20220902072717982.png)]

    ​ 加密第一位也是如此 我们再去看看465是否也是如此。

                    case 465:
                        this.C[3] = this.C[this.W][Q](G[+[]]);
                        console.log(`case ${this.T}: this.C[3] = ${this.C[this.W]}.${Q}(${G[+[]]}) --> ${this.C[3]}`)
                        this.T -= 13 * G.length + 100;
                        break;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9UT09qCB-1662087958908)(_pic/image-20220902072955253.png)]

    ​ case 465 果然也是如此, 那我们依法炮制。按照上面这种方法搜索然后在生成出插桩

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjfXR4el-1662087958909)(_pic/image-20220902073538632.png)]

    ​ 重复步骤不在截图。依次顺序是

    //  加密值字符串拼接 BsdBVlB5vVIR=TbdMQh2skhsHK4scwNOWSamRia2YOaH+LCWTlURM4I/XKjQsHM + c
    168,
    //  生成拼接的加密值 6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE.charAt(11) --> c
    465,
    //  生成 11 & 63 --> 11  ========> 63 定值
    78,
    //  生成 2922411 >>> 18 --> 11  ========> 0 6 12 18 定值
    57,
    //  生成 38827 | 2883584 --> 2922411
    50,
    //  生成参数2 44 << 16 --> 2883584   ========> 44 是数组的值 16 定值
    64,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ​ 完成了以上步骤之后就可以从下往上拿到一个很长的数组,但是中间无用代码太多。

    ​ 我们从上往下看大概可以猜到这个数组是通过运算得到的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pMmqScN3-1662087958909)(_pic/image-20220902080043559.png)]

    ​ 但是依然无法清晰的看到整个加密过程,因为还有很多插桩没有完整。我们给它整个补完,又是一个漫长的过程,注释掉无用的逻辑比如 352 368 这种。拿到一份完全逻辑清晰可见的日志。

    解压之后 打开 日志6.log

    ​ 可以看到完整的环境检测,运算逻辑。

    ​ case 368那里做了一个校验,只在debugger住并且时间超过500毫秒才会生效,估计是走向错误分支,我们只需要固定住就可以。但是我们都是分析日志所以并没有触发这个条件。

           case 368:
                        // this.T -= 500 < S - this.a ? 24 : 8;
                        this.T -= 8;
                        break;
    
    • 1
    • 2
    • 3
    • 4

    ​ 接着我们研究研究加密为什么会变得问题。通过两次运行比对日志发现了,算法为何会变得原因。

    ​ 生成了一个随机数,然后取整把它unshift到了数组的开头参与了运算。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ahuXJJGu-1662087958911)(_pic/image-20220902090633326.png)]

    ​ 有意思的是时间戳并没有参与运算,为了方便调试我们还是写死。

    ​ 如果想用我这份环境调试可以固定 md5 和时间戳 随机数即可用我这份代码日志分析比对,每一行都会相同。

    md5 f1fa96c714c6752f28b162fda60ded03
    Date.now 1661986251253
    Math.random 0.08636862211354912
    
    • 1
    • 2
    • 3

    ​ 接下来进行最后的算法还原,用 日志6.log 这个日志记录来分析还原算法。

    1 - 142 之前都是环境检测 我们直接跳过。

    143 - 271 取md5字符串长度 循环每一位 charCodeAt(i) 压入数组

    // 定义一个空数组
    var md5_charCodeAt_arr = []
    // md5 转 charCodeAt 存放到数组
    for (let i = 0; i < md5_str.length; i++) {
        md5_charCodeAt_arr.push(md5_str.charCodeAt(i))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    272 - 281 继续压入 通过两次对比发现时间戳不影响最后的计算结果

    // 向数组开头添加一个新的元素 0
    md5_charCodeAt_arr.unshift(0)
    // 向数组开头添加一个新的元素 17,也就是上面计算出来的 可随机可固定
    md5_charCodeAt_arr.unshift(17)
    
    • 1
    • 2
    • 3
    • 4

    283 - 310 通过两次对比发现 固定值循环14次.push(14)

    // 往数组中放14个14,此时数组的长度为48
    for (let i = 0; i < 14; i++) {
    	md5_charCodeAt_arr.push(14)
    }
    
    • 1
    • 2
    • 3
    • 4

    311 - 393
    改变md5 通过两次对比发现 分析发现 this.C[1] 是一个固定数组 48,53,57,48,53,51,102,55,100,49,53,101,48,49,100,55
    这里取md5数组的 slice(0, 16) 然后用 this.C[0] 和 this.C[1] 数组每一位进行运算之后 ^ 42

    // 之后通过slice取0-16位
    var md5_charCodeAt_arr1 = md5_charCodeAt_arr.slice(0, 16)
    // md5_charCodeAt_arr1 -> 10, 0, 102, 49, 102, 97, 57, 54, 99, 55, 49, 52, 99, 54, 55, 53
    
    // 固定值
    var charCodeAt_arr_1 = [48, 53, 57, 48, 53, 51, 102, 55, 100, 49, 53, 101, 48, 49, 100, 55];
    
    var new_md5_charCodeAt_arr = [];
    for (var key in md5_charCodeAt_arr1) {
        new_md5_charCodeAt_arr.push(md5_charCodeAt_arr1[key] ^ charCodeAt_arr_1[key] ^ 42);
    }
    // 因为是16位的数组,每个值都需要计算,所以相当于是分了16组,每组计算的结果都放入了一个新的数组中
    // new_md5_charCodeAt_arr -> 16, 31, 117, 43, 121, 120, 117, 43, 45, 44, 46, 123, 121, 45, 121, 40
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    394 - 401

    // 我们给上面的结果定义一个变量不然下面容易看的人头晕 
    // 283 - 310 md5_charCodeAt_arr 
    // 311 - 393 new_md5_charCodeAt_arr
    // 调用 __g.r 方法
    var __g_r_res = __g.r(new_md5_charCodeAt_arr)
    var md5_charCodeAt_arr2 = md5_charCodeAt_arr.slice(16, 48)
    var __g_x_res = __g.x(md5_charCodeAt_arr2, __g_r_res);
    // 拿到结果 下面会用这个长度48的数组进行大量的运算 这里不就是我们需要的那个数组
    var in_calculation = __g_r_res.concat(__g_x_res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    404 - 407

    // 通过两次对比发现 这是一个固定值可以写死 它是通过在变量中取出的值 拼接起来的,发现是固定值之后我没继续跟参数了
    case 168: this.C[3] = 6fpLR + qJO8M/c3j --> 6fpLRqJO8M/c3j
    case 168: this.C[3] = 6fpLRqJO8M/c3j + nYxFkUV --> 6fpLRqJO8M/c3jnYxFkUV
    case 168: this.C[3] = 6fpLRqJO8M/c3jnYxFkUV + C4ZIG12SiH=5v0mXDazWB --> 6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWB
    case 168: this.C[3] = 6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWB + Tsuw7QetbKdoPyAl+hN9rgE --> 6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ​ 最后一步,还记得刚开始猜的循环吗,这里开始了。

    ​ 开始咯,下面就是又臭又长的分析流程,依然是通过两次对比流程发现其中的规律

    ​ 第一组 第二组

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bDe2F1NH-1662087958911)(_pic/image-20220902044028834.png)]

    ​ 第三组 第四组

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ezHohA7W-1662087958912)(_pic/image-20220902044127885.png)]

    ​ 大量的对比发现以下规律这是一个循环

    ​ 每次生成4个字符串,前面拿到的那个大数组会参与运算,从数组 pop() 一个出来

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eO2rW7uQ-1662087958912)(_pic/image-20220902100155719.png)]

    ​ 通过这张整理过后的对比可以得出循环,已第一组数据为例

    var i = 0; var pop = 211;
    var c_3_1 = i % 4;
    var c_3_2 = 8 * i;
    var c_3_3 = 58 >>> c_3_2;
    var c_3_4 = c_3_3 & 255;
    var c_3_5 = pop ^ c_3_4
    
    var a = c_3_5 // 这个值要参与运算 先保留起来
    console.log("c_3_5", c_3_5);
    
    i = 1; var pop = 74;
    c_3_1 = i % 4;
    c_3_2 = 8 * i;
    c_3_3 = 58 >>> c_3_2;
    c_3_4 = c_3_3 & 255;
    c_3_5 = pop ^ c_3_4
    var b1 = c_3_5 << 8 // 74 << 8 -- > 18944
    
    console.log("c_3_5", c_3_5);
    console.log("b1", b1);
    var a1 = a | b1 // 233 | 18944 -- > 19177
    console.log("a1", a1);
    
    
    i = 2; var pop = 167;
    c_3_1 = i % 4;
    c_3_2 = 8 * i;
    c_3_3 = 58 >>> c_3_2;
    c_3_4 = c_3_3 & 255;
    c_3_5 = pop ^ c_3_4
    console.log("c_3_5", c_3_5);
    
    
    var c = c_3_5 << 16 // 10944512
    console.log("c", c);
    var d = a1 | c; // 10963689
    console.log("d", d);
    
    console.log(encode(d));
    // 10963689 = > BsdB
    function encode(param) {
        var salt = '6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE'
        let ret = ''
        // 这里对应点在 case 57
        for (x of [0, 6, 12, 18]) {
            let a = param >>> x
            let b = a & 63
            let c = salt.charAt(b)
            ret = ret + c
        }
        // console.log(ret)
        return ret
    }
    
    
    • 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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RU8Qsijk-1662087958912)(_pic/image-20220902102930503.png)]

    然后我们把它封装成一个函数 请求一下试试看
    在这里插入图片描述
    ​ 完全成功 和日志6.log的一模一样

    x_zse_96 2.0_BsdBVlB5vVIR=TbdMQh2skhsHK4scwNOWSamRia2YOaH+LCWTlURM4I/XKjQsHMc
    
    • 1

    ​ 最后闲谈:讲道理还原纯算是比较浪费时间成本的,像这样已经做过一次知道流程的情况下,整理笔记资料和截图,完全搞清楚每个分支干什么了,写完这篇文章差不多花了10个小时。这种方案对抗vmp显然是不太划算的,相比较而言补环境应该是一个不错的选择。可能大佬都喜欢手撕vmp的快感吧,不过作为学习技术本身就是为了吃懂吃透,为了更好的对抗vmp,实际生产业务肯定还是以最快完成为主。一个练手demo还原纯算的话分析加上还原还是参考其它文章都需要花费1-2天,还是自己还是太菜了。

    最后推荐一下蔡老板的星球,好用不贵。

    参考文章:

    蔡老板 vip群 : ) 佬 的 demo

    渔滒 - 【JS逆向系列】某乎x96参数与jsvmp初体验

    时光依旧不在 - js逆向JSVMP篇新版某乎_x-zes-96算法还原

  • 相关阅读:
    网络编程-流
    羽毛球初学者入门篇(仅个人经验)
    【日常训练】535. TinyURL 的加密与解密
    Linux安装HBase
    无人机控制的研究现状及关键技术
    极速Go语言入门(超全超详细)-进阶篇
    怎样避免执行走样
    docker swarm快速部署redis分布式集群
    CPU设计——Triumphcore——V2版本
    学内核之十六:linux内存管理结构大蓝图
  • 原文地址:https://blog.csdn.net/zhoumi_/article/details/126659351