3

【爬虫系列】0. 无内鬼,破解前端JS参数签名

 3 years ago
source link: https://segmentfault.com/a/1190000040401301
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【爬虫系列】0. 无内鬼,破解前端JS参数签名

发布于 今天 16:22

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我们可以生成,其他的明显是加密后数据和签名。

一点都不高能的预警
先说一下思路:

捞出核心JS文件
读懂加密过程,捞出关键参数
用其他语言实现涉及到的加密函数
对比加密结果是否一致,尝试去伪造请求
捞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
                }

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

读一下,也就这样。

获取当前时间戳 O
生成随机字符串 Y
把传入的b(body)和时间戳组合到一起,设定IV向量为Y,使用AES 加密
把密文 j 和 Y进行SHA256签名
最用把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)

睡觉睡觉。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK