2019年末逆向复习系列之拼夕夕Web端anti_content参数逆向分析

郑重声明:本项目的所有代码和相关文章, 仅用于经验技术交流分享,禁止将相关技术应用到不正当途径,因为滥用技术产生的风险与本人无关。

这篇文章是公众号《云爬虫技术研究笔记》的《2019年末逆向复习系列》的第八篇:《拼夕夕Web端anti_content参数逆向分析》

本次案例+代码已上传至代码库https://github.com/lateautumn4lin/Review_Reverse的路径pdd下面了,欢迎老哥们Fork+Star二连操作。

案例地址

http://yangkeduo.com/

image

背景分析

拼夕夕成立三年就上市,市值直追2/3京东的市值,算得上是互联网历史中的一“神话”了,这次案例来分析一下拼夕夕Web端的anti_content是如何生成的,anti_content能干什么呢?有了anti_content就能使用拼夕夕Web端的搜索接口来获取相应的商品列表了。

请求流程剖析

我们首先来分析下整个请求流程中请求的顺序以及各个请求所需的参数


image

搜索接口参数解析

在这里插入图片描述

可以看到搜索接口地址是http://yangkeduo.com/proxy/api/search,具体的参数是
image

  • pdduid (猜测应该是每个用户的uid,未登录的情况下值为0)
  • source (目前值是index,猜测可以写死)
  • search_met (目前值是manual,猜测可以写死)
  • track_data(目前值是refer_page_id,10002_1576678770851_wDdCbmtN8C,猜测是用来记录用户行为的,猜测也没JB用)
  • list_id(不确定,待会可以寻找)
  • sort(目前值是default,猜测可以写死)
  • filter(目前值是空,猜测可以写死)
  • q(关键词)
  • page: 2
  • size: 50
  • flip(不确定,待会可以寻找)
  • anti_content(不确定,待会可以寻找)

大致分析了全部参数,list_idflipanti_content这三个参数属于未知,想想既然是翻页,这些参数可能是来源于上一页?,验证下

image

list_idflip可以在搜索首页中找到,用来做首次请求
image

每次请求搜索接口返回的响应中可以获取下次请求参数的flip,从值的含义上来看,应该是offset偏移量相关,现在三个未知参数中只剩anti_content这个参数未知,这就是我们本次需要逆向分析的参数。

相邻请求的Cookie更换机制

我们对比下前后两次的调用搜索接口的请求,发送前一个请求的响应中返回了set-cookie

image

接着在第二次调用请求中把cookie中的jsessionid给更换了
image

我们的请求流程就分析到这里,具体流程如下(使用到了昨天我说的手绘风格画图工具):
image

anti_content参数逆向分析

定位加密函数

逆向的第一步就是如何定位加密函数的位置,因为搜索请求是xhr请求,我们直接打个xhr断点(或者想要直接全局搜索anti_content字符都行),在Sourcetab右侧下个断点,断点值就是搜索接口url的一部分---/proxy/api/search

image

接着再把页面往下来,去请求下一页数据,可以看到函数断在这里
image

接着还是一般的流程,看看右侧的调用栈call stack的每个函数来定位可能的调用位置
image

image

我们不能跟踪到异步函数调用前的值,所以我们重新打断点
image

image

可以发现,断点之前anti_content值已经生成,也就是_sent值,我们需要回追
image

追到这里,我们在这个return打断点,因为这里只有一个参数,比较容易观察,我们重新请求
image

发现这个时候的t的值不是之前的anti_content,所以猜测可能是这个函数之后的函数生成的anti_content,我们多次f8跳过,会发现断点断在这里
image

多次试验之后发现,我们再次按f8就会t的值就会生成,因此猜测两次调用之间会找到加密函数,我们f11跟下去
image

image

多尝试几次就会看到跳到了新的一个js文件---RiskControl文件,看着这个文件名,翻译过来是风险控制,也就是风控?,看来很有可能是这个文件,我们来验证下这个函数是否是加密函数
image

继续f11可以看出,然后在console里面调试值,可以发现kt这个函数就是加密的函数,现在我们找到了加密的函数,下面跟进函数去分析。

静态加密函数

我们跟进刚才的kt函数,看看整个函数的逻辑,整个未还原的函数的代码在这里,先大概静态分析下代码

function kt() {
         var t, n = {};
         // 定义变量
            n[h("0xda", "$1%G")] = function(t) {
                return t()
            }
            //h("0xda", "$1%G")的值在console里面是"CeIdF",所以包括这个和下面的几个函数
            都是给n这个dict去赋值的
            ,
            n[h("0xdb", "wZhN")] = h("0xdc", "76m3"),
            n[h("0xdd", "Vfvl")] = function(t, n) {
                return t < n
            }
            ,
            n[h("0xde", "YoRA")] = function(t, n) {
                return t * n
            }
            ,
            n[h("0xdf", "OtK!")] = function(t) {
                return t()
            }
            ,
            n[h("0xe0", "U#^v")] = function(t, n, r) {
                return t(n, r)
            }
            ,
            n[h("0xe1", "[6Hz")] = function(t, n) {
                return t < n
            }
            ,
            n[h("0xe2", "Nj]Q")] = h("0xe3", "7o8w"),
            n[h("0xe4", "L3Mt")] = function(t, n) {
                return t === n
            }
            ,
            n[h("0xe5", "m3X(")] = function(t, n) {
                return t > n
            }
            ,
            n[h("0xe6", "E5c@")] = function(t, n) {
                return t <= n
            }
            ,
            n[h("0xe7", "JQC0")] = function(t, n) {
                return t - n
            }
            ,
            n[h("0xe8", "hlS%")] = function(t, n) {
                return t << n
            }
            ,
            n[h("0xe9", "QnID")] = function(t, n) {
                return t > n
            }
            ,
            n[h("0xea", "XVjd")] = function(t, n) {
                return t << n
            }
            ,
            n[h("0xeb", "xkH%")] = function(t, n) {
                return t === n
            }
            ,
            n[h("0xec", "T[4u")] = h("0xed", "YoRA"),
            n[h("0xee", "rFSk")] = h("0xef", "[6Hz"),
            n[h("0xf0", "$1%G")] = function(t, n) {
                return t + n
            }
            ,
            n[h("0xf1", "Nj]Q")] = h("0xf2", "QnID"),
            n[h("0xf3", "L3Mt")] = h("0xf4", "w@Yj"),
            Y = n[h("0xf5", "$1%G")](n[h("0xf6", "5f)w")](Math[y](), 10), 7) ? "" : "N";
            //生成随机数
            var r = [h("0xb5", "&k7t") + Y]
              , e = (t = [])[F].apply(t, [rt ? [][F](n[h("0xf7", "7o8w")](yt), st[r]()) : f[r](), ut[r](), ct[r](), ft[r](), wt[r](), ht[r](), lt[r](), dt[r](), xt[r](), _t[r](), vt[r]()].concat(function(t) {
                if (Array.isArray(t)) {
                    for (var n = 0, r = Array(t.length); n < t.length; n++)
                        r[n] = t[n];
                    return r
                }
                return Array.from(t)
            }(pt[r]()), [gt[r](), bt[r](), Ct[r]()]));
            //特别长的函数,先是把一个数组concat另一个数组,然后appy这个数组,这段赋值可以得到r和e的值
            n[h("0xe0", "U#^v")](setTimeout, function() {
                n[h("0xf8", "@25z")](Ot)
            }, 0);
            //异步延后执行Ot函数
            for (var i = e[H][x](2)[h("0xf9", "5f)w")](""), a = 0; n[h("0xfa", "rFSk")](i[H], 16); a += 1)
                i[n[h("0xfb", "9njl")]]("0");
            i = i[h("0xfc", "ZM84")]("");
            var o = [];
            n[h("0xfd", "1MxR")](e[H], 0) ? o[U](0, 0) : n[h("0xfe", "T[4u")](e[H], 0) && n[h("0xff", "EZlb")](e[H], n[h("0x100", "mMg5")](n[h("0x101", "&ETh")](1, 8), 1)) ? o[U](0, e[H]) : n[h("0x102", "7o8w")](e[H], n[h("0x103", "76m3")](n[h("0x104", "E5c@")](1, 8), 1)) && o[U](W[_](i[D](0, 8), 2), W[_](i[D](8, 16), 2)),
            //这里的三目运算符用的真牛皮!
            e = [][F]([n[h("0x105", "HMtq")](Y, "N") ? 2 : 1], [0, 0, 0], o, e);
            var c = u[n[h("0x106", "EZlb")]](e)
              , w = [][n[h("0x107", "eKTC")]][h("0x108", "w@Yj")](c, function(t) {
                return String[n[h("0x109", "w@Yj")]](t)
            });
            //依旧是函数调用
            return n[h("0x10a", "T[4u")](n[h("0x10b", "76m3")], s[n[h("0x10c", "@25z")]](w[h("0x10d", "hlS%")]("")))
            //计算出anti_content值
        }

经过上面的分析,比较重要的点是下面图上打出的断点

image

接下来,就是动态调试去扣js啦!

动态分析几个函数

  • Y值
    多次调试Y值,可以发现这个Y就是一个随机值,这里可以重写。
Y = n[h("0xf5", "$1%G")](n[h("0xf6", "5f)w")](Math[y](), 10), 7) ? "" : "N";
image
  • e值
    image

    e的值得出是一个数组,不过具体的扣js就详细说了,大家可以在调试的过程中去简化代码。
  • setTimeout函数
    setTimeout在这里使用
n[h("0xe0", "U#^v")](setTimeout, function() {
                        n[h("0xf8", "@25z")](Ot)
                    }, 0);

看看n[h("0xe0", "U#^v")]是什么函数

n[h("0xe0", "U#^v")] = function(t, n, r) {
                        return t(n, r)
                    }

所以简化来看就是

setTimeout(function() {
                        n[h("0xf8", "@25z")](Ot)
                    },0)
 n[h("0xda", "$1%G")]  = function(t) {
                        return t()
                    }
//也就是马上执行Ot函数

看看Ot函数是什么

