前端逆向工程实战

一、前言

碍于一直抢不到某些优惠券,在海鲜市场里面搜索过后发现有人出同款,且只要登录cookie就可以,猜测应该是已经有人开了,那么今天进行一下前端逆向工程实战和讲解,希望对想学但是一直看不懂的同学有所帮助。
本次教程以某网站活动为例。
地址:https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html
在这里插入图片描述

二、抓包

首先因为这个页面大概只能在app或者小程序上看到,我们要进行抓包。抓包的方式有很多种,Wireshark,fiddler和手机上面的Stream(iOS)。这个页面在实战中还是比较好抓的,但是瞬时请求量很大,所以看的时间比较久。经过选择Stream中的里面的GET与POST请求(排除掉图片之类的),我们得到了上面的网址。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/41810c253af849a5ae0d17b24dec56dc.png
预加载过后短短一秒钟请求就有这么多。
在这里插入图片描述

一个一个尝试:
在这里插入图片描述

三、分析

这一部分本来在浏览器里面也可以,但是网站的风控可能会持续进行302跳转。
我们可以在长长的请求列表里找到一个这样的可疑东西。
在这里插入图片描述
再观察相应情况:

{
    "code": 0,
    "msg": "返回成功!",
    "data": [
        {
            "code": "NO_STOCK",
            "msg": "今日券已发完",
            "playWaySecret": "40c06a5853",
            "playWayId": 109774,
            "restCount": 0,
            "totalCount": 10,
            "playWayStartTime": 1715756400000,
            "title": null,
            "businessCouponType": 1,
            "money": 0.0,
            "minConsume": 0.0,
            "discount": 0.0,
            "maxMinus": 0.0,
            "extend": null,
            "couponIcon": null,
            "bgConfig": "{\"buName\":\"住宿\",\"buCode\":\"hotel\"}",
            "channelUrlKey": "",
            "playWayEndTime": 1715774399000,
            "couponType": 0,
            "playWayTitle": "境外五周年-五折券 ",
            "couponValidityStartTime": 0,
            "couponValidityEndTime": 0,
            "couponUseDeadlineTime": 0
        }
    ]
}

哦,就是这个!给看不懂的小白们解读一下:

请求分析

请求内容(Request)

请求类型:POST
HTTP请求有多种类型,如GET、POST、PUT、DELETE等。POST请求通常用于发送数据到服务器

URLhttps://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon
URL是网络资源的地址。在这个例子中,它指向某网站的一个API端点,用于预检优惠券

参数
请求参数是附加在URL后面,用于向服务器传递额外的信息。

  • k=290373
  • yodaReady=h5
  • csecplatform=4
  • csecversion=2.4.0
  • mtgsig:包含加密的签名信息

请求头
请求头包含关于请求或响应的元信息,如客户端信息、内容类型、授权信息等。关键的请求头包括:

  • Host: cube.meituan.com
  • Accept: */*
  • Sec-Fetch-Site: same-origin
  • Accept-Language: en-GB,en;q=0.9
  • Accept-Encoding: gzip, deflate, br
  • Sec-Fetch-Mode: cors
  • Content-Type: application/json
  • Origin: https://cube.meituan.com
  • User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView
  • Referer: https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html
  • Content-Length: 1016
  • Connection: keep-alive
  • Sec-Fetch-Dest: empty
  • Cookie: 各种跟踪和会话管理相关的 Cookie

请求体
请求体包含实际发送到服务器的数据。在这个例子中,请求体是一个JSON格式的数据,包括:

playWaySecrets:加密的秘密信息。

sourceType:来源类型。

userId:用户ID。

其他参数如requestTime、nonceRandom、requestSign等,用于确保请求的合法性和防止重放攻击。

{
    "playWaySecrets": "40c06a5853",
    "sourceType": "MEI_TUAN",
    "userId": "",
    "requestTime": ,
    "nonceRandom": "eb3480e2-fca6-e769-d95b-cb24dec3cb82",
    "checkGetBeforeTime": false,
    "requestSign": "",
    "cubeActivityUrl": "",
    "cubeActivityId": 290373
}
响应内容(Response)

状态码:200 OK
表示请求成功

响应头:包含关于响应的元信息

  • Server: openresty
  • Date: Wed, 15 May 2024 11:34:00 GMT
  • Content-Type: application/json;charset=UTF-8
  • Transfer-Encoding: chunked
  • Connection: keep-alive
  • Vary: Accept-Encoding
  • M-TraceId: -28587130892389450
  • Access-Control-Allow-Origin: https://cube.meituan.com
  • Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept,__skcy, mtgsig
  • Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Methods: GET,POST,OPTIONS,DELETE
  • Content-Encoding: gzip

响应体:服务器返回的实际数据。在这个例子中,响应体是一个JSON格式的数据,包含优惠券的信息和状态,如:

code:响应状态码,0表示成功。

msg:响应消息。

data:包含具体的优惠券信息,如是否有库存、优惠券类型、数量等。
安全性
签名认证:mtgsig和requestSign是用于签名认证的参数,确保请求的真实性和防止篡改。
Cookie:用于会话管理和身份验证,确保每个请求都能正确识别用户。

三、预检代码实现

只是抓到包这就结束了吗?这样就可以重放请求让我们获得优惠券了吗?让我们去网站上面看一下。打开F12,进入源代码,挑一个比较有特色的请求参数进行搜索,以playWaySecrets为例子,看看JS里面都写了些什么:
在这里插入图片描述
注:这里可能有些难以实现,有时候清除cookie,调整网速,还有及时中断加载都很重要
在这里插入图片描述
哦~虽然源代码经过了webpack加密,但是我们仍然看到了这个请求的完整结构。与此同时,源代码旁边还有这样一条:
在这里插入图片描述
可见预检操作只是用来查询库存,而真正的获取优惠券是后面的sendCoupon请求,这也符合我们的代码规范。
不过做了这么久,虽然还没有到达我们抢到优惠券的目标,还是做一点东西满足以下我们的成就感吧!
让我们尝试重放请求:
重放这个请求的Python代码可以使用 requests 库,它是一个用于发送HTTP请求的简单而强大的库。下面是一个示例代码:

import requests
import json

url = "https://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon"

headers = {
    "Host": "cube.meituan.com",
    "Accept": "*/*",
    "Sec-Fetch-Site": "same-origin",
    "Accept-Language": "en-GB,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Sec-Fetch-Mode": "cors",
    "Content-Type": "application/json",
    "Origin": "https://cube.meituan.com",
    "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView",
    "Referer": "https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html",
    "Content-Length": "1016",
    "Connection": "keep-alive",
    "Sec-Fetch-Dest": "empty",
    "Cookie": ""
}

data = {
    "playWaySecrets": "40c06a5853",
    "sourceType": "MEI_TUAN",
    "userId": "",
    "requestTime": ,
    "nonceRandom": "eb3480e2-fca6-e769-d95b-cb24dec3cb82",
    "checkGetBeforeTime": False,
    "requestSign": "",
    "cubeActivityUrl": "",
    "cubeActivityId": 290373
}

response = requests.post(url, headers=headers, data=json.dumps(data))

print(response.status_code)
print(response.json())

解释:

  1. 导入库:首先导入 requestsjson 库。

  2. URL:定义请求的URL。

  3. 请求头:在 headers 字典中定义请求头,包括 HostAcceptUser-AgentContent-Type 等。

  4. 请求体:在 data 字典中定义请求体,包含需要发送的数据。

  5. 发送请求:使用 requests.post 方法发送POST请求,传入URL、请求头和请求体。

  6. 处理响应:打印响应的状态码和响应内容。

这个代码示例展示了如何使用Python和requests库重放一个HTTP POST请求,包括设置请求头和请求体,并处理响应。
那么结果就是:

{'code': 0, 'msg': '返回成功!', 'data': [{'code': 'NOT_START', 'msg': '未开始', 'playWaySecret': '40c06a5853', 'playWayId': 109774, 'restCount': 15, 'totalCount': 15, 'playWayStartTime': 1715774400000, 'title': None, 'businessCouponType': 1, 'money': 0.0, 'minConsume': 0.0, 'discount': 0.0, 'maxMinus': 0.0, 'extend': None, 'couponIcon': None, 'bgConfig': '{"buName":"住宿","buCode":"hotel"}', 'channelUrlKey': '', 'playWayEndTime': 1715788799000, 'couponType': 0, 'playWayTitle': '境外五周年-五折券 ', 'couponValidityStartTime': 0, 'couponValidityEndTime': 0, 'couponUseDeadlineTime': 0}]}
200
{'code': 0, 'msg': '返回成功!', 'data': [{'code': 'NO_STOCK', 'msg': '今日券已发完', 'playWaySecret': '40c06a5853', 'playWayId': 109774, 'restCount': 0, 'totalCount': 15, 'playWayStartTime': 1715774400000, 'title': None, 'businessCouponType': 1, 'money': 0.0, 'minConsume': 0.0, 'discount': 0.0, 'maxMinus': 0.0, 'extend': None, 'couponIcon': None, 'bgConfig': '{"buName":"住宿","buCode":"hotel"}', 'channelUrlKey': '', 'playWayEndTime': 1715788799000, 'couponType': 0, 'playWayTitle': '境外五周年-五折券 ', 'couponValidityStartTime': 0, 'couponValidityEndTime': 0, 'couponUseDeadlineTime': 0}]}
200

非常令人欣慰的结果对吧!对于整个流程,我们已经完成了一半了。

四、发送代码实现

我们先观察一下这部分的代码:
在这里插入图片描述
这段代码是一个用JavaScript编写的函数,用于处理红包的领取逻辑。它包括了事件监听、数据准备、签名生成、请求发送和响应处理。为了方便理解,我将代码拆分成几个部分并逐一解释。

代码整体结构

  1. 函数定义和事件监听

    key: "takeRedpacket",
    value: function(l) {
        var f = this;
        m.default.on(function(s) {
    
    • takeRedpacket 是一个对象的键,值是一个函数,该函数接收参数 l
    • m.default.on 用于监听一个事件,当事件触发时,执行回调函数。
  2. 数据准备

        (0, y.default)(function(c, e, t) {
            var a, d = t || "", u = e || window.Block.UUID || "";
            c ? (0, b.default)((a = (0, n.default)(p.default.mark(function e(t) {
    
    • y.default 可能是一个工具函数,用于异步执行操作。
    • c 是用户ID,e 是UUID,t 是token。
    • 检查 c 是否存在,如果存在则继续执行。
  3. 准备请求数据

        var n, a, o, r, i;
        return p.default.wrap(function(e) {
            for (; ; )
                switch (e.prev = e.next) {
                case 0:
                    return a = s.server,
                    o = k.default.guid(),
                    n = {
                        positionCityId: l.data.positionCityId,
                        uuid: u,
                        visitCityId: l.data.visitCityId,
                        playWaySecrets: l.data.id,
                        userId: c,
                        sourceType: l.data.platform,
                        weixinCode: "",
                        voucherSourceType: h.default.native ? "INNER_APP" : "OUTER_APP",
                        requestTime: a,
                        nonceRandom: o,
                        requestSign: btoa("playWaySign," + btoa(a + "," + o)),
                        mini_program_token: d,
                        cubeActivityUrl: window.location.href,
                        cubeActivityId: window.Block.ID
                    },
    
    • a 是服务器时间,o 是一个随机生成的GUID。
    • n 是请求数据对象,包含了多个字段,如 positionCityIduuidvisitCityIdplayWaySecretsuserId 等。
    • requestSign 是请求签名,使用 btoa 进行Base64编码。
  4. 添加H5指纹和请求签名

                    h.default.business && (n.ecube_sid = d),
                    a = JSON.parse(t),
                    e.next = 7,
                    f.getFingerPrint();
                case 7:
                    return a.h5_fingerprint = e.sent,
                    n.RiskForm = btoa((0,
                    g.default)(a)),
                    o = {
                        contentType: l.type,
                        contentEncoding: ""
                    },
                    e.next = 12,
                    f.addRequestSignature("POST", T, v.default.queryStringify(n), o);
    
    • 如果 h.default.business 存在,则设置 ecube_sid
    • 调用 f.getFingerPrint 获取H5指纹,并添加到请求数据中。
    • n 转换成查询字符串,并调用 f.addRequestSignature 添加请求签名。
  5. 发送请求并处理响应

                case 12:
                    r = e.sent,
                    i = w.default.post(r.sigUrl).withCredentials().set("Content-Type", l.type),
                    (i = r.mtgsig ? i.set("mtgsig", r.mtgsig) : i).send(n).end(function(a, o) {
                        v.default.log({
                            api: r.sigUrl,
                            request: n,
                            response: a ? a.toString() : o
                        }),
                        a ? (0,
                        x.default)(r.sigUrl, r.mtgsig, n, function(t, e) {
                            t ? (KNB.getNetworkType({
                                success: function(e) {
                                    v.default.raptorLog({
                                        name: "sendCouponError-removesign",
                                        msg: "\u591a\u72b6\u6001\u7ea2\u5305\u53d1\u5238\u9519\u8bef"
                                    }, (0,
                                    g.default)({
                                        err: a.toString(),
                                        xhrErr: t,
                                        sigUrl: r.sigUrl,
                                        status: i.xhr ? i.xhr.status : "\u6ca1\u6709xhr",
                                        readyState: i.xhr ? i.xhr.readyState : "\u6ca1\u6709xhr",
                                        statusText: i.xhr ? i.xhr.statusText : "\u6ca1\u6709xhr",
                                        responseText: i.xhr ? i.xhr.responseText : "\u6ca1\u6709xhr",
                                        responseType: i.xhr ? i.xhr.responseType : "\u6ca1\u6709xhr",
                                        networkType: e.networkType
                                    }))
                                }
                            }),
                            l.callback(a, o)) : (window.Block.trigger("$blockComponentRedpacketRefresh", !0),
                            l.callback(t, {
                                body: e
                            }))
                        }) : (o && o.body && (2032 === o.body.code || 99999 === o.body.code) && v.default.raptorLog({
                            name: "addRequestSignature",
                            msg: "KNB\u9a8c\u7b7e\u5931\u8d25"
                        }, (0,
                        g.default)({
                            err: a,
                            sigUrl: r.sigUrl
                        })),
                        window.Block.trigger("$blockComponentRedpacketRefresh", !0),
                        l.callback(a, o))
                    });
    
    • 发送带有签名的POST请求。
    • 处理响应并记录日志,如果有错误则进行错误处理和回调。
    • 成功时触发 "$blockComponentRedpacketRefresh" 事件,更新页面状态。

要实现这段代码的重放,我们需要获取以下关键信息和数据:

  1. 用户相关信息

    • userId:用户的唯一标识符(例如用户ID)。
    • uuid:用户设备的唯一标识符(可能是设备的UUID)。
  2. 城市相关信息

    • positionCityId:用户当前所在城市的ID。
    • visitCityId:用户访问的城市ID。
  3. 活动相关信息

    • playWaySecrets:活动的密钥或标识符。
    • cubeActivityId:活动的ID。
    • cubeActivityUrl:活动页面的URL。
  4. 请求时间和随机数

    • requestTime:请求的时间戳。
    • nonceRandom:随机生成的字符串(GUID)。
  5. 签名和指纹

    • requestSign:基于特定规则生成的请求签名。
    • h5_fingerprint:设备的指纹信息。
  6. 其他信息

    • sourceType:请求来源类型(例如 INNER_APPOUTER_APP)。
    • voucherSourceType:优惠券来源类型。
    • mini_program_token:小程序的token(如果适用)。
    • weixinCode:微信的授权码(如果适用)。
    • RiskForm:风险评估表单信息(加密后的JSON字符串)。
  7. 必要的库和函数

    • getFingerPrint:获取设备指纹的函数。
    • addRequestSignature:生成请求签名的函数。

我们可以在代码里找到对应的函数:
在这里插入图片描述
从这段代码来看,获取设备指纹是一个异步操作。在JavaScript中,它使用一个回调函数来获取指纹。

在这里插入图片描述
在JavaScript代码中,addRequestSignature 方法调用 c.default.sign 来生成签名,并通过回调函数返回签名的URL和头部信息mtgsig。
然而,事情没有这么简单。这种防御拉满的接口还会调用更多的函数:

       t.sign = function(t, n, o, e, d, i) {
            if (g)
                d();
            else {
                var a = {
                    url: t,
                    method: n,
                    headers: o,
                    data: e
                };
                if (!i || !c.native)
                    return window.H5guard ? (a.headers = {
                        "content-encoding": o.contentEncoding,
                        "content-type": o.contentType
                    },
                    void window.H5guard.sign(a).then(function(n) {
                        d(n)
                    })) : void u(function(n) {
                        n ? (a.headers = {
                            "content-encoding": o.contentEncoding,
                            "content-type": o.contentType
                        },
                        window.H5guard.sign(a).then(function(n) {
                            d(n)
                        })) : d(!1)
                    });
                r.ready(function() {
                    r.addRequestSignature({
                        method: n,
                        url: t,
                        body: e,
                        header: o,
                        success: function(n) {
                            var e = t;
                            n.mtgsig ? "string" == typeof n.mtgsig ? o.mtgsig = JSON.parse(n.mtgsig).mtgsig : o.mtgsig = n.mtgsig.mtgsig : e = n.url,
                            d({
                                url: e,
                                headers: o
                            })
                        },
                        fail: function() {
                            d({
                                url: t
                            })
                        }
                    })
                })
            }
        }
        ,
        t.getFingerPrint = function(e) {
            g ? e() : window.H5guard ? e(window.H5guard.getfp()) : u(function(n) {
                e(n ? window.H5guard.getfp() : "")
            })
        }
        ,
        n.exports = t

至此仍未结束。需要的信息和功能列表:
签名服务(window.H5guard.sign):模拟或替代的签名服务,能够根据输入生成签名和签名后的 URL。
指纹服务(window.H5guard.getfp):模拟或替代的指纹服务,能够生成设备指纹。
本地环境检测(c.native 和 window.H5guard):需要确定如何在 Python 环境中模拟或替代这些检测。
回调函数(用于处理签名和指纹结果):在 Python 中,我们可以使用同步函数或 async/await 模式来模拟回调函数。
异步操作处理(u 和 r.ready):需要处理异步操作以确保操作在适当的时机完成。
调用控制台:
在这里插入图片描述
在这里插入图片描述我们可以发现里面会牵扯越来越多的函数和变量,但是由于已经混淆了,我们几乎不可能硬解这些数据,所以想个办法,能不能尝试直接从接口或者浏览器获取?
控制台中,window.H5guard.getfp()成功返回了指纹数据:
在这里插入图片描述

实现步骤

以下是实现重放这段代码的步骤:

  1. 准备数据

    • 收集用户、城市和活动相关信息。
    • 生成请求时间戳和随机数。
  2. 生成签名和指纹

    • 调用 getFingerPrint 函数获取设备指纹。
    • 调用 addRequestSignature 函数生成请求签名。
  3. 构建请求数据

    • 按照代码中的格式构建请求数据对象。
  4. 发送请求并处理响应

    • 使用 requests 库发送带有签名的POST请求。
    • 处理响应并记录日志。

以下是一个Python代码示例,展示了如何实现这段代码的重放:

import requests
import json
import uuid
import time
from base64 import b64encode

def get_fingerprint():
    # 假设这里是获取设备指纹的函数
    return "example_fingerprint"

def add_request_signature(method, url, query_string, headers):
    # 假设这里是生成请求签名的函数
    # 返回签名和签名的URL
    return {
        "sigUrl": url,
        "mtgsig": "example_mtgsig"
    }

# 准备数据
user_id = "77046512"
uuid = "0000000000000C66B32075291418895F81B0288CBAD8FA167857834046472258"
position_city_id = "118"
visit_city_id = "118"
play_way_secrets = "40c06a5853"
cube_activity_id = 290373
cube_activity_url = "https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html?cube_id=290211&cityId=118&city_id=118"
source_type = "INNER_APP" if False else "OUTER_APP"  # 根据实际情况判断
request_time = int(time.time() * 1000)
nonce_random = str(uuid.uuid4())
request_sign = b64encode(b"playWaySign," + b64encode(f"{request_time},{nonce_random}".encode())).decode()
fingerprint = get_fingerprint()
risk_form = b64encode(json.dumps({"h5_fingerprint": fingerprint}).encode()).decode()

# 构建请求数据
data = {
    "positionCityId": position_city_id,
    "uuid": uuid,
    "visitCityId": visit_city_id,
    "playWaySecrets": play_way_secrets,
    "userId": user_id,
    "sourceType": source_type,
    "weixinCode": "",
    "voucherSourceType": source_type,
    "requestTime": request_time,
    "nonceRandom": nonce_random,
    "requestSign": request_sign,
    "mini_program_token": "",
    "cubeActivityUrl": cube_activity_url,
    "cubeActivityId": cube_activity_id,
    "RiskForm": risk_form
}

headers = {
    "Content-Type": "application/json",
    "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView"
}

# 生成请求签名
signature = add_request_signature("POST", "https://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon", json.dumps(data), headers)

# 发送请求
response = requests.post(signature["sigUrl"], headers=headers, data=json.dumps(data))

# 处理响应
print(response.status_code)
print(response.json())

这是个大坑啊。打包完的工程默认开了严格模式,控制台无法注入,还得换个思路
【未完待续】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

谁的BUG最难改

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

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

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

打赏作者

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

抵扣说明:

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

余额充值