本帖最后由 qiujunjian1 于 2024-01-20 19:58 编辑
目标网站aHR0cHM6Ly9zZWN1cmUuZWxvbmcuY29tL3Bhc3Nwb3J0L2xvZ2luX2NuLmh0bWw/bmV4dHVybD1odHRwczovL3d3dy5lbG9uZy5jb20v
这一次要分析的是这个数美的点选验证。 分析请求接下来对这个验证的请求进行抓包分析,看看有哪几个请求是需要逆向的。 首先是一个register请求,这个对应的是图片的获取 payload参数如图,其中的mode表示的是验证模式,当前是点选验证,如果model的值是slide ,那么说明是滑块验证。 返回的数据是一张点选的图片,还有order字段的值 通过preview可以看到,这个order字段就是文字内容。 所以我们接下来要做的事情就是通过调用这个请求,拿到返回的图片和文字数据,然后通过图片识别出文字的位置,找到坐标点,然后通过代码去构造坐标数据。 接着当我们对图片进行点选的时候,会产生这么一个请求 会发送一个verify请求,如果失败了,会返回REJECT,如果成功的话则是 PASS。 然后再往上会有一个conf请求,这个请求的payload参数和register提交的参数是一样的,所以这俩请求分析一个就可以了。 那么需要分析的核心的请求就只有下面三个: https://captcha1.fengkongcloud.cn/ca/v2/fverify https://captcha1.fengkongcloud.cn/ca/v1/register https://captcha1.fengkongcloud.cn/ca/v1/conf
动态JS无法调试在分析之前我们需要先解决一个问题, 这个conf请求调用栈的JS文件,是一个动态变化的,每次url都不一样,这样就导致我们的断点在下断以后再次断下。 这个问题的解决方案在于将加载JS时的url替换为固定的,可以通过charles或者mimproxy来解决,这两个都可以作为代{过}{滤}理拦截http请求。 在Charles中捕获这个请求,然后右键->SaveResponse,把页面源码保存下来。 打开Charles的Map Local功能
添加一个Mapping,设置了这个以后,当有网络请求访问了我们指定的这个map页面,就会被替换为本地保存的html文件,这样就可以保证JS一直是固定的了。 也可以用mitmproxy写一个拦截脚本来处理这个问题,代码如下: from mitmproxy import http from mitmproxy.http import Request
def request(flow): if flow.url.startswith("https://secure.elong.com/passport/login_cn.html?nexturl=https://www.elong.com/"): with open("res.html",mode="rb",encoding="utf-8") as f: content=f.read()
flow.response = Response.make( 200, content, {"Content-Type":"text/html"} )
def response(flow: http.HTTPFlow): pass
不过还是Charles方便,直接配置好就可以了。 代码混淆处理然后我们来解决代码混淆的问题。 接着抓一个包,找到conf这个请求,这个请求指向的是api.js文件,也就是我们刚刚替换的那个动态的JS 点进去发现,所有的代码都是经过混淆的。 var _0x19e1cf = _0x136e2f[_0x2ae8e9(0x2fd)]
这种混淆实际上就是把字符串,替换成了函数调用,用来干扰分析。我们把这个JS文件保存下来,做一个整体的分析 一共有四个大函数,其中两个是自执行函数。这个页面的混淆代码大部分在第三个自执行函数里面,其他三个函数代码量都比较小。 其中大部分的混淆代码都是在执行_0x1f0d 这个函数。 那么我们就可以把除了第三个大函数以外的函数全部抠下来,拿到本地,去执行一下加密的函数,看能不能得到结果 经过实际测试,确实是可以正常运行,并且打印出对应的字符串。 console.log(_0x1f0d(process.argv[2]))
然后我们把这个混淆函数的参数替换为命令行参数,从而使用python来调用,来达到批量去混淆的目录,python代码如下: import subprocess import re
def Decode(hex_rg): res= subprocess.check_output(f"node main.js {hex_rg}",shell=True) res_string=res.decode("utf-8").strip() return res_string
def run(): with open("f1.js", mode="r", encoding="utf-8") as fr, open("f2.js", mode="w", encoding="utf-8") as fw: for line in fr: match_list = re.findall("(_0x425d8a\((.*?)\))", line) if not match_list: fw.write(line) continue
for func_string,hex_str in match_list: line=line.replace(func_string,f'"{Decode(hex_str)}"') fw.write(line)
if __name__ == '__main__': run()
这个脚本做的事情就是打开f1.js,读取里面的内容,通过正则匹配的方式筛选出符合要求的代码,通过调用nodejs解混淆脚本得到结果,对其进行批量替换。 等遇到需要重点分析的代码,可以用这个方式可以去除部分混淆,提高一些代码可读性,帮助分析代码。这个脚本在后面的分析里面可以帮我们节省很多时间。 conf请求分析我们先来分析第一个conf请求,里面携带了这么几个参数 appId: default organization: xQsKB7v2qSFLFxnvmjdO callback: sm_1705412287345 sdkver: 1.1.3 model: select captchaUuid: 20240116213802Zwas5htESARemRJWfW rversion: 1.0.4 lang: zh-cn channel: DEFAULT
organization是一个ID,这个是固定的,多测几次就会发现,而callback是一个时间戳,那么对于conf这个请求,我们只需要去分析captchaUuid这个字段就可以了。 找到conf请求的调用堆栈,在中间的一个位置打一个断点,反正都是混淆的,在哪都没区别,只要在附近找到了需要跟踪的字段,一直往上找就行了。 断下以后,当前的作用域里面,并没有我们需要的参数,所以需要沿着调用栈一直往上翻 翻到这个调用栈的时候,终于找到了我们需要的相关参数 'captchaUuid': _0x2049e1
所以我们现在就要往上找_0x2049e1 里面的值是从哪来的 _0x2049e1 在这个位置被赋值,那我们接下来就要分析这个代码。
_0x2049e1 = userConfig[_0x4c37ed(0x3be)] || _0x332a8d[_0x243c3a[_0x4c37ed(0x483)]][_0x4c37ed(0x4f4)]();
这个代码实际上就是一个函数调用 这里可以对这个位置的代码进行选中,然后跳转到相应的代码页面 就跳到了这个位置,代码如下: 'getCaptchaUuid': function _0x2d350e() { var _0x49a2b0 = _0x247065 , _0x1163e8 = '' , _0x40ad39 = _0x49a2b0(0x42a) , _0x3531f2 = _0x40ad39['length']; for (var _0x235133 = -0x149e + -0x231d * 0x1 + -0x37bb * -0x1; _0x235133 < -0x2214 + 0xd * 0x223 + 0xe9 * 0x7; _0x235133++) { _0x1163e8 += _0x40ad39['charAt'](Math[_0x49a2b0(0x2c6)](Math[_0x49a2b0(0x134)]() * _0x3531f2)); } return _0x529f28[_0x49a2b0(0x294)](this[_0x49a2b0(0x232)](), _0x1163e8); },
这个代码的分析就没有什么技术含量了,一行一行硬看,反正也没有多少代码 captchaUuid: 20240116213802Zwas5htESARemRJWfW
参考这个captchaUuid 的值,一边调试一边分析,我这里直接说结论 captchaUuid=当前时间+17个随机字符
实现代码如下: def gen_captcha_uuid(): total_string = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" part = "".join([random.choice(total_string) for i in range(18)]) ctime = datetime.datetime.now().strftime("%Y%m%d%H%M%S") captcha_uuid = f"{ctime}{part}" return captcha_uuid
这样的话这个请求我们就搞定了。 分析fverify请求整体代码分析fverify请求携带的参数如下: en: y+ugz9NIWys= dy: Rfpr5oqb5y4= xy: YabT6nmJOC0= tb: 3jSn4gNaAVM= mu: 9Zlg08y1MpaONqmLgK0lCn6u9wurdUqB5gECmblJXlSmnXHCWXXjiqvWSDI1PvzKgJdsNe46/hqf7zZh2MbjiPprUUXfMXi5+mLlFmVm6mrUS0NZ6OhMKZzNznVo51GI44gF1jVQF7Dh4/k62Zo93u4USF1RoqZU4LXyLyhHZMdhlk5nk5/NUMITc7kJqy7ngnBWIDPhl72rZtFxlvPKXxMXZnJU+GwA5pnf4/41T4rM6rYRV9Xr2E9XCLBhKno3eQAtSxt7wlDl6GMcg3LJ81dZGD6UVwy1DJJuG6fMv/M= oc: h9oFKi8cHpg= mp: WYfkIZp7GoA= nu: C0kH/bWLjw8= qd: adKA4Y0ncgGB4U6j47xchBZw9rO2THeeS/Z7vaOjAeKvmR57DOi6wsSJgovJUg2YQGQgooGYYrd2BhXCCkVQeY2gk1w7g2IquFeRYOlajtwqm5aZTx8FBx6ASb9VM7LNiZc0cVEnpnCrnXt0ICRagsT4sIYxGFg+EPenpHhwldu1Ufb8VNYIIrYBESmTOhpdttfd8gUBE36NGzqeXQpvRiqf4JT6eWB73TzFVEtzcjUHskwpvgqSmTYoQwG/gjy5APzSd96aTgTzKeX8I/wdupwYJnWvjmWWJ2R4JprB45o= ww: aOGVECVeH60= kq: mtlOTdT5LOE= jo: lQ90183KgD4=
固定: ostype: web sdkver: 1.1.3 protocol: 180 rversion: 1.0.4 organization: xQsKB7v2qSFLFxnvmjdO act.os: web_pc
已分析 captchaUuid: 20240117123606cSbtzkfecDkBDbC7Sr callback: sm_1705466200576
register接口的返回值 rid: 202401171236155825d945349adfd458
这个请求里面,排除掉不需要分析的部分,剩下需要分析的字段一共有12个。 查看请求的调用栈,在最后两个调用栈下断点 断下之后来分析下当前的这一行代码 this[_0x2c1c24(0x1b5)](_0x5c100a, _0x599170, _0x1ed2cb, _0x28800d, _0x5c6636, _0x4d541b);
为了方便阅读,我把这个代码进行简化 this[""sendRequest""]('https://', "captcha1.fengkongcloud.cn", "/ca/v2/fverify", _0x28800d, _0x5c6636, _0x4d541b);
这个位置实际上是在发送https请求,前面三个参数是在拼接请求的域名 而第四个参数是一个对象,里面包含的内容就是我们需要跟踪的字段。那么我们现在就摇追踪_0x28800d 的来源,看这个数据是怎么生成的。 我们先把这一段代码的混淆给处理一下,用之前解混淆的那个方法。 var 0x2d6657 = { 'nyzWi': "mouseMoveX", 'SZdlb': _0x6f9c3c["pzOLX"], 'TGQiS': function (_0x569f5d, _0x276cc4) { var _0x5d1724 = _0x2c1c24; return _0x6f9c3c["onIeC"](_0x569f5d, _0x276cc4); } };
var _0x1fe1cf, _0x44bcff = this["_config"], _0x599170 = _0x44bcff['domains'], _0x3d6fed = _0x44bcff["fVerifyUrlV2"], _0x1ed2cb = _0x3d6fed === undefined ? _0x3b4628 : _0x3d6fed, _0x49bb92 = _0x44bcff["organization"], _0x20df23 = _0x44bcff["appId"], _0x4143cf = _0x44bcff["channel"], _0x435e2e = _0x44bcff['VERSION'], _0x7306d7 = _0x44bcff['lang'], _0x294474 = _0x44bcff["SDKVER"], _0x1b27c8 = _0x44bcff["_successCallback"], _0x2d5257 = _0x44bcff["mode"], _0x5b58d1 = this['_data'], _0x536387 = _0x5b58d1["errMsg"], _0x74fdb5 = _0x5b58d1["trueWidth"], _0x31e834 = _0x6f9c3c["tIGQt"](_0x74fdb5, undefined) ? -0x1338 + -0x145f + -0x7eb * -0x5 : _0x74fdb5, _0x3717b1 = this["getRegisterData"](_0x6f9c3c["ZGnpS"]), _0x298b01 = this["getMouseAction"](), _0x528bd = _0x6f9c3c["VQpVP"], _0x49f479 = this["getSafeParams"](), _0x28800d = _0x2460cd[_0x6f9c3c["Nlbsb"]]["extend"]((_0x1fe1cf = { 'organization': _0x49bb92 },
_0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, 'mp', this['getEncryptContent'](_0x20df23, _0x6f9c3c["NnjXa"])), _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, 'oc', this["getEncryptContent"](_0x4143cf, "c2659527")), _0x1f0d12['default'])(_0x1fe1cf, 'xy', this["getEncryptContent"](_0x7306d7, _0x6f9c3c["wIUSM"])), _0x1f0d12["default"])(_0x1fe1cf, 'jo', this["getEncryptContent"](_0x49f479, _0x6f9c3c["HwXkn"])), _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, _0x6f9c3c["ZGnpS"], _0x3717b1), _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, _0x6f9c3c["CJfJJ"], _0x435e2e), _0x1f0d12[_0x6f9c3c['Nlbsb']])(_0x1fe1cf, _0x6f9c3c["WWhmm"], _0x294474), _0x1f0d12[_0x6f9c3c['Nlbsb']])(_0x1fe1cf, _0x6f9c3c["VRUfH"], "180"), _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, "ostype", _0x528bd), _0x1fe1cf), _0x298b01)
_0x2460cd["default"]["log"](_0x119bdb["LOG_ACTION"]["SEND_VERIFY"]), this["sendRequest"](_0x5c100a, _0x599170, _0x1ed2cb, _0x28800d, _0x5c6636, _0x4d541b);
当然也可以不处理,直接调试,可能你有解混淆的功夫,人家代码都已经抠完了。 这一段代码在做的事情首先是定义了一个organization 的字典,然后往这个字典里面进行赋值;而_0x298b01 这个也是一个字典,然后再通过extend操作对两个字典进行合并。 而这四个字段的值,都是通过调用加密函数getEncryptContent,然后传入两个参数,来获取到的参数,所以我们可以先对这四个字段进行分析。 然后再对这个函数进行化简,我们要知道传入的参数是什么。化简也没什么好方法,就是一个个的手动替换。 xy: YabT6nmJOC0= oc: h9oFKi8cHpg= mp: WYfkIZp7GoA= jo: lQ90183KgD4=
既然这个四个字段传入的参数是固定的,那么返回的值肯定也是固定的,所以这几个参数我们可以直接写死了。 getEncryptContent函数分析接着来分析这个加密函数 把鼠标选中内容,然后就可以跳转到对应的位置 就可以定位到加密函数的位置 然后用手动挡去混淆的方式,可以看到大致的一些信息,盲猜是一个DES和base64加密。 'mp',this['getEncryptContent']('default', '9cc268c1'), 'oc',this["getEncryptContent"]('DEFAULT', "c2659527")), 'xy',this["getEncryptContent"]('zh-cn', 'b1807581')), 'jo',this["getEncryptContent"]('10', '6d005958')),
那么这里就可以做一个大胆的尝试,用python算法实现一个DES和base64加密,把第一个参数当作是需要加密的字符串,第二个参数当作是Key,看能否输出对应的结果 实现代码如下: if __name__ == '__main__': key = b'9cc268c1' data_string = 'default'
pad_func = lambda text: text + '\0' * (DES.block_size - (len(text.encode('utf-8')) % DES.block_size)) aes = DES.new(key, DES.MODE_ECB) enc_data = aes.encrypt(pad_func(data_string).encode("utf8")) res = base64.b64encode(enc_data).decode('utf-8') print(res)
然后查看运行的结果 mp: WYfkIZp7GoA=
和我们分析的请求中的mp结果是完全一致的。这种方式有些取巧,一部分情况可能不太好使,所以我们还有第二种方式,直接扣代码,大力出奇迹。 把鼠标放在这个位置,直接跳转到DES源码 跳转到这个位置之后 把这个函数还有Base64加密的函数全部扣下来,然后运行,缺什么扣什么,一直扣到不报错为止。 这样也可以拿到同样的结果 分析其他参数前面四个通过DES加密的参数我们已经分析完了,接下来需要分析这个_0x298b01 里面的参数来源。 通过处理过的JS代码可以找到来源,_0x298b01 来自于this["getMouseAction"]() 的函数结果。通过函数名字getMouseAction 大概可以猜到这个对象的数据应该是记录的一些鼠标的坐标信息。 然后跳转到函数代码的位置,我们把这段代码使用解混淆的脚本进行处理。 ![1705496617569](019 数美点选验证协议逆向.assets/1705496617569.png) 重点关注case "spatial_select" 里面的代码,这个里面就是我们所需要的参数,这个switch里面对应的应该是各个不同的验证分支。 spatial_select 对应的是点选,slide 对应的是滑块。
![1705497808508](019 数美点选验证协议逆向.assets/1705497808508.png) 然后函数结束的地方还有其他的一些返回数据 _0x4639e5['qd'] = this["getEncryptContent"](_0x23de95, '3c9ed5cb'),
_0x4639e5['mu'] = this['getEncryptContent'](_0x3602ea, "e7e1eb0d"),
_0x4639e5['ww'] = this["getEncryptContent"](_0x6f9c3c["pxDrO"](_0x53f1f2, _0x5caf5a), '17a94a08'),
_0x4639e5['nu'] = this['getEncryptContent'](_0x4c1632, "390aac0d"),
_0x4639e5['dy'] = this["getEncryptContent"](_0x1a7546, "a9001672"),
_0x4639e5["act.os"] = _0x46483a;
_0x4639e5['tb'] = this["getEncryptContent"](_0x2460cd[_0x6f9c3c["Nlbsb"]]["__userConf"]["console"], '6f5e9847'),
_0x4639e5['en'] = this["getEncryptContent"](_0x2460cd[_0x6f9c3c["Nlbsb"]]["runBotDetection"](), "9fc1337f"),
_0x4639e5['kq'] = this["getEncryptContent"](-(0x7 * -0x537 + -0x1f77 + 0x1 * 0x43f9), _0x6f9c3c['SGQFW'])
这些参数都是通过getEncryptContent 这个函数进行加密的,那么我们只需要搞清楚传入的参数分别是什么数据,就可以对整个请求进行模拟了。 第一个参数和第二个参数是一样的,是一个四个成员的数组,这个就对应了我们进行点选的坐标。 而另外三个参数则是固定值 后两个参数是整张点选图片的宽度和高度,可以直接看下_4fc323 这个对象 里面是这个图片对象的信息,包括图片的宽度和高度,然后把剩下的参数也处理一下 _0x4639e5['qd'] = this["getEncryptContent"](_0x23de95, '3c9ed5cb'), _0x4639e5['mu'] = this['getEncryptContent'](_0x3602ea, "e7e1eb0d"), _0x4639e5['ww'] = this["getEncryptContent"](28504615, '17a94a08'), _0x4639e5['nu'] = this['getEncryptContent'](300, "390aac0d"), _0x4639e5['dy'] = this["getEncryptContent"](150, "a9001672"), _0x4639e5["act.os"] = _0x46483a; _0x4639e5['tb'] = this["getEncryptContent"](1, '6f5e9847'), _0x4639e5['en'] = this["getEncryptContent"](0, "9fc1337f"), _0x4639e5['kq'] = this["getEncryptContent"](-1, 'ebee8dcc')
这一部分的字段,除去_0x23de95 是需要分析的坐标信息外,其他的都可以通过传参的方式,调用getEncryptContent 函数来获取到对应的值。 分析坐标算法接下来我们需要对剩下的两个坐标信息的参数进行分析 其中,_0x23de95 是selectData ,_0x3602ea 是mouseData ,这两个数据全部都来自于this['data'] 这里我们通过调用栈,找到最上一层的堆栈 定位到这个位置发现,this['_data']['mouseData'] 被_0x226da4 赋值了,所以我们继续跟踪_0x226da4 的值。 正常来说switch case分支里面的代码不会被依次执行,但是这个函数不太一样,外面套了一层while循环,然后在switch的变量是一个数组,里面的值是2,0,3,4,1 ,所以这个switch分支会按照20341的顺序去执行。 接下来需要对这整个函数进行分析,还是先用脚本去除部分的混淆 来分析去除混淆后的代码,这样比较方便,在54行这里通过push操作往selectData 里面添加数据,那么我们只需要看被添加的数据是在哪生成的 _0x27bb65 在这里被赋值,而这个就是我们要的坐标
_0x27bb65 = [ _0x42ca39, _0x1d0195["IXgiz"](_0x28ed43['y'], _0x1e9886) / _0x30fbb2, _0x52aa32 ];
这个坐标信息实际上可以拆解为列表中的三个元素,通过逗号分割开,第一个和第三个元素是一个坐标数据,中间的元素是一个算法。我们需要搞清楚这三个数组成员分别是什么。 第三个成员_0x52aa32 实际上就是一个字符串格式的时间戳,前两个应该是当前的坐标通过算法计算出来的结果。 第一个值的调用来源于上面一行代码 _0x42ca39 = _0x1d0195["rlnuS"](_0x28ed43['x'] - _0x5a1fbf, _0xd3e4df);
这个是一个函数调用,传递了两个参数,分别查看一下这几个数据是什么 第一个参数是当前鼠标的X坐标,这个坐标可能和真实的坐标不一样,可能是做了等比例缩放,后续可以通过算法的方式来构造。 第二个参数是图片的宽度,然后再来看一下函数的原型: 'rlnuS': function(_0x4781a3, _0x52d7dc) { return _0x4781a3 / _0x52d7dc;
这个函数的代码很简单,就是一个除法指令。 _0x1d0195["IXgiz"](_0x28ed43['y'], _0x1e9886) / _0x30fbb2
至于第二个值,是当前的y坐标,跟第一个值的算法几乎没啥区别。到这里,我们的坐标算法就分析完成了。 结束最后,如果想要把整个接口进行自动化,需要做这么几个事情。首先把上面的分析过程整理成代码,把每个表单提交的数据对应上, 然后需要对接打码平台,获取到点选验证的图片以后,通过打码平台的接口拿到坐标信息,通过提交坐标信息等数据通过点选验证的校验接口。这样就可以完成这个网站的自动化登陆了。我对这个过程并没有兴趣,只研究点选验证的原理以及逆向分析思路,各位有兴趣可以自行尝试。
注:若转载请注明大神论坛来源(本贴地址)与作者信息。
|