开始学习Android逆向

APK文件结构

随便使用 7-zip 打开一个 apk 文件

列出一些关键的文件和目录的用途

文件 注释
assets目录 存放APK的静态资源文件,比如视频,音频,图片等
lib 目录 armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件
META-INF目录 保存应用的签名信息
res目录 res目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计
AndroidManifest.xml APK的应用清单信息,它描述了应用的名字,版本,权限,引用的库文件等等信息
classes.dex文件 classes.dex是java源码编译后生成的java字节码文件,APK运行的主要逻辑
resources.arsc文件 resources.arsc是编译后的二进制资源文件,它是一个映射表,映射着资源和id,通过R文件中的id就可以找到对应的资源

双开

最简单的思路就是给 apk 换个包名,换个包名之后系统识别会是两个不同的 apk,就能实现共存。

APK逆向

什么是JVM、Dalvik、ART

JVM是JAVA虚拟机,运行JAVA字节码程序

Dalvik是Google专门为Android设计的一个虚拟机,Dalvik有专属的文件执行格式dex(Dalvik executable)

Art(Android Runtime)相当于Dalvik的升级版,本质与Dalvik无异

smali及其语法

smali是Dalvik的寄存器语言,smali代码是dex反编译而来的。

关键字

名称 注释
.class 类名
.super 父类名,继承的上级类名名称
.source 源名
.field 变量
.method 方法名
.register 寄存器
.end method 方法名的结束
public 公有
protected 半公开,只有同一家人才能用
private 私有,只能自己使用
.parameter 方法参数
.prologue 方法开始
.line xxx 位于第xxx行

数据类型对应

smali类型 java类型 注释
V void 无返回值
Z boolean 布尔值类型,返回0或1
B byte 字节类型,返回字节
S short 短整数类型,返回数字
C char 字符类型,返回字符
I int 整数类型,返回数字
J long (64位 需要2个寄存器存储) 长整数类型,返回数字
F float 单浮点类型,返回数字
D double (64位 需要2个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

常用指令

关键字 注释
const 重写整数属性,真假属性内容,只能是数字类型
const-string 重写字符串内容
const-wide 重写长整数类型,多用于修改到期时间。
return 返回指令
if-eq 全称equal(a=b),比较寄存器ab内容,相同则跳
if-ne 全称not equal(a!=b),ab内容不相同则跳
if-eqz 全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez 全称not equal zero(a!=0),a不等于0则跳
if-ge 全称greater equal(a>=b),a大于或等于则跳
if-le 全称little equal(a<=b),a小于或等于则跳
goto 强制跳到指定位置
switch 分支跳转,一般会有多个分支线,并根据指令跳转到适当位置
iget 获取寄存器数据

工具

工具清单

  • Android Studio:开发安卓应用(包括模块),毕竟会开发才能会逆向
  • Jeb:APK逆向的优秀工具
  • JADX:开源的 APK 逆向工具。

目前就这么多,后面遇到再补…

签名校验

这个跟传统密码学的校验还不太一样,这个校验主要校验的可以认为是公钥的 hash。

传统的签名是:我将apk的重要文件进行hash之后,将hash用私钥加密得到一串签名,同时会放置一串公钥信息,验证签名的过程就是把文件 hash 和签名做公钥加密之后比较是否一致,这只是验证签名的完整性,而且通常来说不可能放在代码层校验,因为代码本身是签名要包括的内容。

而真正的签名校验其实是在上述完整性保证的前提下,去验证公钥信息是否一致,因为只有拥有对应的私钥才能够签名,就意味着该 APK 肯定是由真正的开发者发布的。

Hook框架模块编写准备

以Xposed框架为例,在开发 app 时候需要做以下几步以便于 LSPOSED 等 Xposed 框架识别和加载。

  1. 导入 Xposed API 的 jar 包
  2. 打开 build.gradle 将该 jar 的依赖设置为 compileOnly
  3. 在 assets 目录新建 xposed_init 文件,将入口设置为 hook 的主框架
  4. 修改 AndroidManifest.xml 文件

OK,一步步来实现这一步骤,第一步将 jar包 拖入项目中

第二步,打开 build.gradle 或者 build.gradle.kts 找到对应依赖,导入声明方式设置为 compileOnly

第三步,在 assets 目录新建 xposed_init 文件,定义入口点。

这里我指向了 Hook 类,事实上你叫什么都无所谓。

第四步,修改 AndroidManifest.xml 文件,将标签的 application 标签的内容设置为以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 是否是xposed模块,xposed根据这个来判断是否是模块 -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!-- 模块描述,显示在xposed模块列表那里第二行 -->
<meta-data
android:name="xposeddescription"
android:value="这是一个Xposed模块" />
<!-- 最低xposed版本号(lib文件名可知) -->
<meta-data
android:name="xposedminversion"
android:value="89" />

这里插一句这种 XML 格式的一些说明

标签是 XML 的主要内容,以 HTML 为例,当设置一个 a 标签时 <a href='www.example.com'>test</a>,其中

  • test 是 a 标签的内容
  • href 是 a 标签的属性名,www.example.com 就是这个 a 标签 href 属性对应的属性值。

标签不一定是成双成对出现的,可以是“孤儿标签”,例如 HTML 的 <br/>,它仅表示换行的意思,但是有些成双成对的标签它没有内容也可以成为孤儿标签,例如上面的例子可以变成 <a href='www.example.com'/>,这在语法并没有问题,只是这个链接没有可以点击的对象而已。

查看配置文件可以发现,这个 application 默认是一个“孤儿标签”,想给它加内容就需要把它改写成成双成对的形式,以便于向该标签添加内容。

好,做完这几步,这个 app 已经是一个合格的 Xposed 框架了。

hook方法的技巧

先总结一下 hook 的方法:

  • findAndHookMethod:根据方法名和参数列表 hook 指定参数,适用于参数简单的方法。
  • hookAllMethods:仅根据方法名进行 hook,由于 JAVA 存在方法重载,因此可能指定名字会 hook 到原本不想 hook 的方法,仅适用于一些复杂参数的方法。
  • findAndHookConstructor:hook 指定的构造方法。
  • hookAllConstructors:hook 构造方法使用,多用于设置实例变量。
  • setStaticObjectField:设置静态成员值(除基本类型之外的静态变量都可以用这个方法)
  • callStaticMethod:调用某静态方法
  • callMethod:调用非静态方法

这里 hook 的 demo 选择了正己大神对应课程的教程包。

hook普通方法

先写一个 hello world 模块。

用 jadx 打开要 hook 的包,选择 hook a方法,直接复制代码片段,该导的包就从 xposed 里面导就行。

这里写出如下代码

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
package com.xiaoji.xposeddemo;

import android.util.Log;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
XposedBridge.log(param.args[0].toString());
String newstr = "xia0ji2333";
param.args[0] = newstr;
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Log.e("zj2595","返回值修改之前:"+param.getResult().toString()+"\n");
param.setResult("被我修改了233");
}
});
}
}

其中 beforeHookedMethod 用于在调用之前修改函数,通常用于监控参数等目的,在这里也可以选择直接拦截掉,afterHookedMethod 用于在调用之后修改函数,通常用于修改返回值,这里我分别实现修改参数和修改返回值的功能。

加载 xposed 模块,安装之后在 lsposed 里面选择

最后启动 LOG,用 Android Studio 抓日志,hook 成功。

hook复杂方法

com.zj.wuaipojie.Demo 包中的 complexParameterFunc 方法为例。

这个方法甚至有一个 HashMap 泛型,对于这种方法,如果不关心它的复杂参数,完全可以使用 hookAllMethodshook

代码如下:

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
package com.xiaoji.xposeddemo;

import android.util.Log;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}
Class a = loadPackageParam.classLoader.loadClass("com.zj.wuaipojie.Demo");
XposedBridge.hookAllMethods(a, "complexParameterFunc", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
String a1 = param.args[0].toString();
String a2 = param.args[1].toString();
Log.e("zj2595",a1);
Log.e("zj2595",a2);
param.args[0] = "hooked_"+a1;
}
});

}
}

最终结果也是 hook 上了:

hook替换方法

有些方法可能有副作用,例如 a 方法调用了 exit 函数,而我 hook 之后似乎没有办法阻止这个事情发生,这个时候就需要进行替换。

假设这里输出的 这是替换函数 就是一个副作用函数,现在我想阻止它输出。

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
package com.xiaoji.xposeddemo;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}
Class a = loadPackageParam.classLoader.loadClass("com.zj.wuaipojie.Demo");
XposedBridge.hookAllMethods(a,"repleaceFunc",new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
return "";
}
});

}
}

