Frida + FART 聯手:解鎖更強大的 Android 脫殼新姿勢

版權歸作者所有,如有轉發,請注明文章出處:https://cyrus-studio.github.io/blog/

Frida + FART 聯手能帶來什么提升?

  1. 增強 FART 的脫殼能力:解決對抗 FART 的殼、動態加載的 dex 的 dump 和修復;

  2. 控制 FART 主動調用的范圍,讓 FART 更精細化,比如按需進行類甚至是函數的修復。

非雙親委派關系下動態加載的 dex 脫殼問題

由于動態加載的 dex 沒有取改變 android 中 ClassLoader 雙親委派關系,所以動態加載的 dex 沒有自動脫殼。

相關文章:

  • 深入理解 Android ClassLoader 與雙親委派機制

  • 深入剖析 Android 加殼應用運行流程與生命周期劫持方案

在 android studio 中創建一個 plugin module 其中包含一個 FartTest 類源碼如下:

package com.cyrus.example.pluginimport android.util.Logclass FartTest {fun test(): String {Log.d("FartTest", "call FartTest test().")return "String from FartTest."}}

把 plugin-debug.apk push 到 files 目錄下

adb push "D:\Projects\AndroidExample\plugin\build\intermediates\apk\debug\plugin-debug.apk" /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk

ls 一下 files 目錄是否存在 plugin-debug.apk

adb shell ls /sdcard/Android/data/com.cyrus.example/files

在 app 動態加載 files 目錄下的 plugin-debug.apk 并調用 FartTest 的 test 方法

val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apk
val classLoader = DexClassLoader(apkPath,null,this@FartActivity.packageResourcePath,classLoader // parent 設為當前 context 的類加載器
)// classLoader 加載 com.cyrus.example.plugin.FartTest 類并通過反射調用 test 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("test")
method.isAccessible = true
val result = method.invoke(instance) as? Stringlog("動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}")mClassLoader = classLoader

脫殼完成,但是沒有對 plugin-debug.apk 中的目標類 FartTest 發起主動調用

word/media/image1.png

這時候 frida 就派上用場了,因為 frida 本身具有枚舉所有 ClassLoader 的能力。

Frida + FART 脫殼動態加載的 dex

枚舉出所有 ClassLoader 后,再結合 FART 的 api 就可以實現動態加載 dex 的脫殼。

