0x01 题目分析
题目及部分分析结果链接:
链接:https://pan.baidu.com/s/1l68AMYnXJeSksfCaxBdHQA?pwd=tang
提取码:tang
APP界面:
APP界面为一个滑动锁,共16个可连点,由此初步判断密码可能较为复杂,不适合进行穷举。
APK包内容,其中assets中存在额外的classes.dex
0x02 DEX文件格式
在分析之前需要先掌握DEX文件格式的部分知识,掌握DEX文件格式将会使之后的分析更加明了清晰。
首先关于dex文件,dex文件实际是Android系统中的一种可被虚拟机加载解释执行的文件,Android虚拟机分为Dalvik虚拟机(4.4之前)和ART虚拟机(4.4之后),而ART虚拟机继承于Dalvik虚拟机,对其进行优化。ART虚拟机使用AOT技术即Ahead of Time,在APP安装时进行编译,将Dex字节码编译为可被CPU直接执行的二进制代码并缓存,大大提高了APP运行效率。但无论如何Dex文件都被使用作为字节码文件进行使用。
Dex文件对于Android虚拟机就如同Class文件对于Java虚拟机(JVM),而Android虚拟机指令基于寄存器的,而JVM基于栈。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。
当编写并编译APP程序时,编译器先会将源码.java文件(仅说明Java编写的APP)转换成.class字节码文件,而在AndroidSDK中存在dx工具,其将.class文件进行转换,最终转化为.dex文件。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时出现反复的文件加载与解析过程。
如上图所示,dex文件格式大体上分为3部分,分别是文件头、索引区以及数据区。
首先是文件头
magic字段(共8字节):dex文件的头部,用于系统识别,最后一个字节必须为0x00
checksum字段:除 magic 和此字段之外的所有内容的 adler32 校验和(即从第12个字节开始之后的全部内容的校验和),用于检测文件损坏情况
signature字段:(除 magic、checksum 和此字段之外的所有内容)的 SHA-1 签名(哈希);用于对文件进行唯一标识
file_size:整个文件(包括标头)的大小,以字节为单位
header_size:头文件(整个区段)的大小,以字节为单位。此项允许至少一定程度的向后/向前兼容性,而不会使格式失效。
endian_tag:字节序标记。
link_size:链接区段的大小;如果此文件未进行静态链接,则该值为 0
link_off:从文件开头到链接区段的偏移量,如果 link_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 link_data 区段的偏移量。
map_off:从文件开头到映射项的偏移量。该偏移量(必须为非零值)应该是到 data 区段的偏移量,而数据应采用map_list指定的格式。
之后的内容就是一个size字段接着一个off字段,off为从文件开头到映射项的偏移量,size为相应字段的数量,例如这里的class_defs_size字段值为10,代表有10个class def,且class def偏移量为2848(0xB20)。
这里以class数据为例进行进一步分析
跳转到0xB20
进行分析
这里有10个类定义,每个类定义字段为0x20
以Class A为例,字段包含以下内容,重点看class_data_off
,其中红色方框内容本身不包含在此字段中,而是010Editor根据偏移寻找到的内容
这里需要重点分析class_data_off
,class_data_off为11364(0x2C64)
,跳到对应位置进行分析
这里会发现size字段数据类型为uleb128
LEB128(“Little-Endian Base 128”)表示任意有符号或无符号整数的可变长度编码。该格式借鉴了 DWARF3 规范。在 .dex 文件中,LEB128 仅用于对 32 位数字进行编码。
每个 LEB128 编码值均由 1-5 个字节组成,共同表示一个 32 位的值。每个字节最高位均为1(序列中的最后一个字节除外,其最高位为0代表结束)。每个字节的剩余 7 位均为有效负荷,即第一个字节中有 7 个最低有效位,第二个字节中也是 7 个,依此类推。对于有符号 LEB128 (sleb128),序列中最后一个字节的最高有效位会进行符号扩展,以生成最终值。在无符号情况 (uleb128) 下,任何未明确表示的位都会被解译为 0。
之后重点分析 direct_methods
字段,该字段使用一个DexMethod
类型指针,开辟空间存放方法结构DexMethod
DexClass结构示意图如下图所示,需要格外关注CodeOff
,其是代码在文件中的偏移量
如图所示为 偏移0xC74
的位置的代码结构体,一般而言代码抽取类型壳都会抽取insns
部分的字节码内容进行加解密操作
0x03 APP静态流程分析
使用JEB 打开APP 找到MainActivity
,在按键抬起时触发解压释放dex操作,之后将手势传入函数进行校验
分析该函数,发现对classes.dex使用DexClassLoader
自定义加载器进行了动态加载,在此过程中读取了注册的资源A_offset
一并作为参数使用反射
调用isValidate
A_offset的值为[3,3248]
,旁别存在另外一个B_offset
array[1,3420]
,是一个伏笔,先按下不表
之后直接尝试分析assets中的classes.dex,可以看到,先是读取classes.dex并调用了fix
,调用fix时用到了传入的A_offset
,之后使用落地删除动态加载
重新加载dex并调用A.d
方法
而直接从dex来看A.d
目前没用任何东西
仔细分析fix方法,发现其对dex进行了解析并读取了class_data
进行了修改,修改完成后修改签名和checksum以使dex能够正常加载
但是目前不知道它修改了哪个class数据,继续分析,在这个函数被调用传入的int参数为0,并且把buffer指针指向了100
,之后重新指到 地址100位置的int值 + 20 * 传入的参数
,而地址100(0x64) 也就是class_def_off
的位置,而getInt读取并反转byte(小端序需要转换)后得到的就是2848,而传入的参数是0,也就是会定位到
2848 (0xB20)
位置
之后便开始按照上文提到的字段对class A进行解析并返回HashMap
返回fix继续分析getClassData
,发现其根据class_data_off
偏移找到了类A并且进行全解析,最后返回四个结构,这个过程相当于解析了Dex文件中的类A
之后返回fix调用((int[][])class_data_off.get("direct_methods"))[int_3][2] = int_3248;
,这个就读取第4个方法并修改第3个字段为3248
,其中第四个方法为d方法,如下图所示,而前面提到第三个字段其实是code段的偏移,这里修改为3248,到此也就明白了,实际该APP实现了一个通过对dex方法code段偏移进行修改进而达到无法直接进行静态分析的目的
使用010editor进行修改,(注意由于code段偏移类型为uleb128
,故需要进行转换后patch)
修改后成功出现,而这里的md5串无法查到结果,说明手势密码复杂度高无法直接得到密码
但是还没完,我们还需要调用B.d(md5("4155606cedd928c3bb0b93343c81f3f5".getbytes()))
才能得到flag,而b.d根本没有,还是需要修复,但是APP不能自己修复,这个时候想到之前的伏笔B_offset[1,3420]
照猫画虎进行修复
答案就在眼前
0x04 巧用Frida进行Hook分析
这就完了吗?文章题目是深入分析,那必须眼见为实,使用Frida进行Hook查看程序流程
对DexClassLoader 初始化方法进行Hook
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
DexClassLoader.$init.overload('java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.ClassLoader').implementation = function(dexPath,odexPath,libPath,parent){
console.log(dexPath);
return this.$init(dexPath,odexPath,libPath,parent);
}
显示加载了两次,第一次为decode.dex,第二次为1.dex(仅修复了A class)
对HashMap put 方法进行Hook
var HashMap = Java.use("java.util.HashMap");
HashMap.put.overload('java.lang.Object', 'java.lang.Object').implementation = function(key, value){
console.log(key + " : " + value);
return this.put(key,value);
}
和预测一致
对ByteBuffer position 方法进行Hook
var ByteBuffer = Java.use("java.nio.ByteBuffer");
ByteBuffer.position.overload('int').implementation = function(arg){
console.log(arg);
return this.position(arg);
}
发现确实是2848
对FileOutputStream write方法进行Hook
function writeFile(content) {
var file = new File("/data/data/com.octobox.ctf/out.txt", "w");
file.write(content + "\n");
file.flush();
file.close();
}
var FileOutputStream = Java.use("java.io.FileOutputStream");
FileOutputStream.write.overload('[B').implementation = function(arg){
writeFile(arg);
return this.write(arg);
}
这里对这个方法进行hook是因为,在fix修复完A类后使用write
写到1.dex,那我们可以直接hook到此函数对修改后的文件进行输出,就能成功脱取内容
对java.lang.reflect.Method invoke方法进行Hook
var reflectMethod = Java.use("java.lang.reflect.Method");
reflectMethod.invoke.overload('java.lang.Object', '[Ljava.lang.Object;').implementation = function(arg1,arg2){
console.log(arg1);
console.log(arg2);
return this.invoke(arg1,arg2);
}
这里主要是为了看清手势和传入的字符串到底有什么关系
而C.isValidate内刚好在调用A.d时使用了invoke,当手势如下如时,打印如下,也就说明是从0-f对点进行排序,最后依次拼接得到passwd
0x05 总结
对题目进行深入分析能够更好的掌握对应知识并且能够极大的扩充知识面,通过此题能更好的掌握Dex加密解密以及动态加载技术
0x06 参考
https://source.android.google.cn/devices/tech/dalvik/dex-format
https://blog.csdn.net/tabactivity/article/details/78950379
https://blog.csdn.net/zhangmiaoping23/article/details/79447377