Android java层调试之smali插桩
smali插桩是一度是android上最流行的 java层逆向调试方法,即使在当下依然能满足大部分java层逆向分析要求,但是其缺点,一是使用起来每次修改都要重打包,异常繁琐;二是重打包在很多时候是无法实现的,比如,当前很多知名应用运行时会校验安装包的签名,若发现签名错误将会拒绝运行,另外,apk的加壳技术也会使重打包异常棘手,无从下手。总之,应用程序的安全对抗使其重打包困难重重。
先抛开这些不谈,本文只是就smali插桩技术的实现过程做简要说明。
本文smali插桩重打包使用的测试工程下载地址:点击下载
(一)原理
- 假设调试目标是程序A,解压缩后,利用dex2jar将解压包中的dex文件转换为jar文件,然后使用jd-gui静态反汇编并分析jar文件,找到需要插桩的代码位置,假设为C类中的D方法是我们要监视的函数。
- 利用apktool,将A程序的apk文件解包(解包后为smali和资源等)。
- 创建程序B,并实现一个public static类型的方法F,该方法的主要功能是,将要监视的函数的参数或者其他变量等,输出到日志log或者sd卡文件中。
- 使用apktool将B程序的apk文件解包。
- 将解包后B中F方法的smali代码拷贝到A类的smali代码文件中。
- 在C类的D方法中寻找插桩位置,插入smali指令,即使用smali指令调用F方法。
- 利用apktool将A的解包文件重新打包,使用autosign重新签名并安装到android上并运行,查看相应标志的日志和文件,就可以实现对C类中D函数的监控。
(二)例子
-
创建一个test程序,包名为com.example.test。该程序为我们要调试的目标程序,我们想要得到MainActivity类testUserPass中的String类型参数值user和pass。

-
创建第二个程序,包名为com.example.smaliHook,其中有个方法mytestlog,其主要功能是日志输出和文件记录两个String变量的值。
该功能包含两个函数,函数mytestlog有两个String类型输入参数,在程序中获取Context后,分别使用toast、log、写入"/storage/sdcard0/SmaliLog/SmaliLog.txt"文件等3种方式输出两个参数的值,其中写入sd卡文件的操作由函数WriteLogFile完成。
public static void mytestlog(String str1,String str2){
String TAG = "smaliHook";
try {
Class<?> ActivityThread = Class.forName("android.app.ActivityThread");
Method methodcat = ActivityThread.getMethod("currentActivityThread");
Object currentActivityThread = methodcat.invoke(ActivityThread);
Method methodga = currentActivityThread.getClass().getMethod("getApplication");
Context context =(Context)methodga.invoke(currentActivityThread);
if (context == null) {
Log.e(TAG, "context null");
}else{
String str = TAG + "get context ok,package name:" + context.getPackageName()+"/class name:" + context.getClass().getName();
String param = "param 1 is:" + str1 + ",param 2 is:" + str2;
Log.e(TAG,param );
WriteLogFile(param);
//context = context.getApplicationContext();
//Toast.makeText(context, param, Toast.LENGTH_LONG).show();
return ;
}
} catch (Exception e) {
e.printStackTrace();
}
return ;
}
public static void WriteLogFile(String str)
{
String LOG_FILE_PATH = "/storage/sdcard0/SmaliLog/";
String LOG_FILE_NAME = "SmaliLog.txt";
String sdStatus = Environment.getExternalStorageState();
if(!sdStatus.equals(Environment.MEDIA_MOUNTED))
{
return;
}
try
{
String pathName=LOG_FILE_PATH;
String fileName=LOG_FILE_NAME;
File path = new File(pathName);
File file = new File(pathName + fileName);
if( !path.exists())
{
path.mkdir();
}
if( !file.exists() )
{
file.createNewFile();
}
FileOutputStream stream = new FileOutputStream(file,true);
if(str.length() > 0)
{
stream.write(str.getBytes());
}
stream.write("\r\n".getBytes());
stream.close();
}
catch(Exception e)
{
e.printStackTrace();
}
}
- 使用apktool解包。包括第一个和第二个apk包。
apktool解包命令(以test.apk为例):
java -jar apktool.jar d -f test.apk
apktool打包命令(以上文中test.apk为例):
java -jar apktool.jar b test
除了使用第三方的apktool外,还可以使用google sdk自带的工具smali.jar和baksmali.jar来实现。命令执行时记得关闭360,否则一堆提示后,发现刚生成的文件不见了,悲哀:)。
将dex分解为smali文件的命令(以classes.dex为例):
java -jar baksmali-2.0.3.jar classes.dex -o class
将smali文件生成为dex文件的命令(以class为例):
java -jar smali-2.0.3.jar class -o myclass.dex
相对来说,还是apktool.jar比较简单。
反汇编完成后,上述WriteLogFile和mytestlog两个函数的smali代码如下:
.method public static mytestlog(Ljava/lang/String;Ljava/lang/String;)V
.locals 12
.param p0, "str1" # Ljava/lang/String;
.param p1, "str2" # Ljava/lang/String;
.prologue
.line 76
const-string v1, "smaliHook"
.line 78
.local v1, "TAG":Ljava/lang/String;
:try_start_0
const-string v9, "android.app.ActivityThread"
invoke-static {v9}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
move-result-object v0
.line 79
.local v0, "ActivityThread":Ljava/lang/Class;, "Ljava/lang/Class<*>;"
const-string v9, "currentActivityThread"
const/4 v10, 0x0
new-array v10, v10, [Ljava/lang/Class;
invoke-virtual {v0, v9, v10}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
move-result-object v5
.line 80
.local v5, "methodcat":Ljava/lang/reflect/Method;
const/4 v9, 0x0
new-array v9, v9, [Ljava/lang/Object;
invoke-virtual {v5, v0, v9}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v3
.line 81
.local v3, "currentActivityThread":Ljava/lang/Object;
invoke-virtual {v3}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
move-result-object v9
const-string v10, "getApplication"
const/4 v11, 0x0
new-array v11, v11, [Ljava/lang/Class;
invoke-virtual {v9, v10, v11}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
move-result-object v6
.line 82
.local v6, "methodga":Ljava/lang/reflect/Method;
const/4 v9, 0x0
new-array v9, v9, [Ljava/lang/Object;
invoke-virtual {v6, v3, v9}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v2
check-cast v2, Landroid/content/Context;
.line 83
.local v2, "context":Landroid/content/Context;
if-nez v2, :cond_0
.line 84
const-string v9, "context null"
invoke-static {v1, v9}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
.line 102
.end local v0 # "ActivityThread":Ljava/lang/Class;, "Ljava/lang/Class<*>;"
.end local v2 # "context":Landroid/content/Context;
.end local v3 # "currentActivityThread":Ljava/lang/Object;
.end local v5 # "methodcat":Ljava/lang/reflect/Method;
.end local v6 # "methodga":Ljava/lang/reflect/Method;
:goto_0
return-void
.line 86
.restart local v0 # "ActivityThread":Ljava/lang/Class;, "Ljava/lang/Class<*>;"
.restart local v2 # "context":Landroid/content/Context;
.restart local v3 # "currentActivityThread":Ljava/lang/Object;
.restart local v5 # "methodcat":Ljava/lang/reflect/Method;
.restart local v6 # "methodga":Ljava/lang/reflect/Method;
:cond_0
new-instance v9, Ljava/lang/StringBuilder;
invoke-static {v1}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v10
invoke-direct {v9, v10}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
const-string v10, "get context ok,package name:"
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
invoke-virtual {v2}, Landroid/content/Context;->getPackageName()Ljava/lang/String;
move-result-object v10
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
const-string v10, "/class name:"
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
invoke-virtual {v2}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
move-result-object v10
invoke-virtual {v10}, Ljava/lang/Class;->getName()Ljava/lang/String;
move-result-object v10
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
invoke-virtual {v9}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v8
.line 88
.local v8, "str":Ljava/lang/String;
new-instance v9, Ljava/lang/StringBuilder;
const-string v10, "param 1 is:"
invoke-direct {v9, v10}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v9, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
const-string v10, ",param 2 is:"
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
invoke-virtual {v9, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v9
invoke-virtual {v9}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v7
.line 90
.local v7, "param":Ljava/lang/String;
invoke-static {v1, v7}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
.line 92
invoke-static {v7}, Lcom/example/smalihook/MainActivity;->WriteLogFile(Ljava/lang/String;)V
.line 94
const/4 v9, 0x1
invoke-static {v2, v7, v9}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v9
invoke-virtual {v9}, Landroid/widget/Toast;->show()V
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
goto :goto_0
.line 98
.end local v0 # "ActivityThread":Ljava/lang/Class;, "Ljava/lang/Class<*>;"
.end local v2 # "context":Landroid/content/Context;
.end local v3 # "currentActivityThread":Ljava/lang/Object;
.end local v5 # "methodcat":Ljava/lang/reflect/Method;
.end local v6 # "methodga":Ljava/lang/reflect/Method;
.end local v7 # "param":Ljava/lang/String;
.end local v8 # "str":Ljava/lang/String;
:catch_0
move-exception v4
.line 99
.local v4, "e":Ljava/lang/Exception;
invoke-virtual {v4}, Ljava/lang/Exception;->printStackTrace()V
goto :goto_0
.end method
- 将项目smaliHook中,mytestlog和WriteLogFile的smali代码,拷贝到test项目MainActivity类的smali文件中。将上述两个函数mytestlog和WriteLogFile复制到/com/example/test/MainActivity.smali文件中,位置随意。插桩函数mytestlog和WriteLogFile的几点要求:(1) 插桩函数最好是public static 属性,因为java中的非静态方法都要使用类指针来引用,这样一来需要维护整个类的插入工作,大大增加了工作量;(2) 尽量将插桩代码复制到与监视方法的同一个类中;(3) 插桩函数尽量使用局部变量,这样会减少类中变量成员的引用复杂度。上述几点的目的是,使得编写smali插桩指令尽可能的容易。
- 在目标方法中查找合适的插桩位置,并添加smali调用。因为被插入方法一般是public static属性,因此使用的smali调用格式是invoke-static;后面是调用参数列表,比如{p1,p2},其中p1,p2代表上一级调用本函数时的参数,如果要传入的参数是局部变量,就只能用v前缀而不是p前缀来表示变量;再后面是方法的全路径,此处是 Lcom/example/test/MainActivity;->mytestlog(Ljava/lang/String;Ljava/lang/String;)V ,注意smali语法,最后面的大写V代表返回值是void类型。插入的smali调用指令如下:
invoke-static {p1,p2}, Lcom/example/test/MainActivity;->mytestlog(Ljava/lang/String;Ljava/lang/String;)V
插桩后testUserPass函数的smali代码如下:

- 对目标apk进行打包。将插桩后的com.example.test工程重新打包,对打包后的test.apk重新签名。
- 安装test.apk并运行、查看插桩日志输出或者sd卡中的文件输出,就可以得到程序运行中两个参数的值。
更多推荐
所有评论(0)