Android逆向之旅---執行時修改記憶體中的Dalvik指令來改變程式碼邏輯

yangxi_001發表於2016-12-02

一、前言

最近在弄脫殼的時候發現有些加固平臺的加固方式是修改了dex檔案結構,然後在載入dex到記憶體的時候,在進行dex格式修復,從而達到了apk保護的效果,那麼在dex載入到記憶體的時候,如何進行dex格式的修復呢?其實原理就是基於執行時修改記憶體中的Dalvik資料,本文就來用一個簡單的例子來介紹一下如何在記憶體中去修改Dalvik指令程式碼來改變程式碼本生的執行邏輯。在講解本文之前,一定要先看這篇文章:Android中Dex檔案格式詳解 這篇文章主要介紹了關於Dex檔案的格式介紹,這個對於後面修改記憶體中Dex的資料是有很大的作用的,如果沒有閱讀完這篇文章,或者是不瞭解Dex的檔案格式的話,下面介紹的內容將會看的很吃力,因為本文不會在重複的介紹一下Dex的檔案格式了。好了,下面就開始我們今天的介紹主題了。


首先來說一下案例場景:

本文的案例就是將一個類的方法改變他的執行邏輯,原來的邏輯是加法操作,現在改成乘法操作。


二、案例介紹

這裡在TestAdd類中有一個加法的操作方法,然後我們在native層去修改Dalvik指令,修改這個方法為乘法操作


這裡我們可以看看TestAdd類的實現:


只是一個簡單的加法操作,下面來呼叫執行測試:


測試結果如下:


看到了測試結果,發現是乘法,而不是加法的結過,好了,到這裡我們就演示完了專案結構,下面來開始說說如何修改的:


三、程式碼解析

這裡把修改指令的邏輯程式碼放在了native層做的,其實放到Java層也是可以的,但是會發現在操作記憶體位元組的時候,Java會顯得很無力,因為他沒有指標,指標真的很好很強大,特別是在操作檔案和記憶體位元組的時候,那寫程式碼都很爽的。

在MainActivity中定義了一個native方法:


我們使用javah指令產生標頭檔案:


執行命令的時候,注意目錄和類全名沒有字尾。


把產生的標頭檔案拷貝到工程中:



四、原理解析

下面開始寫主要邏輯了,在編寫邏輯之前,這裡我還是說一下Dex的檔案格式,以及本文章需要用到的幾個資料結構:


第一個資料結構:Uleb128

 LEB128 ( little endian base 128 ) 格式 ,是基於 1 個 Byte 的一種不定長度的編碼方式 。若第一個 Byte 的最高位為 1 ,則表示還需要下一個 Byte 來描述 ,直至最後一個 Byte 的最高位為 0 。每個 Byte 的其餘 Bit 用來表示資料 。這裡既然介紹了uleb128這種資料型別,就在這裡解釋一下,因為後面會經常用到這個資料型別,這個資料型別的出現其實就是為了解決一個問題,那就是減少記憶體的浪費,他就是表示int型別的數值,但是int型別四個位元組有時候在使用的時候有點浪費,所以就應運而生了,他的原理也很簡單:

圖只是指示性的用兩個位元組表示。編碼的每個位元組有效部分只有低7bits,每個位元組的最高bit用來指示是否是最後一個位元組。
非最高位元組的bit7為0
最高位元組的bit7為1
將leb128編碼的數字轉換為可讀數字的規則是:除去每個位元組的bit7,將每個位元組剩餘的7個bits拼接在一起,即為數字。

比如:
LEB128編碼的0x02b0 —> 轉換後的數字0x0130
轉換過程:
0x02b0 => 0000 0010 1011 0000 =>去除最高位=> 000 0010 011 0000 =>按4bits重排 => 00 0001 0011 0000 => 0x130

底層程式碼位於:android/dalvik/libdex/leb128.h

這個結構非常重要,也是後面所有結構的基礎,而且在後面看解析程式碼中都會用到這個結構,知道他的原理,解析程式碼就很簡單了。


第二個資料結構:string_ids_item

struct string_ids_item
{
uint string_data_off;
}

這個結構主要是儲存dex檔案中所有的字串的,其中的string_data_off欄位是字串在dex檔案中的相對地址,每個字串都是uleb128格式的,解析完每一個字串都存放到一個字串池中,這個池子中的字串索引值StringIdIndex將會被下面幾個資料結構使用到。

類似於這樣:



第三個資料結構:type_ids_item

struct type_ids_item
{
uint descriptor_idx;
}

這個資料結構,主要儲存的是dex中所有類,方法等型別的id值,這裡的descriptor_ids欄位就是上面的字串常量池中的索引值id,這裡解析完之後也會存放在一個常量池中,給後續的資料結構使用,類似於這樣:



第四個資料結構:class_def_item

struct class_def_item
{
uint class_idx;
uint access_flags;
uint superclass_idx;
uint interfaces_off;
uint source_file_idx;
uint annotations_off;
uint class_data_off;
uint static_value_off;
}

這個資料結構主要儲存的是dex中所有類的元資訊,其中class_idx欄位是一個TypeId型別的,就是上面的TypeId池子中的索引值,其他的欄位不解釋了,還有一個重要的欄位就是class_data_off這個欄位就是類資料在dex檔案中的相對地址。

解析結構類似於:



第五個資料結構:method_id_item

struct method_id_item
{
ushort class_idx;
ushort proto_idx;
uint name_idx;
}

這個資料結構主要儲存的是dex中所有的方法元資訊,其中class_idx欄位是上面類資料結構中的class_idx,name_idx欄位是在字串池中的索引值id,這裡解析完之後,也是需要用一個池子來存放這些MethodId索引值,下面的一些結構需要用到。

解析結構類似於:



第六個資料結構:class_data_item

struct class_data_item
{
uleb128 static_fields_size;
uleb128 instance_fields_size;
uleb128 direct_methods_size;
uleb128 virtual_methods_size;
encoded_field static_fields [ static_fields_size ];
encoded_field instance_fields [ instance_fields_size ];
encoded_method direct_methods [ direct_method_size ];
encoded_method virtual_methods [ virtual_methods_size ];
}

這個資料結構主要儲存的是類中的具體資訊,這裡包括static欄位,類欄位,static方法,類方法等資訊,這個資料結構資料在dex中的位置,就是上面的class_def_item資料結構中的class_data_off欄位的地址。

解析結構如下:



第七個資料結構:encoded_method

struct encoded_method
{
uleb128 method_idx_diff;
uleb128 access_flags;
uleb128 code_off;
}

這個資料結構主要存放的是一個方法指令的元資訊,method_idx_diff欄位是上面的方法型別池中的索引值id,code_off欄位就是這個方法儲存的指令程式碼相對地址。

解析結構如下:



第八個資料結構:code_item

struct code_item
{
ushort registers_size;
ushort ins_size;
ushort outs_size;
ushort tries_size;
uint debug_info_off;
uint insns_size;
ushort insns [ insns_size ];
ushort paddding; // optional
try_item tries [ tyies_size ]; // optional
encoded_catch_handler_list handlers; // optional
}

這個資料結構主要儲存的就是方法的指令的具體資訊,其中ins_size欄位表示的是這個方法的引數個數,insns_size欄位表示的是這個方法的指令條數,insns欄位儲存的就是指令內容,解析結構如下:



到這裡我們就介紹了完了上面的幾種資料結構,下面來總結一下:

首先用一張圖來看看:


1》首先介紹了Uleb128資料結構,這個資料結構是貫穿所有資料結構的,所以這個資料結構是最基本的。

2》然後介紹了string_ids_item資料結構,這裡需要把dex中所有的字串解析出來存放在一個常量池中,後續的資料結構會通過索引值來獲取。比如type_ids_item資料結構中的idx欄位指向的就是這個索引值。

3》然後介紹了type_ids_item資料結構,這裡的欄位主要就是指向字串常量池的索引值,解析完這個資料結構之後也是需要用一個常量池儲存索引值,後續的資料結構會通過索引值來獲取,比如class_idx資訊。

4》然後介紹了class_def_item資料結構,這裡的欄位主要是類的具體資料的偏移地址,類的索引值class_idx(這個是通過TypeId池獲取)。

5》然後介紹了method_id_item資料結構,這裡主要是開始解析方法id內容,其中name_idx欄位是字串常量池中的值(通過StringId池獲取),以及這個方法所在類的class_idx值。

6》然後介紹了class_data_item資料結構,這個結構主要儲存的是這個類對應程式碼的元資訊,其中direct_methods thod欄位和virtual_methods欄位就是儲存的是類對應的指令元資訊。他們的結構是encoded_method。

7》然後介紹了encoded_method資料結構,這個結構中主要儲存的是這個方法對應的元資訊,其中method_idx_diff欄位是上面得到的Method型別的id常量池索引值,code_off欄位是這個方法指令對應的相對地址。

8》最後介紹了code_item資料結構,其中insns欄位就是儲存的是方法的指令資料。


來看看各個資料結構之間的關係:


這裡就算全部解析完了本文需要用到的資料結構,以及這些結構相互引用關係,這裡還需要注意的是:這裡的一些結構的解析地址是存放在dex的頭部資訊中的:



下面我們在看看程式碼怎麼去實現它,首先我們通過上面的例子看到,有一個字串索引常量池,而我們現在如果想改一個方法的指令程式碼的話,肯定得找到他對應的code_off地址即可,現在我們有的入口條件就是:方法名+類名,所以思路是這樣的:

類名=》字串索引常量池=》型別TypeId=》類的定義元資訊ClassDefItem=》類的程式碼元資訊ClassDataItem

方法名=》字串索引常量池+類的TypeId=》方法Id

方法Id+類程式碼元資訊ClassDataItem=》方法的元資訊EncodeMethod=》方法的指令程式碼結構Code


有了這個思路,下面來看具體程式碼吧:

第一步:獲取應用的記憶體資料地址,讀取/proc/pid/maps檔案



第二步:解析出dex檔案的記憶體地址

因為我們知道一個程式的maps記憶體中是有很多資料的,所以這裡需要先找到dex資料對應的起始地址:


下面看程式碼如何進行解析這個起始地址和結束地址:


這裡我們列印一下起始地址和結束地址:


通過列印的資訊,可以看到,我們上面在使用命令列去檢視maps檔案的時候是多個dex檔案的,但是用程式碼去去的時候就是隻有一個dex內容,但是不管怎麼樣,最終的起始地址和結束地址是一樣。


第三步:校驗dex檔案

這裡為什麼說校驗檔案呢?因為在記憶體中存在的是odex檔案了,這個檔案其實是dex檔案進行優化之後的產物,看一下odex檔案的格式:


這個檔案格式定義在DexFile.h標頭檔案中,原始碼位於:Android原始碼目錄\dalvik\libdex\DexFile.h


這裡的u1型別就是一個位元組,u4就是4個位元組,那麼頭部大小就是:8+4*9=40.

所以,我們得到上面的起始地址,是odex檔案的地址,我們需要獲取到dex檔案的偏移地址,然後在進行dex檔案的頭部資訊校驗,這裡主要是校驗dex檔案的魔數:dex\n35,注意中間有一個換行符哦:


下面來看具體程式碼:


這裡首先需要獲取系統的對其頁面大小,一般都是0x1000,然後我們採用的是用記憶體的結束地址開始往前找,記得每次在校驗dex的時候,需要考慮DexOptHeader的大小是40,每次疊加的大小是頁面大小。呼叫findmagic方法來校驗dex檔案:


注意魔數中間有一個換行符即可。


第四步:獲取方法和類的StringId索引值


關於這裡的getStrIdx方法這裡不再說了,程式碼後面會給出地址的,通過類的名稱和方法的名稱獲取他們的StringId值


第五步:獲取方法和類的TypeId索引值


這裡關於getMethodIdx方法這裡不再說了,程式碼後面會給出地址的,通過類的StringId獲取ClassTypeId,再通過ClassTypeId和方法的StringId獲取方法的MethodTypeId值。


第六步:獲取ClassDefItem地址



第七步:獲取方法的指令程式碼


這裡主要獲取指令的大小,然後在獲取指令程式碼,列印出來,我們可以分析指令的含義,看一下列印的結果:


這裡列印了指令大小是3,那麼指令位元組長度就是3*2=6個位元組,下面列印了指令的位元組是:90 00 02 03 0F 00

到這裡,其實我們已經完成了從記憶體中讀取一個方法指定的指令位元組了,下面就來看看如何修改這個指令吧,其實在修改之前,我們肯定得先去看懂這指令的含義。


一條指令其實組成部分就是:指令碼+指令運算元,一般指令的頭幾個位元組就是指令碼,而且指令碼是可以查閱的,這裡需要參考:Bytecode for Dalvik VM  比如這裡的90指令碼:


看到了,這裡的90指令碼就是對應的int型別的加法操作,而且看到指令的格式是:binop vAA,vBB,vCC,再看看這個指令的解釋:


意思是,vBB+vCC=vAA。那麼看到方法的定義是,傳入兩個引數,還需要一個引數用來存放結算值,我們可以反編譯apk得到方法的smali語法看看:


看到了,smali語法就是和我們分析的一樣,那麼這裡的指令分析結果:

90 00 02 03 0F 00



分析完了指令,下面我們可以修改了,這裡把加法變成乘法,其實就是改變操作碼:

通過查閱,92代表乘法指令碼,那麼這裡把90改成92即可,下面就來進行修改:


但是在修改之前還需要做一件事,改變記憶體屬性,因為一般資料被載入到記憶體中是不允許修改了,但是可以藉助mprotect函式來進行修改即可,因為我們只要修改add方法指令,那麼只需要修改這個方法所在記憶體的那一頁進行修改即可,其他的地方不需要改成可寫模式的,改完之後,在使用memcpy函式進行記憶體資料的覆蓋即可:


看到了,通過上面這麼已修改之後,add方法就變成了乘法了,這就是我們在文章開始的時候演示的傳入的是4,5得到的全是20不是9的原因了,到這裡我們也成功的修改了記憶體中的方法指令,下面來總結一下:


六、修改流程步驟總結

1》首先是需要確定method的名字,也就是method字串,再確定method所在class的名字,也就是class字串。這確定了要修改的方法。
2》把2個字串到DexStringId結構的位置進行搜尋,進而得到2個字串在DexStringId中的索引序號。
3》對於class字串,再通過DexStrngId索引序號,到DexTypeId中搜尋,得到class字串在DexTypeId中的索引序號。
4》還是class字串,得到DexTypeId中的索引序號後,再到DexClassDef中搜尋,就可以得到class的DexClassDef的位置。其中classDataOff欄位記錄了DexClassData偏移,這個結構裡可以找到DexMethod結構。
5》而對於method字串,找到DexStringId索引後,再結合上面class的DexTypeId,可以確定DexMethodId的索引序號。
6》 現在我們知道了DexMethodId的索引序號,也知道了存放DexMethod的位置,直接搜尋就可以得到我們要找的DexMethod。這個結構裡的codeoff指向了存放指令的位置。

得到指令之後,我們在對照指令表,來閱讀指令的含義,然後將指令碼進行替換,修改記憶體為可讀可寫模式,最後再把修改之後的指令寫到記憶體中。


專案下載地址:http://download.csdn.net/detail/jiangwei0910410003/9565563


七、存在的問題

上面看到修改記憶體指令其實還是比較簡單的,因為本文主要介紹如何修改記憶體指令原理,所以用一個簡單的例子來進行說明,但是在實際的過程中有這麼一個問題就是我們在修改一個比較複雜的方法指令的時候,如果出現增加或者減少指令的情況,那麼我們的指令程式碼的偏移地址就會發生改變,這樣就會很麻煩,需要進行dex結構修復了,而這部分工作將會變得很難。


八、技術用途

本文介紹了執行時修改記憶體的指令程式碼,其實有兩個用途:

第一個用途:就是在本文開始的時候說到了,現在很多加固平臺會操作apk中的dex檔案,修改他們的資料結構,比如ClassDefItem,然後在記憶體執行dex的時候在進行結構修復,這樣就可以避免一些破解者破解apk的風險了。

第二個用途:在裝置root之後,可以注入到其他程式,然後修改其他程式的核心方法指令,從而達到一些自己想要的想過,但是上面也說道了,這種方式最大的問題是在修改指令之後,需要進行一定的修復,不然dex結構也是被改亂了,程式就會崩了。


九、技術概要

1、瞭解了Dex檔案的幾種資料格式

2、DexOpt格式瞭解以及dex和odex之間的關係

2、分析瞭如何通過類名和方法名找到其對應記憶體的指令程式碼,並且獲取相對應的指令

3、使用mprotect函式進行記憶體模式的修改

4、Dalvik指令位元組碼的瞭解


十、總結

執行時修改記憶體中的指令程式碼思路其實很重要,而且對dex中各個資料結構的瞭解,所以本文的流程是:先了解dex中一些資料結構,然後在設想修改思路,找到記憶體中對應方法的地址,得到指令,然後進行修改覆蓋即可。

相關文章