function invokeAllClassloaders() {Java.perform(function () {try {// 獲取 ActivityThread 類var ActivityThread = Java.use("android.app.ActivityThread");Java.enumerateClassLoaders({onMatch: function (loader) {try {// 過濾掉 BootClassLoaderif (loader.toString().includes("BootClassLoader")) {console.log("[-] 跳過 BootClassLoader");return;}// 調用 fartWithClassLoaderconsole.log("[*] 調用 fartwithClassloader -> " + loader);ActivityThread.fartwithClassloader(loader);} catch (e) {console.error("[-] 調用失敗: " + e);}},onComplete: function () {console.log("[*] 枚舉并調用完畢");}});} catch (err) {console.error("[-] 腳本執行異常: " + err);}});
}setImmediate(invokeAllClassloaders)

把 log 導出到 txt

adb logcat -v time > logcat.txt

打開 app 后執行腳本

frida -H 127.0.0.1:1234 -F -l fart_invoke_all_classloaders.js

從輸出日志可以看到已經成功對 FartTest 類中方法發起主動調用

word/media/image2.png

局部變量的 ClassLoader 枚舉不出來

但還有一個問題呢:局部變量的 ClassLoader 枚舉不出來。

因為:

  • enumerateClassLoaders() 只枚舉當前 VM 中可訪問的、被 GC Root 持有的 ClassLoader;

  • 如果 DexClassLoader 作為臨時變量創建后,沒有被保存,就會被 GC 回收或無法遍歷到。

比如,下面的 Kotlin 代碼中,當 DexClassLoader 為局部變量時就沒有枚舉出這個 DexClassLoader 。

/*** 局部變量的 ClassLoader*/
fun onLocalClassLoaderClicked(log: (String) -> Unit) {val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apkval classLoader = DexClassLoader(apkPath,null,this@FartActivity.packageResourcePath,classLoader // parent 設為當前 context 的類加載器)// classLoader 加載 com.cyrus.example.plugin.FartTest 類并通過反射調用 test 方法val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")val constructor = pluginClass.getDeclaredConstructor()constructor.isAccessible = trueval instance = constructor.newInstance()val method = pluginClass.getDeclaredMethod("test")method.isAccessible = trueval result = method.invoke(instance) as? Stringlog("局部變量的 ClassLoader 動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}\n\n")
}

在構造 ClassLoader 時脫殼

所以,為了解決這種情況,我們 hook DexClassLoader 構造函數去調用 FART 脫殼 就可以解決了。

function fartOnDexclassloader() {Java.perform(function () {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String',     // dexPath'java.lang.String',     // optimizedDirectory'java.lang.String',     // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log("    |- dexPath: " + dexPath);console.log("    |- optimizedDirectory: " + optimizedDirectory);console.log("    |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};});
}setImmediate(fartOnDexclassloader)

啟動 app 并執行腳本

frida -H 127.0.0.1:1234 -l fart_on_dexclassloader.js -f com.cyrus.example

frida 日志如下:

Spawned `com.cyrus.example`. Use %resume to let the main thread start executing!
[Remote::com.cyrus.example]-> %resume
[Remote::com.cyrus.example]-> [+] DexClassLoader created:|- dexPath: /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk|- optimizedDirectory: null|- libPath: /data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk
[*] Calling fartWithClassLoader...
[+] fartWithClassLoader finished.

可以看到成功 hook 到 局部變量的 DexClassLoader 構造函數

從 logcat 可以看到正在對 ClassLoader 中的類方法發起主動調用

word/media/image3.png

等調用完成,進入 fart 目錄下可以看到脫殼下來的文件

wayne:/sdcard/Android/data/com.cyrus.example/fart # ls
12968_class_list.txt            17104392_ins_7079.bin        400440_class_list_execute.txt 54120_dex_file.dex
12968_class_list_execute.txt    17268924_class_list.txt      400440_dex_file_execute.dex   54120_ins_7079.bin
12968_dex_file.dex              17268924_dex_file.dex        4461704_class_list.txt        66552_class_list_execute.txt
12968_dex_file_execute.dex      17268924_ins_7079.bin        4461704_dex_file.dex          66552_dex_file_execute.dex
12968_ins_7079.bin              20996_class_list_execute.txt 4461704_ins_7079.bin          9085048_class_list_execute.txt
16800_class_list_execute.txt    20996_dex_file_execute.dex   536008_class_list.txt         9085048_dex_file_execute.dex
16800_dex_file_execute.dex      21024_class_list_execute.txt 536008_class_list_execute.txt 9248236_class_list.txt
17104392_class_list.txt         21024_dex_file_execute.dex   536008_dex_file.dex           9248236_class_list_execute.txt
17104392_class_list_execute.txt 33196_class_list.txt         536008_dex_file_execute.dex   9248236_dex_file.dex
17104392_dex_file.dex           33196_dex_file.dex           536008_ins_7079.bin           9248236_dex_file_execute.dex
17104392_dex_file_execute.dex   33196_ins_7079.bin           54120_class_list.txt          9248236_ins_7079.bin

控制 FART 主動調用的范圍

FART 中添加的 api 天生為脫殼而生,比如 fartwithClassLoader,loadClassAndInvoke,dumpArtMethod 等等這些接口都可以由 Frida 進行主動調用來控制脫殼精細度。

1. 過濾某些主動調用

hook loadClassAndInvoke 過濾掉某些 class 的主動調用,加快脫殼進程。

比如:過濾掉 androidx.* 、org.jetbrains.* 、kotlinx.* 、org.intellij.* 相關的主動調用

// 前綴過濾邏輯
function shouldSkipClass(name) {return name.startsWith("androidx.") ||name.startsWith("android.") ||name.startsWith("com.google.android.") ||name.startsWith("org.jetbrains.") ||name.startsWith("kotlinx.") ||name.startsWith("kotlin.") ||name.startsWith("org.intellij.");
}function hookLoadClassAndInvoke() {const ActivityThread = Java.use('android.app.ActivityThread');if (ActivityThread.loadClassAndInvoke) {ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {if (shouldSkipClass(className)) {console.log('[skip] loadClassAndInvoke: ' + className);return; // 不調用原函數}console.log('[load] loadClassAndInvoke: ' + className);return this.loadClassAndInvoke(classloader, className, method); // 正常調用};} else {console.log('[-] ActivityThread.loadClassAndInvoke not found');}
}function fartOnDexclassloader() {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String',     // dexPath'java.lang.String',     // optimizedDirectory'java.lang.String',     // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log("    |- dexPath: " + dexPath);console.log("    |- optimizedDirectory: " + optimizedDirectory);console.log("    |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};
}setImmediate(function () {Java.perform(function () {hookLoadClassAndInvoke()fartOnDexclassloader()})
})

執行腳本并輸出日志到 log.txt

frida -H 127.0.0.1:1234 -l fart_loadClassAndInvoke_filter.js -f com.cyrus.example -o log.txt

輸出日志如下:

word/media/image4.png

2. fart thread 調用

由于每個 app 啟動都會自動調用 fartthread,有點影響手機性能。

先去掉 ActivityThread.java 中 fartthread 調用

word/media/image5.png
路徑:frameworks/base/core/java/android/app/ActivityThread.java

通過 frida 調用 fartthread:

function fartThread() {Java.perform(function () {const ActivityThread = Java.use('android.app.ActivityThread')ActivityThread.fartthread()})
}setImmediate(fartThread)

執行腳本針對當前前臺應用啟動 fart thread 開始脫殼

frida -H 127.0.0.1:1234 -F -l fart_thread.js

執行效果如下:

word/media/image6.png

3. 對某個類發起主動調用

如果我們只想單獨對某個類發起主動調用。

通過反射拿到 dumpMethodCode

function findDumpMethodCodeMethod(){let dumpMethodCodeMethod = null;// 反射獲取 dumpMethodCode 方法try {const DexFile = Java.use("dalvik.system.DexFile");const dexFileClazz = DexFile.class;const declaredMethods = dexFileClazz.getDeclaredMethods();for (let i = 0; i < declaredMethods.length; i++) {const m = declaredMethods[i];if (m.getName().toString() === "dumpMethodCode") {m.setAccessible(true);dumpMethodCodeMethod = m;break;}}if (!dumpMethodCodeMethod) {console.log("[-] dumpMethodCode not found in DexFile");return;}console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());} catch (err) {console.log("[-] Exception: " + err);}return dumpMethodCodeMethod
}

調用 LoadClassAndInvoke 對指定類發起主動調用

function invokeClass(targetClassName, dumpMethodCodeMethod) {let foundLoader = findClassLoader(targetClassName)const ActivityThread = Java.use("android.app.ActivityThread");// 調用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)if (ActivityThread.loadClassAndInvoke) {console.log('[load] loadClassAndInvoke: ' + targetClassName);ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);} else {console.log("[-] ActivityThread.loadClassAndInvoke not found");}
}

完整源碼如下:

function findClassLoader(targetClassName) {let foundLoader = null;try {Java.enumerateClassLoaders({onMatch: function (loader) {try {const clazz = loader.loadClass(targetClassName);if (clazz) {console.log("[+] Found class in loader: " + loader.toString());foundLoader = loader;throw "found"; // 快速退出枚舉}} catch (e) {// Ignore: class not found in this loader}},onComplete: function () {}});} catch (e) {if (e !== "found") {console.log("[-] ClassLoader enumeration error: " + e);}}if (!foundLoader) {console.log("[-] Could not find class: " + targetClassName);}return foundLoader
}function findDumpMethodCodeMethod(){let dumpMethodCodeMethod = null;// 反射獲取 dumpMethodCode 方法try {const DexFile = Java.use("dalvik.system.DexFile");const dexFileClazz = DexFile.class;const declaredMethods = dexFileClazz.getDeclaredMethods();for (let i = 0; i < declaredMethods.length; i++) {const m = declaredMethods[i];if (m.getName().toString() === "dumpMethodCode") {m.setAccessible(true);dumpMethodCodeMethod = m;break;}}if (!dumpMethodCodeMethod) {console.log("[-] dumpMethodCode not found in DexFile");return;}console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());} catch (err) {console.log("[-] Exception: " + err);}return dumpMethodCodeMethod
}function invokeClass(targetClassName, dumpMethodCodeMethod) {let foundLoader = findClassLoader(targetClassName)const ActivityThread = Java.use("android.app.ActivityThread");// 調用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)if (ActivityThread.loadClassAndInvoke) {console.log('[load] loadClassAndInvoke: ' + targetClassName);ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);} else {console.log("[-] ActivityThread.loadClassAndInvoke not found");}
}setImmediate(function () {Java.perform(function () {let dumpMethodCodeMethod = findDumpMethodCodeMethod()// TODO: 替換為你的目標類invokeClass("com.cyrus.example.plugin.FartTest", dumpMethodCodeMethod)})
})

執行腳本,附近到當前前臺應用

frida -H 127.0.0.1:1234 -F -l fart_invoke_class.js

輸入如下:

[+] dumpMethodCode Method: private static native void dalvik.system.DexFile.dumpMethodCode(java.lang.Object)
[+] Found class in loader: dalvik.system.DexClassLoader[DexPathList[[zip file "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk, /system/lib64, /system/product/lib64]]]
[load] loadClassAndInvoke: com.cyrus.example.plugin.FartTest

在 Logcat 中可以看到只對指定的類進行了主動加載和調用

word/media/image7.png

代碼與功能整合

整合代碼實現如下功能:

  • 過濾不需要主動調用的類

  • 解決局部變量的 ClassLoader 枚舉不出來問題

  • 解決非雙親委派關系下動態加載的 dex 脫殼問題

完整代碼如下:

// 前綴過濾邏輯
function shouldSkipClass(name) {return name.startsWith("androidx.") ||name.startsWith("android.") ||name.startsWith("com.google.android.") ||name.startsWith("org.jetbrains.") ||name.startsWith("kotlinx.") ||name.startsWith("kotlin.") ||name.startsWith("org.intellij.");
}function hookLoadClassAndInvoke() {const ActivityThread = Java.use('android.app.ActivityThread');if (ActivityThread.loadClassAndInvoke) {ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {if (shouldSkipClass(className)) {console.log('[skip] loadClassAndInvoke: ' + className);return; // 不調用原函數}console.log('[load] loadClassAndInvoke: ' + className);return this.loadClassAndInvoke(classloader, className, method); // 正常調用};} else {console.log('[-] ActivityThread.loadClassAndInvoke not found');}
}function fartOnDexclassloader() {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String',     // dexPath'java.lang.String',     // optimizedDirectory'java.lang.String',     // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log("    |- dexPath: " + dexPath);console.log("    |- optimizedDirectory: " + optimizedDirectory);console.log("    |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};
}function invokeAllClassloaders() {try {// 獲取 ActivityThread 類var ActivityThread = Java.use("android.app.ActivityThread");Java.enumerateClassLoaders({onMatch: function (loader) {try {// 過濾掉 BootClassLoaderif (loader.toString().includes("BootClassLoader")) {console.log("[-] 跳過 BootClassLoader");return;}// 調用 fartWithClassLoaderconsole.log("[*] 調用 fartwithClassloader -> " + loader);ActivityThread.fartwithClassloader(loader);} catch (e) {console.error("[-] 調用失敗: " + e);}},onComplete: function () {console.log("[*] 枚舉并調用完畢");}});} catch (err) {console.error("[-] 腳本執行異常: " + err);}
}setImmediate(function () {Java.perform(function () {// 過濾不需要主動調用的類hookLoadClassAndInvoke()// 解決局部變量的 ClassLoader 枚舉不出來問題fartOnDexclassloader()// 解決非雙親委派關系下動態加載的 dex 脫殼問題invokeAllClassloaders()})
})

啟動 app 執行腳本,并輸出日志到 log.txt

frida -H 127.0.0.1:1234 -l fart.js -f com.cyrus.example -o log.txt

或者 hook 當前前臺 app ,并輸出日志到 log.txt

frida -H 127.0.0.1:1234 -F -l fart.js -o log.txt

輸出日志如下:

word/media/image8.png

在 /sdcard/Android/data/com.cyrus.example/fart 下可以找到脫殼文件

word/media/image9.png

FART 脫殼結束得到的文件列表(分 Execute 與 主動調用兩類):

  1. Execute 脫殼點得到的 dex (*_dex_file_execute.dex)和 dex 中的所有類列表( txt 文件)

  2. 主動調用時 dump 得到的 dex (*_dex_file.dex)和此時 dex 中的所有類列表,以及該 dex 中所有函數的 CodeItem( bin 文件)

完整源碼

開源地址:

  • Android 示例代碼:https://github.com/CYRUS-STUDIO/AndroidExample

  • Frida 腳本源碼:https://github.com/CYRUS-STUDIO/frida_fart

  • FART源碼:https://github.com/CYRUS-STUDIO/FART

相關文章:

  • 干掉抽取殼!FART 自動化脫殼框架與 Execute 脫殼點解析

  • FART 主動調用組件深度解析:破解 ART 下函數抽取殼的終極武器

  • 一步步帶你移植 FART 到 Android 10,實現自動化脫殼

  • FART 自動化脫殼框架優化實戰:Bug 修復與代碼改進記錄

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/95213.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/95213.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/95213.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

TLS/SSL(傳輸層安全協議)

文章目錄一、核心概念二、為什么需要 TLS/SSL&#xff1f;三、工作原理與詳細流程握手步驟詳解&#xff1a;1.ClientHello & ServerHello&#xff1a;2.服務器認證 (Certificate, ServerKeyExchange)&#xff1a;3.客戶端響應 (ClientKeyExchange, Finished)&#xff1a;4.…

什么是 AWS 和 GCE ?

AWS 和 GCE 是兩種不同廠商提供的云計算服務&#xff0c;主要區別在于提供商和產品定位。AWS全稱&#xff1a;Amazon Web Services提供商&#xff1a;亞馬遜 (Amazon)簡介&#xff1a;全球最大的云計算平臺之一&#xff0c;提供完整的云服務&#xff0c;包括&#xff1a; 計算&…

水電站電動機絕緣安全 “不掉線”!在線監測方案筑牢發電保障

對水電站而言&#xff0c;消防水泵、深井水泵等輔助電動機是安全運行的 “關鍵配角”—— 它們常年處于備用狀態&#xff0c;又受潮濕環境影響&#xff0c;絕緣值降低易引發燒毀故障&#xff0c;而傳統定期檢測難以及時捕捉絕緣劣化趨勢&#xff0c;一旦啟動時出問題&#xff0…

【Datawhale之Happy-LLM】3種常見的decoder-only模型——Github最火大模型原理與實踐教程task07

Task07&#xff1a;第三章 預訓練語言模型PLM &#xff08;這是筆者自己的學習記錄&#xff0c;僅供參考&#xff0c;原始學習鏈接&#xff0c;愿 LLM 越來越好?&#xff09; 本篇介紹3種很典的decoder-only的PLM&#xff08;GPT、LlaMA、GLM&#xff09;。目前火&#x1f52…

【卷積神經網絡】卷積神經網絡的三大核心優勢:稀疏交互、參數共享與等變表示

1. 引言 卷積神經網絡(CNN)之所以在計算機視覺、語音識別等領域取得突破性進展,并非偶然。相比傳統的全連接神經網絡,CNN通過三個重要的思想來幫助改進機器學習系統:稀疏交互(sparse interactions)、參數共享(parameter sharing)、等變表示(equivariant representations)。…

網絡共享協議

網絡共享協議是用于在計算機網絡中實現資源共享和數據傳輸的規則或標準。常見的共享協議包括文件共享、打印機共享、互聯網連接共享等。SMB&#xff08;Server Message Block 服務器消息塊&#xff09;SMB是一種網絡共享協議&#xff0c;主要用于局域網中實現不同設備之間的文件…

MD5加密算法詳解與實現

MD5加密算法詳解與實現 什么是MD5加密&#xff1f; MD5&#xff08;Message-Digest Algorithm 5&#xff09;是一種廣泛使用的密碼散列函數&#xff0c;可以產生一個128位&#xff08;16字節&#xff09;的哈希值&#xff0c;通常用32位的十六進制數表示。MD5由Ronald Rivest在…

(nice!!!)(LeetCode 每日一題) 3025. 人員站位的方案數 I (排序)

題目&#xff1a;3025. 人員站位的方案數 I 思路&#xff1a;排序&#xff0c;時間復雜度0(n^2)。 將數組points里的元素先按橫坐標x升序排序&#xff0c;縱坐標y降序排序。第一層for循環枚舉左上角的點&#xff0c;第二層for循環枚舉右下角的點。細節看注釋。 C版本&#xff…

可可圖片編輯 HarmonyOS(4)圖片裁剪

可可圖片編輯 HarmonyOS&#xff08;4&#xff09;圖片裁剪-canvas 前言 可可圖片編輯 實現了圖片的裁剪功能&#xff0c;效果如圖所示。這里的核心技術是使用了canvas。 Canvas 入門 Canvas提供畫布組件&#xff0c;用于自定義繪制圖形&#xff0c;開發者使用CanvasRenderi…

怎么用PS制作1寸證件照(已解決)

方法/步驟一、按住鍵盤上的“Ctrl”“O”打開你要制作的照片二、點擊裁剪工具 (調整為寬:2.5cm&#xff0c;高:3.5cm&#xff0c;分辨率:300像素)&#xff0c;設置之后直接框選出需要剪切保留的位置(使人物居正中)&#xff0c; 然后按上面的“√”&#xff0c;以便確認剪裁三、…

Qt libcurl的下載、配置及簡單測試 (windows環境)

Qt libcurl的下載、配置及簡單測試引言一、libcurl下載二、在Qt Creator中配置三、簡單測試引言 curl&#xff08;Client URL&#xff09;是一個開源的命令行工具和庫&#xff0c;用于傳輸數據支持多種協議&#xff08;如HTTP、HTTPS、FTP、SFTP等&#xff09;。其核心庫libcur…

【Python語法基礎學習筆記】競賽常用標準庫

前言此系列筆記是撥珠自己的學習筆記&#xff0c;自用為主&#xff0c;學習建議移步其他大佬的專門教程。math庫Python 的 math 庫是標準庫之一&#xff0c;提供了大量數學運算相關的函數&#xff0c;適用于基礎數學計算、科學計算等場景。下面詳細介紹其使用方法及常用功能&am…

我的項目我做主:Focalboard+cpolar讓團隊協作擺脫平臺依賴

文章目錄前言1. 使用Docker本地部署Focalboard1.1 在Windows中安裝 Docker1.2 使用Docker部署Focalboard2. 安裝Cpolar內網穿透工具3. 實現公網訪問Focalboard4. 固定Focalboard公網地址前言 “項目管理軟件又漲價了&#xff01;“小團隊負責人小林發愁——剛習慣操作邏輯&…

【3D 入門-4】trimesh 極速上手之 3D Mesh 數據結構解析(Vertices / Faces)

【3D入門-指標篇上】3D 網格重建評估指標詳解與通俗比喻【3D入門-指標篇下】 3D重建評估指標對比-附實現代碼【3D 入門-3】常見 3D 格式對比&#xff0c;.glb / .obj / .stl / .ply Mesh 數據結構解析 1. Vertices&#xff08;頂點&#xff09; original_vertices mesh_ful…

無需服務器,免費、快捷的一鍵部署前端 vue React代碼--PinMe

作為前端的開發&#xff0c;有時候想部署一個項目真的是很“受氣”&#xff0c;要不就是找運維&#xff0c;或者后端&#xff0c;看別人的時間&#xff0c;或者走流程。 現在&#xff0c;有這么一個神器PinMe&#xff0c; 以前部署項目&#xff1a;自己買服務器?域名、 SSL、N…

【LeetCode_26】刪除有序數組中的重復項

刷爆LeetCode系列LeetCode26題&#xff1a;github地址前言題目描述題目與思路分析代碼實現算法代碼優化LeetCode26題&#xff1a; github地址 有夢想的電信狗 前言 本文介紹用C實現leetCode第26題題目鏈接&#xff1a;https://leetcode-cn.com/problems/remove-duplicates-…

CMake構建學習筆記23-SQLite庫的構建

1. 構建思路 在前文中構建了大量的庫包程序&#xff08;參看CMake構建學習筆記-目錄&#xff09;之后&#xff0c;可以總結一下在Windows下使用腳本構建程序的辦法&#xff1a; 使用CMake構建。這是目前最通用最流行的構建方式&#xff0c;大部分C/C程序都在逐漸向這個方向轉…

Watt Toolkit下載安裝并加速GitHub

一、下載 官方地址:(Steam++官網) - Watt Toolkit Gitee下載地址:https://gitee.com/rmbgame/SteamTools/releases/tag/3.0.0-rc.16

DevOps運維與開發一體化及Kubernetes運維核心詳解

前言&#xff1a; 在云原生時代&#xff0c;技術的融合與流程的重構已成為驅動業務創新的核心引擎。Kubernetes作為容器編排的事實標準&#xff0c;其穩定的運維能力是業務應用的基石&#xff1b;而DevOps所倡導的開發與運維一體化文化&#xff0c;則是實現快速交付和價值流動的…

HQX SELinux 權限問題分析與解決

Google自Android 5.0起強制實施的SELinux安全子系統&#xff0c;通過最小權限原則顯著提升了系統安全性&#xff0c;但這也導致開發過程中頻繁出現權限拒絕問題。值得注意的是&#xff0c;即便設備已獲取root權限&#xff0c;SELinux的強制訪問控制機制仍會限制部分敏感操作。 …