当前位置: 首页 > news >正文

淺談Cocos2djs逆向

前言

簡單聊一下cocos2djs手遊的逆向,有任何相關想法歡迎和我討論^^

一些概念

列出一些個人認為比較有用的概念:

  • Cocos遊戲的兩大開發工具分別是CocosCreatorCocosStudio,區別是前者是cocos2djs專用的開發工具,後者則是cocos2d-lua、cocos2d-cpp那些。

  • 使用Cocos Creator 2開發的手遊,生成的關鍵so默認名稱是libcocos2djs.so
  • 使用Cocos Creator 3開發的手遊,生成的關鍵so默認名稱是libcocos.so ( 入口函數非applicationDidFinishLaunching )
  • Cocos Creator在構建時可以選擇是否對.js腳本進行加密&壓縮,而加密算法固定是xxtea,還可以選擇是否使用Zip壓縮

  • libcocos2djs.so裡的AppDelegate::applicationDidFinishLaunching是入口函數,可以從這裡開始進行分析
  • Cocos2djs是Cocos2d-x的一個分支,因此https://github.com/cocos2d/cocos2d-x源碼同樣適用於Cocos2djs

自己寫一個Demo

自己寫一個Demo來分析的好處是能夠快速地判斷某個錯誤是由於被檢測到?還是本來就會如此?

版本信息

嘗試過2.4.2、2.4.6兩個版本,都構建失敗,最終成功的版本信息如下:

  • 編輯器版本:Creator 2.4.13 ( 2系列裡的最高版本,低版本在AS編譯時會報一堆錯誤 )
  • ndk版本:23.1.7779620
  • project/build.gradleclasspath 'com.android.tools.build:gradle:8.0.2'
  • project/gradle/gradle-wrapper.propertiesdistributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip

Cocos Creator基礎用法

由於本人不懂cocos遊戲開發,只好直接用官方的Hello World模板。

首先要設置SDK和NDK路徑

然後構建的參數設置如下,主要需要設置以下兩點:

  • 加密腳本:全都勾上,密鑰用默認的
  • Source Map:保留符號,這樣IDA在打開時才能看到函數名

我使用Cocos Creator能順利構建,但無法編譯,只好改用Android Studio來編譯。

使用Android Studio打開build\jsb-link\frameworks\runtime-src\proj.android-studio,然後就可以按正常AS流程進行編譯

Demo如下所示,在中心輸出了Hello, World!

jsc腳本解密

上述Demo構建中有一個選項是【加密腳本】,它會將js腳本通過xxtea算法加密成.jsc

而遊戲的一些功能就會通過js腳本來實現,因此cocos2djs逆向首要事件就是將.jsc解密,通常.jsc會存放在apk內的assets目錄下

獲取解密key

方法一:從applicationDidFinishLaunching入手

方法二:HOOK

  1. hook set_xxtea_key
// soName: libcocos2djs.so
function hook_jsb_set_xxtea_key(soName) {let set_xxtea_key = Module.findExportByName(soName, "_Z17jsb_set_xxtea_keyRKNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[0].readCString())},onLeave(retval){}})
}
  1. hook xxtea_decrypt
function hook_xxtea_decrypt(soName) {let set_xxtea_key = Module.findExportByName(soName, "xxtea_decrypt");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[2].readCString())},onLeave(retval){}})
}

python加解密腳本

一次性解密output_dir目錄下所有.jsc,並在input_dir生成與output_dir同樣的目錄結構。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

# pip install xxtea-py

# pip install jsbeautifier

import xxtea

import gzip

import jsbeautifier

import os

KEY = "abdbe980-786e-45"

input_dir = r"cocos2djs_demo\assets" # abs path

output_dir = r"cocos2djs_demo\output" # abs path

def jscDecrypt(data: bytes, needJsBeautifier = True):

    dec = xxtea.decrypt(data, KEY)

    jscode = gzip.decompress(dec).decode()

    if needJsBeautifier:

        return jsbeautifier.beautify(jscode)

    else:

        return jscode

def jscEncrypt(data):

    compress_data = gzip.compress(data.encode())

    enc = xxtea.encrypt(compress_data, KEY)

    return enc

def decryptAll():

    for root, dirs, files in os.walk(input_dir):

         

        # 創建與input_dir一致的結構

        for dir in dirs:

            dir_path = os.path.join(root, dir)

            target_dir = output_dir + dir_path.replace(input_dir, "")

            if not os.path.exists(target_dir):

                os.mkdir(target_dir)

        for file in files:

            file_path = os.path.join(root, file)

        

            if not file.endswith(".jsc"):

                continue

             

            with open(file_path, mode = "rb") as f:

                enc_jsc = f.read()

             

            dec_jscode = jscDecrypt(enc_jsc)

             

            output_file_path = output_dir + file_path.replace(input_dir, "").replace(".jsc", "") + ".js"

            print(output_file_path)

            with open(output_file_path, mode = "w", encoding = "utf-8") as f:

                f.write(dec_jscode)

