主頁 > 後端開發 > 【JS 逆向百例】AST 脫混淆實戰,某 ICP 備案號查詢介面 jsjiami v6 分析

【JS 逆向百例】AST 脫混淆實戰,某 ICP 備案號查詢介面 jsjiami v6 分析

2022-05-28 01:10:58 後端開發

關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術干貨!

宣告

本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用于商業用途和非法用途,否則由此產生的一切后果均與作者無關,若有侵權,請在公眾號聯系我立即洗掉!

逆向目標

  • 目標:站 Z 之家網站 ICP 備案號查詢
  • 主頁:aHR0cDovL2ljcC5jaGluYXouY29tLw==
  • 介面:aHR0cDovL2ljcC5jaGluYXouY29tL2hvbWUvR2V0UGVyaW1pdEJ5SG9zdA==
  • 逆向引數:hostTokenpermitToken

本次主要是 AST 解混淆實戰,本例中的 JS 混淆方式是 sojson 旗下的 jsjiami v6 版本,感興趣的可以去官網體驗一下:https://www.jsjiami.com/ ,如果你還不了解 AST,可以先看看 K 哥上期的文章(非常詳細):《逆向進階,利用 AST 技識訓原 JavaScript 混淆代碼》,本文部分 AST 還原代碼直接使用了上期文章中的代碼,所以細節方面不再贅述,有疑問的地方可以參考參考上期文章,

第三方工具

逆向領域大佬云集,市面上已經有很多大佬寫好的解混淆工具了,除了我們自己手動去寫 AST 決議代碼以外,有時候直接使用工具會更加方便,當然并沒有十全十美的工具,不過大部分情況下都能成功解混淆的,以下工具值得去體驗一下:

  • 蔡老板一鍵還原 OB 混淆:https://github.com/Tsaiboss/decodeObfuscator
  • 哲哥 AST 混淆還原框架:https://github.com/sml2h3/ast_tools
  • V 神 Chrome 插件,內置 AST 混淆還原:https://github.com/cilame/v_jstools
  • jsjiami v6 專用解密工具:https://github.com/NXY666/JsjiamiV6-Decryptor

抓包分析

進入主題,首先抓包看看,來到 ICP 備案查詢頁面,查詢結果中,其他資訊都可以直接在相應的 html 原始碼中找到,只有這個備案號是通過介面傳過來的,對應的請求和相關加密引數如下圖所示:

01

02

加密定位

直接搜索關鍵字 hostToken 或者 permitToken 即可定位:

03

04

關鍵代碼:

'data': {
    'kw': kw,
    'hostToken': _0x791532['IIPmq'](generateHostKey, kw),
    'permitToken': _0x791532[_0x404f('?1df', '7Gn4')](generateWordKey, kw)
}

這里的混淆可以手動跟一下,還原后如下:

'data': {
    'kw': kw,
    'hostToken': generateHostKey(kw),
    'permitToken': generateWordKey(kw)
}

kw 是查詢的域名,有用的就是 generateHostKey()generateWordKey() 兩個方法了,跟進去看,代碼經過了 jsjiami v6 混淆:

05

06

07

AST 脫混淆

jsjiami 混淆的特征其實和 OB 混淆是類似的:

  1. 一般由一個大陣列或者含有大陣列的函式、一個陣列位移操作的自執行函式、一個解密函式和加密后的函式四部分組成;
  2. 函式名和變數名通常以 _0x 或者 0x 開頭,后接 1~6 位數字或字母組合;
  3. 陣列位移操作的自執行函式里,有明顯的 push、shift 關鍵字,

本例中,generateHostKey() 方法在 commo.js 里,generateWordKey() 方法在 generatetoken.js 里,結構如下圖所示:

08

觀察 generatetoken.js 檔案,可以發現這里面也有 commo.js 里面的 generateHostKey()getRandom() 方法,從方法名來看貌似是重復了,實際上混淆還原后方法是一樣的,所以這里我們只需要還原 generatetoken.js 就可以了,

檔案結構

  • 混淆 JS 檔案:generatetoken.js
  • AST 還原代碼:generatetokenAst.js
  • 還原后的代碼:generatetokenNew.js

解密函式還原

