一、前言以及環境配置
PS:該文已經首發於某公眾號,介意者勿噴!!!
安卓的加固方案是從19年底開始寫的,到現在為止差不多快一年了,寫這個目的還是學習怎么脫殼,前幾個月再看雪看到有人直接分析殼來學習,不過我感覺從加殼寫起也是一種浪漫。因為個人原因,在類指令抽取殼哪里為半完成狀態,在今年大概率沒有時間接着修改了,在java層的加固就止於此吧!!!(PS:以后有時間會接着修改)
環境配置:
* Android studio v3.5.3
* 華為G621-TL00 android v4.4.4
二、第一代殼:落地加載
1、原理
a、原理很簡單,就是首先將我們的dex文件或者apk文件解密,然后利用DexClassLoader加載器將其加載進內存中,然后利用反射加載待加固的apk的appkication,然后運行待加固程序即可,我畫了個流程圖詳細說明如下:

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

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

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

2、實際操作
ps:因為第一代殼網上一大堆,所以講得很粗略,同時這也不是本文的重點!!!
通過上面的代碼我們可以得到脫殼apk,然鵝待加固的apk放在哪里,網上大多放在脫殼dex的尾部,我又畫了一張圖,應該可以看圖就懂了:

這個我采用通過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重新簽名后,安裝運行,如下圖所示:


3、遇到的問題
運行時報錯如下所示:

解決方案:報錯顯示無法實例化activity,經過檢查是無法加載到正確格式的dex文件,檢查你的解密代碼,即使是你加密是象征型的異或了一個0xff,解密時也不能因為異或0xff值不變而不異或0xff。其次是打包成apk之前刪除簽名文件之后在簽名!!!
三、第二代殼:不落地加載
1、原理
大體原理和第一代殼相同,和第一代殼不同的是,第一代殼將dex文件解密出來會保存到文件中,在通過DexClassLoader加載進內存中,而不落地加載直接重寫DexClassLoader使其可以直接加載字節數組,避免寫入文件中。我們要做的是重寫DexClassLoader,而這涉及到三個函數defineClass、findClass、loadClass,在一個類被加載的時候,會先后調用這三個函數加載一個類,所以我們需要重寫這三個函數,但是我們怎么在重寫的過程中操控dex中的類(通過字節數組加載進來的並不能直接操控)?其實系統的DexClassLoader加載dex進入內存的也必然是通過字節加載的,而在系統so中的libdvm.so中的openDexFile可以直接加載dex文件,那么現在清楚了,我們可以通過編寫so文件調用openDexFile函數加載dex字節數組,值得注意的是,openDexFile函數返回值為一個int類型的cookie,可以簡單理解成一個dex文件的'身份碼',通過該'身份碼'即可操控這個dex文件,至於怎么調用該函數,可以通過dlopen和dlsym函數調用,相關代碼如下所示:


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

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

c、運行截圖如下:

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" },如下所示:

b、運行到加載dex文件中的方法時,app直接閃退
解決方案:重寫的loadClass方法有問題,不能通過直接super調用父類方法,而是應該通過反射調用defineClassNative方法,如下所示:

四、第三代殼:類指令抽取殼
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庫中,如下所示:

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

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


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

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


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

3、遇到的問題
報錯未定義函數,如下所示:

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

五、后記及其相關鏈接
我個人習慣了通過寫加固來學習脫殼,可能時間比直接分析殼來得慢,但是這其中體驗真的酸爽到爆炸,因為個人原因,最后的類指令抽取殼最后一點沒弄完,算是一個小遺憾吧,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方案實現原理(不落地方式加載)
