前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解

【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解

作者头像
胖虎哥
发布2023-05-10 19:26:34
4930
发布2023-05-10 19:26:34
举报

【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解

前言

提示:这里可以添加本文要记录的大概内容:

在上文中,我们讲解了关于Android脱壳的基本办法和实际操作,现在我们来针对脱壳(一代壳)的原理和脱壳相关的基础知识介绍,由于作者能力有限,会尽力的详细描述 一代壳脱壳 的流程及原理,如本文中有任何错误,烦请指正,感谢~


一、Dex加载流程

在日常分析脱壳点过程中,Dex加载的基本流程也是要明白熟悉的

在这里插入图片描述
在这里插入图片描述

DexPathList:该类主要用来查找Dex、SO库的路径,并这些路径整体呈一个数组 Element:根据多路径的分隔符“;”将dexPath转换成File列表,记录所有的dexFile DexFile:用来描述Dex文件,Dex的加载以及Class的查找都是由该类调用它的native方法完成的

我们依次来分析这个过程中的源码

DexPathList

代码语言:javascript
复制
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
**********************      
   this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                         suppressedExceptions, definingContext);    
**********************  
            }

makeDexElements

代码语言:javascript
复制
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
          List<IOException> suppressedExceptions, ClassLoader loader) {
**********************            
       DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);    
**********************         
          }

loadDexFile

代码语言:javascript
复制
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
           String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

loadDex

代码语言:javascript
复制
static DexFile loadDex(String sourcePathName, String outputPathName,
      int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
      return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
  }

DexFile

代码语言:javascript
复制
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
        mCookie = openDexFile(fileName, null, 0, loader, elements);
        mInternalCookie = mCookie;
        mFileName = fileName;
        //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
    }

这里出现的mCookie,mCookie在C/C++层中是DexFile的指针,我们在下面详细讲解

openDexFile

代码语言:javascript
复制
private static Object openDexFile(String sourceName, String outputName, int flags,
        ClassLoader loader, DexPathList.Element[] elements) throws IOException {
       // Use absolute paths to enable the use of relative paths when testing on host.
        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null)
                                    ? null
                                   : new File(outputName).getAbsolutePath(),
                               	   flags,
                                   loader,
                                   elements);
    }

这里就进入了C/C++层

openDexFileNative

在这里插入图片描述
在这里插入图片描述

为了节约篇幅,我们快速分析,中间再经过一些函数

代码语言:javascript
复制
OpenDexFilesFromOat()
MakeUpToDate()
GenerateOatFileNoChecks()
Dex2Oat()

最后进入了Dex2Oat,这就进入了Dex2Oat的编译流程

反之如果我们在下面Dex2Oat的流程中通过Hook相关方法或execv或execve导致dex2oat失败,我们就会返回到OpenDexFilesFromOat

OpenDexFilesFromOat

在这里插入图片描述
在这里插入图片描述

会先在HasOriginalDexFiles里尝试加载我们的Dex,也就是说,倘若我们的壳阻断了dex2oat的编译流程,然后又调用了DexFile的Open函数。

DexFile::Open

在这里插入图片描述
在这里插入图片描述

校验dex的魔术字字段,然后调用DexFile::OpenFile

DexFile::OpenFile

代码语言:javascript
复制
/art/runtime/dex_file.cc
std::unique_ptr<const DexFile> DexFile::OpenFile(int fd,
                                                const std::string& location,
                                                bool verify,
                                                bool verify_checksum,
                                                std::string* error_msg) {
 **************************************
 std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
                                                map->Size(),
                                                location,
                                                dex_header->checksum_,
                                                kNoOatDexFile,
                                                verify,
                                                verify_checksum,
                                                error_msg);   
  **************************************
                                                
                                                }

OpenCommon

在这里插入图片描述
在这里插入图片描述

最后又再次回到DexFile类,这里我们的dex文件加载基本流程分析完毕

二、Dex2Oat编译流程

Dex2oat是google公司为了提高编译效率的一种机制,从Android8.0开始实施,一些加壳厂商实现抽取壳往往会禁用Dex2oat,而针对整体加壳没有禁用的Dex2Oat也成为了脱壳点

在这里插入图片描述
在这里插入图片描述

Exec

代码语言:javascript
复制
/art/runtime/exec_utils.cc
bool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) {
  int status = ExecAndReturnCode(arg_vector, error_msg);
  if (status != 0) {
    const std::string command_line(android::base::Join(arg_vector, ' '));
    *error_msg = StringPrintf("Failed execv(%s) because non-0 exit status",
                              command_line.c_str());
    return false;
  }
  return true;
}

