安卓逆向之第二代:函数抽取型壳

admin 2024年12月5日12:01:45评论9 views字数 21443阅读71分28秒阅读模式

#WingBy小密圈知识星球简介在文末。

前言:本文是通过学习https://bbs.kanxue.com/thread-271139.htm,并结合自己的理解所完成的。

函数抽取型壳的核心思想是对Dex文件中的函数进行单独保护。与整体型壳不同,函数抽取型壳并不保护整个Dex文件,而是针对每个方法(尤其是关键方法)的字节码进行保护。这种保护方式通常采用将函数字节码提取(NOP)并加密或压缩后存储,再在运行时解密回填到Dex文件中。

工作原理如下

  • 加壳时:对于每个函数的字节码(CodeItem),进行加密或者压缩处理,并将加密后的字节码存储在一个安全区域。当程序加载时,只有特定的函数在需要执行时会解密或动态加载对应的字节码。
  • 脱壳时:攻击者需要逐个解析每个函数的字节码,在程序运行时找到相应的字节码区域,并通过特定的解密算法或机制将其还原。

加壳过程如下

首先是处理AndroidManifest.xml,让app开始运行的时候是以代理类的形式运行的,所以需要备份原Application的类名和写入壳的代理Application的类名

首先获取application名  封装到了getApplicationName中 通过axmlParser分析xml

public static String getValue(String file,String tag,String ns,String attrName){ byte[] axmlData = IoUtils.readFile(file); AxmlParser axmlParser = new AxmlParser(axmlData); try { while (axmlParser.next() != AxmlParser.END_FILE) { if (axmlParser.getAttrCount() != 0 && !axmlParser.getName().equals(tag)) { continue; } for (int i = 0; i < axmlParser.getAttrCount(); i++) { if (axmlParser.getNamespacePrefix().equals(ns) && axmlParser.getAttrName(i).equals(attrName)) { return (String) axmlParser.getAttrValue(i); } } } } catch (Exception e) { e.printStackTrace(); } return null; } public static String getApplicationName(String file) { return getValue(file,"application","android","name"); }

对application进行修改  通过 addApplicationAttribute  给applicationname设置新的名字 也就是我们的代理application

public static void writeApplicationName(String inManifestFile, String outManifestFile, String newApplicationName){ ModificationProperty property = new ModificationProperty(); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME,newApplicationName)); FileProcesser.processManifestFile(inManifestFile, outManifestFile, property); }

然后抽掉函数,所谓抽掉函数,就是抽掉了函数的字节码,也就是CodeItem中的insns。

逻辑就是:首先读取dex,然后遍历dex中的classdef,再遍历classdef中的method也就是函数,然后获取他的字节码,替换字节码为nop指令

public static List<Instruction> extractAllMethods(File dexFile, File outDexFile) { List<Instruction> instructionList = new ArrayList<>(); Dex dex = null; RandomAccessFile randomAccessFile = null; byte[] dexData = IoUtils.readFile(dexFile.getAbsolutePath()); IoUtils.writeFile(outDexFile.getAbsolutePath(),dexData); try { dex = new Dex(dexFile); randomAccessFile = new RandomAccessFile(outDexFile, "rw"); Iterable<ClassDef> classDefs = dex.classDefs(); for (ClassDef classDef : classDefs) { ...... if(classDef.getClassDataOffset() == 0){ String log = String.format("class '%s' data offset is zero",classDef.toString()); logger.warn(log); continue; } ClassData classData = dex.readClassData(classDef); ClassData.Method[] directMethods = classData.getDirectMethods(); ClassData.Method[] virtualMethods = classData.getVirtualMethods(); for (ClassData.Method method : directMethods) { Instruction instruction = extractMethod(dex,randomAccessFile,classDef,method); if(instruction != null) { instructionList.add(instruction); } } for (ClassData.Method method : virtualMethods) { Instruction instruction = extractMethod(dex, randomAccessFile,classDef, method); if(instruction != null) { instructionList.add(instruction); } } } } catch (Exception e){ e.printStackTrace(); } finally { IoUtils.close(randomAccessFile); } return instructionList; }

代码解读

初始化和读取文件 这部分代码读取了原始的Dex文件并将其内容写入到目标输出Dex文件(outDexFile)中。

byte[] dexData = IoUtils.readFile(dexFile.getAbsolutePath()); IoUtils.writeFile(outDexFile.getAbsolutePath(), dexData);