def decryptOne(path):

    with open(path, mode = "rb") as f:

        enc_jsc = f.read()

     

    dec_jscode = jscDecrypt(enc_jsc, False)

    output_path = path.split(".jsc")[0+ ".js"

    with open(output_path, mode = "w", encoding = "utf-8") as f:

        f.write(dec_jscode)

def encryptOne(path):

    with open(path, mode = "r", encoding = "utf-8") as f:

        jscode = f.read()

    enc_data = jscEncrypt(jscode)

     

    output_path = path.split(".js")[0+ ".jsc"

    with open(output_path, mode = "wb") as f:

        f.write(enc_data)

if __name__ == "__main__":

    decryptAll()

jsc文件的2種讀取方式

為實現對遊戲正常功能的干涉,顯然需要修改遊戲執行的js腳本。而替換.jsc文件是其中一種思路,前提是要找到讀取.jsc文件的地方。

方式一:從apk裡讀取

我自己編譯的Demo就是以這種方式讀取/data/app/XXX/base.apkassets目錄內的.jsc文件。

cocos引擎默認使用xxtea算法來對.jsc等腳本進行加密,因此讀取.jsc的操作定然在xxtea_decrypt之前。

跟cocos2d-x源碼,找使用xxtea_decrypt的地方,可以定位到LuaStack::luaLoadChunksFromZIP

向上跟會發現它的bytes數據是由getDataFromFile函數獲取

繼續跟getDataFromFile的邏輯,它會調用getContents,而getContents裡是調用fopen來打開,但奇怪的是hook fopen卻沒有發現它有打開任何.jsc文件

後來發現調用的並非FileUtils::getContents,而是FileUtilsAndroid::getContents

它其中一個分支是調用libandroid.soAAsset_read來讀取.jsc數據,調用AAssetManager_open來打開.jsc文件。

繼續對AAssetManager_open進行深入分析( 在線源碼 ),目的是找到能夠IO重定向的點:

AAssetManager_open裡調用了AssetManager::open函數

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

// frameworks/base/native/android/asset_manager.cpp

AAsset* AAssetManager_open(AAssetManager* amgr, const char* filename, int mode)

{

    Asset::AccessMode amMode;

    switch (mode) {

    case AASSET_MODE_UNKNOWN:

        amMode = Asset::ACCESS_UNKNOWN;

        break;

    case AASSET_MODE_RANDOM:

        amMode = Asset::ACCESS_RANDOM;

        break;

    case AASSET_MODE_STREAMING:

        amMode = Asset::ACCESS_STREAMING;

        break;

    case AASSET_MODE_BUFFER:

        amMode = Asset::ACCESS_BUFFER;

        break;

    default:

        return NULL;

    }

    AssetManager* mgr = static_cast<AssetManager*>(amgr);

    // here

    Asset* asset = mgr->open(filename, amMode);

    if (asset == NULL) {

        return NULL;

    }

    return new AAsset(asset);

}

AssetManager::open調用openNonAssetInPathLocked

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// frameworks/base/libs/androidfw/AssetManager.cpp

Asset* AssetManager::open(const char* fileName, AccessMode mode)

{

    AutoMutex _l(mLock);

    LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager");

    String8 assetName(kAssetsRoot);

    assetName.appendPath(fileName);

    size_t i = mAssetPaths.size();

    while (i > 0) {

        i--;

        ALOGV("Looking for asset '%s' in '%s'\n",

                assetName.string(), mAssetPaths.itemAt(i).path.string());

        // here

        Asset* pAsset = openNonAssetInPathLocked(assetName.string(), mode, mAssetPaths.itemAt(i));

        if (pAsset != NULL) {

            return pAsset != kExcludedAsset ? pAsset : NULL;

        }

    }

    return NULL;

}

AssetManager::openNonAssetInPathLocked先判斷assets是位於.gz還是.zip內,而.apk.zip基本等價,因此理應會走else分支。

1

奇怪的是當我使用frida hook驗證時,能順利hook到`openAssetFromZipLocked`,卻hook不到`getZipFileLocked`,顯然是不合理的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

// frameworks/base/libs/androidfw/AssetManager.cpp

Asset* AssetManager::openNonAssetInPathLocked(const char* fileName, AccessMode mode,

    const asset_path& ap)

{

    Asset* pAsset = NULL;

    if (ap.type == kFileTypeDirectory) {

        String8 path(ap.path);

        path.appendPath(fileName);

        pAsset = openAssetFromFileLocked(path, mode);

        if (pAsset == NULL) {

            /* try again, this time with ".gz" */

            path.append(".gz");

            pAsset = openAssetFromFileLocked(path, mode);

        }

        if (pAsset != NULL) {

            //printf("FOUND NA '%s' on disk\n", fileName);

            pAsset->setAssetSource(path);

        }

    // run this branch

    else {

        String8 path(fileName);

                // here

        ZipFileRO* pZip = getZipFileLocked(ap);

        if (pZip != NULL) {

            ZipEntryRO entry = pZip->findEntryByName(path.string());

            if (entry != NULL) {

                 

                pAsset = openAssetFromZipLocked(pZip, entry, mode, path);

                pZip->releaseEntry(entry);

            }

        }

        if (pAsset != NULL) {

            pAsset->setAssetSource(

                    createZipSourceNameLocked(ZipSet::getPathName(ap.path.string()), String8(""),

                                                String8(fileName)));

        }

    }

    return pAsset;

}

嘗試繼續跟剛剛hook失敗的AssetManager::getZipFileLocked,它調用的是AssetManager::ZipSet::getZip

1

同樣用frida hook `getZip`,這次成功了,猜測是一些優化移除了`getZipFileLocked`而導致hook 失敗。

1

2

3

4

5

6

7

// frameworks/base/libs/androidfw/AssetManager.cpp

ZipFileRO* AssetManager::getZipFileLocked(const asset_path& ap)

{

    ALOGV("getZipFileLocked() in %p\n"this);

    return mZipSet.getZip(ap.path);

}

ZipSet::getZip會調用SharedZip::getZip,後者直接返回mZipFile

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// frameworks/base/libs/androidfw/AssetManager.cpp

ZipFileRO* AssetManager::ZipSet::getZip(const String8& path)

{

    int idx = getIndex(path);

    sp<SharedZip> zip = mZipFile[idx];

    if (zip == NULL) {

        zip = SharedZip::get(path);

        mZipFile.editItemAt(idx) = zip;

    }

    return zip->getZip();

}

ZipFileRO* AssetManager::SharedZip::getZip()

{

    return mZipFile;

}

尋找mZipFile賦值的地方,最終會找到是由ZipFileRO::open(mPath.string())賦值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// frameworks/base/libs/androidfw/AssetManager.cpp

AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen)

    : mPath(path), mZipFile(NULL), mModWhen(modWhen),

      mResourceTableAsset(NULL), mResourceTable(NULL)

{

    if (kIsDebug) {

        ALOGI("Creating SharedZip %p %s\n"this, (const char*)mPath);

    }

    ALOGV("+++ opening zip '%s'\n", mPath.string());

    // here

    mZipFile = ZipFileRO::open(mPath.string());

    if (mZipFile == NULL) {

        ALOGD("failed to open Zip archive '%s'\n", mPath.string());

    }

}

1

從`frameworks/base/libs/androidfw/Android.bp`可知上述代碼的lib文件是`libandroidfw.so`,位於`/system/lib64/`下。將其pull到本地然後用IDA打開,就能根據IDA所示的函數導出名稱/地址對這些函數進行hook。

方式二:從應用的數據目錄裡讀取

無論是方式一還是方式二,.jsc數據都是通過getDataFromFile獲取。而getDataFromFile裡調用了getContents

1

getDataFromFile -> getContents

在方式一中,我一開始看的是FileUtils::getContents,但其實是FileUtilsAndroid::getContents才對。

只有當fullPath[0] == '/'時才會調用FileUtils::getContents,而FileUtils::getContents會調用fopen來打開.jsc

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// https://github.com/cocos2d/cocos2d-x/blob/76903dee64046c7bfdba50790be283484b4be271/cocos/platform/android/CCFileUtils-android.cpp

FileUtils::Status FileUtilsAndroid::getContents(const std::string& filename, ResizableBuffer* buffer) const

{

    static const std::string apkprefix("assets/");

    if (filename.empty())

        return FileUtils::Status::NotExists;

    string fullPath = fullPathForFilename(filename);

    if (fullPath[0] == '/')

            // here

        return FileUtils::getContents(fullPath, buffer);

         

    // 方式一會走這裡....

}

替換思路

正常來說有以下幾種替換腳本的思路:

  1. 找到讀取.jsc文件的地方進行IO重定向。

  2. 直接進行字節替換,即替換xxtea_decypt解密前的.jsc字節數據,或者替換xxtea_decypt解密後的明文.js腳本。

    這裡的替換是指開闢一片新內存,將新的數據放到這片內存,然後替換指針的指向。

  3. 直接替換apk裡的.jsc,然後重打包apk。

  4. 替換js明文,不是像2那樣開闢一片新內存,而是直接修改原本內存的明文js數據。

經測試後發現只有134是可行的,2會導致APP卡死( 原因不明??? )。

思路一實現

從上述可知第一種.jsc讀取方式會先調用ZipFileRO::open(mPath.string())來打開apk,之後再通過AAssetManager_open來獲取.jsc

hook ZipFileRO::open看看傳入的參數是什麼。

function hook_ZipFile_open(flag) {let ZipFile_open = Module.getExportByName("libandroidfw.so", "_ZN7android9ZipFileRO4openEPKc"); console.log("ZipFile_open: ", ZipFile_open)return Interceptor.attach(ZipFile_open,{onEnter: function (args) {console.log("arg0: ", args[0].readCString());},onLeave: function (retval) {}});
}

可以看到其中一條是當前APK的路徑,顯然assets也是從這裡取的,因此這裡是一個可以嘗試重定向點,先需構造一個fake.apk push 到/data/app/XXX/下,然後hook IO重定向到fake.apk實現替換。

對我自己編譯的Demo而言,無論是以apktool解包&重打包的方式,還是直接解壓縮&重壓縮&手動命名的方式來構建fake.apk都是可行的,但要記得賦予fake.apk最低644的權限。

以下是我使用上述方法在我的Demo中實踐的效果,成功修改中心的字符串。

但感覺這種方式的實用性較低( 什至不如直接重打包… )

思路二嘗試(失敗)

連這樣僅替換指針指向都會導致APP卡死??

function hook_xxtea_decrypt() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "xxtea_decrypt"), {onEnter(args) {let jsc_data = args[0];let size = args[1].toInt32();let key = args[2].readCString();let key_len = args[3].toInt32();this.arg4 = args[4];let target_list = [0x15, 0x43, 0x73];let flag = true;for (let i = 0; i < target_list.length; i++) {if (target_list[i] != Memory.readU8(jsc_data.add(i))) {flag = false;}}this.flag = flag;if (flag) {let new_size = size;let newAddress = Memory.alloc(new_size);Memory.protect(newAddress, new_size, "rwx")Memory.protect(args[0], new_size, "rwx")Memory.writeByteArray(newAddress, jsc_data.readByteArray(new_size))args[0] = newAddress;}},onLeave(retval) {}})}

