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取出并動态加載運作到加載布局檔案的代碼時就會出錯。
網上有些文章提供的解決辦法是直接将源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所使用的腳本是拿網上的。
這樣子并不能修複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()