创建Dex对象 通过Dex类加载原始的Dex文件,Dex类是对Dex文件的一个封装,提供了访问Dex文件各个部分的方法。

dex = new Dex(dexFile);

遍历所有的类 dex.classDefs()返回一个迭代器,用于遍历Dex文件中所有的类定义(ClassDef)。

Iterable<ClassDef> classDefs = dex.classDefs(); for (ClassDef classDef : classDefs) { ... }

检查并跳过无效的类 对于没有类数据(classDataOffset == 0)的类,直接跳过。

if(classDef.getClassDataOffset() == 0){ String log = String.format("class '%s' data offset is zero", classDef.toString()); logger.warn(log); continue; }

读取类的数据 通过readClassData方法读取类的具体数据,获取该类的直接方法(directMethods)和虚拟方法(virtualMethods)。

ClassData classData = dex.readClassData(classDef); ClassData.Method[] directMethods = classData.getDirectMethods(); ClassData.Method[] virtualMethods = classData.getVirtualMethods();

提取方法的字节码 对每个直接方法和虚拟方法,调用extractMethod方法(函数逻辑后面解读)提取该方法的字节码,并将其封装为Instruction对象。有效的Instruction对象会被添加到instructionList中。

for (ClassData.Method method : directMethods) { Instruction instruction = extractMethod(dex, randomAccessFile, classDef, method); if(instruction != null) { instructionList.add(instruction); } } for (ClassData.Method method : virtualMethods) { Instruction instruction = extractMethod(dex, randomAccessFile, classDef, method); if(instruction != null) { instructionList.add(instruction); } }

错误处理和资源释放 捕获异常,最后关闭randomAccessFile。

catch (Exception e) { e.printStackTrace(); } finally { IoUtils.close(randomAccessFile); }

返回结果 最终返回一个Instruction列表,包含了从Dex文件中提取的所有方法的字节码信息。

return instructionList;

extractMethod自定义函数就是用来处理每一个要抽取的函数方法的

private static Instruction extractMethod(Dex dex ,RandomAccessFile outRandomAccessFile,ClassDef classDef,ClassData.Method method) throws Exception{ String returnTypeName = dex.typeNames().get(dex.protoIds().get(dex.methodIds().get(method.getMethodIndex()).getProtoIndex()).getReturnTypeIndex()); String methodName = dex.strings().get(dex.methodIds().get(method.getMethodIndex()).getNameIndex()); String className = dex.typeNames().get(classDef.getTypeIndex()); //native函数 if(method.getCodeOffset() == 0){ String log = String.format("method code offset is zero,name = %s.%s , returnType = %s", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName)); logger.warn(log); return null; } Instruction instruction = new Instruction(); //16 = registers_size + ins_size + outs_size + tries_size + debug_info_off + insns_size int insnsOffset = method.getCodeOffset() + 16; Code code = dex.readCode(method); //容错处理 if(code.getInstructions().length == 0){ String log = String.format("method has no code,name = %s.%s , returnType = %s", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName)); logger.warn(log); return null; } int insnsCapacity = code.getInstructions().length; //insns容量不足以存放return语句,跳过 byte[] returnByteCodes = getReturnByteCodes(returnTypeName); if(insnsCapacity * 2 < returnByteCodes.length){ logger.warn("The capacity of insns is not enough to store the return statement. {}.{}() -> {} insnsCapacity = {}byte(s),returnByteCodes = {}byte(s)", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName), insnsCapacity * 2, returnByteCodes.length); return null; } instruction.setOffsetOfDex(insnsOffset); //这里的MethodIndex对应method_ids区的索引 instruction.setMethodIndex(method.getMethodIndex()); //注意:这里是数组的大小 instruction.setInstructionDataSize(insnsCapacity * 2); byte[] byteCode = new byte[insnsCapacity * 2]; //写入nop指令 for (int i = 0; i < insnsCapacity; i++) { outRandomAccessFile.seek(insnsOffset + (i * 2)); byteCode[i * 2] = outRandomAccessFile.readByte(); byteCode[i * 2 + 1] = outRandomAccessFile.readByte(); outRandomAccessFile.seek(insnsOffset + (i * 2)); outRandomAccessFile.writeShort(0); } instruction.setInstructionsData(byteCode); outRandomAccessFile.seek(insnsOffset); //写出return语句 outRandomAccessFile.write(returnByteCodes); return instruction; }

代码解读

获取方法信息:从Dex文件中提取当前方法的返回类型、方法名以及类名。通过method.getMethodIndex()获取方法的索引,再通过多层查找得到对应的返回类型、方法名和类名。

String returnTypeName = dex.typeNames().get(dex.protoIds().get(dex.methodIds().get(method.getMethodIndex()).getProtoIndex()).getReturnTypeIndex()); String methodName = dex.strings().get(dex.methodIds().get(method.getMethodIndex()).getNameIndex()); String className = dex.typeNames().get(classDef.getTypeIndex());

检查Native方法 如果方法没有代码(codeOffset == 0),通常是原生(native)方法或没有字节码的函数,直接跳过并返回null。

if(method.getCodeOffset() == 0){ String log = String.format("method code offset is zero,name = %s.%s , returnType = %s", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName)); logger.warn(log); return null; }

