本篇文章是Android逆向系列的第三篇,開始介紹Dalvik虛擬機的相關知識,認識dex和smali文件格式和熟悉Dalvik位元組碼及指令集,對Dalvik指令集有個大概的了解就可以開始簡單的反編譯靜態分析了,後面提及了安卓開發四大組件和使用Eclipse開發一個簡單的apk例子,最後以一個破解實例加深全文的知識概念,進一步熟悉工具的使用及Dalvik指令集。
一、Dalvik
1、Dalvik介紹
Dalvik是google專門為Android操作系統設計的一個虛擬機,Dalvik VM是基於寄存器的,而JVM是基於棧的;Dalvik有專屬的文件執行格式dex(dalvik executable),而JVM則執行的是java位元組碼。Dalvik VM比JVM速度更快,佔用空間更少。
在Java代碼中我們無法修改某個邏輯,所以需要將java代碼翻譯成smali代碼,也就是將dex文件轉換為smali文件。可以這樣理解,dalvik裡面的smali是可以修改的,而java代碼是修改不了的,那麼我們想要去破解也就是把Java代碼改成smali代碼,修改smali代碼之後再回編譯回去同時java邏輯也發生了改變,這是一種破解的思路。
Smali格式是dex格式的一種直觀可讀形式
Smali文件可以認為是Davilk的位元組碼文件
詳見後續的Smali介紹
2、Dalvik寄存器命名法
Dalvik虛擬機參數傳遞方式中的規定:假設一個函數使用到M個寄存器,其中函數的參數是N個,那麼參數使用最後的N個寄存器,局部變數使用從頭開始的前M-N個寄存器
Dalvik寄存器有兩種命名法
v命名法
v命名法採用以小寫字母「v」開頭的方式表示函數中用到的局部變數與參數,所有的寄存器命名從v0開始,依次遞增。
參數寄存器 v(m-n)~vm
局部變數寄存器 v0~vn
p命名法
基本上類似,主要是參數寄存器是使用p命名寄存器,而局部變數寄存器還是使用v命名寄存器
參數寄存器 p0~pn
變數寄存器 v0~vn
3、v命名法Smali代碼分析
Smali代碼如下圖,首先看第一行
static public DecryptDemo->getHelloWorld(Ljava/lang/string;I)Ljava/lang/string;
第一行中調用了一個getHelloWorld()方法,括弧內的表示有兩個參數Ljava/lang/String和I,用分號;隔開,返回值的類型為Ljava/lang/String
中間部分的.regsize:[5]表示有5個寄存器
第一個紅框中調用了方法將v2、v3寄存器值存入,返回了一個v2。第二個紅框中調用了方法將v0、v4寄存器值存入,返回一個v0。
invoke-virtual虛方法調用,調用的方法運行時確認實際調用,和實例引用的實際對象有關,動態確認的

4、p命名法Smali代碼分析
同樣第一行可以看出調用了一個getHelloWorld()方法,兩個參數Ljava/lang/String和I,用分號;隔開,返回值的類型為Ljava/lang/String
invoke-virtual {v1, p0}, Ljava/lang/stringBuilder;->append (Ljava/lang/String;)Ljava/lang/StringBuilder;move-result-object v1
第一個紅框在LJava/lang/StringBuilder類中調用了一個append的方法拼接傳來的String,返回一個LJava/lang/StringBuilder類型,傳入參數位於p0處,傳出參數位於v1處,返回的是一個move-result-object
第二個紅框類似,調用了一個append的方法拼接傳來的String返回一個LJava/lang/StringBuilder類型,傳入參數位於p1處,傳出參數位於v0處

5、dex文件反編譯工具
Dalvik 虛擬機並不支持直接執行 JAVA 位元組碼,所以會對編譯生成的 .class 文件進行翻譯、重構、解釋、壓縮等處理,這個處理過程是由 dx 進行處理,處理完成後生成的產物會以 .dex 結尾,稱為 Dex 文件。
整個編譯/反編譯涉及到的工具及流程如下:
1)編譯出smali文件流程
.java ==> .class ==> .dex ==> .smali
2)dx.jar腳本將class文件打包成dex文件
dx --dex --output=Test.dex com/xxx/ooo/Test.class
3)Baksmali.jar腳本將dex文件反編譯成smali文件
java -jar baksmali.jar -o smali_out/ source.dex
4)smali.jar腳本將smali文件打包成dex文件
java -jar smali.jar smali_out/ -o source.dex
6、Dalvik位元組碼類型
Davilk位元組碼只有兩種類型:基本類型和引用類型,對象和數組都是引用類型。
基本類型和無返回值的void類型都是用一個大寫字母表示
對象類型用字母L加對象的全限定名來表示
數組類型用[來表示
全限定名是什麼?
以String為例,其完整名稱是java.lang.String,那麼其全限定名就是java/lang/String;。即java.lang.String的」.」用」/」代替,並在末尾添加分號」;」做結束符
具體規則如下所示:
類型描述符 Java類型V voidZ BooleanB byteS stringC charI intJ longF floatD doubleL Java對象類型[ 數組類型
解釋下Java對象類型:L可以表示java類型中的任何類,比如在Java代碼中的java.lang.String對應在Davlik中描述是Ljava/lang/String
二、Dalvik指令集
上面只是簡單了解了Dalvik位元組碼,具體每個方法涉及到的邏輯還需要Dalvik指令集來解釋,下面介紹Dalvik指令集,由於Dalvik虛擬機是基於寄存器架構的,其指令集的風格更偏向於x86中的彙編指令
數據定義指令
const指令定義代碼中變數、常量、類等數據
<table><thead><tr><th>指令</th><th>描述</th></tr></thead><tbody><tr><td>const/4 vA,#+B</td><td>將數值符號擴展為32後賦值給寄存器vA</td></tr><tr><td>const-wide/16 vAA,#+BBBB</td><td>將數值符號擴展為64位後賦值個寄存器對vAA</td></tr><tr><td>const/high16 vAA, #+BBBB0000</td><td>將數值右邊零擴展為32位後賦給寄存器vAA</td></tr><tr><td>const-string vAA,string[@BBBB](https://github.com/BBBB “@BBBB”)</td><td>通過字元串索引高走字元串賦值給寄存器vAA</td></tr><tr><td>const-class vAA,type[@BBBB](https://github.com/BBBB “@BBBB”)</td><td>通過類型索引獲取一個類的引用賦值給寄存器vAA</td></tr></tbody></table>
數據操作指令
move指令用於操作代碼中的數據
<table><thead><tr><th>指令</th><th>描述</th></tr></thead><tbody><tr><td>move vA,vB</td><td>將vB寄存器的值賦值給vA寄存器,vA和vB寄存器都是4位</td></tr><tr><td>move/from16 vAA,VBBBB</td><td>將vBBBB寄存器(16位)的值賦值給vAA寄存器(7位),from16表示源寄存器vBBBB是16位的</td></tr><tr><td>move/16 vAAAA,vBBBB</td><td>將寄存器vBBBB的值賦值給vAAAA寄存器,16表示源寄存器vBBBB和目標寄存器vAAAA都是16位</td></tr><tr><td>move-object vA,vB</td><td>將vB寄存器中的對象引用賦值給vA寄存器,vA寄存器和vB寄存器都是4位</td></tr><tr><td>move-result vAA</td><td>將上一個invoke指令(方法調用)操作的單字(32位)非對象結果賦值給vAA寄存器</td></tr><tr><td>move-result-wide vAA</td><td>將上一個invoke指令操作的雙字(64位)非對象結果賦值給vAA寄存器</td></tr><tr><td>mvoe-result-object vAA</td><td>將上一個invoke指令操作的對象結果賦值給vAA寄存器</td></tr><tr><td>move-exception vAA</td><td>保存上一個運行時發生的異常到vAA寄存器</td></tr></tbody></table>
比較指令
cmp/cmpl用於比較兩個寄存器值,cmp大於結果表示1,cmpl大於結果表示-1。
<table><thead><tr><th>指令</th><th>說明</th></tr></thead><tbody><tr><td>cmpl-float vAA,vBB,vCC</td><td>比較兩個單精度的浮點數.如果vBB寄存器中的值大於vCC寄存器的值,則返回-1到vAA中,相等則返回0,小於返回1</td></tr><tr><td>cmpg-float vAA,vBB,vCC</td><td>比較兩個單精度的浮點數,如果vBB寄存器中的值大於vCC的值,則返回1,相等返回0,小於返回-1</td></tr><tr><td>cmpl-double vAA,vBB,vCC</td><td>比較兩個雙精度浮點數,如果vBB寄存器中的值大於vCC的值,則返回-1,相等返回0,小於則返回1</td></tr><tr><td>cmpg-double vAA,vBB,vCC</td><td>比較雙精度浮點數,和cmpl-float的語意一致</td></tr><tr><td>cmp-double vAA,vBB,vCC</td><td>等價與cmpg-double vAA,vBB,vCC指令</td></tr></tbody></table>
跳轉指令
用於跳轉至不同的地址處,Davlik提供了三種跳轉指令,goto、swicth和if跳轉
<table><thead><tr><th>指令</th><th>操作</th></tr></thead><tbody><tr><td>goto +AA</td><td>無條件跳轉到指定偏移處(AA即偏移量)</td></tr><tr><td>packed-switch vAA,+BBBBBBBB</td><td>有規律分支跳轉指令.vAA寄存器中的值是switch分支中需要判斷的,BBBBBBBB則是偏移表(packed-switch-payload)中的索引值,</td></tr><tr><td>spare-switch vAA,+BBBBBBBB</td><td>無規律分支跳轉指令,和packed-switch類似,只不過BBBBBBBB偏移表(spare-switch-payload)中的索引值</td></tr><tr><td>if-eq vA,vB,target</td><td>vA,vB寄存器中的相等,等價於java中的if(a==b),比如if-eq v3,v10,002c表示如果條件成立,則跳轉到current position+002c處.其餘的類似</td></tr><tr><td>if-ne vA,vB,target</td><td>等價與java中的if(a!=b)</td></tr><tr><td>if-lt vA,vB,target</td><td>vA寄存器中的值小於vB,等價於java中的if(a`<`b)</td></tr><tr><td>if-gt vA,vB,target</td><td>等價於java中的if(a`>`b)</td></tr><tr><td>if-ge vA,vB,target</td><td>等價於java中的if(a`>=`b)</td></tr><tr><td>if-le vA,vB,target</td><td>等價於java中的if(a`<=`b)</td></tr></tbody></table>
返回指令
return指令用於返回方法的執行結果
<table><thead><tr><th>指令</th><th>說明</th></tr></thead><tbody><tr><td>return-void</td><td>什麼也不返回</td></tr><tr><td>return vAA</td><td>返回一個32位非對象類型的值</td></tr><tr><td>return-wide vAA</td><td>返回一個64位非對象類型的值</td></tr><tr><td>return-object vAA</td><td>反會一個對象類型的引用</td></tr></tbody></table>
方法調用指令
invoke-virtual: 調用實例的虛方法(普通方法)invoke-super: 調用實例的父類/基類方法invoke-direct: 調用實例的直接方法invoke-static: 調用實例的靜態方法invoke-interface: 調用實例的介面方法
實例操作指令
操作對象實例相關
<table><thead><tr><th>指令</th><th>描述</th></tr></thead><tbody><tr><td>new-instance vAA,type[@BBBB](https://github.com/BBBB “@BBBB”)</td><td>構造一個指定類型的對象將器引用賦值給vAA寄存器.此處不包含數組對象</td></tr><tr><td>instance-of vA,vB,type[@CCCC](https://github.com/CCCC “@CCCC”)</td><td>判斷vB寄存器中對象的引用是否是指定類型,如果是,將v1賦值為1,否則賦值為0</td></tr><tr><td>check-cast vAA,type[@BBBB](https://github.com/BBBB “@BBBB”)</td><td>將vAA寄存器中對象的引用轉成指定類型,成功則將結果賦值給vAA,否則拋出ClassCastException異常.</td></tr></tbody></table>
空操作指令
nop指令無實際意義,一般用於代碼對齊
還有些指令未介紹到,稍微了解下就可以了,在實際試驗中遇到再進行解釋學習
三、安卓開發四大組件
提到安卓開發,必然會提及其四大組件Activity、Service、BroadcastReceiver、ContentProvider,其功能分別為
Activity: 控制程序界面的呈現service: 提供後台運行服務BroadcastReceiver: 提供接收廣播功能ContentProvider: 支持多個應用存儲和讀取數據
1、Activity活動
Activity提供了一個用戶完成相關操作的界面,一個apk中通常含有多個Activity活動,需要在Android Manifest.xml中進行聲明才可以調用。
Activity生命周期
Acticity流程開始,先調用onCreate()方法創建Acticity,再調用onStart()方法使該Acticity由不可見轉為可見,接著調用onResume()方法,使得用戶可以操作界面獲得焦點,Acticity開始運行。之後暫停調用onPause()方法,使得頁面失去焦點無法操作(可重新調用onResume()獲得焦點繼續操作),再調用onStop()方法使得界面不可見(若是對話框可見),此時可以調用onRestart()方法重新恢復到onStart()狀態前,或者調用onDestroy()方法後,Acticity界面全部消失,Acticity流程結束。

2、Service服務
Service服務,不能與用戶交互的,不能自己啟動的,運行在後台的程序如果我們退出應用時, Service進程並沒有結束,它仍然在後台運行,那我們什麼時候會用到Service呢?比如我們播放音樂的時候,有可能想邊聽音樂邊幹些其他事情,當我們退出播放音樂的應用,如果不用Service,我們就聽不到歌了,所以這時便就得用到Service了,又比如當我們一個應用的數據是通過網路獲取的,不同時間(一段時間)的數據是不同的這時候我們可以用Service在後台定時更新,而不用每打開應用的時候在去獲取。
Service生命周期
Service的生命周期並不像Activity那麼複雜,它只繼承了onCreate(), onStart(), onDestroy()三個方法,當我們第一次啟動Service時,先後調用oncreate()和onStart()這兩個方法,當停止Service時,則執行onDestroy()方法,這裡需要注意的是,如果Service已經啟動了,當我們再次啟動Service時,不會在執行oncreate()方法,而是直接執行onStart()方法。
3、BroadcastReceiver廣播接收者
BroadcastReceiver 用於接收和發送系統級的通知,使得Android的任意一個應用可以接收來自於系統和其他應用的消息
4、ContentProvider內容提供者
ContentProvider 用於不同應用程序之間實現數據共享的功能,提供了一套完整的機制,允許一個程序訪問另一個程序中的數據且同時能保證被訪數據的安全性。使用ContentProvider是 Android 實現跨程序共享數據的標準方式
ContentProvider兩種實現方法:
- 使用現有的內容提供器來讀取和操作相應程序中的數據
- 創建自己的內容提供器給我們程序的數據提供外部訪問介面。
應用程序通過內容提供器對其數據提供了外部訪問介面API,任何其他的應用程序就都可以對這部分數據進行訪問。例如:Android系統中自帶的電話簿、簡訊、媒體庫等程序都提供了類似的訪問介面API。
四、Eclipse 開發工具使用
這部分簡單介紹下Eclipse,並開發一個簡單的apk並在模擬器/真機上運行
1、新建安卓應用項目
1)新建Android Application Project

2)填寫新建應用的名字

3)設置應用程序的圖標

4)選擇空白組件
選擇activity組件,有不同的類型,可以自行選擇,這裡方面先選擇空白組件的

之後選擇Finish即可
2、項目文件介紹
第一步創建完項目後,顯示如下的頁面

在左邊項目欄中可以找到主程序的代碼MainActivity.java,雙擊查看

AndroidManifest.xml是任何應用程序的清單文件,包含了程序所有的聲明和一些配置信息,比如安卓的版本和一些安卓圖標名字等配置的信息

Eclipse提供了Manifest.xml的圖形化操作和代碼操作如下

3、構建項目
在左邊的選項欄隨便添加些組件即可,深入學習請自行google安卓開發

4、運行項目
將新建的項目導出運行

選擇雷電模擬器

雙擊啟動

五、Jadx-gui 反編譯工具使用
這裡介紹下Jadx工具鋼的簡單使用,接下來進入第六節的破解實例中
小技巧:直接拖進去再按搜索類才完整地完成反編譯工作
1、載入文件及介紹
載入貪吃蛇apk文件,主要反編譯有兩個文件,源代碼和資源文件,資源文件對應apk中的文件(這裡用壓縮軟體打開apk文件查看到)

2、簡單搜索類

3、函數跳轉
選擇函數,按住Ctrl+左鍵可以直接跳轉至函數聲明處。比如這裡的BuyFailed()

