Reverse-头条

摘要

本文内容转自网络,个人学习记录使用,请勿传播

头条新闻抓取

需求

  • 将头条中的新闻咨询进行数据爬取。

数据包分析

  • 滚轮下滑,会加载出更多的新闻咨询数据,因此,数据的加载是基于ajax请求实现的。

  • 通过抓包工具,捕获ajax数据包,发现一个feed的数据包的响应数据中存在加载出的新闻数据。

    • url:https://www.toutiao.com/api/pc/list/feed

    • 请求参数:

      • 1
        2
        3
        4
        5
        6
        channel_id: 3189398999  【固定】
        max_behot_time: 1650364906 【时间戳】
        category: pc_profile_channel 【固定】
        aid: 24 【固定】
        app_name: toutiao_web 【固定】
        _signature: _02B4Z6wo00901ZogFsQAAIDA4u79wxDuXSGaBBJAAATX7cy7h0X5W7RfeqRU9vU4c57nKyTqBEVpuutMIr7033MXo70zZveziaEdhwxpiZ46YXA8.L2YZz8QdKDh-ZUBUrtzyJ4A7aXdUB1621 【签名:动态变化的,具有时效性】
    • 请求头

前置知识点

js三目运算符

1
2
条件?值1:值2
解释:如果条件成立,则执行值1,否则执行值2

js的逻辑运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a;
a = 1==1 || 2==2;
//a为true

var a;
a = 3 || 4 //只需要查看运算符左侧的值,如果值为真,则直接返回该值,无序计算右侧的值
//a为3

var a,b;
a = 66 == (b=123);
//a为false

var a,n;
a = 1 > (n=2) || 1 == 1 ? 9 : 8;
//a为9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//window.byted_acrawler不为空、window.byted_acrawler.sign不为空)
var o = (null === (n = window.byted_acrawler) || void 0 === n ? void 0 : null === (a = n.sign) || void 0 === a ? void 0 : a.call(n, i)) || "";

//分析:
(n = window.byted_acrawler) ==》 not null
null === (n = window.byted_acrawler) ==》 false
//至此,表达式可以为:void 0 ==》void 0
var o = (false || void 0 === n ? void 0 : null === (a = n.sign) || void 0 === a ? void 0 : a.call(n, i)) || "";
//void 0 === n ==> false

//值2:null === (a = n.sign) || void 0 === a ? void 0 : a.call(n, i)
//分析值2:
//null === (a = n.sign) ==》 false
//void 0 === a ==> false
//null === (a = n.sign) || void 0 === a ==> false
//至此:值2表达式可以写为:
//false ? void 0 : a.call(n, i) ==> a.call(n, i)

//因此整个大表达式就等同于:
//a.call(n, i) || "" ==>大概率为:a.call(n, i)
//a.call(n, i) ==> window.byted_acrawler.sign.call(n,i)

执行js函数

  • 目前可以有两种方式用于执行js函数

    • 方式1:常规方式
    1
    2
    3
    4
    5
    6
    7
    //函数定义
    function sign(n,a){
    //this的指向没有发生改变
    console.log(n+a)
    }
    //函数调用
    sign('name:','bobo')
    • 方式2:call机制
      • call的作用:
        • call函数必须通过一个被定义过的函数的名字进行调用。
        • call函数有(call的调用者表示函数的参数个数+1)个参数
          • 参数1:会被赋值给call的调用者表示函数内部的this
          • 参数2,3,4 … :赋值给call的调用者表示函数的具体参数
    1
    2
    3
    4
    5
    6
    7
    //函数定义:没一个函数内部都会有一个this指针,该this就好比是python中的self
    function sign(n){
    //this = 123
    console.log(n)
    }
    //基于call机制的函数调用
    sign.call(123,456) //参数123会被赋值给sign函数内部的this,456会被赋值给sing的参数n。
  • 练习:

1
2
window.byted_acrawler.call(n,i) 等同于:window.byted_acrawler(i)
a.call(n, i) 等同于:a(i)

函数的参数

