zoukankan      html  css  js  c++  java
  • 【爬虫系列】0. 无内鬼,破解前端JS参数签名

    PS:这是一个系列,坐等我慢慢填坑。

    PS:不太会直接能跑的代码,抛砖引玉。

    PS:那些我也不太熟练的就不搞了,包括(破滑块、验证码..)

    PS: 反编译搞Apk会有很长的几个文章,稍后慢慢更。

     

     


     

    最近,和某XX单位的网站gang上了。

    他们家的网页只允许在微信客户端打开,抓包就跟蛋疼了。

    不过,手上有Root后的Google Nexus5X,也有 whistle 跨平台抓包工具

    这个倒没太折腾,抓包工具证书往手机系统根证书一扔,完事。

    安卓7.0及以上用户证书导入的问题 - entr0py - 博客园

     

    抓到了包,下面蛋疼的事情开始了。


    前言: body 加密

    嗯?请求Body是这一串东西?

    嗯?时隔三年,神奇海螺又出现了?

    // json
    {
        "encryKey": "14a625eb2ec957f9b53412b01de37044e7e2aa6b4b911111c75091cba2a0315b",
        "data": "44bc0dab8db8017603586f40554742d14a0c23dd009e35cae5b5ac87dbf7962a311fae30070763d2b48b564d72191fd07a881ebcccfb7c0fdd33e4067bc5119cee5e2fa5eaac10da995c86c8a092dcc3",
        "sign": "cc3f924bbb6d57a15bd3e130230f51e55a04fa9e459d177440fbd10bce4b02d0",
        "timestamp": 1627224237000
    }

    很明显,每个单词我们都知道,每个字母和数值我们也懂。

    但是....

    除了timestamp我们可以生成,其他的明显是加密后数据和签名。


    一点都不高能的预警

    先说一下思路:

    1. 捞出核心JS文件
    2. 读懂加密过程,捞出关键参数
    3. 用其他语言实现涉及到的加密函数
    4. 对比加密结果是否一致,尝试去伪造请求

    捞JS

     

    首先这货的微信浏览器的,所以没办法使用浏览器开发者工具。

    不过,抓包上面不是搞掂了么?直接从抓包结果看HTML就完事。

    乖乖一个个请求看,找到第一个返回HTML的响应体。

    于是,找到了这个...

    哦, 看到了....

    <script src="/umi.a029b1fd.js"></script>

    看到这货,本宝宝小心脏有点乱跳了。

    访问一看。

    害,看起来没的那么简单啊,明显这货是被webpack打包后的JS文件。

    先下载回来再说...


    umi.a029b1fd.js 下载到本地,一看1.5M。

    打开一看,毫无疑问没有格式化...

    得嘞,大JS文件格式化,先打开我的Ubuntu机器再说。

    哦,VS Code format崩;加大内存,继续崩。

    搜了一波,找到了神器 Online JavaScript beautifier

    文件扔上去,再下载下来...

    完事。

     

    毫无疑问,这就是webpack打包后的东西了。

    没得事,全局搜一波上面的参数。

    完美,看到这个,是不是答案已经出来了。

    看看,每个参数怎么算的都告诉我了,还能做撒?还需要做撒?

    于是,我午睡去了。

    ........

    其实,最头疼的东西就在这里了。

    这时候,很多人会说,上AST 还原JS嘛。

    AST抽象语法树--最基础的javascript重点知识,99%的人根本不了解 - SegmentFault 思否

    啧啧啧。

    道理是这个道理,不过还有其他的思路吗?

    直接写个index.html 引入这个JS也成的啊。

    <html>
    
    <body>
        <h1>test</h1>
    </body>
    
    <script src="./app.js"></script>
    </html>

     

    开始解JS

     var O = Date.parse(new Date),
     Y = Object(h["e"])(!1, 16),
     j = Object(h["a"])(JSON.stringify({
     data: b,
     timestamp: O
                            }), Y),
     P = Object(h["f"])(j, Y);
     T = {
     encryKey: Object(h["a"])(Y, h["b"]),
     data: j,
     sign: P,
     timestamp: O
    }

    在代码里面看到了一堆这种 h["a"] h["e"],然后跟着参数(j, Y)。

    我们明显知道,这是JavaScript的一个函数调用,h看起来是一个map或者是对象,

    这里是在调用它的a方法,传入了(j, Y)

    在这里,我们最想知道的就是h["a"]的定义是什么样的,

    因为知道定义实现,也就能还原完整代码逻辑。

    跟一点代码(VS Code跳转定义功能),我们能看到h是什么?

    h = n("jNxd"),

    看到这里其实是很头疼的,n是个什么玩意我们完全无从得知。

    不过这里也能得到点信息,各种各样的函数或者对象都是绑定在”n“上的,

    我们只要拿到n,我们需要的h,h[a], h[b] 都知道是什么了。

    怎么拿到n呢? 友情提示,善用debugger。


    开始寻找n

    刚刚我们已经完整把app.js(umi.a029b1fd.js格式化之后的文档)导入我们的index.html

    用浏览器打开看看页面。

    页面没什么问题,我们尝试在app.js上面加点debugger吧。

    加在哪呢?(目的只有一个,能获取的到n)

    ....h附近前面可以吗?

    浏览器控制台打开,刷新页面,切换到Console页面。

     

    试试这里能不能有n对象。

    咦,看起来有戏。

    试试 h = n("jNxd")

    很好,很好,看起来这里是OK的,

    h["a"]也是一个函数,符合我们上面看到的。

    点击一下上面h["a"]输出的内容,可以跳转到函数定义。

    于是,我们来到了重头戏。

               s = (e, t) => {
                    var n = i.a.enc.Utf8.parse(t),
                        r = i.a.enc.Utf8.parse(t),
                        o = i.a.enc.Utf8.parse(e),
                        a = i.a.AES.encrypt(o, n, {
                            iv: r,
                            mode: i.a.mode.CBC,
                            padding: i.a.pad.Pkcs7
                        });
                    return i.a.enc.Hex.stringify(a.ciphertext)
                },
                u = (e, t) => {
                    var n = i.a.enc.Utf8.parse(t),
                        r = i.a.enc.Utf8.parse(t),
                        o = i.a.enc.Hex.parse(e),
                        a = i.a.enc.Base64.stringify(o),
                        s = i.a.AES.decrypt(a, n, {
                            iv: r,
                            mode: i.a.mode.CBC,
                            padding: i.a.pad.Pkcs7
                        });
                    return s.toString(i.a.enc.Utf8).toString()
                },
                c = (e, t) => i.a.enc.Hex.stringify(i.a.HmacSHA256(e, t)),
                l = (e, t, n) => {
                    var r = "",
                        i = t,
                        o = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "-", ".", "~", "!", "@", "#", "$", "%", "^", "*", "(", ")", "_", ":", "<", ">", "?"];
                    e && (i = Math.round(Math.random() * (n - t)) + t);
                    for (var a = 0; a < i; a += 1) {
                        var s = Math.round(Math.random() * (o.length - 1));
                        r += o[s]
                    }
                    return r
                }
    

    看看,代码都出来了,还需要撒?

    今天的教程结束,早点睡....

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     


    微微一笑,好像没那么简单。

     

    宝哥微微一笑,发现事情没那么简单。

     

     

    已知,上面这一堆东西,

    要不是 i.a.AES,要不是 HmacSHA256

    没什么花样。

     

    那么最大的问题就是,

    这个加密过程是怎么搞的。

    加密向量是什么?秘钥在哪?

    SHA256用的是什么参数?参与加密的数据是怎么拼接的?

     

    PS:还在写....

     


    重头戏上场

    回到上面的代码

    if (p["d"] && "get" !== u.toLowerCase() && !g) {
                        var O = Date.parse(new Date),
                            Y = Object(h["e"])(!1, 16),
                            j = Object(h["a"])(JSON.stringify({
                                data: b,
                                timestamp: O
                            }), Y),
                            P = Object(h["f"])(j, Y);
                        T = {
                            encryKey: Object(h["a"])(Y, h["b"]),
                            data: j,
                            sign: P,
                            timestamp: O
                        }
     }
    

    这里可以看出每个变量是怎么来的。

    encryKey = Object(h["a"])(Y, h["b"]) // 调用了a方法

    O= Date.parse(newDate) // 生成了时间戳

    Y=Object(h["e"])(!1,16) // 调用了e方法

    P=Object(h["f"])(j,Y) 调用了f方法

     

    于是我们执行一下看看。

     

    Y看起来是个随机字符串,j,p看起来都是字母+数字组合起来的字符串。

     

    分别到定义出看看是撒。

    h["e"]

     l = (e, t, n) => {
                    var r = "",
                        i = t,
                        o = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "-", ".", "~", "!", "@", "#", "$", "%", "^", "*", "(", ")", "_", ":", "<", ">", "?"];
                    e && (i = Math.round(Math.random() * (n - t)) + t);
                    for (var a = 0; a < i; a += 1) {
                        var s = Math.round(Math.random() * (o.length - 1));
                        r += o[s]
                    }
                    return r
                }
    

    哦,生成了随机字符串。

    h["a"]

                // n("jNxd")["a"] encryKey
                s = (e, t) => {
                    var n = i.a.enc.Utf8.parse(t),
                        r = i.a.enc.Utf8.parse(t),
                        o = i.a.enc.Utf8.parse(e),
                        a = i.a.AES.encrypt(o, n, {
                            iv: r,
                            mode: i.a.mode.CBC,
                            padding: i.a.pad.Pkcs7
                        });
                    return i.a.enc.Hex.stringify(a.ciphertext)
                },

    哦, AES.encrypt 加密,使用的是CBC/Pkcs7对齐

    h["f"] HmacSHA256

    (e, t) => i.a.enc.Hex.stringify(i.a.HmacSHA256(e, t))

    h["b"] 直接返回了一个固定的字符串。(毫无疑问是IV向量和加密Key)

     

    看看,没了啊。

    核心的加密代码就这点。

     

                            var O = Date.parse(new Date),
                            Y = Object(h["e"])(!1, 16),
                            j = Object(h["a"])(JSON.stringify({
                                data: b,
                                timestamp: O
                            }), Y),
                            P = Object(h["f"])(j, Y);
                        T = {
                            encryKey: Object(h["a"])(Y, h["b"]),
                            data: j,
                            sign: P,
                            timestamp: O
                        }
    

    所以重点代码又回到这里了,看懂这里就是所有的逻辑了。

    读一下,也就这样。

    1. 获取当前时间戳 O
    2. 生成随机字符串 Y
    3. 把传入的b(body)和时间戳组合到一起,设定IV向量为Y,使用AES 加密
    4. 把密文 j 和 Y进行SHA256签名
    5. 最用把Y也用AES 加密,这个时候加密IV向量为h["b"]

     

    换个人话

     

    写死了一个iv向量,随机生成一个16位的key,从iv向量对这个Key加密,

    用这个Key作为另一个iv变量对请求体Body加密,

    然后把上面一堆东西做一个sha256的签名。

    哦,说好的前端参数签名加密。


    到这里,其实破解过程已经完成了。

    这基本也是我睡醒之后,看了台风吃了晚饭回来之后,

    开始抄Python 把上面逻辑实现一波的前置思路了。

    这个时候,我们也要知道一些东西。

    JS加密库 CryptoJS

    Python对应的加密库 pycrypto

    最后用Python实现这个完整逻辑还是折腾了好一会的,

    也抄了不少别的代码,最后贴一下。

    from Crypto.Cipher import AES
    import base64
    import time
    import binascii
    
    
    class AesEncrypt:
        def __init__(self, key, iv):
            self.key = key.encode('utf-8')
            self.iv = iv.encode('utf-8')
    
        # @staticmethod
        def pkcs7padding(self, text):
            """明文使用PKCS7填充 """
            bs = 16
            length = len(text)
            bytes_length = len(text.encode('utf-8'))
            padding_size = length if (bytes_length == length) else bytes_length
            padding = bs - padding_size % bs
            padding_text = chr(padding) * padding
            self.coding = chr(padding)
            return text + padding_text
    
        def aes_encrypt(self, content):
            """ AES加密 """
            cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
            # 处理明文
            content_padding = self.pkcs7padding(content)
            # 加密
            encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8'))
            # 重新编码
            result = str(base64.b64encode(encrypt_bytes), encoding='utf-8')
            print("加密hex:", str(binascii.hexlify(encrypt_bytes),encoding='utf-8'))
            return result
    
        def aes_encrypt_to_hex(self, content):
            """ AES加密 """
            cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
            # 处理明文
            content_padding = self.pkcs7padding(content)
            # 加密
            encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8'))
            # 重新编码
            # result = str(base64.b64encode(encrypt_bytes), encoding='utf-8')
            return str(binascii.hexlify(encrypt_bytes),encoding='utf-8')
    
        def aes_decrypt(self, content):
            """AES解密 """
            cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
            content = base64.b64decode(content)
            text = cipher.decrypt(content).decode('utf-8')
            return text.rstrip(self.coding)
    
    
    if __name__ == '__main__':
        key = '123'
        iv = '123'
    
        ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        p_json = {
            "CompanyName": "testmall",
          		"UserId": "test",
          		"Password": "grasp@101",
          		"TimeStamp": "2019-05-05 10:59:26"
        }
        a = AesEncrypt(key=key, iv=iv)
        e = a.aes_encrypt("123")
        d = a.aes_decrypt(e)
        print("加密:", e)
        print("解密:", d)
    

     

    好了,

    真完了,

    睡觉睡觉。

    编辑于 8 分钟前
  • 相关阅读:
    Linux 之 Memcached
    Linux 之 MySQL主从同步
    Linux 之 rsync实现服务器的文件同步
    A.01.03-模块的输入—模拟量输入
    A.01.02—模块的输入—高端输入
    A.01.01—模块的输入—低端输入
    复位电路
    边沿触发和电平触发的区别
    深入理解傅里叶变换
    电压跟随器
  • 原文地址:https://www.cnblogs.com/liguobao/p/15059568.html
Copyright © 2011-2022 走看看