准备Instruction对象 创建一个Instruction对象,用于保存当前方法的字节码信息。insnsOffset计算的是指令数据的偏移位置,dex.readCode(method)用于读取方法的字节码。

Instruction instruction = new Instruction(); int insnsOffset = method.getCodeOffset() + 16; Code code = dex.readCode(method);

容错处理 如果读取到的字节码为空,则认为该方法没有有效的字节码,跳过处理并返回null

if(code.getInstructions().length == 0){ String log = String.format("method has no code,name = %s.%s , returnType = %s", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName)); logger.warn(log); return null; }

检查字节码容量是否足够 如果字节码容量不足以存储返回语句(return语句),则跳过当前方法的处理,返回null。

int insnsCapacity = code.getInstructions().length; byte[] returnByteCodes = getReturnByteCodes(returnTypeName); if(insnsCapacity * 2 < returnByteCodes.length){ logger.warn("The capacity of insns is not enough to store the return statement. {}.{}() -> {} insnsCapacity = {}byte(s),returnByteCodes = {}byte(s)", TypeUtils.getHumanizeTypeName(className), methodName, TypeUtils.getHumanizeTypeName(returnTypeName), insnsCapacity * 2, returnByteCodes.length); return null; }

设置Instruction对象的属性 设置Instruction对象的偏移、方法索引和字节码数据的大小。

instruction.setOffsetOfDex(insnsOffset); instruction.setMethodIndex(method.getMethodIndex()); instruction.setInstructionDataSize(insnsCapacity * 2);

提取原字节码并替换为NOP指令 提取原方法的字节码并将其保存到byteCode数组中。此处通过outRandomAccessFile读取原字节码,然后将其替换为NOP指令(writeShort(0))。

byte[] byteCode = new byte[insnsCapacity * 2]; for (int i = 0; i < insnsCapacity; i++) { outRandomAccessFile.seek(insnsOffset + (i * 2)); byteCode[i * 2] = outRandomAccessFile.readByte(); byteCode[i * 2 + 1] = outRandomAccessFile.readByte(); outRandomAccessFile.seek(insnsOffset + (i * 2)); outRandomAccessFile.writeShort(0); } instruction.setInstructionsData(byteCode);

写入return语句 将return语句写入到字节码的原位置。returnByteCodes是根据方法的返回类型生成的字节码(例如对于int类型,return语句的字节码可能是0x12,表示返回一个整数值)。

outRandomAccessFile.seek(insnsOffset); outRandomAccessFile.write(returnByteCodes);

返回Instruction对象 返回Instruction对象,包含了方法的字节码信息及其修改后的数据。

return instruction;

APP运行的时候需要进行脱壳,主要Hook两个函数:MapFileAtAddress和LoadMethod。MapFileAtAddress函数的目的是在我们加载dex能够修改dex的属性,让加载的dex可写,这样我们才能把字节码填回dex,用dobby框架进行hook

void* MapFileAtAddressAddr = DobbySymbolResolver(GetArtLibPath(),MapFileAtAddress_Sym()); DobbyHook(MapFileAtAddressAddr, (void *) MapFileAtAddress28,(void **) &g_originMapFileAtAddress28);

然后添加可写属性(PROT_WRITE)

