在安卓逆向的过程中常常会面临三个场景
想要了解某个方法是否有被调用
经过常规的代码分析后,想要知道App某些方法内部某些变量的值
App在Jave层的一些安全检测代码在我们的研究阶段需要屏蔽及修改
以上场景虽然使用Xposed、Frida、Java层代码动态调试效率会更高,但是目前市面上一些主流app存在对这些hook框架的检测,因此Smali代码注入与修改同样十分具有价值。
本文将分为以下四个部分,完成某电商app的smali代码log注入与原代码修改
相关软件下载
本教程中用到的所有软件都保存在该网盘中
https://pan.baidu.com/s/1XsMANMxzdxlrQAnEjIedxQ
密码:zzkk
首先,什么是smali代码?
Smali 实质上就是安卓的Dalvik虚拟机操作码, Java 字节码,一句 Java 代码会对应多句 Smali 代码
下面来看一个例子
Java源代码
package cn.com.zifeng.utils;
// 进行简单的加法运算
public class Calculate {
public static int add(int a,int b){
return a+b;
}
}
Smali代码解析
文件基本结构
.class
类名修饰符.super
父类的类名.source
源文件名.implements
实现的接口.annotation
注解.field
字段.method
方法基本方法体
.method 描述符 方法名(参数类型)返回类型
方法代码...
.end method
寄存器(暂时存放数据的地方)
寄存器分为普通寄存器及参数寄存器,普通寄存器为v(0-n),参数寄存器p(0-N),在上面的例子中方法的一开始标明了*.locals 1*,此处用来声明普通寄存器的个数
ps:在非静态方法中,会默认申请一个参数寄存器p0,用来存储当前类的实例(this)
类型参照
Java | Smali |
---|---|
void | V |
boolean | Z (不同) |
byte | B |
short | S |
char | C |
int | I |
long | J (不同) |
float | F |
double | D |
例子:
I ==》 int
Ljava/lang/String ==》 String
[i ==> int[]
字段声明
声明语法:
.field 描述符 字段名:字段类型
例子:
Java:
public int number;
Smali:
.field public number:I;
方法调用
调用语法:
invoke-xxxxxx {参数列表}, 类名->方法名(参数类型)返回类型
指令类型:
指令名称 | 含义 |
---|---|
invoke-virtual | 调用虚方法 (涉及Java的多态,例如定义为: Object string = “123”; string.equals(“123”); 此处实际调用了String的equals方法,Smali中将会使用invoke-virtual调用) |
invoke-direct | 直接调用方法(直接调用无法被重写的方法,提高效率) |
invoke-static | 调用静态方法 |
invoke-super | 调用父类方法 |
invoke-interface | 调用接口方法 |
基础语法
语法 | 语义 |
---|---|
.field public isNull:z | 定义变量 |
.method | 定义方法 |
.end method | 方法结束 |
.param | 方法参数 |
.prologue | 方法开始 |
.line 12 | 此方法位于第12行 |
const/4 v0, 0x0 | 把0x0赋值给v0 |
return-void | 函数返回void |
new-instance | 创建实例 |
iput-object | 对象赋值 |
iget-object | 调用对象 |
常用跳转语法
语法 | 语义 |
---|---|
if-eq vA, vB, :cond_ | 如果vA等于vB则跳转到:cond_ |
if-ne vA, vB, :cond_ | 如果vA不等于vB则跳转到:cond_ |
if-lt vA, vB, :cond_ | 如果vA小于vB则跳转到:cond_ |
if-ge vA, vB, :cond_ | 如果vA大于等于vB则跳转到:cond_ |
if-gt vA, vB, :cond_ | 如果vA大于vB则跳转到:cond_ |
if-le vA, vB, :cond_ | 如果vA小于等于vB则跳转到:cond_ |
if-eqz vA, :cond_ | 如果vA等于0则跳转到:cond_ |
if-nez vA, :cond_ | 如果vA不等于0则跳转到:cond_ |
if-ltz vA, :cond_ | 如果vA小于0则跳转到:cond_ |
if-gez vA, :cond_ | 如果vA大于等于0则跳转到:cond_ |
if-gtz vA, :cond_ | 如果vA大于0则跳转到:cond_ |
if-lez vA, :cond_ | 如果vA小于等于0则跳转到:cond_ |
介绍
apktool主要用于逆向apk文件。它可以将资源解码,并在修改后可以重新构建它们。它还可以执行一些自动化任务,例如构建apk。
主要功能
将资源解码成原来的形式(包括resources.arsc,class.dex等)
将解码的资源重新打包成apk/jar
组织和处理依赖于框架资源的APK
Smali调试
执行自动化任务
使用方法
这里不对apktool的全部功能做详细介绍,这里主要介绍在该教程中用到的功能,反编译/重编译
反编译
反编译前准备:
具体步骤:
将该apk文件放置到apktool的相同目录下
运行cmd窗口切换至该目录
执行命令:
apktool -r d xxx.apk
// 参数说明
-r表示不反编译资源文件,如果不加-r参数在重打包过程当中可能会出现资源找不到导致打包失败的问题
d表示反编译
重编译
重编译前的准备:
具体步骤:
运行cmd切换至apktool目录
重编译apk,执行命令
apktool b xxx
xxx为上一步反编译生成的xxx.apk同名文件夹
执行完毕后在该同名文件夹下的dist目录会生成一个未签名的apk文件
cmd切换至该文件夹下的dist目录下
生成密钥文件,执行命令
E:\jdk1.8_271\bin\keytool.exe -genkeypair -alias app.keystore -keyalg RSA -validity 100 -keystore app.keystore
// 参数说明
开头为keytool.exe 文件的全路径,该文件位于jdk中
-genkeypair 为生成密钥对
-alias 为处理条目别名
-keyalg 密钥算法名称
-validity 有效天数
-keystore 生成密钥库名称
使用该keystore对apk进行签名,输入命令
E:\jdk1.8_271\bin\jarsigner.exe -verbose -keystore app.keystore -signedjar com.xunmeng.pinduoduo.apk com.xunmeng.pinduoduo.apk app.keystore
// 参数说明
开头为jarsigner.exe文件的全路径,该文件位于jdk中
-verbose 签名时候输出详细信息
-keystore 指定密钥库
-signedjar 签名后生成的apk名称,同名即可
其他参数:
介绍
通过自定义log类注入到app的目录下,并且在app执行过程中完成调用,常用于输出app执行过程中寄存器的值或调用信息。
具体步骤
具体实现
这里首先给出一份MyLog.smali代码以及其对照的java代码,如果不想自定义可以直接把该MyLog.smali代码放置到目标app相应smali文件夹中
Java代码:
import android.util.Log;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
/**
* @author: wzf
* @date: 2021/03/017 17:11:14
* @version: 1.0.0
* @description: 该类用于注入smali,静态调试打印寄存器信息
*/
public class MyLog {
// 打印List类型
public static void logD(List list) {
list.forEach(bean -> Log.d("crawler", "logList:" + toString(bean)));
}
// 打印Map类型
public static void logD(Map map) {
map.entrySet().forEach(bean -> {
Log.d("crawler", "logMap:" + toString(((Map.Entry) bean).getKey()) + "--->" + toString(((Map.Entry) bean).getValue()));
});
}
// 打印其他类型
public static void logD(Object object){
Log.d("crawler",object.getClass()+":"+toString(object));
}
// 假设没有重写toStirng方法则通过该反射方法输出field
public static String toString(Object obj) {
try{
StringBuffer strBuf = new StringBuffer();
Class cla = obj.getClass();
/**
* 对于基本数据类型和String直接返回
*/
if (cla == Integer.class || cla == Short.class || cla == Byte.class || cla == Long.class
|| cla == Double.class || cla == Float.class || cla == Boolean.class || cla == String.class
|| cla == Character.class) {
strBuf.append(obj);
return strBuf.toString();
}
/**
* 对数组类型的处理
*/
if (cla.isArray()) {
strBuf.append("[");
for (int i = 0; i < Array.getLength(obj); i++) {
if (i > 0) strBuf.append(",");
Object val = Array.get(obj, i);
if (val != null && !val.equals("")) {
strBuf.append(toString(val));
}
}
strBuf.append("]");
return strBuf.toString();
}
//获取所有属性
Field[] fields = cla.getDeclaredFields();
//设置所有属性方法可访问
AccessibleObject.setAccessible(fields, true);
strBuf.append("[");
for (int i = 0; i < fields.length; i++) {
Field fd = fields[i];
strBuf.append(fd.getName() + "=");
try {
if (!fd.getType().isPrimitive() && fd.getType() != String.class) {
strBuf.append(toString(fd.get(obj)));
} else {
strBuf.append(fd.get(obj));
}
} catch (Exception e) {
e.printStackTrace();
}
if (i != fields.length - 1)
strBuf.append(",");
}
strBuf.append("]");
return strBuf.toString();
} catch (Exception e){
return "invoke错误,错误信息:"+e;
}
}
}
Smali代码(有点长):
.class public LMyLog;
.super Ljava/lang/Object;
.source "MyLog.java"
# direct methods
.method public constructor <init>()V
.locals 0
.line 16
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method static synthetic lambda$logD$0(Ljava/lang/Object;)V
.locals 2
.param p0, "bean" # Ljava/lang/Object;
.line 20
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "logList:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-static {p0}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
const-string v1, "crawler"
invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method static synthetic lambda$logD$1(Ljava/lang/Object;)V
.locals 2
.param p0, "bean" # Ljava/lang/Object;
.line 26
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "logMap:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-object v1, p0
check-cast v1, Ljava/util/Map$Entry;
invoke-interface {v1}, Ljava/util/Map$Entry;->getKey()Ljava/lang/Object;
move-result-object v1
invoke-static {v1}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v1, "--->"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-object v1, p0
check-cast v1, Ljava/util/Map$Entry;
invoke-interface {v1}, Ljava/util/Map$Entry;->getValue()Ljava/lang/Object;
move-result-object v1
invoke-static {v1}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
const-string v1, "crawler"
invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 27
return-void
.end method
.method public static logD(Ljava/lang/Object;)V
.locals 2
.param p0, "object" # Ljava/lang/Object;
.line 32
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;
const-string v1, ":"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-static {p0}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
const-string v1, "crawler"
invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 33
return-void
.end method
.method public static logD(Ljava/util/List;)V
.locals 1
.param p0, "list" # Ljava/util/List;
.line 20
sget-object v0, L-$$Lambda$MyLog$nM9EONxxK20HWpTl1L8s_b-BcWM;->INSTANCE:L-$$Lambda$MyLog$nM9EONxxK20HWpTl1L8s_b-BcWM;
invoke-interface {p0, v0}, Ljava/util/List;->forEach(Ljava/util/function/Consumer;)V
.line 21
return-void
.end method
.method public static logD(Ljava/util/Map;)V
.locals 2
.param p0, "map" # Ljava/util/Map;
.line 25
invoke-interface {p0}, Ljava/util/Map;->entrySet()Ljava/util/Set;
move-result-object v0
sget-object v1, L-$$Lambda$MyLog$L3GR9fwRjR-BVLzaRcZHRwImXmo;->INSTANCE:L-$$Lambda$MyLog$L3GR9fwRjR-BVLzaRcZHRwImXmo;
invoke-interface {v0, v1}, Ljava/util/Set;->forEach(Ljava/util/function/Consumer;)V
.line 28
return-void
.end method
.method public static toString(Ljava/lang/Object;)Ljava/lang/String;
.locals 10
.param p0, "obj" # Ljava/lang/Object;
.line 38
new-instance v0, Ljava/lang/StringBuffer;
invoke-direct {v0}, Ljava/lang/StringBuffer;-><init>()V
.line 39
.local v0, "strBuf":Ljava/lang/StringBuffer;
invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
move-result-object v1
.line 44
.local v1, "cla":Ljava/lang/Class;
const-class v2, Ljava/lang/Integer;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Short;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Byte;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Long;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Double;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Float;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Boolean;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/String;
if-eq v1, v2, :cond_8
const-class v2, Ljava/lang/Character;
if-ne v1, v2, :cond_0
goto/16 :goto_4
.line 54
:cond_0
invoke-virtual {v1}, Ljava/lang/Class;->isArray()Z
move-result v2
const-string v3, ","
const-string v4, "]"
const-string v5, "["
if-eqz v2, :cond_4
.line 55
invoke-virtual {v0, v5}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 56
const/4 v2, 0x0
.local v2, "i":I
:goto_0
invoke-static {p0}, Ljava/lang/reflect/Array;->getLength(Ljava/lang/Object;)I
move-result v5
if-ge v2, v5, :cond_3
.line 57
if-lez v2, :cond_1
invoke-virtual {v0, v3}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 58
:cond_1
invoke-static {p0, v2}, Ljava/lang/reflect/Array;->get(Ljava/lang/Object;I)Ljava/lang/Object;
move-result-object v5
.line 60
.local v5, "val":Ljava/lang/Object;
if-eqz v5, :cond_2
const-string v6, ""
invoke-virtual {v5, v6}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z
move-result v6
if-nez v6, :cond_2
.line 61
invoke-static {v5}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v6
invoke-virtual {v0, v6}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 56
.end local v5 # "val":Ljava/lang/Object;
:cond_2
add-int/lit8 v2, v2, 0x1
goto :goto_0
.line 64
.end local v2 # "i":I
:cond_3
invoke-virtual {v0, v4}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 65
invoke-virtual {v0}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;
move-result-object v2
return-object v2
.line 69
:cond_4
invoke-virtual {v1}, Ljava/lang/Class;->getDeclaredFields()[Ljava/lang/reflect/Field;
move-result-object v2
.line 72
.local v2, "fields":[Ljava/lang/reflect/Field;
const/4 v6, 0x1
invoke-static {v2, v6}, Ljava/lang/reflect/AccessibleObject;->setAccessible([Ljava/lang/reflect/AccessibleObject;Z)V
.line 75
invoke-virtual {v0, v5}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 76
const/4 v5, 0x0
.local v5, "i":I
:goto_1
array-length v7, v2
if-ge v5, v7, :cond_7
.line 77
aget-object v7, v2, v5
.line 78
.local v7, "fd":Ljava/lang/reflect/Field;
new-instance v8, Ljava/lang/StringBuilder;
invoke-direct {v8}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v7}, Ljava/lang/reflect/Field;->getName()Ljava/lang/String;
move-result-object v9
invoke-virtual {v8, v9}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v9, "="
invoke-virtual {v8, v9}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v8}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v8
invoke-virtual {v0, v8}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 80
:try_start_0
invoke-virtual {v7}, Ljava/lang/reflect/Field;->getType()Ljava/lang/Class;
move-result-object v8
invoke-virtual {v8}, Ljava/lang/Class;->isPrimitive()Z
move-result v8
if-nez v8, :cond_5
invoke-virtual {v7}, Ljava/lang/reflect/Field;->getType()Ljava/lang/Class;
move-result-object v8
const-class v9, Ljava/lang/String;
if-eq v8, v9, :cond_5
.line 81
invoke-virtual {v7, p0}, Ljava/lang/reflect/Field;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v8
invoke-static {v8}, LMyLog;->toString(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v8
invoke-virtual {v0, v8}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
goto :goto_2
.line 83
:cond_5
invoke-virtual {v7, p0}, Ljava/lang/reflect/Field;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v8
invoke-virtual {v0, v8}, Ljava/lang/StringBuffer;->append(Ljava/lang/Object;)Ljava/lang/StringBuffer;
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
.line 87
:goto_2
goto :goto_3
.line 85
:catch_0
move-exception v8
.line 86
.local v8, "e":Ljava/lang/Exception;
invoke-virtual {v8}, Ljava/lang/Exception;->printStackTrace()V
.line 88
.end local v8 # "e":Ljava/lang/Exception;
:goto_3
array-length v8, v2
sub-int/2addr v8, v6
if-eq v5, v8, :cond_6
.line 89
invoke-virtual {v0, v3}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 76
.end local v7 # "fd":Ljava/lang/reflect/Field;
:cond_6
add-int/lit8 v5, v5, 0x1
goto :goto_1
.line 92
.end local v5 # "i":I
:cond_7
invoke-virtual {v0, v4}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 93
invoke-virtual {v0}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;
move-result-object v3
return-object v3
.line 47
.end local v2 # "fields":[Ljava/lang/reflect/Field;
:cond_8
:goto_4
invoke-virtual {v0, p0}, Ljava/lang/StringBuffer;->append(Ljava/lang/Object;)Ljava/lang/StringBuffer;
.line 48
invoke-virtual {v0}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;
move-result-object v2
return-object v2
.end method
Android Studio中新建安卓项目,创建完成后在java目录下新建类MyLog完成自定义类编写(记得在java目录下编写,不然后续需要修改smali文件中的包名)
编写完毕后选择Build=>Build APk打包
进入build目录下找到刚才生成的apk
将该apk复制到apktool目录下执行反编译操作
进入生成的app-debug目录下拿到对应的MyLog.smali文件
使用Android Studio打开该项目,并且进入到我们想要输出打印的类,此处我们想要静态分析的类名为a,在smali文件夹当中(此处会有多个smali文件夹,需要找到我们所在的类在哪个smali文件夹中)
在目标位置注入打印代码
介绍
上一步我们完成了MyLog类的注入以及在目标App中调用我们的自定义类,接下来对项目进行重编译及签名,并且运行在模拟器上查看日志打印。
具体步骤
具体实现
把生成的apk安装到模拟器下并且运行
AS中打开Terminal窗口,并且输入以下指令(adb需要提前连接好模拟器,如果没连接好请使用adb connect指令来连接)
adb logcat
启动目标app
能够在Terminal界面中看到我们插入的日志输出
以上完成了在安卓逆向当中的smali代码注入与app的重编译及签名操作,主要介绍了静态分析的基本流程及常用工具。在实际逆向的过程当中我们能够通过修改app的代码来帮助我们屏蔽检测以及自定义代码的注入,有时候如果仅仅想查看方法的调用情况使用动态分析效率可能更高,但是静态分析对于逆向工作的开展以及逆向思维的形成有着重要的意义,此外他也是动态分析的基础。