网址:第十八题 jsvmp 洞察先机 - 猿人学
本题目标:抓取 5 页数字,计算加和并提交结果
一般情况下,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 一致,第四行是由第二行和第三行两个一样的内容组合而成的,第二行是由十三位时间戳去掉后三位转换为十六进制后,去掉前两位得到的结果,至此分析完成:
// 引用 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));
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()