新浪微博移动网页端手势验证接口破解流程

1,455 阅读4分钟

最近做了一个APP,微博登录后超级话题自动签到。在抓取登录接口时,发现有的账号在登录时需要手势验证,于是研究了一下微博手势验证的算法。历时三天,终于成功了。

base64的解密和加密使用了包: js-base64,请先下载

1、判断是否需要验证

第一步,在账号登录前,需要判断账号是否需要手势验证。有些账号不需要验证,直接就允许登录,而有些账号则需要验证。 【GET】https://login.sina.com.cn/sso/prelogin.php?checkpin=1&entry=mweibo&su=aHR5d29ya18wQHNpbmEuY29t

  • 参数:
    • su:微博账号名的base64位加密
  • 返回:
    • showpin:为1时需要验证,为0时不需要验证

2、获取验证码

第二步,获取验证的id和验证码图片。图片大小为160x160,在一个5x5的div里,每个div的大小为32x32。 【GET】https://captcha.weibo.com/api/pattern/get?ver=1.0.0&source=ssologin&usrname=htywork_0@sina.com&line=160&side=100&radius=30&_rnd=0.053927914628985496

  • 参数:
    • usrname:用户名
  • 返回:
    • id:当前验证的id
    • path_enc:图片地址

返回数据:

{
    "id": "f8276e749e98c3cf0ac36ce9ee761ebc389ee761ebc3",
    "path_enc": "data:image/gif;base64,R0lGODdhoACgAIQAAJSSlNTS1KyurOzu7KSipMTCxOTi5JyanLS2tPz6/Nza3KyqrJSWlNTW1LSytPTy9KSmpMTGxOTm5JyenLy6vPz+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAoACgAAAF/mAljmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH5/fyD4YAkRDACAXgoTAAAMTRQIFI6Qj5GUk5aSmI5gmZeODgeKoZWckEChp6ipqqpgq6qEhK6nQAq1tre4ubq3YLu3DQixiw2+toZfFAwNx17JCsxdFADL0FvSz9Va0tTZWM7dWdvg3gDY41Xi5+gM5upS6e7v5fFT8PRP3/dNCIuh+k0PYhH61yRCKEYElyRIpCghkwaLEDpUAurPRCUOpl1Ucm1jEnseieQLWQQkySAd/k+K1KhyyMiWQEzqgRRA27xjiibUJMcNkIICACiEYxeNKI+XPRQwEIon5Q6nPhQEzSPTRtUcUi3egZqDKw+pTLeyfDo2KgCtdpDiULtDadi0ZbvG/XqW6k2y7ZJObTr3xlUcWakafTo46lK7PeUmpou2jle/d82+ddzXamWsdZsW7rr562G+iyGH1hGYb961kfVOpvN3Rmsbpbd29jub9Gexo2u8rhEb7mnIv0nvxd1jN43ejmvrVg74NtzcNIzPQM46NergmFfPke6Cewzq25m7Fg/bOWXori8DziwWe3TrbYc/L67+Bng5bK2S522+OnoZ3sFwXxyPWeaeffKp/uSWXo2dBFZS/ZE04HHstfQgXdqFNOF0FSoYYXkZerShDCM6dGF8IW5UooAdOvghfyletOILM/5zonAxTgSRAAzCJEIwB0CYY0IBKfIfiw2GJM0iEySAoo+oRBBfkh7BEtEDtg1JUCoOCEeliAx8SaGWBNXYgpn3LOgZmTa2iKCYMia4Hpv6oLmCnfGo2daLKroJm58iyokgnWkCSiGcUYyiaCeMchKEnllqEcCSslTqyqOGcohoEwEkUsynn2JK6J2CylNABUptykZ+RvgBiwuQUnZgERIsUEgLEiCw0x0BICCBFLv6KOywxBZr7LHIJqvsssw2mxAhRzp7AgALJkhLAwAGAPIrMoZU+wWWfARwq7Xklmvuueimq+667Lbr7rvwphECADs=|NV81XzE5XzE1XzNfMF83XzExXzRfMjRfNV8xM184XzIxXzFfMjNfMTdfMTBfMTZfMTRfMl8xOF8yMl82XzlfMTJfMjA=",
    "times": 3,
    "time": 0,
    "type": 1
}