function Ot() {
                    f[h("0xce", "&ETh")](),
                    [st, ut][G](function(t) {
                        t[V] = []
                    })
                }

先看第一个函数

f[h("0xce", "&ETh")] = function() {
                    [z, j, T, S][D](function(t) {
                        t[y] = []
                    })
                }
 //在console里得到具体的值

image

scrollTopjs中表示垂直滚动条位置,应该是检测是否滑动的参数,这里写死就行。

  • return
return n[h("0x10a", "T[4u")](n[h("0x10b", "76m3")], s[n[h("0x10c", "@25z")]](w[h("0x10d", "hlS%")]("")))

具体可以自行简化,f11调试可以看到这里

image

可以看到这里的函数,t应该是anti_content的乱码,a("0x13", "Dd5H")encode ,这个函数应该是把乱码的anti_content还原出来,具体的大家可以去扣,这里就不细讲了。

代码实战

构造函数加密服务

这次构造加密函数不使用python去调用js脚本,而是使用node直接去调用,原因主要有几个:

  1. Python调用js的库主要是Pyexecjs,然而作者已经宣布不维护了,可以参考
    https://gist.github.com/doloopwhile/8c6ec7dd4703e8a44e559411cb2ea221
  2. 毕竟是跨语言调用,使用python调用js不如原生调用来的实际和方便。
  3. 使用node服务框架包装加密函数,解耦了函数之间的关系,更方便之后的维护和修改。

基于以上的原因,选择node服务框架来调用js调用暴露出接口,node服务框架选用node生态中占有份额最大的express,使用简单上手,和pythonflask一样,几行代码启动一个服务。

  • 先安装好express模块
npm install express --save
  • 基本express例子
const express = require('express');
const bodyParser = require('body-parser');
// 创建应用实例
const app = express();
pp.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/get_anti_content', function (req, res) {
    let anti_result = o()()["messagePackSync"]("http://yangkeduo.com/search_result.html");
    console.log(
        "获取anti_content值为: %s", anti_result
    );
    res.json(
        {
            anti_result: anti_result
        }
    )
});
// 监听8000端口并在运行成功后向控制台输入服务器启动成功!
const server = app.listen(8000, function () {
    let host = server.address().address;
    let port = server.address().port;
    console.log(
        "node服务启动,监听地址为: http://%s:%s", host, port
    )
});
  • 启动服务
node xxx.js
  • 效果如图


    image

测试例子

先从首页获取fliplist_id

def get_pdd_search_lst(search_name: str) -> None:
    with requests.get(
        url=f"http://yangkeduo.com/search_result.html?search_key={search_name}",
        headers=headers
    ) as response:
        data = re.findall(r"window.rawData=([\s\S]*?)</script>", response.text)
        if not data:
            raise Exception("extract json error")
        data = data[0].strip().strip(";")
        json_data = json.loads(data)["store"]["data"]["ssrListData"]
        msg_data = dict(json.loads(json_data["loadSearchResultTracking"]["req_params"]),
                        **{"flip": json_data["flip"]})

然后调用node服务获取anti_content

with requests.get(
        url=f"http://yangkeduo.com/proxy/api/search",
        headers=headers,
        params={
            "pdduid": "4787727322403",
            "source": "search",
            "search_met": "",
            "track_data": "refer_page_id,10169_1576665846887_tfHPiWnbtu",
            "list_id": msg_data["list_id"],
            "sort": "default",
            "filter": "",
            "q": search_name,
            "page": 2,
            "size": 50,
            "flip": msg_data["flip"],
            "anti_content": requests.get('http://localhost:8000/get_anti_content').json()["anti_result"]
        }
    ) as lst_response:
        print(lst_response.json())

关键要点

  1. 记住,调用搜索接口的时候需要在headers中加上AccessToken,不加的话会报错
{'server_time': 1576763884, 'error_code': 40001}
  1. 搜索接口要登录才能使用,所以需要会用登录的Cookie
  2. 每次调用搜索接口的时候从响应中获取最新的JSESSIONID

复习要点

  1. 逆向分析的第一步是如何寻找加密函数的入口。
  2. 由于Python调用Js的各种麻烦,因此直接使用node直接调用加密方法并作为服务暴露出来,解耦语言之间的障碍。
  3. 时间和精力允许的情况下,尽量还原混淆代码,方便日后维护。
  4. 在逆向分析的同时,学习对方的反爬策略。

作者相关

号主介绍

多年反爬虫破解经验,AKA“逆向小学生”,沉迷数据分析和黑客增长不能自拔,虚名有CSDN博客专家和华为云享专家。

私藏资料

呕心沥血从浩瀚的资料中整理了独家的“私藏资料”,公众号内回复“私藏资料”即可领取爬虫高级逆向教学视频以及多平台的中文数据集

小学生都推荐的好文

2019年末逆向复习系列之知乎登录formdata加密逆向破解

2019年末逆向复习系列之今日头条WEB端_signature、as、cp参数逆向分析

2019年末逆向复习系列之百度指数Data加密逆向破解

2019年末逆向复习系列之努比亚Cookie生成逆向分析

2019年末逆向复习系列之淘宝M站Sign参数逆向分析

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容