安卓代碼跟蹤方式學習筆記

分享一下學習過程中的心得,主要獻給和我一樣起步晚又對安卓產生濃烈興趣的童鞋們!

 

一、代碼跟蹤的介紹&使用工具

 

代碼跟蹤常用於調試程序中,跟蹤並瞭解程序的執行軌跡和執行邏輯。這樣來說,對Java這樣的高級語言來說,我們容易理解也容易調試。但是像一些低級語言,例如ASM、Smali來說難度就很大了,因爲反彙編這樣的語言,我們需要一次性記住很多亂七八糟的關鍵詞才能成功,因而代碼跟蹤的技術就誕生了,並且在一些大程序中的運用更爲廣泛,同時也是現在逆向一些大而複雜的Android-app極爲重要的一種手段。

相信很多人都玩過PC端,對OD的動態調試也應該特別熟悉。OD很強大,其中的F2(斷點),F8(單步走)使得調試程序事半功倍,我相信用過得沒有說不好的吧。而Android-app與PC software的性質是不同的。在Android-app裏是把Apk文件Dump成字節碼,分析,修改,再重新編譯。有一個很大的不同點就是Android-app但是正是由於開源,使得Dump出來的字節碼是“死”的,這是Java的一個特性。幸運的是,Apktool很好的解決了這個問題,它的出現使得Android-app的調試得到了轉機,受益的是大家。

既然不能進行完全的調試,想到的辦法是用系統的log函數來呈現程序運行到關鍵時候的關鍵信息來達到相應的效果。比如說,你想知道程序運行到某個時候某個變量裏裝的是什麼,就可以用log的方式顯示。

獲取Android程序的log信息一共有幾種方式。最好的選擇當時是用DDMS,因爲它和Android SDK是一夥的!它可以顯示很多系統的log信息以及一些其他的亂七八糟的信息。同時,可以設置過濾器來進行標籤過濾、log等級(error,warning,info,debug,verbose)過濾、進程過濾,也能結束進程。相當強大。如圖:


二、跟蹤方法

 

1.Logging大法

因爲Apps沒有控制檯所以它們只能通過Android 的API中的log函數來進行輸出。

關於android/util/Log的介紹如下:

Log extends Object

一共有5個方法:Log.v() Log.d() Log.i()Log.w() and Log.e(),它們分別對應:

int

ASSERT

Priority constant for the println method.

int

DEBUG

Priority constant for the println method; use Log.d.

int

ERROR

Priority constant for the println method; use Log.e.

int

INFO

Priority constant for the println method; use Log.i.

int

VERBOSE

Priority constant for the println method; use Log.v.

int

WARN

Priority constant for the println method; use Log.w

Log.d一般來說是用的最多的。(本來log.v是最多的,但是log.v並不參加編譯)

寫一個測試類來分析Java代碼如下:

public class Log {
	public Log() {}
	
	public static void Log(String msg)
	{
		Log(msg, "Log");
	}
	
	public static void Log(Object XX)
	{
		Log("Log", XX.toString());
	}
	
	public static void Log(Object XX, String tag)
	{
		Log(tag, XX.toString());
	}
	
	public static void Log(String tag, String msg)
	{
		Log.d(tag, msg);
	}
	
	public static void ToastLog(Context contx, String msg, int duration)
	{
		Toast.makeText(contx, msg, duration).show();
	}
}

反彙編成Smali代碼如下:

# direct methods
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 8
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

.method public static Log(Ljava/lang/Object;)V
    .registers 3
    .parameter "XX"

    .prologue
    .line 17
    const-string v0, "Log"

    invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Lcom/Ericky/Log;->Log(Ljava/lang/String;Ljava/lang/String;)V

    .line 18
    return-void
.end method

.method public static Log(Ljava/lang/Object;Ljava/lang/String;)V
    .registers 3
    .parameter "XX"
    .parameter "tag"

    .prologue
    .line 22
    invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v0

    invoke-static {p1, v0}, Lcom/Ericky/Log;->Log(Ljava/lang/String;Ljava/lang/String;)V

    .line 23
    return-void
