zoukankan      html  css  js  c++  java
  • 百度登录加密协议分析

    百度登录加密协议分析(上) - 七夜的故事 - 博客园 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。

  • 相关阅读:
    input搜索框实时检索功能实现(超简单,核心原理请看思路即可)
    django blank 和autonow
    dwebsocket的坑
    vue 动态添加active+父子传值
    NO 2,人生苦短,我学python之python+selenium元素定位
    NO 1,人生苦短,我学python之python+selenium自动化环境搭建
    SPU与SKU概念
    数据库,缓存数据一致性常用解决方案总结
    利用注解 + 反射消除重复代码
    Nacos学习与实战
  • 原文地址:https://www.cnblogs.com/rsapaper/p/15700974.html
Copyright © 2011-2022 走看看