Mkdir700's Note

Mkdir700's Note

爬虫总结_雷速体育_Canvas字体加密

1496
2021-04-10

网址:https://live.leisu.com/wanchang

可以看到这个比分是使用canvas绘制上去的。

image-20210409192938030

了解Canvas

首先了解下canvas

canvas是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素.例如,它可以用于绘制图表、制作图片构图或者制作简单的(以及不那么简单的)动画.

主要了解下 canvas绘制文本

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_text

看了上面的简单的教程,姑且粗略地认为绘制文本前需要提供文本。

例如绘制hello world 则需要提供hello world这个字符串。

在下方链接体验canvas文本绘制

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_text#textbaseline%E4%BE%8B%E5%AD%90

345

寻找绘制方法

通过上面的了解,我们只需要找到数据在哪里就可以了。

F12查看network,浏览一遍下来没有发现明显的数据交互请求。全是js、css、png等静态文件。

查看html源代码,也没有明显的数据。

html中的canvas的标签,js需要定位到canvas标签才继续操作,所以我们可以试着全局搜索canvas关键字。

F12打开审查工具,Ctrl+Shift+F全局搜索

image-20210409200025714

一一查看搜索结果,查看到最后一个js文件(match_layout_wc_xxx.js),明显这个js代码被混淆了。一般可以理解为,开发者只会对关键代码进行混淆。

说明这个文件的代码不想给其他人看。

image-20210409200136835

继续往下看,可以看到这明显是Vue框架的写的。这个文件是一个Vue写的子组件。

image-20210409200528451

当这个组件被挂载时,将执行this[_x116622[80]](),即调用this[_x11622[80]]方法。

image-20210409200721551

对这个位置打上断点,刷新网页,在此断点后查看_x11622[80]

image-20210409200958145

所以,这个组件被挂载时,即执行this.drwaBase()

这个this.drwaBase方法在哪里呢,Vue的方法统一定义在methods内,或者在这个js文件里搜搜看。

image-20210409201223538

我们把断点打在drwaBase方法里的开头和结尾,让其跑完整个drwaBase方法。

345

可以看到每跑一次drwaBase方法,界面上就有一行数据显示出来。

在结合drwaBase方法没有参数,所以在挂载这个组件前,数据就已经加载出来了。

所以,我需要找到数据何时被初始化。

跟着调用栈往上找。

wanchang-xxxx.js的文件中找到函数名为initData方法,这个引起了我们的注意。可以看到有一个明显的JSON, 在这一行上打断点。刷新开始调试。

image-20210410125057329

image-20210410125450740

所以,这句代码原本的样子可以还原为:

let _ = JSON.parse($.rot(base64_, STATIC_CONFIG.KST))

其中STATIC_CONFIG.KST根据多次调试,这个值一直是整数6

let _ = JSON.parse($.rot(base64_, 6))

此时,我们接着看下_是什么,展开其中一个数组,可以看到这就是我们所需的数据。

image-20210410125937325

明确需要寻找什么

那么现在,我们的首要目的就是找到base64_变量是什么时候赋值的以及$.rot方法是什么。

找寻方法,理清执行流程

Console里输入$.rot

image-20210410130222153

Console返回的代码是实际执行的。也就是:

$.rot = function (t, e) {
    const i = roott(t, e);
    return pushmsg(i)
}

点击返回内容可以直接跳转到此方法。

function(t) {
    try {
        let e = ["t", "ro"];
        if (!t || !window.LeisuJS)
            return;
        t[e[1] + e[0]] = function(t, e) {
            const i = roott(t, e);
            return pushmsg(i)
        }
    } catch (t) {
        "prod" != STATIC_CONFIG.NODE_ENV && console.error(t)
    }
}(jQuery),

简单的分析,传了一个Jquery进去

然后对其设置了一个方法

t[e[1] + e[0]] = function(t, e) {
    const i = roott(t, e);
    return pushmsg(i)
}

替换一下就是:

t.rot = function(t, e) {
    const i = roott(t, e);
    return pushmsg(i)
}

可以看到这就是实际执行的代码。

那么到分析到现在整体流程有个大概的雏形了。

  1. 超长字符串传入rot方法;
  2. 经过roottpushmsg方法的处理,最终得到有效数据

接着我们使用同样的方法找roottpushmsg

roott

