我直接小脑上传(x,书太老了,Android 知识更新快,不过学学基础还行
刚开始接触 android 对于这个东西也没有什么头绪,在阅读书籍 《 Android软件安全与逆向分析》(丰生强著) 虽然书中已经给了学习步骤,但是脑子里依旧杂乱。故将得到的知识点写成杂记,待之后希望将可以将各个知识点串联起来
第二章
编写第一个安卓程序以及对该程序进行逆向
————————
编写第一个程序
书中所介绍的开发环境是 Eclipse,但是由于本人比较懒逼,在安装 sdk 的时候已经安装了 Android studio 所以直接用的这玩意写的,写的是最简单的注册程序,程序涉及到界面、文本框、按钮,类似于 windows 的 MFC,但是 java 的类功能更加集成完全。
注册程序的检查函数很显然是要写在 button 中的,但是我不是很懂 Android 程序中项目的结构,只是了解那些里面大致都是些什么东西,而且 Android studio 中的目录结构非常多很复杂,我也不明白在建立文件的时候的各种名称对应这什么内容(先埋坑,后研究
Android studio目录如下
我们所需要写的按钮响应函数就放在图中的红色方框内,并且这个结构目录很奇妙:可以选不同类型的目录结构
所选的目录结构类型不一样,所展现的内容以及内容编排的方式都不一样,如下图
和 Project 对应的 MainActivity 函数位于此处
程序内容编写
界面的设计在下图红框中的文件中呈现,左边是代码,右边是设计效果(虽然有个 TextView 位置看着很奇怪,但是加载到模拟器里面是好的(x
在 MainActivity 中的函数主要内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 package com.example.firstandroid;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); EditText editusername = (EditText) findViewById(R.id.edit_name); EditText editsn=(EditText) findViewById(R.id.edit_sn); Button btn=(Button) findViewById(R.id.button); btn.setOnClickListener( new View .OnClickListener() { @Override public void onClick (View v) { try { if (!checkSN(editusername.getText().toString().trim(),editsn.getText().toString().trim())) { Toast.makeText(MainActivity.this ,"注册失败" ,Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this ,"注册成功" ,Toast.LENGTH_SHORT).show(); } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } } ); } public static String toHexString (byte [] byteArray) { final StringBuilder hexString = new StringBuilder ("" ); if (byteArray == null || byteArray.length <= 0 ) return null ; for (int i = 0 ; i < byteArray.length; i++) { int v = byteArray[i] & 0xFF ; String hv = Integer.toHexString(v); if (hv.length() < 2 ) { hexString.append(0 ); } hexString.append(hv); } return hexString.toString().toLowerCase(); } private boolean checkSN (String username,String sn) throws NoSuchAlgorithmException { try { if (username==null || (username.length()==0 )) return false ; if (sn==null || (sn.length()==0 )) return false ; MessageDigest digest = MessageDigest.getInstance("MD5" ); digest.reset(); digest.update(username.getBytes()); byte [] bytes=digest.digest(); String hexstr=toHexString(bytes); StringBuilder sb=new StringBuilder (); for (int i=0 ;i<hexstr.length();i+=2 ) { sb.append(hexstr.charAt(i)); } String userSN=sb.toString(); if (!userSN.equalsIgnoreCase(sn)) return false ; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return false ; } return true ; } }
逻辑很简单,但是这里涉及到了一个回调函数的知识,可以发现在 MainActivity 函数中是没有明显调用 onClick 函数的,但是在程序运行过程中却被执行了,可以简单看看 了解一下回调。
简单说一说回调
setOnClickListener 函数源码
1 2 3 4 5 6 public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
根据源码我们可以发现该函数所需要的参数的类型是 OnClickListener,在里面所执行的操作是将点击设置为可执行,并且将传入的参数赋值给了 getListenerInfo().mOnClickListener。可以推测出来 getListenerInfo() 的返回值类型应该是一个类,mOnClickListener 是类中的一个属性
getListenerInfo() 以及 ListenerInfo 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ListenerInfo getListenerInfo () { if (mListenerInfo != null ) { return mListenerInfo; } mListenerInfo = new ListenerInfo (); return mListenerInfo; } static class ListenerInfo { protected OnFocusChangeListener mOnFocusChangeListener; private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners; protected OnScrollChangeListener mOnScrollChangeListener; private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners; public OnClickListener mOnClickListener; protected OnLongClickListener mOnLongClickListener; protected OnContextClickListener mOnContextClickListener; protected OnCreateContextMenuListener mOnCreateContextMenuListener; private OnKeyListener mOnKeyListener; private OnTouchListener mOnTouchListener; private OnHoverListener mOnHoverListener; private OnGenericMotionListener mOnGenericMotionListener; private OnDragListener mOnDragListener; private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener; OnApplyWindowInsetsListener mOnApplyWindowInsetsListener; }
也就是说,通过 setOnClickListener 这个函数,我们达到了如图的效果(猜测,很有可能不准确
可是明明就只是调用了 setOnClickListener 函数照理来说代码应该就只有短短一行才是,但是我们所写的调用代码却非常长。这种情况的出现是所传入参数的类型所导致的
OnClickListener 源码
1 2 3 4 5 6 7 8 public interface OnClickListener { void onClick (View v) ; }
这是一个接口类,接口类建立一个对象就必须对里面的方法进行重写,在我们所编写程序中我们需要在重写的 onClick() 函数中进行注册码的校验工作,这也是为什么我们所写的函数长的原因。
反编译 APK 文件
直接使用 apktool 可以对文件进行编译或者反编译
1 2 apktool d[ecode] [OPTS] <file.apk> [<dir>] // 反编译 apk 文件命令 apktool b[uild] [OPTS] [<app_path>] [<out_file>] // 编译 apk 文件命令
第三章——Dalvik虚拟机
Dalivik 虚拟机基于寄存器架构
dalvik 字节码和 smali 汇编的关系
虽然 Android 程序是由 java 所编写,但是 Android 操作系统它所运行的是 dalvik 虚拟机,dalvik 虚拟机运行的是由 Java 字节码转换而来的 dalvik 字节码。
由于 dalvik 字节码生涩难懂,可读性很弱,因此研究人员们给出了 Dalvik 字节码的一种助记方式:smali 语法。我们可以借助一些工具:apktool、jeb等。将 dex 文件转换成 smali 文件进行阅读。
dex文件的生成
新建一个Main.java,使用命令
编译生成 Main.class 文件
dx 工具是 AndroidSDK 中自带的一个东西,可以将 class 文件转换为 dex 文件,教程 。通过下面的命令
1 dx --dex --output=Main.dex Main.class
生成 dex 文件,在这里我们也可以看到 .class 文件和 .dex 文件之间的关系,.class 文件是在 java 虚拟机中的可执行文件,但是 Android 中不支持 java 虚拟机的运行,因此需要将 .class 文件进行转换得到 .dex 文件并使其能在 dalvik 虚拟机中运行
小坑点
我尝试在 eclipse 中建立项目,然后直接使用项目文件夹 /bin/项目名称/Main.class 文件去进行转换,但是会报错
仔细看看使用 javac 编译生成的 Main.class 文件和 eclipse 生成的 Main.class 文件,其实是有差别的
细节就不去深究(先埋个坑吧
Dalvik指令分析
如何解释dalvik字节码 、dalvik指令集速查
指令语法由指令的位描述 和指令格式标识 来决定。
位描述约定:
每16位的字采用空格分隔
每个字母表示四位,每个字母按顺序从高字节开始,排列到低字节。每四位之间可能用 ‘|’ 来表示不同内容
顺序采用 A~Z 的单个大写字母作为一个4位的操作码,op 表示一个8位的操作码
'Ø’表示这字段所有位为0
指令格式表示约定:
大多由三个字符组成,前面两个是数字,最后一个是字母
第一个数字表示指令由多少个16位的字组成。
第二个数字是表示指令最多使用寄存器的个数。特殊标记 ‘r’ 使用一定范围内的寄存器
第三个字母位类型码,表示指令用到的额外数据类型
看了上面肯定依旧不明白到底要怎么看这个东西,太抽象了。
直接来看看实例,我们将上面生成的 dex 文件直接拖入 ida 中,并且通过设置显示字节码
注意点 :由于 IDA 显示原因,实际上这些字节码在内存中的存储顺序是这样的
对指令 1241 进行解析举例
字节码 12 为操作码(opcode),查询获得对应的指令格式标识为 11n
11n 的意义可以根据指令格式标识约定知晓:
第一个数字 1 表示指令由1个16位字组成
第二个数字 1 表示指令最多使用1个寄存器
n 表示一个 4 位立即数
对应的位描述为 const/4 vA, #+B
更具位描述的约定知晓:
字节码 41 中的第一位 4 为给定的字面值4,可以简单理解为一个占有4位字大小的数值4
字节码 41 中的第二位 1 为寄存器下标
结合起来就是
12
4
1
操作码
立即数
寄存器下标
const
4
v1
我们可以发现在 ida 中我们是找不到指令格式标识的,这是因为 ida 已经根据字节码都分析好了,将每组指令和指令对应的字节码都进行了对应
第四章——Android可执行文件
Android程序生成步骤
APK 目录结构
介绍链接
APK 文件的本质实际上是一个压缩包,使用 zip 格式解压软件对 apk 文件进行解压,会发现它由一些图片资源与其他文件组成,这些内容的名称和作用大致如下
名称
作用
assets目录
存放需要打包到 apk 中的静态文件
lib目录
程序依赖的 native 库
res目录
存放应用程序的资源
META-INF目录
存放应用程序签名和目录证书
AndroidManifest.xml
应用程序配置文件
classes.dex
dex 可执行文件
resources.arsc
资源配置文件
META-INF 目录:存放签名信息,用来保证 apk 包的完整性和系统的安全
assets 目录:用于存放需要打包到 apk 中的静态文件
该目录支持任意深度的子目录,用户可以根据自己的需求任意部署文件夹架构。
res 目录下的文件会在 .R 文件中生成对应的资源ID,访问的时候需要 AssetManager 类。
lib 目录:存放应用程序依赖的 native 库文件,一般是用 C/C++ 编写,这里的 lib 库可能包含4种不同类型,根据 cpu 型号来分类
res 目录:用于存放资源文件,存放在该文件夹下的所有文件都会映射到 Android 工程的 .R 文件中,生成对应的 ID,
dex文件结构
文章参考链接
Java 版本 dex 文件解析源码
Dex 文件整体结构如下所示
从 head 到 data 之间的结构体可以理解为 “索引结构区”,真实的数据存放在 data 数据区
对应到文件中就长这个样子
dex head
由结构体 DexHeader 定义,占用 0x70 个字节,声明如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct DexHeader { u1 magic[8]; // 魔数 u4 checksum; // adler 校验值 u1 signature[kSHA1DigestLen]; // sha1 校验值 u4 fileSize; // DEX 文件大小 u4 headerSize; // DEX 文件头大小 u4 endianTag; // 字节序 u4 linkSize; // 链接段大小 u4 linkOff; // 链接段的偏移量 u4 mapOff; // DexMapList 偏移量 u4 stringIdsSize; // DexStringId 个数 u4 stringIdsOff; // DexStringId 偏移量 u4 typeIdsSize; // DexTypeId 个数 u4 typeIdsOff; // DexTypeId 偏移量 u4 protoIdsSize; // DexProtoId 个数 u4 protoIdsOff; // DexProtoId 偏移量 u4 fieldIdsSize; // DexFieldId 个数 u4 fieldIdsOff; // DexFieldId 偏移量 u4 methodIdsSize; // DexMethodId 个数 u4 methodIdsOff; // DexMethodId 偏移量 u4 classDefsSize; // DexCLassDef 个数 u4 classDefsOff; // DexClassDef 偏移量 u4 dataSize; // 数据段大小 u4 dataOff; // 数据段偏移量 };
u 表示无符号数,u1 u2 u4 u8 分别为 8 16 32 64 位无符号数
magic 字段标识了一个有效的 dex 文件,目前它的值固定为 dex.035.
1 2 3 64 65 78 0a 30 33 35 00 对应内容为 dex + 换行符 + DEX 版本 + NUl(空)
checksum 为 dex 文件的校验和,用于判断 dex 文件是否被损坏或篡改
mapoff 字段指定了 DexMapList 结构的文件偏移
dex map list
由结构体 DexMapList 所定义
1 2 3 4 5 struct DexMapList { u4 size; //DexMapItem 的个数 DexMapItem list[1]; //DexMapItem 结构数组 }
结构体 DexMapItem 结构声明如下
1 2 3 4 5 6 7 struct DexMapItem { u2 type; //kDexType开头的类型 u2 unused; //未使用,用于字节对齐 u4 size; //指定类型的个数 u4 offset; //指定类型数据的文件偏移 }
下面就是一个例子,需要注意的是这里字节存放的顺序是,内存中存放的顺序是 0100,但实际上是 0001
结构体中的 type 字段为一个枚举常量,如下所示,通过对比数值可以知道它的具体类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 enum { kDexTypeHeaderItem = 0x0000, kDexTypeStringIdItem = 0x0001, kDexTypeTypeIdItem = 0x0002, kDexTypeProtoIdItem = 0x0003, kDexTypeFieldIdItem = 0x0004, kDexTypeMethodIdItem = 0x0005, kDexTypeClassDefItem = 0x0006, kDexTypeMapList = 0x1000, kDexTypeTypeList = 0x1001, kDexTypeAnnotationSetRefList = 0x1002, kDexTypeAnnotationSetItem = 0x1003, kDexTypeClassDataItem = 0x2000, kDexTypeCodeItem = 0x2001, kDexTypeStringDataItem = 0x2002, kDexTypeDebugInfoItem = 0x2003, kDexTypeAnnotationItem = 0x2004, kDexTypeEncodedArrayItem = 0x2005, kDexTypeAnnotationsDirectoryItem = 0x2006, }; 上图中的例子:0x0001---->kDexTypeStringIdItem
根据这个名字也可以理解,map 地图,有指向、引导的意思,我们可以根据 dex map list 找到不同 Type 的内容在 dex 文件中的位置和大小,从而可以对其内容进行解析
string_ids
由 DexStringId 结构体对象构成,可以根据该结构体对象的字段在 dex 文件中获取字符串
在 KDexTypeIdItem 的 size 和 offset 以及 DexHeader 的 stringIdsSize 和 stringIdsOff 字段都有说明,分别是连续的 DexStringId 对象个数、第一个对象所在的偏移处
DesStringId 结构体只有一个字段,声明如下
1 2 3 struct DexStringId { u4 stringDataOff; //字符串数据偏移 };
下面的例子就表明在 0x1CE 偏移处有字符串 “”
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void parseDexString() { log("\nparse DexString"); try { int stringIdsSize = dex.getDexHeader().string_ids__size; for (int i = 0; i < stringIdsSize; i++) { int string_data_off = reader.readInt(); byte size = dexData[string_data_off]; // 第一个字节表示该字符串的长度,之后是字符串内容 String string_data = new String(Utils.copy(dexData, string_data_off + 1, size)); DexString string = new DexString(string_data_off, string_data); dexStrings.add(string); log("string[%d] data: %s", i, string.string_data); } } catch (IOException e) { e.printStackTrace(); } }
type_ids
由结构体 DexTypeId 对象构成,用于表示类型信息
在 KDexTypeTypeIdItem 以及 DexHeader 中也均有说明,说明字段和 string_ids 十分类似
DexType 结构体定义如下
1 2 3 4 struct DexTypeId { u4 descriptorIdx; /指向 DexStringId 列表的索引 }
意思就是 descriptorIdx 的值为字符串索引,指向 string_ids 中的内容,通过字符串索引得到的值在 data 池中得到字符串,最后根据所得字符串解析对应类型
下面的例子,通过 descriptorIdx 的值 0x8 指向 string_ids 中的 string_id[8],通过 string_id[8] 的值找到字符串 ‘V’,字符串 ‘V’ 对应类型就是 void
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void parseDexType() { log("\nparse DexTypeId"); try { int typeIdsSize = dex.getDexHeader().type_ids__size; for (int i = 0; i < typeIdsSize; i++) { int descriptor_idx = reader.readInt(); DexTypeId dexTypeId = new DexTypeId(descriptor_idx, dexStringIds.get(descriptor_idx).string_data); dexTypeIds.add(dexTypeId); log("type[%d] data: %s", i, dexTypeId.string_data); } } catch (IOException e) { e.printStackTrace(); } }
proto_ids
由结构体 DexProtoId 对象构成,该结构体是一个方法声明结构体
DexProtorId 结构体定义如下
1 2 3 4 5 6 struct DexProtoId { u4 shortyIdx; //指向 DexStringId 列表的索引 u4 returnTypeIdx; //指向 DexTypeId 列表的索引 u4 parametersOff; //指向 DexTypeList 的偏移 };
shortyIdx:方法声明字符串
returnTypeIdx:方法返回类型字符串
parametersOff:指向一个 DexTypeList 结构体,存放了方法的参数列表
DexTypeList 结构体的定义如下
1 2 3 4 5 6 7 8 9 10 struct DexTypeList { u4 size; //接下来 DexTypeItem 结构体个数 DexTypeItem list[1]; //DexTypeItem 结构 }; struct DexTypeItem { u2 typeIdx; //指向 DexTypeId 列表的索引 };
例子如下:
根据这些字段,获得 DexStringId、DexTypeId 列表的索引值 0x2、0x0 以及 DexTypeList 的偏移
根据索引值找到方法声明、方法返回类型
根据偏移量找到 DexTypeList 结构体,根据该结构中的字段可以获取参数个数以及参数类型
最后就可以获得结果
方法声明
返回类型
参数列表
III
I(int)
2个参数,类型均为I
解析代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void parseDexProto() { log("\nparse DexProto"); try { int protoIdsSize = dex.getDexHeader().proto_ids__size; for (int i = 0; i < protoIdsSize; i++) { int shorty_idx = reader.readInt(); int return_type_idx = reader.readInt(); int parameters_off = reader.readInt(); DexProtoId dexProtoId = new DexProtoId(shorty_idx, return_type_idx, parameters_off); log("proto[%d]: %s %s %d", i, dexStringIds.get(shorty_idx).string_data, dexTypeIds.get(return_type_idx).string_data, parameters_off); if (parameters_off > 0) { parseDexProtoParameters(parameters_off); } dexProtos.add(dexProtoId); } } catch (IOException e) { e.printStackTrace(); } }
fieId_ids
DexFieldId结构体对象组成,结构中的数据均为索引值,该结构体定义如下
1 2 3 4 5 6 struct DexFieldId { u2 classIdx; //类的类型,指向 DexTypeId 列表的索引 u2 typeIdx; //字段类型,指向 DexTypeId 列表的索引 u4 nameIdx; //字段名,指向 DexStringId 列表的索引 };
得到的结果如下
类类型
字段类型
字段名
Ljava/lang/System;
Ljava/io/PrintStream;
out
method_ids
由 DexMethodId 结构体对象组成,结构中的数据也均为索引值,指明了方法所在的类、方法的声明以及方法名
该结构体定义如下
1 2 3 4 5 6 struct DexMethodId { u2 classIdx; /* 类的类型,指向 DexTypeId 列表的索引 */ u2 protoIdx; /* 声明类型,指向 DexProtoId 列表的索引 */ u4 nameIdx; /* 方法名,指向 DexStringId 列表的索引 */ };
获得的结果如下
类类型
方法声明
方法名
Main
III(int(int,int))
add
class_def
由 DexClassDef 结构体组成,该结构体的声明如下
1 2 3 4 5 6 7 8 9 10 11 struct DexClassDef { u4 classIdx; /* 类的类型,指向 DexTypeId 列表的索引 */ u4 accessFlags; /* 访问标志 */ u4 superclassIdx; /* 父类类型,指向 DexTypeId 列表的索引 */ u4 interfacesOff; /* 接口,指向 DexTypeList 的偏移 */ u4 sourceFileIdx; /* 源文件名,指向 DexTypeStringId 列表的索引 */ u4 annotationsOff; /* 注解,指向 DexAnnotationsDirectoryItem 结构 */ u4 classDataOff; /* 指向 DexClassData 结构的偏移 */ u4 staticValuesOff; /* 指向 DexEncodedArray 结构的偏移 */ };
DexClassDef 比上面的结构体更加复杂
classdex:索引值,表示类的类型
accessFlags:类的访问标志,是以 ACC_ 开头的一个枚举值
superclassIdx:父类类型索引值
interfacesOff:如果类中有接口的声明或实现,该字段会指向一个 DexTypeList 结构,否则该值为 0
sourceFileIdx:字符串索引值,表示类所在的源文件的名称
annotationsOff:指向注解目录结构,根据类型不同会有注解类、注解方法、注解字段与注解参数。如果类中没有注解,则该值为0
classDataOff:指向 DexclassData 结构,是类的数据部分
StaticValuesOff:指向 DexEncodedArray 结构,记录类中的静态数据
其中的 DexClassData 结构声明在 DexClass.h 文件中,声明如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct DexClassData { DexClassDataHeader header; // 指定字段与方法的个数 DexField* staticFields; // 静态字段,DexField 结构 DexField* instanceFields; // 实例字段,DexField 结构 DexMethod* directMethods; // 直接方法,DexMethod 结构 DexMethod* virtualMethods; // 虚方法,DexMethod 结构 }; 其中的 DexClassDataHeader 结构记录了当前类中字段与方法的数目,在 DexClass.h 文件中声明,声明如下 struct DexClassDataHeader { u4 staticFieldsSize; // 静态字段个数 u4 instanceFieldsSize; // 实例字段个数 u4 directMethodsSize; // 直接方法个数 u4 virtualMethodsSize; // 虚方法个数 }; Ps:在 DexClass.h 文件中所有结构的 u4 类型都是 uleb128 类型,这是一个可变长度类型,由于大多数情况下这些字段的值可以用小于2个字节的空间来表示,所以采用 uleb128 会节省更多的储存空间
DexField 结构描述了字段的类型与访问标志,它的结构声明如下
1 2 3 4 struct DexField { u4 fieldIdx; /* 指向 DexFieldId 的索引 */ u4 accessFlags; };
DexMethod 结构描述方法的原型、名称、访问标志以及代码数据块,它的结构声明如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct DexMethod { u4 methodIdx; /* 指向 DexMethodId 的索引 */ u4 accessFlags; /* 访问标志 */ u4 codeOff; /* 指向 DexCode 结构的偏移 */ }; 其中的 codeOff 字段指向了一个 DexCode 结构体,DexCode 结构体描述了方法更详细的信息以及方法中指令的内容,该结构体声明如下 struct DexCode { u2 registersSize; // 使用的寄存器个数 u2 insSize; // 参数个数 u2 outsSize; // 调用其他方法时使用的寄存器个数 u2 triesSize; // try/catch 语句个数 u4 debugInfoOff; // 指向调试信息的偏移 u4 insnsSize; // 指令集的个数,以2字节为单位 u2 insns[1]; // 指令集 /* 2字节空间用于结构体对齐 */ /* try_item[triesSize] DexTry 结构 */ /* Try/Catch 中 handler 的个数 */ /* catch_handler_item[handlersSize],DexCatchHandler结构 */ };
终于到了存放指令集的结构了,我们从上到下来好好分析一下
图片中的红色方框上下内容一一对应,按照结构体中各个字段的说明,理解其含义难度不大,在这里就不展开细说
关注最后的 class_data_off 字段,它指向了一个 DexClassData 结构体,我们根据其偏移量 0x28E 找到结构体中的内容
前面 4 个字段为结构体 DexClassDataHeader 中的内容,4个 uleb128 值结果分别为 0、0、2、1,表示该类不含字段,有2个直接方法与1个虚方法
直接对 DexMethod 进行解析
第一个字段的为 DexMethodId 索引,值为 0x0,得到 “” 方法;第二个字段为访问标志,类型为 ACC_PUBLIC|ACC_CONSTRUCTOR;第三个字段为 DexCode 的偏移,值为0x150
从 0x150 处开始解析 DexCode 结构体
前面三个字段得出的结果为:寄存器个数、参数、内部函数使用寄存器的个数都为1个。insns_size 的字段值为 4,说明方法中有4条指令,指令为 “7010 0400 0000 0e00”。之后按照 dalvik 指令分析的方式去分析这些指令即可
根据 70 查找获得 opcode 为 invoke-direct
指令格式标识为 35c
查找到 35c 的指令格式为 “A|G|op BBBB F|E|D|C”
且有 7 种表示方式
[A=5] op {vC, vD, vE, vF, vG}, meth@BBBB
[A=5] op {vC, vD, vE, vF, vG}, site@BBBB
[A=5] op {vC, vD, vE, vF, vG}, type@BBBB
[A=4] op {vC, vD, vE, vF}, kind @BBBB
[A=3] op {vC, vD, vE}, kind @BBBB
[A=2] op {vC, vD}, kind @BBBB
[A=1] op {vC}, kind @BBBB
[A=0] op {}, kind @BBBB
根据指令 7010 0400 0000 得到 A、G 的值分别为 1、0;BBBB 为 0x4,F=E=D=C=0。按照格式 [A=1] op {vC}, kind @BBBB 去解析,由于 BBBB 为 kind@ 类型,它是指向 DexMethod 列表的索引值,通过其索引找到方法名 “”。指令 0e00 直接查表得到 return-void,最后得到结果如下的指令代码
1 2 7010 0400 0000 invoke-direct {0}, Ljava/lang/Object; .<init>: ()v 0e00 return-void
第五章——静态分析Android程序
一直都搞不清楚各种类型文件的关系,大致理了一下也不知道对不对
apk文件就相当于是压缩包,里面有在 dalvik 虚拟机中运行的 dex 文件以及各种资源和运行 apk 时所需的库
对 apk 文件进行反编译就相当于将 dex 文件转化为具有进一步可读性的 smali 汇编
通过工具 dex2jar 可以直接将 .dex 文件转化为 .jar 文件,.jar 文件就是反编译后的 java 源码文件。
工具 jd-gui 能够查看反编译后的 Java 源码文件,简单来说就是能将 .jar 文件变成 java 代码展现出来
之前一直在纠结这个 apktool 反编译之后的文件和工具反编译之后的文件之间有什么关系,那现在看来就是没啥大关系
smali代码阅读
循环语句
switch 分支语句
看 switch 分支语句推理出所对应的 java 代码没有什么太大的难度,重点来讲讲如何从 dex 文件中分析得出 switch 语句中的 case 语句块的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 .method private packedSwitch(I)Ljava/lang/String; .locals 1 .parameter "i" .prologue .line 21 const/4 v0, 0x0 .line 22 .local v0, str:Ljava/lang/String; packed-switch p1, :pswitch_data_0 # packed-switch 分支,pswitch_data_0 为 case 区域 .line 36 # default 分支 const-string v0, "she is a person" .line 39 :goto_0 return-object v0 .line 24 :pswitch_0 const-string v0, "she is a baby" .line 25 goto :goto_0 .line 27 :pswitch_1 const-string v0, "she is a girl" .line 28 goto :goto_0 .line 30 :pswitch_2 const-string v0, "she is a woman" .line 31 goto :goto_0 .line 33 :pswitch_3 const-string v0, "she is an obasan" .line 34 goto :goto_0 .line 22 nop :pswitch_data_0 .packed-switch 0x0 # case 区域,数值起始值为 0 :pswitch_0 # pswitch_ 后的数值为 case 分支需要判断的值 :pswitch_1 :pswitch_2 :pswitch_3 .end packed-switch .end method
指令 packed_switch 表示 switch 分支的开始,该指令对应的机器码为 2B 02 13 00 00 00,具体分析如下
2B 为 opcode 代表指令 packed_switch,该指令在 Dalvik 中的格式为:packed_switch vAA,+BBBBBBBB。其中 “+BBBBBBBB” 为 packed-switch-payload 格式的偏移
02 为 VAA 即寄存器 p1
00000013 为偏移量 0x13
在 ida 中找到 packed_switch 语句的偏移地址:0x2cb1a。计算得出结构体 packed-switch-payload 的偏移地址:0x2cb1a+2*0x13=0x2cb40
找到 packed-switch-payload 所对应的内容
该结构体的格式如下
1 2 3 4 5 6 7 struct packed-switch-payload { ushort ident; //值固定为0x100 ushort size; //case数目 int first_key; //初始case的值 int[] targets; //每个case相对switch指令处的偏移 };
也就是说
那么 6,9,0xc,0xf 都代表着4个 case 对于语句 packed_switch 的偏移量,再根据 packed_switch 语句的地址 0x2cb1a 就可以计算得出 case0-case3 每一块的偏移地址
switch 分支除了 packed_switch 还有 sparse_switch,它们之间的区别就在于 packed_switch 的 case 判断值是顺序递增的,而 sparse_switch 的 case 判断值是由case 值-> case 标号 的形式给出的
1 2 3 4 5 6 :sswitch_data_0 .sparse-switch 0x5 -> :sswitch_0 #case 5 0xf -> :sswitch_1 #case 15(0xf) 0x23 -> :sswitch_2 #case 35(0x23) 0x41 -> :sswitch_3 #case 65(0x41)
其他分析过程都和 packed_switch 大同小异,由于 case 判断值不再是顺序递增,因此 spare-switch-payload 结构和上面的 packed 也有所区别
1 2 3 4 5 6 7 struct sparse-switch-payload { ushort ident; //值固定为0x0200 ushort size; //case数目 int[] keys; //每个case的值,顺序从低到高 int[] targets; //每个case相对switch指令处的偏移 };
也就是说
try/catch 语句
不同于 switch 语句,try/catch 语句没有对应的 dalvik 指令,它是通过相关的数据结构来保存相关信息,在 dex 文件格式中有 Dexcode 数据结构,声明如下
1 2 3 4 5 6 7 8 9 10 11 12 13 struct DexCode { u2 registersSize; // 使用的寄存器个数 u2 insSize; // 参数个数 u2 outsSize; // 调用其他方法时使用的寄存器个数 u2 triesSize; // try/catch 语句个数 u4 debugInfoOff; // 指向调试信息的偏移 u4 insnsSize; // 指令集的个数,以2字节为单位 u2 insns[1]; // 指令集 /* 2字节空间用于结构体对齐 */ /* try_item[triesSize] DexTry 结构 */ /* Try/Catch 中 handler 的个数 */ /* catch_handler_item[handlersSize],DexCatchHandler结构 */ };
通过里面的信息可以得到 try/catch 语句中每个 try 语句块的范围
如果是嵌套式的 try/catch 语句对应的 smali 语句会多产生虚拟的 try/catch,具体例子如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void a() { try { ... try { ... }catch(xxx){ ... } }catch(YYY){ ... } }
当在执行 catch(xxx) 中的代码时,如果也出现了异常,那么这个异常就会向外抛出并判断是不是 YYY 异常。smali 汇编中多出来的那个 try/catch 结构就用于处理这种情况。
idaPro 静态分析
idapro 支持对 dalvik 指令集的反汇编,对于 Android 程序来对,我们先从 apk 文件中解压得到 classes.dex 文件,然后使用 ida 对该文件进行分析
导入 dex.idc 文件,里面有大部分 dex文件的数据结构,使用 ALT+Q 修改结构体类型,CTRL+S 段间跳转,可以根据 dex 文件结构和 dex.idc 文件中的数据结构整理 .dex 文件
dex 文件的所有方法都可以通过 Exports 选项卡查看,方法的命名规则如下
例子如下
1 invoke-super {this, c}, <ref ResourceCursorAdapter.swapCursor(ref) imp. @ _def_ResourceCursorAdapter_swapCursor@LL>
其中的前半部分 invoke-super {this, c}, <ref ResourceCursorAdapter.swapCursor(ref)
前面的 ref 为 swapCursor( ) 方法的返回类型,后面的 ref 为该方法的参数类型
后半部分 imp. @ _def_ResourceCursorAdapter_swapCursor@LL> 是 ida 的自动识别,imp 表明该方法为 Android SDK 中的 API,@后面的部分为 API 的声明,类名于方法名之间使用下划线做分隔