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

本文记录了学习JS逆向破解广东省公共资源交易平台的全过程。包括数据采集流程、cookie生成逻辑、headers生成关键函数的定位与逆向,还介绍了t1()函数的逆向方法,最后给出了完整的JS和Python代码,强调仅供学习,请勿滥用爬虫。

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


前言

学习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.
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值