smali插桩是一度是android上最流行的 java层逆向调试方法,即使在当下依然能满足大部分java层逆向分析要求,但是其缺点,一是使用起来每次修改都要重打包,异常繁琐二是重打包在很多时候是无法实现的,比如,当前很多知名应用运行时会校验安装包的签名,若发现签名错误将会拒绝运行,另外,apk的加壳技术也会使重打包异常棘手,无从下手。总之,应用程序的安全对抗使其重打包困难重重。

先抛开这些不谈,本文只是就smali插桩技术的实现过程做简要说明。

本文smali插桩重打包使用的测试工程下载地址:点击下载

(一)原理

  1. 假设调试目标是程序A,解压缩后,利用dex2jar将解压包中的dex文件转换为jar文件,然后使用jd-gui静态反汇编并分析jar文件,找到需要插桩的代码位置,假设为C类中的D方法是我们要监视的函数。
  2. 利用apktool,将A程序的apk文件解包(解包后为smali和资源等)。
  3. 创建程序B,并实现一个public static类型的方法F,该方法的主要功能是,将要监视的函数的参数或者其他变量等,输出到日志log或者sd卡文件中。
  4. 使用apktool将B程序的apk文件解包。
  5. 将解包后B中F方法的smali代码拷贝到A类的smali代码文件中。
  6. 在C类的D方法中寻找插桩位置,插入smali指令,即使用smali指令调用F方法。
  7. 利用apktool将A的解包文件重新打包,使用autosign重新签名并安装到android上并运行,查看相应标志的日志和文件,就可以实现对C类中D函数的监控。

(二)例子

  1. 创建一个test程序,包名为com.example.test。该程序为我们要调试的目标程序,我们想要得到MainActivity类testUserPass中的String类型参数值user和pass。
    在这里插入图片描述

  2. 创建第二个程序,包名为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();   
        }   
    }
    
  1. 使用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


  1. 将项目smaliHook中,mytestlog和WriteLogFile的smali代码,拷贝到test项目MainActivity类的smali文件中。将上述两个函数mytestlog和WriteLogFile复制到/com/example/test/MainActivity.smali文件中,位置随意。插桩函数mytestlog和WriteLogFile的几点要求:(1) 插桩函数最好是public static 属性,因为java中的非静态方法都要使用类指针来引用,这样一来需要维护整个类的插入工作,大大增加了工作量(2) 尽量将插桩代码复制到与监视方法的同一个类中;(3) 插桩函数尽量使用局部变量,这样会减少类中变量成员的引用复杂度。上述几点的目的是,使得编写smali插桩指令尽可能的容易
  2. 在目标方法中查找合适的插桩位置,并添加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代码如下:
在这里插入图片描述

  1. 对目标apk进行打包。将插桩后的com.example.test工程重新打包,对打包后的test.apk重新签名。
  2. 安装test.apk并运行、查看插桩日志输出或者sd卡中的文件输出,就可以得到程序运行中两个参数的值。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