思路四實現

參考這位大佬的文章可知cocos2djs內置的v8引擎最終通過evalString來執行.jsc解密後的js代碼。

在正式替換前,最好先通過hook evalString的方式保存一份目標js( 因為遊戲的熱更新策略等原因,可能導致evalString執行的js代碼與你從apk裡手動解密.jsc得到的js腳本有所不同 )。

function saveJscode(jscode, path) {var fopenPtr = Module.findExportByName("libc.so", "fopen");var fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);var fclosePtr = Module.findExportByName("libc.so", "fclose");var fclose = new NativeFunction(fclosePtr, 'int', ['pointer']);var fseekPtr = Module.findExportByName("libc.so", "fseek");var fseek = new NativeFunction(fseekPtr, 'int', ['pointer', 'int', 'int']);var ftellPtr = Module.findExportByName("libc.so", "ftell");var ftell = new NativeFunction(ftellPtr, 'int', ['pointer']);var freadPtr = Module.findExportByName("libc.so", "fread");var fread = new NativeFunction(freadPtr, 'int', ['pointer', 'int', 'int', 'pointer']);var fwritePtr = Module.findExportByName("libc.so", "fwrite");var fwrite = new NativeFunction(fwritePtr, 'int', ['pointer', 'int', 'int', 'pointer']);let newPath = Memory.allocUtf8String(path);let openMode = Memory.allocUtf8String('w');let str = Memory.allocUtf8String(jscode);let file = fopen(newPath, openMode);if (file != null) {fwrite(str, jscode.length, 1, file)fclose(file);}return null;
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {saveJscode(jscode.readCString(), "/data/data/XXXXXXX/test.js");}}})
}