ExecAndReturnCode

在这里插入图片描述
在这里插入图片描述

而我们就可以通过Hook execv或execve来禁用Dex2Oat,而如果我们不禁用dex2oat,execve函数是用来调用dex2oat的二进制程序实现对dex文件的加载,我们这时候找到dex2oat.cc这个文件,找到main函数

代码语言:javascript
复制
/art/dex2oat/dex2oat.cc
 int main(int argc, char** argv) {
  int result = static_cast<int>(art::Dex2oat(argc, argv));
  if (!art::kIsDebugBuild && (RUNNING_ON_MEMORY_TOOL == 0)) {
    _exit(result);
  }
  return result;

这里我们调用了Dex2oat

Dex2Oat

代码语言:javascript
复制
/art/dex2oat/dex2oat.cc
static dex2oat::ReturnCode Dex2oat(int argc, char** argv) {
   **************************************
   dex2oat::ReturnCode setup_code = dex2oat->Setup();
    dex2oat::ReturnCode result;
  if (dex2oat->IsImage()) {
    result = CompileImage(*dex2oat);
  } else {
    result = CompileApp(*dex2oat);
 }
   **************************************
}

Dex2oat中会对dex文件进行逐个类逐个函数的编译,setup()函数完成对dex的加载

然后顺序执行,就会进入CompileApp

编译过程中会按照逐个函数进行编译,就会进入CompileMethod

在这里插入图片描述
在这里插入图片描述

到这里Dex2oat的基本流程就分析完毕

三、类加载流程

要理解DexFile为什么如此重要,首先我们要清除Android APP的类加载流程。Android的类加载一般分为两类隐式加载和显式加载

代码语言:javascript
复制
1.隐式加载:
    (1)创建类的实例,也就是new一个对象
    (2)访问某个类或接口的静态变量,或者对该静态变量赋值
    (3)调用类的静态方法
    (4)反射Class.forName("android.app.ActivityThread")
    (5)初始化一个类的子类(会首先初始化子类的父类)
2.显示加载:
    (1)使用LoadClass()加载
    (2)使用forName()加载

我们详细看一下显示加载:

代码语言:javascript
复制
Class.forName 和 ClassLoader.loadClass加载有何不同:
(1)ClassLoader.loadClass也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作)
(2)Class.forName这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作

我们在详细来看一下在类加载过程中的流程:

java层

在这里插入图片描述
在这里插入图片描述

我们可以发现类加载中关键的DexFile,该类用来描述Dex文件,所以我们的脱壳对象就是DexFile

这里从DexFile进入Native层中,还有一个关键的字段就是mCookie

在这里插入图片描述
在这里插入图片描述

后面我们详细的介绍mCookie的作用

我们进一步分析,进入Native层

Native层

/art/runtime/native/[dalvik_system_DexFile.cc

在这里插入图片描述
在这里插入图片描述

ConvertJavaArrayToDexFiles对cookie进行了处理

在这里插入图片描述
在这里插入图片描述

通过这里的分析,我们可以知道mCooike转换为C/C++层指针后,就是dexfile的索引

我们继续分析DefineClass

代码语言:javascript
复制
art/runtime/class_linker.cc
mirror::Class* ClassLinker::DefineClass(Thread* self,
                                      const char* descriptor,
                                        size_t hash,
                                       Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,
                                        const DexFile::ClassDef& dex_class_def) {
***************
LoadClass(self, *new_dex_file, *new_class_def, klass);
***************
}

LoadClass

代码语言:javascript
复制
art/runtime/class_linker.cc
void ClassLinker::LoadClass(Thread* self,
3120                            const DexFile& dex_file,
3121                            const DexFile::ClassDef& dex_class_def,
3122                            Handle<mirror::Class> klass) {
3123  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
3124  if (class_data == nullptr) {
3125    return;  // no fields or methods - for example a marker interface
3126  }
3127  LoadClassMembers(self, dex_file, class_data, klass);
3128}

LoadClassMembers

代码语言:javascript
复制
art/runtime/class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
                                   const DexFile& dex_file,
                                   const uint8_t* class_data,
                                   Handle<mirror::Class> klass) {
***************
      LoadMethod(dex_file, it, klass, method);
      LinkCode(this, method, oat_class_ptr, class_def_method_index);
***************
}

LoadMethod

代码语言:javascript
复制
art/runtime/class_linker.cc
void ClassLinker::LoadMethod(const DexFile& dex_file,
                           const ClassDataItemIterator& it,
                            Handle<mirror::Class> klass,
                             ArtMethod* dst) {
}

LinkCode

在这里插入图片描述
在这里插入图片描述

我们可以发现这里就进入了从linkcode后就进入了解释器中,并对是否进行dex2oat进行了判断,我们直接进入解释器中继续分析

我们知道Art解释器分为两种:解释模式下和quick模式下,而我们又知道Android8.0开始进行dex2oat

代码语言:javascript
复制
如果壳没有禁用dex2oat,那类中的初始化函数运行在解释器模式下 
如果壳禁用dex2oat,dex文件中的所有函数都运行在解释器模式下
则类的初始化函数运行在解释器模式下

所以一般的加壳厂商会禁用掉dex2oat,这样可以是所有的函数都运行在解释模式下,所以一些脱壳点选在dex2oat流程中,可能针对禁用dex2oat的情况并不使用,我们这里主要针对整体加壳,就不展开讲述,最后我们得知解释器中会运行在Execute下

Execute

代码语言:javascript
复制
art/runtime/interpreter/interpreter.cc
static inline JValue Execute(
    Thread* self,
    const DexFile::CodeItem* code_item,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_){

***************
      ArtMethod *method = shadow_frame.GetMethod();
***************
    
    }

这里我们大致分析完成了类加载的思路

四、DexFile详解

前面我们分析了很多,对dex加载、类加载等都已经有了一个很详细的了解,而最终一切的核心就是DexFile,DexFile就是我们脱壳所关注的重点,寒冰大佬在拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点中提到,在ART下只要获得了DexFile对象,那么我们就可以得到该dex文件在内存中的起始地址和大小,进而完成脱壳。

我们先查看一些DexFile的结构体

在这里插入图片描述
在这里插入图片描述

只要我们能获得起始地址begin和大小size,就可以成功的将dex文件脱取下来,这里我们记得DexFile含有虚函数表,所以根据C++布局,要偏移一个指针

在这里插入图片描述
在这里插入图片描述

而DexFile类还给我们提供了方便的API

在这里插入图片描述
在这里插入图片描述

这样只要我们找到函数中有DexFile对象,就可以通过调用API来进一步dump dex文件,由此按照寒冰大佬的思想,大量的脱壳点由此产生

(1)直接查找法

我们通过直接在Android源码中搜索DexFile,就可以获得海量的脱壳点

在这里插入图片描述
在这里插入图片描述

我们通过在IDA中搜索libart.so导出的DexFile,同样可以获得大量的脱壳点

在这里插入图片描述
在这里插入图片描述

(2)间接查找法

这里就是寒冰大佬在文章中提到的通过ArtMethod对象的getDexFile()获取到ArtMethod所属的DexFile对象的这种一级间接法,通过Thread的getCurrentMethod()函数首先获取到ArtMethod或者通过ShadowFrame的getMethod获取到ArtMethod对象,然后再通过getDexFile获取到ArtMethod对象所属的DexFile的二级间接法

代码语言:javascript
复制
getDexFile()
getMethod()

五、ArtMethod详解

上面我们已经详细分析了DexFile的文件结构,我们知道通过ArtMethod可以获得DexFile,那么为啥又要单独提ArtMethod呢,因为ArtMethod在抽取壳和VMP等壳中扮演了重要的角色

ArtMethod结构体

在这里插入图片描述
在这里插入图片描述

我们通过ArtMethod可以获得codeitem的偏移和方法索引,熟悉dex结构的朋友知道codeitem就是代码实际的值,而codeitem则再后续加壳技术扮演了至关重要的地址,而且ArtMethod还有非常丰富的方法,可以帮助大家实现很多功能,所以在脱壳工作中也是十分重要的

总结

以上就是今天要讲的内容,主要借鉴了 随风而行 大佬的一些思路及文章,将文章在复写一遍目的是为了加深记忆,在以后的学习过程中可以很方便的查看~

参考文献

https://security-kitchen.com/2022/12/04/Packer5/ https://bbs.kanxue.com/thread-254555.htm#msg_header_h2_2

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解
  • 前言
  • 一、Dex加载流程
  • 二、Dex2Oat编译流程
  • 三、类加载流程
  • 四、DexFile详解
    • (1)直接查找法
      • (2)间接查找法
      • 五、ArtMethod详解
      • 总结
      • 参考文献
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档