.end method

.method public static Log(Ljava/lang/String;)V
    .registers 2
    .parameter "msg"

    .prologue
    .line 12
    const-string v0, "Log"

    invoke-static {v0, p0}, Lcom/Ericky/Log;->Log(Ljava/lang/String;Ljava/lang/String;)V

    .line 13
    return-void
.end method

.method public static Log(Ljava/lang/String;Ljava/lang/String;)V
    .registers 2
    .parameter "tag"
    .parameter "msg"

    .prologue
    .line 28
    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 29
    return-void
.end method

.method public static ToastLog(Landroid/content/Context;Ljava/lang/String;)V
    .registers 3
    .parameter "contx"
    .parameter "msg"

    .prologue
    .line 33
    const/4 v0, 0x5

    invoke-static {p0, p1, v0}, Lcom/Ericky/Log;->ToastLog(Landroid/content/Context;Ljava/lang/String;I)V

    .line 34
    return-void
.end method

.method public static ToastLog(Landroid/content/Context;Ljava/lang/String;I)V
    .registers 4
    .parameter "contx"
    .parameter "msg"
    .parameter "duration"

    .prologue
    .line 40
    invoke-virtual {p0}, Landroid/content/Context;->getApplicationContext()Landroid/content/Context;

    .line 41
    invoke-static {p0, p1, p2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 42
    return-void
.end method

取出第一個方法:

.method public static Log(Ljava/lang/Object;)V
    .registers 3
.parameter "XX"

    .prologue
    .line 17
    const-string v0, "Log"

    invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Lcom/Ericky/Log;->Log(Ljava/lang/String;Ljava/lang/String;)V

    .line 18
    return-void
.end method

在這個方法裏的第一個參數就是標籤,在DDMS中是可以過濾的。因此,可以利用這個特點插入我們要加的信息,從而輕鬆的定位到關鍵的地方。在實際操作的過程中,可以打開androidAPI的文檔,對着看。通常的做法是用Log.d,還要用到toString()這個API,同時需要2個變量,一個變量存放你的標籤以及另一個存放你的對象。值得注意的是,即使你根本不用這個2個變量,也需要將.locals和.registers的數量增加。修改完了之後記得檢查好代碼,至少我們自己能讀懂它。

還有一點需要我們注意,當你改完之後最好在根目錄下運行,否則記得還要改class的路徑。不然的話有可能出現ClassDefNotFound錯誤,程序強制退出。下面是修改的代碼:


const-string v0, "Ericky tag"
const-string v1, "important log message"
invoke-static {v0, v1}, Lcom/Ericky/Log;->Log(Ljava/lang/String;Ljava/lang/String;)V

這是一個簡單的例子,但是有的時候我們是需要顯示更多的字符串,那我們應該怎麼辦呢?需要用到一個StringBuilder類。StringBuilder類的用法在我之前的一篇文章中已經有用到了,很容易理解。http://bbs.pediy.com/showthread.php?t=194955

還有一個方法就是大家蒐集一些沒有加密的app的smali源碼,這就是我們自己的圖書館啊!誰用誰知道!

1.棧跟蹤法

 

有時候破解的時候,我們會找一些關鍵的註冊字符串,例如“Register、failed、limit、pro”等等,但是很多程序其實在剛啓動的時候就檢測完了用戶的合法性,所以可能造成定位不準確。一些混淆過的程序一般都是這樣的。這時候我們就可以用棧跟蹤法了,可以查看堆棧,能看到它所調用的方法,查看堆棧代碼:

invoke-static {},Ljava/lang/Thread;->dumpStack()V

它的標籤爲:System.err  實際效果:

java.lang.Throwable: stack dump
  at java.lang.Thread.dumpStack(Thread.java:612)
  at com.lohan.testbed.SomeClass.superFun(SomeClass.java:113)
  at com.lohan.testbed.Main.onCreate(Main.java:48)
  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2679)
  at android.app.ActivityThread.access$2300(ActivityThread.java:125)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2033)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:123)
  at android.app.ActivityThread.main(ActivityThread.java:4627)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:521)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
  at dalvik.system.NativeStart.main(Native Method)