e.roott = function(t, e) {
    for (var i = "", n = 0; n < t.length; n++) {
        var a = t.charCodeAt(n)
        , o = a;
        a >= 65 && a <= 90 && (o = (a - 65 - 1 * e + 26) % 26 + 65),
            a >= 97 && a <= 122 && (o = (a - 97 - 1 * e + 26) % 26 + 97),
            i += String.fromCharCode(o)
    }
    return i
}

pushmsg

e.pushmsg = function(t) {
    let e = "";
    if ("undefined" == typeof window)
        try {
            e = nodeAtob(t)
        } catch (t) {
            console.log(t)
        }
    else
        e = atob(t);
    const i = e.split("").map(function(t) {
        return t.charCodeAt(0)
    })
    , n = new Uint8Array(i)
    , a = pako.inflate(n);
    return e = function(t) {
        let e, i, n, a, o = "";
        const r = t.length;
        for (e = 0; e < r; )
            switch ((i = t[e++]) >> 4) {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                    o += String.fromCharCode(i);
                    break;
                case 12:
                case 13:
                    n = t[e++],
                        o += String.fromCharCode((31 & i) << 6 | 63 & n);
                    break;
                case 14:
                    n = t[e++],
                        a = t[e++],
                        o += String.fromCharCode((15 & i) << 12 | (63 & n) << 6 | (63 & a) << 0)
            }
        return o
    }(new Uint16Array(a)),
        unescape(e)
}
}(),

roott可以很容易理解,pushmsg刚开始也能看懂直到pako.inflate(n);

进去看了下这个inflate,有点复杂不知道这一步是在干什么。

image-20210410132450799

这看着有点像调用其他的工具包,于是我去搜索pake.inflate

image-20210410132640953

一个解压库,将数据解压就可以用到这个pako

找寻原始数据base64_

直接搜索

base64_已经是处于加密状态,是一个非常长的字符串,有理由相信实际返回的原始数据就是这个超长字符串。我们的拿出字符串的前几位去全局搜索。

image-20210410142314200

image-20210410142344874

看了下base64_被执行赋值操作的js文件是wc-xxxx.js

然后简单调试发现,请求这个js文件时,无需携带cookie,所以尝试直接请求这个js文件,成功获取响应。

hook方法

base64_是一个全局变量,如果我们能在base64_被赋值的那一瞬间进入debug状态,然后插卡调用栈即可看到window.base64_在何时被赋值的。

写一个hook函数

(
function (){
	'use strict';
	Object.defineProperty(
		window, 'base64_', {
	    	set: function(v) {
	        	console.log('window.base64_正在被赋值');
	        	debugger;
	        	return v;
		    }
		}
	)
}
)();

找一个在window.base64_被赋值前就执行的js代码片段,越早越好。

image-20210410145325203

进入此函数,随便打个断点

image-20210410145418167

然后刷新界面,进入断点状态后,在console粘贴hook代码

image-20210410145541945

F8继续运行到下一个断点处

image-20210410145632883

此时查看调用栈

image-20210410145723677

复现js代码

打开webstorm,新建一个nodejs项目

pushmsg中使用了两个包,分别是atobpako,所以安装这两个包并导入。

npm install atob
npm install pako
const pako = require('pako')
const atob = require('atob')

编写rot函数

function rot (t, e) {
    const i = roott(t, e);
    return pushmsg(i)
}

编写roott函数

function roott(t, e) {
    for (var i = "", n = 0; n < t.length; n++) {
        var a = t.charCodeAt(n)
            , o = a;
        a >= 65 && a <= 90 && (o = (a - 65 - 1 * e + 26) % 26 + 65),
        a >= 97 && a <= 122 && (o = (a - 97 - 1 * e + 26) % 26 + 97),
            i += String.fromCharCode(o)
    }
    return i
}

编写pushmsg函数

我们导入的是atob这个包所以,前面的一些判断直接删掉。

function pushmsg (t) {
    let e = "";
    e = atob(t);
    const i = e.split("").map(function(t) {
        return t.charCodeAt(0)
    })
        , n = new Uint8Array(i)
        , a = pako.inflate(n);
    return e = function(t) {
        let e, i, n, a, o = "";
        const r = t.length;
        for (e = 0; e < r; )
            switch ((i = t[e++]) >> 4) {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                    o += String.fromCharCode(i);
                    break;
                case 12:
                case 13:
                    n = t[e++],
                        o += String.fromCharCode((31 & i) << 6 | 63 & n);
                    break;
                case 14:
                    n = t[e++],
                        a = t[e++],
                        o += String.fromCharCode((15 & i) << 12 | (63 & n) << 6 | (63 & a) << 0)
            }
        return o
    }(new Uint16Array(a)),
        unescape(e)
}