在原來混淆后的 JS 里,解密函式是 _0x530e,首先觀察整個 JS,呼叫了很多次解密函式,類似于:_0x530e('1', '7XEq')

注意這里代碼里面有一些特殊字符,類似于 RLERLO 之類的,如果在 VSCode 打開是一些 U+202BU+202E 的字符,實際上這是 RTLO (Right-to-Left Override) 字符,U+202BU+202E 的意思分別是根據記憶體順序從左至右和從右至左顯示字符,感興趣的可以網上搜索了解一下,這里并不影響我們進行還原操作,但是如果直接復制過來的話就會導致前后文顯示的順序不對,所以本文中為了方便描述,粘貼的部分代碼就手動去掉了這些字符,

09

10

所以第一步我們要還原一下解密函式,把所有 _0x530e 呼叫的地方直接替換成實際值,首先需要將大陣列、自執行函式、加密函式和解密函式分割開,將代碼放到 astexplorer.net 看一下,也就是將 body 的前四部分和后面剩余部分分割開來,如下圖所示:

11

分割代碼:

const fs = require("fs");
const parse = require("@babel/parser").parse;
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")

// 匯入混淆代碼并決議為 AST
const oldCode = fs.readFileSync("generatetoken.js", {encoding: "utf-8"});
const astCode = parse(oldCode);

// 獲取整個 AST 節點的長度
let astCodeLength = astCode.program.body.length

// 獲取解密函式的名字 也就是 _0x530e
let decryptFunctionName = astCode.program.body[3].id.name

// 分割加密函式和解密函式,即 body 的前四部分和后面剩余部分
let decryptFunction = astCode.program.body.slice(0, 4)
let encryptFunction = astCode.program.body.slice(4, astCodeLength)

// 獲取加密函式和解密函式的方法多種多樣,比如可以挨個取值并轉換成 JS 代碼
// 這樣做就不需要將解密函式賦值給整個 AST 節點了
// let decryptFunction = "";
// for(let i=0; i<4; i++){
//     decryptFunction += generate(astCode.program.body[i], {compact: true}).code
// }
// eval(decryptFunction);

在上面的獲取加密函式和解密函式的代碼中,方法不是唯一的,多種多樣,比如直接回圈取 body 并轉換成 JS 代碼,比如直接人工把大陣列、自執行函式和解密函式三部分,拿出來放到一個新檔案里,然后匯出解密方法,后續直接呼叫也可以,

在本例中,拿到解密函式后,需要將其賦值給整個 AST 節點,然后再將整個 AST 節點轉換成 JavaScript 代碼,這里注意有可能會檢測代碼是否格式化,所以建議轉換要加一個 compact 引數,避免格式化,轉換完成后 eval 執行一下,讓陣列位移操作完成,然后我們就可以直接呼叫解密函式,即 _0x530e()

// 將解密函式賦值給整個 AST 節點
astCode.program.body = decryptFunction

// 將 AST 節點轉換成 JS 代碼,并 eval 執行一下
decryptFunction = generate(astCode, {compact: true}).code
eval(decryptFunction);

// 測驗一下,直接呼叫 _0x530e 函式可以正確拿到結果
// 輸出 split
// console.log(_0x530e('?b', 'Zp9G'))

現在我們能直接呼叫解密函式 _0x530e() 了,接下來要做的就是怎么把混淆代碼中所有呼叫 _0x530e() 的地方替換成真實值,在此之前,我們要把加密函式(generateKey()generateHostKey()generateWordKey()getRandom())賦值給整個 AST 節點,此時整個節點就沒有大陣列、自執行函式和解密函式了,解密函式 _0x530e() 已經被寫入記憶體,所以后面不影響我們呼叫,

老樣子,還是先在 astexplorer.net 看一下呼叫 _0x530e() 的地方,以 _0x530e('b', 'Zp9G') 為例,其真實值應該是 split,對比一下替換前后的結構,如下圖所示:

12

13

可以看到節點由原來的 CallExpression 變成了 StringLiteral,所以我們可以遍歷 CallExpression,如果函式名為解密函式名,那就通過 path.toString() 方法獲取節點原始碼,也就類似 _0x530e('b', 'Zp9G') 的原始碼,然后 eval 執行一下獲取其真實值,再使用 types.stringLiteral() 構建 StringLiteral 節點,最后通過 path.replaceInline() 方法替換節點,遍歷代碼如下:

// 將加密函式賦值給整個 AST 節點,此時整個節點就沒有大陣列、自執行函式和解密函式了
astCode.program.body = encryptFunction

// 呼叫解密函式,直接計算出類似以下方法的值并替換
// 混淆代碼:_0x530e('?b', 'Zp9G')
// 還原后:split
const visitor1 = {
    CallExpression(path){
        if (path.node.callee.name === decryptFunctionName && path.node.arguments.length === 2){
            path.replaceInline(types.stringLiteral(eval(path.toString())))
        }
    }
}

// 遍歷節點
traverse(astCode, visitor1)

// 將 AST 節點轉換成 JS 代碼并寫入到新檔案里
const result = generate(astCode, {concise:true}).code
fs.writeFile("./generatetokenNew.js", result, (err => {console.log(err)}))

自此,第一步的解密函式還原就完成了,可以看一下還原前后的對比,如下圖所示淺藍色標記的地方,所有呼叫 _0x530e() 的地方都被還原了:

14

大物件還原

初步還原后我們的代碼里就只剩下以下四個方法:

  • generateKey()
  • generateHostKey()
  • generateWordKey()
  • getRandom()

再觀察代碼,發現每個方法一開始都有個大的物件,他們分別是:

  • _0x3b79c6
  • _0x278b2d
  • _0x4115c4
  • _0xd8ec33

后續的代碼也在不斷呼叫這個物件的方法,比如 _0x3b79c6["esdtg"](_0x2e5848["length"], 0x4) 實際上就是 _0x2e5848["length"] != 0x4,如下圖所示:

15

首先我們將這四個大的物件單獨提取出來,還是保持原來的鍵值對樣式,提取完成后洗掉這兩個節點,遍歷代碼如下:

let functionName = {
    "_0x3b79c6": {},
    "_0x278b2d": {},
    "_0x4115c4": {},
    "_0xd8ec33": {}
}

// 單獨提取出四個大物件
const visitor2 = {
    VariableDeclarator(path){
        for (let key in functionName){
            if (path.node && path.node.id.name == key) {
                const properties = path.node.init.properties
                for (let i=0; i<properties.length; i++){
                    functionName[key][properties[i].key.value] = properties[i].value
                }
                // 寫入物件后就可以洗掉該節點了
                path.remove()
            }
        }
    }
}

這里要注意,大的物件里面,有 +-== 之類的二項式計算,也有直接為字串的,還有變成函式呼叫的,如下所示:

var _0x3b79c6 = {
    'MuRlB': function (_0x3ca134, _0x50ee94) {
        return _0x3ca134 + _0x50ee94;
    }, 
    'Ucwyj': function (_0x32bfa3, _0x3b191b) {
        return _0x32bfa3(_0x3b191b);
    }, 
    'YrYQW': '#IpValue'
}

針對不同的情況有不同的處理方法,同時還要注意傳參和 return 回傳的引數位置,不要還原后把 a - b 搞成 b - a 了,當然在本例中傳入和回傳的順序是一樣的,就不需要考慮這個問題,

字串還原

首先來看字串,有以下幾種情況:

  • _0x3b79c6['YrYQW'] 為例,實際上其值為字串 '#IpValue',觀察其結構,是一個 MemberExpression,在一個串列里;
  • _0x278b2d['pjbyX'] 為例,實際上其值為字串 '3|2|1|4|5|0|6',觀察其結構,是一個 MemberExpression,在一個字典里;
  • _0x278b2d['CnTaO'] 為例,雖然也是一個 MemberExpression,也在一個字典里,但實際上是二項式計算,所以要排除在外,

16

17

18

所以我們在寫遍歷代碼時,同時要注意這三種情況,滿足條件后直接取原來大物件對應的節點進行替換即可,遍歷代碼如下所示:

// 函式替換,字串替換:將類似 _0x3b79c6['YrYQW'] 變成 '#IpValue'
const visitor3 = {
    MemberExpression(path) {
        for (let key in functionName){
            if (path.node.object && path.node.object.name == key && path.inList ) {
                path.replaceInline(functionName[key][path.node.property.value])
            }
            if (path.node.object && path.node.object.name == key && path.parent.property && path.parent.property.value =https://www.cnblogs.com/ikdl/p/="split") {
                path.replaceInline(functionName[key][path.node.property.value])
            }
        }
    }
}

二項式計算替換

再來看看二項式計算的情況,以 _0x278b2d['CnTaO'](_0x691267["length"], 0x1) 為例,實際上是做減法運算,即 _0x691267["length"] - 0x1,看一下替換前后對比:

19

20

對于這種情況,我們可以直接提取兩個引數,然后提取大物件里對應方法的運算子,然后將引數和運算子直接連接起來組成新的節點(binaryExpression)并替換即可,遍歷代碼如下:

// 函式替換,二項式計算:將類似 _0x278b2d['CnTaO'](_0x691267["length"], 0x1) 變成 _0x691267["length"] - 0x1
const visitor4 = {
    CallExpression(path){
        for (let key in functionName) {
            if (path.node.callee && path.node.callee.object && path.node.callee.object.name == key) {
                let func = functionName[key][path.node.callee.property.value]
                if (func.body.body[0].argument.type == "BinaryExpression") {
                    let operator = func.body.body[0].argument.operator
                    let left = path.node.arguments[0]
                    let right = path.node.arguments[1]
                    path.replaceInline(types.binaryExpression(operator, left, right))
                }
            }
        }
    }
}

方法呼叫還原

_0x4115c4["PJbSm"](getRandom, 0x64, 0x3e7) 為例,實際上是 getRandom(0x64, 0x3e7),看一下替換前后對比:

21

22

對于這種情況,傳入的第一個引數為方法名稱,后面的都是引數,那么可以直接取第一個元素為方法名稱,使用 slice(1) 方法取后面所有的引數(因為后面的引數個數是不一定的),然后構造新的節點(callExpression)并替換即可,這部分遍歷代碼可以和前面二項式的替換相結合,代碼如下:

// 函式替換,二項式計算:將類似 _0x278b2d['CnTaO'](_0x691267["length"], 0x1) 變成 _0x691267["length"] - 0x1
// 函式替換,方法呼叫:將類似 _0x4115c4["PJbSm"](getRandom, 0x64, 0x3e7) 變成 getRandom(0x64, 0x3e7)
const visitor4 = {
    CallExpression(path){
        for (let key in functionName) {
            if (path.node.callee && path.node.callee.object && path.node.callee.object.name == key) {
                let func = functionName[key][path.node.callee.property.value]
                if (func.body.body[0].argument.type == "BinaryExpression") {
                    let operator = func.body.body[0].argument.operator
                    let left = path.node.arguments[0]
                    let right = path.node.arguments[1]
                    path.replaceInline(types.binaryExpression(operator, left, right))
                }
                if (func.body.body[0].argument.type == "CallExpression") {
                    let identifier = path.node.arguments[0]
                    let arguments = path.node.arguments.slice(1)
                    path.replaceInline(types.callExpression(identifier, arguments))
                }
            }
        }
    }
}

自此,第二步的大物件還原就完成了,可以看一下還原前后的對比,如下圖所示淺藍色標記的地方,所有呼叫四個大物件(_0x3b79c6_0x278b2d_0x4115c4_0xd8ec33)的地方都被還原了:

23

switch-case 反控制流平坦化

經過前面幾步的還原之后,我們發現 generateHostKey()generateWordKey()getRandom() 方法里都有一個 switch-case 的控制流,關于反控制流平坦化的講解在我上期文章有很詳細的介紹,不理解的可以看看上期文章,此處也不再贅述了,直接貼代碼了:

// switch-case 反控制流平坦化
const visitor5 = {
    WhileStatement(path) {
        // switch 節點
        let switchNode = path.node.body.body[0];
        // switch 陳述句內的控制流陣列名,本例中是 _0x28073a、_0x2efb35、_0x187fb8
        let arrayName = switchNode.discriminant.object.name;
        // 獲取控制流陣列系結的節點
        let bindingArray = path.scope.getBinding(arrayName);
        // 獲取節點整個運算式的引數、分割方法、分隔符
        let init = bindingArray.path.node.init;
        let object = init.callee.object.value;
        let property = init.callee.property.value;
        let argument = init.arguments[0].value;
        // 模擬執行 '3|2|1|4|5|0|6'['split']('|') 陳述句
        let array = object[property](argument)
        // 也可以直接取引數進行分割,方法不通用,比如分隔符換成 , 就不行了
        // let array = init.callee.object.value.split('|');

        // switch 陳述句內的控制流自增變數名,本例中是 _0x38c69e、_0x396880、_0x3b3dc7
        let autoIncrementName = switchNode.discriminant.property.argument.name;
        // 獲取控制流自增變數名系結的節點
        let bindingAutoIncrement = path.scope.getBinding(autoIncrementName);
        // 可選擇的操作:洗掉控制流陣列系結的節點、自增變數名系結的節點
        bindingArray.path.remove();
        bindingAutoIncrement.path.remove();

        // 儲存正確順序的控制流陳述句
        let replace = [];
        // 遍歷控制流陣列,按正確順序取 case 內容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個節點是 continue 陳述句,則洗掉 ContinueStatement 節點
                if (types.isContinueStatement(consequent[consequent.length - 1])) {
                    consequent.pop();
                }
                // concat 方法拼接多個陣列,即正確順序的 case 內容
                replace = replace.concat(consequent);
            }
        );
        // 替換整個 while 節點,兩種方法都可以
        path.replaceWithMultiple(replace);
        // path.replaceInline(replace);
    }
}

其他細節還原

到這里其實大部分混淆都已經還原了,已經很容易分析其邏輯了,還剩下一些細節,我們也還原一下,主要有以下細節:

  • 十六進制、Unicode 編碼等,轉正常字符;
  • 物件屬性還原,比如 _0x3cbc20['length'] 轉換成 _0x3cbc20.length
  • 運算式還原,比如 !![] 直接計算成 true;
  • 洗掉未參考的變數,比如 _0xodD= "jsjiami.com.v6";
  • 洗掉冗余邏輯代碼,只保留 if 為 true 的,

這些還原代碼在我上期文章有詳細講過,結合代碼,在 astexplorer.net 對照其結構看,也能理解,同樣也不贅述了,直接貼代碼:

const visitor5 = {
    // 十六進制、Unicode 編碼等,轉正常字符
    "StringLiteral|NumericLiteral"(path){
        delete path.node.extra;
    },
    // _0x3cbc20["length"] 轉換成 _0x3cbc20.length
    MemberExpression(path){
        if (path.node.property.type == "StringLiteral") {
            path.node.computed = false
            path.node.property = types.identifier(path.node.property.value)
        }
    },
    // 運算式還原,!![] 直接計算成 true
    "BinaryExpression|UnaryExpression"(path) {
        let {confident, value} = path.evaluate()
        if (confident){
            path.replaceInline(types.valueToNode(value))
        }
    },
    // 洗掉未參考的變數,比如 _0xodD = "jsjiami.com.v6";
    AssignmentExpression(path){
        let binding = path.scope.getBinding(path.node.left.name);
        if (!binding) {
            path.remove();
        }
    }
}

// 洗掉冗余邏輯代碼,只保留 if 為 true 的
const visitor6 = {
    IfStatement(path) {
        if(path.node.test.type == "BooleanLiteral") {
            if(path.node.test.value) {
                path.replaceInline(path.node.consequent.body)
            } else {
                path.replaceInline(path.node.alternate.body)
            }
        }
    }
}

自此 jajiami v6 混淆就還原完畢了,還原前后對比一下,代碼量縮短了很多,邏輯也更加清楚了,如下圖所示:

24

最后結合 Python 代碼,攜帶生成的 hostTokenpermitToken,成功拿到備案號:

25

完整代碼

原混淆代碼 generatetoken.js、AST 脫混淆代碼 generatetokenAst.js、還原后的代碼 generatetokenNew.js,以及 Python 測驗代碼均在 GitHub,均有詳細注釋,歡迎 Star,所有內容僅供學習交流,嚴禁用于商業用途、非法用途,否則由此產生的一切后果均與作者無關,在倉庫中下載的檔案學習完畢之后請于 24 小時內洗掉!

代碼地址:https://github.com/kgepachong/crawler/tree/main/icp_chinaz_com

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/481663.html

標籤:Python

上一篇:獲取C#串列中兩個不同值組合的最高頻率

下一篇:Conda 創建 Python 虛擬環境不純凈的問題(2021.1.18)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more