從第三行可以知道dumpStack實際上是在SomeClass類中的superFun()函數裏。然後superFun()函數又是在onCreate()函數裏。以此類推。不過值得一提的是,如果app被混淆了,並且debugging 信息被刪除了或者是被重命名了,會出現“Unknownsource”,但是還是可以判斷的。

1.線程和處理器

很多時候app用了大量線程之間的通信導致了棧跟蹤法變得不是那麼有效了,這個時候如果你還是打印出堆棧的信息的話,只會追溯到最後的那個線程的消息處理器,而原始的那個線程就被隱藏了。當然可以通過手工來追溯,方法是根據代碼中的一些函數類似於sendMessage()等等。這裏有個更簡單的方式:

// 在handleMessage() 中的smali代碼
invoke-static {v0}, Lsample/thread/messaging/ThreadMessaging;->access$0(Lsample/thread/messaging/ThreadMessaging;)Landroid/os/Handler;
move-result-object v2
invoke-virtual {v2}, Landroid/os/Handler;->obtainMessage()Landroid/os/Message;
move-result-object v1
//假設v2是你的處理器
invoke-virtual {v2}, Landroid/os/Handler;->getLooper()Landroid/os/Looper;
move-result-object v2
invoke-virtual {v2}, Landroid/os/Looper;->getThread()Ljava/lang/Thread;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/Thread;->getName()Ljava/lang/String;
move-result-object v2


但是2個線程如果同時在運行,這個方法就不好使了,原因就留給大家去思考了。

 

1.“遺留下的寶藏”

 

這個方法雖然說跟運氣脫不了干係,但在實際的運用中還是比較廣泛的。因爲程序員是人,是人就會有疏忽。在編寫程序的時候,程序員會用很多的log來調試他自己寫的程序是否正確,因此就給逆向人員留下了一筆寶貴的財富。當然,很有可能有一個Boolean類型調試變量來控制是否輸出log信息,類似下面:

public class MyClass {
  private Context myContext;
  private boolean DebugMode;
  
  public MyClass(Context c)
  {
    myContext = c;
    DebugMode = false;
  }
  
  public void someMethod()
  {
    SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(myContext);

		if ( DebugMode )
			Log.d("someMethod", "running someMethod now!");
          
    SharedPreferences.Editor editor = settings.edit();
    try {
      editor.putString("mobileID", getMobileID());
    }
    catch (Exception e) {
        e.printStackTrace();
    }    
    editor.commit();
  }
}


那我們只需要找到DebugMode的位置,把它置成“1”即可。

5.SmaliDebugging(APKtools)

如果log信息都被程序員刪除了,那怎麼辦?此時此刻,我們不得不拿出神器Apktools來調試一段一段的smali語句了,根據官方介紹,還可以單步走。是不是很誘人呢?具體可以看:http://code.google.com/p/android-apktool/wiki/SmaliDebugging

 

6.JDB神器

這是一個很強大的工具,應該是類似GNU或者GDB調試器。打開DDMS獲取端口,然後連接調試器。

在Linux下的命令:

jdb -attach localhost:8616

在Windows下的命令:

jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8616

出現這樣的文字說明成功了:

next toyour App and the console should display something like this:

Setuncaught java.lang.Throwable

Setdeferred uncaught java.lang.Throwable

Initializingjdb ...

三、總結

當然還有更多更好的方法,希望大牛們繼續補充。學習Android已經1個多星期了,菜鳥一枚。很多地方難免有疏漏之處或者錯誤,歡迎各位童鞋指正,也歡迎多多交流,共同學習進步。

ByEricky

2014.12.4





發佈了31 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章