FART 主动调用组件设计和源码分析
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
现有脱壳方法存在的问题
脱壳粒度集中在 DexFile 整体,当前对 apk 保护的粒度在函数粒度,这就导致了脱壳与加固的不对等,无法应对函数粒度的加固保护。
Dalvik 下的基于主动调用的自动化脱壳方案,首次将粒度下降到函数粒度
-
https://github.com/zyq8709/DexHunter
-
https://github.com/F8LEFT/FUPK3
存在问题:
1、Dalvik 慢慢淡出视野,无法应对 ART 环境
2、当前没有一个好的脱壳修复框架,无法应用到 VMP 函数的修复
主动调用相关概念
1、被动调用
指 app 正常运行过程中发生的调用,该过程只对 dex 中部分的类完成了加载,同时也只是对 dex 中的部分函数完成了调用。
2、主动调用
通过构造虚拟调用,从而达到欺骗 “壳” ,让壳误以为 app 在执行正常的函数调用流程从而达成对 dex 中所有类函数的虚拟调用。
被动调用也可以用来完成函数粒度的修复。如当前通过正常运行 app ,待 app 将 dex 中的类正常加载并完成相关的函数的正常调用后再进行 dex 的 dump 的脱壳方法。
被动调用脱壳的缺点: 存在修复函数不全的问题。由于测试用例无法覆盖 dex 中所有的函数,导致代码覆盖率低,只能对 app 运行过程中调用过的函数的修复。
主动调用的优点: 能够覆盖 dex 中所有的函数,从而完成更彻底的函数粒度的修复。同时,函数的修复准确度同主动调用链的构造深度有关。
FART 中要解决的三个问题
1、如何构造主动调用链并让每一个函数都到达主动调用过程,但是又不影响 app 的正常运行?
2、如何根据 ArtMethod 定位内存中对应的 CodeItem 的起始地址?
3、如何遍历 dex 中的所有函数并完成主动调用?
如何构造主动调用链?
标准 Java 函数调用示例代码:
// 1. 查找 Java 类
jclass clazz = env->FindClass("com/cyrus/example/jniexample/JNIExample");
if (clazz == nullptr) {LOGI("Class not found");return env->NewStringUTF("Class not found");
}// 2. 获取 Java 方法 ID
jmethodID methodId = env->GetStaticMethodID(clazz, "helloFromJava", "()Ljava/lang/String;");
if (methodId == nullptr) {LOGI("Method not found");return env->NewStringUTF("Method not found");
}// 3. 调用 Java 方法
jstring resultStr = (jstring) env->CallStaticObjectMethod(clazz, methodId);
标准流程:
-
通过 FindClass 得到 jclass
-
通过 GetStaticMethodID / GetMethodId 得到 jmethodID
-
通过一系列 Call 开头的函数完成调用(比如这里的 CallStaticObjectMethod)
FindClass
FindClass 的作用: 通过 ClassLinker 的 FindClass 取得目标类的 jclass
static jclass FindClass(JNIEnv* env, const char* name) {CHECK_NON_NULL_ARGUMENT(name);Runtime* runtime = Runtime::Current();ClassLinker* class_linker = runtime->GetClassLinker();std::string descriptor(NormalizeJniClassDescriptor(name));ScopedObjectAccess soa(env);ObjPtr<mirror::Class> c = nullptr;if (runtime->IsStarted()) {StackHandleScope<1> hs(soa.Self());Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);} else {c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());}return soa.AddLocalReference<jclass>(c);}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=654
GetMethodID / GetStaticMethodID 的作用:根据 jclass 对象、方法名和签名信息取得对应的 ArtMethod 用于下一步的调用
static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig) {CHECK_NON_NULL_ARGUMENT(java_class);CHECK_NON_NULL_ARGUMENT(name);CHECK_NON_NULL_ARGUMENT(sig);ScopedObjectAccess soa(env);return FindMethodID(soa, java_class, name, sig, false);}static jmethodID GetStaticMethodID(JNIEnv* env, jclass java_class, const char* name,const char* sig) {CHECK_NON_NULL_ARGUMENT(java_class);CHECK_NON_NULL_ARGUMENT(name);CHECK_NON_NULL_ARGUMENT(sig);ScopedObjectAccess soa(env);return FindMethodID(soa, java_class, name, sig, true);}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=980
FindMethodID
GetStaticMethodID 和 GetMethodID 最终都是走到 FindMethodID ,只是传参 is_static 不同
static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,const char* name, const char* sig, bool is_static)REQUIRES_SHARED(Locks::mutator_lock_) {ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(), soa.Decode<mirror::Class>(jni_class));if (c == nullptr) {return nullptr;}ArtMethod* method = nullptr;auto pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize();if (c->IsInterface()) {method = c->FindInterfaceMethod(name, sig, pointer_size);} else {method = c->FindClassMethod(name, sig, pointer_size);}if (method != nullptr && ShouldDenyAccessToMember(method, soa.Self())) {method = nullptr;}if (method == nullptr || method->IsStatic() != is_static) {ThrowNoSuchMethodError(soa, c, name, sig, is_static ? "static" : "non-static");return nullptr;}return jni::EncodeArtMethod(method);
}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=430
最后返回的是 jni::EncodeArtMethod(method),这是 ART 中将 ArtMethod* 转换为 jmethodID 的封装过程。
ALWAYS_INLINE
static inline jmethodID EncodeArtMethod(ArtMethod* art_method) {return reinterpret_cast<jmethodID>(art_method);
}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.h;l=55
jmethodID 在 ART 中其实就是一个 ArtMethod*,而 EncodeArtMethod 就是把 ArtMethod* 安全地封装成 jmethodID。
CallObjectMethod
CallObjectMethod / CallStaticObjectMethod 函数源码如下:
static jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID mid, ...) {va_list ap;va_start(ap, mid);ScopedVAArgs free_args_later(&ap);CHECK_NON_NULL_ARGUMENT(obj);CHECK_NON_NULL_ARGUMENT(mid);ScopedObjectAccess soa(env);JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap));return soa.AddLocalReference<jobject>(result.GetL());}static jobject CallStaticObjectMethod(JNIEnv* env, jclass, jmethodID mid, ...) {va_list ap;va_start(ap, mid);ScopedVAArgs free_args_later(&ap);CHECK_NON_NULL_ARGUMENT(mid);ScopedObjectAccess soa(env);JValue result(InvokeWithVarArgs(soa, nullptr, mid, ap));jobject local_result = soa.AddLocalReference<jobject>(result.GetL());return local_result;}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=997
InvokeVirtualOrInterfaceWithVarArgs 与 InvokeWithVarArgs
JValue InvokeVirtualOrInterfaceWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,jobject obj, jmethodID mid, va_list args) {// We want to make sure that the stack is not within a small distance from the// protected region in case we are calling into a leaf function whose stack// check has been elided.if (UNLIKELY(__builtin_frame_address(0) < soa.Self()->GetStackEnd())) {ThrowStackOverflowError(soa.Self());return JValue();}ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj);ArtMethod* method = FindVirtualMethod(receiver, jni::DecodeArtMethod(mid));bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();if (is_string_init) {// Replace calls to String.<init> with equivalent StringFactory call.method = WellKnownClasses::StringInitToStringFactory(method);receiver = nullptr;}uint32_t shorty_len = 0;const char* shorty =method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetShorty(&shorty_len);JValue result;ArgArray arg_array(shorty, shorty_len);arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);InvokeWithArgArray(soa, method, &arg_array, &result, shorty);if (is_string_init) {// For string init, remap original receiver to StringFactory result.UpdateReference(soa.Self(), obj, result.GetL());}return result;
}JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa, jobject obj, jmethodID mid,va_list args)REQUIRES_SHARED(Locks::mutator_lock_) {// We want to make sure that the stack is not within a small distance from the// protected region in case we are calling into a leaf function whose stack// check has been elided.if (UNLIKELY(__builtin_frame_address(0) < soa.Self()->GetStackEnd())) {ThrowStackOverflowError(soa.Self());return JValue();}ArtMethod* method = jni::DecodeArtMethod(mid);bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();if (is_string_init) {// Replace calls to String.<init> with equivalent StringFactory call.method = WellKnownClasses::StringInitToStringFactory(method);}ObjPtr<mirror::Object> receiver = method->IsStatic() ? nullptr : soa.Decode<mirror::Object>(obj);uint32_t shorty_len = 0;const char* shorty =method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetShorty(&shorty_len);JValue result;ArgArray arg_array(shorty, shorty_len);arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);InvokeWithArgArray(soa, method, &arg_array, &result, shorty);if (is_string_init) {// For string init, remap original receiver to StringFactory result.UpdateReference(soa.Self(), obj, result.GetL());}return result;
}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/reflection.cc;l=616
所以,无论是静态方法还是非静态方法最终都会调用到 InvokeWithArgArray
void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,ArtMethod* method, ArgArray* arg_array, JValue* result,const char* shorty)REQUIRES_SHARED(Locks::mutator_lock_) {uint32_t* args = arg_array->GetArray();if (UNLIKELY(soa.Env()->IsCheckJniEnabled())) {CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(kRuntimePointerSize), args);}method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/reflection.cc;l=450
结论:对于 Java 函数的调用,最终由该函数对应的 ArtMethod 对象的 Invoke 函数完成。
如何 dump CodeItem?
dumpMethodCode
DexFIle 类添加 native 函数 dumpMethodCode,用于提取指定 ArtMethod 对应的 Dex 字节码(CodeItem)。
static void DexFile_dumpMethodCode(JNIEnv* env, jclass, jobject method) {// 将当前线程从 kNative 状态切换到 kRunnable,表示线程可以安全访问 Java 对象(如 jobject)。ScopedFastNativeObjectAccess soa(env);// 如果 Java 传入的 Method 不为 nullif(method != nullptr){// 将 java.lang.reflect.Method 转换为 ART 内部的 ArtMethod 指针ArtMethod* artmethod = ArtMethod::FromReflectedMethod(soa, method);// 调用 myfartInvokemyfartInvoke(artmethod);}return;
}
libcore/dalvik/src/main/java/dalvik/system/DexFile.java
public final class DexFile {private static native void dumpMethodCode(Object m);
}
myfartInvoke 方法中主动调用 ArtMethod 的 Invoke 方法
extern "C" void myfartInvoke(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {JValue *result = nullptr;Thread *self = nullptr;uint32_t temp = 6;uint32_t *args = &temp;uint32_t args_size = 6;artmethod->Invoke(self, args, args_size, result, "fart");
}
在 ArtMethod::Invoke 方法中,检测到 self 是 nullptr,也就是 myfartInvoke 的主动调用时,调用 dumpArtMethod 把解密后的函数 dump 下来
void ArtMethod::Invoke(Thread * self, uint32_t * args, uint32_t args_size, JValue * result,const char *shorty) {if (self == nullptr) {dumpArtMethod(this);return;}...
}
art/runtime/art_method.cc
获取 CodeItem 起始地址
CodeItem 起始地址就在 ArtMethod 的 dex_code_item_offset_ 字段。
不同 Android 版本略有不同
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/art_method.h;l=755
通过 GetCodeItemOffset() 方法拿到 CodeItem 的起始地址
https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/art_method.h;l=378
CodeItem 结构定义
Android 源码中 CodeItem 的结构定义如下:
struct CodeItem : public dex::CodeItem {// DEX 字节码必须按照 4 字节对齐static constexpr size_t kAlignment = 4;private:// 方法使用的虚拟寄存器数量(包括本地变量和参数)uint16_t registers_size_;// 方法的入参占用的寄存器数量uint16_t ins_size_;// 方法调用其他方法时所需的最大出参寄存器数量(即调用其他方法时的参数空间)uint16_t outs_size_;// try-catch 块的数量。如果不为 0,则在 insns_ 后紧跟 try_item 和 catch_handler。uint16_t tries_size_;// 调试信息在 DEX 文件中的偏移,指向 debug_info 结构// 包括局部变量名、源码行号映射等uint32_t debug_info_off_;// 指令(insns_)数组长度,单位是 2 字节(code units)// 每条指令通常是 2 字节对齐,有些指令占用多个 code unituint32_t insns_size_in_code_units_;// 指令数组(实际大小是可变的,柔性数组)// 存放 DEX 字节码指令,insns_size_in_code_units_ 表示其长度uint16_t insns_[1];
};
https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/standard_dex_file.h;l=35
https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/compact_dex_file.h;l=87
CodeItem 前 16 字节是固定结构
字节偏移 | 字段名 | 含义说明 | 大小(字节) |
---|---|---|---|
0x00 | registers_size_ | 方法使用的寄存器数(本地变量 + 参数) | 2 |
0x02 | ins_size_ | 方法参数占用的寄存器数(入参) | 2 |
0x04 | outs_size_ | 调用其他方法所需的最大出参寄存器数(临时参数空间) | 2 |
0x06 | tries_size_ | try-catch 块数量,非 0 时表示有异常处理结构 | 2 |
0x08 | debug_info_off_ | 调试信息在 DEX 文件中的偏移 | 4 |
0x0C | insns_size_in_code_units_ | 指令数组长度(单位为 2 字节 code unit) | 4 |
共计 | 16 字节 |
CodeItem 前 16 字节是方法的执行元信息,后面的 insns_ 是变长的字节码数组,长度由 insns_size_in_code_units_ 决定,之后可能还有异常处理相关结构(try_items 和 catch_handlers)。
TryItem
TryItem 结构体源码如下 :
// Raw try_item.
struct TryItem {uint32_t start_addr_;uint16_t insn_count_;uint16_t handler_off_;
};
https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/dex_file_structs.h;l=196
字段名 | 类型 | 说明 |
---|---|---|
start_addr_ | uint32_t | 指向 code_item->insns[] 的某个偏移,表示 try 块的起始位置(单位为 16-bit 指令) |
insn_count_ | uint16_t | try 块中包含多少条指令(单位为 16-bit 指令) |
handler_off_ | uint16_t | 是 hander 结构的偏移,这个偏移是从 insns_ + sizeof(TryItem) * tries_size_ 开始的,用于描述 catch/finally 的处理逻辑 |
完整示例:带各种 TryItem 情况的 Kotlin 类
package com.cyrus.example.shellclass TryItemExample {// ✅ 0. 没有 try-catch(不会生成 TryItem)fun noTryCatch(): Int {val a = 1val b = 2return a + b}// ✅ 1. 简单 try-catch(一个 TryItem)fun simpleTryCatch(): String {return try {val x = 10 / 2"Result: $x"} catch (e: Exception) {"Caught Exception"}}// ✅ 2. 多个 catch 分支(一个 TryItem,多个 handler entry)fun multiCatch(input: String?): Int {return try {input!!.length} catch (e: NullPointerException) {-1} catch (e: Exception) {-2}}// ✅ 3. try-catch-finally(一个 TryItem + finally handler)fun tryCatchFinally(): Int {return try {1 / 0} catch (e: ArithmeticException) {-100} finally {println("finally block executed")}}// ✅ 4. 嵌套 try-catch(两个 TryItem,嵌套结构)fun nestedTryCatch(): String {return try {try {val data = "123".toInt()"Parsed: $data"} catch (e: NumberFormatException) {"Inner Catch"}} catch (e: Exception) {"Outer Catch"}}// ✅ 5. 只有 finally,无 catch(一个 TryItem,无 handler entry)fun onlyFinally(): Int {try {val x = 1 + 1} finally {println("executing finally without catch")}return 0}// ✅ 6. 多个独立 try 块(多个 TryItem,非嵌套)fun multipleTryBlocks(): Int {try {val a = 10 / 2} catch (e: Exception) {println("First catch")}try {val b = "abc".toInt()} catch (e: NumberFormatException) {println("Second catch")}return 0}
}
编译运行得到 dex 文件
通过 GDA 查看 dex 中 multipleTryBlocks 函数的 TryItem 信息,可以看到 0004 到 0005 就是第一个 try 块的开始和结束地址
在 010Editor 中打开 dex ,在如下路径
dex_class_defs/class_def[17]/class_def[11] public final com.cyrus.example.shell.TryItemExample/class_data/virtual_methods/method[1] public final int com.cyrus.example.shell.TryItemExample.multipleTryBlocks()/code/tries[2]
可以看到 TryItem 数据
CatchHandlerItem
在 DEX 文件中,TryItem 表示一个异常捕获区域,其对应的异常处理器(handler)由 remaining_count_ 指示 catch/finally 块的数量和类型:
-
如果 remaining_count_ <= 0 表示最后一个是 finally 块。
-
如果 remaining_count_ > 0 表示只有 catch 块。
-
remaining_count_ 的绝对值表示 catch 块的个数。
-
如果 remaining_count_ == 0 表示没有 catch 块,只有 finally 块。
CatchHandlerItem:表示一个 catch 分支(即一个异常类型和对应的处理地址)。
class CatchHandlerIterator {...struct CatchHandlerItem {dex::TypeIndex type_idx_; // 捕获异常的类型索引(指向 Dex 文件中的 type_ids 表)uint32_t address_; // 异常处理器的代码地址(从 code_item.insns 的偏移量)} handler_;const uint8_t* current_data_; // the current handler in dex file.int32_t remaining_count_; // number of handlers not read.bool catch_all_; // is there a handler that will catch all exceptions in case// that all typed handler does not match.
};
https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/dex_file_exception_helpers.h;l=67
在 010Editor 中打开 dex ,在如下路径
dex_class_defs/class_def[17]/class_def[11] public final com.cyrus.example.shell.TryItemExample/class_data/virtual_methods/method[1] public final int com.cyrus.example.shell.TryItemExample.multipleTryBlocks()/code/handlers
可以看到 CatchHandlerItem 数据
一个 TryItem 对应一个 CatchHandlerIterator ,CatchHandlerIterator 中有一个或多个 CatchHandlerItem,共同组成 try catch/finally 块。
CodeItem 的长度计算和 dump
CodeItem 的长度计算和 dump,两种情况:
-
无异常处理
-
有异常处理
具体实现代码在 FART 项目 art/runtime/art_method.cc 的 dumpArtMethod 函数里
extern "C" void dumpArtMethod(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {// 分配临时路径缓冲区char *dexfilepath = (char *) malloc(sizeof(char) * 2000);if (dexfilepath == nullptr) {LOG(INFO) << "ArtMethod::dumpArtMethod invoked, methodname: "<< PrettyMethod(artmethod).c_str()<< " malloc 2000 byte failed";return;}// 获取进程名(用于保存路径)int fcmdline = -1;char szCmdline[64] = { 0 };char szProcName[256] = { 0 };int procid = getpid();sprintf(szCmdline, "/proc/%d/cmdline", procid);fcmdline = open(szCmdline, O_RDONLY, 0644);if (fcmdline > 0) {read(fcmdline, szProcName, 256);close(fcmdline);}// 如果成功获取进程名if (szProcName[0]) {// 获取方法所属的 DexFileconst DexFile *dex_file = artmethod->GetDexFile();const char *methodname = PrettyMethod(artmethod).c_str();const uint8_t *begin_ = dex_file->Begin(); // dex 数据起始地址size_t size_ = dex_file->Size(); // dex 大小// 创建 fart 路径memset(dexfilepath, 0, 2000);sprintf(dexfilepath, "%s", "/sdcard/fart");mkdir(dexfilepath, 0777);// 创建 fart/进程名 路径memset(dexfilepath, 0, 2000);sprintf(dexfilepath, "/sdcard/fart/%s", szProcName);mkdir(dexfilepath, 0777);// 保存 dex 文件到 /sdcard/fart/进程名/xxx_dexfile.dexmemset(dexfilepath, 0, 2000);sprintf(dexfilepath, "/sdcard/fart/%s/%d_dexfile.dex", szProcName, (int)size_);int dexfilefp = open(dexfilepath, O_RDONLY, 0666);if (dexfilefp > 0) {// 如果文件已存在,不再写入close(dexfilefp);dexfilefp = 0;} else {// 写入完整 dex 数据dexfilefp = open(dexfilepath, O_CREAT | O_RDWR, 0666);if (dexfilefp > 0) {write(dexfilefp, (void *) begin_, size_);fsync(dexfilefp);close(dexfilefp);}}// 获取该方法的 CodeItem(即字节码结构体)const DexFile::CodeItem * code_item = artmethod->GetCodeItem();if (LIKELY(code_item != nullptr)) {int code_item_len = 0;uint8_t *item = (uint8_t *) code_item;// 如果有异常处理结构,计算尾部偏移if (code_item->tries_size_ > 0) {const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item, code_item->tries_size_));// 解析 handler 数据结构,返回 handler 数据结束的指针位置uint8_t *tail = codeitem_end(&handler_data);code_item_len = (int) (tail - item);} else {// 没有 try 块,长度 = 16字节头部 + 指令长度(每个指令2字节)code_item_len = 16 + code_item->insns_size_in_code_units_ * 2;}// 组合保存路径memset(dexfilepath, 0, 2000);int size_int = (int) dex_file->Size();uint32_t method_idx = artmethod->get_method_idx();sprintf(dexfilepath, "/sdcard/fart/%s/%d_%ld.bin", szProcName, size_int, gettidv1());// 打开保存 CodeItem 信息的文件int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);if (fp2 > 0) {lseek(fp2, 0, SEEK_END);// 写入方法信息(name、method_idx、offset、code_item_len)memset(dexfilepath, 0, 2000);int offset = (int)(item - begin_);sprintf(dexfilepath,"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",methodname, method_idx, offset, code_item_len);int contentlength = strlen(dexfilepath);write(fp2, (void *) dexfilepath, contentlength);// base64 编码指令数据并写入long outlen = 0;char *base64result = base64_encode((char *) item, (long)code_item_len, &outlen);write(fp2, base64result, outlen);write(fp2, "};", 2);fsync(fp2);close(fp2);// 清理内存if (base64result != nullptr) {free(base64result);base64result = nullptr;}}}}// 释放路径缓冲区if (dexfilepath != nullptr) {free(dexfilepath);dexfilepath = nullptr;}
}
解析 CodeItem 的异常处理数据部分,返回 handler 数据结束的指针位置
uint8_t *codeitem_end(const uint8_t **pData) {// 读取 handler 列表的数量(即有多少个 try 块)uint32_t num_of_list = DecodeUnsignedLeb128(pData);// 遍历每个 try 块的 handler 列表for (; num_of_list > 0; num_of_list--) {// 读取当前 handler 列表中 handler 的数量,可能为负值,表示存在 catch-all handlerint32_t num_of_handlers = DecodeSignedLeb128(pData);// 取绝对值,得到实际的 type-handler 对数量int num = num_of_handlers;if (num_of_handlers <= 0) {num = -num_of_handlers; // catch-all handler 也算在内,但单独处理}// 读取每个 handler 的 type_idx 和 addressfor (; num > 0; num--) {DecodeUnsignedLeb128(pData); // type_idxDecodeUnsignedLeb128(pData); // address}// 如果存在 catch-all handler,再额外读取一个地址if (num_of_handlers <= 0) {DecodeUnsignedLeb128(pData); // catch-all address}}// 此时 *pData 已指向异常处理部分结束的位置return (uint8_t *)(*pData);
}
如何实现主动调用?
fart 函数中遍历当前 App 的 ClassLoader,拿到 ClassLoader 需要判断不是系统的 BootClassLoader,不然会把系统框架的 dex dump 下来
public static void fart() {ClassLoader appClassloader = getClassloader();if(appClassloader == null){Log.e("ActivityThread", "appClassloader is null");return;}if(appClassloader.toString().indexOf("java.lang.BootClassLoader") == -1){fartWithClassLoader(appClassloader);}ClassLoader tmpClassloader = appClassloader;ClassLoader parentClassloader = appClassloader.getParent();while(parentClassloader != null){if(parentClassloader.toString().indexOf("java.lang.BootClassLoader") == -1){fartWithClassLoader(parentClassloader);}tmpClassloader = parentClassloader;parentClassloader = parentClassloader.getParent();}
}
fartWithClassLoader 函数中遍历 ClassLoader 中的所有 DexFile,获取所有类名,并调用 native 方法 dumpMethodCode。
public static void fartWithClassLoader(ClassLoader appClassloader) {Log.i("ActivityThread", "fartWithClassLoader " + appClassloader.toString());// 用于存放获取到的 dexFile 对象List<Object> dexFilesArray = new ArrayList<Object>();// 获取 DexPathList 对象实例Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");// 获取 dexElements 字段,类型为 DexPathList$Element[],每个 element 封装了 dexFileObject[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");// 声明 dexElements 中的 dexFile 字段Field dexFile_fileField = null;try {dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");} catch (Exception e) {e.printStackTrace();}// 通过类加载器反射获取 dalvik.system.DexFile 类Class DexFileClazz = null;try {DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");} catch (Exception e) {e.printStackTrace();}// 要调用的 native 方法Method getClassNameList_method = null;Method defineClass_method = null;Method dumpMethodCode_method = null;// 遍历 DexFile 类中的方法,设置需要的 native 方法强制可访问for (Method field : DexFileClazz.getDeclaredMethods()) {if (field.getName().equals("getClassNameList")) {getClassNameList_method = field;getClassNameList_method.setAccessible(true);}if (field.getName().equals("defineClassNative")) {defineClass_method = field;defineClass_method.setAccessible(true);}if (field.getName().equals("dumpMethodCode")) {dumpMethodCode_method = field;dumpMethodCode_method.setAccessible(true);}}// 获取 mCookie 字段(DexFile 中指向 native Dex 的句柄)Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");// 遍历每一个 dex element(即每个 dex 文件)for (int j = 0; j < ElementsArray.length; j++) {Object element = ElementsArray[j];Object dexfile = null;try {// 通过反射获取 DexFile 对象dexfile = (Object) dexFile_fileField.get(element);} catch (Exception e) {e.printStackTrace();}if (dexfile == null) {continue;}// 加入列表dexFilesArray.add(dexfile);// 获取 native mCookie 对象Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie");if (mcookie == null) {continue;}// 调用 native 方法 getClassNameList(mcookie) 获取 dex 中包含的类名数组String[] classnames = null;try {classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);} catch (Exception e) {e.printStackTrace();continue;} catch (Error e) {e.printStackTrace();continue;}// 遍历所有类名,对每个类调用 dumpMethodCodeif (classnames != null) {for (String eachclassname : classnames) {loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method);}}}return;
}
主动加载 dex 中的所有类需要对 dex 文件解析,获取 dex 中的所有类列表,两种解决方案:
-
手动解析 dex 文件
-
直接调用 aosp 源码中已有的 api:getClassNameList 即可(需要通过反射一步步获取当前 ClassLoader 当中的 mCookie)
拿到 ClassLoader 的 dexElements ,迭代 dexElements 通过反射拿到 DexFile 的 mCookie
https://cs.android.com/android/platform/superproject/+/android10-release:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java;l=69
调用 DexFile.getClassNameList 传递 mCookie 得到 dex 的 class list
https://cs.android.com/android/platform/superproject/+/android10-release:libcore/dalvik/src/main/java/dalvik/system/DexFile.java;l=432
拿到 ClassNameList 后,调用 loadClassAndInvoke 进行主动加载;
LoadClassAndInvoke 中对类进行加载,遍历类中构造函数和普通函数,进行主动调用。
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);Class resultclass = null;try {// 使用给定的 ClassLoader 加载指定类名的类resultclass = appClassloader.loadClass(eachclassname);} catch (Exception e) {e.printStackTrace();return;} catch (Error e) {e.printStackTrace();return;}// 如果类加载成功if (resultclass != null) {try {// 获取该类的所有构造方法(包括 private 的)Constructor<?> cons[] = resultclass.getDeclaredConstructors();for (Constructor<?> constructor : cons) {if (dumpMethodCode_method != null) {try {// 调用 native 方法 dumpMethodCode 传入构造方法对象// 发起主动调用 并 dump methoddumpMethodCode_method.invoke(null, constructor);} catch (Exception e) {e.printStackTrace();continue;} catch (Error e) {e.printStackTrace();continue;}} else {Log.e("ActivityThread", "dumpMethodCode_method is null ");}}} catch (Exception e) {e.printStackTrace();} catch (Error e) {e.printStackTrace();}try {// 获取该类的所有方法(包括 private 的)Method[] methods = resultclass.getDeclaredMethods();if (methods != null) {for (Method m : methods) {if (dumpMethodCode_method != null) {try {// 调用 native 方法 dumpMethodCodedumpMethodCode_method.invoke(null, m);} catch (Exception e) {e.printStackTrace();continue;} catch (Error e) {e.printStackTrace();continue;}} else {Log.e("ActivityThread", "dumpMethodCode_method is null ");}}}} catch (Exception e) {e.printStackTrace();} catch (Error e) {e.printStackTrace();}}
}
通过 loadClass 加载类,为什么不用 Class.forName?因为 Class.forName 会调用类的初始化方法,有一些对抗手段会在类初始化中执行,所以用 loadClass,loadClass 不会调用类的初始化方法。
resultclass = appClassloader.loadClass(eachclassname);
进入到 ArtMethod 的 Invoke 方法后,识别出是我们自己发起的主动调用后,执行 dumpArtMethod
void ArtMethod::Invoke(Thread* self, uint32_t* args,uint32_t args_size, JValue* result,const char* shorty) {// 若 self 为空,直接 dump 当前 ArtMethodif (self == nullptr) {dumpArtMethod(this);return;}...
}
相关文章:
FART 主动调用组件设计和源码分析
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ 现有脱壳方法存在的问题 脱壳粒度集中在 DexFile 整体,当前对 apk 保护的粒度在函数粒度,这就导致了脱壳与加固的不对等,无…...
windows使用ollama部署deepseek及qwen
ollama 参考文档 ollama 官方文档 GitHub仓库 基础环境: NVIDIA 1660TI 6G 下载 ollma是一款开源工具,支持在本地计算机(无需联网)快速部署和运行大型语言模型(LLM),如 LLaMA、Mistral、G…...
【11408学习记录】考研英语辞职信写作三步法:真题精讲+妙句活用+范文模板
应聘信 英语写作2005年考研英语真题小作文写作思路第一段第二段妙句7 9妙句11补充3补充4 第三段 妙句成文 每日一句词汇第一步:找谓语第二步:断句第三步:简化主句原因状语从句 英语 写作 2005年考研英语真题小作文 Directions: Two m…...
湖北理元理律师事务所:债务优化如何实现“减负不降质”?
债务压力下,如何在保障基本生活品质的同时科学规划还款,是许多债务人面临的现实难题。湖北理元理律师事务所通过多年实务经验,总结出一套“法律财务心理”的复合型解决方案。本文基于公开案例与法律框架,解析其服务逻辑中的可借鉴…...
python fastapi + react, 写一个图片 app
1. 起因, 目的: 上厕所的时候,想用手机查看电脑上的图片,但是又不想点击下载。此app 应运而生。 2. 先看效果 单击图片,能放大图片 3. 过程: 过程很枯燥。有时候, 有一堆新的想法。 但是做起来太麻烦,…...
Golang的Web应用架构设计
# Golang的Web应用架构设计 介绍 是一种快速、高效、可靠的编程语言,它在Web应用开发中越来越受欢迎。Golang的Web应用架构设计通常包括前端、后端和数据库三个部分。在本篇文章中,我们将详细介绍Golang的Web应用架构设计及其组成部分。 前端 在Golang的…...
软件设计师“UML”真题考点分析——求三连
一、考点分值占比与趋势分析 综合知识题分值统计表 年份考题数量分值分值占比考察重点2018222.67%类图关系、序列图消息流2019334.00%对象图特征、部署图辨析2020222.67%组件图特性、泛化关系2021334.00%聚合/组合区别、交互图应用2022222.67%用例图参与者、状态图转换202344…...
Nginx端口telnet不通排查指南
nginx已经配置server及端口20002,telnet不通:telnet 127.0.0.1 20002 Trying 127.0.0.1... telnet: connect to address 127.0.0.1: Connection refused 一、检查 systemctl status nginx.service nginx: [emerg] bind() to 0.0.0.0:20002 failed (13…...
C++ 函数对象、仿函数与 Lambda 表达式详解
C 函数对象、仿函数与 Lambda 表达式详解 在 C 中,函数对象(Function Object)、仿函数(Functor) 和 Lambda 表达式 是三种实现可调用行为的技术,它们在功能上类似,但语法和适用场景有所不同。 …...
More Effective C++:改善编程与设计(下)
目录 条款19:了解临时对象的来源 条款20:协助完成“返回值优化” 条款21:利用重载技术避免隐式类型转换 条款22:考虑以操作符复合形式(op)取代其独身形式(op) 条款23:考虑使用其他程序库 条款24:了解virtual functions、mul…...
C++:判断闰年
【描述】 判断某年是否是闰年。 【输入】 输入只有一行,包含一个整数a(0 < a < 3000) 【输出】 一行,如果公元a年是闰年输出Y,否则输出N 【样例输入】 2006 【样例输出】 N 【提示】 公历纪年法中,能被4整除的大多是闰年&am…...
C+++STL(一)
/ 文章目录 模版C作为静态类型语言宏可以摆脱数据类型的限制利用宏构建通用函数框架 函数模版函数模版的定义函数模版的使用函数模版的分析实例化函数模版的条件 函数模版扩展二次编译隐式推断类型实参函数模版的重载 bilibili 学习网址:https://www.bilibili.com/…...
C 语言学习笔记(函数2)
内容提要 函数 函数的调用函数的声明函数的嵌套关系函数的递归调用数组做函数参数 函数 函数的调用 调用方式 ①函数语句: test (); //对于无返回值的函数,直接调用 int res max(2,4); //对于有返回值的函数,一般需要在主调函…...
Spring的后置处理器是干什么用的?扩展点又是什么?
Spring 的后置处理器和扩展点是其框架设计的核心机制,它们为开发者提供了灵活的扩展能力,允许在 Bean 的生命周期和容器初始化过程中注入自定义逻辑。 1. 后置处理器(Post Processors) 后置处理器是 Spring 中用于干预 Bean 生命…...
Java大数据机器学习模型在金融衍生品风险建模中的创新实践
摘要 本文深入探讨Java技术栈在大数据与机器学习领域的独特优势,及其在金融衍生品风险建模中的突破性应用。通过分析分布式计算框架与机器学习库的整合方案,揭示Java在构建复杂金融风险模型时的技术可行性。结合信用违约互换(CDS)…...
leetcode3403. 从盒子中找出字典序最大的字符串 I-medium
1 题目:从盒子中找出字典序最大的字符串 I 官方标定难度: 给你一个字符串 word 和一个整数 numFriends。 Alice 正在为她的 numFriends 位朋友组织一个游戏。游戏分为多个回合,在每一回合中: word 被分割成 numFriends 个 非空…...
Effective C++阅读笔记(item 1-4)
文章目录 理解模板类型推导理解auto类型推导理解decltype学会查看类型推导结果 理解模板类型推导 c的auto特性是建立在模板类型推到的基础上。坏消息是当模板类型推导规则应用于auto环境时,有时不如应用于template时那么直观。我们可能很自然的期望T和传递进函数的…...
python自学笔记4 控制结构
条件语句 略 循环语句 略 range函数 enumerate() 函数 可以将一个可迭代对象转换为一个由索引和元素组成的枚举对象。 索引的起始编号是0,也可以传入第二参数来指定其起始编号 zip函数 打包范围以两者最短的长度为准 以两者较长的长度为准的函数为itertool…...
VTK|显示三维图像的二维切片
参考: VTK显示三维图像的二维切片 文章目录 实现类头文件实现类源文件如何调用项目git链接 以中心点坐标横切面 实现类头文件 /*** file MeshSliceController.h* brief 该头文件定义了 MeshSliceController 类,用于显示切面图。* details 该类负责处理与…...
day 30
模块和库的导入 导入官方库 标准导入:导入整个库 直接使用import语句 # 方式1:导入整个模块 import mathprint("方式1:使用 import math") print(f"圆周率π的值:{math.pi}") print(f"2的平方根…...
Linux云计算训练营笔记day11【Linux CentOS7(cat、less、head、tail、lscpu、lsblk、hostname、vim、which、mount、alias)】
Linux云计算 云计算是一种服务,是通过互联网按需提供计算资源的服务模式 程序员写代码的,部署上线项目 买服务器(一台24小时不关机的电脑,为客户端提供服务) 20万 买更多的服务器 Linux(命令) windows(图形化) 就业岗位: 云计算工程师 li…...
使用Python和FastAPI构建网站爬虫:Oncolo医疗文章抓取实战
使用Python和FastAPI构建网站爬虫:Oncolo医疗文章抓取实战 前言项目概述技术栈代码分析1. 导入必要的库2. 初始化FastAPI应用3. 定义请求模型4. 核心爬虫功能4.1 URL验证和准备4.2 设置HTTP请求4.3 发送请求和解析HTML4.4 提取文章内容4.5 保存结果和返回数据 5. AP…...
光纤克尔非线性效应及其在光通信系统中的补偿教程-3.2 克尔效应
需要结合上一期的文章,光纤克尔非线性效应及其在光通信系统中的补偿教程-3.1 非线性极化性 光纤中的非线性效应源于三阶感性 χ ( 3 ) \chi^{(3)} χ(3)。 光纤中非线性效应的主要来源之一是由 χ ( 3 ) \chi^{(3)} χ(3)引起的非线性折射,即克尔效应&a…...
【Tools】VMware Workstation 17.6 Pro安装教程
00. 目录 文章目录 00. 目录01. VMware Workstation 17.6简介02. VMware Workstation 17.6新功能03. VMware Workstation 17.6特性04. VMware Workstation 17.6下载05. VMware Workstation 17.6安装06. VMware Fusion 和 Workstation免费07. 附录 01. VMware Workstation 17.6简…...
Unity10分钟回顾指南
🎮 Unity10分钟回顾指南 欢迎踏上Unity场景创作之旅!本教程将带你从零开始,循序渐进地掌握Unity场景制作的全部技能。无论你是游戏开发爱好者还是专业开发者,这份指南都将成为你的得力助手。 第一章:Unity基础认知 1.…...
SeleniumBase - 多合一浏览器自动化框架
手动编写Selenium脚本,繁琐且常遇“掉坑”?SeleniumBase来救场!这款基于Selenium的Python框架集测试、爬虫、RPA于一体,支持多浏览器、并行测试、CAPTCHA绕过和智能等待,堪称Web自动化的“瑞士军刀”。不少行业大佬盛赞…...
【人工智能导论】第2.3章知识表示、确定性推理
1、李明的父亲是教师,用谓词逻辑可以表示为Teacher(father(Liming))这里father(Liming)是( ) A、常量 LIMING B、变元 X未知的可取多个值的对象 C、函数 X的父亲 D、一元…...
【QT】一个界面中嵌入其它界面(一)
在 Qt 中嵌入其他界面通常可以通过以下几种方式实现。以下是详细的步骤说明和示例代码: 方法 1:直接通过布局嵌入子部件 如果目标界面是 QWidget 的子类,可以直接将其添加到父窗口的布局中。 步骤: 创建子界面类: //…...
[学习]POSIX消息队列的原理与案例分析(完整示例代码)
POSIX消息队列的原理与案例分析 文章目录 POSIX消息队列的原理与案例分析摘要关键词一、引言1.1 研究背景与意义1.2 国内外研究现状1.3 研究内容与方法 二、POSIX消息队列的基本原理2.1 消息队列概述2.2 POSIX消息队列的特性2.2 POSIX消息队列的特性2.3 POSIX消息队列的内部机制…...
IDC数据中心动力环境监控系统解决方案
文档围绕 IDC 数据中心动力环境监控系统解决方案展开,先介绍数据中心分级,包括国家规范的 A/B/C 级和美国 TIA-942 标准的 Tier1-Tier4 级,强调动环监控对数据中心的重要性。接着阐述系统架构,涵盖底端设备层、采集层、接入层、服务层、应用层,具备数据采集、分析、可视化…...
WebSphere Application Server(WAS)8.5.5教程第五讲
续前篇! 一、Web 应用部署与类加载策略 Web 应用部署与类加载策略是 WebSphere Application Server(WAS)日常管理的核心部分,尤其对运行大型企业级 Java 应用(如 BAW)非常关键。本讲将分两部分讲解&#…...
Golang中的runtime.LockOSThread 和 runtime.UnlockOSThread
在runtime中有runtime.LockOSThread 和 runtime.UnlockOSThread 两个函数,这两个函数有什么作用呢?我们看一下标准库中对它们的解释。 runtime.LockOSThread // LockOSThread wires the calling goroutine to its current operating system thread. // T…...
计算圆周率 (python)
使用模特卡罗方法(模拟法),模拟撒点100000次,计算圆周率π 输入格式: 一个整数,表示随机数种子 输出格式: 计算的π值,结果小数点后保留5位数字 输入样例: 在这里给出一组输入。例如: 10…...
机器学习EM算法原理及推导
在机器学习与统计推断中,我们经常会遇到“缺失数据”或“潜在变量”(latent variables)的情形:样本并非完全可观测,而部分信息被隐藏或丢失。这种情况下,直接对观测数据做极大似然估计(Maximum …...
Linux项目部署全攻略:从环境搭建到前后端部署实战
Linux项目部署全攻略:从环境搭建到前后端部署实战 注:根据黑马程序员javawebAI视频课程总结: 视频地址 详细讲义地址 一、Linux基础入门:为什么选择Linux? 要成为一名Java开发工程师,掌握Linux是企业级…...
人工智能重塑医疗健康:从辅助诊断到个性化治疗的全方位变革
人工智能正在以前所未有的速度改变着医疗健康领域,从影像诊断到药物研发,从医院管理到远程医疗,AI 技术已渗透到医疗服务的各个环节。本文将深入探讨人工智能如何赋能医疗健康产业,分析其在医学影像、临床决策、药物研发、个性化医…...
ubuntu系统 | dify+ollama+deepseek搭建本地应用
1、安装 Ollama 下载并安装 Ollama (llm) wangqiangwangqiang:~$ curl -fsSL https://ollama.ai/install.sh | bash >>> Installing ollama to /usr/local >>> Downloading Linux amd64 bundle0.3% curl -fsSL https://ollama.ai/install.sh (下…...
NHANES最新指标推荐:C-DII
文章题目:Non-linear relationship between the childrens dietary inflammatory index and asthma risk: identifying a critical inflection point in US children and adolescents DOI:10.3389/fnut.2025.1538378 中文标题:儿童饮食炎症指…...
【PhysUnits】4.4 零类型(Z0)及其算术运算(zero.rs)
一、源码 该代码定义了一个类型系统中的零类型Z0,并为其实现了基本的算术运算(加法、减法、乘法、除法)。这是一个典型的类型级编程示例,使用Rust的类型系统在编译期进行数学运算。 //! 零类型(Z0)及其算术运算实现 //! //! 本…...
【大模型面试每日一题】Day 23:如何设计一个支持多模态(文本+图像)的大模型架构?
【大模型面试每日一题】Day 23:如何设计一个支持多模态(文本图像)的大模型架构? 📌 题目重现 🌟🌟🌟 面试官:我们需要构建一个同时处理文本和图像的多模态大模型&#…...
BUUCTF PWN刷题笔记(持续更新!!)
ciscn_2019_c_1 64位,没有开启保护。点进去没发现明显的漏洞函数,考虑泄露libc基地址的rop构造。先看看有多少gadget 估计也够用了。puts函数只接受一个参数,观看汇编看看用的哪个寄存器传输的参数。 用的是edi。但是我们怎么找到so的版本呢…...
Tare使用MCP|Win11安装UV
servers/src/git at main modelcontextprotocol/servers GitHub 进入Installation | uv 打开powershell 运行 (如果要删除 文章中也有删除的链接) powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" …...
小白的进阶之路-人工智能从初步到精通pytorch的基本流程详解-1
Pytorch大致流程一览表: 主题内容1 准备数据数据几乎可以是任何东西,但在开始之前,我们将创建一条简单的直线2 建立模型在这里,我们将创建一个模型来学习数据中的模式,我们还将选择损失函数,优化器并构建一个训练循环。3 拟合模型我们已经有了数据和模型,现在让模型尝试…...
linux kernel 编译
1. 获取内核源码 从官方仓库获取: 访问kernel.org下载所需版本的Linux内核源码压缩包(如linux-x.y.z.tar.xz)。或者使用Git克隆特定版本: bash git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.gitcd linux-stablegit checkout vX.Y.Z # 切…...
【氮化镓】偏置对GaN HEMT 单粒子效应的影响
2025年5月19日,西安电子科技大学的Ling Lv等人在《IEEE Transactions on Electron Devices》期刊发表了题为《Single-Event Effects of AlGaN/GaN HEMTs Under Different Biases》的文章,基于实验和TCAD仿真模拟方法,研究了单粒子效应对关断状态、半开启状态和开启状态下AlG…...
Assistants API
一、前言 0.1、从轰动一时的 OpenAI DevDay 说起 2023 年 11 月 6 日,OpenAI DevDay 发表了一系列新能力,其中包括:GPT Store 和 Assistants API 这一波操作一度被认为是创业公司终结者 0.2、GPTs 和 Assistants API 本质是降低开发门槛 可操控性和易用性之间的权衡与折中…...
AcWing 223. 阿九大战朱最学——扩展欧几里得算法
题目来源 223. 阿九大战朱最学 - AcWing题库 题目描述 自从朱最学搞定了 QQ 农场以后,就开始捉摸去 QQ 牧场干些事业,不仅在自己的牧场养牛,还到阿九的牧场放牛! 阿九很生气,有一次朱最学想知道阿九牧场奶牛的数量…...
开发指南116-font-size: 0的使用
平台前台的css样式里有几个地方用到了font-size: 0,这是个使用小技巧。原理说明:font-size 属性用于定义元素中文本的大小。当设置 font-size: 0 时,意味着该元素内的文本将不占据空间。当元素的 font-size 设置为零时,该元素内的…...
算法-数对的使用
1、数对可用于数组排序中,并且可记忆化排序前的元素下标 #include<iostream> #include<string> #include<bits/stdc.h> using namespace std; typedef long long ll; const int N 2e5 10; pair<int, int> a[N]; void solve() {ll n;cin …...
EmoBox:我与 CodeBuddy 共创的 Emoji 表情分类小工具
我正在参加CodeBuddy「首席试玩官」内容创作大赛,本文所使用的 CodeBuddy 免费下载链接:腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 最近我萌生了一个想法,想做一个小而美的工具——一个叫「EmoBox」的 emoji 表情分类应用࿰…...