利用Memory.scan來找到修改的位置

function findReplaceAddr(startAddr, size, pattern) {Memory.scan(startAddr, size, pattern, {onMatch(address, size) {console.log("target offset: ", ptr(address - startAddr))return 'stop';},onComplete() {console.log('Memory.scan() complete');}});
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {let pattern = "76 61 72 20 65 20 3D 20 64 2E 50 6C 61 79 65 72 41 74 74 72 69 62 75 74 65 43 6F 6E 66 69 67 2E 67 65 74 44 72 65 61 6D 48 6C 70 65 49 74 65 6D 44 72 6F 70 28 29 2C";findReplaceAddr(jscode, size, pattern);}}})
}

最後以Memory.writeU8來逐字節修改,不用Memory.writeUtf8String的原因是它默認會在最終添加'\0'而導致報錯。

function replaceEvalString(jscode, offset, replaceStr) {for (let i = 0; i < replaceStr.length; i++) {Memory.writeU8(jscode.add(offset + i), replaceStr.charCodeAt(i))}
}// 例:
function cheatAutoChopTree(jscode) {let replaceStr = 'true || "                                 "';replaceEvalString(jscode, 0x3861f6, replaceStr)
}

某砍樹手遊實踐

以某款砍樹遊戲來進行簡單的實踐。

遊戲有自動砍樹的功能,但需要符合一定條件

如何找到對應的邏輯在哪個.jsc中?直接搜字符串就可以。

利用上述替換思路4來修改對應的js判斷邏輯,最終效果:

結語

思路4那種替換手段有大小限制,不能隨意地修改,暫時還未找到能隨意修改的手段,有知道的大佬還請不嗇賜教,有任何想法也歡迎交流^^

後記

在評論區的一位大佬指點下,終於是找到一種更優的替換方案,相比起思路4來說要方便太多了。
最開始時我其實也嘗試過這種直接的js明文替換,但APP會卡死/閃退,現在才發現是frida的api所致,那時在開辟內存空間時使用了Memory.alloc、Memory.allocUtf8String,改成使用libc.so的malloc就不會閃退了,具體為什麼會這樣我也不清楚,看看以後有沒有機會研究下frida的源碼吧^^

相关文章:

淺談Cocos2djs逆向

前言 簡單聊一下cocos2djs手遊的逆向&#xff0c;有任何相關想法歡迎和我討論^^ 一些概念 列出一些個人認為比較有用的概念&#xff1a; Cocos遊戲的兩大開發工具分別是CocosCreator和CocosStudio&#xff0c;區別是前者是cocos2djs專用的開發工具&#xff0c;後者則是coco…...

选择器(结构伪类选择器,伪元素选择器),PxCook软件,盒子模型

结构为类选择器 伪元素选择器 PxCook 盒子模型 (内外边距&#xff0c;边框&#xff09; 内外边距合并&#xff0c;塌陷问题 元素溢出 圆角 阴影: 模糊半径&#xff1a;越大越模糊&#xff0c;也就是越柔和 案例一&#xff1a;产品卡片 <!DOCTYPE html> <html lang&q…...

CentOS 7系统 OpenSSH和OpenSSL版本升级指南

文章目录 CentOS 7系统 OpenSSH和OpenSSL版本升级指南环境说明当前系统版本当前组件版本 现存安全漏洞升级目标版本升级准备工作OpenSSL升级步骤1. 下载和解压2. 编译安装3. 配置环境 OpenSSH升级步骤1. 下载和解压2. 编译安装3. 创建systemd服务配置4. 更新SSH配置文件5. 设置…...

使用 Comparable 和 Comparator 接口对集合排序

使用 Comparable 和 Comparator 接口对集合排序&#xff1a; 1. 使用 Comparable 接口&#xff1a; 当你希望一个类的对象能够按照某种自然顺序进行排序时&#xff0c;可以实现 Comparable 接口 并重写 compareTo() 方法。 实现步骤&#xff1a; 1.1 实现 Comparable<T&g…...

最新常见的图数据库对比,选型,架构,性能对比

图数据库排名 地址&#xff1a;https://db-engines.com/en/ranking/graphdbms 知识图谱查询语言 SPARQL、Cypher、Gremlin、PGQL 和 G-CORE 语法 / 语义 / 特性 SPARQL Cypher Gremlin PGQL G-CORE 图模式匹配查询 语法 CGP CGP CGP(无可选)1 CGP CGP 语义 子…...

混合合并两个pdf文件

混合两个pdf 1、在线免费交替和混合奇数和偶数PDF页面2、有什么软件把两个 PDF 交叉合并&#xff1f;3、pdfsam本地合并 如何Google翻译的原文和译文合并&#xff0c;&#xff08;沉浸式翻译效果相对较好&#xff09; 1、在线免费交替和混合奇数和偶数PDF页面 https://deftpd…...

OpenCV-Python实战(9)——滤波降噪

一、均值滤波器 cv2.blur() img cv2.blur(src*,ksize*,anchor*,borderType*)img&#xff1a;目标图像。 src&#xff1a;原始图像。 ksize&#xff1a;滤波核大小&#xff0c;&#xff08;width&#xff0c;height&#xff09;。 anchor&#xff1a;滤波核锚点&#xff0c…...

uniapp——微信小程序读取bin文件,解析文件的数据内容(三)

微信小程序读取bin文件内容 读取用户选择bin文件&#xff0c;并解析数据内容&#xff0c;分包发送给蓝牙设备&#xff1b; 文章目录 微信小程序读取bin文件内容读取文件读取内容返回格式 API文档&#xff1a; getFileSystemManager 关于App端读取bin文件&#xff0c;请查看&…...

Python 中常用的算法

1. 排序算法 用于将数据按特定顺序排列。 冒泡排序&#xff08;Bubble Sort&#xff09;选择排序&#xff08;Selection Sort&#xff09;插入排序&#xff08;Insertion Sort&#xff09;快速排序&#xff08;Quick Sort&#xff09;归并排序&#xff08;Merge Sort&#xf…...

xadmin后台首页增加一个导入数据按钮

xadmin后台首页增加一个导入数据按钮 效果 流程 1、在添加小组件中添加一个html页面 2、写入html代码 3、在urls.py添加导入数据路由 4、在views.py中添加响应函数html代码 <!DOCTYPE html> <html lang...

Kubernetes: NetworkPolicy 的实践应用

一、Network Policy 是什么,在云原生领域有和作用 Network Policy 是 Kubernetes 官方提出来的一种网络策略的规范&#xff0c;用户通过编写符合对应规范的规则来控制 k8s 集群内 L3 和 L4 层的网络流量。 NetworkPolicy 主要的功能就是实现在云原生领域的容器网络管控它给用…...

计算机体系结构期末复习3:GPU架构及控制流问题

目录 一、GPU设计思路 1.简化流水线、增加核数 2.单指令多线程&#xff08;SIMT&#xff09; 3.同时驻留大量线程 4.总思路&#xff1a;多线程单指令多线程 二、GPU的控制流问题 1.什么是控制流问题 2.怎么应对分支分歧 一、GPU设计思路 1.简化流水线、增加核数 2.单指…...

excel怎么删除右边无限列(亲测有效)

excel怎么删除右边无限列&#xff08;亲测有效&#xff09; 网上很多只用第1步的&#xff0c;删除了根本没用&#xff0c;还是存在&#xff0c;但是隐藏后取消隐藏却是可以的。 找到右边要删除的列的第一个空白列&#xff0c;选中整个列按“ctrlshift>(向右的小箭头)”&am…...

ChatGPT-4助力学术论文提升文章逻辑、优化句式与扩充内容等应用技巧解析。附提示词案例

目录 1.扩写&#xff08;expansion/paraphrasing&#xff09; 2.优化&#xff08;optimization&#xff09; 3.缩写&#xff08;optimization&#xff09; 4.提取关键词&#xff08;keyword extraction&#xff09; 5.短语转换&#xff08;phrase transformation&#xff…...

C++和OpenGL实现3D游戏编程【连载19】——着色器光照初步(平行光和光照贴图)(附源码)

1、本节要实现的内容 我们在前期的教程中,讨论了在即时渲染模式下的光照内容。但在我们后期使用着色器的核心模式下,会经常在着色器中使光照,我们这里就讨论一下着色器光照效果,以及光照贴图效果,同时这里知识会为后期的更多光照效果做一些铺垫。本节我们首先讨论冯氏光照…...

html+css网页制作 美食 美食网5个页面

htmlcss网页制作 美食 美食网5个页面 网页作品代码简单&#xff0c;可使用任意HTML辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#xff0…...

Mac 12.1安装tiger-vnc问题-routines:CRYPTO_internal:bad key length

背景&#xff1a;因为某些原因需要从本地mac连接远程linxu桌面查看一些内容&#xff0c;必须使用桌面查看&#xff0c;所以ssh无法满足&#xff0c;所以决定安装vnc客户端。 问题&#xff1a; 在mac上通过 brew install tiger-vnc命令安装, 但是报错如下&#xff1a; > D…...

遥感图像车辆检测-目标检测数据集

遥感图像车辆检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接: https://pan.baidu.com/s/1XVlRTVWpXZFi6ZL_Xcs7Rg?pwdaa6g 提取码: aa6g 数据集信息介绍&#xff1a; 共有 1035 张图像和一一对应的标注文件 标注文件格式提供了…...

51c自动驾驶~合集43

我自己的原文哦~ https://blog.51cto.com/whaosoft/12930230 #ChatDyn 上交大最新ChatDyn&#xff1a;一句话操纵三维动态 理解和生成真实的三维虚拟世界是空间智能的核心。所生成的三维虚拟世界能够为自动驾驶、具身智能等AI系统提供高质量闭环仿真训练场&#xff0c;高效…...

随机变量是一个函数-如何理解

文章目录 一. 随机变量二. 随机变量是一个函数-栗子(一对一)1. 掷骰子的随机变量2. 掷骰子的随机变量&#xff08;求点数平方&#xff09;3. 抛硬币的随机变量4. 学生考试得分的随机变量 三. 随机变量是一个函数-理解(多对一) 一. 随机变量 随机变量就是定义在样本空间上的函数…...

云计算在医疗行业的应用

云计算在医疗行业的应用广泛而深入&#xff0c;为医疗服务带来了前所未有的变革。以下是对云计算在医疗行业应用的详细解析&#xff1a; ### 一、医疗数据共享与整合 云计算平台具有强大的数据存储和处理能力&#xff0c;使得医疗数据共享与整合成为可能。通过云计算平台&…...

Cursor提示词

你是一位经验丰富的项目经理&#xff0c;对于用户每一次提出的问题&#xff0c;都不急于编写代码&#xff0c;更多是通过深思熱虑、结构化的推理以产生高质量的回答&#xff0c;探索更多的可能方案&#xff0c;并从中寻找最佳方案。 约束 代码必须可以通过编译回答尽量使用中…...

C++ 设计模式:单例模式(Singleton Pattern)

链接&#xff1a;C 设计模式 链接&#xff1a;C 设计模式 - 享元模式 单例模式&#xff08;Singleton Pattern&#xff09;是创建型设计模式&#xff0c;它确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问这个实例。单例模式在需要全局共享资源或控制实例数量的…...

C++中生成0到180之间的随机数

在C中生成0到180之间的随机数&#xff0c;可以使用标准库中的和头文件。提供了rand()函数来生成随机数&#xff0c;而提供了time()函数来设置随机数生成的种子。这样每次运行程序时&#xff0c;生成的随机数序列都会不同。 以下是一个简单的示例代码&#xff0c;展示了如何生成…...

[.闲于修.]Autosar_UDS_笔记篇_ISO14229-1

前言&#xff1a;闲来无事&#xff0c;摸鱼无趣&#xff0c;准备细读一下14229&#xff0c;记录一些容易被忽略掉的内容 正文&#xff1a;&#xff08;以下数字代表章节&#xff09; 7、Application layer protocol 7.5.6 多个并发请求消息 常见的服务器实现在服务器中只有一…...

如何利用云计算进行灾难恢复?

云计算环境下的灾难恢复实践指南 天有不测风云&#xff0c;企业的IT系统也一样&#xff0c;我见过太多因为没有做好灾备而吃大亏的案例。今天就和大家聊聊如何用云计算来做灾难恢复。 一个惊心动魄的真实案例&#xff1a;某电商平台的主数据中心因为市政施工不小心挖断了光纤…...

Redis - 1 ( 7000 字 Redis 入门级教程 )

一&#xff1a; Redis 1.1 Redis 简介 Redis 是一种基于键值对&#xff08;key-value&#xff09;的 NoSQL 数据库&#xff0c;与其他键值对数据库不同&#xff0c;Redis 的值可以是多种数据结构和算法的组合&#xff0c;如字符串&#xff08;string&#xff09;、哈希&#…...

[羊城杯 2024]不一样的数据库_2

题目描述&#xff1a; 压缩包6 (1).zip需要解压密码&#xff1a; 尝试用ARCHPR工具爆破一下&#xff1a; &#xff08;字典可自行在github上查找&#xff09; 解压密码为&#xff1a;753951 解压得到13.png和Kee.kdbx文件&#xff1a; 二维码图片看上去只缺了正常的三个角&…...

租赁系统的数字化转型与高效管理新模式分析

内容概要 在当今瞬息万变的市场环境中&#xff0c;租赁系统的数字化转型显得尤为重要。信息技术的迅猛发展不仅改变了我们的生活方式&#xff0c;也迫使企业重新审视其运营模式。为了顺应这一潮流&#xff0c;租赁系统亟需通过高效管理新模式来提升运营效率&#xff0c;从而保…...

Selenium+Java(21):Jenkins发送邮件报错Not sent to the following valid addresses解决方案

问题现象 小月妹妹近期在做RobotFrameWork自动化测试,并且使用Jenkins发送测试邮件的时候,发现报错Not sent to the following valid addresses,明明各个配置项看起来都没有问题,但是一到邮件发送环节,就是发送不出去,而且还不提示太多有用的信息,急的妹妹脸都红了,于…...

【每日学点鸿蒙知识】文字识别、快捷登录、输入法按钮监听、IDE自动换行、资产访问等

【每日学点鸿蒙知识】24.09.07 1、API使用&#xff1a; hms.ai.ocr.textRecognition&#xff08;文字识别&#xff09;&#xff1f; 需要接入API文档https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/core-vision-text-recognition-api-V5中的文字识别…...

LabVIEW化工实验室设备故障实时监测

化工实验室中&#xff0c;各类设备的运行状态直接影响实验的精度与安全性。特别是重要分析仪器的突发故障&#xff0c;可能导致实验中断或数据失效。为了实现设备运行状态的实时监控与故障快速响应&#xff0c;本文提出了一套基于LabVIEW的解决方案&#xff0c;通过多参数采集、…...

小程序学习05——uniapp路由和菜单配置

目录 一、路由 二、如何管理页面及路由&#xff1f; 三、pages.json 页面路由 四、 tabBar 一、路由 路由&#xff1a;在前端&#xff0c;往往指代用不同地址请求不同页面&#xff0c;决定了用户如何在应用的不同页面之间导航。 菜单&#xff1a;对于每个路径&#xff08;…...

漏洞分析 | Apache Struts文件上传漏洞(CVE-2024-53677)

漏洞概述 Apache Struts是美国阿帕奇&#xff08;Apache&#xff09;基金会的一个开源项目&#xff0c;是一套用于创建企业级Java Web应用的开源MVC框架。 近期&#xff0c;网宿安全演武实验室监测到Apache Struts在特定条件下&#xff0c;存在文件上传漏洞&#xff08;网宿评…...

【VBA】EXCEL - VBA 遍历工作表的 5 种方法,以及注意事项

目录 1. 遍历单列数据并赋值 2. 遍历整个工作表的数据区域并赋值 3. 遍历指定范围的数据并赋值 4. 遍历多列数据并赋值 5. 遍历所有工作表中的数据并赋值 注意事项&#xff1a; 1. 遍历单列数据并赋值 Sub UpdateColumnData()Dim ws As WorksheetSet ws ThisWorkbook.S…...

CSS浮动

浮动 可以让块级元素待在一行&#xff0c;紧挨着&#xff0c;没有空格 float&#xff1a;left 浮动的元素会脱离正常的文档系统&#xff0c;像浮云一样飘起来浮动元素后面的正常元素会自动补位浮动元素会被父元素的宽高所束缚&#xff0c;所以不算完全的脱离文档流当浮动元素…...

gitlab 还原合并请求

事情是这样的&#xff1a; 菜鸡从 test 分支切了个名为 pref-art 的分支出来&#xff0c;发布后一机灵&#xff0c;发现错了&#xff0c;于是在本地用 git branch -d pref-art 将该分支删掉了。之后切到了 prod 分支&#xff0c;再切出了一个相同名称的 pref-art 分支出来&…...

【GPT】Coze使用开放平台接口-【8】创建应用

coze 可以用来创建简单的应用啦&#xff0c;这样测试起来会比原本的 Agent 更加方便&#xff0c;我们来看看如何创建一个“语音Real不Real”的应用。这个应用就是来检测语音是否是伪造的&#xff0c;克隆或者是合成的。先看下原本 Agent 的样子&#xff1a; 深度伪造语音检测&a…...

海外盲盒系统开发,助力企业全球化发展

近几年来&#xff0c;在海外市场中&#xff0c;盲盒已经成为了一种新的时尚单品&#xff0c;深受东南亚等海外消费者的喜爱。同时&#xff0c;泡泡玛特在海外的成功也为国内企业提供了发展机遇&#xff0c;盲盒出海具有广阔的发展前景&#xff01;‌ 随着信息技术的快速发展&a…...

pytorch 梯度判断函数介绍

PyTorch 提供了一些函数用于判断当前的梯度计算状态以及张量是否需要梯度。这些函数帮助开发者在训练、推理和调试过程中了解和控制梯度计算行为。 PyTorch 梯度判断函数 1. torch.is_grad_enabled() 功能: 判断当前是否启用了全局的梯度计算状态。返回值: 布尔值,True 表…...

每日一题 367. 有效的完全平方数

367. 有效的完全平方数 低效率法 class Solution { public:bool isPerfectSquare(int num) {if(num 1){return true;}long num1 num;for(int i1;i< num/2;i){if((long)(i)*i num){return true;}}return false;} };二分法 class Solution { public:bool isPerfectSquar…...

图像转换 VM与其他格式互转

目录 前言 图像转换 1.相机取流转VM对应类型图像格式 1.1 相机采图转流程输入和Group输入(ImageBaseData_V2) 1.2 相机采图转图像源SDK输入(ImageBaseData) 1.3 相机采图转模块输入(InputImageData) 1.4 相机采图转算子输入(CmvdImage) 2.Bitmap取图与VM对应图像格式互…...

streamlit、shiny、gradio、fastapi四个web APP平台体验

streamlit、shiny、gradio、fastapi四个web APP平台体验 经常被问的问题就是&#xff1a;web APP平台哪个好&#xff1f;该用哪个&#xff1f;刚开始只有用streamlit和shiny&#xff0c;最近体验了一下gradio和fastapi&#xff0c;今天根据自己的体会尝试着回答一下。 使用R语…...

BootstrapTable处理表格

需求背景 历史项目使用 BootstrapTable 作为前端组件 应客户需要调整&#xff1a; 冻结前四列对于大文本文字显示部分内容&#xff0c;鼠标悬浮显示完整内容 冻结列 1、引入相关CSS,JS CSS <link rel"stylesheet" href"/css/bootstrap.min.css"> …...

家政预约小程序04活动管理表结构设计

目录 1 创建活动表2 创建活动规则表3 创建活动参与记录表总结 为了满足我们日常的营销&#xff0c;我们通常需要搞一些活动&#xff0c;比如满减、折扣、团购等。启动活动后&#xff0c;会在首页进行显示&#xff0c;当用户访问小程序的时候&#xff0c;就可以参与活动&#xf…...

WPF使用OpenCvSharp4

WPF使用OpenCvSharp4 创建项目安装OpenCvSharp4 创建项目 安装OpenCvSharp4 在解决方案资源管理器中&#xff0c;右键单击项目名称&#xff0c;选择“管理 NuGet 包”。搜索并安装以下包&#xff1a; OpenCvSharp4OpenCvSharp4.ExtensionsOpenCvSharp4.runtime.winSystem.Man…...

STM32-笔记23-超声波传感器HC-SR04

一、简介 HC-SR04 工作参数&#xff1a; • 探测距离&#xff1a;2~600cm • 探测精度&#xff1a;0.1cm1% • 感应角度&#xff1a;<15 • 输出方式&#xff1a;GPIO • 工作电压&#xff1a;DC 3~5.5V • 工作电流&#xff1a;5.3mA • 工作温度&#xff1a;-40~85℃ 怎么…...

4G报警器WT2003H-16S低功耗语音芯片方案开发-实时音频上传

一、引言 在当今社会&#xff0c;安全问题始终是人们关注的重中之重。无论是家庭、企业还是公共场所&#xff0c;都需要一套可靠的安全防护系统来保障人员和财产的安全。随着科技的飞速发展&#xff0c;4G 报警器应运而生&#xff0c;为安全防范领域带来了全新的解决方案。…...

机器学习中的欠拟合

当模型不能够准确地表达输入与输出的关系时&#xff0c;就是欠拟合。它在训练集和未见过的数据都会产生高误差率。过度拟合则在训练集表现出低误差率&#xff0c;只有对未见过的数据表现出高误差率。 当模型太过于简单时&#xff0c;它需要更多的训练时间、更多的输入特征、更…...

数据结构之栈和队列

栈的定义&#xff1a; 我们要记住这8个字&#xff0c;先进后出&#xff0c;后进先出 我们对于栈的操作只有两个&#xff0c;进栈和出栈 栈的顺序结构初始化&#xff1a;&#xff08;和顺序表差不多&#xff09; 代码实现&#xff1a; 栈的顺序结构进栈&#xff1a; 代码实现…...