Android第一代殼demo編寫

S1mba發表於2021-02-01

Android第一代殼Demo編寫

前言

這篇文章是對姜維大佬的這篇文章[Android中的Apk的加固(加殼)原理解析和實現]的補充。建議先看一編姜維大佬的這篇文章再看。

姜維大佬寫那篇文章的時間距今已久,如果要按照他文章中的思路編寫你需要Android4.4以下的環境也就是Dalvik虛擬機器的環境。而且我在用Dv虛擬機器進行試驗時也執行不了。不明原因,現在猜想可能是當時抄的指令碼問題。

這裡講一下我在Android5.0及以上環境下實現第一代殼demo過程中遇到的問題以及應對方法。

後面將待加殼APK稱為源APK,殼程式稱為殼APK,加殼後的APK稱為加殼APK。

0x1 DEX的載入問題

首先遇到的是DEX的載入問題。在某個和Android版本升級之後,DexClassLoader的程式碼發生了改變。已經不可以直接載入Dex檔案了。需要將Dex轉化為jar或者apk進行載入,否則載入過程中取出來的Dex檔案載入進去也沒用,檔案還會被刪除掉。在attachBasecontext中嘗試載入MainActivity的時候會報錯。

  • 解決方法

將程式碼中的這句

DexClassLoader dcl = new DexClassLoader(apkName,dexPath,libPath,oldCl);

dcl的型別改為DexClassLoader的父類ClassLoader。

並使用dex2jar將源APK的DEX檔案轉為jar包,再用dx工具轉為Android可以載入的jar包。具體命令為

dex2jar 源APK.apk -o target.jar -> 得到普通jar包
dx --dex --output=target.jar target,jar  ->得到Android可載入的jar

拼接時就用得到的jar包代替源APK的Dex拼接。

0x2 資源載入問題

載入完Dex後遇到的就是資源載入的問題。因為姜維大佬的方法是將源APK的DEX拼接到殼APK的DEX後,在執行時在取出來進行載入。那麼這個APK執行時的佈局等資源是沒有被打包過去的,只有程式碼。當殼APK將DEX取出並動態載入執行到載入佈局檔案的程式碼時就會出錯。

img

網上有些文章提供的解決辦法是直接將源APK的資原始檔複製到殼APK中。但是不同的專案佈局檔案對應的id是不一樣的,需要重新將資源的id編輯過於麻煩。

  • 解決方法

將拼接後的DEX檔案放回源APK中就不用再自己動手去載入資源了。

0x3 AppCompatActivity問題

解決上面兩個問題後加殼APK還是不能執行,報錯中有對Android版本的描述如:

Before Android4.1…………balalbala

且報錯中有Appcompat的字樣

查資料發現這個AppCompatActivity是更換art虛擬機器之後才有的。與普通的Activity不同。AppCompat的佈局檔案會在上方有個標題欄which is Activity所沒有的。

  • 解決方法

目前只想到將源APK的Activity繼承的父類全部改為Activity。畢竟這只是一個小demo,不是重點。以後找到方法再進行改進。

0x4 Dex拼接後Dex頭的修復問題

若不修復Dex頭,Dex檔案是不會被正確識別的。之前拼接Dex所使用的指令碼是拿網上的。

image-20210201213549982

這樣子並不能修復SHA1欄位和CheckSum欄位,還會覆蓋掉檔案大小欄位。因為這樣子寫實際寫進去的是得到SHA1對應的ASCII碼的十六進位制數。得不到正確的Dex檔案。

  • 解決方法

這個是我修改過的指令碼

import binascii
import os
import hashlib
import zlib
import zipfile
import shutil
from xml.dom import minidom

unshellpath = r"D:\CodeAS\shell1\app\build\outputs\apk\debug\app-debug.apk"
srcapkpath = r"D:\CodeAS\srcapk\app\build\outputs\apk\debug\app-debug.apk"


