隱藏smali方法后
java源碼:
int b = fun2();
baksmali解釋為:
invoke-virtual {v1}, <int MainActivity.fun2() imp. @ _def_MainActivity_fun2@I>
查看字節碼:
6E 10 4E 32 01 00
6E 為 OP_INVOKE_VIRTUAL
要看 OP_INVOKE_VIRTUAL 指令的字節碼格式,解釋器是如何做指令和參數解釋的
官方文檔:【Dalvik bytecode】、【Dalvik Executable instruction formats】
invoke-virtual 后面有至少3個參數
A:
參數字數(4 位)B:
方法引用索引(16 位)C..G:
參數寄存器(每個寄存器各占 4 位)
再看invoke-vitrual 這類指令的id 是 35c
看35c這類指令的格式
看看ID 的含義
大多數格式 ID 包含三個字符:前兩個是十進制數,最后一個是字母。第一個十進制數表示格式中 16 位代碼單元的數量。第二個十進制數表示格式包含的最大寄存器數量(使用最大值是因為某些格式可容納的寄存器數量為可變值),特殊標識“r
”表示已對寄存器的數量范圍進行編碼。最后一個字母以半助記符的形式表示該格式編碼的任何其他數據類型。例如,“21t
”格式的長度為 2,包含一個寄存器引用,另外還有一個分支目標。
所以35c = 有3個16位代碼單元(3個“單詞" 空格分割),最大支持5個寄存器數,c表示其他數據類型為常量池索引
按位布局
A|G|op BBBB F|E|D|C
每個以 空格 分割的區域被稱作“單詞”
每個單詞占16位
”|“ 豎線用來平均分割該單詞內的位寬
“op
”一詞用於表示格式內八位操作碼的位置
解析一下:
這里有3個單詞,每個單詞16位(2個字節),因為是小端序,所以需要重新按適合人類閱讀的方式排列一下
6E 10 4E 32 01 00 就成了
10 6E 32 4E 00 01
第一個單詞區域:
A|G|op = A(4位)G(4位)op(8位)
1|0 |6E = A=1 ,G=0, op=6E
op=6E = Invoke-vitural
A=1
G=0
第二個單詞區域:
BBBB = 32 4E
第三個單詞區域:
F|E|D|C = 00 01
F=0
E=0
D=0
C=1
根據A = 1
其操作碼是
最后整理一下
op {vC},kind@BBBB 等於
op=6E=invoke-vitral
C = 1 , 那么{vC} = {v1}
BBBB=324E ,那么kind@BBBB = kind@324E
整個指令為
invoke-vitural {v1},kind@324E
現在看起來和backsmali解釋的非常相似了
根據 B:
方法引用索引(16 位)
那么我們去看看324E = 12878 (十進制)的方法是什么
所以在執行階段可以看出即使把class_defs[]中的方法 隱藏后
在解析指令的時候,是根據方法索引在method_ids[] 里去找的方法,然后根據方法名稱和簽名打印出smali的指令的人類可讀命令字符串就完事了
但執行的時候會拋出異常
所以估計是找到method 對象后,根據給出的 class_idx 找到class,然后根據
1.函數名 和 函數簽名
或
2.相同method_ids 的索引號(也就是324E)
去找該class 下 的 viturl方法列表(因為這里是invoke-vitural指令)中匹配的方法
但是發現找不到(因為原來fun2 方法指向的方法索引被改為為 fun1),就拋出異常(具體要看虛擬機如何執行invoke-viturl指令的)
反編譯:
apktool 會提示 #dupliate method ignord ,並反編譯出 fun1 的代碼
jeb 不提示異常,並解析出fun1的代碼2次
android studio 不提示異常,但是看得到fun2 的導出(斜體字體,類似依賴的外部方法),但是無法查看bytecode
dex2jar 會提示 duplicated method
ida 不提示異常,看不到 fun2 有export,也沒有找到 fun2的代碼
對於隱藏方法,但不隱藏bytecode的做法
1.# Method 0 (0x0)
2.# Size of bytecode (in 16-bit units): 0x2 但是下面沒有bytecode
手動c一下(轉為code解析),正好2個16位的
於是浮出水面,但是IDA除了看mehtod(0x0) 沒辦法發現存在異常的線索,所以還是先校驗一次比較靠譜
校驗:
1.檢查method id 為0的
2.根據mehtods_defs[] 總大小 除以 單個元素大小,得到實際總元素數量,對比 virtual_methods_size 的數值,看是否一致
但是進一步,可以把 virtual_methods_size 的大小減一,並且將隱藏方法的Dex_Method 刪掉,重新計算文件索引和offset、checksum等,在運行時,打開dex文件二進制流到內存,用dex對象解析他,並插入隱藏的方法,然后重新計算偏移,池索引號,文件offset,checksum等,然后用dexclassloader 加載后,運行。
不如在編譯時給編譯器增加某個編譯選項,可以不把一些方法編譯進主dex(這個在編譯環節應該比重新計算修改后dex,計算一大堆offset要好吧)
這種方法其實是將dex內本來存在有字節碼的方法索引和方法字節碼本身去掉,只留下DexMehtodID,因為指令的 中的 kind@BBBB 需要給出一個和索引號對應的DexMethodId元素,而這個元素內包含該方法所屬的1.類 2.方法簽名 3.方法名
由於反編譯時,該方法所定義的類中的DexMethod 和 字節碼被刪除(或隱藏),所以反編譯器認為這個所指向的方法是一個外部引用 (如 當你調用 android.util.Log 中的i方法時,Log類的字節碼不需要你打包進apk,因為在運行時會自動加載)
(圖中,該dex中只有1個自定義class,但需要用到androiod.util.Log.i 方法,但是該類並未在dex中定義,只有一個DexmethodID存在)
可以更進一步修改,既然刪掉了方法的定義,那不如也把調用該方法的相關字節碼也修改掉,讓他指向一個別的方法,這樣 DexMethodID 也可以刪掉或者修改成具有混淆意義的方法了。因為加載這個dex前,總要修復,所以這個dex其實只是個可以任意修改,具有dex外表用於迷惑反編譯器,而實際上卻像是個分卷壓縮文件中的一部分文件(帶有一半的正確信息),還有另一半需要在 這個dex本身的一些可以第一次正常運行的方法里(無論java層還是native層,就算native,首次加載也是在java層代碼被首次運行后才會加載) 將完整的dex還原,或者在運行時,根據需要運行到的方法動態還原(類似 ELF .plt lazyLoad),在1級還原中將方法內的字節碼指向一個 自定義的動態連接器,攜帶可以標識方法簽名的參數,在需要執行時動態釋放出字節碼然后執行(2級還原),這樣如果在運行時沒有執行到的方法,始終不會有完整,正確的字節碼被釋放到內存里的dex中,即使用core dump,也無法獲得完整的,正確的dex文件
總結:
本質是將修改后的dex當作加密文件,讓反編譯者在反編譯時卻不知道(因為並不是完全的密文導致不可讀,有很大的可讀性,產生了混淆),實際在使用這個方法之前,需要先按加密方式的反方法修復被改動的地方,重新將dex加載,然后執行(可以通過反射方式)