yrx比赛平台刷题记录
2.js混淆_动态cookie1
首先使用hook cookie的方式定位到加密位置:
采用“手动翻译”的方式,把每个函数都拿出来运行一下,简化一下这句加密代码,可见_0x313b78是要逆向的目标,加下来就是扣代码的过程了,其中有几个格式化检测,扣下来太长了就不放了。
function GET_SDK(){
var document = {};
var navigator = {'vendorSub':'',}
//这部分是扣下来的加密代码
document.cookie = _0x313b78(Date.parse(new Date()))+ "|"+ Date.parse(new Date())
return document.cookie
}
def get_m():
filename = '2.js'
ctx = execjs.compile(open(filename,'r',encoding = 'utf8').read())
m = ctx.eval('GET_SDK()')
return m
headers = { 'cookie': f'sessionid=24v7hqkro91o8xm6fppg5nib133zl4yf;m={get_m()}',
'User-Agent': 'yuanrenxue.project'}
sum_all = 0
for i in range(1,6):
url = 'https://match.yuanrenxue.com/api/match/2?page=%d'%i
res = requests.get(url = url,headers = headers,verify = False)
print(res.json())
for value_dic in res.json()['data']:
sum_all += int(value_dic['value'])
print('和为:',sum_all)
3.访问逻辑-推心置腹
反爬点:请求头的顺序,session的保持
经观察可发现,每次请求数据的时候需要先请求一次jssm这个文件,查看网页的源码也可以得到验证:
解决方案:
session保持、使用fiddler查看真实请求头。
import requests
import pandas as pd
from requests.packages import urllib3
urllib3.disable_warnings()
session = requests.Session()
session.headers = {'Host': 'match.yuanrenxue.com',
'Connection': 'keep-alive',
'Content-Length': '0',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"',
'sec-ch-ua-mobile': '?0',
'User-Agent': 'yuanrenxue.project',
'sec-ch-ua-platform': '"Windows"',
'Accept': '*/*',
'Origin': 'https://match.yuanrenxue.com',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Referer': 'https://match.yuanrenxue.com/match/3',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cookie': 'sessionid=t1eoazv6q9q95n4v4ma4nwe9v5k1gpcj'
}
list_all = []
url_jssm = 'https://match.yuanrenxue.com/jssm'
for i in range(1,6):
url_api = 'https://match.yuanrenxue.com/api/match/3?page=%d'%i
session.post(url = url_jssm,verify = False)
res = session.get(url = url_api,verify = False)
for value_dic in res.json()['data']:
list_all.append(int(value_dic['value']))
result = pd.value_counts(list_all)
print(result)
4.雪碧图、样式干扰
反爬点:css偏移
经观察可以发现返回数据是这样的:
import re
import base64,hashlib
import requests
from bs4 import BeautifulSoup
from requests.packages import urllib3
urllib3.disable_warnings()
class Match_5:
def __init__(self) -> None:
#数字的映射字典
self.cast_num_dic = {
'放入对应的值':0,
'放入对应的值':1,
'放入对应的值':2,
'放入对应的值':3,
'放入对应的值':4,
'放入对应的值':5,
'放入对应的值':6,
'放入对应的值':7,
'放入对应的值':8,
'放入对应的值':9
}
self.headers = {
'Cookie': 'sessionid=1khdr163qlqejo6c6uwj4xbvkoxiwwvi',
'User-Agent': 'yuanrenxue.project'
}
def __parse_json(self):
sum_all = 0
for i in range(1,6):
print('当前请求第%d页'%i)
url_api = 'https://match.yuanrenxue.com/api/match/4?page=%d'%i
res = requests.get(url = url_api,headers = self.headers,verify = False)
key = res.json()['key']
value = res.json()['value']
info = res.json()['info']
display_none = self.__get_display(key,value)
soup = BeautifulSoup(info,'lxml')
for td in soup.select('td'):
actual_num = {}
imgs = [img for img in td.select('img') if display_none not in img['class']]
for index,img in enumerate(imgs):
left_css = float(re.search('left:(.*?)px',img['style']).group(1)) / 11.5
img_num = self.cast_num_dic[img['src']]
actual_num[int(index + left_css)] = img_num
sum_all += int(''.join(str(actual_num[i]) for i in range(len(actual_num))))
return sum_all
def __get_display(self,key,value):
b64_input = key + value
bytesStr = b64_input.encode(encoding='utf-8')
b64str = base64.b64encode(bytesStr)
b64_out = b64str.decode(encoding = 'utf-8') .replace('=', '')
md5_input = b64_out.encode(encoding = 'utf-8')
display_none = hashlib.md5(md5_input).hexdigest()
return display_none
def go(self):
ans = self.__parse_json()
print('总和为:', ans)
match_5 = Match_5()
match_5.go()
5.js混淆-乱码增强
进来发现cookie中有m和RM4hZBv0dDon443M两个加密参数,用油猴脚本Hook一下,发现m输出了5次,前四次的m在1716行生成,第五次的m在866行生成 ,RM4hZBv0dDon443M在978行生成。
第五次生成的m参数也是发送请求的m参数。_$yw也是url中的m,所以对于m这个参数来说,目标就是扣_0x474032这个函数。
接下来就是扣代码:
在扣代码的过程中,遇到b64pad未定义,此时可以进入浏览器调试,发现b64pad的值没有改变,可以写死为1。
接下来依然是依照缺啥补啥的原则扣代码。直到_0x474032函数可以被运行时,与浏览器生成的m进行一个验证,看看扣的代码对不对。
此时会发现虽然_0x474032函数成功运行了,但是依然没有得到一致的输出,说明某个地方出错了。
对hook进行一下修改:
(function() {
'use strict';
var pre = "";
Object.defineProperty(document, 'cookie', {
get: function() {
console.log('Getting cookie');
return pre
},
set: function(val) {
console.log('Setting cookie====>', val);
console.log('_$6_为--->' + _$6_ + ',_$tT为--->' + _$tT + ',_$Jy为--->' + _$Jy);
debugger ;
pre = val;
}
})
})();
此时可以发现前四个m都是在1716行生成的,生成规律与_$6_,_$tT,_$Jy的关系如下图所示:
第五个m在866行生成,生成规律与_$6_,_$tT,_$Jy的关系如下图所示:
结合以上分析可以知道,前四次的m生成中,_$6_都是8821003647,_$tT依次是-172015004、-717253467(三次),_$Jy为461512024、时间戳(三次)。
可以hook一下_$Jy与_$tT,可以看是怎么生成的,可以发现_$tT虽然在1721行进行了赋值,但并没有使用这里的值,所以_$tT搞清楚了。再看_$Jy,可以得知第一次的_$Jy是在274行生成,后三次的_$Jy是在1720行生成的时间戳。
第五次m的这三个值是写死的,分别为-389564586、-660478335、-405537848,此时已经可以将这几个值直接写成第五次的值就行了,我这里为了尽量还原它的算法以达到练习js逆向的效果,将它还原了出来:
这其中有几个坑需要注意,我刚开始的时候踩坑踩得很难受,像下图中的_$Jy与_$tT在扣代码的过程中不会报错,但是值为undefined,需要注意第273行与274行的这俩行代码,是这里给第一次m进行了赋值。
再者是_0x11a7a2这个函数,一定要注意它是如何运行的。可以多次进行浏览器调试之后,将其中几个值写死,也可以像我一样在脚本的开头补一下它的环境和指纹。我是补了它的$_zw这个数组。
m参数的加密函数_0x474032扣下来之后,RM4hZBv0dDon443M参数就好办了,可以发现它是来自于window的_$ss,hook一下_$ss就知道是1229行来的,观察后发现是一个标准的AES,require(“crypto-js”)即可。
扣下来的主流程如下:
function GET_SDK(){
//这部分是扣下来的代码,省略。
//这部分是扣下来的代码,省略。
//这部分是扣下来的代码,省略。
for (i = 1; i <= 4; i++) {
console.log('*****************第' + i + '个m生成中*****************');
_$Wa = Date.parse(new Date());
_0x4e96b4['_$pr']['push'](_0x474032(_$Wa));
delete _0x4e96b4['_$Jy'];
delete _0x4e96b4['_$tT'];
_0x4e96b4['_$Jy'] = _0x2d5f5b();
// _0x4e96b4['_$tT'] = _0x2d5f5b() - _0x12eaf3();
_0x4e96b4['_$tT'] = -0x2ac06b5b; //2057
}
delete _0x4e96b4['_$tT'];
delete _0x4e96b4['_$Jy'];
_0x4e96b4['_$6_'] = -0x173848aa;
_0x4e96b4['_$tT'] = -0x275e197f;
_0x4e96b4['_$Jy'] = -0x182c0438;
console.log('*****************第5个m生成中*****************');
_$yw = new _0x35bb1d()['valueOf']()['toString']();
cookie_m = _0x474032(_$yw);
_0x4e96b4['_$pr']['push'](_0x474032(_$yw));
_0x4e96b4['_$is'] = _$yw;
_$Ww = _$Tk['enc']['Utf8']['parse'](window['_$pr']['toString']());
_0x4e96b4['_$qF'] = CryptoJS['enc']['Utf8']['parse'](_0x4e96b4['btoa'](_$yw)['slice'](0x0, 0x10));
_0x29dd83 = _$Tk['AES']['encrypt'](_$Ww, _0x4e96b4['_$qF'], {
'mode': _$Tk['mode']['ECB'],
'padding': _$Tk['pad']['Pkcs7']
});
cookie_RM4hZBv0dDon443M = _0x29dd83['toString']();
return {
'cookie': 'm=' + cookie_m + ';RM4hZBv0dDon443M=' + cookie_RM4hZBv0dDon443M,
'm': window._$is,
'f': window.$_zw[23]
}
}
总结:一定要注意window = global、window = {}以及window = this的区别,这决定了你扣的代码该如何修改细节,刚开始的时候因为这个地方错了很多次。
7.动态字体,随风漂移
反爬点:动态字体
首先打开F12,观察抓包结果:
可以看到,数字2342对应xf789、xb925、xb642、xf789,先记住这个结果,再看下一页的抓包:
可以看到,数字5553对应xb458、xb458、xb458、xe178。
我们在上一页的结果中,数字3对应了xb925;但在这一页的结果中,数字3对应了xe178。可以得知这个数字的映射关系是在改变的。我们再看下面这段代码:
从这段代码可以看出,我们抓包得到的数据中,woff中的数据被获取并用以下载一个字体包,woff中的数据是一段base64编码后的二进制数据。
我们分别把刚才抓到的两个包所对应的字体文件下载下来,后缀名改为woff,再保存一份ttf格式的字体文件,用在线字体编辑器打开:
使用python中的fontTools可以将上面两个包转为xml文件,方便对比查看:
# 加载字体文件:
font = TTFont('字体文件.woff')
# 保存为xml文件:
font.saveXML('字体文件.xml')
fontTool包的具体使用可以查看十一大佬的文章
经过上面一番操作,我们有以下几个文件:
对比一下两个字体:
经过对比,我们可以找到两个字体文件的共同点:
两个同样的数字1,其name不同,但坐标on的数据是一致的
利用上面的这个特性,我们可以建立字体的映射关系:
经过以上分析,我们下面可以开始写代码了:
def parse_woff():
word_dic = {映射字典}
res_dic = {}
font = TTFont('./7_woff.woff')
for uni in font.getGlyphNames()[1:]: #依次获取name标签的值,不要第一个,并for循环遍历
on_data = font['glyf'][uni].flags #此处返回的是对应的name标签的bytearray
on_key = ''.join([str(n) for n in on_data]) #获得对应的name标签的on坐标值,如1001101111
corresponding_num = uni.replace('uni','&#x') #替换一下,用以和网页中响应的数据进行映射
res_dic.update({corresponding_num:word_dic[on_key]})
return res_dic
此函数每次返回的res_dic即为每次解析好的字体对应关系,如下图所示:
dic_ans = {}
for page in range(1,6):
yyq = 1
print('当前请求第%d页'%page)
url = f'https://match.yuanrenxue.com/api/match/7?page={page}'
res = requests.get(url = url,headers = headers)
data_list = res.json()['data']
woff_content = base64.b64decode(res.json()['woff']) #获取的是woff文件的二进制数据,直接在下面以二进制写入文件即可
with open('7_woff.woff','wb') as fp:
fp.write(woff_content)
res_dic = parse_woff() #解析,并返回对应的字体映射关系
for i in range(0,len(data_list)):
data_list2 = data_list[i]['value'].split(' ')[:-1]
data = ''.join([res_dic[num] for num in data_list2])
name = player_name_list[yyq + (page-1)*10 ] #根据网页中的名字计算逻辑,此处略过
dic_ans.update({name:int(data)})
yyq+=1
print('所有玩家及其对应的胜点字典为:\n',dic_ans,'\n')
print('胜点最高的玩家为:\n',max(dic_ans, key=lambda x: dic_ans[x]))
结果:
15.备周则意怠,常见则不疑(wasm)
打开F12,观察找到数据所在的包,发现有两个参数,一个是加密的m值,另一个是页码page值。目标就是逆向m值。
很容易发现m加密的位置:
此时发现q函数是native code,是通过wasm文件定义的。
于是接下来的流程就是调用第三方python库pywasm来调用encode这个函数了:
import math
import random
import time
import pywasm
import requests
def get_m():
t = int(time.time())
t1 = int(t / 2)
t2 = int(t / 2 - math.floor(random.random() * 50 + 1))
vm = pywasm.load("main.wasm")
r = vm.exec("encode", [t1, t2])
m = f"{r}|{t1}|{t2}"
return m
sums = 0
headers = {'cookie': 'sessionid=9a2irg2q2x66wrkfbm685cu6fpkb23rt;',
'User-Agent': 'yuanrenxue.project'}
for i in range(1, 6):
url = f"https://match.yuanrenxue.com/api/match/15?page={i}&m={get_m()}"
print(url)
response = requests.get(url = url,headers = headers).json()
for each in response["data"]:
sums += each["value"]
print('总和为:',sums)
其中有一个巨坑,在刚开始的时候我下载这个wasm的方式是点入sources里面,右键这个wasm文件来下载,但发现这样的wasm文件python会报错:【magic header not detected】,后来尝试了在network里面下载(open in new tab)才解决。可能是因为在sources里面看到的wasm文件已经是编译过的代码导致的。
16.js逆向-window蜜罐(webpack)
反爬点:
①:node环境与浏览器环境不同,进行“投毒”
②:webpack的扣取方法
③:格式化检测
主要流程:
首先打开F12查看抓包,很容易就能定位到加密点:
然后按照缺啥补啥的方式去扣代码即可,以这种扣代码的方式可以算作解法一。需要注意在try-catch或if-else中的投毒,看看在浏览器里是什么。
也可以把f、_0x4c28、_0x34e7写死,我这里选择尽量还原代码的方式,没有写死。
除了这种硬扣的方法以外,还可以根据webpack的特性去改写它:
webpack的一般逻辑:
!function (allModule) {
function useModule(whichModule) {
allModule[whichModule].call(null, "hello world!");
}
useModule(0) //导入的allModule是数组形式,这里以这样的方式去引用
}([
function module0(param) {console.log("module0: " + param)},
function module1(param) {console.log("module1: " + param)}
]);
!function (allModule) {
function useModule(whichModule) {
allModule[whichModule].call(null, "hello world!");
}
useModule('module1') //导入的allModule是对象形式,这里以这样的方式去引用
}({
module0: function (param) {console.log("module0: " + param)},
module1: function (param) {console.log("module1: " + param)}
});
webpack的五步改写流程:
①找到加载器(加载模块的方法):function n(r){}即为加载器
②找到调用的模块:依次调用了127、58、732
③构造一个自执行方法
④导出加密方法
⑤编写自定义方法,按照流程加密
如下即为解法二,改写webpack,127、58、732里无用的代码要注意删除,不然会影响代码运行:
18.jsvmp-洞察先机
插桩打日志直接出秘钥的解决方法
打一下xhr断点可以很容易跟到如图所示的位置,发现XMLHttpRequest.prototype.open方法被改写了,那么跟进去看一看
我先试着用解决抖音的vmp的办法去插桩:找到最大坨的那个函数插桩
如图,打一下这个_[2]里面的东西,至于为什么要打它,当然是调试出来的,里面有一些我们可能需要的关键字符串
此时结合上面两张图已经解出这道题了:AES-CBC加密,填充方式为Pkcs7
但也可以看到
“key”:{“words”:[909402422,845427045,909402422,845427045],“sigBytes”:16}
“iv”:{“words”:[909402422,845427045,909402422,845427045],“sigBytes”:16}
这还需要我们还原为它们本来的样子才行,可以用这个方法:
先另开一个空白网页,参考我的这篇文章,把AES源码导进来:
这里我给出字符串与32位整数数组互转的办法:
// 将字符串转化为字节数组
var text = "64a62d1e64a62d1e";
var encoder = new TextEncoder();
var data = encoder.encode(text); // data为Uint8Array类型的字节数组
// 将字节数组转化为32位整数数组
var blockCount = Math.ceil(data.length / 4);
var blocks = [];
for (var i = 0; i < blockCount; i++) {
var word = 0;
for (var j = 0; j < 4; j++) {
var index = i * 4 + j;
if (index < data.length) {
word |= data[index] << (8 * (3 - j));
}
}
blocks.push(word);
}
console.log(blocks) // 输出结果:[909402422, 845427045, 909402422, 845427045]
// 将32位整数数组转化为字节数组
var encryptedBlocks = [909402422, 845427045, 909402422, 845427045];
var words = CryptoJS.lib.WordArray.create(encryptedBlocks);
var encrypted = CryptoJS.enc.Hex.parse(words.toString());
var text = CryptoJS.enc.Utf8.stringify(encrypted);
console.log(text); // 输出结果:64a62d1e64a62d1e
根据这个方法,我们可以得到key和iv,同时也能得到加密前的原文,也在日志里打出来了。
不过可以发现key、iv是64a62d1e64a62d1e,加密前原文是"2|80m696,78m701,77m702,76m703,76d703,76u703"这种形式,接下来就研究它们是怎么生成的。
通过搜索key和iv,我们找到日志中它出现的第一个地方:
要怎么定位到最后一个时间戳的断点呢?可以通过条件断点解决,我们搜索一下6 '
(即日志开头的字符串),发现有几十个,数一下这个关键位置是第几个:
那么定义一个window.Mycount = 0;在这里打上条件断点:
这样当第42次进入__U===6的逻辑时,会停在此处,之后单步慢慢调试就可以跟到这里,可以看出来这就是一个十进制转十六进制而已:
之后经过这里的一大坨三元表达式把时间戳拼起来了:
至于加密的原字符串2|80m696,78m701,77m702,76m703,76d703,76u703,2一看就是页码,后面的一坨可以直接固定写死。
请求就完事了:
from Crypto.Cipher import AES
import base64
from Crypto.Util.Padding import pad
import requests
import time
import math
headers = {
"user-agent": "yuanrenxue.project",
"cookie":"sessionid="
}
# 加密
def AES_encrypt(text,key,iv):
key = key.encode('utf-8')
iv = iv.encode('utf-8')
padding_text = pad(text.encode(), AES.block_size, style='pkcs7')
cryptos = AES.new(key, AES.MODE_CBC, iv)
cipher_text = cryptos.encrypt(padding_text)
return base64.b64encode(cipher_text).decode()
def get_data(page):
timestamp = math.floor(time.time())
enc_text = f"{page}|80m696,78m701,77m702,76m703,76d703,76u703"
key_and_iv = str(hex(timestamp)[2:]) + str(hex(timestamp)[2:])
request_url = f"https://match.yuanrenxue.cn/match/18data"
params = {
"page": page,
"t": timestamp,
"v": AES_encrypt(enc_text,key_and_iv,key_and_iv)
}
res = requests.post(request_url,headers=headers,params=params)
return res.json()
sum_all = 0
for page in range(1,6):
print(f"请求第{page}页:")
data = get_data(page)
print(data)
for v in data["data"]:
value = v["value"]
sum_all += value
print("答案为",sum_all)