然而,path_enc并不是base64位图片,而是图片和坐标信息,使用|分割,这时需要函数来处理, 首先要有html来展示路径:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    .pattern-father, .pattern { width: 160px; height: 160px; }
    .pattern-father { position: relative; border: 3px solid #b3b3b3; }
    .pattern:after{ content: '\200B'; display: block; height: 0; clear: both; }
    .pattern-children { float: left; width: 32px; height: 32px; }
    .pattern-canvas { position: absolute; z-index: 1; top: 0; left: 0; width: 100%; height: 100%; }
  </style>
</head>
<body>
<div class="pattern-father" id="pattern-father">
  <div class="pattern" id="pattern"></div>
  <canvas class="pattern-canvas" id="pattern-canvas" width="160" height="160"></canvas>
</div>
</body>
</html>

然后通过计算来显示路径:

/**
 * 计算图片位置
 * @param { string } bgUrl: 图片地址
 * @param { Array<string> } lock
 */
function recombineShadow(bgUrl, lock){
  let html = '';
  for(let e, f, g, h = lock[0], i = lock[1], j = lock.slice(2), l = 160, m = 160, n = 0; n < j.length; n++){
    e = l / h;
    f = m / i;
    g = '-' + j[n] % h * e + 'px -' + parseInt(j[n] / h) * f + 'px';
    html += `<div class="pattern-children" style="background-image: ${ bgUrl }; background-position: ${ g };"></div>`;
  }
  document.getElementById('pattern').innerHTML = html;
}

/**
 * 初始化图片地址
 * @param { string } imageUrl:base64图片|token
 */
function hint(imageUrl){
  if(imageUrl){
    const u = imageUrl.split('|');
    const bg = `url(${ u[0] })`;
    const lock = Base64.decode(u[1]).split('_');
    recombineShadow(bg, lock);
  }
}

通过hint函数,我们在html上展示了四宫格。

3、计算路径

【GET】https://captcha.weibo.com/api/pattern/verify?ver=1.0.0&id=29218517fb4894ae45ec0d1c80f93d54b8c80f93d54b&usrname=htywork_0@sina.com&source=ssologin&path_enc=&data_enc=

  • 参数:
    • id:验证码id
    • usrname:用户名
    • path_enc:四宫格的箭头顺序(比如'1234','1423')和id,使用p.encode加密后的字符串
    • data_enc:坐标使用q.encode加密后的字符串
  • 返回:
    • code:状态
    • msg:提示信息

参数示例:id=f8276e749e98c3cf0ac36ce9ee761ebc389ee761ebc3&path_enc=aa79797794c&data_enc=!)*(((((((((^)^)(((^)(((((((())(((()()()(*((((((())((((^*((^)^)^)^)(^*^6^3^6^7^3^/^-^*^*^0^2^2^5^,(^))()*)((((((*((())(()*((()(((|!)-^*^-^.^0^,^)^*^)^)^)^)(^*^*^,^*^,^/^*^*^*^*^)^*^-^)^)^,^)^)^,^)^*^*^)^.(^*^*^)^*^*^)^*(^*^)^)^*^*^)^)^))^)(()**((((((((((((()(((*/.22./0.,-,,),),-*,,*))(|(!(7B9@899::D3::9?99?8?:8?A9:6:?7::::8::9:?9:?999Cc6:9FM!(D1E199?8:9?99::9::99!*hj!)o?A?9:8:::9:9::?99:::9?99!*E(

坐标获取和计算的完整代码,其中pq函数都是直接从源代码内拷贝过来的,为防止计算错误,故没有格式化;canvas是为了显示轨迹:

/**
 1、【GET】https://login.sina.com.cn/sso/prelogin.php?checkpin=1&entry=mweibo&su=aHR5d29ya18wQHNpbmEuY29t&callback=jsonpcallback1523621527728
    判断是否需要验证码
    su:base64的用户名
    返回:showpin=1,需要验证码

 2、【GET】https://captcha.weibo.com/api/pattern/get?ver=1.0.0&source=ssologin&usrname=htywork_0@sina.com&line=160&side=100&radius=30&_rnd=0.053927914628985496&callback=pl_cb
    获取验证码
    返回:id:当前id,path_enc:验证码地址

 3、【GET】https://captcha.weibo.com/api/pattern/verify?ver=1.0.0&id=29218517fb4894ae45ec0d1c80f93d54b8c80f93d54b&usrname=htywork_0@sina.com&source=ssologin&path_enc=&data_enc=&callback=pl_cb
    验证验证码
    ver: 1.0.0
    id
    usrname: htywork_0@sina.com
    source: ssologin
    path_enc:路径和id的算法
    data_enc:路径坐标的算法
    返回:code:"100000",msg:"验证成功"
         code:"100001",msg:"验证码已过期"
         code:"100002",msg:"验证错误"
         code:"100004",msg:"轨迹验证错误"
 */
 
// base64的解密和加密使用了包: js-base64

/* =========================== 计算data_enc path_enc =========================== */
const p = {
  decode:function(a,b){var c=function(b){for(var c,d=a,e=(b[0],b[1]),f=8,g=3;f>4;f--,g--)c=e[g]%f,d=d.substr(0,c)+d.substr(c+1);return d}(function(a){for(var c,d=[],e=[],f=a,g=0;4>g;g++)c=b.charAt(f),e.push(f),d.push(c),f=c.charCodeAt(0)%32;return[d,e]}(3));return function(a){for(var b=[],c=+(!+[]+!0+!0+!0+!0+!0+!0+!0+!0+[]+(!+[]+!0+!0+!0+!0+!0+!0+[])),d=0;d<a.length;d++)b.push(a[d].charCodeAt(0)-c);return b.join("")}(c)},
  encode:function(a,b){for(var c=b.length-2,d=b.slice(c),e=[],f=0;f<d.length;f++){var g=d.charCodeAt(f);e[f]=g>57?g-87:g-48}d=c*e[0]+e[1];var h,i=parseInt(a)+d,j=b.slice(0,c),k=[20,50,200,500],l=[],m={},n=0;f=0;for(var o in k)l.push([]);for(var p=j.length;p>f;f++)h=j.charAt(f),m[h]||(m[h]=1,l[n].push(h),n++,n==l.length&&(n=0));for(var q,r=i,s="",t=k.length-1;r>0&&!(0>t);)r-k[t]>=0?(q=parseInt(Math.random()*l[t].length),s+=l[t][q],r-=k[t]):t-=1;return s}
};

const q = {
  seed:"()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnop~$^!|",
  numberTransfer:function(a){for(var b=this.seed,c=b.substr(0,b.length-3),d=c.length,e=b.substr(-2,1),f=b.substr(-3,1),g=0>a?f:"",a=Math.abs(a),h=parseInt(a/d),i=[a%d];h;)g+=e,i.push(h%d),h=parseInt(h/d);for(var j=i.length-1;j>=0;j--)g+=0==j?c.charAt(i[j]):c.charAt(i[j]-1);return 0>a&&(g=f+g),g},
  arrTransfer:function(a){for(var b=[a[0]],c=0;c<a.length-1;c++){for(var d=[],e=0;e<a[c].length;e++)d.push(a[c+1][e]-a[c][e]);b.push(d)}return b},
  encode:function(a){for(var b=this.seed.substr(-1),c=this.arrTransfer(a),d=[],e=[],f=[],g=0;g<c.length;g++)d.push(this.numberTransfer(c[g][0])),e.push(this.numberTransfer(c[g][1])),f.push(this.numberTransfer(c[g][2]));return d.join("")+b+e.join("")+b+f.join("")}
};

/* =========================== 位置计算 =========================== */

/**
 * 计算图片位置
 * @param { string } bgUrl: 图片地址
 * @param { Array<string> } lock
 */
function recombineShadow(bgUrl, lock){
  let html = '';
  for(let e, f, g, h = lock[0], i = lock[1], j = lock.slice(2), l = 160, m = 160, n = 0; n < j.length; n++){
    e = l / h;
    f = m / i;
    g = '-' + j[n] % h * e + 'px -' + parseInt(j[n] / h) * f + 'px';
    html += `<div class="pattern-children" style="background-image: ${ bgUrl }; background-position: ${ g };"></div>`;
  }
  document.getElementById('pattern').innerHTML = html;
}

/**
 * 初始化图片地址
 * @param { string } imageUrl:base64图片|token
 */
function hint(imageUrl){
  if(imageUrl){
    const u = imageUrl.split('|');
    const bg = `url(${ u[0] })`;
    const lock = Base64.decode(u[1]).split('_');
    recombineShadow(bg, lock);
  }
}

/* =========================== dom =========================== */
const canvas = document.getElementById('pattern-canvas');
const ctx = canvas.getContext('2d');
/* canvas画圆圈 */
ctx.strokeStyle = '#b3b3b3';
ctx.lineWidth = 3;
// 1
ctx.beginPath();
ctx.arc(30, 30, 20, 0, 2 * Math.PI);
ctx.stroke();
// 2
ctx.beginPath();
ctx.arc(130, 30, 20, 0, 2 * Math.PI);
ctx.stroke();
// 3
ctx.beginPath();
ctx.arc(30, 130, 20, 0, 2 * Math.PI);
ctx.stroke();
// 4
ctx.beginPath();
ctx.arc(130, 130, 20, 0, 2 * Math.PI);
ctx.stroke();

/* =========================== touch事件 =========================== */
const father = document.getElementById('pattern-father');
const fobj = father.getBoundingClientRect();
const START = 'ontouchstart' in document ? 'touchstart' : 'mousedown';
const MOVE = 'ontouchmove' in document ? 'touchmove' : 'mousemove';
const END = 'ontouchend' in document ? 'touchdown' : 'mouseup';

ctx.strokeStyle='#ff7f00';
ctx.lineWidth = 5;

let trace = [];
let startTime = null;
let old = null;
const zuobiao = [];

/* 坐标范围 */
function zuobiaofanwei(x, y){
  if(y >= 0 && y <= 60){
    if(x >= 0 && x <= 60){
      return '1';
    }else if(x >= 100 <= 160){
      return '2';
    }else{
      return null;
    }
  }else if(y >= 100 && y <= 160){
    if(x >= 0 && x <= 60){
      return '3';
    }else if(x >= 100 <= 160){
      return '4';
    }else{
      return null;
    }
  }else{
    return null;
  }
}

/* 事件 */
function onStart(event){
  event.preventDefault();
  startTime = new Date().getTime();
  const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
  const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
  const sx = Math.round(x - fobj.left);
  const sy = Math.round(y - fobj.top);
  trace.push([sx, sy, 0]);
  //
  const z = zuobiaofanwei(sx, sy);
  if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
  //
  old = [sx, sy];
  canvas.addEventListener(MOVE, onMove, false);
  canvas.addEventListener(END, onEnd, false);
}

function onMove(event){
  event.preventDefault();
  const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
  const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
  const t = new Date().getTime();
  const sx = Math.round(x - fobj.left);
  const sy = Math.round(y - fobj.top);
  trace.push([sx, sy, t - startTime]);
  //
  const z = zuobiaofanwei(sx, sy);
  if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
  //
  ctx.beginPath();
  ctx.moveTo(...old);
  ctx.lineTo(sx, sy);
  ctx.stroke();
  old = [sx, sy];
}

function onEnd(event){
  event.preventDefault();
  const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
  const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
  const t = new Date().getTime();
  const sx = Math.round(x - fobj.left);
  const sy = Math.round(y - fobj.top);
  trace.push([sx, sy, t - startTime]);
  canvas.removeEventListener(MOVE, onMove);
  canvas.removeEventListener(END, onEnd);
  //
  const z = zuobiaofanwei(sx, sy);
  if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
  //
  ctx.beginPath();
  ctx.moveTo(...old);
  ctx.lineTo(sx, sy);
  ctx.stroke();
  old = null;

  // 测试时便于复制粘贴
  console.log(`id=${ window.yanzhengid }&path_enc=${ p.encode(zuobiao.join(''), window.yanzhengid) }&data_enc=${ q.encode(trace) }`);
}

canvas.addEventListener(START, onStart, false);

4、登录

【POST】https://passport.weibo.cn/sso/login

  • 请求头:
    • Referer:https://passport.weibo.cn/signin/login?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn
  • 参数:
    • username:用户名
    • password:密码
    • vid:前面验证码的id

最后来一张成果图,显示我不是在吹牛: