千锋教育-做有情怀、有良心、有品质的职业教育机构

手机站
千锋教育

千锋学习站 | 随时随地免费学

千锋教育

扫一扫进入千锋手机站

领取全套视频
千锋教育

关注千锋学习站小程序
随时随地免费学习课程

当前位置:首页  >  技术干货  > 爬虫之js逆向解析(滑块验证码)

爬虫之js逆向解析(滑块验证码)

来源:千锋教育
发布人:qyf
时间: 2022-09-19 17:44:53 1663580693

  为什么要做逆向

  动态网页爬虫一般可分为两种:Selenium爬取和接口爬取。两种方式各有优缺点:前者我们己经介绍了selenium的使用和验证码、滑块的使用,其虽然可以很好地处理网页异步加载问题,但面对大型爬虫任务时,效率还是比较低的;后者虽然爬取速度较快,但请求参数很可能是动态变化的,这时就需要利用一些前端的知识,重新构造参数,整个过程通常称为JS逆向。先来看一下简单的请求:

Picture

  但是往往在我们编写爬虫时,可能会碰到以下两种问题:

  • 所需要爬取的数据在网页源代码中并不存在;

  • 点击下一页跳转页面时,网页的URL 并没与发生变化;

  造成这种问题原因是,你所正在爬取的页面采取了动态加载的方式

  动态加载网页其显示的页面则是经过Javascript处理数据后生成的结果,可以发生改变。

  JavaScript是一种运行在浏览器中的解释型编程语言,JavaScript非常值得学习,它既适合作为学习编程的入门语言,也适合当作日常开发的工作语言。JavaScript可以收集用户的跟踪数据,不需要重载页面即可直接提交表单,可在页面中嵌入多媒体文件,甚至可以运行网页游戏等。在很多看起来非常简单的页面背后通常使用了许多JavaScript文件。比如:

Picture(1)

  这些数据的来源有多种,可能是经过Javascript计算生成的,也可能是通过Ajax加载的。Ajax = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML),其最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页的内容。

  逆向工程

  对于动态加载的网页,我们想要获取其网页数据,需要了解网页是如何加载数据的,该过程就被成为逆向工程。

  对于使用了Ajax 请求技术的网页,我们可以找到Ajax请求的具体链接,直接得到Ajax请求得到的数据。

  需要注意的是,构造Ajax请求有两种方式:

  • 原生的Ajax请求,会直接创建一个XMLHTTPRequest对象。

  • 调用jQuery的ajax()方法。一般情况下,$.ajax()会返回其创建的XMLHTTPRequest对象;但是,如果$.ajax()的dataType参数指定了为script或jsonp类型,$.ajax()不再返回其创建的XMLHTTPRequest对象。

  JQuery补充:

  在大型互联网公司的不断推广下,JavaScript生态圈也在不断的完善,各种类库、API接口层出不穷。

  jQuery是一个快速、简洁的JavaScript框架,是继Prototype之后又一个优秀的JavaScript代码库(或JavaScript框架)。jQuery设计的宗旨是“Write Less, Do More”,即倡导写更少的代码,做更多的事情。

  对于这两种方式,只要创建返回了XMLHTTPRequest对象,就可以通过Chrome浏览器的调试工具在NetWork窗口通过设置XHR过滤条件,直接筛选出Ajax请求的链接;如果是$.ajax()并且dataType指定了为script或jsonp,则无法通过这种方式筛选出来。

  案例分析

  这次搞得还是滑块哦���,话不多说直接开搞数美滑块,因为小红书、蘑菇街、脉脉、斗鱼等很多都用了数美的验证码。整体难度还可以就是动态参数有点东西的呢!

  数美验证码官网:https://www.ishumei.com/trial/captcha.html

Picture(2)

  数美滑块的验证码主要的难点有以下几点:

  request的请求参数,是动态变化的。名称是动态变化,加密的密钥也是动态变化的,这就有点难搞了

  每天小版本更新的频率1-2次,必须得能够实现完全自动化,否则人工很难及时的调整验证码的参数,来不及。

  js里的混淆的变量也是动态变化的

  验证码注册

Picture(3)

  先看一下register

Picture(4)

  下图是响应结果:bg和fg是验证码图片地址 https://castatic.fengkongcloud.com/bg

Picture(5)

  计算滑块位置

  根据上一步可以得到验证图片的地址。

  验证码图片:https://castatic.fengkongcloud.com/crb/set-000006/v2/07ee613eeb1b43bed7daa24c7b288ea0bg.jpg

  滑块图片:https://castatic.fengkongcloud.com/crb/set-000006/v2/07ee613eeb1b43bed7daa24c7b288ea0fg.png

  使用opencv查找并匹配图像模板中的滑块。

  需要注意的是,这里是以原图计算的,而页面上的图片大小只有(300,150),(应用不同的产品可能大小也不同)

  所以需要按比例进行缩小或者放大。

  验证

  对应的api地址是:https://captcha.fengkongcloud.com/ca/v2/fverify?...

Picture(6)

  查询字符串参数:

Picture(7)

  params参数里的 dv,qe,ou,cf等等,都经过了DES加密,

  破解方式分析

  打开控制台多看几遍请求过程,我们基本就明白请求步骤了。具体的分析过程就不再赘述。

  所携带的请求参数如下:

  该接口返回的js参数,是下一步需要请求的目标。

  提取js参数

  js地址:https://castatic.fengkongcloud.com/pr/auto-build/v1.0.3-144/captcha-sdk.min.js

  需要提取该js中的参数名,会在最后验证的时候使用(注:一般情况下参数名不会变),但是这些请求参数都是变化的。

  获取js的response,搜索上面的参数我们没有找到,但是发现了倒序的名字

  通过查看调用栈,打断点,一层层分析,发现js做了ob混淆。

  JS混淆有很多种,这里举几个:UglifyJS,JScrambler,jsbeautifier.org,JSDetox,obfuscator.io 等,像下面的代码就是ob混淆。

  开头定义了一个大数组,然后对这个大数组里的内容进行位移,再定义一个解密函数。后面大部分的值都调用了这个解密函数,以达到混淆的效果。如果想还原可以使用ob混淆还原工具:https://github.com/DingZaiHub/ob-decrypt

  当然不进行混淆还原也可以通过断点很快的定位到具体的函数加密的位置

  再次请求走到这里,而这里是一部分的参数的加密,先进去看下它是怎么加密的

  进来了走到这可以看到是DES加密,参数分别是加密的密码,要加密的参数,后面两个是数字呢就是模式选择了,1,0是加密,0,0是解密,在这里是加密。

  我们输出在console中输出一下,这四个参数看一下

  那么问题来了,这个密码"b64ccadf"哪来的呢,别急,我们重新再来一遍!很快我们又进来走到这,_0x1c2865是什么怎么是乱码的呢?

  console输出一下看看

Picture(8)

  密码搞到了,加密方式也晓得了,然后参数一个一个整过去就Ok了。

  返回结果response:

Picture(9)

  message = success,riskLevel=PASS 说明验证通过

  完整代码

  """

  数美滑块验证码破解验证

  """

  import base64

  import json

  import random

  import re

  import time

  from io import BytesIO

  import cv2

  import numpy as np

  import requests

  from pyDes import des, ECB

  CAPTCHA_DISPLAY_WIDTH = 310

  CAPTCHA_DISPLAY_HEIGHT = 155

  p = {}

  def pad(b):

  """

  块填充

  """

  block_size = 8

  while len(b) % block_size:

  b += b'\0'

  return b

  def split_args(s):

  """

  分割js参数

  """

  r = []

  a = ''

  i = 0

  while i < len(s):

  c = s[i]

  if c == ',' and (a[0] != '\'' or len(a) >= 2 and a[-1] == '\''):

  r.append(a)

  a = ''

  elif c:

  a += c

  i += 1

  r.append(a)

  return r

  def find_arg_names(script):

  """

  通过js解析出参数名

  """

  names = {}

  a = []

  for r in re.findall(r'function\((.*?)\)', script):

  if len(r.split(',')) > 100:

  a = split_args(r)

  break

  r = re.search(r';\)(.*?)\(}', script[::-1]).group(1)

  v = split_args(r[::-1])

  d = r'{%s}' % ''.join([((',' if i else '') + '\'k{}\':([_x0-9a-z]*)'.format(i + 1)) for i in range(15)])

  k = []

  r = re.search(d, script)

  for i in range(15):

  k.append(r.group(i + 1))

  n = int(v[a.index(re.search(r'arguments;.*?,(.*?)\);', script).group(1))], base=16)

  for i in range(n // 2):

  v[i], v[n - 1 - i] = v[n - 1 - i], v[i]

  for i, b in enumerate(k):

  t = v[a.index(b)].strip('\'')

  names['k{}'.format(i + 1)] = t if len(t) > 2 else t[::-1]

  return names

  def get_encrypt_content(message, key, flag):

  """

  接口参数的加密、解密

  """

  des_obj = des(key.encode(), mode=ECB)

  if flag:

  content = pad(str(message).replace(' ', '').encode())

  return base64.b64encode(des_obj.encrypt(content)).decode('utf-8')

  else:

  return des_obj.decrypt(base64.b64decode(message)).decode('utf-8')

  def get_random_ge(distance):

  """

  生成随机的轨迹

  """

  ge = []

  y = 0

  v = 0

  t = 1

  current = 0

  mid = distance * 3 / 4

  exceed = 20

  z = t

  ge.append([0, 0, 1])

  while current < (distance + exceed):

  if current < mid / 2:

  a = 15

  elif current < mid:

  a = 20

  else:

  a = -30

  a /= 2

  v0 = v

  s = v0 * t + 0.5 * a * (t * t)

  current += int(s)

  v = v0 + a * t

  y += random.randint(-5, 5)

  z += 100 + random.randint(0, 10)

  ge.append([min(current, (distance + exceed)), y, z])

  while exceed > 0:

  exceed -= random.randint(0, 5)

  y += random.randint(-5, 5)

  z += 100 + random.randint(0, 10)

  ge.append([min(current, (distance + exceed)), y, z])

  return ge

  def make_mouse_action_args(distance):

  """

  生成鼠标行为相关的参数

  """

  ge = get_random_ge(distance)

  args = {

  p['k']['k5']: round(distance / CAPTCHA_DISPLAY_WIDTH, 2),

  p['k']['k6']: get_random_ge(distance),

  p['k']['k7']: ge[-1][-1] + random.randint(0, 100),

  p['k']['k8']: CAPTCHA_DISPLAY_WIDTH,

  p['k']['k9']: CAPTCHA_DISPLAY_HEIGHT,

  p['k']['k11']: 1,

  p['k']['k12']: 0,

  p['k']['k13']: -1,

  'act.os': 'android'

  }

  return args

  def get_distance(fg, bg):

  """

  计算滑动距离

  """

  target = cv2.imdecode(np.asarray(bytearray(fg.read()), dtype=np.uint8), 0)

  template = cv2.imdecode(np.asarray(bytearray(bg.read()), dtype=np.uint8), 0)

  result = cv2.matchTemplate(target, template, cv2.TM_CCORR_NORMED)

  _, distance = np.unravel_index(result.argmax(), result.shape)

  return distance

  def update_protocol(protocol_num, js_uri):

  """

  更新协议

  """

  global p

  r = requests.get(js_uri, verify=False)

  names = find_arg_names(r.text)

  p = {

  'i': protocol_num,

  'k': names

  }

  def conf_captcha(organization):

  """

  获取验证码设置

  """

  url = 'https://captcha.fengkongcloud.com/ca/v1/conf'

  args = {

  'organization': organization,

  'model': 'slide',

  'sdkver': '1.1.3',

  'rversion': '1.0.3',

  'appId': 'default',

  'lang': 'zh-cn',

  'channel': 'YingYongBao',

  'callback': 'sm_{}'.format(int(time.time() * 1000))

  }

  r = requests.get(url, params=args, verify=False)

  resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))

  return resp

  def register_captcha(organization):

  """

  注册验证码

  """

  url = 'https://captcha.fengkongcloud.com/ca/v1/register'

  args = {

  'organization': organization,

  'channel': 'DEFAULT',

  'lang': 'zh-cn',

  'model': 'slide',

  'appId': 'default',

  'sdkver': '1.1.3',

  'data': '{}',

  'rversion': '1.0.3',

  'callback': 'sm_{}'.format(int(time.time() * 1000))

  }

  r = requests.get(url, params=args, verify=False)

  resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))

  return resp

  def verify_captcha(organization, rid, key, distance):

  """

  提交验证

  """

  url = 'https://captcha.fengkongcloud.com/ca/v2/fverify'

  args = {

  'organization': organization,

  p['k']['k1']: 'default',

  p['k']['k2']: 'YingYongBao',

  p['k']['k3']: 'zh-cn',

  'rid': rid,

  'rversion': '1.0.3',

  'sdkver': '1.1.3',

  'protocol': p['i'],

  'ostype': 'web',

  'callback': 'sm_{}'.format(int(time.time() * 1000))

  }

  args.update(make_mouse_action_args(distance))

  key = get_encrypt_content(key, 'sshummei', 0)

  for k, v in args.items():

  if len(k) == 2:

  args[k] = get_encrypt_content(v, key, 1)

  print(args)

  r = requests.get(url, params=args, verify=False)

  resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))

  return resp

  def get_verify(organization):

  """

  进行验证

  """

  resp = conf_captcha(organization)

  protocol_num = re.search(r'build/v1.0.3-(.*?)/captcha-sdk.min.js', resp['detail']['js']).group(1)

  if not p.get('id') or protocol_num != p['i']:

  update_protocol(protocol_num, ''.join(['https://', resp['detail']['domains'][0], resp['detail']['js']]))

  resp = register_captcha(organization)

  rid = resp['detail']['rid']

  key = resp['detail']['k']

  domain = resp['detail']['domains'][0]

  fg_uri = resp['detail']['fg']

  bg_uri = resp['detail']['bg']

  fg_url = ''.join(['http://', domain, fg_uri])

  bg_url = ''.join(['http://', domain, bg_uri])

  r = requests.get(fg_url, verify=False)

  fg = BytesIO(r.content)

  r = requests.get(bg_url, verify=False)

  bg = BytesIO(r.content)

  distance = get_distance(fg, bg)

  print(distance)

  r = verify_captcha(organization, rid, key, int(distance / 600 * 310))

  return rid, r

  def test():

  organization = 'RlokQwRlVjUrTUlkIqOg'

  # rid是验证过程中响应的标示,r是最后提交验证返回的响应

  rid, r = get_verify(organization)

  print(rid, r)

  # riskLevel为PASS说明验证通过

  if r['riskLevel'] == 'PASS':

  # 具体可抓包查看,接口:/api/sns/v1/system_service/slide_captcha_check

  pass

  if __name__ == '__main__':

  test()

  大家用同样的方法赶快试一试小红书,蘑菇街等网站登陆吧!

tags:
声明:本站稿件版权均属千锋教育所有,未经许可不得擅自转载。
10年以上业内强师集结,手把手带你蜕变精英
请您保持通讯畅通,专属学习老师24小时内将与您1V1沟通
免费领取
今日已有369人领取成功
刘同学 138****2860 刚刚成功领取
王同学 131****2015 刚刚成功领取
张同学 133****4652 刚刚成功领取
李同学 135****8607 刚刚成功领取
杨同学 132****5667 刚刚成功领取
岳同学 134****6652 刚刚成功领取
梁同学 157****2950 刚刚成功领取
刘同学 189****1015 刚刚成功领取
张同学 155****4678 刚刚成功领取
邹同学 139****2907 刚刚成功领取
董同学 138****2867 刚刚成功领取
周同学 136****3602 刚刚成功领取
相关推荐HOT