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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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的强制访问控制机制仍会限制部分敏感操作。 …