百度登錄加密協議分析


百度登錄加密協議分析(上) - 七夜的故事 - 博客園 https://www.cnblogs.com/qiyeboy/p/5722424.html

百度登錄加密協議分析(下) - 七夜的故事 - 博客園 https://www.cnblogs.com/qiyeboy/p/5728293.html

 好了,廢話不多說,咱們進入今天的主題,講解一下前段時間做的百度登錄加密協議分析,由於寫的比較詳細,篇幅有點多,所以就分為上下兩篇來寫。由於百度登錄使用的是同一套加密規則,所以這次就以百度雲盤的登錄為例進行分析。

 

 第一部分:
  首先打開firebug,訪問 http://yun.baidu.com/,監聽網絡數據。
  
    流程:
      1.輸入賬號和密碼,點擊登錄。
      2.點擊登錄。( 第一次post,這時候會出現驗證碼)
      3.會出現驗證碼,輸入驗證碼,
      4.最后點擊登錄成功上線。( 第二次post登錄成功)
 
 
    根據以往的分析經驗,一般需要進行 兩次登錄,來比較post請求出去的數據, 哪些字段是不變的,哪些字段是動態改變的。同樣上述的流程,這次也會重復一次。將兩次登錄過程中產生的post請求保存下來。
 
  
    在一次成功的登錄過程中,我們需要點擊兩次登錄按鈕,也就出現了 兩次post請求
 
  
  
  咱們先關注 最后一次post的請求內容。
  
  
  這個時候從 賬號登出,清除cookie信息,再進行一次登錄過程,再把post出去的數據,記錄下來,進行比較哪些是變化的。
  
   通過兩次的比較,我們可以發現:
 
   apiver=v3
  callback=parent.bd__pcbs__yqrows
  charset=utf-8
  codestring=jxGa206f4ef6540e2a5023014affd01abcc160fde066101382d
  countrycode=
  crypttype=12
  detect=1
  foreignusername=
  gid=58DDBCC-672F-423D-9A02-688ACB9EB252
  idc=
  isPhone=
  logLoginType=pc_loginBasic
  loginmerge=true
  logintype=basicLogin
  mem_pass=on
    password
  quick_user=0
  rsakey=kvcQRN4WQS1varzZxXKnebLGKgZD5UcV
  safeflg=0
  staticpage=http://yun.baidu.com/res/static/thirdparty/pass_v3_jump.html
  subpro=netdisk_web
  token=69a056f475fc955dc16215ab66a985af
  tpl=netdisk
  tt=1469844379327
  u=http://yun.baidu.com/
  username
  verifycode=1112
  
 其中 標有綠色的字段都是不變化的,其他都是變化的。
 
 
  接着看一下變化的字段:
 
    callback 不清楚是什么
    codestring 不清楚是什么
    gid 一個生成的ID號
    password 加密后的密碼
    ppui_logintime 時間,不知道有沒有用
    rsakey RSA加密的密鑰(可以推斷出密碼肯定是經過了RSA加密)
    token 訪問令牌
    tt 時間戳
    verifycode 驗證碼
 
    上面標為 綠色的部分,都是可以簡單獲取的,所以先不用考慮。
 

