來自i春秋作者:penguin_wwy

一、理論基礎(我們先講道理)

上回說到我們找到了dex中的加密字符串 提取加密字符串。 觀眾老爺們問:那么找到這些加密字符串有什么作用呢?該看不懂的還是看不懂啊。。。

那么今天我就來告訴大家,找到的這些加密字符串我們該怎么利用。 首先來觀察一下加密字符串出現時的場景,一般情況下是這樣

paramContext.getSharedPreferences(Fegli.a("SjUIVhhB:&Zi2}3mo@i"), 0);

對于動態調用,或者反射等等之類的行為來說,加密的字符串肯定是需要解密之后才能用的。也就是說加密字符串一般會作為解密函數的輸入,而解密函數的輸出則會成為目標函數如Class.forName之類的函數輸入。 看完Java代碼我們再來看看smali代碼

[color=#000000]const-string v0, "SjUIVhhB:&Zi2}3mo@i"
?
invoke-static {v0}, Lcom/molniya/free/Fegli;->a(Ljava/lang/String;)Ljava/lang/String;
?
move-result-object v0
?
invoke-virtual {p1, v0, v1}, Landroid/content/Context;->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;[/color]

由于加密字符串是直接寫到函數中的,沒有用變量保存,所以在smali中必然是const-string指令,之后的下一條指令必然是調用解密函數,也就是說是invoke指令。換句話說,我們找到一條const-string指令,它的值又恰好是加密字符串,并且它的下一條指令是invoke類型的指令,那么調用的這個函數就極大的可能(99.99999999...%)是解密函數了。我們還可以進行函數檢查,比如這個函數的輸入是不是一個Ljava/lang/String類型,輸出是不是Ljava/lang/String類型,如果都是,那我們可以斷定,這個就是解密函數(此處應有掌聲,啪、啪、啪)。

二、實踐過程(弟兄們,抄家伙動手)

下面我們就可以在Androguard的基礎上來實現了。 首先,我們先看看Androguard為我們提供了哪些東西。如果大家讀過源碼的話(沒讀過也沒關系,反正我讀過)應該可以發現這樣一句

vmx = analysis.VMAnalysis( vm )

這個vm就是我們之前講的DalvikVMFormat類,它保存了dex文件的全部結構。這個VMAnalysis,從名字就可以看出來和分析有關。在這個類的初始化當中有這樣一段

for i in self.vm.get_methods():
????x = MethodAnalysis( self.vm, i, self )
????self.methods.append( x )
????self.hmethods[ i ] = x
????self.__nmethods[ i.get_name() ] = x

從vm中獲得所有method,然后調用MethodAnalysis進行分析。在MethodAnalysis中我發現了這個

code = self.method.get_code()

還有這個

bc = code.get_bc()

以及這個

instructions = [i for i in bc.get_instructions()]

不知道instructions 是什么意思的童鞋可以查一下英文字典,這個在計算機中表示指令的意思。也就是說這個instructions列表,保存了函數中的所有指令 這里我們需要要簡單了解一下Dalvik的指令集,詳細內容可以看這里http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html 。

具體的內容很難說清楚(反正我是很難說清楚,掌握的還不透徹),為了不誤人子弟,我就簡單說說我們用到的內容。正常情況下我們反編譯出來的smali代碼和指令集的字節碼是對應的也就是instructions的每一個元素,代表一行samli代碼(不是Java代碼)。每一行smali代碼由4或者6字節組成,第一個字節表示op值,也就是代表一個操作。比如const-string的op值為0x1a,invoke-static的op值為0x71。而其他字節根據op值決定的操作類型分別代表寄存器編號啊,寄存器數量啊等等等等。還是舉例說明,比如我們看到的const-string v0, "Hello"這句代碼會由4字節指令構成。第一個字節為0x1a,表示const-string操作符。第二個字節表示寄存器下標,0就是v0,1就指v1。三四字節會表示操作的字符串在字符串池中的id(注意!!!)。

再舉個例子,比如invoke-static {v0}, Lcom/molniya/free/Fegli;->a(Ljava/lang/String;)Ljava/lang/String;這句。會有6個字節指令。第一個字節0x71表示invoke-static操作符。第二個字節的高四位,指調用這個函數需要的寄存器個數(注意,如果是靜態函數,那么寄存器個數和參數個數相等。如果不是,那么要增加一個p0寄存器,保存this指針)。第三和第四字節保存被調用method在method_id,每個methon_id為一個MethodIdsItem結構,該結構三個元素

public short class_idx;
public short proto_idx;
public int name_idx;

第一個指向它所屬的class,第二個是函數原型,第三個是函數名稱。 第五和第六個字節每四位代表一個寄存器。等等,第二個字節的低四位呢。嗯,保存的是第五個寄存器。。。(思索臉(′?ω?`))其實看到這里我挺驚訝的,并不是因為它保存的是第五個寄存器的值,而是在以往我看的arm體系中,會用四個寄存器保存參數,不夠的話再通過棧保存。這里我也不知道為什么會是奇數個(也有可能是我想多了),不夠了怎么辦。。。還是學的不夠深入。哪位表哥了解,還請指教一下。扯遠了。

簡單的介紹一下指令集,我們繼續。現在可以獲得每個函數的指令,我們就可以遍歷這些指令,op值為0x1a的就檢查它操作的字符串是不是加密字符串,如果是就看它下一行指令,op值在不在0x6e到0x72之間(invoke-virtual、super、direct、static、interface的op值),如果在就獲取可以它的method_id,然后檢查參數類型返回類型,都符合那這個method就是解密函數了。 總結一下過程:

獲取指令 ——> 遍歷指令 ——> 如果是const-string ——> 檢查字符串 ——> 符合則檢查下一條指令 ——> 符合則獲取method,再檢查類型。

看起來步驟也不是很多,但必須對dex文件結構有清醒的認識,還需要一點點指令集的知識。

下面是我寫的核心代碼

class decryptMethonA:
????def __init__(self, encrypt, vm):
????????self.encrypt = encrypt
????????self.vm = vm
????????self.methons = self.vm.get_methods()
????????#self.register = 0
????????self.methon_dict = {}
????????self.methon_info = []
?
????def analyze(self):
????????for methon in self.methons:
????????????code = methon.get_code()
????????????if code == None:
????????????????continue
?
????????????bc = code.get_bc()
????????????instructions = [i for i in bc.get_instructions()] #獲取指令
????????????flag = 0
????????????for i in instructions:
????????????????if flag == 1:
????????????????????self.add_methon(i)??????? #如果是檢查下一條指令
????????????????????flag = 0
????????????????if self.searchFor(i): #op是否為0x1a
????????????????????flag = 1
?
????def searchFor(self, ins):
????????op_value = ins.get_op_value()
????????if op_value == 0x1a:
????????????string_name = self.vm.get_cm_string(ins.get_ref_kind())
????????????return string_name in self.encrypt
????????return False
?
????def add_methon(self, ins):
????????op_value = ins.get_op_value()
????????if (op_value >= 0x6e and op_value <= 0x72) or (op_value >= 0x74 and op_value <= 0x78):
????????????idx_meth = ins.get_ref_kind()
????????????meth_info = self.vm.get_cm_method(idx_meth)
????????????if meth_info[2][1] == 'Ljava/lang/String;':
????????????????if meth_info not in self.methon_info:
????????????????????self.methon_info.append(meth_info)
????????????????????self.methon_dict[self.methon_info.index(meth_info)] = 1
????????????????else:
????????????????????self.methon_dict[self.methon_info.index(meth_info)] += 1
?
????def get_meth_dict(self):
????????return self.methon_dict
?
????def get_meth_info(self):
????????return self.methon_info

三、測試結果(激動人心的時刻)

圖片有點小,觀眾老爺們將就一下,但還是可以看到,我們成功輸出了這個函數。

四、總結性發言

說幾點問題。

第一:上次說到我們判斷隨機字符串,也就是判斷哪個字符串是加密字符串的算法還有誤差,正確率不高。那么對于我們判斷解密函數會不會有影響呢?其實我覺得沒有。我們大可對每個加密字符串(其中包含了誤判)進行搜索,然后統計我們找到的解密函數每個的次數,次數最多的一定是解密函數。找到解密函數后可以再回頭看它的參數,一定是加密字符串,又可以將加密字符串中誤判的過濾。

第二:找到解密函數之后,怎么辦。最簡單的可以寫個apk,不干別的。就加載這個dex,然后通過反射,找到解密函數,將加密字符串傳入,然后調用,就可以獲得正確的字符串了。我已經通過代碼實現了,大家有興趣也可以試試,核心代碼其實就三句

DexClassLoder classLoder = new DexClassLoder(目標dex,..., ..., ...);
Class clazz = classLoder.loadClass(目標類);
clazz.getMethod(目標函數,object [] {...}).invoke(null, 加密字符串);

就完成了。這樣我們就可以實現完全自動化解密dex中的加密字符串。

本文由i春秋學院提供:http://bbs.ichunqiu.com/thread-11730-1-1.html?from=paper


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/43/