这里需要使用 XC_MethodReplacement 并实现 replaceHookedMethod 接口,直接给一个 return "" 即可。

可以看到原本应该输出的 这是替换函数 没有了。

hook变量的技巧

在说这个之前,区分一下两种类型的变量,一种是静态的,它只有一份,一种是非静态的,它随具体实例。

比如人这个类,数量就是对于整个人类而言的,它只有一个,所以通常来说它的定义是静态的变量。

再比如身高对人类而言,每个人都有一个确定的身高,所以通常来说它只能定义为非静态变量。

Hook静态变量

使用 setStaticObjectField 方法即可。

以图中的静态方法为例

直接调用方法修改即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xiaoji.xposeddemo;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}

final Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader);
XposedHelpers.setStaticObjectField(clazz, "staticField", "Hooked Static Field Value by xia0ji233");
}
}

结果也是成功修改:

Hook非静态变量

直接看代码和结果吧,主要使用 hookAllConstructors 方法去做

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
package com.xiaoji.xposeddemo;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}

final Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader);
XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//param.thisObject获取当前所属的对象
Object ob = param.thisObject;
XposedHelpers.setIntField(ob,"privateInt",114514);
}
});
}
}

这里 hook 该类所有的构造函数,并在构造函数结束之后(实现 afterHookedMethod 方法)对成员的属性进行修改,成功修改实例的成员值。

Hook构造函数

这里有参构造和无参构造一起 hook,用的是同一种方法,参数需要自己加才能 hook 到指定构造方法。

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
package com.xiaoji.xposeddemo;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}

XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.e("zj2595","Demo Constructor Hooked");
}
});
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.e("zj2595","Demo(String) Constructor Hooked, param: " + param.args[0]);
}
});
}
}

因为无参构造调用了有参构造,所以无参的构造 hook 先输出,再输出有参构造,如果是调用结束后的 hook 输出顺序则会相反。

其它类型的hook

主动调用

当需要主动调用某方法时,需要使用方法 callMethod,不过这里只能调用非静态方法,因此必须拿到实例才能调用,实例可以自己新建也可以用某些方法拿到。

如果是静态类,那就传类和静态方法名即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xiaoji.xposeddemo;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}
Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo",loadPackageParam.classLoader);
XposedHelpers.callMethod(clazz.newInstance(),"refl");
}
}

这里是非静态方法的主动调用,如果要调用静态方法,则换成

1
2
Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo",loadPackageParam.classLoader);
XposedHelpers.callMethod(clazz,"staticFunc");

这里展示一下调用非静态方法的结果:

反射调用

先介绍一下反射吧(实则我也不懂)

讲反射之前先讲一讲正射,正射是加载类最常见的一种方式。

1
2
Apple apple = new Apple();
apple.setPrice(4);

通过导入包 apple 并直接调用构造方法构造类的方法称为正射,正射需要在编译阶段就确定类。

反射则是通过运行时动态加载类。

1
2
3
4
5
Class clz = Class.forName("com.xiaoji.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

反射调用

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
package com.xiaoji.xposeddemo;
import android.util.Log;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return ;
}

Class clazz = Class.forName("com.zj.wuaipojie.Demo",false,loadPackageParam.classLoader);
Method refl = clazz.getDeclaredMethod("refl");
refl.setAccessible(true);
refl.invoke(clazz.newInstance());
}
}

成功通过反射调用方法,调用私有方法时,需要使用 setAccessible 把权限设置一下。

Hook内部类

基本与 hook 普通方法一致,使用 findAndHookMethod 方法,在传类名的时候需要使用类似 com.zj.wuaipojie.Demo$InnerClass 的写法,用 $ 区分内部类。


后记

对于 Android 而言,我算是迈出了一小步吧,最近的几个月,可能在对新知识的学习上懈怠了不少,Android 对我而言是很早就进规划,但是一直没有动力学习的知识。感谢吾爱破解的正己大神,写出如此完善简洁易懂的教材,让我在学习的路上能够节省大量时间,也能对学习的知识一直保持一定的兴趣。

希望接下来三个月内,可以把已有的知识点学完,等着过年抢吾爱的安卓红包了