跳到主要内容

南川逆向笔记 | 攻破 PDD 风控系统(一)

· 阅读需 9 分钟

昨夜酉时,一顿困意席卷而来,简单冲洗后便沉沉睡去,深夜恍惚间风雨大作,爽风阵阵,一洗晚春高温,待至凌晨四五点,急雨退去,夜色渐明,雅雀欢喜,想来近日为推进项目,鲜有片刻缓息,此时却是难得的机会,遂有此篇,以备将来不时之需。

PDD 风控系统的关键

毫无疑问:Anti-Content

打开任意一个XHR请求信息,都会有anti-content请求参数,实践证明,每个请求只要带有合法(且匹配)的anti-contentcookieuser-agent,即可通过网关。

image-20210511051354770

PDD 风控系统的调试

打开开发者控制台,选择Network面板,勾选Preserve logDisable cache,随便选择一个含有Anti-Content参数的请求,复制目标网址。

image-20210511052009797

打开Source面板,添加XHR端点,输入目标网址:

image-20210511052242962

刷新页面,触发断点:

image-20210511052529476

右击左边source tree根节点top选择search all files,或者直接快捷键⌥ ⌘ F,输入目标关键字anti-content,可以发现出现了几个目标结果:

image-20210511053236123

接下来比较细节,详细步骤如下,首先随意点选一个目标,将定位到目标文件,但是是webpack压缩后的,所以要用{}进行格式美化(这个功能属实是反反爬的最强对手,因为任何代码在格式上无论怎么压缩基本都可以一键还原,因为压缩后的代码必须也要符合语法规范也要能够运行),格式化之后再点击刷新,会多出来一个目标选项,叫app.js.formatted,点击展开,然后再选择其中任意一项:

image-20210511053556134

接下来,可以发现,其实这个Anti-Content是一个中间站,会多次调用,有一个声明、生成、使用的流程。理论上来说,swicth case语句是用来描述多个同类但无联系的选项关系的,但是呢,一方面这是打包后的代码,另一方面开发者不会在某些业务逻辑上难为自己,这将导致自己的代码可维护性很差,所以我们可以直接认为,这里的逻辑就是声明、生成然后使用:

image-20210511054225067

那么我们就只要知道,如何生成就可以了。在e = n.sent()处加断点,然后取消原来的url断点(它的使命已经完成了,我们可以更进一步了),此时右边的Breakpoints会自动多一个断点,这是代码行级别而非文件级别的:

image-20210511054648709

再次刷新,发现崩溃了,这是很正常的,由于长时间断点,导致不符合实际的请求逻辑:

image-20210511054814527

我们再再次刷新即可,但是发现数据直接就加载出来了,没有停在断点处,这也是很正常的,因为这个断点是文件格式化后的断点,正常运行时是压缩的文件,它无法直接识别出行级别的断点:

image-20210511055044163

所以原来的XHR断点的使命还没有完成,我们还得把它抓回来,给勾上,重新刷新。熟悉的味道扑面而来,数据加载的小圈不断旋转,仿佛被开了无限月读。

image-20210511055245725

这个时候,就要开始跳跃调试了。

image-20210511055625869

我们直接按F8即可。

好吧,又直接越过了……看样子我们之前的断点打的还是太任性了。再来,重新捕获到文件级别,搜索anti-content,选择另一个文件,再次格式化,再次捕获到一个生成anti-content的语句,加上断点:

image-20210511060052105

然后F8,结果又直接返回了……但别急,如果再次刷新,会发现已经直接定位在了目标位置:

image-20210511060320301

此时,按ESC打开分离式控制台,主动运行语句试试,会发现就是目标结果,而且还是同步的,这很关键,因为如果是异步的,我们就很难直接确定是不是我们的目标函数了:

image-20210511060449773

接下来更细节,F11跟进函数内部后,我们要分析一下语句,在反调试过程中,任何分支语句都是很关键的,它们对应着业务逻辑,这里的意思就是变量o是一个二维数组,其中第一维是一个布尔整数,第二维是个字符串,如果第一维为 0,就返回第二维(目标字符串),否则就报错,报错信息为第二维(根据经验,是一个更短一些的字符串,最后的展示形式为Ajax返回中的VerifyAuthToken字段。这里值得小心的是o数组直接打印时看似很短,实际是浏览器把中间段给压缩了,取了前 50、后 49 加中间的英文省略号(占一位)(输入:⌥ ;,别问我是怎么知道的,问就是:https://sspai.com/post/45516 ,顺便夸一句:少数派这个平台的文章质量是真地高):

image-20210511061304660

但如果我们再注意看,会发现,这个send函数的外面正是定义了o的地方,所以这个w = function(e, t) {...}函数就是核心风控函数了,它在这个函数内部进行初始化、赋值、输出等操作,所以我们可以直接在这个函数加断点,然后o.send()那个函的断点可以去掉了,它的使命是真正完成了。接下来,我们再次刷新,成功截在了初始化位置:

image-20210511063001317

这个时候,我们需要知道这个函数,大概是什么规模,点击设置,勾选Bracket matchingCode folding,其中,Bracket matching允许你在括号的左右端按⌃ M定位到另一端,而Code folding允许你将函数折叠,从而直接确定函数的边界。

image-20210511063346844

折叠后,光标选中整个函数,发现一共有3972个字符,在这个选中的函数里,搜索o =赋值,有83个 匹配,这好吗?这不好,那么对于3972个字符的加密函数,我们又该怎么分析,以及对于接近三千行的加密文件,我们该怎么分析,且听下回分解,天大亮了,该干活了(^_^)

image-20210511063646786

image-20210511063855344

最后声明

  1. 技术是纯粹的,出于对人类向大自然求索而习得的点滴智慧的敬畏,为精进自己的技术以求向真理比别人更进一步,付出自己的时间、精力、智力、金钱等一切投入,都是值得且应当被鼓励的。
  2. 在此过程之中,无意中可能涉及到某些企业的商业利益,对某些企业的系统安全产生威胁,对此我们深感抱歉,同样也深怀感激,我们承诺一切技术不会用于非正当用途。
  3. 本文读者应同样承诺,不将本文技术用于任何非正当用途,任何通过习用本文技术,所构成的潜在的不可预估的风险,应与本文无关。