Frida脱壳脚本解读原创

admin 2022年10月30日19:39:28Frida脱壳脚本解读原创已关闭评论432 views字数 9751阅读32分30秒阅读模式

仅适用于安卓 5 及以上版本(ART 模式)

安卓 4.4 以前使用的是 Dalvik 模式,两者在脱壳上有所区别。

壳可以分为几类#

加壳技术 技术特点 脱壳难度 说人话就是:
第一代:DEX 整体加密型壳 采用 Dex 整体加密,动态加载运行的机制 较容易被还原,通过自动化脱壳工具或脚本即可从内存中 dump 出 dex 文件 把 dex 隐藏起来
第二代:DEX 函数抽取型壳 粒度更细,将方法单独抽取出来,加密保存,解密执行 可以从根本上进行还原的,dump 出所有的运行时的方法体,填充到 dump 下来的 dex 中去的,这也是 fart 的核心原理 隐藏 dex 并删掉期中中部分方法
第三代:VMP、Dex2C 壳 独立虚拟机解释执行、语义等价语法迁移,强度最高 Dex2C 目前是没有办法还原的,只能跟踪进行分析; VMP 虚拟机解释执行保护的是映射表,只要心思细、功夫深,是可以将映射表还原的 虚拟机里跑虚拟机

不过目前遇到的大多是一代和二代壳,三代暂未遇到过。实际上,APP 加的壳越复杂,运行效率越低,像支付宝和微信,就从不加壳(因为他们的服务都在云端,本地的修改不影响云数据,所以加不加壳其实无所谓)

本次脱壳目标为一代壳。

加壳 APP 运行原理#

																			APP启动  
																				⬇️
																		先加载壳的dex  
																				⬇️
																壳读取源app的dex文件   
																				⬇️
																		解密源dex文件   
																				⬇️
																将解密后的dex载入内存 
																				⬇️
													通过映射表还原抽取的方法(二代壳需要)
																				⬇️
																		 源dex运行

三代壳是先运行虚拟机,再运行 dex。

脱壳原理#

目的:拿到可供分析的字节码(dex)

目标:内存中已解密的源 dex

那么如果已知解密的源 dex 的内存基址以及文件长度,那么拿到这个 dex 源码就简单多了。

翻一翻
安卓源码(戳我直达)
或者你自己动态调试一下安卓 App 的加载过程,就会发现,在 libart.so 中有一个函数 Openmemory 提供了 dex 加载,从这里可以获得源 dex 的内存基址(base)以及文件长度(size):

