如果說使用dex2jar和JD-GUI獲得了一個APP反編譯後的JAVA代碼,再結合smali代碼調試器來進行調試還不夠爽,不夠暢快的話,下面將介紹一個幫助分析代碼執行流程的大神器。這個神器優點很多,不過遺憾的是它有一個致命的缺點!就是威力太大,能讓使用它的人快速分析出一個復雜APP的執行流程,快速定位關鍵之處進行修改以達到各種目的,尤其對於像我一樣的Android逆向新手來說,這是非常致命的。為什麼非常致命?因為使用了該神器後,1個小時就找到了關鍵代碼,弄清楚執行邏輯,1天之內就實現了程序,解決了外行人看來難度很高的問題。由此帶來的後果就是自我感覺良好,自己感覺自己很牛逼,蒙蔽了自己的雙眼,終日沉溺在這種驕傲的狀態中,從而不能繼續虛心刻苦學習技術知識,久而久之,在技術水平上落後別人一大截,對自身發展造成嚴重影響!所以使用該神器前必須清楚地認識到可能帶來的這些弊端,確認自己能調整好心態以後再繼續往下看,否則請按ALT+F4關閉。
一般的商業APP代碼量巨大,而且做過混淆處理。我所面對的這個APP反編譯後僅JAVA代碼文本就達到了100多MB,做過混淆處理後,代碼裡幾乎看不見一個局部變量的名字,大部分的函數名、類成員變量名都是abcdefg之類,且反編譯後的代碼看起來怪怪的。如果僅僅是靜態分析,讀這些代碼,將會是一件非常痛苦的事情。尤其是對我這種Android正向開發都不會的新手來說,某個按鈕點擊的響應函數在哪裡,下拉刷新的響應函數在哪裡,找起來很困難。好不容易找到了登錄按鈕的響應函數,順著函數調用一層一層往裡看,又遇到了一些抽象方法和異步操作,無法僅從按鈕響應函數的調用棧上找到最後發送網絡請求的關鍵代碼。雖然可以結合smali調試器來分析,下斷點後查看實現了抽象方法的具體對象是什麼,查看調用棧理清調用結構,但整個工作還是進行得很緩慢。再加上調試器各種奔潰和不准,搞去搞來各種心煩,導致了一個嚴重的後果,就是搞著搞著就不由自主的打開了游戲,以調節郁悶的心情,從而擱置了項目進度。
思來想去,我覺得與其主動去分析它的代碼執行流程,不如讓它來主動告訴我它的代碼執行流程。怎麼告訴?首先想到的是打日志,在上篇文章中我們通過打開調試開關,修改它的smali代碼重定向它的日志到android.util.Log,然後打開DDMS在LogCat中看到了它的全部日志。不過這樣還是遠遠不夠,因為它的日志只記錄了一些運行中的狀況和錯誤。我的目的是想讓它的日志告訴我,它調用了哪些函數,以及調用的先後順序。在上篇文章中講到,我們也可以通過TraceView來分析它從登陸按鈕點下到登錄結果出來的這個過程中調用的所有函數,不過TraceView給出的結果充斥著很多很多的系統函數,而且難以看出調用順序,非常不好用。如果能讓這個app通過日志主動告訴我們它調用了哪些函數,且不包含系統函數,僅僅是它自身代碼的函數,那該多好。
首先想到的方法就是手工修改它的smali代碼,插入日志。假如目標APP有一個函數的smali代碼如下:
.method protected getLoginPassword()Ljava/lang/String;
.registers 2
.prologue
.line 809
iget-object v0, p0, Lcom/ali/user/mobile/login/ui/AliUserLoginActivity;->mPasswordInput:Lcom/alipay/mobile/commonui/widget/keyboard/APSafeEditText;
invoke-virtual {v0}, Lcom/alipay/mobile/commonui/widget/keyboard/APSafeEditText;->getSafeText()Landroid/text/Editable;
move-result-object v0
invoke-interface {v0}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
在這段smali代碼裡插入三行代碼:
const-string v0, "InjectLog"
const-string v1, "com.ali.user.mobile.login.ui.AliUserLoginActivity.getLoginPassword()"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
這三行smali代碼對應的JAVA代碼是:
android.util.Log.d("InjectLog", "com.ali.user.mobile.login.ui.AliUserLoginActivity.getLoginPassword()");
當然你可以可以插入其它各種各樣的代碼,你可以先創建一個Android項目,把JAVA代碼寫出來,編譯,再反編譯,再把對應的smali代碼復制出來粘貼進去。
好了,插入完成以後,重新打包,安裝,運行。每當執行這個函數的時候,我們就可以在LogCat中看到輸出,我們就知道它調用了這個函數,包名、類名、函數名都有了。試想,如果我們把它整個app的所有smali代碼中的所有函數全部插入這三行smali代碼,重新打包,安裝,運行。然後,就可以沖一杯咖啡,打開DDMS,打開LogCat,靜靜的看著它把所有的函數調用流程輸出來。然後我們在APP上點一下登錄,整個登錄過程暴露無遺!函數的調用順序就是日志輸出的時間順序,沒有任何雜質,沒有系統函數,那麼純淨,那麼完美。此刻它仿佛沒有了任何秘密,一絲不掛的站在你面前,任由你熾熱的目光在它身上那些精致的部位上掃來掃去。
且慢,先擦干口水。事情沒有那麼容易,就像追一個漂亮的女生一樣,你送一次禮物就想追到她,欣賞她美麗的酮體?做白日夢吧!至少得送上萬次禮物才行。也就是說你需要在這個app的上萬個函數中插入這三行smali代碼,看到這裡,首先要做的事情就是先把你手中的鐵錘收起來,不要打我。
我們當然不可能手工去完成這上萬次操作,我們是程序員,這種重復枯燥的工作自然讓程序來完成。我們可以寫一個文本分析的小程序,遍歷反編譯出來的代碼目錄下的所有smali文件,利用字符串搜索法和正則表達式,找出一個個的函數,並且從smali文件第一行.class的定義中獲取包名和類名,然後從.method的定義中獲取函數名、參數、返回值信息,生成上面的三行smali代碼,插進去。
"自動化"、"批量"、"文本處理"、"腳本",想到這些關鍵詞,自然就想到了Python,用它來干這個事情將會更加得心應手。經過幾小時奮戰,完成了這個批量插代碼的Python腳本。自動批量插入後對APP重新打包,安裝,運行,奔潰了。仔細想想,對,還有寄存器的問題沒有處理,有的函數本來是沒有局部變量,沒有使用寄存器的,".method"定義的函數塊中的第一行為".registers 0",而我插入的代碼裡用到了兩個寄存器。後來修改了Python腳本,在分析每個函數的時候也檢查registers,如果registers小於2個則改為2個。重新插入代碼後,對APP重新打包,安裝,運行,還是奔潰了。後來看了這篇文章:https://liuzhichao.com/p/919.html了解了registers的意義,發現我自己寫的Android APP反編譯以後每個函數第一行都是.locals,而我反編譯的這個商業APP的代碼中每個函數第一行都是.registers。使用.registers聲明寄存器數量有個很大的不好之處就是參數寄存器也包含在.registers聲明的寄存器中,如果一個函數有兩個參數,沒有局部變量,那麼這個函數會聲明.registers 2,我的腳本檢測到這裡認為寄存器夠用,然後就插入了代碼,而我插入的代碼裡用到了v0和v1寄存器,在賦值的時候把參數寄存器的內容覆蓋了,因此帶來了一些問題。而且如果它的代碼裡本來就使用了v0、v1寄存器,我直接這樣把代碼插進去,也會帶來一些影響。
要解決這個問題,可以繼續完善Python腳本,對它的smali代碼進行更多的分析,分析這個函數已經用了多少個寄存器,有多少個參數寄存器,序號分別是什麼,然後再生成合適的插入代碼並修改.registers數量,但這樣做的話就比較麻煩了。偷懶是我的作風,而這種做法顯然不符合我的作風,於是我覺得不應該再繼續完善Python腳本,而是精簡Python腳本。Python腳本不再分析它smali代碼中的函數,而是直接不管三七二十在每個函數中插入一行對void PrintFunc()的調用代碼。void PrintFunc()是我自己寫的函數,無參數無返回值,調用它對應的smali代碼大概是這樣的:
invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V
不使用任何寄存器,沒有返回值,顯然這樣的代碼插入到目標APP中的函數中,不會對宿主函數造成任何影響。同時新建一個Android應用項目,寫下如下JAVA代碼:
package com.hook.testsmali;
import android.util.Log;
public class InjectLog
{
public static void PrintFunc()
{
Thread cur_thread = Thread.currentThread();
StackTraceElement stack[] = cur_thread.getStackTrace();
Log.d("InjectLog", stack[3].toString() + "[" + cur_thread.getId() + "]");
}
}
注意,我的包名是com.hook.testsmali,類名是InjectLog,函數名是PrintFunc()。你寫的JAVA代碼不必和我一樣,但調用PrintFunc()的smali代碼要和JAVA代碼的包名、類名、函數名一致。
從上面的代碼可以看到,在PrintFunc()的實現上,先獲取調用棧的信息,然後取出調用棧中下標為3的元素,這個正是調用void PrintFunc()的調用者的函數名、包名、類名,然後隨同線程ID信息,通過日志輸出。隨後精簡我們的Python腳本,Python腳本需要干的事情就是遍歷所有smali文件,向smali中的每一個函數中插入invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V 完畢!然後我們對自己寫的這個APP進行反編譯,拿到這個void PrintFunc()函數的smali代碼文件InjectLog.smali,在目標APP的代碼文件夾中創建這個路徑com/hook/testsmali然後把InjectLog.smali放進去,重新打包目標APP,安裝。然後打開DDMS,設置LogCat過濾器過濾出Tag為InjectLog的日志。
接下來,噓!!運行目標APP,此後在目標APP中的每一次點擊,都是那麼酣暢淋漓,LogCat中的日志如潮水般湧出,四處噴濺。對,就是那麼的絲滑,那麼的暢快,那麼的清澈,有沒一絲雜質。效果如下圖所示,每一個過程,每一個步驟,都赤裸裸的展現在你面前,看得你面紅耳赤!
我們也可以不輸出線程ID,因為LogCat已經標示出TID來了,但是導出日志後這個TID就不見了,所以還是輸出一下。日志的輸出時間順序就是函數的調用順序,順著這個調用順序看dex2jar和JD-GUI反編譯出的JAVA代碼,定位超快、效率超高,分分種種搞出些事情來。什麼?你問我搞什麼事情?它都一絲不掛在你面前了,你還問我搞什麼事情!
最後貼上用於批量插smali代碼的Python腳本,雖然丑陋但是簡單粗暴、野蠻有效:
import os
class ParserError(Exception):
pass
# 注入代碼到一個函數塊中
def inject_code_to_method_section(method_section):
# 靜態構造函數,無需處理
if method_section[0].find("static constructor") != -1:
return method_section
# synthetic函數,無需處理
if method_section[0].find("synthetic") != -1:
return method_section
# 抽象方法,無需處理
if method_section[0].find("abstract") != -1:
return method_section
# 生成待插入代碼行
inject_code = [
'\n',
' invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V\n',
'\n'
]
#插入到.prologue的下一行
is_inject = False
for i in range(0, len(method_section)):
if method_section[i].find(".prologue") != -1:
is_inject = True
method_section[i + 1: i + 1] = inject_code
break
if not is_inject:
raise ParserError("找不到.prologue")
return method_section
def inject_log_code(content):
new_content = []
method_section = []
is_method_begin = False
for line in content:
if line[:7] == ".method":
is_method_begin = True
method_section.append(line)
continue
if is_method_begin:
method_section.append(line)
else:
new_content.append(line)
if line[:11] == ".end method":
if not is_method_begin:
raise ParserError(".method不對稱")
is_method_begin = False
new_method_section = inject_code_to_method_section(method_section)
new_content.extend(new_method_section)
method_section.clear()
return new_content
def main():
walker = os.walk("./")
for root, directory, files in walker:
for file_name in files:
if file_name[-6:] != ".smali":
continue
file_path = root + "/" + file_name
print(file_path)
file = open(file_path)
lines = file.readlines()
file.close()
new_code = inject_log_code(lines)
file = open(file_path, "w")
file.writelines(new_code)
file.close()
if __name__ == '__main__':
main()
需要注意的是在向目標APP的smali代碼的每個函數中插代碼時,並不是所有函數都需要插,類的靜態構造函數一般不需要插,因為它是用來初始化類的靜態成員的,沒有太多的關鍵代碼。synthetic函數也不需要插,我不知道synthetic函數是什麼意思,但是發現smali中的synthetic函數並沒有被dex2jar和JD-GUI反編譯為JAVA函數,所以忽略它。另外抽象方法也不需要插,這個不用解釋。當然這個批量自動插代碼的程序可以使用任何編程語言實現,它只是個文本處理工具。
另外在實際使用中,並不一定要給目標APP的所有函數插代碼,我們可以先根據包名猜測一下它的功能,然後對這個包下的所有函數進行插代碼。
最後,千萬不要低估此神器的威力,我在使用過程中屢試不爽,結合smali代碼調試器,很快就分析出登錄按鈕點下去後干了什麼,以及最終發送了什麼HTTP請求,收到什麼響應內容。親手試一試,相當亦可賽艇!