以GeeTest为例的滑动验证码破解 - 参数a

以GeeTest为例的滑动验证码破解 - 参数a

2016-12-24更新:由于中山大学教务系统使用的GeeTest已经从5.5.9升级到了5.5.40,原文中的方法虽然同样适用,但是由于代码混淆的关系,变量名称有所变化,现在文章(包括截图)已经更新到与最新的变量名相符。

我又回来了,上回我们分析4号包,发现参数a是最为复杂的。这次我们就来搞清楚a到底是如何构成的,而从下一篇文章开始我们就会开始模拟4号包,完成最终的验证。

看来我们距离完成破解就只有一步之遥了啊!(剧透:其实不是)

上次我们说到,a是从这里来的:

na.s(a.id)

而na.s则是这样一个函数:

function(a) {
    for (var b, f = c(Q.f("arr", a)), g = [], h = [], i = [], j = 0, k = f.length; k > j; j++)
        b = e(f[j]), b ? h.push(b) : (g.push(d(f[j][0])),
            h.push(d(f[j][1]))),
            i.push(d(f[j][2]));
    return g.join("") + "!!" + h.join("") + "!!" + i.join("")
}

这里面有几个函数是非内部生成的,也就是Q.f、c、d以及e。为了找到这几个函数的定义,我们需要在Q.f里面下断点。而首先我们必须找到这个函数。在Debugger里搜索Q.f("arr",a)(中间是没有空格的)找到这块代码:

显然这就是我们要找的函数,在这里下个断点,拖动滑块,break下来之后,到console分别输入cde看看结果:

我们得到了想要的三个函数。经过整理,它们分别是:

function (a) {
    for (var b = [], c = 0, d = a.length - 1; c < d; c++) {
        var e = [];
        e[0] = Math.round(a[c + 1][0] - a[c][0]),
            e[1] = Math.round(a[c + 1][1] - a[c][1]),
            e[2] = Math.round(a[c + 1][2] - a[c][2]),
            0 === e[0] && 0 === e[1] && 0 === e[2] || b.push(e)
    }
    return b
}
function (a) {
    var b = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr",
        c = b.length,
        d = "",
        e = Math.abs(a),
        f = parseInt(e / c);
    f >= c && (f = c - 1),
        f && (d = b.charAt(f)),
        e %= c;
    var g = "";
    return a < 0 && (g += "!"),
        d && (g += "$"),
        g + d + b.charAt(e)
}
function (a) {
    for (var b = [[1, 0], [2, 0], [1, -1], [1, 1], [0, 1], [0, -1], [3, 0], [2, -1], [2, 1]], c = "stuvwxyz~", d = 0, e = b.length; d < e; d++)
        if (a[0] == b[d][0] && a[1] == b[d][1])
            return c[d];
    return 0
}

扫过一眼,目测没有外部参数,非常好。实际上就算我们看漏了,在.NET里运行起来的时候也会发出编译错误,所以并不用担心。

好了既然cde都很能直接复制,那么剩下的就只Q.f了,搞定这个就可以了。还是在Console里,输入Q.f查看结果:

function (a,b){return Q.z[b][a]}

此刻我的心情:

这是在逗我?原来Q.f只是为了取出某个值。所以之前pastime中使用了Q.f是为了取出endTime,而在na.s中,使用它就是为了取出arr这个值,而a.id作为参数传入na.s只是因为它是这个数组的识别符而已。这其实也证实了我们之前的猜想,a.id是无关验证的。

好了,Q.f并不是大boss,看来arr才是,那么我们继续在Console里输入Q.f("arr",a)试试看:

相信聪明的读者已经猜出这是做什么用的了,但是我看到的时候仍然是一脸懵B,追查生成arr的代码才搞明白。

如无意外大家应该都见过这个吧:

这是Google的reCaptcha验证码,比GeeTest的滑动验证码还要厉害的是,它竟然只需要用户点一个Checkbox就可以分辨是否机器人。(当然如果失败的话,你将面临看图选物游戏)

reCaptcha的原理据说是(未考证)记录用户鼠标轨迹,上传到服务器,服务器分析是否真人操作,然后返回结果。而且估计服务器端肯定做了学习功能,也就是说你不能找一群印度阿三来点几次记录轨迹然后replay,因为服务器端在这段轨迹多次重复之后就会辩认出这是自动的了。

为什么突然提到这个?因为这个arr就是记录了你的鼠标轨迹啊(╯‵□′)╯︵┻━┻

冷静,我们先点开看前几个数组:

你自己拉动的结果和我的图里的肯定不一样,但是能确定的是,无论拉动多少次,第0组的总是负负零,而第1组是零零零。

从第2组开始,0位的慢速增长,而1位的(图中无法体现,多看几组即可)有时增加有时减少,但是大多数情况下不变,而且始终围绕在0附近变化。2位的数据则是快速增长。

我们看最后几组数据:

实只要一开始能猜出是鼠标轨迹,这里就很明显了。我们一直在横向拉动,因此0位的数据是x移动距离,而1位的则是y,2位的从数量级可以猜测是时间。

我们让程序继续执行,在之前设置的另一个breakpoint停下(就是在生成请求包那里)。回到Console,分别看看pastimel

谜团解开!原来通过的时间也被记录在鼠标的轨迹里作为最后一组的时间值,这就是服务器能够验证pastime的原因!不过认真再想想,其实passtime这个参数的设定就是为了防止破解而不是为了算分。如果单纯为了算分,直接从轨迹取最后的时间就可以了。

至于l的值,则大致对应0位的x偏移值。(多次重复实验后发现,m基本上但并不总是和0位相同,有时候会差1,但是仍然能通过验证)

好好好,从第2组开始的数组我们都已经清楚了用途了,而第1组总是三个零。所以我们最后只需要找到第0组的出处就大功告成了!

说倒轻松,到底要怎么找?还记得我们是通过Q.f("arr"来取出这个二维数组的,所以"arr"便是关键。在Debugger中搜索,出现不少结果,一个个看看:

我说,Edge的搜索功能就不能case sensitive吗,都被坑多少次了,继续看下一个搜索结果:

THIS! 这就是我们要找的东西!首先确认余下的搜索结果只有我们上面取出数组加密的引用之后,就开始研究这两个地方了。右侧的那个是获取了数组之后push了一个新的东西进去。

左侧的:

function(a,b){Q.p("arr",[a],b)}

这个函数就干一件事,调用Q.p。既然可以从一个函数中直接引用P,那么Q应该是个global variable吧,所以我们直接在当前的断点里去Console输入Q.p看看是什么:

function (a,b,c){return Q.z[c][a]=b,b}

所以,Q.p("arr",[a],b)这句代码实现的功能等同于:

Q.z[b]["arr"] = [a];

如果这里的a是一个数组的话,那么这句代码就相当于将arr设置为了一个只包含a这个数组的二维数组,也就是完成了arr的初始化!没错这就是我们要的,我们要找的就是第0位的数组,也就是这里的a。

好了,在Q.p那里下一个断点,重新拖动验证码...实际上还没来得及拖动,刚点下去就被断下来了,这也是意料之中的,毕竟是初始值。

断下来之后,我们就要寻找调用这个函数的源头了,看看右下角的Call Stack:

双击Anonymous function,来到这里:

到底哪个才是我们要找的调用?鼠标移动到函数名上,会浮现函数的定义:

就是这个!实际上如果你细心点还可以发现,在调用了这个na.h之后,下一句就调用了na.r([0,0,0],a.id),这个实际上就是在用我们搜索结果里面右侧的函数进行push操作。这也验证了我们之前发现的第1组永远是三个零的假设。

把调用na.h的那句代码截出来:

na.h([
    Math.round(j.left - g),
    Math.round(j.top - h),
    0],
 a.id)

所以我们只要找到g、h和j就可以了。首先是j,往前看一句代码可以找到:

j=f.getBoundingClientRect();

很好很好,可是f呢:

var f=b(".gt_slider_knob");

可以的,然后b呢?

等等,好像没有必要追下去了,这个显示就是返回了class为gt_slider_knob的元素吧。在Dom里面找找这个类:

找的就是它!所以k这个矩形代表的就是图里的这个边框。

那么接下来就找g和h了:

g=c.clientX||c.changedTouches&&c.changedTouches[0].clientX
h=c.clientY||c.changedTouches&&c.changedTouches[0].clientY

原来g就是用户点击的x坐标,而h就是用户点击的y坐标。所以第0个数组的0位就是用户点击位置距离滑块矩形左侧的偏移的相反数,而1位就是点击位置与矩形顶部距离的相反数。

然而。。。

我就想问传送这两个数据给服务器是为了干嘛?这个又是一个本地独立生成无法被验证的数据啊!好了好了脑细胞白死了。

至此我们对于4号包的分析也完成了!不总结了还是不清楚的话再读一遍!

然而你以为下一篇我们直接模拟一发就结束了吗?

Naïve!

连人手拖动都无法达到100%的通过率,我们要怎么模拟鼠标轨迹才能达到?

下一篇文章里,我们尝试一个简单的轨迹生成方法,只求能成功通过一次,以此证明我们在这之前所做的所有的事都是正确的。在那之后,我们会开始优化算法,达到完美破解。

总算写完了,累死我了,还不快点赞!

编辑于 2016-12-24

文章被以下专栏收录