【图灵Python爬虫逆向】题七:千山鸟飞绝

题目背景

题目地址:https://stu.tulingpyton.cn/problem-detail/7/

这一题为中等难度

打开控制台时会发现进入无限debug,可以通过右键点击"一律不在此处暂停"来绕过这个障碍。
图片

一、请求与响应分析

1. 请求参数分析

首先观察网络请求,发现请求中包含一个加密参数x
图片

图片

同时注意到响应数据也是加密的,这意味着后续还需要进行解密操作。

2. Cookie分析

接下来检查Cookie,看是否存在异常值:
图片

图片

经检查,Cookie中没有发现特殊的加密参数。

3. Headers分析

继续观察请求头部信息:
图片

图片

在Headers中发现两个会变化的关键值:

  • Ts:明显是时间戳
  • M:某种加密参数

二、调用栈分析

查看调用栈,寻找加密逻辑的源头:
图片

根据经验,前两个jQuery相关的调用可以忽略,重点关注中间三个pagination7.js的调用。首先跟进loadPage函数,发现代码已被混淆:

图片

在代码中随机打一个断点开始调试。通过鼠标悬停查看混淆代码的内容,发现这里是有关请求URL构造的代码,但请求参数中只有page,没有我们之前看到的x参数,说明这里不是最终URL生成的地方。

图片

三、定位加密参数生成位置

1. 使用XHR断点定位

使用XHR断点,监听包含https://stu.tulingpyton.cn/api/problem-detail/7/data/?page=1&x=的请求URL,重新调试:
图片

成功跳转到URL中存在x参数的位置。从请求参数中可以看到,这里不仅有x参数,还有我们之前在headers中发现的加密参数m和时间戳ts

2. 追踪参数来源

向前分析堆栈,一直找到最先出现x的栈(_0x210282.<computed> (pagination7.js:1)),在此处打断点继续调试。

分析发现_0x2e86e1中保存的是请求的各种参数:
图片

既然在之前的堆栈中loadPage直接跳转到了当前脚本,说明请求参数必然在当前脚本中被处理。使用Ctrl+F搜索第一次出现_0x2e86e1的位置,打上断点继续调试:

图片

3. 定位URL处理代码

跟踪调试直到下方代码处,发现到这里为止URL仍是原始的,而下方是之前已打过断点的URL生成后的位置,说明在这个函数体内对URL进行了处理:

图片

关键代码如下:

return _0x2683e4[_0x486427(0x1ff)](_0x1646e9 => {
   
   
    const _0x507fcb = _0x486427;
    _0x5e001c[_0x507fcb(0x20e)](_0x5e001c[_0x507fcb(0x195)],   _0x5e001c['\x53\x62\x53\x55\x6f']) ? _0x1d75db(_0x37f599) : _0x2e86e1 = _0x5e001c['\x64\x66\x64\x59\x79'](_0x1646e9, _0x2e86e1) || _0x2e86e1;
}

四、请求拦截器分析

1. 发现拦截器

仔细观察代码,找到关键部分:_0x2e86e1 = _0x5e001c['\x64\x66\x64\x59\x79'](_0x1646e9, _0x2e86e1) || _0x2e86e1;

图片

鼠标悬停查看_0x5e001c['\x64\x66\x64\x59\x79']函数的定义:

图片

发现这个函数的作用是将第一个参数作用在第二个参数上,即_0x1646e9(_0x2e86e1)。跟进_0x1646e9函数,又回到了pagination7.js中:

图片

这是一个名为addRequestInterceptor的函数——一个请求拦截器!这解释了为什么最初的URL在发送时被修改:它被这个拦截器拦截并进行了额外处理。

2. 拦截器代码分析

打上断点,查看完整代码:

$['\x61\x64\x64\x52\x65\x71\x75\x65\x73\x74\x49\x6e\x74\x65\x72\x63\x65\x70\x74\x6f\x72'](function(_0x2410d2) {
   
   
    const _0x489b98 = a0_0x1d6583
      , _0x228f9a = {
   
   
        '\x43\x47\x72\x41\x6a': function(_0x58264f, _0x5e62f3) {
   
   
            return _0x58264f + _0x5e62f3;
        },
        '\x77\x43\x58\x7a\x45': function(_0x33cf1b, _0x3d2697) {
   
   
            return _0x33cf1b(_0x3d2697);
        }
    };
    let _0x2498e1 = new Date()['\x67\x65\x74\x54\x69\x6d\x65']()
      , _0x48003c = window['\x65\x65\x65\x65'](_0x228f9a[_0x489b98(0x24d)](_0x489b98(0x1b2), _0x2498e1));
    return _0x2410d2[_0x489b98(0x205)] = _0x2410d2[_0x489b98(0x205)] || {
   
   },
    _0x2410d2[_0x489b98(0x205)]['\x6d'] = _0x48003c,
    _0x2410d2[_0x489b98(0x205)][_0x228f9a['\x43\x47\x72\x41\x6a']('\x74', '\x73')] = _0x2498e1,
    _0x2410d2['\x75\x72\x6c'] += _0x489b98(0x251) + _0x228f9a[_0x489b98(0x257)](encodeURIComponent, dd['\x61']['\x53\x48\x41\x32\x35\x36'](_0x228f9a['\x43\x47\x72\x41\x6a'](_0x48003c, '\x78\x78\x6f\x6f'))),
    _0x2410d2;
})

通过分析可以看出:

  • _0x2498e1后面跟着new Date(),显然是一个时间戳
  • _0x228f9a[_0x489b98(0x24d)]的功能是将参数拼接成字符串,两个参数分别是_0x489b98(0x1b2)(固定值xialuo)和时间戳_0x2498e1
  • \x6dm的意思,_0x2410d2[_0x489b98(0x205)]['\x6d'] = _0x48003c是对m参数赋值
  • window['\x65\x65\x65\x65']是加密方法

3. 加密函数分析

加密函数的定义如下:

window['\x65\x65\x65\x65'] = function(_0x4e41fb, _0x151d45, _0x21b59a) {
   
   
    const _0x43632d = {
   
   
        '\x73\x62\x53\x65\x71': function(_0x5c8b90, _0xd55cb3) {
   
   
            return _0x5c8b90(_0xd55cb3);
        },
        '\x45\x43\x74\x6d\x44': function(_0x4a67ff, _0x118540) {
   
   
            const _0x490314 = a0_0x26e9;
            return _0x8103a1[_0x490314(0x266)](_0x4a67ff, _0x118540);
        },
        '\x6f\x55\x5a\x43\x4a': _0x8103a1['\x79\x49\x49\x69\x49'],
        '\x6c\x50\x4f\x63\x67': function(_0x4f9718, _0x2b4281) {
   
   
            return _0x4f9718 === _0x2b4281;
        },
        '\x4b\x72\x74\x57\x76': '\x4d\x54\x70\x76\x6f',
        '\x4b\x4a\x53\x68\x49': function(_0x170523, _0x569ee6, _0x14f401) {
   
   
            return _0x8103a1['\x70\x50\x48\x53\x6a'](_0x170523, _0x569ee6, _0x14f401);
        }
    };
    return _0x151d45 ? _0x21b59a ? _0x8103a1['\x70\x4f\x52\x47\x4b'](_0x5333d7, _0x151d45, _0x4e41fb) : function(_0x32e6c1, _0x317277) {
   
   
        const _0x41d125 = a0_0x26e9;
        return _0x43632d['\x6c\x50\x4f\x63\x67'](_0x41d125(0x22e), _0x43632d[_0x41d125(0x1d3)]) ? _0x43632d['\x73\x62\x53\x65\x71'](_0xf4482e, _0x43632d['\x45\x43\x74\x6d\x44'](_0x43632d[_0x41d125(0x282)](_0x41d125(0x1ab), _0x45a923), _0x43632d[_0x41d125(0x19f)])) : _0x43632d['\x73\x62\x53\x65\x71'](_0x4646b5, _0x43632d['\x4b\x4a\x53\x68\x49'](_0x5333d7, _0x32e6c1, _0x317277));
    }(_0x151d45, _0x4e41fb) : _0x21b59a ? _0x8103a1['\x58\x76\x66\x4c\x51'](_0x3fdbf5, _0x4e41fb) : function(_0x5beac5) {
   
   
        const _0x3f75fe = a0_0x26e9;
        return _0x43632d[_0x3f75fe(0x277)](_0x4646b5, _0x43632d[_0x3f75fe(0x277)](_0x3fdbf5, _0x5beac5));
    }(_0x4e41fb);
}

经过分析,实际执行的代码是:

function(_0x5beac5) {
   
   
    const _0x3f75fe = a0_0x26e9;
    return _0x43632d[_0x3f75fe(0x277)](_0x4646b5, _0x43632d[_0x3f75fe(0x277)](_0x3fdbf5, _0x5beac5));
}(_0x4e41fb);

简化后就是:return _0x4646b5(_0x3fdbf5(_0x5beac5))

将相关函数提取出来,构建Python脚本运行,成功返回数据:
图片

五、响应数据解密分析

解密部分相对简单。回到最初发送请求的'\x73\x75\x63\x63\x65\x73\x73'(success)部分,打上断点:
图片

运行到断点处,查看数据:
图片

发现数据已被解密,继续查看堆栈回溯,发现还有一个响应数据拦截器:
图片

解密的核心代码如下:

const _0x10319d = _0x50770d[_0x4cb5c4(0x1f5)](xxxxoooo, _0x3dd6df['\x72'])
    , _0x3f6d4e = x1['\x70\x61\x72\x73\x65'](_0x10319d);
return _0x3f6d4e;

主要使用的是xxxxoooo函数,其中密钥设置如下:

let kkkk = dd['\x61']['\x65\x6e\x63']['\x55\x74\x66\x38'][a0_0x1d6583(0x279)]('\x78\x78\x78\x78\x78\x78\x78\x78\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f')
  , iiii = dd['\x61'][a0_0x1d6583(0x213)]['\x55\x74\x66\x38'][a0_0x1d6583(0x279)]('\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x41\x42\x43\x44\x45\x46');

提取相关函数,补充环境,即可完成对响应数据的解密。

最后~~~

复盘一下下

这道题目虽然标为中等难度,但作为JS逆向入门者确实有些挑战。整个过程就像一场侦探游戏,从请求参数和响应数据的加密现象出发,层层深入寻找蛛丝马迹。

最有价值的经验是学会了如何定位加密点。通过分析网络请求、设置断点、追踪调用栈,最终发现了关键的请求拦截器。这让我意识到,现代网站通常不会在明面上直接加密,而是通过各种拦截器、中间件在请求发送前或响应接收后悄悄处理数据。

对于混淆代码,不必一开始就试图理解全部。可以先找到关键变量(如本题中的时间戳、加密参数x和m),然后顺藤摸瓜,借助浏览器的调试工具(如变量悬停查看)逐步定位核心逻辑。最终,复杂的加密过程其实就是_0x4646b5(_0x3fdbf5(_0x5beac5))这样简单的函数嵌套。

JS逆向不是只靠猜测和运气,而是需要耐心、观察力和逻辑思维。与其盲目尝试,不如沉下心来一步步分析,让代码自己"说话"。

下面附上完整的pythonjs代码

完整Python代码

import requests
import execjs
import time

cookies = {
   
   
    'Hm_lvt_b5d072258d61ab3cd6a9d485aac7f183': '1744098732',
    'HMACCOUNT': '764A7B05229BE584',
    'sessionid': '94q4ek0xqrop8nhln9ndop819vwea79m',
    'Hm_lpvt_b5d072258d61ab3cd6a9d485aac7f183': '1744225027',
}

with open('answer.js', 'r') as f:
    js = f.read()

ctx = execjs.compile(js)
sum = 0
for page in range(1, 21):
    ts = str(int(time.time() * 1000))
    url = ctx.call('getUrl', ts,page)
    headers =  ctx.call('getHeaders', ts)
    response = requests.get(url, cookies=cookies, headers=headers)
    encrypted_data = response.json()['r']
    real_data =ctx.call('decode',encrypted_data)
    for item in real_data:
        sum += item
print(sum)

完整JS代码

const CryptoJS = require('crypto-js');

//======================  混淆随机乱序数组初始化  =======================//
(function(_0x1e2fce, _0x8a1a01) {
   
   
    const _0x41e328 = a0_0x26e9
      , _0x14caa7 = _0x1e2fce();
    while (!![]) {
   
   
        try {
   
   
            const _0x506482 = -parseInt(_0x41e328(0x1d2)) / 0x1 + parseInt(_0x41e328(0x298)) / 0x2 * (parseInt(_0x41e328(0x1de)) / 0x3) + parseInt(_0x41e328(0x269)) / 0x4 + -parseInt(_0x41e328(0x1ae)) / 0x5 * (-parseInt(_0x41e328(0x1a2)) / 0x6) + -parseInt(_0x41e328(0x20f)) / 0x7 + parseInt(_0x41e328(0x227)) / 0x8 * (parseInt(_0x41e328(0x1e1)) / 0x9) + parseInt(_0x41e328(0x1ec)) / 0xa * (parseInt(_0x41e328(0x1ad)) / 0xb);
            if (_0x506482 === _0x8a1a01)
                break;
            else
                _0x14caa7['push'](_0x14caa7['shift']());
        } catch (_0x34b90b) {
   
   
            _0x14caa7['push'](_0x14caa7['shift']());
        }
    }
}(a0_0x23a7, 0x61b63));
_0x8103a1 = {
   
   
    '\x58\x76\x66\x4c\x51': function(_0x1f16eb, _0x4b287c) {
   
   
        return _0x1f16eb(_0x4b287c);
    },
    '\x59\x73\x69\x78\x4e': _0x257c98(0x268),
    '\x77\x4c\x4d\x74\x4b': '\x62\x6c\x74\x43\x49',
    '\x7a\x4c\x4a\x4a\x67': _0x257c98(0x1af),
    '\x6b\x68\x4a\x5a\x5a': function(_0x3a97a1, _0x2c50cf) {
   
   
        return _0x3a97a1 === _0x2c50cf;
    },
    '\x41\x50\x6c\x43\x53': _0x257c98(0x21e),
    '\x47\x6e\x6d\x52\x68': '\x28\x28\x28\x2e\x2b\x29\x2b\x29\x2b\x29\x2b\x24',
    '\x69\x6b\x78\x44\x62': function(_0x3e221e, _0x422cb4) {
   
   
        return _0x3e221e + _0x422cb4;
    },
    '\x43\x41\x4d\x6d\x6c': function(_0x35dcf5, _0x2e0bb8) {
   
   
        return _0x35dcf5 & _0x2e0bb8;
    },
    '\x54\x50\x4d\x46\x46': function(_0x3f988e, _0x239cba) {
   
   
        return _0x3f988e | _0x239cba;
    },
    '\x42\x69\x67\x69\x49': function(_0x5f4291, _0x2990f6) {
   
   
        return _0x5f4291 << _0x2990f6;
    },
    '\x41\x41\x67\x4e\x64': function(_0x28507a, _0x5694d2) {
   
   
        return _0x28507a + _0x5694d2;
    },
    '\x6c\x62\x72\x6c\x48': function(_0x4a448b, _0x4dd77a) {
   
   
        return _0x4a448b & _0x4dd77a;
    },
    '\x7a\x49\x6c\x74\x59': '\x58\x6a\x4a\x61\x44',
    '\x6e\x76\x62\x71\x4c': '\x71\x4d\x79\x64\x76',
    '\x4d\x55\x63\x56\x4b': function(_0x3e9328, _0x23e1e6) {
   
   
        return _0x3e9328 | _0x23e1e6;
    },
    '\x49\x52\x76\x44\x48': function(_0x1d2d3b, _0xf5c68b) {
   
   
        return _0x1d2d3b >>> _0xf5c68b;
    },
    '\x70\x6c\x6c\x76\x51': function(_0x4db1a6, _0x1fd946) {
   
   
        return _0x4db1a6 - _0x1fd946;
    },
    '\x4f\x47\x5a\x57\x65': function(_0x14c2b1, _0x496c4b, _0x40c400) {
   
   
        return _0x14c2b1(_0x496c4b, _0x40c400);
    },
    '\x76\x71\x71\x54\x4b': function(_0x3b7af4, _0x5127e5, _0x3a4e1f) {
   
   
        return _0x3b7af4(_0x5127e5, _0x3a4e1f);
    },
    '\x43\x7a\x64\x6e\x52': function(_0x293a2c, _0x5e65c3, _0x41015a) {
   
   
        return _0x293a2c(_0x5e65c3, _0x41015a);
    },
    '\x51\x4a\x76\x4b\x72': '\x47\x45\x54',
    '\x6f\x62\x4b\x44\x6c': _0x257c98(0x220),
    '\x6c\x45\x6a\x65\x6c': '\x62\x52\x65\x64\x6e',
    '\x6c\x4f\x77\x4a\x54': function(_0x25f36e, _0x1febd2, _0x2ddfb9, _0x577c6b, _0x5edabc, _0x3d8127, _0x5ca917) {
   
   
        return _0x25f36e(_0x1febd2, _0x2ddfb9, _0x577c6b, _0x5edabc, _0x3d8127, _0x5ca917);
    },
    '\x43\x65\x6c\x75\x57': function(_0xd305a, _0x36abdf) {
   
   
        return _0xd305a | _0x36abdf;
    },
    '\x4e\x6a\x50\x42\x49': function(_0x3d861e, _0x312c8d) {
   
   
        return _0x3d861e >>> _0x312c8d;
    },
    '\x49\x59\x4f\x49\x6d': function(_0x354fdc, _0x19cd48) {
   
   
        return _0x354fdc !== _0x19cd48;
    },
    '\x53\x4b\x64\x58\x66': _0x257c98(0x28b),
    '\x73\x4b\x65\x63\x4f': function(_0x39d26d, _0xed3be3) {
   
   
        return _0x39d26d | _0xed3be3;
    },
    '\x53\x52\x73\x70\x4c': function(_0x408734, _0x373b2c) {
   
   
        return _0x408734 & _0x373b2c;
    },
    '\x72\x6c\x52\x61\x62': function(_0x516b91, _0x3daee5) {
   
   
        return _0x516b91 ^ _0x3daee5;
    },
    '\x4f\x62\x45\x62\x78': function(_0x5481d1, _0x352c81) {
   
   
        return _0x5481d1 ^ _0x352c81;
    },
    '\x76\x56\x57\x59\x4d': function(_0xaf14f6, _0x4a842f) {
   
   
        return _0xaf14f6 | _0x4a842f;
    },
    '\x70\x65\x4e\x69\x44': '\x78\x69\x61\x6c\x75\x6f',
    '\x4d\x4d\x42\x59\x41': function(_0x3ec662, _0x3b92dc) {
   
   
        return _0x3ec662 + _0x3b92dc;
    },
    '\x48\x7a\x59\x76\x72': function(_0x4fa6e6, _0x4aaa9c) {
   
   
        return _0x4fa6e6 === _0x4aaa9c;
    },
    '\x7a\x45\x57\x43\x75': _0x257c98(0x1b0),
    '\x6e\x62\x68\x6b\x7a': _0x257c98(0x258),
    '\x56\x6f\x66\x62\x73': _0x257c98(0x1c6),
    '\x51\x44\x6c\x77\x78': functio
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luck_ff0810

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值