第二部分:
 
  (1) 采取 倒序的分析方式,上面說了一下 第二次post的值,接着咱們分析一下, 第一次post的數據內容。
 
 
  通過 兩次post比較,可以發現一下 字段的變化:
 
    callback 第一次post 已經產生,第二次post內容 發生變化
    codestring 第一次post時 沒有數據,第二次post 產生數據
    gid 第一次post 已經產生,第二次post內容 沒有發生變化
    password 第一次post 已經產生,第二次post內容 發生變化
    ppui_logintime 第一次post 已經產生,第二次post內容 發生變化
    rsakey 第一次post 已經產生,第二次post內容 沒有發生變化
    token 第一次post 已經產生,第二次post內容 沒有發生變化
 
  從上面可以看到出現明顯變化的是 codestring ,從無到有
   可以基本上確定 codestring 是在 第一次post之后產生的,所以codestring 這個字段應該是在 第一次post之后的響應中找到。
    果然不出所料:
 
 
  codestring 這個字段的獲取位置已經確定
  ————————————————————————————————————————————————————————————————————————
 
  (2) 接下來 分析 第一次post已經產生,第二次post內容沒有發生變化的字段
    gid
    rsakey
    token
 
  根據網絡響應的順序,從下到上,看看能不能發現一些 敏感命名的鏈接(這是之前的經驗)
 
  從 第一次post的往上看,一個 敏感的鏈接就出現了。
 
 
 
 
 
 
 
  通過查看響應我們找到 rsakey,雖然在 響應中變成了key,可是 值是一樣的。
   通過之前的信息,我們知道密碼是 通過RSA加密的,所以 響應中的publickey可能是公鑰,所以這個要 重點注意
 
 
  咱們還可以發現callback 字段,參數中出現callback字段,之后響應中也出現 了  callback字段的值將響應包裹取來,由此可以推斷
   callback字段可能只是進行標識作用。不參與實際的參數校驗
 
   通過這個get鏈接的參數,我們可以得出結論:
 
    gid和token可以得到rsakey參數:
    gid token ------->>>>>rsakey
 
 
 
  分析 gid和token字段
 
  為了加快速度,咱們直接在 firebug的搜索框中輸入token:
  搜索兩三次就發現了 token的出處。
 
 
 
 
 
  通過get請求的參數可以得出這樣的結論:
    通過gid可以得出來Token
    gid----------->>>>>>>>token
 
 
   最后咱們分析一下gid:
    依然是 搜索gid ,搜索幾次就在這個腳本中發現了gid的存在:
 
 
  
      格式化腳本之后,咱們看一下這個 gid是怎么產生的
      通過 gid:e.guideRandom ,我們可以知道gid是由 guideRandom這個函數產生的,接着在腳本中搜索這個函數;
 
 
 
  最后找個了 這個函數的原型,但是 通過代碼可以看到,這個是隨機生成的一個字符串,這就好辦了( 百度。。。其實當時我是無語的)。
    gid = this.guideRandom = function () {
      return 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (e) {
      var t = 16 * Math.random() | 0,
      n = 'x' == e ? t : 3 & t | 8;
      return n.toString(16)
      }).toUpperCase()
    }()
 
總結一下:
 
  codestring:從第一次post之后的響應中提取出來
  
  gid: 有一個已知函數guideRandom 隨機產生,可以通過調用函數獲取
 
 

 

  第三部分:    

  分析 第一次post已經產生,第二次post內容發生變化的字段
    callback
    password
    ppui_logintime
  
   通過之前的分析,可以了解到callback 可能沒啥用,所以放到后面再分析。
   一般來說password的是最難分析的,所以也放到后面分析。
    
  3.1  接下來分析ppui_logintime,依舊是搜索ppui_logintime,在下面鏈接中找到了ppui_logintime的出處
  
  找到了 timeSpan: 'ppui_logintime',接着搜索timespan
 
 
  
   找到了 r.timeSpan = (new Date).getTime() - e.initTime,
 
   接着搜索initTime
 
  咱們看一下_initApi什么時候調用的,通過搜索找到以下代碼:
  
復制代碼
_eventHandler: function() {
    var e,
        t = {
            focus: function(t) {
                var n = e.fireEvent('fieldFocus', {
                    ele: this
                });
                n && (this.addClass(e.constant.FOCUS_CLASS), this.removeClass(e.constant.ERROR_CLASS), baidu(e.getElement(t + 'Label')).addClass(e.constant.LABEL_FOCUS_CLASS))
            },
            blur: function(t) {
                var n = e.fireEvent('fieldBlur', {
                    ele: this
                });
                n && (this.removeClass(e.constant.FOCUS_CLASS), baidu(e.getElement(t + 'Label')).removeClass(e.constant.LABEL_FOCUS_CLASS))
            },
            mouseover: function() {
                var t = e.fireEvent('fieldMouseover', {
                    ele: this
                });
                t && this.addClass(e.constant.HOVER_CLASS)
            },
            mouseout: function() {
                var t = e.fireEvent('fieldMouseout', {
                    ele: this
                });
                t && this.removeClass(e.constant.HOVER_CLASS)
            },
            keyup: function() {
                e.fireEvent('fieldKeyup', {
                    ele: this
                })
            }
        },
        n = {
            focus: {
                userName: function() {
                    e.config.loginMerge && e.getElement('loginMerge') && (e.getElement('loginMerge').value = 'true', e.getElement('isPhone').value = '')
                },
                password: function() {
                    e._getRSA(function(t) {
                        e.RSA = t.RSA,
                            e.rsakey = t.rsakey
                    })
                },
                verifyCode: function() {}
            },
            blur: {
                userName: function() {},
                password: function(t) {
                    var n = this.get(0).value;
                    n.length && e.validate(t)
                },
                verifyCode: function(t) {
                    var n = this.get(0).value;
                    n.length && e.validate(t)
                }
            },
            change: {
                userName: function() {
                    var t = this.get(0).value;
                    e._loginCheck(t)
                },
                verifyCode: function() {}
            },
            click: {
                verifyCodeChange: function(t, n) {
                    e.getElement('verifyCode').value = '',
                        e._doFocus('verifyCode'),
                        e.getVerifyCode(),
                        n.preventDefault()
                }
            },
            keyup: {
                verifyCode: function() {
                    var t = e.getElement('verifyCode'),
                        n = baidu(t);
                    t.value && 4 == t.value.length ? e._asyncValidate.checkVerifycode.call(e, {
                        error: function(t) {
                            n.addClass(e.constant.ERROR_CLASS),
                                e.setGeneralError(t.msg)
                        },
                        success: function() {
                            n.removeClass(e.constant.ERROR_CLASS),
                                e.clearGeneralError()
                        }
                    }) : e.$hide('verifyCodeSuccess')
                }
            },
            submit: function(t) {
                e.submit(),
                    t.preventDefault()
            }
        };
    return {
        entrance: function(i) {
            e = this;
            var r = (baidu(i.target), i.target.name);
            if (!r && i.target.id) {
                var o = i.target.id.match(/\d+__(.*)$/);
                o && (r = o[1])
            }
            r && (t.hasOwnProperty(i.type) && t[i.type].apply(baidu(i.target), [
                r,
                i
            ]), n.hasOwnProperty(i.type) && ('function' == typeof n[i.type] && n[i.type].apply(baidu(i.target), [
                i
            ]), n[i.type].hasOwnProperty(r) && n[i.type][r].apply(baidu(i.target), [
                r,
                i
            ])), e.initialized || 'focus' != i.type || e._initApi())
        }
    }
}(),
復制代碼
  通過分析上面的js代碼可以看出來,發生點擊,內容改變,按鍵按下等事件可能會調用initApi()。
 
  通過上面的代碼我們可以知道ppui_logintime是從你輸入登錄信息,一直到你點擊登錄按鈕提交的這段時間,
 
  因此 我們通過之前的抓包,直接把ppui_logintime的值保存下來即可。
  
  3.2 接着咱們分析 callback,看看這字段到底是干什么用的(最后發現沒啥用,和上一篇得出來的推斷差不多)。搜索callback,紅色標注的地方是不是和post出去的內容有重復。
 
  
    
callback ='bd__cbs__'+Math.floor(2147483648 *Math.random()).toString(36)
   這個時候 callback的生成當時也就確定了
 
  3.3 最后分析 password的加密方式:搜索password,發現敏感內容。 
 
 
 
  通過下斷點,動態調試可以知道password,是 通過公鑰pubkey對密碼進行加密,最后輸出進行base64編碼
上圖的xn()就是在進行base64編碼。
 
   如果大家對javascript的RSA加密不熟悉,可以推薦看一下                         https://github.com/travist/jsencrypt/blob/master/src/jsencrypt.js。
等你看完了這個開源項目,你會發現,百度使用的RSA加密函數和上面的連命名幾乎一樣,這也就是為什么能這么快分析出RSA加密的原因。
 
  3.4  如何使用 python進行RSA加密
復制代碼
采用的是RSA加密方式:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
password = 'xxxxxxxx'
with open('pub.pem') as f:
    pubkey = f.read()
    rsakey = RSA.importKey(pubkey)
    cipher = PKCS1_v1_5.new(rsakey)
    cipher_text = base64.b64encode(cipher.encrypt(password))
    print cipher_text
復制代碼

  3.5 由於之前安裝了pyv8,所以不把gid,callback等js函數翻譯成python了,翻譯過來也很簡單,如果你電腦上沒裝pyv8,就試着翻譯一下。

復制代碼
    function callback(){
        return 'bd__cbs__'+Math.floor(2147483648 * Math.random()).toString(36)

    }
    function gid(){
        return 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (e) {
        var t = 16 * Math.random() | 0,
        n = 'x' == e ? t : 3 & t | 8;
        return n.toString(16)
        }).toUpperCase()

    }
復制代碼

  3.6 似乎還有驗證碼沒說,其實就是兩個鏈接,一個是獲取驗證碼的鏈接,一個是檢測驗證碼是否正確的鏈接。驗證碼獲取很簡單,這里就不詳細說了。下面我會把整個登錄的源代碼,貼在下面有興趣的,可以去玩一下。

總結: 

  下面我用python模擬了一下登錄,使用了requests和pyv8(其實想偷懶),代碼如下:

復制代碼
#coding:utf-8
import base64
import json
import re
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
import PyV8
from urllib import quote
import requests
import time

if __name__=='__main__':
    s = requests.Session()
    s.get('http://yun.baidu.com')
    js='''
    function callback(){
        return 'bd__cbs__'+Math.floor(2147483648 * Math.random()).toString(36)

    }
    function gid(){
        return 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (e) {
        var t = 16 * Math.random() | 0,
        n = 'x' == e ? t : 3 & t | 8;
        return n.toString(16)
        }).toUpperCase()

    }
    '''
    ctxt = PyV8.JSContext()
    ctxt.enter()
    ctxt.eval(js)
    ###########獲取gid#############################3
    gid = ctxt.locals.gid()
    ###########獲取callback#############################3
    callback1 = ctxt.locals.callback()
    ###########獲取token#############################3
    tokenUrl="https://passport.baidu.com/v2/api/?getapi&tpl=netdisk&subpro=netdisk_web&apiver=v3" \
             "&tt=%d&class=login&gid=%s&logintype=basicLogin&callback=%s"%(time.time()*1000,gid,callback1)

    token_response = s.get(tokenUrl)
    pattern = re.compile(r'"token"\s*:\s*"(\w+)"')
    match = pattern.search(token_response.text)
    if match:
        token = match.group(1)

    else:
        raise Exception
    ###########獲取callback#############################3
    callback2 = ctxt.locals.callback()
    ###########獲取rsakey和pubkey#############################3
    rsaUrl = "https://passport.baidu.com/v2/getpublickey?token=%s&" \
             "tpl=netdisk&subpro=netdisk_web&apiver=v3&tt=%d&gid=%s&callback=%s"%(token,time.time()*1000,gid,callback2)
    rsaResponse = s.get(rsaUrl)
    pattern = re.compile("\"key\"\s*:\s*'(\w+)'")
    match = pattern.search(rsaResponse.text)
    if match:
        key = match.group(1)
        print key

    else:
        raise Exception
    pattern = re.compile("\"pubkey\":'(.+?)'")
    match = pattern.search(rsaResponse.text)
    if match:
        pubkey = match.group(1)
        print pubkey

    else:
        raise Exception
    ################加密password########################3
    password = 'xxxxxxx'#填上自己的密碼
    pubkey = pubkey.replace('\\n','\n').replace('\\','')
    rsakey = RSA.importKey(pubkey)
    cipher = PKCS1_v1_5.new(rsakey)
    password = base64.b64encode(cipher.encrypt(password))
    print password
    ###########獲取callback#############################3
    callback3 = ctxt.locals.callback()
    data={
        'apiver':'v3',
        'charset':'utf-8',
        'countrycode':'',
        'crypttype':12,
        'detect':1,
        'foreignusername':'',
        'idc':'',
        'isPhone':'',
        'logLoginType':'pc_loginBasic',
        'loginmerge':True,
        'logintype':'basicLogin',
        'mem_pass':'on',
        'quick_user':0,
        'safeflg':0,
        'staticpage':'http://yun.baidu.com/res/static/thirdparty/pass_v3_jump.html',
        'subpro':'netdisk_web',
        'tpl':'netdisk',
        'u':'http://yun.baidu.com/',
        'username':'xxxxxxxxx',#填上自己的用戶名
        'callback':'parent.'+callback3,
        'gid':gid,'ppui_logintime':71755,
        'rsakey':key,
        'token':token,
        'password':password,
        'tt':'%d'%(time.time()*1000),


    }
    ###########第一次post#############################3
    post1_response = s.post('https://passport.baidu.com/v2/api/?login',data=data)
    pattern = re.compile("codeString=(\w+)&")
    match = pattern.search(post1_response.text)
    if match:
    ###########獲取codeString#############################3
        codeString = match.group(1)
        print codeString

    else:
        raise Exception
    data['codestring']= codeString
    #############獲取驗證碼###################################
    verifyFail = True
    while verifyFail:
        genimage_param = ''
        if len(genimage_param)==0:
            genimage_param = codeString

        verifycodeUrl="https://passport.baidu.com/cgi-bin/genimage?%s"%genimage_param
        verifycode = s.get(verifycodeUrl)
        #############下載驗證碼###################################
        with open('verifycode.png','wb') as codeWriter:
            codeWriter.write(verifycode.content)
            codeWriter.close()
        #############輸入驗證碼###################################
        verifycode = raw_input("Enter your input verifycode: ");
        callback4 = ctxt.locals.callback()
        #############檢驗驗證碼###################################
        checkVerifycodeUrl='https://passport.baidu.com/v2/?' \
                        'checkvcode&token=%s' \
                        '&tpl=netdisk&subpro=netdisk_web&apiver=v3&tt=%d' \
                        '&verifycode=%s&codestring=%s' \
                        '&callback=%s'%(token,time.time()*1000,quote(verifycode),codeString,callback4)
        print checkVerifycodeUrl
        state = s.get(checkVerifycodeUrl)
        print state.text
        if state.text.find(u'驗證碼錯誤')!=-1:
            print '驗證碼輸入錯誤...已經自動更換...'
            callback5 = ctxt.locals.callback()
            changeVerifyCodeUrl = "https://passport.baidu.com/v2/?reggetcodestr" \
                                  "&token=%s" \
                                  "&tpl=netdisk&subpro=netdisk_web&apiver=v3" \
                                  "&tt=%d&fr=login&" \
                                  "vcodetype=de94eTRcVz1GvhJFsiK5G+ni2k2Z78PYRxUaRJLEmxdJO5ftPhviQ3/JiT9vezbFtwCyqdkNWSP29oeOvYE0SYPocOGL+iTafSv8pw" \
                                  "&callback=%s"%(token,time.time()*1000,callback5)
            print changeVerifyCodeUrl
            verifyString = s.get(changeVerifyCodeUrl)
            pattern = re.compile('"verifyStr"\s*:\s*"(\w+)"')
            match = pattern.search(verifyString.text)
            if match:
            ###########獲取verifyString#############################3
                verifyString = match.group(1)
                genimage_param = verifyString
                print verifyString

            else:
                verifyFail = False
                raise Exception

        else:
            verifyFail = False
    data['verifycode']= verifycode
    ###########第二次post#############################3
    data['ppui_logintime']=81755

   ####################################################
   # 特地說明,大家會發現第二次的post出去的密碼是改變的,為什么我這里沒有變化呢?
   #是因為RSA加密,加密密鑰和密碼原文即使不變,每次加密后的密碼都是改變的,RSA有隨機因子的關系
   #所以我這里不需要在對密碼原文進行第二次加密了,直接使用上次加密后的密碼即可,是沒有問題的。
   # ###################################################################################

    post2_response = s.post('https://passport.baidu.com/v2/api/?login',data=data)
    if post2_response.text.find('err_no=0')!=-1:
        print '登錄成功'

    else:
        print '登錄失敗'
復制代碼

  我把整個代碼上傳到git上了:https://github.com/qiyeboy/baidulogin.git,大家可以star和fork。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM