【js逆向学习】新手视角破解「广东省公共资源交易平台」

声明:目的是用于记录逆向思路,而非提供结果。仅供学习参考,请勿滥用爬虫


前言

学习js逆向,破解广东省公共资源交易平台,详细记录个人探索、踩坑全过程。
网站地址: aHR0cHM6Ly95Z3AuZ2R6d2Z3Lmdvdi5jbi8jLzQ0L2p5Z2c=


一、前戏

首先开始,我个人处理这种文本数据采集基本流程都是这样:

  • 检查页面、清除cookie和请求记录、重新刷新页面
  • 定位需要的数据请求接口
  • 复制接口curl, 转成python请求尝试能否正常使用request/等方式请求
  • 关键步骤
    • 看响应内容是否需要解密->先处理解密
    • 看是否需要特殊cookie、headers、param参数加密生成
  • 编写完整代码

这里我们打开链接、重新刷新后,直接用关键文本去搜(比如标题、链接),这里很显然直接就能搜索出结果:在这里插入图片描述
于是我们就能容易定位到/v2/items 接口,响应数据没有加密,包含了我们想要的内容。
复制curl,通过https://spidertools.cn/#/curl2Request转成python-request请求
在这里插入图片描述
执行后能看到正常请求成功(如果403 就是已经过了一会, 已经失效了,重新拿一个就可以)
这里大致看一下请求,很显然包含了cookie、headers中也有些特殊的东西,表单参数没什么特别的

二、cookie生成

尝试将cookie 删除后会直接导致403,所以我们先看下cookie的生成逻辑

  1. cookie中包含两个值,_horizon_sid和_horizon_uid,全局搜索一下。搜出来只有一个结果,但是可以看到应该是做了映射,那就换成cookie_key_sid、cookie_key_uid重新搜在这里插入图片描述
  2. 重新搜索,在结果中能够比较明显看出,cookie_key_sid生成的关键逻辑就是genUUID()方法在这里插入图片描述
  3. 同样的cookie_key_uid在这里插入图片描述
  4. 直接搜或者打断点再跳转到该方法,比较明显能看出来其实就是用来生成了一些特定格式的随机字符串
    在这里插入图片描述
  5. 那么理论上来说 固定cookie也是可以的、或者自己保持格式随便改些数,也没问题。当然这里如果追求严谨,就按它的逻辑抠下来,也完全没问题(只能确定客户端生成逻辑,但在不知道后端校验逻辑的情况下,可以选择保守的方式。只是通常情况下都不需要考虑这么多)。看个人选择

三、headers生成关键函数定位

