【JavaScript 逆向】猿人学 web 第十八题:jsvmp,洞察先机

案例目标

网址:第十八题 jsvmp 洞察先机 - 猿人学

本题目标:抓取 5 页数字,计算加和并提交结果

常规 JavaScript 逆向思路

一般情况下,JavaScript 逆向分为三步:

  • 寻找入口:逆向在大部分情况下就是找一些加密参数到底是怎么来的,关键逻辑可能写在某个关键的方法或者隐藏在某个关键的变量里,一个网站可能加载了很多 JavaScript 文件,如何从这么多的 JavaScript 文件的代码行中找到关键的位置,很重要;

  • 调试分析:找到入口后,我们定位到某个参数可能是在某个方法中执行的了,那么里面的逻辑是怎么样的,调用了多少加密算法,经过了多少赋值变换,需要把整体思路整理清楚,以便于断点或反混淆工具等进行调试分析;

  • 模拟执行:经过调试分析后,差不多弄清了逻辑,就需要对加密过程进行逻辑复现,以拿到最后我们想要的数据

接下来开始正式进行案例分析:

寻找入口

F12 打开开发者人员工具,刷新网页进行抓包,在 Network 中可以看到数据接口为 18data?page=1,响应预览中可以看到当前页面各数字数据:

但是并没有看到类似加密的字段,通过对比,点击第二页的后,数据接口变成了 18data?page=2&t=XXX&v=XXX,请求 url 中多出了两个参数 t 和 v:

t 看起来像是时间戳,不过时间戳一般为 13 位,这里只有 10 位,v 是一串加密内容,从 Initiator 处跟栈到 getdata 中,逆向分析以下加密逻辑:

调试分析

会跳转到 18 文件的第 764 行,点击左下角的 { } 对其进行格式化操作,在第 1453 行 send 处打下断点调试分析,在第 1423 行创建了一个 XMLHttpRequest 对象,XMLHttpRequest 用于在后台与服务器交换数据,后面通过 xml.open( ) 建立链接,一个 HTTP 请求,xml.send( ) 发送请求,连接对象为当前接口的 url:

在第 1427 行定义了个 data 参数,在第 1429 行打断点调试,data 传入了当前页面所有数字:

  • responseText:将响应信息作为字符串返回

  • responseXML:将响应信息格式化为 XML 文档格式返回

在第 1452 行建立连接处打断点,鼠标选中 xml.open 进入到 y__ 中:

在第 1365 行,这块就是 jsvmpzl 框架混淆过的代码,全是大小写的 v、u、y 和下划线,非常不便于调试分析,第 1415 行能看到该框架的版本,在第 1385 行打下断点:

先分析一下,_U__ 函数有四个参数,后三个定义在第 771 行:

  • ___:判断是 node 环境还是浏览器环境

  • v__:window 环境和鼠标点击事件(argument)

  • V__:返回指定 unicode 编码对应的字符

_ 参数在这一块并没有定义,鼠标选中后查看,展开后发现了特别的字段 AES、mode、pad,这里就看起来是数组中的对象经过了 AES 加密,并且加密模块为 CBC,填充方式为 Pkcs7:

并且这里类似于鼠标滑点坐标,断点调试时也发现只要鼠标在页面晃动断点就能断住:

在第 1385 行打断点,向上跟栈,跟到 _y__ 处,这里将 __ 数组作为参数传递给了 yU[_v] 函数:

return yU_[_v].apply(yU_, __)

在第 1338 行,在控制台打印输出一下,_v 为 createEncryptor,是个加密方法,yU_ 有个 encryptBlock,跟进去发现又跳转回了 y__ 函数位置:

__ 中看到了关键字 iv、mode、padding,更确定了这里有过 AES 加密处理:

在该行插桩打下日志断点,鼠标在网页中滑动控制台会打印出如下内容,与 _[1][0]['mouse'] 中的鼠标坐标一致:

这部分和 _U__ 函数都囊括在第 979 行的 __V 函数体中,往后跟栈,所有的堆栈都走到其中,可以尝试 hook 该函数中的 _ 参数的加密内容,找找突破口__V 函数有五个参数,hook 内容如下:

encrypt = _[1][0]['CryptoJS']['AES']['encrypt']
_[1][0]['CryptoJS']['AES']['encrypt']=function(a,b,c,d,e){
    var result = encrypt(a,b,c,d,e);
    console.log(result.toString())
    debugger;
    return a;
}

先在第 1385 行打断点断住后,将以上内容输入到控制台中进行 hook,不然会报错显示 _ 未定义,双击打印出的结果,即可进入到虚拟机中:

取消其他断点,点击第二页,即会在虚拟机中 hook 代码的 debugger;处断住,并会在控制台打印出如下内容:

通过与 v 参数对比,内容相似,长度匹配,即 __V 函数完成了对 v 参数的加密,且使用了 AES 加密,向上跟栈到 _y 中,又会跳转到第 1338 行,此处为加密位置:

上文讲过 __ 数组作为参数传递给了 yU[_v] 函数,__ 数组中的三个对象分别对应 hook 函数中的 a、b、c:

一般的 AES 加密方式如下,需要传入 text、key、iv,即对应 a、b、c.iv:

function aesEncrypt() {
    var key = CryptoJS.enc.Utf8.parse(aesKey),
        iv = CryptoJS.enc.Utf8.parse(aesIv),
        srcs = CryptoJS.enc.Utf8.parse(text),
        encrypted = CryptoJS.AES.encrypt(srcs, key, {
            iv: iv,
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Iso10126
        });
    return encrypted.toString();
}
​
var aesKey = 'YyRose',
    aesIv = 'YyRose',
    text = 'YyRose';
​
console.log(aesEncrypt())

打印一下这三部分内容:

可以看出来 a 是页码加上 | 符再加上鼠标坐标组成的,那 b 和 c.iv 偏移量呢,在第 1338 行插桩打印下日志:

取消其他断点,点击第二页,打印出来的内容中有如下部分,第四行样式与 b 和 c.iv 一致,第四行是由第二行和第三行两个一样的内容组合而成的,第二行是由十三位时间戳去掉后三位转换为十六进制后,去掉前两位得到的结果,至此分析完成:

完整代码

JavaScript 代码

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function vEncrypt(text_num) {
    var text = text_num + "|67m508,66m509,66d509,66m509,66u509"
    var timestamp = Math.round(Date.parse(new Date())/1000);
    var aesIv = timestamp.toString(16) + timestamp.toString(16);
    var key = CryptoJS.enc.Utf8.parse(aesIv),
        iv = CryptoJS.enc.Utf8.parse(aesIv),
        srcs = CryptoJS.enc.Utf8.parse(text),
        encrypted = CryptoJS.AES.encrypt(srcs, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
    return encrypted.toString();
}

// console.log(vEncrypt(2));

Python 代码

sessionid 要改为自己的:

import time

import execjs
import requests
import re


def yrx18_demo():
    num_sum = 0
    for page_num in range(1, 6):
        with open('yrx18.js', 'r', encoding='utf-8') as f:
            encrypt = f.read()
            v = execjs.compile(encrypt).call('vEncrypt', page_num)
        headers = {
            "user-agent": "yuanrenxue.project",
        }
        cookies = {
            "sessionid": " your sessionid ",
        }
        params = {
            "t": str(int(time.time() * 1000))[:-3],
            "v": v
        }
        url = "https://match.yuanrenxue.com/match/18data?page=%s" % page_num
        response = requests.get(url, headers=headers, cookies=cookies, params=params)
        for i in range(10):
            value = response.json()['data'][i]
            num = re.findall(r"'value': (.*?)}", str(value))[0]
            num_sum += int(num)
    print(num_sum)


if __name__ == '__main__':
    yrx18_demo()

猜你喜欢

转载自blog.csdn.net/Yy_Rose/article/details/126973147