一. 初識Apk、Dalvik字節碼以及Smali
Dalvik(Android操作系統的虛擬機)
Dalvik VM是基於寄存器的,而JVM是基於棧的。 Dalvik有專屬的文件執行格式dex(dalvik executable),而JVM執行的則是Java字節碼,Dalvik VM比JVM速度更快,佔用空間更少。
通過Dalvik的字節碼我們不能直接看到原來的邏輯代碼,這時需要藉助如ApkTool或dex2jar+jd-gui工具來查看。但是,最終我們修改Apk需要操作的文件是 .smali文件,而不是導出來的Java文件重新編譯。
Smali–破解的重中之重
Smali、Baksmali分別是指安卓系統裏的Java虛擬機(Dalvik)所使用的一種 .dex 格式文件的彙編器、反彙編器。其語法是一種寬鬆式的Jasmin/dedexer語法,而且它實現了 .dex格式所有功能(註解、調試信息、線路信息等)。
當我們對Apk文件反編譯後,便會生成此類的文件。在Dalvik字節碼中,寄存器都是32位的,能夠支持任何類型,64位類型(Long/Double)用兩個寄存器表示;Dalvik字節碼有兩種類型:原始類型、引用類型(包括對象和數組)。
二. 初識Apk、Dalvik字節碼以及Smali
1. 原始類型:
B- - -byte
C- - -char
D- - - double
F- - - float
I- - -int
J- - -long
S- - - short
V- - -void
Z- - -boolean
XXX- - -array
Lxxx/yyy- - -object
最後兩項:數組的表示方式是:在基本類型前加上前中括號“[”,例如int數組和float數組分別表示爲:[I、[F;對象的表示則以L作爲開頭,格式是LpackageName/objectName;(注意必須有個分號跟在最後),例如String對象在smali中爲:Ljava/lang/String;,其中java/lang對應java.lang包,String就說定義在該包中的一個對象。
類裏面的內部類又如何在smali中引用呢?答案是LpackageName/objectName”符號。
方法的定義一般爲:
Func-Name(Para-Type1Para-Typr2Para-Type3…)Return-Type
注意參數與參數之間沒有任何分隔符,例如:
1). hello ()V
沒錯,這就是void hello()。
2). hello (III)Z
這個則是boolean hello(int,int,int)。
3). hello (Z[I[ILjava/lang/String;J)Ljava/lang/String;
上面表示:String hello(boolean,int[],int[],String,long)
2. Smali基本語法
.field private isFlag:z 定義變量
.method 方法
.parameter 方法參數
.prologue 方法開始
.line 123 此方法位於第123行
invoke-super 調用父函數
const/high16 v0,0x7f03. 把0x7f03賦值給v0
invoke-direct 調用函數
return-void 函數返回void
.end method 函數結束
new-instance 創建實例
iput-object 對象賦值
iget-object 調用對象
invoke-static 調用靜態函數
條件跳轉分支:
"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-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_
三. 深入smali文件
Smali中的包信息:
.class public Lcom/aaaaa;
.super Lcom/bbbbb;
.source "ccccc.java"
這是一個由ccccc.java編譯得到的smali文件(第3行)
它是com.aaaaa這個package下的一個類(第1行)
繼承自com.bbbbb這個類(第2行)
smali中的聲明
一般來說在Smali文件中是這個樣子的:
這個聲明是內部類的聲明:aaa這個類它有兩個成員內部類——qqq和www。
寄存器知識補充:
在smali裏的所有操作都必須經過寄存器來進行:本地寄存器用v開頭數字結尾的符號來表示,如v0、v1、v2、… 參數寄存器則使用p開頭數字結尾的符號來表示,如p0、p1、p2、… 特別注意的是,p0不一定是函數中的第一個參數,在非static函數中,p0代指“this”,p1表示函數的第一個參數,p2代表函數中的第二個參數 … 而在static函數中p0纔對應第一個參數(因爲Java的static方法中沒有this方法)。
寄存器簡單實例分析:
const/4 v0,0x1
iput-boolean v0,p0,Lcom/aaa;->IsRegistered:Z
上面兩句smali代碼,首先它使用了v0本地寄存器,並把值0x1存到v0中,然後第二句用iput-boolean這個指令把v0中的值存放到com.aaa.IsRegistered這個成員變量中。
即相當於:this.IsRegistered=true;(上面說過,在非static函數中p0代表的是“this”,在這裏就是com.aaa實例)。
smali中的成員變量:
成員變量的格式是:
.field public/private [static] [final] varName:<類型>
對於不同的成員變量也有不同的指令:
一般來說,獲取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等。
操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等。
沒有“-object”後綴的表示操作的成員變量對象是基本數據類型,帶“-object”表示操作的成員變量是對象類型,特別地,boolean類型則使用帶“-boolean”的指令操作。
Smali成員變量指令簡析(一)
sget-object v0, Lcom/aaa;->ID:Ljava/lang/String;
sget-object 就是用來獲取變量值並保存到緊接着的參數的寄存器中,在本例中,它獲取ID這個String類型的成員變量並放到v0這個寄存器中。
注意:前面需要該變量所屬的類的類型,後面需要加一個冒號和該成員變量的類型,中間是“->”表示所屬關係。
Smali成員變量指令簡析(二)
iget-object v0, p0, Lcom/aaa;->view:Lcom/aaa/view;
可以看到iget-object指令比sget-object多了一個參數,就是該變量所在類的實例,在這裏就是p0即“this”。
獲取array的話我們用aget和aget-object,指令使用和上述一致。
Smali成員變量指令簡析(三)
put指令的使用和get指令是統一的,如下:
const/4 v3,0x0
sput-object v3, Lcom/aaa;->timer:Lcom/aaa/timer;
相當於:this.timer=null;
注意:這裏因爲是賦值object,所以是null
Smali成員變量指令簡析(四)
.local v0, args:Landroid/os/Message;
const/4 v1,0x12
input v1,v0, Landroid/os/Message;->what:I
相當於:args.what = 18;(args是Message的實例)
四. Smali函數分析
Smali中的函數調用
smali中的函數和成員變量一樣也分爲兩種類型,分別爲direct和virtual。
direct method和virtual method的區別:
簡單來說,direct method就是private函數,其餘的public和protected函數都屬於virtual method。所以在調用函數時,有invoke-direct、invoke-virtual,另外還有invoke-static、invoke-super以及invoke-interface等幾種不同的指令。
當然其實還有invoke-XXX/range指令的,這是參數多於4個的時候調用的指令,比較少見,瞭解下即可。
1)invoke-static:用於調用static函數的
例如:
invoke-static {}, Lcom/aaa;->CheckSignature()Z
這裏注意到invoke-static後面有一對大括號“{}”,其實是調用該方法的實例+參數列表,由於這個方法既不需要參數也是static的,所以{}內爲空,再看一個:
const-string v0, “NDKLIB”
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
這個是調用static void System.loadLibrary(String)來加載NDK編譯的so庫的方法,同樣也是這裏v0就是參數“NDKLIB”了。
2)invoke-super:調用父類方法用的指令,一般用於調用onCreate、onDestroy等方法。
3)invoke-direct:調用private函數:
invoke-direct {p0}, Landroid/app/TabActivity;-><init>()V
這裏init()就是定義在TabActivity中的一個private函數。
4)invoke-virtual:用於調用protected或public函數,同樣注意修改smali時不要錯用invoke-direct或invoke-static:
sget-object v0, Lcom/dddd;->bbb:Lcom/ccc;
invoke-virtual {v0,v1}, Lcom/ccc;->Messages(Ljava/lang/Object;)V
這裏相信大家都很清楚了:
v0是bbb:Lcom/ccc
v1是傳遞給Messages方法的Ljava/lang/Object參數。
5)invoke-xxxxx/range:當方法的參數多於5個時(含5個),不能直接使用以上的指令,而是在後面加上“/range”,range表示範圍,使用方法也有所不同:
invoke-direct/range {v0…v5}, Lcmb/pb/ui/PBContainerActivity;->h(ILjava/lang/CharSequence;Ljava/lang/String;Landroid/content/Intent;I)Z
需要傳遞v0到v5一共6個參數,這時候大括號內的參數採用省略形式,且需要連續。
Smali中函數返回的結果的操作:
在Java代碼中調用函數和返回函數結果可以用一條語句完成,而在Smali裏則需要分開來完成,在使用上述指令後,如果調用的函數返回非void,那麼還需要用到move-result(返回基本數據類型)和move-result-object(返回對象)指令。
const-string v0, “Eric”
invoke-static {v0}, Lcmb/pbi;->t(Ljava/lang/String;)Ljava/lang/String;
move-result-object v2
v2保存的就是調用t方法返回的String字符串。
Smali中函數實體分析—if函數分析:
.method private ifRegistered()Z
.locals 2 //在這個函數中本地寄存器的個數
.prologue //指定程序的開始處,混淆過後的代碼可能會沒有這一說明
const/4 v0, 0x1. //v0賦值爲1
.local v0, tempFlag:Z
if-eqz v0, :cond_0 //判斷v0是否等於0,等於0則跳到cond_0執行
const/4 v1, 0x1 //符合條件分支
:goto_0 //標籤
return v1 //返回v1的值
:cond_0 //標籤
const/4 v1, 0x0 //cond_0分支
goto :goto_0 //跳到goto_0執行 即返回v1的值 這裏可以改成return v1,也是一樣的
.end method
Smali中函數實體分析—for函數分析:
const/4 v0, 0x0 //v0=0
.local v0, i:I
:goto_0
if-lt v0, v3, :cond_0 //v0小於v3 則跳轉到cond_0並執行分支:cond_0
return-void
:cond_0 //標籤
iget-object v1, p0, Lcom/aaa/MainActivity;->listStrings:Ljava/util/List; //引用對象
const-string v2, “Eric”
invoke-interface {v1, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z //List是接口,執行接口方法add
add-int/lit8 v0, v0, 0x1 //將第二個v0寄存器中的值,加上0x1的值放入第一個寄存器中,實現自增長
goto :goto_0 //回去:goto_0標籤
六. 瞭解JAVA反編譯工具
總的來說Java程序和Android程序的區別在於Android程序是基於組件、基於配置的。
常量池是由常量項(cp_info)組成的。常量池類似於結構體,主要有兩個元素。一個是tag,還有一個是info[]數組。tag用來判定info[]數組的類型。tag對應的類型圖如下:
class_index的作用就是指向CONSTANT_class_index
name_and_type_index的作用就是指向CONSTANT_NameAndType_info。
sget-object 就是用來獲取變量值並保存到緊接着的參數的寄存器中。
invoke-direct:沒有被覆蓋方法的調用,即不用動態根據實例所引用的調用,編譯時,靜態確認的,一般是private或方法。
invoke-virtual:虛方法調用,調用的方法運行時確認實際調用,和實例引用的實際對象有關,動態確認的,一般是帶有修飾符protected或public的方法。
Move-result-object v2 就是將上一條指令的結果存放在v2寄存器中。
ARM彙編
ARM具有31個通用寄存器,6個狀態寄存器
ARM處理器支持以下運行模式
1. 用戶模式:ARM處理器正常的程序執行狀態。
2. 快速中斷模式:用於高速數據傳輸或通道處理。
3. 外部中斷模式:用於通用的中斷處理。
4. 管理模式:操作系統使用的保護模式。
5. 數據訪問終止模式:當數據或指令預取終止時進入該模式,可用於模擬存儲及存儲保護。
6. 系統模式:運行具有特權的操作系統任務。
7. 未定義指令中止模式:當未定義的指令執行時進入該模式。
ARM彙編語言程序結構
#####(1)處理器架構定義
.arch armv5te @處理器架構
.fpu softvfp @協處理器類型
.ebi_attribute 20,1 @接口屬性
.ebi_attribute 21,1
.ebi_attribute 23,1
.ebi_attribute 24,1
.ebi_attribute 25,1
.ebi_attribute 26,1
.ebi_attribute 30,1
.ebi_attribute 18,1
.arch指定了ARM處理器架構。
.armv5te表示本程序在armv5te架構處理器上運行。
.fpu指定了協處理器的類型。
softvfp表示使用浮點運算庫來模擬協處理器運算。
.ebi_attribute 指定了一些接口屬性。
(2)段定義
.section 定義只讀數據,屬性是默認
.text 定義了代碼段
(3)註釋與標號
註釋方法:
/*...*/ 多行註釋
@ 單行註釋
標號:<標號名>:
例如:
loop:
...
end loop
(4)彙編器指令
程序中所有以"."開頭的指令都是彙編指令,它們不屬於ARM指令集。
部分彙編器指令:
.file: 指定了源文件名
.align: 代碼對齊方式
.ascii: 聲明字符串
.global: 聲明全局變量
.type: 指定符號的類型
(5)字符串與參數傳遞
聲明函數的方法:
.global 函數名
.type 函數名,%function
函數名:
<...函數體...>
聲明一個實現兩個數相加的函數的代碼:
.global MyAdd
.type MyAdd,%function
MyAdd:
add r0,r0,r1
mov pc,lr
ARM彙編規定:R0-R3這4個寄存器用來傳遞函數調用的第1到第4個參數,超過的參數通過堆棧來傳遞。
(6)ARM處理器尋址方式
-
立即尋址
mov R0,#1234 @# 作爲前綴,表示十六進制時以“0x”開頭
-
寄存器尋址
mov R0,R1
-
寄存器位移尋址
五種位移操作:
(1)LSL:邏輯左移,移位後寄存器空出的低位補0
(2)LSR:邏輯右移,移位後寄存器空出的高位補0
(3)ASR:算數右移,移動過程中符號位不變。如果操作數是整數,則移位後空出的高位補0,否則補1
(4)ROR:循環右移,移位後移出的低位填入移位空出的高位
(5)RRX:帶擴展的循環右移,操作數右移移位,移位空出的高位用C標誌的值填充。
例如:
mov R0,R1,LSL #2
-
寄存器間接尋址
LDR R0,[R1]
-
基址尋址
LDR R0,[R1,#-4]
-
多寄存器尋址
LDMIN R0,{R1,R2,R3,R4} LDM是數據加載指令 指令的後綴IA表示每次執行完成加載操作後R0寄存器的值自增1 ARM中,字表示的是一個32位 R1=[R0] R2=[R0+#4] R3=[R0+#8] R4=[R0+#12]
注:+#4是因爲32位佔4個字節
-
堆棧尋址
STMFD SP!,{R1-R7,LR} 入棧,多用於保存子程序“現場” LDMFD SP!,{R1-R7,LR} 出棧,多用於回到子程序現場
-
塊拷貝尋址
塊拷貝可實現連續地址數據從存儲器的某一位置拷貝到另一位置。
LDMIN R0! , {R1-R3} @從寄存器指向的存儲單元中讀取3個字到R1-R3寄存器。
-
相對尋址
程序計數器PC的當前值爲基地址,指令中的地址標號作爲偏移量,將兩者相加之後得到操作數的有效地址。
ARM和Thumb指令
Thumb是16位的ARM彙編。
如同樣的beq,bne這兩個彙編指令,用ARM的4個HEX數表示時,其HEX值爲0A,1A,而當用2個HEX數表示時,其HEX值爲D0,D1。
在動態調試的時候,IDA無法分辨ARM和Thumb指令。所以需要人工去進行糾正和調整。
ARM寄存器
R0-R7: 通用寄存器
R8-R10: 不常用的通用寄存器
R11: 基址寄存器(FP)
R12: 暫時寄存器(IP)
R13: 堆棧指針(SP)
R14: 鏈接寄存器(LR)
CPSR: 狀態寄存器
ARM指令集
B 無條件跳轉
BL 帶鏈接的無條件跳轉
BLX 帶狀態的無條件跳轉
BNE 不相等跳轉
BEQ 相等跳轉
寄存器交互指令
LDR #從存儲器中加載數據到寄存器
LDR R1, [R2] #把R2指向的地址的數據給R1
STR #把寄存器的數據存儲到存儲器
STR R1, [R2] #在R2指向的地址存儲R1
LDM #將存儲器的數據加載到一個寄存器列表
LDM R0, {R1,R2,R3} #把R0中的數據一次加載到R1,R2,R3
SDM #將一個寄存器列表的數據存儲到指定的存儲器
SDM R0, {R1,R2,R3} #把R1,R2,R3加載到R0單元
PUSH #入棧
POP #出棧
數據傳送指令
MOV #將立即數或寄存器的數據傳送到目標寄存器