設為首頁收藏本站

艾歐踢論壇

 找回密碼
 立即註冊

QQ登錄

只需一步,快速開始

搜索
熱搜: 活動 交友 discuz
查看: 302|回復: 0
打印 上一主題 下一主題

Android App 逆向入門之二:修改 smali 程式碼

[複製鏈接]
跳轉到指定樓層
樓主
發表於 2022-12-12 19:44:00 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
什麼是 Smali在我們利用 apktool d 拆開的內容中,有一個資料夾叫做 smali,裡面存放著的就是從 classes.dex 還原出來的東西,也就是程式碼。
但這些程式碼跟你想的可能不太一樣,例如說我們可以來看看 smali/com/cymetrics/demo/MainActivity.smali:
.class public Lcom/cymetrics/demo/MainActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MainActivity.java"


  1. # direct methods
  2. .method public constructor <init>()V
  3.     .locals 0

  4.     .line 16
  5.     invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V

  6.     return-void
  7. .end method


  8. # virtual methods
  9. .method protected onCreate(Landroid/os/Bundle;)V
  10.     .locals 1

  11.     .line 20
  12.     invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V

  13.     const p1, 0x7f0b001c

  14.     .line 21
  15.     invoke-virtual {p0, p1}, Lcom/cymetrics/demo/MainActivity;->setContentView(I)V

  16.     const p1, 0x7f080122

  17.     .line 22
  18.     invoke-virtual {p0, p1}, Lcom/cymetrics/demo/MainActivity;->findViewById(I)Landroid/view/View;

  19.     move-result-object p1

  20.     check-cast p1, Landroidx/appcompat/widget/Toolbar;

  21.     .line 23
  22.     invoke-virtual {p0, p1}, Lcom/cymetrics/demo/MainActivity;->setSupportActionBar(Landroidx/appcompat/widget/Toolbar;)V

  23.     const p1, 0x7f08007a

  24.     .line 25
  25.     invoke-virtual {p0, p1}, Lcom/cymetrics/demo/MainActivity;->findViewById(I)Landroid/view/View;

  26.     move-result-object p1

  27.     check-cast p1, Lcom/google/android/material/floatingactionbutton/FloatingActionButton;

  28.     .line 26
  29.     new-instance v0, Lcom/cymetrics/demo/MainActivity$1;

  30.     invoke-direct {v0, p0}, Lcom/cymetrics/demo/MainActivity$1;-><init>(Lcom/cymetrics/demo/MainActivity;)V

  31.     invoke-virtual {p1, v0}, Lcom/google/android/material/floatingactionbutton/FloatingActionButton;->setOnClickListener(Landroid/view/View$OnClickListener;)V

  32.     return-void
  33. .end method

  34. .method public onCreateOptionsMenu(Landroid/view/Menu;)Z
  35.     .locals 2

  36.     .line 38
  37.     invoke-virtual {p0}, Lcom/cymetrics/demo/MainActivity;->getMenuInflater()Landroid/view/MenuInflater;

  38.     move-result-object v0

  39.     const/high16 v1, 0x7f0c0000

  40.     invoke-virtual {v0, v1, p1}, Landroid/view/MenuInflater;->inflate(ILandroid/view/Menu;)V

  41.     const/4 p1, 0x1

  42.     return p1
  43. .end method

  44. .method public onOptionsItemSelected(Landroid/view/MenuItem;)Z
  45.     .locals 2

  46.     .line 47
  47.     invoke-interface {p1}, Landroid/view/MenuItem;->getItemId()I

  48.     move-result v0

  49.     const v1, 0x7f08003f

  50.     if-ne v0, v1, :cond_0

  51.     const/4 p1, 0x1

  52.     return p1

  53.     .line 54
  54.     :cond_0
  55.     invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onOptionsItemSelected(Landroid/view/MenuItem;)Z

  56.     move-result p1

  57.     return p1
  58. .end method
複製代碼



如果你覺得看起來不是很好閱讀,那是正常的。

Smali 是跑在 Android Dalvik VM 上的 byte code,有著自己的一套語法規則,如果想要看到我們熟悉的 Java 程式碼,必須要將 smali 還原成 Java。
利用 jadx 還原出 Java 程式碼接著我們要用到另外一套工具:jadx,GitHub 上面它對自己的描述是:Dex to Java decompiler。

安裝過程我一樣省略,接著我們用 jadx 把 apk 拆開:
# -r 代表不要把 resource 拆開,因為我們只關注程式碼
# -d 代表目的地
jadx -r demoapp.apk -d jadx-demoapp


跑完以後就會看到多了一個 jadx-demoapp 的資料夾,我們點進去裡面的 sources/com/cymetrics/demo/MainActivity.java,可以看到如下內容:
  1. package com.cymetrics.demo;

  2. import android.os.Bundle;
  3. import android.view.Menu;
  4. import android.view.MenuItem;
  5. import android.view.View;
  6. import androidx.appcompat.app.AppCompatActivity;
  7. import androidx.appcompat.widget.Toolbar;
  8. import com.google.android.material.floatingactionbutton.FloatingActionButton;
  9. import com.google.android.material.snackbar.Snackbar;
  10. /* loaded from: classes.dex */
  11. public class MainActivity extends AppCompatActivity {
  12.     /* JADX INFO: Access modifiers changed from: protected */
  13.     @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
  14.     public void onCreate(Bundle bundle) {
  15.         super.onCreate(bundle);
  16.         setContentView(R.layout.activity_main);
  17.         setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
  18.         ((FloatingActionButton) findViewById(R.id.fab)).setOnClickListener(new View.OnClickListener() { // from class: com.cymetrics.demo.MainActivity.1
  19.             @Override // android.view.View.OnClickListener
  20.             public void onClick(View view) {
  21.                 Snackbar.make(view, "Replace with your own action", 0).setAction("Action", (View.OnClickListener) null).show();
  22.             }
  23.         });
  24.     }

  25.     @Override // android.app.Activity
  26.     public boolean onCreateOptionsMenu(Menu menu) {
  27.         getMenuInflater().inflate(R.menu.menu_main, menu);
  28.         return true;
  29.     }

  30.     @Override // android.app.Activity
  31.     public boolean onOptionsItemSelected(MenuItem menuItem) {
  32.         if (menuItem.getItemId() == R.id.action_settings) {
  33.             return true;
  34.         }
  35.         return super.onOptionsItemSelected(menuItem);
  36.     }
  37. }
複製代碼

這才是我們想看到的內容嘛!因為這個 apk 沒有經過混淆,所以幾乎可以看到完整的 java 檔案,跟原始碼差不了多少。
簡單講一下混淆(Obfuscation),混淆就是把程式碼打亂,讓人不容易看出來原本的程式碼是什麼,例如說把變數名字都換成 aa, bb, cc, dd 這種沒有意義的名稱之類的,就是最基本的混淆。在 Android 開發中通常透過 ProGuard 這個工具來做混淆。
像上面那樣的程式碼很明顯就沒有混淆過,讓人很容易就能看出原本的邏輯。

這次我們要來改動的程式碼在 com/cymetrics/demo/FirstFragment.java:
  1. package com.cymetrics.demo;

  2. import android.os.Bundle;
  3. import android.view.LayoutInflater;
  4. import android.view.View;
  5. import android.view.ViewGroup;
  6. import android.widget.TextView;
  7. import androidx.fragment.app.Fragment;
  8. import com.scottyab.rootbeer.RootBeer;
  9. /* loaded from: classes.dex */
  10. public class FirstFragment extends Fragment {
  11.     @Override // androidx.fragment.app.Fragment
  12.     public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle bundle) {
  13.         return layoutInflater.inflate(R.layout.fragment_first, viewGroup, false);
  14.     }

  15.     @Override // androidx.fragment.app.Fragment
  16.     public void onViewCreated(View view, Bundle bundle) {
  17.         super.onViewCreated(view, bundle);
  18.         view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() { // from class: com.cymetrics.demo.FirstFragment.1
  19.             @Override // android.view.View.OnClickListener
  20.             public void onClick(View view2) {
  21.                 TextView textView = (TextView) view2.getRootView().findViewById(R.id.textview_first);
  22.                 if (new RootBeer(view2.getContext()).isRooted()) {
  23.                     textView.setText("Rooted!");
  24.                 } else {
  25.                     textView.setText("Safe, not rooted");
  26.                 }
  27.             }
  28.         });
  29.     }
  30. }
複製代碼


主要的邏輯是這一段:
  1. public void onClick(View view2) {
  2.     TextView textView = (TextView) view2.getRootView().findViewById(R.id.textview_first);
  3.     if (new RootBeer(view2.getContext()).isRooted()) {
  4.         textView.setText("Rooted!");
  5.     } else {
  6.         textView.setText("Safe, not rooted");
  7.     }
  8. }
複製代碼