首先观察headers, 可以比较明显看出来,其中包含几个特别的参数在这里插入图片描述
第一步还是先尝试删除,看是否是必需的。实际尝试后发现这几个都是必需的。
其中X-Dgi-Req-App明显是固定的,X-Dgi-Req-Nonce看起来可能是随机的,X-Dgi-Req-Signature不知道是什么,X-Dgi-Req-Timestamp时间戳就不用说了。所以我们重点确定Nonce和Signature的生成逻辑。

  • 先尝试直接全局搜Signature,啥都找不到。搜其他的也是
  • 可以先尝试搜索/items定位,确实可以找到一处。但是打上断点调试,调用栈太多了很难看明白,还是尝试换个办法
    (以下是header生成核心方法的定位步骤,比较琐碎可以跳过)

  • 尝试打上XHR断点,刷新后断在send处在这里插入图片描述

  • 然后通过调用堆栈往前找,可以看到这里前几个都是Promise相关的执行逻辑(这里要简单了解Promise即可)。前两个都可以看到header已经生成好了。再往前看到异步调用栈,打个断点看看。在这里插入图片描述

  • 这里如果简单理解下代码,可以看出是实现了一个串行异步操作,通过then调用可以看出是依次执行h中的函数在这里插入图片描述
    简单从python角度理解就是类似于(并不准确):

    • 定义了一个函数列表 h : 包含[strip, split, sort ];定义一个c, 初始是"abcdefg";然后就是执行"abcdefg".strip().split().sort()。只是这里是异步的

    这里核心在于,我们可以看出c初始是一个Promise对象,通过Promise.resolve(n)定义,而这里n就是一个请求对象的东西在这里插入图片描述
    并且从中我们看到这里的headers中还尚未包含我们想要的Signature什么的。
    当然这里也会发现请求地址「url」并不是我们想要的,因为我们刷新了页面,其他接口请求前也走到了这里,并不是请求/item时断住的。
    但是可以敏锐的察觉到这里或许就是一些请求发起前的预处理,并且在执行h中的某个函数后,就向请求headers中添加了一些东西。
    当然我们就需要验证一下, 验证方式就是:

    • 这里先打个条件断点,让它只有在请求接口是/item时才断住,看下这时headers中有没有Signature
    • 如果没有,再然后从这里执行下去,看执行后,headers中会不会有Signature

    取消原来的断点,右键点击在原位置改为打上条件断点。在这里插入图片描述 在这里插入图片描述
    for循环后也打上个断点,可以是return的位置。方便直接跳到最后
    这里因为c是Promise链并不能直接看到c的结果,可以逐步向下走,到send处,才能确定。
    或者也可以在h列表中的每个函数都打上断点,这样执行到这几个函数时,就可以看到每个函数执行的内容,是否有关于headers的操作。(事实上h的第一个函数执行u(o)就是)在这里插入图片描述
    这里过程琐碎,不再细致说明

  • 除了通过打上XHR断点,其实还有其他方式。比如我们发现headers中其实包含时间戳Timestamp,那么可以通过搜索new Date、或者Date.now()。比如这里搜索Date.now()在这里插入图片描述

    • 可以看到有一堆结果,实际上定位起来也比较麻烦,就是分析代码、打断点,也比较费时间

  • 通过以上方法,或者其他方式,最终我们就可以定位到下面这个函数u(),接下来就是抠这个函数了在这里插入图片描述

四、headers生成关键函数逆向

关掉其他断点,只在关键函数u(o)中打个断点,重新刷新页面,断住后,可以看到传入的o是个请求对象,但是url并不是/item。
所以同样,这里我们将普通断点改为条件断点o.url.includes(‘/items’) ,重新刷新

向下走几步,可以看出前面之所以没办法直接搜索出X-Dgi-Req-App是因为是通过qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 54, 25, 25])这种方式实现的,在这里插入图片描述
向下继续走到最后,可以大致分析出以下内容:

  • X-Dgi-Req-App 固定
  • X-Dgi-Req-Nonce 通过hne(16)方法生成
  • 对应/item接口是post请求,只会走到else执行
  • X-Dgi-Req-Signature 通过t1方法生成
    • 传入四个参数:p是将字典参数转成固定url参数格式,其余就是其他的App、Nonce和固定的’k8tUyS$m’
    • 但t1此处结果并不是字符串而是个init对象,包含个words数组

先按整体逻辑将js代码写下来, 整理一下, 删除明显不需要的东西, 补上明显可以看出的东西

function u() {
        // _o.inc();
        const  a = Date.now()
          , l = hne(16)
          , c = 'k8tUyS$m'
          , d = {
            'X-Dgi-Req-App': 'ggzy-portal',
            'X-Dgi-Req-Nonce': l,
            'X-Dgi-Req-Timestamp': a
        };
        const p = t1({
            p: 'type=trading-type&openConvert=false&keyword=&siteCode=44&secondType=A&tradingProcess=&thirdType=%5B%5D&projectType=&publishStartTime=&publishEndTime=&pageNo=1&pageSize=10',
            t: a,
            n: l,
            k: c
        });
        d['X-Dgi-Req-Signature'] = p
        return d
    }
console.log(u())

关键就是其中的hne()和t1()方法。重新刷新,开始补
hne()方法没什么特殊的,跳转进入后把对应的缺失的函数和变量补上。会发现其实就是个生成固定长度的随机英文+数字字符串(所以其实固定写死也可以)在这里插入图片描述

function dne(e, t) {
    switch (arguments.length) {
    case 1:
        return parseInt(Math.random() * e + 1, 10);
    case 2:
        return parseInt(Math.random() * (t - e + 1) + e, 10);
    default:
        return 0
    }
}
const lF = "zxcvbnmlkjhgfdsaqwertyuiop0987654321QWERTYUIOPLKJHGFDSAZXCVBNM"
  , fne = lF + "-@#$%^&*+!";
function hne(e) {
    return [...Array(e)].map(()=>lF[dne(0, 61)]).join("")
}

t1()函数逆向

t1()函数内部处理相对复杂,这里有好几种方式处理,更简单的和更原始的,这里列举两个例子

  1. 直接发现是SHA256加密,用SHA256算法替换原来的逻辑
    跳转到t1,发现uK函数,其参数就是将已知内容拼接成字符串。pne()方法没什么特别的,直接补就行。
    在这里插入图片描述
    进入uK发现实际调用是_createHelper,搜一下,找到_createHelper,发现是赋值给SHA256方法
    在这里插入图片描述
    在这里插入图片描述
    这里不管具体逻辑怎么嵌套,已经可以猜测,此处uK实际上就是sha256加密,加密参数也很清晰。那就直接写一个sha256加密试试,对比结果看看
    比如用python写一个
import hashlib
def calculate_sha256(message):
    sha256_hash = hashlib.sha256()
    sha256_hash.update(message.encode('utf-8'))
    return sha256_hash.hexdigest()
# 这个message就是u + o + decodeURIComponent(r) + n拼接的
message = 'HKO6UceFvo8xZALuk8tUyS$mkeyword=&openConvert=false&pageNo=1&pageSize=10&projectType=&publishEndTime=&publishStartTime=&secondType=A&siteCode=44&thirdType=[]&tradingProcess=&type=trading-type1712116609325'

sha256_digest = calculate_sha256(message)
print("SHA-256摘要:", sha256_digest)
# 输出:SHA-256摘要: 12d9f16fef71a00c4dd87d50580de92917f2bc8382fd173d281e724c2a791942

浏览器上直接执行到最后,对比下最终请求包含的Signature,显然就是一样的在这里插入图片描述
这里还有个要注意的点是,如果调试时间太长,发起请求是网页会报错什么时间不一致,是因为请求对时间戳有校验超过一定时间差就不行了,重新刷新即可。并且重新刷新前,我们要仅保留最开始的条件断点,使其只有在构造/item接口请求时才断住,不然其他请求走到这也断住了,会产生干扰。条件断点断住后再打开其他的即可在这里插入图片描述

到这里,t1()方法就很清晰了,那就没什么难点的,用js代码实现或者python代码实现都行。
这里就不贴了。

  1. 通用方式,不管是什么加密,一步步补上
    上述使用sha256加密替换的方式,并不适用于所有网站,或者说即使是我们也不一定都能发现。如果是实际开发,当然是怎么简单怎么来,但这里如果我们目的是学习逆向处理思路,还可以参考以下过程
    还是先走到uK方法内,从这里开始处理在这里插入图片描述
    这里可以看到创建了一个v.init实例化对象,E是undefined。然后调用finalize。
    这里也有两种方式,可以抠出完整的v对象,也可以单纯顺着finalize方法抠。这里就单纯从finalize方法开始抠。
    不管v.init(E)。直接看finalize,进入finalize方法,打上断点,执行进去在这里插入图片描述
    可以看到这里实际执行了_append方法。进入_append方法。在这里插入图片描述
    同样的,接下来进入m.parse,再从m.parse进入h.parse,其中就是对v的处理,然后又创建一个d.init对象,d.init创建了一个包含v、y值的对象,对应word和sigBytes的对象。把这个方法单独写出来
    在这里插入图片描述

    到这里先把代码写一下,目前是梳理到_append()方法

function init(v, y) {
    v = this.words = v || [],
    y != undefined ? this.sigBytes = y : this.sigBytes = v.length * 4
}
function h_parse(v) {
    for (var y = v.length, E = [], B = 0; B < y; B++)
        E[B >>> 2] |= (v.charCodeAt(B) & 255) << 24 - B % 4 * 8;
    return new init(E,y)
}
function m_parse(v) {
    return h_parse(unescape(encodeURIComponent(v)))
}
var _data = new init
var _nDataBytes = 0
function _append(v) {
    typeof v == "string" && (v = m_parse(v)),
    _data = v,// this._data也是new d.init,创建的内容值为空,这里没有拿到concat, 直接赋值跟concat效果一样
    _nDataBytes += v.sigBytes
}
function finalize(v) {
    v && _append(v);
    var y = _doFinalize();
    return y
}
function uK(y, E) {
        return finalize(y)
    }
function pne(e) {
    let t = "";
    return typeof e == "object" ? t = Object.keys(e).map(n=>`${n}=${e[n]}`).sort().join("&") : typeof e == "string" && (t = e.split("&").sort().join("&")),
    t
}
function t1(e={}) {
    const {p: t, t: n, n: u, k: o} = e
      , r = pne(t);
    return uK(u + o + decodeURIComponent(r) + n)
}

这里关键就是_data、_nDataBytes、init方法的定义
然后就是_doFinalize(), 依次补齐_process()、以及其中_doProcessBlock等等方法,还有_hash什么的。太过琐碎,具体就不再仔细列举,缺什么就补上即可。


u()方法补完,执行后得到的结果以下这种格式,其中Signature需要进一步转换

{
  'X-Dgi-Req-App': 'ggzy-portal',
  'X-Dgi-Req-Nonce': '7wEVLIF6qYU4QwVV',
  'X-Dgi-Req-Timestamp': 1712125314880,
  'X-Dgi-Req-Signature': init {
    words: [
        806825146, 842258491,
       2032025445, 274988583,
      -1296157461, 752774242,
      -1482948191, 591972000
    ],
    sigBytes: 32
  }
}

往后u()方法后继续执行,定位Signature转换逻辑,从后续Qn.from(e.headers)进入、最后会找到一个Da(): return e === !1 || e == null ? e : Y.isArray(e) ? e.map(Da) : String(e)就是核心方法了
(这里偷懒不再详细写调试过程了,太累了)

五、完整代码

js代码


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

const lF = "zxcvbnmlkjhgfdsaqwertyuiop0987654321QWERTYUIOPLKJHGFDSAZXCVBNM"

  , fne = lF + "-@#$%^&*+!";
function qu(e=[]) {
    return e.map(t=>fne[t]).join("")
}
function dne(e, t) {
    switch (arguments.length) {
    case 1:
        return parseInt(Math.random() * e + 1, 10);
    case 2:
        return parseInt(Math.random() * (t - e + 1) + e, 10);
    default:
        return 0
    }
}
function hne(e) {
    return [...Array(e)].map(()=>lF[dne(0, 61)]).join("")
}
function pne(e) {
    let t = "";
    return typeof e == "object" ? t = Object.keys(e).map(n=>`${n}=${e[n]}`).sort().join("&") : typeof e == "string" && (t = e.split("&").sort().join("&")),
    t
}
function init(v, y) {
    v = this.words = v || [],
    y != undefined ? this.sigBytes = y : this.sigBytes = v.length * 4
}


var d = []
function _doProcessBlock(p, h) {
    for (var m = _hash.words, C = m[0], g = m[1], v = m[2], y = m[3], E = m[4], B = m[5], A = m[6], _ = m[7], O = 0; O < 64; O++) {
        if (O < 16)
            d[O] = p[h + O] | 0;
        else {
            var P = d[O - 15]
              , L = (P << 25 | P >>> 7) ^ (P << 14 | P >>> 18) ^ P >>> 3
              , ue = d[O - 2]
              , N = (ue << 15 | ue >>> 17) ^ (ue << 13 | ue >>> 19) ^ ue >>> 10;
            d[O] = L + d[O - 7] + N + d[O - 16]
        }
        var H = E & B ^ ~E & A
          , Q = C & g ^ C & v ^ g & v
          , W = (C << 30 | C >>> 2) ^ (C << 19 | C >>> 13) ^ (C << 10 | C >>> 22)
          , R = (E << 26 | E >>> 6) ^ (E << 21 | E >>> 11) ^ (E << 7 | E >>> 25)
          , I = _ + R + H + c[O] + d[O]
          , te = W + Q;
        _ = A,
        A = B,
        B = E,
        E = y + I | 0,
        y = v,
        v = g,
        g = C,
        C = I + te | 0
    }
    m[0] = m[0] + C | 0,
    m[1] = m[1] + g | 0,
    m[2] = m[2] + v | 0,
    m[3] = m[3] + y | 0,
    m[4] = m[4] + E | 0,
    m[5] = m[5] + B | 0,
    m[6] = m[6] + A | 0,
    m[7] = m[7] + _ | 0
}
function _process(v) {
    var y, E = _data, B = E.words, A = E.sigBytes, _ = 16, O = _ * 4, P = A / O;
    v ? P = Math.ceil(P) : P = Math.max((P | 0) - 0, 0);
    var L = P * _
      , ue = Math.min(L * 4, A);
    if (L) {
        for (var N = 0; N < L; N += _)
            _doProcessBlock(B, N);
        y = B.splice(0, L),
        E.sigBytes -= ue
    }
    return new init(y,ue)
}
var l = [],c = [];
(function() {
    function p(g) {
        for (var v = Math.sqrt(g), y = 2; y <= v; y++)
            if (!(g % y))
                return !1;
        return !0
    }
    function h(g) {
        return (g - (g | 0)) * 4294967296 | 0
    }
    for (var m = 2, C = 0; C < 64; )
        p(m) && (C < 8 && (l[C] = h(Math.pow(m, 1 / 2))),
        c[C] = h(Math.pow(m, 1 / 3)),
        C++),
        m++
}
)();
var _hash = new init(l.slice(0))
function _doFinalize() {
    var p = _data
      , h = p.words
      , m = _nDataBytes * 8
      , C = p.sigBytes * 8;
    return h[C >>> 5] |= 128 << 24 - C % 32,
    h[(C + 64 >>> 9 << 4) + 14] = Math.floor(m / 4294967296),
    h[(C + 64 >>> 9 << 4) + 15] = m,
    p.sigBytes = h.length * 4,
    _process(),
    _hash
    }
function finalize(v) {
    v && _append(v);
    var y = _doFinalize();
    return y
}
function h_parse(v) {
    for (var y = v.length, E = [], B = 0; B < y; B++)
        E[B >>> 2] |= (v.charCodeAt(B) & 255) << 24 - B % 4 * 8;
    return new init(E,y)
}
function m_parse(v) {
    return h_parse(unescape(encodeURIComponent(v)))
}
function clamp() {
    var v = []
      , y = 0
    v[y >>> 2] &= 4294967295 << 32 - y % 4 * 8,
    v.length = Math.ceil(y / 4)
}
var _data = new init
var _nDataBytes = 0
function _append(v) {
    typeof v == "string" && (v = m_parse(v)),
    _data = v,
    _nDataBytes += v.sigBytes
}

function uK(y, E) {
        return finalize(y)
    }
function t1(e={}) {
    const {p: t, t: n, n: u, k: o} = e
      , r = pne(t);
    return uK(u + o + decodeURIComponent(r) + n)
}
function u(param) {
        // _o.inc();
        const a = Date.now()
          , l = hne(16)
          , c = 'k8tUyS$m'
          , d = {
            [qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 54, 25, 25])]: qu([11, 11, 0, 21, 62, 25, 24, 19, 20, 15, 7]),
            [qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 60, 24, 5, 2, 18])]: l,
            [qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 40, 23, 6, 18, 14, 20, 15, 6, 25])]: a
        };
        const p = t1({
            // p: 'type=trading-type&openConvert=false&keyword=&siteCode=44&secondType=A&tradingProcess=&thirdType=%5B%5D&projectType=&publishStartTime=&publishEndTime=&pageNo=1&pageSize=10',
            p: param,
            t: a,
            n: l,
            k: c
        });
        d[[qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 53, 23, 11, 5, 15, 20, 22, 19, 18])]] = p
        return d
    }
function deal_param(data) {
    rst = ""
    for (k in data) {
        rst += k + "=" + data[k] + "&"
    }
    return rst.slice(0, -1)
}
function setHead(param) {
    if (typeof param == 'string'){
        param = JSON.parse(param)}
    rst1 = u(deal_param(param))
    const wordArray = CryptoJS.lib.WordArray.create(rst1['X-Dgi-Req-Signature']['words']);
    const hexString = CryptoJS.enc.Hex.stringify(wordArray);
    rst1['X-Dgi-Req-Signature'] = hexString;
    return rst1
}
//
// param = {
//     "type": "trading-type",
//     "openConvert":false,
//     "keyword": "",
//     "siteCode": "44",
//     "secondType": "A",
//     "tradingProcess": "",
//     "thirdType": "[]",
//     "projectType": "",
//     "publishStartTime": "",
//     "publishEndTime": "",
//     "pageNo": 1,
//     "pageSize": 10
// }
// console.log(setHead(param))

这里还有个点需要注意的是,如果在js代码中调试时使用console.log(setHead(param))执行,用完必须注释掉。因为其中有对全局变量执行了_nDataBytes += v.sigBytes,在使用python代码调用时,编译时先执行一次,调用后再次执行,会导致_nDataBytes值会被加两次,就不对了
python代码

import execjs
import requests
import json

from proxy.get_proxy import get_proxy

headers = {
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/json",
    "Origin": "https://ygp.gdzwfw.gov.cn",
    "Pragma": "no-cache",
    "Referer": "https://ygp.gdzwfw.gov.cn/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
    "X-Dgi-Req-App": "ggzy-portal",
    "sec-ch-ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"macOS\""
}
cookies = {
    "_horizon_uid": "095b34f0-a40b-40a0-89d9-11dcefe38afd",
    "_horizon_sid": "8946bb7e-79f8-40c8-bd0f-c356a7903607"
}
url = "https://ygp.gdzwfw.gov.cn/ggzy-portal/search/v2/items"
data = {
    "type": "trading-type",
    "openConvert": False,
    "keyword": "",
    "siteCode": "44",
    "secondType": "A",
    "tradingProcess": "",
    "thirdType": "[]",
    "projectType": "",
    "publishStartTime": "",
    "publishEndTime": "",
    "pageNo": 1,
    "pageSize": 10
}
with open('test.js', 'r') as f:
    js_ = f.read()
js_c = execjs.compile(js_)
header_rst = js_c.call('setHead', json.dumps(data))
headers['X-Dgi-Req-App'] = header_rst['X-Dgi-Req-App']
headers['X-Dgi-Req-Signature'] = header_rst['X-Dgi-Req-Signature']
headers['X-Dgi-Req-Nonce'] = header_rst['X-Dgi-Req-Nonce']
headers['X-Dgi-Req-Timestamp'] = str(header_rst['X-Dgi-Req-Timestamp'])


data = json.dumps(data, separators=(',', ':'))
response = requests.post(url, headers=headers, cookies=cookies, data=data, proxies=get_proxy())

print(response.text)
print(response)

总结

仅供学习参考,如有疑问欢迎交流,一起学习进步

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值