六、貪吃蛇apk破解
1、貪吃蛇apk破解簡介
在Jadx中搜索到支付失敗的字元串,發現BuyFailed()和BuySccess()函數,我們可以將這兩個函數調整位置或者修改,不過在Java代碼層不能修改,只能在Smali代碼層中修改,先了解下Smali代碼和一些底層的知識
2、apk程序上手研究
在商店頁面中點擊購買按鈕,顯示支付失敗,如下圖。
我們的目標:免費購買全部皮膚

3、Jadx 工具反編譯分析
拖入該文件,搜索」支付取消」的位置,簡單查看該處代碼,可以發現支付取消和支付失敗均會跳轉至BuyFailed()方法處,而支付成功會跳轉至BuySccess()方法處,我們可以想到將成功方法覆蓋失敗方法進而實現免費購買的效果,接著跟進在Smali代碼層分析。

4、Android Killer 工具反編譯|Smali代碼分析
將apk程序拖進Android Killer進行反編譯,在工程搜索中搜索」支付取消」字眼,跳轉到含有該字元的smali代碼處

但是此時有個小問題,怎麼確定這裡的smali代碼對應的是剛剛看到的Java代碼呢?Android Killer提供了反編譯回Java代碼的功能,點擊下圖上方的標誌,查看Java源碼,可以發現是一致的。

5、替換smali代碼|回編譯
找到支付成功的smali代碼處,如下紅框部分

將其覆蓋支付失敗和支付取消的smali代碼處

保存並回編譯
6、查看效果
可以發現,已經可以免費購買了

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/225264.html
微信掃一掃
支付寶掃一掃