一、前言
在對某個 apk 文件進行代碼注入的時候,我們面對的往往是反編譯后的 smali 代碼,而不是直接的 Java 源碼文件,因而了解 smali 語法基礎還是很有必要的。在這里先介紹下 Dalvik 虛擬機:Dalvik 是 Google 專門為 Android 平臺設計的虛擬機。雖然 Android 程序可以使用 Java 語言來進行開發,但 Dalvik VM 和 Java VM 是兩款不同的虛擬機。Dalvik VM 基于寄存器,而 Java VM 基于棧 。Dalvik VM 有專門的文件執行格式 dex (Dalvik Executable),而 Java VM 則執行的是 Java 字節碼。DVM 比 JVM 速度更快,占用的空間更少。
二、smali文件結構
下面的 smali 代碼取自某個測試 demo(通過 apktool 反編譯 .apk 文件獲取,這里先對 smali 語法格式進行介紹),目的是先對 smali 的文件內容結構有個大概的了解,有利于后面對語法細節講解的時候有個整體把握。
.class public abstract Lcom/happy/learnsmali/BaseActivity;.super Landroidx/appcompat/app/AppCompatActivity;.source "BaseActivity.kt" # interfaces.implements Lcom/happy/learnsmali/action/ActivityAction;.implements Lcom/happy/learnsmali/action/ClickAction;.implements Lcom/happy/learnsmali/action/HandlerAction;.implements Lcom/happy/learnsmali/action/BundleAction;.implements Lcom/happy/learnsmali/action/KeyboardAction; # annotations.annotation system Ldalvik/annotation/MemberClasses; value = { Lcom/happy/learnsmali/BaseActivity$Companion;, Lcom/happy/learnsmali/BaseActivity$OnActivityCallback; }.end annotation .annotation system Ldalvik/annotation/SourceDebugExtension; value = "SMAPBaseActivity.ktKotlin*S Kotlin*F+ 1 BaseActivity.ktcom/happy/learnsmali/BaseActivity+ 2 fake.ktkotlin/jvm/internal/FakeKt*L1#1,179:11#2:180*E".end annotation # static fields.field public static final Companion:Lcom/happy/learnsmali/BaseActivity$Companion; .field public static final RESULT_ERROR:I = -0x2 # instance fields.field private final activityCallbacks$delegate:Lkotlin/Lazy; # direct methods.method public static synthetic $r8$lambda$mAxgPA6JBXhjuhBfNvUeqmKUmlk(Lcom/happy/learnsmali/BaseActivity;Landroid/view/View;)V .locals 0 invoke-static {p0, p1}, Lcom/happy/learnsmali/BaseActivity;->initSoftKeyboard$lambda-0(Lcom/happy/learnsmali/BaseActivity;Landroid/view/View;)V return-void.end method .method static constructor ()V .locals 2 new-instance v0, Lcom/happy/learnsmali/BaseActivity$Companion; const/4 v1, 0x0 invoke-direct {v0, v1}, Lcom/happy/learnsmali/BaseActivity$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V sput-object v0, Lcom/happy/learnsmali/BaseActivity;->Companion:Lcom/happy/learnsmali/BaseActivity$Companion; return-void.end method .method public constructor ()V // ....end method
上面的代碼中,如果你剛開始接觸 smali 代碼,看得是一頭霧里云里的話那是正常的,下面我將進行解析,讀懂這些符號的含義有利于在我們反編譯 apk 進行注入代碼的時候達到事半功倍的效果。
smali中的繼承、接口、包信息
首先我們先看看開頭的幾行:
.class public abstract Lcom/happy/learnsmali/BaseActivity; // .class 表示類路徑 包名+類名.super Landroidx/appcompat/app/AppCompatActivity; // .super 表示父類的路徑.source "BaseActivity.kt" // 表示源碼文件名 # interfaces.implements Lcom/happy/learnsmali/action/ActivityAction;.implements Lcom/happy/learnsmali/action/ClickAction;.implements Lcom/happy/learnsmali/action/HandlerAction;.implements Lcom/happy/learnsmali/action/BundleAction;.implements Lcom/happy/learnsmali/action/KeyboardAction; # annotations.annotation system Ldalvik/annotation/MemberClasses; value = { Lcom/happy/learnsmali/BaseActivity$Companion;, Lcom/happy/learnsmali/BaseActivity$OnActivityCallback; }.end annotation
1-3行定義基本信息:表示有源文件 BaseActivity.kt 反編譯得到的 smali 文件(第三行),文件路徑位于 com/happy/learnsmali/(第二行),繼承于 androidx/appcompat/app/AppCompatActivity(第三行)。
5-9行定義接口信息:表示 BaseActivity 類實現的接口類有:
- com/happy/learnsmali/action/ActivityAction
- com/happy/learnsmali/action/ClickAction
- com/happy/learnsmali/action/HandlerAction
- com/happy/learnsmali/action/BundleAction
- com/happy/learnsmali/action/KeyboardAction
11-16行定義內部類:表示 BaseActivity 類有兩個內部類 -- Companion 和 OnActivityCallback。
分析完 smali 開頭的文件信息,我們可以據此可以構造出 java 代碼:
class BaseActivity extends AppCompatActivity implements ActivityAction, ClickAction, HandlerAction, BundleAction, KeyboardAction { class Companion { // ... } class OnActivityCallback { // ... }}
其他方法
# virtual methods //Representation is a virtual method.method protected onCreate(Landroid/os/Bundle;)V .locals 1 .param p1, "savedInstanceState" # Landroid/os/Bundle; .line 10 invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V .line 11 const/high16 v0, 0x7f050000 invoke-virtual {p0, v0}, Lcom/justart/samlidemo/MainActivity;->setContentView(I)V .line 12 return-void.end method
- 方法以 .method 開始,以 .end method 結束;
- 位于第一行的最后 V 表示返回類型為 void;
- 方法參數 Landroid/os/Bundle; 表示方法 onCreate() 的參數為 Bundle 類型;
- . param 表示方法的參數名稱為 savedInstanceState;
- 最后 return-void 表示返回的值類型為 void;
三、數據類型
- byte:B
- char:C
- double:D
- float:F
- int:I
- long:J
- short:S
- void:V
- boolean:Z
- array:[XXX
- Object:Lxxx/yyy
相信有 JNI 基礎會對上面的數據類型好明白,這里解析上面的最后兩項:
array:[XXX
在基礎類型前加 [ 表示數組類型,例如 int 數組和 byte 數組為 [I、[B。
Object:Lxxx/yyy
以 L 開頭的類型表示為對象,如 String 對象對應表示為 Ljava/lang/String; (對象類型需要后面跟分號),其中 java/lang 表示 java.lang 包,String 表示該包路徑下的一個對象。
這里可能會有童鞋有疑惑,如果類是使用 Ljava/lang/String; 來表示,那么內部類又應該在 smali 中如何定義呢?可能使用過 Java 反射的童鞋腦海里面閃過 $ 符號。是的,在 smali 語法中同樣是使用 Ljava/lang/String$xxx; 來表示 xxx 是 String 類的內部類。
四、寄存器
Dalvik VM 與 JVM 最大的區別之一就是 Dalvik VM 是基于寄存器的。基于寄存器是什么意思呢?個人理解的是有點類似于匯編語言,通過寄存器來存儲數據、傳遞數據。在 smali 中本地寄存器用 v 開頭的字母 + 數字來表示,如 v0、v1、v2 、...,而參數寄存器則使用 p 開頭 + 數字來表示,如 p1、p2、p3 、...。特別注意的是,p0 參數寄存器不一定是表示第一個參數,在非 static 函數中,p0 表示 this,p1 則表示第一個參數,p2 表示函數中的第二個參數。而在 static 函數中 p0 則才對應第一個參數(因為 Java 的 static 的方法沒有對象的概念)。本地寄存器沒有限制,理論上是可以任意使用的。
五、成員變量
下面繼續介紹有關成員變量的內容:
# static field.field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = "installationId"http://... # instance field.field private _activityPackageName:Ljava/lang/String;
上面定義的 static field 和 instance field 均為成員變量,格式是:
.field pubilc/private [static] [final] varName:<類型>
static field 和 instance field 雖然均為成員變量,但它們還是存在區別的。當然最明顯的區別就是是否與對象相關,static field 是類層面的概念,而 instance field 是對象層面的概念。
出現成員變量,那就意味著有變量的賦值與取值。在 smali 語法中,取值指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object 等,而賦值指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object 等。
iget / iput 分別表示 instance field 成員變量的取值和賦值;
sget / sput 分別表示 static field 成員變量的取值和賦值;
是否為 instance field 還是 static field 成員的取值和賦值指令,根據指令前綴判斷即可。帶 -object 后綴表示操作的是成員變量是對象類型,而不帶該后綴則表示操作的是基本數據類型。特別地,boolean 基本數據類型使用帶 -boolean 后綴。
下面有個例子:
const/4 v0, 0x0 iput-boolean v0, p0, Lcom/disney/xx/XxActivity;->isRunning:Z
在上面的例子中,使用了 v0 本地寄存器,并且把 0x0 傳遞到 v0 本地寄存器,然后第二句使用 iput-boolean 指令把 v0 寄存器中的值傳遞到 com.disney.xx.XxActivity 的成員變量 isRunning。即相當于:this.isRunning = false;(上面提到,在非 static 函數中 p0 表示為 this ,在這里則表示為 com.disney.xx.XxActivity 的對象實例)。
static field 成員變量
sget-object v0, Lcom/disney/xx/XxActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;
操作指令 sget-object 是用來獲取靜態成員變量并保存在緊接的本地參數列表中。在這里,把位于 com.disney.xx.XxActivity 類中的靜態成員 PREFS_INSTALLATION_ID 的值傳遞給本地寄存器 v0。
instance field 成員變量
iget-object v0, p0, Lcom/disney/xx/XxActivity;->_view:Lcom/disney/common/WMWView;
操作指令 iget-object 也是用來獲取類成員變量并保存在緊接的本地參數列表中。這里把 com.disney.xx.XxActivity 類中的對象成員 _view 賦值給本地寄存器 v0 中。
通過觀察上面的 static field 靜態成員變量 和 instance field 類成員變量,可以總結出以下的格式:
<本地寄存器>, [<參數寄存器>], <變量所屬的類變量> ->varName:<變量類型>
put 指令和上面提到的 get 指令格式是類似的,這里可以直接通過看下面的例子:
const/4 v3, 0x0 sput-object v3, p0, Lcom/disney/xx/XxActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;
Java 代碼表示: this.globalIapHandler = null; (null = 0x0)
.local v0, wait:Landroid/os/Message; const/4 v1, 0x2 iput v1, v0, Landroid/os/Message;->what:I
Java 代碼表示: wait.what = 0x2;(wait 是 Message 的實例)
六、函數調用
函數定義的格式:
function (type1type2type3...)RetValue
需要注意的是函數的參數類型需要定義為 smali 語法中的類型,同時參數之間不可以有其他的分隔符,例子如下:
helloSmali ()V
表示 void helloSmali()
helloSmali ([BI)Z
表示 boolean helloSmali(byte[], int)
helloSmali (ZLjava/lang/String;[I[I)V
表示 void helloSmali(boolean, String, int[], int[])
在 smali 中函數和成員變量一樣也分為兩種類型,但不同于成員變量中的 static field 靜態成員變量 和 instance field 類成員變量,函數中的是 direct method 和 virtual method。那么函數的 direct method 和 virtual method 有什么區別呢?簡單來說,direct method 就是 private 函數,而 virtual method 則是 public 和 protect 函數。
所以在調用函數的時候,有 invoke-direct、invoke-virtual,另外還有 invoke-static、invoke-super 以及 invoke-interface 等幾種不同的指令。同時還存在著 invoke-XXX/range 指令,這是參數傳參個數大于 4 個的時候調用的指令。
invoke-static
invoke-static {}, Lcom/disney/xx/UnlockHelper;->unlockCrankypack()Z
invoke-static 表示調用的是類靜態函數。Java 代碼表示為:UnlockHelper.unlockCrankypack(),這里注意到 invoke-static 后緊接著 {},表示的是調用該方法的實例 + 參數列表,由于這個方法既不需要參數,也是類靜態方法,所以 {} 內為空,再看一個例子:
const-string v0, "fmodex" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
這里調用的是 static void System.loadLibrary(String) 來加載 so 庫,而 v0 則表示傳參 fmodex。
invoke-super
表示調用父類方法用的指令,在重載的方法都可以看到。
invoke-direct
表示調用 private 函數的方法,如:
invoke-direct {p0}, Lcom/disney/xx/XxActivity;->getGlobalIapHandler()Lcom/disney/config/GlobalPurchaseHandler;
這里的 GlobalPurchaseHandler getGlobalIapHandler() 表示 getGlobalIapHandler() 是定義在 XxActivity 類中,權限為 private 的方法。
invoke-virtual
表示調用的是 protected 或 public 函數。
sget-object v0, Lcom/disney/xx/XxActivity;->shareHandler:Landroid/os/Handler; invoke-virtual {v0, v3}, Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V
這里的 v0 可以表示為 shareHandler:Landroid/os/Handler,v3 則表示為 removeCallbacksAndMessages 方法的 Ljava/lang/Object; 類型的傳參。
invoke-xxxxx/range
表示當方法參數 >= 5 時,需要在后面加上 /range 。
可能有童鞋會注意到,上面的例子都是在 調用函數 這個操作,貌似沒有取函數返回值的操作?在 smali 代碼中,如果調用的函數返回非 void,那么還需要用到 move-result (返回基本數據類型) 和 move-result-object (返回對象):
const/4 v2, 0x0 invoke-virtual {p0, v2}, Lcom/disney/xx/XxActivity;->getPreferences(I)Landroid/content/SharedPreferences; move-result-object v1
v1 表示調用 this.getPreferences(0) 方法返回的 SharedPreferences 類型的對象。
invoke-virtual {v2}, Ljava/lang/String;->length()I move-result v2
v2 表示 String.length() 返回的 int 基本類型。
七 舉例分析
上面初步對函數變量、方法定義、調用的進行解析,下面通過舉例進一步對 smali 語法進行分析:
.method protected onDestroy()V .locals 0 .line 79 invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onDestroy()V .line 80 invoke-virtual {p0}, Lcom/happy/learnsmali/BaseActivity;->removeCallbacks()V .line 81 return-void.end method
這個是我們熟悉的 onDestroy() 函數。首先我們看到函數內第一句:.locals 0 ,表示在這個函數中用到的本地寄存器的個數,這里因為調用的方法沒有使用到本地本地寄存器,因而本地寄存器的個數為 0。如果我在該方法中添加:this.isExited = true,那么上述方法應該修改為:
.method protected onDestroy()V .locals 1 .line 79 invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onDestroy()V .line 80 invoke-virtual {p0}, Lcom/happy/learnsmali/BaseActivity;->removeCallbacks()V .line 81 const/4 v0, 0x1 iput-boolean v0, p0, Lcom/happy/learnsmali/BaseActivity;->exited:Z .line 82 return-void.end method
因為修改后的 onDestroy() 函數使用到了一個本地寄存器 v0,所以把 .locals 0 修改為 .locals 1 。另外可能你也會注意到 .line 這個標識符,它表示 smali 這一行代碼在 Java 中對應代碼中所在的位置行號。平常當我們在 Android Studio 上調試程序發生崩潰的時候, logcat 中提示發生崩潰所在的代碼行號也是該值。當然,該標識符不是必須的,但為了方便調試還是建議保留吧。
看雪學苑
FreeBuf
FuzzWiki
安全圈
雷石安全實驗室
ChaMd5安全團隊
GoUpSec
LemonSec
安全圈
看雪學苑
看雪學苑
黑白之道