Web Spider XHR breakpoint stack and value reverse case (3)

insert image description here

statement

此次案例只为学习交流使用,抓包内容、敏感网址、数据接口均已做脱敏处理,切勿用于其他非法用途;


foreword

Target website address: aHR0cHM6Ly9tdXNpYy45MXEuY29tLw==


提示:以下是本篇文章正文内容,下面案例可供参考

1. Resource recommendation

Download and use JS reverse encryption and decryption tool: https://blog.csdn.net/EXIxiaozhou/article/details/128034062

2. Mission statement

1. Enter keywords such as singer name and song name to retrieve relevant information;
2. Enter the song ID of the platform to complete the song download; the
insert image description here
final result
insert image description here

3. Website analysis

Implementation ideas

  • 1. Get song information through keyword search;
  • 2. Request an interface through the song ID to get the play link of the song;
  • 3. Download the song through the play link of the song;

1. Open the website, enter keywords in the search box on the homepage, and press the Enter key, you can find that the data is in the webpage, just replace the search keywords;

Keywords to retrieve song information url: aHR0cHM6Ly9tdXNpYy45MXEuY29tL3NlYXJjaD93b3JkPcfgu6i0yQ==The insert image description here
data can be seen directly, use the analysis module to directly extract the desired data
insert image description here
2, analyze and obtain the XHR interface of the song download chain, come to the song playback page, the download link of the song is returned by the following interface Get the XHR interface url of
the song download chain: aHR0cHM6Ly9tdXNpYy45MXEuY29tL3YxL3NvbmcvdHJhY2tsaW5r
insert image description here
The parameter submitted when analyzing the request is a GET request, sign is an MD5 encrypted string, appid is the version number, TSID is the song ID, and timestamp is 10 digits Timestamp
The encrypted plaintext data is the following string, just replace the two parameters TSID and timestamp

f'TSID={
    
    TSID歌曲ID}&appid=16073360&timestamp={
    
    timestamp时间戳}0b50b02fd0d73a9c4c8c3a781c30845f'

insert image description here
3. Get the download link of the song and open it to play it directly
insert image description here

4. XHR breakpoint debugging, use WT-JS to restore JS encryption code

reverse thinking

  • 1. Through the browser, resource panel, add XHR breakpoint, let it stop before the browser request, so that we can analyze the request parameters;
  • 2. Through the call stack under the resource panel, follow the value to find the encrypted code;
  • 3. After getting the encrypted plaintext, use WT-JS to restore the JS encryption code
  • 4. Call JS code through python code to realize the whole task;

1. According to the url of the interface, set the XHR breakpoint. Here we take the XHR interface of obtaining the song download chain as an example;
if the browser sends a request to this interface, it will make a breakpoint before sending the package;
insert image description here
2. Refresh the page on the song playback page You can trigger the XHR breakpoint. You can find that the code parameters here have been generated. You need to follow the value through the call stack to find the encrypted code, and follow it one by one. The normal process is to switch from the stack to a new function every time. Cancel the previous breakpoint, re-set the breakpoint at the new function, and refresh the operation;
insert image description here
come here to the plaintext encryption function by calling the stack and the value, and re-set the breakpoint in the createSign() method;
pay attention to the breakpoint at the return, break Before and after (to avoid repeated calculation of the value), the encrypted plaintext is the value of r += secret;
insert image description here
3. Restore the JS encryption code, click to generate the JS encryption code, paste it into the pycharm editor for debugging, and
insert image description here
open pycharm to debug the JS encryption code
insert image description here

5. Code implementation

1. JS encryption code: encode.js

var CryptoJS = CryptoJS || (function (Math, undefined) {
    
    
    var crypto;
    if (typeof window !== 'undefined' && window.crypto) {
    
    
        crypto = window.crypto;
    }
    if (typeof self !== 'undefined' && self.crypto) {
    
    
        crypto = self.crypto;
    }
    if (typeof globalThis !== 'undefined' && globalThis.crypto) {
    
    
        crypto = globalThis.crypto;
    }
    if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
    
    
        crypto = window.msCrypto;
    }
    if (!crypto && typeof global !== 'undefined' && global.crypto) {
    
    
        crypto = global.crypto;
    }
    if (!crypto && typeof require === 'function') {
    
    
        try {
    
    
            crypto = require('crypto');
        } catch (err) {
    
    }
    }
    var cryptoSecureRandomInt = function () {
    
    
        if (crypto) {
    
    
            if (typeof crypto.getRandomValues === 'function') {
    
    
                try {
    
    
                    return crypto.getRandomValues(new Uint32Array(1))[0];
                } catch (err) {
    
    }
            }
            if (typeof crypto.randomBytes === 'function') {
    
    
                try {
    
    
                    return crypto.randomBytes(4).readInt32LE();
                } catch (err) {
    
    }
            }
        }
        throw new Error('Native crypto module could not be used to get secure random number.');
    };
    var create = Object.create || (function () {
    
    
        function F() {
    
    }
        return function (obj) {
    
    
            var subtype;
            F.prototype = obj;
            subtype = new F();
            F.prototype = null;
            return subtype;
        };
    }());
    var C = {
    
    };
    var C_lib = C.lib = {
    
    };
    var Base = C_lib.Base = (function () {
    
    
        return {
    
    
            extend: function (overrides) {
    
    
                var subtype = create(this);
                if (overrides) {
    
    
                    subtype.mixIn(overrides);
                }
                if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
    
    
                    subtype.init = function () {
    
    
                        subtype.$super.init.apply(this, arguments);
                    };
                }
                subtype.init.prototype = subtype;
                subtype.$super = this;
                return subtype;
            }, create: function () {
    
    
                var instance = this.extend();
                instance.init.apply(instance, arguments);
                return instance;
            }, init: function () {
    
    }, mixIn: function (properties) {
    
    
                for (var propertyName in properties) {
    
    
                    if (properties.hasOwnProperty(propertyName)) {
    
    
                        this[propertyName] = properties[propertyName];
                    }
                }
                if (properties.hasOwnProperty('toString')) {
    
    
                    this.toString = properties.toString;
                }
            }, clone: function () {
    
    
                return this.init.prototype.extend(this);
            }
        };
    }());
    var WordArray = C_lib.WordArray = Base.extend({
    
    
        init: function (words, sigBytes) {
    
    
            words = this.words = words || [];
            if (sigBytes != undefined) {
    
    
                this.sigBytes = sigBytes;
            } else {
    
    
                this.sigBytes = words.length * 4;
            }
        }, toString: function (encoder) {
    
    
            return (encoder || Hex).stringify(this);
        }, concat: function (wordArray) {
    
    
            var thisWords = this.words;
            var thatWords = wordArray.words;
            var thisSigBytes = this.sigBytes;
            var thatSigBytes = wordArray.sigBytes;
            this.clamp();
            if (thisSigBytes % 4) {
    
    
                for (var i = 0; i < thatSigBytes; i++) {
    
    
                    var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                    thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
                }
            } else {
    
    
                for (var j = 0; j < thatSigBytes; j += 4) {
    
    
                    thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2];
                }
            }
            this.sigBytes += thatSigBytes;
            return this;
        }, clamp: function () {
    
    
            var words = this.words;
            var sigBytes = this.sigBytes;
            words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
            words.length = Math.ceil(sigBytes / 4);
        }, clone: function () {
    
    
            var clone = Base.clone.call(this);
            clone.words = this.words.slice(0);
            return clone;
        }, random: function (nBytes) {
    
    
            var words = [];
            var r = (function (m_w) {
    
    
                var m_w = m_w;
                var m_z = 0x3ade68b1;
                var mask = 0xffffffff;
                return function () {
    
    
                    m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
                    m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
                    var result = ((m_z << 0x10) + m_w) & mask;
                    result /= 0x100000000;
                    result += 0.5;
                    return result * (Math.random() > .5 ? 1 : -1);
                }
            });
            var RANDOM = false, _r;
            try {
    
    
                cryptoSecureRandomInt();
                RANDOM = true;
            } catch (err) {
    
    }
            for (var i = 0, rcache; i < nBytes; i += 4) {
    
    
                if (!RANDOM) {
    
    
                    _r = r((rcache || Math.random()) * 0x100000000);
                    rcache = _r() * 0x3ade67b7;
                    words.push((_r() * 0x100000000) | 0);
                    continue;
                }
                words.push(cryptoSecureRandomInt());
            }
            return new WordArray.init(words, nBytes);
        }
    });
    var C_enc = C.enc = {
    
    };
    var Hex = C_enc.Hex = {
    
    
        stringify: function (wordArray) {
    
    
            var words = wordArray.words;
            var sigBytes = wordArray.sigBytes;
            var hexChars = [];
            for (var i = 0; i < sigBytes; i++) {
    
    
                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                hexChars.push((bite >>> 4).toString(16));
                hexChars.push((bite & 0x0f).toString(16));
            }
            return hexChars.join('');
        }, parse: function (hexStr) {
    
    
            var hexStrLength = hexStr.length;
            var words = [];
            for (var i = 0; i < hexStrLength; i += 2) {
    
    
                words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
            }
            return new WordArray.init(words, hexStrLength / 2);
        }
    };
    var Latin1 = C_enc.Latin1 = {
    
    
        stringify: function (wordArray) {
    
    
            var words = wordArray.words;
            var sigBytes = wordArray.sigBytes;
            var latin1Chars = [];
            for (var i = 0; i < sigBytes; i++) {
    
    
                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                latin1Chars.push(String.fromCharCode(bite));
            }
            return latin1Chars.join('');
        }, parse: function (latin1Str) {
    
    
            var latin1StrLength = latin1Str.length;
            var words = [];
            for (var i = 0; i < latin1StrLength; i++) {
    
    
                words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
            }
            return new WordArray.init(words, latin1StrLength);
        }
    };
    var Utf8 = C_enc.Utf8 = {
    
    
        stringify: function (wordArray) {
    
    
            try {
    
    
                return decodeURIComponent(escape(Latin1.stringify(wordArray)));
            } catch (e) {
    
    
                throw new Error('Malformed UTF-8 data');
            }
        }, parse: function (utf8Str) {
    
    
            return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
        }
    };
    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
    
    
        reset: function () {
    
    
            this._data = new WordArray.init();
            this._nDataBytes = 0;
        }, _append: function (data) {
    
    
            if (typeof data == 'string') {
    
    
                data = Utf8.parse(data);
            }
            this._data.concat(data);
            this._nDataBytes += data.sigBytes;
        }, _process: function (doFlush) {
    
    
            var processedWords;
            var data = this._data;
            var dataWords = data.words;
            var dataSigBytes = data.sigBytes;
            var blockSize = this.blockSize;
            var blockSizeBytes = blockSize * 4;
            var nBlocksReady = dataSigBytes / blockSizeBytes;
            if (doFlush) {
    
    
                nBlocksReady = Math.ceil(nBlocksReady);
            } else {
    
    
                nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
            }
            var nWordsReady = nBlocksReady * blockSize;
            var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
            if (nWordsReady) {
    
    
                for (var offset = 0; offset < nWordsReady; offset += blockSize) {
    
    
                    this._doProcessBlock(dataWords, offset);
                }
                processedWords = dataWords.splice(0, nWordsReady);
                data.sigBytes -= nBytesReady;
            }
            return new WordArray.init(processedWords, nBytesReady);
        }, clone: function () {
    
    
            var clone = Base.clone.call(this);
            clone._data = this._data.clone();
            return clone;
        }, _minBufferSize: 0
    });
    var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
    
    
        cfg: Base.extend(),
        init: function (cfg) {
    
    
            this.cfg = this.cfg.extend(cfg);
            this.reset();
        }, reset: function () {
    
    
            BufferedBlockAlgorithm.reset.call(this);
            this._doReset();
        }, update: function (messageUpdate) {
    
    
            this._append(messageUpdate);
            this._process();
            return this;
        }, finalize: function (messageUpdate) {
    
    
            if (messageUpdate) {
    
    
                this._append(messageUpdate);
            }
            var hash = this._doFinalize();
            return hash;
        }, blockSize: 512 / 32,
        _createHelper: function (hasher) {
    
    
            return function (message, cfg) {
    
    
                return new hasher.init(cfg).finalize(message);
            };
        }, _createHmacHelper: function (hasher) {
    
    
            return function (message, key) {
    
    
                return new C_algo.HMAC.init(hasher, key).finalize(message);
            };
        }
    });
    var C_algo = C.algo = {
    
    };
    return C;
}(Math));

(function (Math) {
    
    
    var C = CryptoJS;
    var C_lib = C.lib;
    var WordArray = C_lib.WordArray;
    var Hasher = C_lib.Hasher;
    var C_algo = C.algo;
    var T = [];
    (function () {
    
    
        for (var i = 0; i < 64; i++) {
    
    
            T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0;
        }
    }());
    var MD5 = C_algo.MD5 = Hasher.extend({
    
    
        _doReset: function () {
    
    
            this._hash = new WordArray.init([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]);
        }, _doProcessBlock: function (M, offset) {
    
    
            for (var i = 0; i < 16; i++) {
    
    
                var offset_i = offset + i;
                var M_offset_i = M[offset_i];
                M[offset_i] = ((((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00));
            }
            var H = this._hash.words;
            var M_offset_0 = M[offset + 0];
            var M_offset_1 = M[offset + 1];
            var M_offset_2 = M[offset + 2];
            var M_offset_3 = M[offset + 3];
            var M_offset_4 = M[offset + 4];
            var M_offset_5 = M[offset + 5];
            var M_offset_6 = M[offset + 6];
            var M_offset_7 = M[offset + 7];
            var M_offset_8 = M[offset + 8];
            var M_offset_9 = M[offset + 9];
            var M_offset_10 = M[offset + 10];
            var M_offset_11 = M[offset + 11];
            var M_offset_12 = M[offset + 12];
            var M_offset_13 = M[offset + 13];
            var M_offset_14 = M[offset + 14];
            var M_offset_15 = M[offset + 15];
            var a = H[0];
            var b = H[1];
            var c = H[2];
            var d = H[3];
            a = FF(a, b, c, d, M_offset_0, 7, T[0]);
            d = FF(d, a, b, c, M_offset_1, 12, T[1]);
            c = FF(c, d, a, b, M_offset_2, 17, T[2]);
            b = FF(b, c, d, a, M_offset_3, 22, T[3]);
            a = FF(a, b, c, d, M_offset_4, 7, T[4]);
            d = FF(d, a, b, c, M_offset_5, 12, T[5]);
            c = FF(c, d, a, b, M_offset_6, 17, T[6]);
            b = FF(b, c, d, a, M_offset_7, 22, T[7]);
            a = FF(a, b, c, d, M_offset_8, 7, T[8]);
            d = FF(d, a, b, c, M_offset_9, 12, T[9]);
            c = FF(c, d, a, b, M_offset_10, 17, T[10]);
            b = FF(b, c, d, a, M_offset_11, 22, T[11]);
            a = FF(a, b, c, d, M_offset_12, 7, T[12]);
            d = FF(d, a, b, c, M_offset_13, 12, T[13]);
            c = FF(c, d, a, b, M_offset_14, 17, T[14]);
            b = FF(b, c, d, a, M_offset_15, 22, T[15]);
            a = GG(a, b, c, d, M_offset_1, 5, T[16]);
            d = GG(d, a, b, c, M_offset_6, 9, T[17]);
            c = GG(c, d, a, b, M_offset_11, 14, T[18]);
            b = GG(b, c, d, a, M_offset_0, 20, T[19]);
            a = GG(a, b, c, d, M_offset_5, 5, T[20]);
            d = GG(d, a, b, c, M_offset_10, 9, T[21]);
            c = GG(c, d, a, b, M_offset_15, 14, T[22]);
            b = GG(b, c, d, a, M_offset_4, 20, T[23]);
            a = GG(a, b, c, d, M_offset_9, 5, T[24]);
            d = GG(d, a, b, c, M_offset_14, 9, T[25]);
            c = GG(c, d, a, b, M_offset_3, 14, T[26]);
            b = GG(b, c, d, a, M_offset_8, 20, T[27]);
            a = GG(a, b, c, d, M_offset_13, 5, T[28]);
            d = GG(d, a, b, c, M_offset_2, 9, T[29]);
            c = GG(c, d, a, b, M_offset_7, 14, T[30]);
            b = GG(b, c, d, a, M_offset_12, 20, T[31]);
            a = HH(a, b, c, d, M_offset_5, 4, T[32]);
            d = HH(d, a, b, c, M_offset_8, 11, T[33]);
            c = HH(c, d, a, b, M_offset_11, 16, T[34]);
            b = HH(b, c, d, a, M_offset_14, 23, T[35]);
            a = HH(a, b, c, d, M_offset_1, 4, T[36]);
            d = HH(d, a, b, c, M_offset_4, 11, T[37]);
            c = HH(c, d, a, b, M_offset_7, 16, T[38]);
            b = HH(b, c, d, a, M_offset_10, 23, T[39]);
            a = HH(a, b, c, d, M_offset_13, 4, T[40]);
            d = HH(d, a, b, c, M_offset_0, 11, T[41]);
            c = HH(c, d, a, b, M_offset_3, 16, T[42]);
            b = HH(b, c, d, a, M_offset_6, 23, T[43]);
            a = HH(a, b, c, d, M_offset_9, 4, T[44]);
            d = HH(d, a, b, c, M_offset_12, 11, T[45]);
            c = HH(c, d, a, b, M_offset_15, 16, T[46]);
            b = HH(b, c, d, a, M_offset_2, 23, T[47]);
            a = II(a, b, c, d, M_offset_0, 6, T[48]);
            d = II(d, a, b, c, M_offset_7, 10, T[49]);
            c = II(c, d, a, b, M_offset_14, 15, T[50]);
            b = II(b, c, d, a, M_offset_5, 21, T[51]);
            a = II(a, b, c, d, M_offset_12, 6, T[52]);
            d = II(d, a, b, c, M_offset_3, 10, T[53]);
            c = II(c, d, a, b, M_offset_10, 15, T[54]);
            b = II(b, c, d, a, M_offset_1, 21, T[55]);
            a = II(a, b, c, d, M_offset_8, 6, T[56]);
            d = II(d, a, b, c, M_offset_15, 10, T[57]);
            c = II(c, d, a, b, M_offset_6, 15, T[58]);
            b = II(b, c, d, a, M_offset_13, 21, T[59]);
            a = II(a, b, c, d, M_offset_4, 6, T[60]);
            d = II(d, a, b, c, M_offset_11, 10, T[61]);
            c = II(c, d, a, b, M_offset_2, 15, T[62]);
            b = II(b, c, d, a, M_offset_9, 21, T[63]);
            H[0] = (H[0] + a) | 0;
            H[1] = (H[1] + b) | 0;
            H[2] = (H[2] + c) | 0;
            H[3] = (H[3] + d) | 0;
        }, _doFinalize: function () {
    
    
            var data = this._data;
            var dataWords = data.words;
            var nBitsTotal = this._nDataBytes * 8;
            var nBitsLeft = data.sigBytes * 8;
            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
            var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
            var nBitsTotalL = nBitsTotal;
            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ((((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00));
            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ((((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00));
            data.sigBytes = (dataWords.length + 1) * 4;
            this._process();
            var hash = this._hash;
            var H = hash.words;
            for (var i = 0; i < 4; i++) {
    
    
                var H_i = H[i];
                H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00);
            }
            return hash;
        }, clone: function () {
    
    
            var clone = Hasher.clone.call(this);
            clone._hash = this._hash.clone();
            return clone;
        }
    });
    function FF(a, b, c, d, x, s, t) {
    
    
        var n = a + ((b & c) | (~b & d)) + x + t;
        return ((n << s) | (n >>> (32 - s))) + b;
    }
    function GG(a, b, c, d, x, s, t) {
    
    
        var n = a + ((b & d) | (c & ~d)) + x + t;
        return ((n << s) | (n >>> (32 - s))) + b;
    }
    function HH(a, b, c, d, x, s, t) {
    
    
        var n = a + (b ^ c ^ d) + x + t;
        return ((n << s) | (n >>> (32 - s))) + b;
    }
    function II(a, b, c, d, x, s, t) {
    
    
        var n = a + (c ^ (b | ~d)) + x + t;
        return ((n << s) | (n >>> (32 - s))) + b;
    }
    C.MD5 = Hasher._createHelper(MD5);
    C.HmacMD5 = Hasher._createHmacHelper(MD5);
}(Math));

function MD5_Encrypt(word) {
    
    
    return CryptoJS.MD5(word).toString();
    //反转:
    //return CryptoJS.MD5(word).toString().split("").reverse().join("");
}

2. execjs module calls JS code

import execjs
with open(file='encode.js', mode='r', encoding='utf-8') as fis:
     js_code = fis.read()  # 读取JS代码文件
js_obj = execjs.compile(js_code)  # 激将JS代码传入
js_obj.call('function', 'params')  # 调用JS的函数, 参数1:函数名、参数2:该函数所需要的参数

3. For more information, please pay attention to the official account [Programmer Xiaozhou]

6. Pip module installation

mirror address

  • Tsinghua: https://pypi.tuna.tsinghua.edu.cn/simple
  • Alibaba Cloud: http://mirrors.aliyun.com/pypi/simple/
  • University of Science and Technology of China https://pypi.mirrors.ustc.edu.cn/simple/
  • Huazhong University of Science and Technology: http://pypi.hustunique.com/
  • Shandong University of Technology: http://pypi.sdutlinux.org/
  • Douban: http://pypi.douban.com/simple/

The third-party modules and corresponding versions used in the case

  • requests==2.27.0
  • PyExecJS==1.5.1
  • lxml==4.9.1

pip specified module installation: pip install module name -i https://pypi.tuna.tsinghua.edu.cn/simple
pip specified requirements.txt file installation: pip install -i https://pypi.doubanio.com/simple/ -r requirements.txt


Notice

All content in this article is for learning and communication only. The content of captured packets, sensitive URLs, and data interfaces have been desensitized, and it is strictly prohibited to be used for commercial or illegal purposes. Otherwise, all consequences arising therefrom have nothing to do with the author. If there is any infringement , please contact me to delete immediately!

This article is prohibited from reprinting without permission, and any secondary dissemination after modification is prohibited. The author is not responsible for any accidents caused by unauthorized use of the technology explained in this article. If there is any infringement, please contact the author immediately on the official account [programmer Xiaozhou] delete!

Guess you like

Origin blog.csdn.net/EXIxiaozhou/article/details/128712138