std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
                                                  size_t size,
                                                   const std::string& location,
                                                   uint32_t location_checksum,
                                                   MemMap* mem_map,
                                                  const OatDexFile* oat_dex_file,
                                                   std::string* error_msg) {
  CHECK_ALIGNED(base, 4);  // various dex file structures must be word aligned
  std::unique_ptr<DexFile> dex_file(
      new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
  if (!dex_file->Init(error_msg)) {
    dex_file.reset();
  }
  return std::unique_ptr<const DexFile>(dex_file.release());
}

Android 4.4 版本之前 系统函数在 libdvm.so

Android 5.0 之后 系统函数在 libart.so

Dex 文件头部格式#

dex 文件分三块:头部、索引区、数据区。此处只讲解头部格式。

img

文件头部格式:

struct header_item{
      ubyte[8] magic;//文件魔数 8字节
      unit checksum;//校验 4字节
      ubyte[20] siganature;//签名 20字节
      uint file_size;//文件大小 4字节
      uint header_size;//头文件大小
      unit endian_tag;//一般为固定值0x12345678
      uint link_size;//链接数据大小
      uint link_off;//链接数据偏移
      uint map_off;//data区的数据集合
      uint string_ids_size;//字符串数量
      uint string_ids_off;//字符串偏移
      uint type_ids_size;//所有类型数量
      uint type_ids_off;//类型偏移
      uint proto_ids_size;//方法参数信息数量
      uint proto_ids_off;//偏移
      uint method_ids_size;//方法信息数量
      uint method_ids_off;//偏移
      uint class_defs_size;//类信息个数
      uint class_defs_off;//偏移
      uint data_size;//数据大小
      uint data_off;//偏移
}

此处主要关注文件魔数,这是用于识别 dex 文件的标志位。前四位是 dex 标志,后四位是版本号(安卓编译版本)。

{ 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00 } = “dex\n035\0

由于 dex 文件中采用的是小端字节序的编码方法(
不理解什么是字节序吗?戳我!
),实际存储的数据应该是:

{ 0x0a 0x78 0x65 0x64 0x00 0x35 0x33 0x30 } = “\nxed\0530

Java 怎么调用 OpenMemory?#

javaCode.OpenMemory的导出函数名(参数等...);
									⬇️
OpenMemory导出函数名映射到libart.so文件中的OpenMemory方法

可以理解为:OpenMemory导出函数名 是 libart.so 文件的对外开放的接口。

但是这个导出函数名不是一成不变的,在不同的安卓版本上,这个名称是不一样的。

我们可以通过导出虚拟机上的 libart.so 文件来获取导出函数名。

Android 10.0 以前,libart.so 在/system/lib 文件夹下,64 位的在/system/lib64

Android 10.0 以后,libart.so 被集成到了 apex 中

  • 使用 IDA 打开 libart.so 文件,搜索 OpenMemory 获取导出函数名:

image-20210923142923157

dexElements 数组#

  • 在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。
  • DexClassLoader 可以用来加载 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件
  • DexClassLoader 和 PathClassLoader 的基类 BaseDexClassLoader 查找 class 是通过其内部的 DexPathList pathList 来查找的
  • DexPathList 内部有一个 Element[] dexElements 数组,其 findClass() 方法(源码如下)的实现就是遍历该数组,查找 class ,一旦找到需要的类,就直接返回,停止遍历:
public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

总结一下,一个 class 的加载过程为:

																		DexClassLoader 
																					⬇️
																			DexPathList 
																					⬇️
																				Element[]

而,Element 存储了我们的 Dex 文件数据,下面截取了部分代码:

public Element(File file, ZipFile zipFile, DexFile dexFile) {
            this.file = file;
            this.zipFile = zipFile;
            this.dexFile = dexFile;
}

那么 dexElements 数组 就是存储了所有的 Dex 文件数据。

那么基础问题都解决了,就可以上脚本了~

脱壳脚本讲解#

脚本地址:
戳我直达
,这是个典型的一代壳脱壳脚本。

JS 部分#

var DEX_MAGIC = 0x0A786564;

var dexrec = [];

var openmemory = Module.findExportByName(
  "libart.so", 
  "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_"
  
);

if(openmemory != undefined) {
	console.log("openmemory at" + openmemory);
	Interceptor.attach(openmemory, {
		onEnter: function (args) {
      
      
      
			if(Memory.readU32(args[1]) == DEX_MAGIC) {
        
				dexrec.push(args[1]);
			}
		},
		onLeave: function (retval) {

		}
	});
}

if(Java.available) {
  
	Java.perform(function(){
    
		var dexBase64 = "";
   
		var application = Java.use("android.app.Application");
		var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
		var Base64 = Java.use("android.util.Base64");
		var FileOutputStream = Java.use("java.io.FileOutputStream");
		var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
		var reflectField = Java.use("java.lang.reflect.Field");
		var reflectMethod = Java.use("java.lang.reflect.Method");
		var reflectObject = Java.use("java.lang.Object");
		var reflectClass = Java.use("java.lang.Class");
		var reflectString = Java.use("java.lang.String");
		var reflectClassloader = Java.use("java.lang.ClassLoader");


		if(application != undefined) {
      
			application.attach.overload('android.content.Context').implementation = function(context) {
        
        
				var result = this.attach(context);
        
				var classloader = context.getClassLoader();
        
				
				var filesDir = context.getFilesDir();
        
				
				var codeCacheDir = context.getCodeCacheDir();
      
				console.log("files dir: " + filesDir);
				console.log("code cache dir: " + codeCacheDir);
      
				if(classloader != undefined) {
          
					var casedloader = Java.cast(classloader, BaseDexClassLoader);
          
					var dexbytes = Base64.decode(dexBase64, 0);
          
					var dexpath = filesDir + "/emmm.dex";
          
					var fout = FileOutputStream.$new(dexpath);
          
					fout.write(dexbytes, 0, dexbytes.length);
          
					fout.close();
        
					console.log("write dex to " + dexpath);

          
					var dexstr = dexpath.toString();
					var cachestr = codeCacheDir.toString();
       
          
					var dyndex = DexClassLoader.$new(
            dexstr, 
            cachestr,
            cachestr, 
            classloader
          );
        
					console.log(dyndex.toString());
        
          
					var EnumerateClass = dyndex.loadClass("com.smartdone.EnumerateClass");
          
					var castedEnumerateClass = Java.cast(EnumerateClass, reflectClass);
          
					var methods = castedEnumerateClass.getDeclaredMethods();
					
					var loadAllClass = undefined;
					for(var i in methods) {
            
						console.log(methods[i].getName());
            
						if(methods[i].getName() == "loadAllClass") {
							console.log("find loadAllClass");
							loadAllClass = methods[i];
              
						}
					}
          
					if(loadAllClass != undefined) {
            
						console.log("loadAllClass: " + loadAllClass.toString());
            
						var args = Java.array('Ljava.lang.Object;',[classloader]);
            
						var classlist = loadAllClass.invoke(null , args);
            
						console.log("start dump dex ");
            
						for(var i in dexrec) {
              
							if(Memory.readU32(dexrec[i]) == DEX_MAGIC) {
                
                
                
								var dex_len = Memory.readU32(dexrec[i].add(0x20));
                
								var dumppath = filesDir.toString() + "/" + dex_len.toString(0x10) + ".dex";
								console.log(dumppath);
                
								var dumpdexfile = new File(dumppath, "wb");
                
                
								dumpdexfile.write(Memory.readByteArray(dexrec[i], dex_len));
                
								dumpdexfile.close();
								console.log("write file to " + dumppath);
							}
						}
					}


				} else {
					console.error("unable get classloader");
				}
				return result;
			}
		}
	});
}

Dex 部分#

此处 JAVA 代码由 Smali 转换而来

package com.smartdone;

import android.util.Log;
import dalvik.system.DexFile;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;

public class EnumerateClass {
    private static final String TAG = "FRiDA_UNPACK";

    public static ArrayList getClassNameList(ClassLoader classLoader) {
        int i;
      
        ArrayList classNameList = new ArrayList();
        try {
          
            Object dexElements = EnumerateClass.getObjectField(EnumerateClass.getObjectField(classLoader, "pathList"), "dexElements");
        
          
          
          
            int dexElementsLength = Array.getLength(dexElements);
            i = 0;
            while(true) {
            label_8:
              	
                if(i >= dexElementsLength) {
                  
                    goto label_24;
                }
              
              
              
              Enumeration enumerations = ((DexFile)EnumerateClass.getObjectField(Array.get(dexElements, i), "dexFile")).entries();
                while(true) {
                  
                    if(!enumerations.hasMoreElements()) {
                        ++i;
                        break;
                    }
                  
                    classNameList.add(((String)enumerations.nextElement()));
                }
            }
        }
        catch(Exception v1) {
          
            goto label_24;
        }

        ++i;
        goto label_8;
    label_24:
      	
        Collections.sort(classNameList);
        return classNameList;
    }

  
    public static String[] getClassNameListArray(ClassLoader classLoader) {
        ArrayList namelist = EnumerateClass.getClassNameList(classLoader);
        String[] retval = new String[namelist.size()];
        namelist.toArray(((Object[])retval));
        return retval;
    }

 
  
    public static Object getObjectField(Object object, String fieldName) {
        Class clazz = object.getClass();
        while(!clazz.getName().equals(Object.class.getName())) {
          
            try {
              
                Field field = clazz.getDeclaredField(fieldName);
              
                field.setAccessible(true);
              
                return field.get(object);
            }
            catch(NoSuchFieldException e) {
                e.printStackTrace();
              
                clazz = clazz.getSuperclass();
            }
            catch(IllegalAccessException e2) {
              
                e2.printStackTrace();
            }
        }

        return null;
    }

  
    public static void loadAllClass(ClassLoader classLoader) {
        int v6_1;
        Method[] methods;
        Class clazz;
        try {
          
            Iterator v1 = EnumerateClass.getClassNameList(classLoader).iterator();
            while(true) {
                if(!v1.hasNext()) {
                    return;
                }
            
                Object v2 = v1.next();
              
                clazz = classLoader.loadClass(((String)v2));
              
                methods = clazz.getDeclaredMethods();
              
                Log.d("FRiDA_UNPACK", "load class: " + clazz.getName());
                v6_1 = 0;
            label_19:
              	
                while(v6_1 < methods.length) {
                    goto label_20;
                }
            }

        label_20://此处循环获取所有method并进行反射调用
          
            Method method = methods[v6_1];
          
            Object[] objs = new Object[method.getParameterTypes().length];
            Log.d("FRiDA_UNPACK", "try to load method: " + clazz.getName() + "-->" + method.getName());
          
          
          
            method.invoke(null, objs);
            Log.d("FRiDA_UNPACK", "success");
            ++v6_1;
            goto label_19;
        }
        catch(Throwable v0) {
        }
    }
}

另一种脱壳方案#

之前讲过了,安卓的 libart.so 文件提供了 OpenMemory 函数来加载 dex,如果在加载后,将 dex 直接写出去,也可达到脱壳效果。

std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
                                                  size_t size,
                                                   const std::string& location,
                                                   uint32_t location_checksum,
                                                   MemMap* mem_map,
                                                  const OatDexFile* oat_dex_file,
                                                   std::string* error_msg) {
  CHECK_ALIGNED(base, 4);  // various dex file structures must be word aligned
  std::unique_ptr<DexFile> dex_file(
      new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
  if (!dex_file->Init(error_msg)) {
    dex_file.reset();
  }
  //Start:此处开始加入自己的dex导出代码
    __android_log_print(ANDROID_LOG_DEBUG,"DexFile::OpenMemory","size is:%zu,location is:%s", size, location.c_str());
  //此处只是针对com.autohome.mycar编写的导出代码,并不具有通用性,通用型的应该有更好的写法
  //此处还有一个问题,这个只针对单dex项目写的导出,多dex未做处理
    if (!strcmp(location.c_str(),"/data/data/com.autohome.mycar/.jiagu/classes.dex"))
    {
      //获取文件指针
      int fd = open("/data/data/com.autohome.mycar/classes.dex",O_CREAT|O_EXCL|O_WRONLY,S_IRWXU);
         __android_log_print(ANDROID_LOG_DEBUG,"copy is starting!","hello");
      
      if (fd>0)
          write(fd,base,size);//写出dex数据
        else
          __android_log_print(ANDROID_LOG_DEBUG,"copy is failed!","codeis:%d",fd);
        close(fd);
    }
    //End:此处开始加入自己的dex导出代码
  return std::unique_ptr<const DexFile>(dex_file.release());
}
  • 理论上这种方案可以脱大部分的壳,不过有一个缺点,需要自己编译 rom 并刷机。

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年10月30日19:39:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Frida脱壳脚本解读原创https://cn-sec.com/archives/1379915.html