1
2
3
4
5
function sign(){
//arguments是用来接收函数所有的参数,arguments好比是python中的args
console.log(arguments) //arguments是一个数组,内部存储的是函数传递过来所有的参数
};
sign(1,2,3,4,5);

对象合并

1
2
3
4
5
v1 = {'name':'bobo'};
v2 = {'age':30};
//assign:将v2对象的数据合并到v1中。
Object.assign(v1,v2);
console.log(v1);

编译执行js代码

方式1:nodejs

1
2
3
4
5
6
//v1.js
function func(arg){
return 'hello:'+arg;
};
result = func('bobo');
console.log(result);

在终端中:

1
node v1.js

制定一个python脚本,来编译执行js程序

1
2
3
4
import subprocess
#getoutput是用来执行终端指令的
result = subprocess.getoutput('node v1.js')
print(result)

注意:至此,我们就可以通过python程序来执行js程序!

方式2:pyexecJs

可以通过python程序来执行js程序

浏览器环境的模拟(重点)

什么是nodejs

  • Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境
    • node.js是一个开发环境,可以实现在服务器端运行js代码。
  • 在 Node.js 出现之前,JavaScript 通常作为客户端程序设计语言使用,以JavaScript 写出的程序常在用户的浏览器上运行。Node.js 的出现使 JavaScript 也能用于服务端编程。
  • 某些网站的后台系统中就会使用nodejs作为后端语言。有的网站则使用python。
  • node.js环境安装成功后,则会自动安装好一个npm工具,该工具是用来管理和下载第三方的js模块,类似于python的pip。

什么是浏览器环境?为何需要它?

  • 有些时候,我们从浏览器上拷贝下来的js代码,在改写的时候会失败,终极原因就是缺少了浏览器环境。

    • 1
      2
      3
      4
      5
      function sign(){
      return navigator.userAgent
      };
      sign();
      //在发条改写工具中改写失败,但是在浏览器的console可以执行成功。
  • 浏览器环境是指,js代码运行时有可能需要依赖的代码环境。比如在浏览器开发者工具的console中可以读取到如下几个内置对象的值(这些值都是浏览器给的)

1
2
3
window,document,navigator,location等内置对象。
navigator.userAgent 就是UA等
#这些内置对象都是需要依附于浏览器才可以产生的有效的值【如果直接在发条里改写是不成功的】。

注意:有的网站的加密解密操作会借用到这些内置对象中的某些值,因此我们在实现逆向过程中缺少了这些值(没有浏览器环境)就会报错!

实现模拟浏览器环境操作

  • 环境安装:

    • nodejs开发环境(已装)

    • jsdom(nodejs中的一个模块)

      • jsdom就是用nodejs实现的用于测试的虚拟浏览器。
      • 安装:(最好使用管理员身份运行下述终端指令)
      1
      2
      3
      4
      npm install node-gyp@latest 
      npm explore -g npm -- npm i node-gyp@latest //这个是一些依赖环境的自动补充,否则直接安装jsdom可能会报错
      npm install jsdom -g
      //-g是全局安装,不携带是局部位置安装

      或者:(如果使用上述操作安装失败,则使用如下方式)

      1
      2
      3
      sudo npm install -g yarn
      sudo npm install -g tyarn
      sudo npm install -g jsdom
    • 安装canvas

    1
    npm install canvas -g

补全/模拟浏览器环境

  • 在浏览器的开发者工具的console中,会看到window这个内置对象,其中包含了如下浏览器环境中才有的对象:

    • window.navigator

    • window.document

    • window.location等等

jsdom的基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//v2.js
const jsdom = require("jsdom");
const { JSDOM } = jsdom;//导入jsdom模块
//页面内容
const html = "<!DOCTYPE html><p>Hello world</p>";
//UA伪装(不是头信息伪装)
const resourceLoader = new jsdom.ResourceLoader({
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36",
});
//创建jsdom这个虚拟的浏览器对象
const dom = new JSDOM(html,{
//伪装的内容
url: "https://www.toutiao.com",
referrer: "https://example.com/",
contentType: "text/html",
resources: resourceLoader, //UA伪装
})

