为什么会写这个呢?其实很多东西都是之前写的,最近又整理了一下,也算是一个温故而知新吧!
之前一直找看一下传说中的goto类抽取,也想看看到底是怎么实现的,以及对比bangbang/ijm等有什么区别。
也问了很多人,也委托几个大佬帮我留意一下样本,也是没了后续。
直到今年又有了几个新的样本,也蛮有意思的,分析完了就找到了那个goto样本,分析之后。这篇文章也就对我最近捣鼓的样本的一些总结吧!
如果看下去的话,请不要嫌弃文章的啰嗦,因为要照顾一些刚接触的。所以我会尽可能地完整的去过一遍流程。为了方便学习,这里以Android8.1,在Android12等高版本就有有很多的不适用性,但相应的也出现了一些配套的Api,这里不展开细说。
本篇文章以一个Java的方法执行的全过程为切入点的,即如下的伪代码
谈到安卓的Java虚拟机,那必定离不开安卓的art虚拟机。早期还有dalvikvm虚拟器。不过也成为了一种过去式的历史。不过现在也一另外的形式存在。那就是dexvmp
那么我么今天就以Java的代码如何在Android的上运行做一个分享。

在Android中有几个版本跨域比较大,那就是4.4.4/7.12/8.1/12/15,art虚拟机就是在Android的4.4.4中发布,伴随这art虚拟机的引入,安卓也走入了一个全新的时代,即从解释型虚拟机到编译后的二进制文件。
如果有学习过Java的开发,那都会遇到jar,dex就是在Android上的“jar”。姑且可以这样子理解(jar可以直接改为.zip打开,dex不行,jar可以放一些资源,dex只能放smail文件)
涉及到jar/dex,那么就会有一个属性的东西类加载器“ClassLoader”,这个在上一次分享中,有介绍过不同JDK实现有提过类加载的一个过程,这次会比较详细的分享他的一些过程。


他的默认实现,从这里也很生动的体现他的“双亲委派机制”解决类加载器的重复加载问题,从中也可以发现,loadClass的会调用findClass进行查找,以及他们的权限修饰符,可以通过继承的形式进行覆盖。
下面三个就是他在Android的三个实现子类



通过上面的图片来看,他们并没有在这里实现什么,甚至也没什么区别,都是继承于BaseDexClassLoader这个类,那么我们重点就在这了。
上面三个实现ClassLoader的子类,我们也比较容易理解他们的作用。
BootClassLoader
这个看名字就知道啦,所以跳过咯

通过ClassLoader的代码分析,我们知道loadClass会调用findClass去查找并加载类,那么我们只需要关注findClass的实现即可

在这里我们可以看到会调用pathList的findClass调用,以及下面的方法,addDexPath,也是调用pathList的.addDexPath方法
pathList的定义


这里我们可以看到DexPathList的findClass也是调用dexElements数组的中实现的findClass方法
dexElements初始化刚好在DexPathList的构造函数中,也就是在makeInMemoryDexElements这个函数中


dexElements是makeInMemoryDexElements返回的,Element初始化的过程中还会创建一个DexFile对象,

可以通过这两个相邻的函数可以发现,这就是加载两种不同形式的dex的函数,上面的是通过ByteBuffer[],下面的是通过List
那我们以文件加载的形式去分析Dex的加载,那也就是会调用makeDexElements方法中的loadDexFile中返回
那也就是


我们跟一下DexFile的构造函数,这里也很明显可以看到有两个不同的构造函数,一个是传递的是ByteBuffer,一个是String的fileName
然后我们回到Element的findClass
Element

那么最终的类加载就会变成
DexFile的defineClassNative

正常逆向分析的思维就是,一路跟到底,最后才是看如何构造一个全链路。
先到初始化,再到查找嘛,????
这里我们就换一下,先看他如何出初始化再捣鼓的
####### 插眼
openDexFile
这个是在DexFile的初始化的时候,可能会执行,取决于是传递byte[]还是dexPath,也就是不同类加载器的区别了。
现在我们以DexClassLoader的形式去加载的话(PathClassLoader也是一样的)去进行分析,也就是会执行dexPath的这个构造函数

最终调用到native的方法,那么我们继续跟去
dalvik_system_DexFile.cc
DexFile_openDexFileNative