這一段會去呼叫一個第三方的 library 檢查是否有 root,有的話就顯示 Rooted!,沒有的話就顯示 Safe, not rooted。
在研究程式碼邏輯時,我們可以看著 java 程式碼,但如果要改 code 的話,就不是改 java code 這麼簡單了,我們必須要直接去改 smali 的 code,才能把 app 重新打包回去。

修改 smali 程式碼
還記得我們用 Apktool 解開的資料夾嗎?smali 程式碼就在那裡面,路徑是:smali/com/cymetrics/demo/FirstFragment$1.smali,仔細找一下內容,就可以找到 onClick 的程式碼:
  1. # virtual methods
  2. .method public onClick(Landroid/view/View;)V
  3.     .locals 2

  4.     .line 32
  5.     invoke-virtual {p1}, Landroid/view/View;->getRootView()Landroid/view/View;

  6.     move-result-object v0

  7.     const v1, 0x7f08011c

  8.     invoke-virtual {v0, v1}, Landroid/view/View;->findViewById(I)Landroid/view/View;

  9.     move-result-object v0

  10.     check-cast v0, Landroid/widget/TextView;

  11.     .line 34
  12.     new-instance v1, Lcom/scottyab/rootbeer/RootBeer;

  13.     invoke-virtual {p1}, Landroid/view/View;->getContext()Landroid/content/Context;

  14.     move-result-object p1

  15.     invoke-direct {v1, p1}, Lcom/scottyab/rootbeer/RootBeer;-><init>(Landroid/content/Context;)V

  16.     .line 35
  17.     invoke-virtual {v1}, Lcom/scottyab/rootbeer/RootBeer;->isRooted()Z

  18.     move-result p1

  19.     if-eqz p1, :cond_0

  20.     const-string p1, "Rooted!"

  21.     .line 36
  22.     invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

  23.     goto :goto_0

  24.     :cond_0
  25.     const-string p1, "Safe, not rooted"

  26.     .line 38
  27.     invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

  28.     :goto_0
  29.     return-void
  30. .end method
複製代碼


簡單講解一下一些基礎的 smali 語法,.method public onClick(Landroid/view/View;)V 就是說有一個 public 的 method 叫做 onClick,接收一個參數類型是 android/view/View,括號最後面的 V 則代表 void,沒有回傳值。
.locals 2 指的是這個 function 會用到兩個暫存器,也就是 v0 跟 v1,如果你用到 v2 的話就會出錯,因此如果需要更多暫存器,記得要改這邊。
參數的話會用 p 來表示,通常 p0 代表 this,p1 就是第一個參數,因此 invoke-virtual {p1}, Landroid/view/View;->getRootView()Landroid/view/View; 就是把第一個參數丟進去呼叫 getRootView() 這個 method。
而這整段裡面,核心的程式碼是這一段:
  1. .line 35
  2. invoke-virtual {v1}, Lcom/scottyab/rootbeer/RootBeer;->isRooted()Z

  3. move-result p1

  4. if-eqz p1, :cond_0

  5. const-string p1, "Rooted!"

  6. .line 36
  7. invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

  8. goto :goto_0

  9. :cond_0
  10. const-string p1, "Safe, not rooted"
複製代碼


if-eqz p1, :cond_0 指的就是如果 p1 是 0,就跳到 :cond_0 的地方,
而 p1 是 RootBeer->isRooted() 的回傳值。也就是說,p1 代表著 root 檢查的結果,只要能把 p1 改掉,就能偽造不同的結果。


這邊有很多種改法,例如說把原本的 if-eqz 改成 if-nez,就可以反轉邏輯,或我們可以直接將 p1 硬改成 0,順便加上 log 確認我們有執行到這裡:
  1. .line 35
  2. invoke-virtual {v1}, Lcom/scottyab/rootbeer/RootBeer;->isRooted()Z

  3. move-result p1

  4. # 加上 log,印出 "we are here"
  5. const-string v1, "we are here"
  6. invoke-static {v1, v1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

  7. # 將 p1 直接硬改成 0
  8. const/4 p1, 0x0

  9. if-eqz p1, :cond_0

  10. const-string p1, "Rooted!"

  11. .line 36
  12. invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

  13. goto :goto_0

  14. :cond_0
  15. const-string p1, "Safe, not rooted"
複製代碼


加上那三行以後存檔,接著照著上一篇講的重新打包,安裝在手機上,打開 app 以後先看 log。
要看 Android 的 log 的話,需要用 adb logcat 這個指令來看,但如果你直接輸入這個指令,會噴一堆 log 出來,在這邊教大家兩個好用的指令。
第一個是 adb logcat -c,可以清掉之前的 log,第二個是:
adb logcat --pid=`adb shell pidof -s com.cymetrics.demo`
可以看到指定 package name 的 log,排除其他雜訊,這個真的很好用。
準備就緒以後,按下 app 內的 CHECK ROOT 按鈕,就會看到一條新的 log:
01-25 09:32:06.528 27651 27651 E we are here: we are here
以及畫面上出現的 Safe, not rooted 的字樣,就大功告成了。

更改其他地方的程式碼
剛剛我們改動了 fragment 中的程式碼,也就是程式的邏輯,把 isRooted() 的回傳值取代掉,讓它永遠是 false,繞過了檢查。
但如果程式中還有其他地方也會做類似的檢查那就麻煩了,因為我們必須找出每一個做檢查的地方,然後都做類似的事情,把每一處都改掉。
因此,一個比較有效率的方法是直接去改動這個第三方 library 的程式碼,讓 isRooted 永遠都回傳 false,這樣就算 app 在多個地方都有檢查,也會一起被繞過。
呼叫 function 時的程式碼是 Lcom/scottyab/rootbeer/RootBeer;->isRooted(),因此我們可以順藤摸瓜找到這個檔案:com/scottyab/rootbeer/RootBeer.smali,搜尋 isRooted 就會找到程式碼:
  1. .method public isRooted()Z
  2.     .locals 1

  3.     .line 44
  4.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->detectRootManagementApps()Z

  5.     move-result v0

  6.     if-nez v0, :cond_1

  7.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->detectPotentiallyDangerousApps()Z

  8.     move-result v0

  9.     if-nez v0, :cond_1

  10.     const-string v0, "su"

  11.     invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/RootBeer;->checkForBinary(Ljava/lang/String;)Z

  12.     move-result v0

  13.     if-nez v0, :cond_1

  14.     .line 45
  15.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->checkForDangerousProps()Z

  16.     move-result v0

  17.     if-nez v0, :cond_1

  18.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->checkForRWPaths()Z

  19.     move-result v0

  20.     if-nez v0, :cond_1

  21.     .line 46
  22.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->detectTestKeys()Z

  23.     move-result v0

  24.     if-nez v0, :cond_1

  25.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->checkSuExists()Z

  26.     move-result v0

  27.     if-nez v0, :cond_1

  28.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->checkForRootNative()Z

  29.     move-result v0

  30.     if-nez v0, :cond_1

  31.     invoke-virtual {p0}, Lcom/scottyab/rootbeer/RootBeer;->checkForMagiskBinary()Z

  32.     move-result v0

  33.     if-eqz v0, :cond_0

  34.     goto :goto_0

  35.     :cond_0
  36.     const/4 v0, 0x0

  37.     goto :goto_1

  38.     :cond_1
  39.     :goto_0
  40.     const/4 v0, 0x1

  41.     :goto_1
  42.     return v0
  43. .end method
複製代碼


想要 patch 這個函式非常簡單,我們讓它永遠都回傳 false 就好:
  1. .method public isRooted()Z
  2.     .locals 1
  3.    
  4.     # 在開頭新增底下這兩行,永遠回傳 false
  5.     const/4 v0, 0x0
  6.     return v0
  7.    
  8.     # 以下省略...
  9. .end method
複製代碼


接著一樣重新打包之後安裝在手機上,就能看到繞過的成果。
總結
在這篇裡面我們學到了如何閱讀基本的 smali 程式碼以及修改它,也學到了該如何利用 adb logcat 來看 Android app 的 log,並且實際下去修改 smali,反轉原本的邏輯,去繞過 app 對於 root 的檢查。


加上 log 是一個我覺得雖然看起來好像很笨很沒效率,但其實很有用的方法,就跟寫程式出錯的時候我會加一大堆 console.log 一樣,透過 log 來確認程式的執行流程跟自己預期中的相符,對於還原邏輯很有幫助。


最後,這篇我只有稍微提了一下 smali,如果想更了解 smali 的語法,可以參考底下文章:


分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 轉播轉播 分享分享 分享淘帖
回復

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 立即註冊

本版積分規則

小黑屋|Archiver|手機版|艾歐踢創新工坊    

GMT+8, 2024-6-1 07:34 , Processed in 0.225422 second(s), 20 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回復 返回頂部 返回列表