void* MapFileAtAddress28(uint8_t* expected_ptr, size_t byte_count, int prot, int flags, int fd, off_t start, bool low_4gb, bool reuse, const char* filename, std::string* error_msg){ int new_prot = (prot | PROT_WRITE); if(nullptr != g_originMapFileAtAddress28) { return g_originMapFileAtAddress28(expected_ptr,byte_count,new_prot,flags,fd,start,low_4gb,reuse,filename,error_msg); } }

loadmethod

当一个类被加载的时候,它的调用链是这样的,也就是说,当一个类被加载,它是会去调用LoadMethod函数的,我们看一下它的函数原型:

ClassLoader.java::loadClass -> DexPathList.java::findClass -> DexFile.java::defineClass -> class_linker.cc::LoadClass -> class_linker.cc::LoadClassMembers -> class_linker.cc::LoadMethod

他有两个重要的参数DexFile和ClassDataItemIterator dexfile可以知道当前加载的函数,所在的类的所在的dex的信息

void ClassLinker::LoadMethod(const DexFile& dex_file, const ClassDataItemIterator& it, Handle<mirror::Class> klass, ArtMethod* dst);

ClassDataItemIterator结构如下

class ClassDataItemIterator{ ...... // A decoded version of the method of a class_data_item struct ClassDataMethod { uint32_t method_idx_delta_; // delta of index into the method_ids array for MethodId uint32_t access_flags_; uint32_t code_off_; ClassDataMethod() : method_idx_delta_(0), access_flags_(0), code_off_(0) {} private: DISALLOW_COPY_AND_ASSIGN(ClassDataMethod); }; ClassDataMethod method_; // Read and decode a method from a class_data_item stream into method void ReadClassDataMethod(); const DexFile& dex_file_; size_t pos_; // integral number of items passed const uint8_t* ptr_pos_; // pointer into stream of class_data_item uint32_t last_idx_; // last read field or method index to apply delta to DISALLOW_IMPLICIT_CONSTRUCTORS(ClassDataItemIterator); };

其中最重要的参数是code_off_,它的值是当前加载的函数的CodeItem相对于DexFile的偏移。也就是说我们通过loadmethod可以知道函数字节码真正所在的位置。hook住loadmethod后让他去执行我们自定义的loadmethod

void LoadMethod(void *thiz, void *self, const void *dex_file, const void *it, const void *method, void *klass, void *dst) { if (g_originLoadMethod25 != nullptr || g_originLoadMethod28 != nullptr || g_originLoadMethod29 != nullptr) { uint32_t location_offset = getDexFileLocationOffset(); uint32_t begin_offset = getDataItemCodeItemOffset(); callOriginLoadMethod(thiz, self, dex_file, it, method, klass, dst); ClassDataItemReader *classDataItemReader = getClassDataItemReader(it,method); uint8_t **begin_ptr = (uint8_t **) ((uint8_t *) dex_file + begin_offset); uint8_t *begin = *begin_ptr; // vtable(4|8) + prev_fields_size std::string *location = (reinterpret_cast<std::string *>((uint8_t *) dex_file + location_offset)); if (location->find("base.apk") != std::string::npos) { //code_item_offset == 0说明是native方法或者没有代码 if (classDataItemReader->GetMethodCodeItemOffset() == 0) { DLOGW("native method? = %s code_item_offset = 0x%x", classDataItemReader->MemberIsNative() ? "true" : "false", classDataItemReader->GetMethodCodeItemOffset()); return; } uint16_t firstDvmCode = *((uint16_t*)(begin + classDataItemReader->GetMethodCodeItemOffset() + 16)); if(firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){ NLOG("this method has code no need to patch"); return; } uint32_t dexSize = *((uint32_t*)(begin + 0x20)); int dexIndex = dexNumber(location); auto dexIt = dexMap.find(dexIndex - 1); if (dexIt != dexMap.end()) { auto dexMemIt = dexMemMap.find(dexIndex); if(dexMemIt == dexMemMap.end()){ changeDexProtect(begin,location->c_str(),dexSize,dexIndex); } auto codeItemMap = dexIt->second; int methodIdx = classDataItemReader->GetMemberIndex(); auto codeItemIt = codeItemMap->find(methodIdx); if (codeItemIt != codeItemMap->end()) { CodeItem* codeItem = codeItemIt->second; uint8_t *realCodeItemPtr = (uint8_t*)(begin + classDataItemReader->GetMethodCodeItemOffset() + 16); memcpy(realCodeItemPtr,codeItem->getInsns(),codeItem->getInsnsSize()); } } } } }

代码解读

检查是否需要调用原始 LoadMethod 函数 这行检查是否有一个原始的 LoadMethod 函数存在。如果 g_originLoadMethod25、g_originLoadMethod28 或 g_originLoadMethod29 中任何一个不为 nullptr,表示系统可能会使用这些原始函数来加载方法。

if (g_originLoadMethod25 != nullptr || g_originLoadMethod28 != nullptr || g_originLoadMethod29 != nullptr) {

获取 Dex 文件中的一些偏移量 location_offset 和 begin_offset 分别是 Dex 文件中某些数据项的偏移量,可能指向文件中的方法或代码区域。

uint32_t location_offset = getDexFileLocationOffset(); uint32_t begin_offset = getDataItemCodeItemOffset();

调用原始 LoadMethod 函数 这行代码调用原始的 LoadMethod 函数(具体由 g_originLoadMethod25、g_originLoadMethod28 或 g_originLoadMethod29 指定),将当前参数传递给它以进行方法加载。

callOriginLoadMethod(thiz, self, dex_file, it, method, klass, dst);

获取并检查 ClassDataItemReader ClassDataItemReader 是一个用于读取类数据项的对象,负责从 Dex 文件中读取特定方法的相关数据。

ClassDataItemReader *classDataItemReader = getClassDataItemReader(it, method);

检查 Dex 文件位置并处理 base.apk 该段代码检查 Dex 文件的位置是否包含 "base.apk",这是 Android 应用程序的主要 APK 文件。如果是,继续执行以下处理。

std::string *location = (reinterpret_cast<std::string *>((uint8_t *) dex_file + location_offset)); if (location->find("base.apk") != std::string::npos) {

处理 native 方法或没有代码的方法 如果方法的代码项偏移量为 0,则说明该方法是一个 native 方法或没有代码。此时,函数会输出日志并返回,不进行后续操作。

if (classDataItemReader->GetMethodCodeItemOffset() == 0) { DLOGW("native method? = %s code_item_offset = 0x%x", classDataItemReader->MemberIsNative() ? "true" : "false", classDataItemReader->GetMethodCodeItemOffset()); return; }

检查并修复方法代码 检查方法代码的第一个字节(通过 classDataItemReader->GetMethodCodeItemOffset() 获取偏移量)。如果该字节不是特定的值(0x0012、0x0016 或 0x000e),则说明方法的代码不需要修补,直接返回。

uint16_t firstDvmCode = *((uint16_t*)(begin + classDataItemReader->GetMethodCodeItemOffset() + 16)); if(firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){ NLOG("this method has code no need to patch"); return; }

获取 Dex 文件大小并处理代码项 获取 Dex 文件的大小,并根据 location 查找对应的 Dex 索引。

uint32_t dexSize = *((uint32_t*)(begin + 0x20)); int dexIndex = dexNumber(location); auto dexIt = dexMap.find(dexIndex - 1); if (dexIt != dexMap.end()) { ... }

修补方法代码 如果 dexMemIt 不存在,则更改 Dex 文件的保护属性,允许修改其内容。

if(dexMemIt == dexMemMap.end()){ changeDexProtect(begin, location->c_str(), dexSize, dexIndex); }

从 Dex 文件中获取并修补方法代码 如果找到对应的 codeItem,则将方法的代码(指令)从 Dex 文件中的 CodeItem 复制到 realCodeItemPtr 指向的内存位置,从而修补方法的代码。

auto codeItemMap = dexIt->second; int methodIdx = classDataItemReader->GetMemberIndex(); auto codeItemIt = codeItemMap->find(methodIdx); if (codeItemIt != codeItemMap->end()) { CodeItem* codeItem = codeItemIt->second; uint8_t *realCodeItemPtr = (uint8_t*)(begin + classDataItemReader->GetMethodCodeItemOffset() + 16); memcpy(realCodeItemPtr, codeItem->getInsns(), codeItem->getInsnsSize()); }

修复了函数之后,接下来我们要重新加载dex, 其实,Dex 文件在 App 启动时已经被加载过一次,但为什么我们还需要再次加载呢?原因在于,系统加载的 Dex 文件是以只读的方式加载到内存中的,这意味着我们无法对这部分内存进行修改。而且,系统加载 Dex 的过程发生在 Application 启动之前,这时我们的代码还没有执行,因此无法感知到已经加载的 Dex 文件。所以,我们需要在应用启动后再次加载 Dex 文件,以便让应用能够访问和操作这些文件  

private ClassLoader loadDex(Context context){ String sourcePath = context.getApplicationInfo().sourceDir; String nativePath = context.getApplicationInfo().nativeLibraryDir; ShellClassLoader shellClassLoader = new ShellClassLoader(sourcePath,nativePath,ClassLoader.getSystemClassLoader()); return shellClassLoader; }

自定义的ClassLoader  这段代码定义了一个 ShellClassLoader 类,它继承自 PathClassLoader,并且重载了两个构造函数  

public class ShellClassLoader extends PathClassLoader { private final String TAG = ShellClassLoader.class.getSimpleName(); public ShellClassLoader(String dexPath,ClassLoader classLoader) { super(dexPath,classLoader); } public ShellClassLoader(String dexPath, String librarySearchPath,ClassLoader classLoader) { super(dexPath, librarySearchPath, classLoader); } }

替换dexElements

这一步是为了使ClassLoader从我们新加载的dex文件中加载类, 它把 oldClassLoader 和 newClassLoader 中的 Dex 元素(即 .dex 文件的路径)合并起来,形成一个新的 Dex 元素列表,并更新 oldClassLoader 中的 DexPathList。代码如下:

void mergeDexElements(JNIEnv* env,jclass klass,jobject oldClassLoader,jobject newClassLoader){ jclass BaseDexClassLoaderClass = env->FindClass("dalvik/system/BaseDexClassLoader"); jfieldID pathList = env->GetFieldID(BaseDexClassLoaderClass,"pathList","Ldalvik/system/DexPathList;"); jobject oldDexPathListObj = env->GetObjectField(oldClassLoader,pathList); if(env->ExceptionCheck() || nullptr == oldDexPathListObj ){ env->ExceptionClear(); DLOGW("mergeDexElements oldDexPathListObj get fail"); return; } jobject newDexPathListObj = env->GetObjectField(newClassLoader,pathList); if(env->ExceptionCheck() || nullptr == newDexPathListObj){ env->ExceptionClear(); DLOGW("mergeDexElements newDexPathListObj get fail"); return; } jclass DexPathListClass = env->FindClass("dalvik/system/DexPathList"); jfieldID dexElementField = env->GetFieldID(DexPathListClass,"dexElements","[Ldalvik/system/DexPathList$Element;"); jobjectArray newClassLoaderDexElements = static_cast<jobjectArray>(env->GetObjectField( newDexPathListObj, dexElementField)); if(env->ExceptionCheck() || nullptr == newClassLoaderDexElements){ env->ExceptionClear(); DLOGW("mergeDexElements new dexElements get fail"); return; } jobjectArray oldClassLoaderDexElements = static_cast<jobjectArray>(env->GetObjectField( oldDexPathListObj, dexElementField)); if(env->ExceptionCheck() || nullptr == oldClassLoaderDexElements){ env->ExceptionClear(); DLOGW("mergeDexElements old dexElements get fail"); return; } jint oldLen = env->GetArrayLength(oldClassLoaderDexElements); jint newLen = env->GetArrayLength(newClassLoaderDexElements); DLOGD("mergeDexElements oldlen = %d , newlen = %d",oldLen,newLen); jclass ElementClass = env->FindClass("dalvik/system/DexPathList$Element"); jobjectArray newElementArray = env->NewObjectArray(oldLen + newLen,ElementClass, nullptr); for(int i = 0;i < newLen;i++) { jobject elementObj = env->GetObjectArrayElement(newClassLoaderDexElements, i); env->SetObjectArrayElement(newElementArray,i,elementObj); } for(int i = newLen;i < oldLen + newLen;i++) { jobject elementObj = env->GetObjectArrayElement(oldClassLoaderDexElements, i - newLen); env->SetObjectArrayElement(newElementArray,i,elementObj); } env->SetObjectField(oldDexPathListObj, dexElementField,newElementArray); DLOGD("mergeDexElements success"); }

代码解读

获取 BaseDexClassLoader 类及其 pathList 字段 首先,通过 JNI FindClass 方法获取 dalvik/system/BaseDexClassLoader 类,这个类是 Android 中加载 .dex 文件的基类。然后,通过 GetFieldID 获取 BaseDexClassLoader 类中的 pathList 字段,该字段是 DexPathList 类型,存储了与 .dex 文件相关的元素。

jclass BaseDexClassLoaderClass = env->FindClass("dalvik/system/BaseDexClassLoader"); jfieldID pathList = env->GetFieldID(BaseDexClassLoaderClass, "pathList", "Ldalvik/system/DexPathList;");

获取 oldClassLoader 和 newClassLoader 的 pathList

jobject oldDexPathListObj = env->GetObjectField(oldClassLoader, pathList); if (env->ExceptionCheck() || nullptr == oldDexPathListObj) { env->ExceptionClear(); DLOGW("mergeDexElements oldDexPathListObj get fail"); return; } jobject newDexPathListObj = env->GetObjectField(newClassLoader, pathList); if (env->ExceptionCheck() || nullptr == newDexPathListObj) { env->ExceptionClear(); DLOGW("mergeDexElements newDexPathListObj get fail"); return; }

获取 DexPathList 类,以及它的 dexElements 字段,这个字段存储的是 DexPathList$Element 类型的数组,表示 .dex 文件的元素。

jclass DexPathListClass = env->FindClass("dalvik/system/DexPathList"); jfieldID dexElementField = env->GetFieldID(DexPathListClass, "dexElements", "[Ldalvik/system/DexPathList$Element;");

获取 oldClassLoader 和 newClassLoader 的 dexElements

jobjectArray newClassLoaderDexElements = static_cast<jobjectArray>(env->GetObjectField(newDexPathListObj, dexElementField)); if (env->ExceptionCheck() || nullptr == newClassLoaderDexElements) { env->ExceptionClear(); DLOGW("mergeDexElements new dexElements get fail"); return; } jobjectArray oldClassLoaderDexElements = static_cast<jobjectArray>(env->GetObjectField(oldDexPathListObj, dexElementField)); if (env->ExceptionCheck() || nullptr == oldClassLoaderDexElements) { env->ExceptionClear(); DLOGW("mergeDexElements old dexElements get fail"); return; }

合并 Dex 元素 获取 oldClassLoader 和 newClassLoader 的 dexElements 数组长度,分别存储在 oldLen 和 newLen 中。

jint oldLen = env->GetArrayLength(oldClassLoaderDexElements); jint newLen = env->GetArrayLength(newClassLoaderDexElements); DLOGD("mergeDexElements oldlen = %d , newlen = %d", oldLen, newLen);

创建一个新的 jobjectArray,长度为 oldLen + newLen,用于存储合并后的 Dex 元素。

ElementClass 是 DexPathList$Element 类,用于存储每个 Dex 元素。

jclass ElementClass = env->FindClass("dalvik/system/DexPathList$Element"); jobjectArray newElementArray = env->NewObjectArray(oldLen + newLen, ElementClass, nullptr);

遍历 newClassLoaderDexElements 数组,将其中的每个 Dex 元素复制到新的数组 newElementArray 中。

for (int i = 0; i < newLen; i++) { jobject elementObj = env->GetObjectArrayElement(newClassLoaderDexElements, i); env->SetObjectArrayElement(newElementArray, i, elementObj); }

遍历 oldClassLoaderDexElements 数组,将其中的每个 Dex 元素复制到新的数组 newElementArray 中,索引从 newLen 开始。

for (int i = newLen; i < oldLen + newLen; i++) { jobject elementObj = env->GetObjectArrayElement(oldClassLoaderDexElements, i - newLen); env->SetObjectArrayElement(newElementArray, i, elementObj); }

更新 oldDexPathListObj 中的 dexElements 字段 将合并后的 newElementArray 设置回 oldDexPathListObj 的 dexElements 字段。

env->SetObjectField(oldDexPathListObj, dexElementField, newElementArray); DLOGD("mergeDexElements success");

这样应用就可以正常使用函数了

我们的星球涵盖了红蓝攻防、企业/公益SRC、各类证书挖掘包括但不限于EDU、CNVD等、Java/PHP0Day代码审计、免杀对抗、云安全攻防、CTF、安全开发等等,还有很多高质量的文章,以及秘密工具。别的星球有的我们未必没有。

安卓逆向之第二代:函数抽取型壳

原文始发于微信公众号(Ting的安全笔记):安卓逆向之第二代:函数抽取型壳

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月5日12:01:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓逆向之第二代:函数抽取型壳https://cn-sec.com/archives/3470452.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息