def fixCheckSum(shell):
    shell.seek(0x0C)
    data = shell.read()
    checksum = zlib.adler32(data)
    strchecksum = str(hex(checksum))
    strchecksum = strchecksum.replace("0x", "")
    if not len(strchecksum) % 2 == 0:
        strchecksum = strchecksum.zfill(len(strchecksum)+1)
    shell.seek(0x08)
    shell.write(bytearray.fromhex(strchecksum)[::-1])
    
def fixSHA1(shell):
    shell.seek(0x20)
    signBytes = shell.read()
    sha1 = hashlib.sha1()
    sha1.update(signBytes)
    sign = sha1.hexdigest()
    b = bytes.fromhex(sign)
    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 fixXML():
    doc = minidom.parse(srcapkpath.replace('.apk', r'\AndroidManifest.xml'))
    root = doc.documentElement
    app_node = root.getElementsByTagName('application')[0]
    app_name = app_node.getAttribute('android:name')
    app_node.setAttribute('android:name', 'app.simba.shell1.ShellApplication')
    meta = doc.createElement('meta-data')
    meta.setAttribute('android:name', 'ApplicationName')
    meta.setAttribute('android:value', app_name)
    app_node.appendChild(meta)

    doc.writexml(open(srcapkpath.replace(
        '.apk', r'\AndroidManifest.xml'), 'w'), encoding='utf-8')


def main():
    
    if os.path.exists(unshellpath.replace('.apk','')):
        shutil.rmtree(unshellpath.replace('.apk',''))

    apk = zipfile.ZipFile(unshellpath)
    for n in apk.namelist():
        apk.extract(n,unshellpath.replace('.apk',''))

    for root, dirs, files in os.walk(unshellpath.replace('.apk', '')):
        for f in files:
            if os.path.join(root, f).endswith('.dex'):
                unshelldexpath = os.path.join(root, f)
    print('[*] 成功反編譯shellAPK')

    os.system('apktool.bat -f -s d ' + srcapkpath +
              ' -o ' + srcapkpath.replace('.apk', ''))
    srcjarpath = srcapkpath.replace('.apk','.jar')
    os.system('dex2jar.bat -f '+srcapkpath+' -o '+srcjarpath)
    os.system('java -jar dx.jar --dex --output='+srcjarpath+' '+srcjarpath)
    for root, dirs, files in os.walk(srcapkpath.replace('.apk', '')):
        for f in files:
            if os.path.join(root, f).endswith('.dex'):
                os.remove(os.path.join(root, f))
    print('[*] 成功反編譯待加殼APK')

    unshelldex = open(unshelldexpath, 'rb+')
    tmpshell = unshelldex.read()
    unshellArray = bytearray(tmpshell)
    unshelldex.close()
    print('[*] 成功讀取脫殼DEX檔案')

    tmp = open(srcjarpath,'rb+')
    srcjarArray = bytearray(tmp.read())
    tmp.close()

    tmpdex = unshellArray +srcjarArray+IntToHex(len(srcjarArray))

    f = open(srcapkpath.replace('.apk','/classes.dex'),'wb+')
    f.write(tmpdex)
    f.close()

    shell = open(srcapkpath.replace('.apk','/classes.dex'),'rb+')
    fixFileSize(shell, len(tmpdex))
    fixSHA1(shell)
    fixCheckSum(shell)
    shell.close()

    print('[*] DEX檔案修復完畢')

    fixXML()
    print('[*] AndroidManifest檔案修改完畢')

    outputpath = os.path.dirname(
        srcapkpath)+r'\shell_'+os.path.basename(srcapkpath)
    os.system('apktool.bat  b '+srcapkpath.replace('.apk', '')+' -o ' +
              outputpath)
    print('[*] APK重打包完畢')

    os.system('jarsigner -keystore '+r'D:\CodeAS\mykey.jks'+' -signedjar ' +
              outputpath.replace('.apk', '_signed.apk')+' '+outputpath+' mykey '+'-storepass 123456')


if __name__ == "__main__":
    main()

相關文章