这里会调用OpenDexFilesFromOat返回,然后通过ConvertDexFilesToJavaArray函数处理之后返回到Java层
OpenDexFilesFromOat

会先判断是否加载oat相关的代码。这里我们默认第一次执行,也就不会涉及到oat/odex/vdex等相关的东西了,因为可以通过这里dump到完整的dex,壳代码会禁止相关的优化,或者是屏蔽了。。。
屏蔽也有一些好处,有一些inline的优化,也是很头疼,屏蔽之后就可以一劳永逸了,代价没那么快了。
这里的快的概念,是对于正常开发者的,而对于我们学习来说,也就没那么重要了。


所以上面的dex_files就会为空,从而执行DexFile::Open
,这里就不一路分析到底了,感兴趣的可以继续分析下去
OatFile* OatFile::Open
LoadDexFiles
OatFileAssistant::LoadDexFiles
std::unique_ptr
OatFileBase* OatFileBase::OpenOatFile
DexFile::Open
这里的DexFile::Open也是一路执行到DexFile::OpenFile
DexFile::OpenFile

在这里我们就会进入到一个特殊的点了,开始将dex文件映射到内存中去。至于为什么特殊?
等下会解释。
MemMap* MapFile

MapFile也是调用MapFileAtAddress
MemMap::MapFileAtAddress

MapFileAtAddress进一步调用MapInternal
MemMap::MapInternal
MapInternal之后就到了我们遇到的特殊点了,使用mmap去映射内存

我们可以往上翻,会注意到他的权限是PROT_READ,

DexFile::OpenCommon
到这里我们就看完分配内存了,我们接下来往下看DexFile::OpenCommon

中也没存做啥,只是创建个DexFile对象,然后返回
ConvertDexFilesToJavaArray
·
我们可以看到他是传递个oat_file以及DexFile的vec
细心的看的话,就知道她他会创建个数据,第一个元素放到的是oat的对象的地址,第二个就是DexFile(一个或多个)
然后返回,最终就是DexFile的mCookie对象了,下面会有图片比较直观的体现
dalvik_system_DexFile.h


刚才我们去看了DexFile的创建以及初始化过程,接下来我们继续看类的定义
DexFile_defineClassNative

调用class_linker->DefineClass的
class_linker.cc
ClassLinker::DefineClass

DefineClass中也是进一步调用LoadClass
ClassLinker::LoadClass

LoadClass进一步调用LoadClassMembers
ClassLinker::LoadClassMembers

在这里我们就可以看到比较的多东西了
ClassLinker::LoadMethod

static void LinkCode
在这里我们就可以触发第二个彩蛋
Class_newInstance

类实例的创建

这里我们能看到类的初始化入口
或者这个路径(没有测试,只是代码搜素搜到这个)
GetFieldID

FindFieldID

EnsureInitialized

最终也是会执行到ClassLinker::EnsureInitialized
ClassLinker::EnsureInitialized

ClassLinker::EnsureInitialized调用ClassLinker::InitializeClass
ClassLinker::InitializeClass

ClassLinker::InitializeClass调用ClassLinker::FixupStaticTrampolines
ClassLinker::FixupStaticTrampolines

到这里我们就可以看到他的参数了klass,至于这个是什么,后面会有测试进行的说明
既然我们说了类是怎么加载的,然后到类定义,方法的加载,类的初始化,那么就顺便把最后的一个:方法的执行
Method

Method继承于Executable。这个需要考的,现在先记住这个就好了
invoke

这个就是Java的方法反射执行的入口了
Method_invoke

Method_invoke调用InvokeMethod
InvokeMethod

InvokeMethod调用InvokeWithArgArray
同时我们也看到了
这种奇怪的代码转换,在后面还会有很多,,哈哈哈
InvokeWithArgArray

InvokeWithArgArray做好准备参数检查的准备
就执行method->Invoke

ArtMethod::Invoke

到这一步我们就进入了ArtMethod的世界!
开源的类抽取还原框架fart,也就是以这个为入口进行虚假调用并触发方法的修复
这里我们可以看到一段描述
然后我们就执行到了
art::interpreter::EnterInterpreterFromInvoke
进入解释模式
EnterInterpreterFromInvoke

在这里会判断是否为jni方法
我们此次分析的是普通的Java函数,所以会执行Execute
Execute

这里应该是执行到一些优化之后的代码吧?我们要看的是解释模式,所以忽略
ArtInterpreterToCompiledCodeBridge

这里也是从新执行一遍method->Invoke,
最终应该会执行到EnterInterpreterFromInvoke的InterpreterJni去吗?
ExecuteSwitchImpl

在这里解析指令了咯?所以结束了?
不,这才刚开始。
上面的说那么多无非就是想引入一些点:LoadMethod、、DexFile::Open、Execute等等方法 还有mCookie等字段是什么?
e1cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5M7X3g2X3j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7^5i4K6u0W2x3g2)9J5k6e0m8Q4y4h3k6J5z5o6q4Q4x3V1k6^5M7X3g2X3i4K6u0r3j5i4u0@1i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8X3q4J5N6q4)9#2k6X3#2W2N6r3S2G2k6q4)9J5k6h3R3`.
77cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5M7X3g2X3j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7^5i4K6u0W2x3g2)9J5k6e0m8Q4y4h3k6J5z5o6q4Q4x3V1k6^5M7X3g2X3i4K6u0r3j5i4u0@1i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8X3#2A6M7Y4u0G2M7W2)9J5c8X3y4D9j5i4y4K6i4K6u0W2K9l9`.`.
a27K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5M7X3g2X3j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7^5i4K6u0W2x3g2)9J5k6e0m8Q4y4h3k6J5z5o6q4Q4x3V1k6^5M7X3g2X3i4K6u0r3j5i4u0@1i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8X3c8W2P5q4)9#2k6X3k6A6L8r3g2Q4x3X3g2Z5
376K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5M7X3g2X3j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7^5i4K6u0W2x3g2)9J5k6e0m8Q4y4h3k6J5z5o6q4Q4x3V1k6^5M7X3g2X3i4K6u0r3L8r3W2T1j5$3!0J5k6g2)9J5c8X3!0B7L8s2g2F1K9g2)9J5c8Y4y4J5j5#2)9J5c8X3#2S2K9h3&6Q4x3V1k6B7j5i4k6S2i4K6u0r3K9X3q4$3j5g2)9J5c8X3I4S2L8X3N6Q4x3V1k6o6L8r3q4K6M7#2)9J5k6h3A6S2N6X3p5`.

看到这图,是不是一切都豁然开朗了?
那么费力的去讲解这么多的流程,无非就是想解释,最终的数据加载到内存了,都成了什么样子了?
到这里我们是不是会有很多奇怪的想法?
那我们以一个demo为例:

比如我们可以拿到一个Method,进一步我们可以拿到他的类定义、然后就是dex缓存,从而拿到dexFile,然后是就是计算code_item的偏移?然后拿到指令?
对的,就顺便脱壳,也做了类修复


既然可以拿到指令? 那我是否可以修改指令?
是的,上面也提到了类加载的内存映射只有READ的权限,也就需要hook去修改,或者使用下面的方式(详情可以看dpt-shell的作者分析的文章)

然后就根据实际需求去修改就好了
或者替换偏移?

如果指令大小不够了怎么办?
直接mmap一个区域也给他弄个偏移就好了

或者我们可以做一个动态的指令生成? 如何动态的生成指令?

直接编译拿到之后直接整个方法定义长得差不多的,直接拿过来就可以用了(fid,mid,这些可能会错乱哈哈啊哈)
如何直接写指令去实现?

直接用asm去写就好了
这里的参数传递的规则有点奇怪,得从3/4开始的
上面的一个整体效果:::

输出
这两个是偏移替换的,可以看到run执行了fake/fake2的代码


......
动态修改&执行?提高我们的风控难度?对吧?
Constructor

这里的是构造函数,同理分析也是差不多的,所以跳过吧!哈哈哈
Executable
Executable的定义


artMethod指针
private long artMethod;
这个东西就很关键了,那就是artMethod的地址
还能有栈回溯方便吗?

new Exception().printStackTrace();
abort

Instruction::DumpString
ArtMethod *artmeth = reinterpret_cast<ArtMethod *>(methid);
还有上面的,很多的都是可以直接转换的了
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 6天前
被陈可牛编辑
,原因: 上传图片