本文要分享的内容是去年为了抢鞋而分析 极验(GeeTest)反爬虫防护的笔记,由于篇幅较长(为了多混点CB)我会按照我的分析顺序,分成如下四个主题与大家分享:
- 极验反爬虫防护分析之交互流程分析
- 极验反爬虫防护分析之接口交互的解密方法
- 极验反爬虫防护分析之接口交互的解密方法补遗
- 极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路
本文是第四篇, 也是最后一篇,网上大部分针对极验的绕过方法大都是模拟手工滑动滑块的方式,但是通过上面几篇文章的分析,我们是能知道Geetest已经对目前市面上大多自动化测试的工具进行了监测,包括 Selenium甚至electron等。所以基于这些工具的破解不是不行,只是人家官方没有严查,不长久的,稳妥之计还是要直接从封包入手。下面进入正文~
背景图片乱序的还原
如《极验反爬虫防护分析之交互流程分析》第五步的分析,得到的 bg
和fullbg
图片都是乱序处理后的图片,要判断滑动的距离及轨迹需要将图片进行还原。如下图:
1.jpg
还原后的代码为:
function SEQUENCE() { var e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_"); for (var t, n = [], r = 0; r < 52; r++) { t = 2 * parseInt(e[parseInt(r % 26 / 2)]) + r % 2; parseInt(r / 2) % 2 || (t += r % 2 ? -1 : 1); t += r < 26 ? 26 : 0; n["push"](t); } return n;}var result = SEQUENCE();console.log(result.join(", "));
至此,我们知道它是通过两次折叠构建出来52个元素的散列表。通过固定的公式将图片上下、左右互换并根据散列表的值进行乱序。通过分析代码中的字符串常亮6_11_7_10_4_12_3_1_0_5_2_9_8
是在slide.7.6.0.js文件中,一开始的方法中定义的:$_DAEAF = decodeURI('N-%60%13)nN-%60%1C%1...
,decodeURI解码后的数组第911位就是此字符串常量, 如下图:
2.jpg
继续跟进绘图的代码:
3.jpg
将混淆的代码还原之后,如下:
function $_GEN(t, e) { var $_CJDIX = $_AB.$_Ei()[4][26]; for (; $_CJDIX !== $_AB.$_Ei()[8][24];) { switch ($_CJDIX) { case $_AB.$_Ei()[16][26]: t = t[$_DEAo(65)], e = e[$_DDJm(65)]; var n = t["width"], r = t["height"], i = document[$_DDJm(27)]($_DEAo(91)); i["width"] = n, i["height"] = r; var CanvasRenderingContext2D = i["getContext"]("2d"); $_CJDIX = $_AB.$_Ei()[8][25]; break; case $_AB.$_Ei()[8][25]: CanvasRenderingContext2D["drawImage"](t, 0, 0); var CanvasRenderingContext2D = e["getContext"]("2d"); e["height"] = r, e["width"] = WIDTH; for (var a = r / 2, u = 0; u < 52; u += 1) { var c = SEQUENCE % 26 * 12 + 1, _ = 25 < SEQUENCE ? a : 0, l = CanvasRenderingContext2D["getImageData"](c, _, 10, a); CanvasRenderingContext2D["putImageData"](l, u % 26 * 10, 25 < u ? a : 0); } $_CJDIX = $_AB.$_Ei()[0][24]; break; } }}
将以上JS编写为还原图片的Python代码如下:
import numpy as npfrom PIL import Image import matplotlib.pyplot as pltdef sequence(): t = 0 n = [] e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_") for r in range(0, 52): t = 2 * int(e[int(r%26/2)]) + r % 2 if 0 == int(r/2)%2: t += -1 if (r%2) else 1 t += 26 if (r<26) else 0 n.append(t) return ndef gen(_seq, _img): """ 用于将图片还原 @param _seq: 图片的序列号,也就是 Sequence 方法生成的结果 @param _img: 图片 @return new img """ r = 160 a = int(r / 2) np_image = np.array(img) new_np_img = np.zeros((160, 312, 3), dtype=np.uint8) for u in range(0, 52): c = _seq % 26 * 12 + 1 _ = int(a if (25 < _seq) else 0) xpos = u % 26 * 10 ypos = a if (25 < u) else 0 # var l = getImageData(c, _, 10, a); # putImageData(l, u % 26 * 10, 25 < u ? a : 0); slice_img = np_image[_:(_+a), c:(c+10)] n = len(slice_img[0]) new_np_img[ypos:(ypos+a), xpos:(xpos+n)] = slice_img return new_np_imgif __name__ == "__main__": seq = sequence() img = Image.open('/Users/datochan/WorkSpace/VSCProjects/nike-bot/Test/src/images/fullbg.jpg') newimg = gen(seq, img) plt.imshow(newimg) plt.show()
找一个待处理的图片:https://static.geetest.com/pictures/gt/6edec3cc1/6edec3cc1.jpg
,测试结果如下图:
4.jpg
至此,图片乱序还原的问题搞定。
滑动轨迹的加密方法
同样的方法跟踪滑块失败后的请求,分析回溯来到如下代码:
"$_CHBV": function (t, e, n) { var $_CABJD = $_AB.$_Ds, $_CABIQ = ['$_CACCE'].concat($_CABJD), $_CACAM = $_CABIQ[1]; $_CABIQ.shift(); var $_CACBm = $_CABIQ[0]; var r = this, i = r[$_CABJD(78)]; var o = { "lang": i[$_CABJD(172)] || $_CACAM(161), // 语言固定为 zh-hk || zh-cn "userresponse": $_CEI(t, i[$_CABJD(139)]), // t=滑动的距离,用户响应的内容, $_CABJD(139) = "challenge" 的值 "passtime": n, // 滑块消耗的时间=鼠标轨迹每个点耗时相加 "imgload": r[$_CABJD(744)], "aa": e, // 滑动轨迹的加密字符 "ep": r[$_CABJD(764)]() }; i[$_CABJD(118)] && (o[$_CABJD(221)] = t); o["rp"] = $_DCj(i[$_CACAM(159)] + i[$_CABJD(139)][$_CACAM(151)](0, 32) + o[$_CABJD(736)]); var s = r[$_CACAM(791)](); // rsa加密的aes密钥 var a = AES[$_CABJD(389)](gjson[$_CACAM(160)](o), r[$_CABJD(751)]()); // 将上面的json用aes加密 var u = Base64[$_CACAM(739)](a), c = { "gt": i[$_CABJD(159)], "challenge": i[$_CACAM(139)], "lang": o[$_CABJD(172)], "pt": r[$_CACAM(686)], "w": u + s }; ...}
至此,我们找到了移动滑块后提交的参数w的来历。根据以往经验s是aes加密用到密钥,用rsa加密后的密文。重点分析u的来历。向上回溯可知u的组成,将代码还原为:
/** * 用于计算 rp 的hash值 */function $_DCj(t) { function u(t, e) { return t << e | t > 32 - e; } function c(t, e) { var n, r, i, o, s; return i = 2147483648 & t, o = 2147483648 & e, s = (1073741823 & t) + (1073741823 & e), (n = 1073741824 & t) & (r = 1073741824 & e) ? 2147483648 ^ s ^ i ^ o : n | r ? 1073741824 & s ? 3221225472 ^ s ^ i ^ o : 1073741824 ^ s ^ i ^ o : s ^ i ^ o; } function e(t, e, n, r, i, o, s) { return c(u(t = c(t, c(c(function a(t, e, n) { return t & e | ~t & n; }(e, n, r), i), s)), o), e); } function n(t, e, n, r, i, o, s) { return c(u(t = c(t, c(c(function a(t, e, n) { return t & n | e & ~n; }(e, n, r), i), s)), o), e); } function r(t, e, n, r, i, o, s) { return c(u(t = c(t, c(c(function a(t, e, n) { return t ^ e ^ n; }(e, n, r), i), s)), o), e); } function i(t, e, n, r, i, o, s) { return c(u(t = c(t, c(c(function a(t, e, n) { return e ^ (t | ~n); }(e, n, r), i), s)), o), e); } function o(t) { var n = "", r = ""; for (var e = 0; e <= 3; e++) { n += (r = "0" + (t > 8 * e & 255)["toString"](16))["substr"](r["length"] - 2, 2); } return n; } var s, a, _, l, f, h, d, p, g, m; for (s = function v(t) { var e, n = t["length"], r = n + 8, i = 16 * (1 + (r - r % 64) / 64), o = Array(i - 1), s = 0, a = 0; while (a < n) s = a % 4 * 8, o[e = (a - a % 4) / 4] = o[e] | t["charCodeAt"](a) << s, a++; return s = a % 4 * 8, o[e = (a - a % 4) / 4] = o[e] | 128 << s, o[i - 2] = n << 3, o[i - 1] = n > 29, o; }(t = function w(t) { t = t["replace"](/\r\n/g, "\n"); for (var e = "", n = 0; n < t["length"]; n++) { var r = t["charCodeAt"](n); r < 128 ? e += String["fromCharCode"](r) : (127 < r && r < 2048 ? e += String["fromCharCode"](r 6 | 192) : (e += String["fromCharCode"](r 12 | 224), e += String["fromCharCode"](r 6 & 63 | 128)), e += String["fromCharCode"](63 & r | 128)); } return e; }(t)), d = 1732584193, p = 4023233417, g = 2562383102, m = 271733878, a = 0; a < s["length"]; a += 16) p = i(p = i(p = i(p = i(p = r(p = r(p = r(p = r(p = n(p = n(p = n(p = n(p = e(p = e(p = e(p = e(l = p, g = e(f = g, m = e(h = m, d = e(_ = d, p, g, m, s[a + 0], 7, 3614090360), p, g, s[a + 1], 12, 3905402710), d, p, s[a + 2], 17, 606105819), m, d, s[a + 3], 22, 3250441966), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 4], 7, 4118548399), p, g, s[a + 5], 12, 1200080426), d, p, s[a + 6], 17, 2821735955), m, d, s[a + 7], 22, 4249261313), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 8], 7, 1770035416), p, g, s[a + 9], 12, 2336552879), d, p, s[a + 10], 17, 4294925233), m, d, s[a + 11], 22, 2304563134), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 12], 7, 1804603682), p, g, s[a + 13], 12, 4254626195), d, p, s[a + 14], 17, 2792965006), m, d, s[a + 15], 22, 1236535329), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 1], 5, 4129170786), p, g, s[a + 6], 9, 3225465664), d, p, s[a + 11], 14, 643717713), m, d, s[a + 0], 20, 3921069994), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 5], 5, 3593408605), p, g, s[a + 10], 9, 38016083), d, p, s[a + 15], 14, 3634488961), m, d, s[a + 4], 20, 3889429448), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 9], 5, 568446438), p, g, s[a + 14], 9, 3275163606), d, p, s[a + 3], 14, 4107603335), m, d, s[a + 8], 20, 1163531501), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 13], 5, 2850285829), p, g, s[a + 2], 9, 4243563512), d, p, s[a + 7], 14, 1735328473), m, d, s[a + 12], 20, 2368359562), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 5], 4, 4294588738), p, g, s[a + 8], 11, 2272392833), d, p, s[a + 11], 16, 1839030562), m, d, s[a + 14], 23, 4259657740), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 1], 4, 2763975236), p, g, s[a + 4], 11, 1272893353), d, p, s[a + 7], 16, 4139469664), m, d, s[a + 10], 23, 3200236656), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 13], 4, 681279174), p, g, s[a + 0], 11, 3936430074), d, p, s[a + 3], 16, 3572445317), m, d, s[a + 6], 23, 76029189), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 9], 4, 3654602809), p, g, s[a + 12], 11, 3873151461), d, p, s[a + 15], 16, 530742520), m, d, s[a + 2], 23, 3299628645), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 0], 6, 4096336452), p, g, s[a + 7], 10, 1126891415), d, p, s[a + 14], 15, 2878612391), m, d, s[a + 5], 21, 4237533241), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 12], 6, 1700485571), p, g, s[a + 3], 10, 2399980690), d, p, s[a + 10], 15, 4293915773), m, d, s[a + 1], 21, 2240044497), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 8], 6, 1873313359), p, g, s[a + 15], 10, 4264355552), d, p, s[a + 6], 15, 2734768916), m, d, s[a + 13], 21, 1309151649), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 4], 6, 4149444226), p, g, s[a + 11], 10, 3174756917), d, p, s[a + 2], 15, 718787259), m, d, s[a + 9], 21, 3951481745), d = c(d, _), p = c(p, l), g = c(g, f), m = c(m, h); return (o(d) + o(p) + o(g) + o(m))["toLowerCase"]();}// console.log($_DCj("2328764cdf162e8e60cc0b04383fef81" + "4de7bb253d3999b2dca4d35049959acf7k"))var SlideObject = { "$_CEAQ": function() { // PerformanceTiming var t = window["Performance"]["timing"]; return { "a": t["navigationStart"], "b": t["unloadEventStart"], "c": t["unloadEventEnd"], "d": t["redirectStart"], "e": t["redirectEnd"], "f": t["fetchStart"], "g": t["domainLookupStart"], "h": t["domainLookupEnd"], "i": t["connectStart"], "j": t["connectEnd"], "k": t["secureConnectionStart"], "l": t["requestStart"], "m": t["responseStart"], "n": t["responseEnd"], "o": t["domLoading"], "p": t["domInteractive"], "q": t["domContentLoadedEventStart"], "r": t["domContentLoadedEventEnd"], "s": t["domComplete"], "t": t["loadEventStart"], "u": t["loadEventEnd"] }; }, "$_CHCg": function () { return { "v": "7.6.0", // slide.js的版本号 "f": $_DCj(this["gt"] + this["challenge"]) || "", "te": false, // touchEvent, PC端只有鼠标事件,没有触摸事件 "me": true, // mouseEvent "tm": this["$_CEAQ"]() }; }, /** * 用来处理 userresponse 参数 * t: 滑动的距离 * e: Challenge's value */ "$_CEI": function (t, e) { for (var n = e["slice"](32), r = [], i = 0; i < n["length"]; i++) { var o = n["charCodeAt"](i); r[i] = 57 < o ? o - 87 : o - 48; } n = 36 * r[0] + r[1]; var s, a = Math["round"](t) + n; var u = [[], [], [], [], []], c = {}, _ = 0; i = 0; for (var l = (e = e["slice"](0, 32))[ "length"]; i < l; i++) { c[s = e["charAt"](i)] || (c[s] = 1, u[_]["push"](s), _ = 5 == ++_ ? 0 : _); } var f, h = a, d = 4, p = "", g = [1, 2, 5, 10, 50]; while (0 < h) { if (0 <= h - g[d]) { f = parseInt(Math["random"]() * u[d]["length"], 10); p += u[d][f]; h -= g[d]; } else { u["splice"](d, 1); g["splice"](d, 1); d -= 1; } } return p; }, /** * * @param {*} e 轨迹数组 * @param {*} t 用于处理滑动轨迹数组每个元素的回调方法 */ "$_HBM": function (e, t) { var n = [], i = e["length"]; if (e["map"]) { e["map"](t); return; } for (var r = 0; r < i; r += 1) { n[r] = t(e[r], r); } }, /** * _pt_array: 滑动的轨迹数组 */ "$_BAFz": function (_pt_array) { function n(t) { var s = ""; var e = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr"; var n = e["length"], r = "", i = Math["abs"](t), o = parseInt(i / n); n <= o && (o = n - 1), o && (r = e["charAt"](o)); return t < 0 && (s += "!"), r && (s += "$"), s + r + e["charAt"](i %= n); } var t = function (t) { var e, n, r, i = [], o = 0, a = t["length"] - 1; for (var s = 0; s < a; s++) { e = Math["round"](t[s + 1][0] - t[s][0]); n = Math["round"](t[s + 1][1] - t[s][1]); r = Math["round"](t[s + 1][2] - t[s][2]); 0 == e && 0 == n && 0 == r || (0 == e && 0 == n ? o += r : (i["push"]([e, n, r + o]), o = 0)); } return 0 !== o && i["push"]([e, n, o]), i; }(_pt_array); // 滑动的轨迹数组 var r = [], i = [], o = []; this["$_HBM"](t, function (t) { var e = function (t) { var e = [[1, 0], [2, 0], [1, -1], [1, 1], [0, 1], [0, -1], [3, 0], [2, -1], [2, 1]]; for (var n = 0; n < e["length"]; n++) { if (t[0] == e[n][0] && t[1] == e[n][1]) return "stuvwxyz~"[n]; } return 0; }(t); e ? i["push"](e) : (r["push"](n(t[0])), i["push"](n(t[1]))), o["push"](n(t[2])); }); return r["join"]("") + "!!" + i["join"]("") + "!!" + o["join"](""); }, /** * t: $_BAFz的返回值 * e: 接口返回值中C的值: [12, 58, 98, 36, 43, 95, 62, 15, 12] * n: 接口返回的s值 */ "$_BGDl": function(t, e, n){ if (!e || !n) return t; var r, i = 0, o = t, s = e[0], a = e[2], u = e[4]; while (r = n["substr"](i, 2)) { i += 2; var c = parseInt(r, 16); var _ = String["fromCharCode"](c); var l = (s * c * c + a * c + u) % t["length"]; o = o["substr"](0, l) + _ + o["substr"](l); } return o; }, /** * t: 滑动的距离: 鼠标轨迹最后一个坐标点的X值 * e: 加密后的鼠标轨迹($_BGDl的返回值) * n: 用户滑动的耗时=鼠标轨迹每个点耗时相加 */ "$_CHBV": function(t, e, n) { var r = this, i = r[$_CABJD(78)]; // i是get.php返回的集合 var o = { "lang": "zh_hk" || "zh_cn", "userresponse": $_CEI(t, i["challenge"]), // t=鼠标轨迹最后一个坐标点的X值 "passtime": n, // 滑块消耗的时间=鼠标轨迹每个点耗时相加 "imgload": r[$_CABJD(744)], // 图片加载需要的时间 "aa": e, // 滑块轨迹加密后的字符集 "ep": this["$_CHCg"]() // 基础环境信息收集 }; o["rp"] = $_DCj(i["gt"] + i["challenge"]["slice"](0, 32) + o["passtime"]); // 会话信息的hash加密 var s = rsa_encrypt(), a = AES_encrypt(gjson(o), aes_key), u = Base64_encrypt(a); var c = { "gt": i[$_CABJD(159)], "challenge": i[$_CACAM(139)], "lang": "zh_hk", "pt": r[$_CACAM(686)], "w": u + s } // TODO: 提交post请求 }}// 调用示例var pt_list = [[-37,-41,0], [0,0,0], [3,0,251], [6,0,266], [9,0,283], [13,0,300], [15,0,316], [17,0,333], [19,0,349], [20,0,366], [20,0,383], [20,0,400], [20,0,430]];var _ = SlideObject["$_BGDl"](SlideObject["$_BAFz"](pt_list), [12, 58, 98, 36, 43, 95, 62, 15, 12], "35304332")console.log(_);
其中 SlideObject["$_CEAQ"]
方法依赖于浏览器的windows环境,如下图:
5.jpg
继续跟进,发现"ep"值,是windows的Performance.timing
中的值。
6.jpg
因此,可以根据 Performance.timing
时间产生的先后顺序以及时间间隔,用当前的时间戳减去相应的值来模拟。由于相关代码比较简单,为了节省篇幅就不给出了。
滑动轨迹生成的思路
由于极验采用人工智能的方式对滑动的轨迹进行的验证,因此如果我们比较随意的生成鼠标滑动轨迹基本是肯定被封的,因此我们要详细分析一下鼠标轨迹的规律,
通之前介绍的调试手段,手工滑动滑块,获取到鼠标滑动轨迹的集合数组如下:
[[-37,-41,0], [0,0,0], [3,0,251], [6,0,266], [9,0,283], [13,0,300], [15,0,316], [17,0,333], [19,0,349], [20,0,366], [20,0,383], [20,0,400], [20,0,430]]
每个点的组成为:[x, y, timestamp],含义如下:
- x 坐标: 从0开始,一直到滑动结束, 每个坐标间隔越大说明滑动越快,静止不动就不变。
- y 坐标: 可以为正,也可以为负数,都是个位数。取值范围: [-2, 2), 多取值0,次之是-1,极少的-2和正1
- timestamp: 鼠标在当前点停留的时间(毫秒)
经反复测试得知还有如下规律:
- 滑动轨迹第一个坐标点(X,Y)是负数,其取值范围在(-40, -18)
- 第二个坐标点是0,0,0,从第三甚至第四个坐标点开始
- y坐标的取值范围比较简单,人手横向滑动的轨迹一般负责先减少,在快速增加,再慢慢减少的轨迹。
因为y的取值比较简单,只考虑x坐标与z坐标的关系,将手工调试取10个坐标,以时间为X坐标,滑动距离为Y坐标,打印出来绘图为:
7.jpg
图像的轨迹有点儿像 tanh
和 arctan
的混合体,如下图:
8.jpg
我们将两个图像整合、移动并添加一些噪点,最终生成的图像为:
9.jpg
这样的图像很像我们之前采集的鼠标轨迹图像了。至此,鼠标滑动轨迹的X坐标生成方法就告一段落。剩下的是滑动时间的分配。
对上面十次滑动的坐标集合,计算出每个坐标点消耗的时间,对时间进行汇总,如下表: