新浪微博移動網頁端手勢驗證介面破解流程

告白_花_狼發表於2018-04-13

最近做了一個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

最後來一張成果圖,顯示我不是在吹牛:

新浪微博移動網頁端手勢驗證介面破解流程

相關文章