const window = dom.window; // window 对象,直接获取浏览器环境中的window对象
//发现location中仅仅存储了最基本的内容,在浏览器的console中可以查看到location更多的全部内容
console.log(window.location);
console.log(window.navigator);//空
console.log(window.document);//和location同理,一般情况下此处获取的document的值足够用了

终端:

node v2.js

jsdom的完全使用(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//v3.js
const jsdom = require("jsdom");
const { JSDOM } = jsdom;//导入jsdom模块
//页面内容
const html = "<!DOCTYPE html><p>Hello world</p>";
//UA伪装(不是头信息伪装)
const resourceLoader = new jsdom.ResourceLoader({
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36",
});
const dom = new JSDOM(html,{
//伪装的内容
url: "https://www.toutiao.com",
referrer: "https://example.com/",
contentType: "text/html",
resources: resourceLoader, //UA伪装
})

window = global // window 对象 全局对象
document = dom.window.document //全局对象

//存储了location和navigator的值,这些值都可以从网站的开发者工具的console中获取
const params = {
location: {
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator: {
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "MacIntel",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
webdriver: false
}
};
//将params加入到全局变量中,变身为全局变量
Object.assign(global,params)

console.log(params.location);
console.log(params.navigator);
//或者
//console.log(location);
//console.log(navigator);

//代码作用:几乎完全模拟了一个真实的浏览器开发环境

终端:

node v2.js

global关键字

  • 在nodejs中默认会有一个global的关键字(全局变量)
1
2
3
//v4.js
v1 = 123;
console.log(global);

终端:

node v4.js

发现:global本身是一个字典,可以存储所有的全局变量。自己定义的全局变量都会存储到global这个字典中。

1
2
3
4
5
6
7
global.v1 = 'bobo';
global.v2 = 'jay';
global.v3 = {
'age':30
};
//定义了三个全局变量
console.log(v1,v2,v3.age);

具体的请求分析

  • 滚轮下滑,会加载出更多的新闻咨询数据,因此,数据的加载是基于ajax请求实现的。

  • 通过抓包工具,捕获ajax数据包,发现一个feed的数据包的响应数据中存在加载出的新闻数据。

    • url:https://www.toutiao.com/api/pc/list/feed

    • 请求方式:get

    • 请求参数:

      • 1
        2
        3
        4
        5
        6
        channel_id: 3189398999  【固定】
        max_behot_time: 1650364906 【时间戳】
        category: pc_profile_channel 【固定】
        aid: 24 【固定】
        app_name: toutiao_web 【固定】
        _signature: _02B4Z6wo00901ZogFsQAAIDA4u79wxDuXSGaBBJAAATX7cy7h0X5W7RfeqRU9vU4c57nKyTqBEVpuutMIr7033MXo70zZveziaEdhwxpiZ46YXA8.L2YZz8QdKDh-ZUBUrtzyJ4A7aXdUB1621 【签名:动态变化的,具有时效性】
    • 请求头

全局搜索_signature
  • 发现在index数据包中出现了_signature关键字,在可疑位置打断点,刷新请求确定断点:

    • 1
      2
      3
      4
      5
      var r = S(n, e);
      e.params = H(H({}, e.params), {}, {
      _signature: r
      });
      //r就是动态变化请求参数的值,这个值是由S(n, e)调用返回的,
    • S函数的内部实现

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        function S(e, t) {
        var n, r;
        var a = "".concat(location.protocol, "//").concat(location.host, "/toutiao");
        if (false)
        ;var o = {
        url: a + e
        };
        if (t.data)
        o.body = t.data;
        var i = (null === (r = null === (n = window.byted_acrawler) || void 0 === n ? void 0 : n.sign) || void 0 === r ? void 0 : r.call(n, o)) || "";
        return i //返回的i就是签名的值(动态变化的请求参数的值),在此处打上一个断点,然后是的程序走到该断点的位置,观察i是不是我们想要的签名数据。
        }
      • 处理返回值i:

        • 1
          2
          3
          4
          5
          6
          7
          var i = (null === (r = null === (n = window.byted_acrawler) || void 0 === n ? void 0 : n.sign) || void 0 === r ? void 0 : r.call(n, o)) || "";
          //上述表达式前置知识点中分析过,且得到了结果:window.byted_acrawler.sign.call(n, o)==》window.byted_acrawler.sign(o)
          //至此:
          var i = window.byted_acrawler.sign(o)
          //此处的参数o(固定值:同一个板块下是固定,不同板块是变化):
          {"url": "https://www.toutiao.com/api/pc/list/feed?offset=0&channel_id=94349549395&max_behot_time=0&category=pc_profile_channel&disable_raw_data=true&aid=24&app_name=toutiao_web"
          }
    改写sign
    • 进入到sign的实现内部,发现了sign的原始实现
    1
    2
    3
    4
    5
    6
    7
    (U = function e() {
    var f = arguments;
    return e.y > 0 ? K(b, e.c, e.l, f, e.z, this, null, 0) : (e.y++,
    K(b, e.c, e.l, f, e.z, this, null, 0)) //打断点,执行到该断点位置
    }
    //分析返回值:三目运算,发现e.y这个表达式返回true,因此返回值就是K(b, e.c, e.l, f, e.z, this, null, 0)。
    //因此其实sign函数返回值是有K函数返回的,因此只需要改写K函数即可!
    • 改写K,会发现K的参数比较多,每一个参数的生成机制都很复杂,函数的调用栈比较深。

    • 测试:直接将当前K函数所在的js文件中的代码全部复制,保存到一个js文件中(v20.js),在创建一个html文件(v20_face.html)实现简易化的改写,是否可以成功:

      • v20_face.html

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script src="v20.js"></script>
        </head>
        <body>

        </body>
        </html>
        • 直接在浏览器中执行html,在浏览器的console可以看到window.byted_acrawler.sign返回的结果,因此基于浏览器环境,js代码改写成功了!
补充浏览器环境
  • 大致观测改写的代码

  • 1
    2
    3
    4
    5
    var glb;
    //函数定义
    (glb = "undefined" == typeof window ? global : window)._$jsvmprt = function(b, e, f) {xxx}
    ,//函数调用
    (glb = "undefined" == typeof window ? global : window)._$jsvmprt('xxx',['xx','xx',...],undefined)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//v30.js
const jsdom = require("jsdom");
const { JSDOM } = jsdom;//导入jsdom模块
//页面内容
const html = "<!DOCTYPE html><p>Hello world</p>";
//UA伪装(不是头信息伪装)
const resourceLoader = new jsdom.ResourceLoader({
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36",
});
const dom = new JSDOM(html,{
//伪装的内容
url: "https://www.toutiao.com",
referrer: "https://example.com/",
contentType: "text/html",
resources: resourceLoader, //UA伪装
})

window = global // window 对象 全局对象
document = dom.window.document //全局对象

//存储了location和navigator的值,这些值都可以从网站的开发者工具的console中获取
const params = {
location: {
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator: {
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "MacIntel",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
webdriver: false
}
};
//将params加入到全局变量中,变身为全局变量
Object.assign(global,params)

//改写代码
var glb;
//函数定义
(glb = "undefined" == typeof window ? global : window)._$jsvmprt = function(b, e, f) {xxx}
,//函数调用
(glb = "undefined" == typeof window ? global : window)._$jsvmprt('xxx',['xx','xx',...],undefined)
  • 通过node运行改写好后的js程序会报错:

    • export是nodejs中的一个关键字,将外部文件中的方法作用在当前的js代码中(好比是python的import)

    • 修改js代码:

      • 将函数的第二个参数列表中第三个元素(携带了export关键字的语句:”undefined” != typeof exports ? exports : void 0)该语句在浏览器console中执行返回的是undefine,因此在代码中就可以将该语句改为void 0(undefine),从而就不出现exports关键字。程序运行就没问题了。

      • python脚本获取签名

      • 1
        2
        3
        4
        import subprocess
        url = 'https://www.toutiao.com/api/pc/list/feed?offset=0&channel_id=94349549395&max_behot_time=0&category=pc_profile_channel&disable_raw_data=true&aid=24&app_name=toutiao_web'
        signature = subprocess.getoutput('node v30.js %s'%url)
        print(signature)