复制一个base64_进去调用rot运行试试看,可以看到解密成功。

image-20210410134135235

使用Python如何解密

我们的爬虫是用python写的,那么如何去使用这个nodejs解密的结果呢?

有两种方法,

  1. 在nodejs上起一个API服务,在Python中请求数据解密接口就行了。
  2. 使用纯Python代码复现解密代码。

借助nodejs API服务

nodejs有很多框架,目前我对nodejs不太熟悉,去搜了下,express这个库比较流行。

用什么都可以,因为我们的接口就是在本子自己调用,不必考虑其他情况。

网上抄代码:

server.js

let express = require('express');
// 我们自己写的解密文件
const bb = require('./decode.js')
let app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());//数据JSON类型
app.use(bodyParser.urlencoded({ extended: false }));//解析post请求数据

app.all('*',function(req,res,next){
    let origin=req.headers.origin;
    res.setHeader('Access-Control-Allow-Origin',"*");
    res.setHeader('Access-Control-Allow-Headers','Content-Type');
    next();
})

app.post('/data',function(req,res){
    console.log(req.body);
    var base = req.body.base
    // 调用解密方法
    var result = bb.rot(base)
    res.send(result)
})

app.listen(8080)

启动服务

node server.js

效果演示

345

使用Python复现代码

atob是将base64字符串解码

pako是将zlib压缩文件

类似这些功能的包在Python中也用,还都是内置包

atob -> base64.b64decode
pako.inflate -> zlib.decompress

rot

def rot(t, e):
    i = roott(t, e)
    return pushmsg(i)

roott

# function roott(t, e) {
#     for (var i = "", n = 0; n < t.length; n++) {
#         var a = t.charCodeAt(n)
#             , o = a;
#         a >= 65 && a <= 90 && (o = (a - 65 - 1 * e + 26) % 26 + 65),
#         a >= 97 && a <= 122 && (o = (a - 97 - 1 * e + 26) % 26 + 97),
#             i += String.fromCharCode(o)
#     }
#     return i
# }
def roott(t, e):
    i = ""
    for n in range(len(t)):
        a = ord(n)
        o = a
        if 65 <= a <= 90:
            o = (a - 65 - 1 * e + 26) % 26 + 65
        if 97 <= a <= 122:
            o = (a - 97 - 1 * e + 26) % 26 + 97
        i += chr(o)
     return i

如果嫌麻烦,可以使用pyexecjs,就像这样

import execjs

js = """function roott(t, e) {
    for (var i = "", n = 0; n < t.length; n++) {
        var a = t.charCodeAt(n)
            , o = a;
        a >= 65 && a <= 90 && (o = (a - 65 - 1 * e + 26) % 26 + 65),
        a >= 97 && a <= 122 && (o = (a - 97 - 1 * e + 26) % 26 + 97),
            i += String.fromCharCode(o)
    }
    return i
}"""
s = execjs.compile(js)
...
# 调用roott
s.call('roott', t, e)

pushmsg

import base64
import zlib
from urllib import parse

def pushmsg(t):
    # 对应atob
    r = base64.b64decode(t.encode())
    # 解压
    b = zlib.decompress(r)
    p = parse.unquote(b.decode())
    return p.replace('%', '\\').encode().decode('unicode-escape')

完整代码

import base64
import zlib
from urllib import parse


def roott(t, e):
    i = ""
    for n in t:
        a = ord(n)
        o = a
        if 65 <= a <= 90:
            o = (a - 65 - 1 * e + 26) % 26 + 65
        if 97 <= a <= 122:
            o = (a - 97 - 1 * e + 26) % 26 + 97
        i += chr(o)
    return i


def pushmsg(t):
    # 对应atob
    r = base64.b64decode(t.encode())
    # 解压
    b = zlib.decompress(r)
    p = parse.unquote(b.decode())
    print(p.replace('%', '\\').encode().decode('unicode-escape'))


def rot(t, e):
    i = roott(t, e)
    return pushmsg(i)


if __name__ == '__main__':
    base64_ = "kL7ylBr73JgC9w/XFj..."
    e = 6
    print(rot(base64_, e))

效果演示