版权声明:原创不易,本文禁止抄袭、转载,侵权必究!
oklink简介:
oklink是一个区块链机构,包括eth、btc、tron、polygon、bsc等区块链浏览器,它们的交易、地址、标签等数据大部分可以在该网站找到
JS逆向实战目标:
假设你有一个任务,需要根据各链的地址把每个地址的标签数据爬取下来,方便你另一个同事-机器学习/数据挖掘工程师建模分析该地址的一些隐含信息,比如该地址是否是洗钱地址,是否是黑客地址等
JS逆向实战示例地址:
https://www.oklink.com/cn/eth/address/0xdac17f958d2ee523a2206206994597c13d831ec7
如上所述,我们就用太坊的地址-即以0x开头的42位长度地址作为示例
JS逆向实战示例页面:
注:红色框里面就是我们要爬取的数据
加密参数:
请求头加密参数:
加密数据:
抓取数据一般有几种方式,网页元素定位(css/xpath/re等)、接口请求(ajax、fetch等)、自动化模拟(selenium/palywrigth等)等;在其返回的json标签数据是经过加密的,这时候我们可能会想用网页元素定位的方式去抓取数据;但对其网页html进行分析时,会发现定位不到标签数据
如果用自动化模拟的方式抓取数据,性价比太低了,假如有10万条数据,那么就要模拟点击10万次,数据量越大,爬取速度越慢,这可不行;为了使效果最优,我们只有选择通过JS逆向破解其加密参数了
JS逆向分析一般有几种方式:全局搜索(也可局部搜索)、断点调式(ajax断点/DOM断点/监听器断点等)、hook(钩子/拦截技术)等
全局搜索是我们进行逆向分析最直接,也是最简单、首选的方式,我们先看看该接口的API是啥,如下图
如上图所示,该API接口为:
https://www.oklink.com/api/explorer/v1/eth/address/0xdac17f958d2ee523a2206206994597c13d831ec7/more?t=1680606330657
其中有一个网址参数t,是一个13位长度的时间戳
而请求头加密参数为:
x-apikey: LWIzMWUtNDU0Ny05Mjk5LWI2ZDA3Yjc2MzFhYmEyYzkwM2NjfDI3OTE3MTc0NDE3NjQxNjU=
该参数看起来好像是base64加密,我们使用在线工具解密试试:
在线加密解密工具网址:
https://33tool.com/base64/
-b31e-4547-9299-b6d07b7631aba2c903cc|2791717441764165
如果直接解密,我们用该解密后的字符串直接请求,请求是失败的,那么加密过程肯定还经过一些其他的加密或编码逻辑处理
使用全局搜索,如下图:
输入参数x-apikey进行全局搜索,接着进行格式化,查看JS代码:
很不错,看来能够直接搜索到,变量名和方法名并没有经过JS混淆,对爬虫还是比较友好的
观察JS代码可轻易看出,在通过setRequestHeader()方法给参数x-apikey设置值时,是使用getApiKey()方法实现的,使用快键键ctrl+f搜索方法名getApiKey:
观察getApiKey()方法里的代码逻辑,可以轻易看出最后返回的是this.comb(e, t),this类似于python中的self,方法comb有两个参数,e是由方法encryptApiKey()得到的,而t参数首先获取当前时间,是一个13位长度的时间戳,然后再调用方法encryptTime(t),对时间参数t进行加密逻辑处理,作为comb的第二个参数
先看看encryptApiKey()是怎样进行加密的:
{
key: "encryptApiKey",
value: function() {
var t = this.API_KEY
, e = t.split("")
, n = e.splice(0, 8);
return t = e.concat(n).join("")
}
}
最开始的变量t是一个写死的字符串:
['a', '2', 'c', '9', '0', '3', 'c', 'c', '-', 'b', '3', ………………]
变量e是对变量t进行分割,返回一个由单个字符组成的列表:
['a', '2', 'c', '9', '0', '3', 'c', 'c', ………………]
然后删除列表e的前8个字符并返回,组成变量n,也是一个列表:
['a', '2', 'c', '9', '0', '3', 'c', 'c']
最后以列表e在前,列表n在后,进行无符号连接,得到最终变量t,也就是方法getApiKey()中的变量e
-b31e-4547-9299-b6d07b7631aba2c903cc
再来看看encryptTime()方法里的逻辑:
{
key: "encryptTime",
value: function(t) {
var e = (1 * t + a).toString().split("")
, n = parseInt(10 * Math.random(), 10)
, r = parseInt(10 * Math.random(), 10)
, o = parseInt(10 * Math.random(), 10);
return e.concat([n, r, o]).join("")
}
}
从JS代码可看出,此处的变量t是一个13位长度的整型时间戳,通过分析,此处的变量a也是一个写死的整型变量1111111111111,计算之后,转换为str类型再进行无符号分割成列表组成变量e,而变量n,r,o都是一个0-10之间的随机整数,最后把这四个变量无符号连接起来得到最终的返回值,也就是方法getApiKey()中的变量t
最后来看看最终的方法comb():
{
key: "comb",
value: function(t, e) {
var n = "".concat(t, "|").concat(e);
return window.btoa(n)
}
}
这里需要注意一点,this.comb(e, t),这里的变量e,t传给方法comb之后,实参变量e变成了形参变量t,而实参变量t变成了形参变量e,然后以形参t在前,形参e在后的顺序使用连接符“|”进行连接,最终返回的变量是经过btoa()方法,也就是base64方法加密过的变量
由于此处的JS逆向是比较简单的,并没有逻辑混淆和JS混淆等,使用python编写代码即可完成,不需要再使用execujs、node.js等去模拟执行JS代码了
通过以上分析,逆向之后的代码如下:
import requests
import time
import random
import base64
def get_apikey():
API_KEY = "a2c903cc-b31e-4547-9299-b6d07b7631ab"
key1 = API_KEY[0:8]
key2 = API_KEY[8:]
new_key = key2 + key1
current_time = int(time.time() * 1000)
new_time = str(1 * current_time + 1111111111111)
random1 = str(random.randint(0, 9))
random2 = str(random.randint(0, 9))
random3 = str(random.randint(0, 9))
current_time = new_time + random1 + random2 + random3
last_key = new_key + '|' + current_time
x_apiKey = base64.b64encode(last_key.encode('utf-8'))
return str(x_apiKey, encoding='utf-8')
运行get_apikey()方法:
LWIzMWUtNDU0Ny05Mjk5LWI2ZDA3Yjc2MzFhYmEyYzkwM2NjfDI3OTE3ODQ3MTU2NDMzMjk=
逆向代码编写完成
接下里只剩测试了,因为api接口和逆向代码都要使用时间戳,为了保持时间戳一致性,我们将时间戳作为参数传递给get_apikey(now_time),再编写简单的代码进行测试,代码如下:
# -*- coding: utf-8 -*-
import requests
import time
import random
import base64
def get_apikey(now_time):
API_KEY = "a2c903cc-b31e-4547-9299-b6d07b7631ab"
key1 = API_KEY[0:8]
key2 = API_KEY[8:]
new_key = key2 + key1
new_time = str(1 * now_time + 1111111111111)
random1 = str(random.randint(0, 9))
random2 = str(random.randint(0, 9))
random3 = str(random.randint(0, 9))
now_time = new_time + random1 + random2 + random3
last_key = new_key + '|' + now_time
x_apiKey = base64.b64encode(last_key.encode('utf-8'))
return str(x_apiKey, encoding='utf-8')
now_time = int(time.time()) * 1000
headers = {
'x-apikey': get_apikey(now_time),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'
}
api = f'https://www.oklink.com/api/explorer/v1/eth/address/0xdac17f958d2ee523a2206206994597c13d831ec7/more?t={now_time}'
res = requests.get(url=api, headers=headers)
print(res.json())
控制台输出(格式化一下):
{
'code': 0,
'msg': '',
'detailMsg': '',
'data':
{
'entityTags':
['QayvIUbQGpJhs4QOJk7Ccw==: dlWG6vsFQhA+YAnbzdnYNg==. igTdUMG1sXqlL+ISnaIU8Q=='],
'propertyTags':
['BYqzosCjwa3Hdj/jGp99Xg==', 'B3N0UYJLaM9LPazO98GU9Q==']
}
}
现在可以正常返回response了,但从输出结果可看出,即使我们破解了请求头加密参数,它的响应response标签数据仍然是加密的,那么现在我们是不是要进一步破解该标签数据的加密逻辑呢?
再进一步分析响应中标签数据的加密逻辑固然是可行的,但是我们想想,无论JS里面采取怎样的加密逻辑,其最初的字符串是不变的:
API_KEY = "a2c903cc-b31e-4547-9299-b6d07b7631ab"
如果我们使用该字符串来代替x-apikey直接进行请求,会发生什么样的效果呢?@>_<@
没错,我们把x-apikey直接写死成API_KEY:
now_time = int(time.time()) * 1000
headers = {
'x-apikey': 'a2c903cc-b31e-4547-9299-b6d07b7631ab',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'
}
api = f'https://www.oklink.com/api/explorer/v1/eth/address/0xdac17f958d2ee523a2206206994597c13d831ec7/more?t={now_time}'
res = requests.get(url=api, headers=headers)
print(res.json())
控制台输出(格式化一下):
{
'code': 0,
'msg': '',
'detailMsg': '',
'data':
{
'entityTags':
['DeFi: Tether. USDT Stablecoin'],
'propertyTags':
['ERC20', 'Tether USDT']
}
}
观察一下,这种思路是可行的,而且这应该才是我们想要的方式,我们不仅得到解密后的真实数据,里面还包含了区块链地址所属类型:Defi、ERC20,而这个类型数据在网页上面是不可见的,只有通过接口请求数据才获取得到,至此,我们完成了JS反逆向,而不是JS逆向
那么这是为什么呢?竟然可以直接通过写死的字符串成功请求数据,而且得到的数据还是未经加密的数据
原因可能是这个网站采取了反向的JS逆向逻辑,使得那些太专注于JS逆向构建逻辑代码的数据抓取者会陷入一个误区,反而找不到其最真实而又简单直接的这种方式,被打了一个反包围
这种反向JS逆向逻辑算是比较另类的存在了,虽然比较简单,但值得我们注意和反思
oklink逆向完整源码下载
免责声明:本篇文章仅供学习与研究使用,切勿用于违法途径!
Author:小鸿的摸鱼日常,Goal:让编程更有趣!
专注于算法、爬虫,网站,游戏开发,数据分析、自然语言处理,AI等,期待你的关注,让我们一起成长、一起Coding!
版权说明:本文禁止抄袭、转载 ,侵权必究!