安卓加固方案從落地加載到類指令抽取編寫報告

一、前言以及環境配置

    PS:該文已經首發於某公衆號,介意者勿噴!!!

    安卓的加固方案是從19年底開始寫的,到現在爲止差不多快一年了,寫這個目的還是學習怎麼脫殼,前幾個月再看雪看到有人直接分析殼來學習,不過我感覺從加殼寫起也是一種浪漫。因爲個人原因,在類指令抽取殼哪裏爲半完成狀態,在今年大概率沒有時間接着修改了,在java層的加固就止於此吧!!!(PS:以後有時間會接着修改)

  環境配置:

    
    * Android studio v3.5.3
    * 華爲G621-TL00 android v4.4.4


二、第一代殼:落地加載

1、原理

    a、原理很簡單,就是首先將我們的dex文件或者apk文件解密,然後利用DexClassLoader加載器將其加載進內存中,然後利用反射加載待加固的apk的appkication,然後運行待加固程序即可,我畫了個流程圖詳細說明如下:

        1.png

    b、上面說了大概原理,現在來說明一下具體細節,我們知道,在一個app開始運行的時候,第一個加載的類是ActivityThread,該類有個關鍵屬性currentActivityThread,通過該屬性能夠獲取到一系列其他關鍵的屬性,例如mPackages,通過該屬性,我們可以獲取到mClassLoader屬性,通過替換該屬性我們可以替換系統加載器,如下所示:

2.png

    接着來說怎麼獲取待加固apk的application,這個通過在脫殼apk的AndroidManifest.xml中使用meta-data來獲取,如下所示:

3.png

    在然後就是怎麼替換application,我們可以知道在android.app.LoadedApk類中有一個方法makeApplication可以生成一個application,通過該方法生成一個application,然後通過替換android.content.ContentProvider類中的mContext屬性完成application的替換,如下圖所示:

4.png

2、實際操作

    ps:因爲第一代殼網上一大堆,所以講得很粗略,同時這也不是本文的重點!!!

    通過上面的代碼我們可以得到脫殼apk,然鵝待加固的apk放在哪裏,網上大多放在脫殼dex的尾部,我又畫了一張圖,應該可以看圖就懂了:

5.png

    這個我採用通過python讀取二進制然後重新計算chunksum和簽名字段實現,代碼入戲:

import binascii
import hashlib
import zlib

def fixCheckSum(shell):
    shell.seek(0x0c)
    data = shell.read()
    checksum = zlib.adler32(data)
    strchecksum = str(hex(checksum))
    strchecksum = strchecksum.replace('0x','')
    b = bytes(strchecksum.encode('utf-8'))
    a = bytearray(b)
    c = binascii.hexlify(binascii.unhexlify(bytes(a))[::-1])
    dataCheckSum = bytearray(c)
    shell.seek(0x08)
    shell.write(dataCheckSum)

def fixSHA1(shell):
    shell.seek(0x20)
    signBytes = shell.read()
    sha1 = hashlib.sha1()
    sha1.update(signBytes)
    sign = sha1.hexdigest()
    tmp = bytes(sign.encode('utf-8'))
    b = bytearray(tmp)
    shell.seek(0x0c)
    shell.write(b)

def fixFileSize(shell,num):
    b = bytearray()
    for i in range(4):
        number = int(num % 256)
        b.append(number)
        num = num >> 8
    shell.seek(0x20)
    shell.write(b)

def IntToHex(num):
    b = bytearray()
    for i in range(4):
        number = int(num % 256)
        b.append(number)
        num = num >> 8
    b.reverse()
    return b

def main():
    sourceApk = open('sourceApk.apk','rb+',True)
    unshell = open('unshell.dex','rb+',True)
    filename = 'classes.dex'
    
    tmpApk = sourceApk.read()
    print('[*] 成功讀取待加殼的APK文件')
    sourceArray = bytearray(tmpApk)
    tmpDex = unshell.read()
    print('[*] 成功讀取脫殼DEX文件')
    unshellArray = bytearray(tmpDex)
    print('[-] 待加殼APK文件開始加密,加密類型爲:未加密')
    
    sourceApkLen = len(sourceArray)
    unshellLen = len(unshellArray)
    print('[+] 加密後的APK大小爲' + str(sourceApkLen) + 'Byte')
    totalLen = sourceApkLen + unshellLen + 4
    
    tmpByteArray = unshellArray + sourceArray
    newdex = tmpByteArray + IntToHex(sourceApkLen)
    print('[+] 所有二進制數據合成完畢')
    
    shellTmp = open(filename,'wb+',True)
    shellTmp.write(newdex)
    shellTmp.close()
    print('[+] 數據寫入' + filename + '完畢')
    
    shell = open(filename,'rb+',True)
    fixFileSize(shell,len(newdex))
    print('[+] 文件大小修改完畢')
    fixSHA1(shell)
    print('[+] 文件SHA-1簽名頭部修改完畢')
    fixCheckSum(shell)
    print('[+] 文件校驗頭頭部修改完畢')
    print('[+] 待加殼APK文件sourceApk.apk加殼完畢,加殼後DEX文件' + filename + '生成完畢')
    shell.close()
    
if __name__ == '__main__':
    main()

    將上述apk重新簽名後,安裝運行,如下圖所示:

6.png

7.png

3、遇到的問題

    運行時報錯如下所示:

8.png

    解決方案:報錯顯示無法實例化activity,經過檢查是無法加載到正確格式的dex文件,檢查你的解密代碼,即使是你加密是象徵型的異或了一個0xff,解密時也不能因爲異或0xff值不變而不異或0xff。其次是打包成apk之前刪除簽名文件之後在簽名!!!


三、第二代殼:不落地加載

1、原理

    大體原理和第一代殼相同,和第一代殼不同的是,第一代殼將dex文件解密出來會保存到文件中,在通過DexClassLoader加載進內存中,而不落地加載直接重寫DexClassLoader使其可以直接加載字節數組,避免寫入文件中。我們要做的是重寫DexClassLoader,而這涉及到三個函數defineClassfindClassloadClass,在一個類被加載的時候,會先後調用這三個函數加載一個類,所以我們需要重寫這三個函數,但是我們怎麼在重寫的過程中操控dex中的類(通過字節數組加載進來的並不能直接操控)?其實系統的DexClassLoader加載dex進入內存的也必然是通過字節加載的,而在系統so中的libdvm.so中的openDexFile可以直接加載dex文件,那麼現在清楚了,我們可以通過編寫so文件調用openDexFile函數加載dex字節數組,值得注意的是,openDexFile函數返回值爲一個int類型的cookie,可以簡單理解成一個dex文件的'身份碼',通過該'身份碼'即可操控這個dex文件,至於怎麼調用該函數,可以通過dlopendlsym函數調用,相關代碼如下所示:

9.png

10.png

2、實際操作

    a、首先編寫樣本,這裏我寫了一個類和一個方法,作用就是打印一個特徵字符串,如下所示:

11.png

    b、將上面的樣本打包成apk後提取出dex文件然後放置到assest文件夾下(該文件夾需要自己建立)供程序調用(ps:我這裏圖方便未對dex文件加密然後解密,有需要的可以加上),然後脫殼apk和上面的第一代殼沒什麼區別,唯一不同的是就是我們使用的是我們自己重寫的DexClassLoader,如下圖所示:

12.png

    c、運行截圖如下:

13.png

3、遇到的問題

  a、報錯java.lang.UnsatisfiedLinkError: Native method not found

    解決方案:在配置文件中添加packagingOptions{ pickFirst "lib/armeabi-v7a/libtwoshell.so" pickFirst "lib/arm64-v8a/libtwoshell.so" pickFirst "lib/x86/libtwoshell.so" pickFirst "lib/x86_64/libtwoshell.so" },如下所示:

14.png

  b、運行到加載dex文件中的方法時,app直接閃退

    解決方案:重寫的loadClass方法有問題,不能通過直接super調用父類方法,而是應該通過反射調用defineClassNative方法,如下所示:

15.png


四、第三代殼:類指令抽取殼

1、原理

    a、什麼是類指令抽取殼,從名字就能看出來,就是把dex文件中的方法指令抽空,變成nop,然後在運行時再將指令還原!!!

    b、指令抽取可以通過010修改,現在來說指令還原,其餘代碼和第二代基本一樣,不一樣的地方在加載完dex之後執行指令還原函數,指令還原現在有兩種方法,第一種是通過讀取maps文件獲取加載的dex文件地址,然後對dex文件進行解析,找到被nop的指令處進行還原(ps:該種方法需要及其熟悉dex文件格式,不瞭解的可以看我之前的文章關於解析dex文件,因爲我之前解析的時候用的是python,改成c要大量時間,所以我選擇了第二種方法);第二種方法就是通過免root hook系統函數(最簡單的就是deFindClass函數)然後進行指令還原!!!

    c、接下來就將一下怎麼通過hook dexFindClass函數來進行指令還原(PS:看懂下面的內容需要理解dex文件格式)。dexFindClass函數在libdvm.so庫中,如下所示:

16.png

    免root hook框架有點多,我選擇的是android inline hook,原因很簡單,很適合在so層使用,其他的經過我測試不知道爲啥我寫出來的沒反應,該框架github地址:https://github.com/ele7enxxh/Android-Inline-Hook,用法可以參考作者github,該inline hook框架需要原函數地址、新函數地址和原始函數的二級指針,用法如下所示(怎麼使用不是重點,接下來的纔是重點,所以這裏比較粗略):

17.png

    我們要hook的是dexFindClass函數,該函數定義在DexFile.h文件中,該函數返回值爲一個類結構指針,第二個參數爲類名字,通過該參數我們就可以指定類進行指令還原,如下所示:

18.png

19.png

    上面我們得到的classDataOff,我們可以通過該地址獲取到類數據,該偏移地址指向的是一個DexClassData結構,該結構的header存儲了相關類信息,該結構的directMethods指針指向的方法的結構題,如下所示:

20.png

    通過directMethods指針我們可以順着找到DexMethod結構體,通過該結構體的methodIdx調用系統函數dexGetMethodIddexStringById可以獲取到方法名字,精確還原方法指令,通過該結構的codOff(這是個偏移地址)可獲取方法指令,該偏移地址指向DexCode結構,該結構即存儲了方法指令,利用memcpy替換即可達到指令還原的效果,如下所示:

21.png

22.png

2、實踐操作

    java層基本和第二代殼一樣,只是多了一個調用hook的函數,so層關鍵代碼如下所示:(ps:不知道爲啥Android inline hook穩定性很差,上一個測試app還得行,下一個就瘋狂報錯了,所以代碼是基本完成了,但是android inline hook報錯未解決,有時間我會修改)

23.png

3、遇到的問題

    報錯未定義函數,如下所示:

24.png

    解決方案:在CmakeLists.txt文件中將jni文件夾下面所有引用到的文件都包含進去,如下所示:

25.png


五、後記及其相關鏈接

    我個人習慣了通過寫加固來學習脫殼,可能時間比直接分析殼來得慢,但是這其中體驗真的酸爽到爆炸,因爲個人原因,最後的類指令抽取殼最後一點沒弄完,算是一個小遺憾吧,20年應該沒時間來彌補這個遺憾了,希望21年我有時間來把這個遺憾補上吧!!!

    源碼github鏈接:https://github.com/windy-purple/androidshell

  參考鏈接:
    Android免Root權限通過Hook系統函數修改程序運行時內存指令邏輯
    Android逆向之旅—運行時修改內存中的Dalvik指令來改變代碼邏輯
    Android中免root的hook框架Legend原理解析
    https://github.com/asLody/legend
    https://github.com/ele7enxxh/Android-Inline-Hook
    Android APK加固-完善內存dex
    利用動態加載技術加固APK原理解析
    Android插件化框架之動態加載Activity(一)
    Android APK 加固之動態加載dex(一)
    Android中實現「類方法指令抽取方式」加固方案原理解析
    Android中apk加固完善篇之內存加載dex方案實現原理(不落地方式加載)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章