Android一代整体壳简易实现和踩坑记录
Android一代整体壳简易实现和踩坑记录
- 参考资料
- 整体思路
- 源程序
- 壳程序
- 加壳代码
- 尾声
参考资料
[1] dex-shell
[2] Activity 的启动流程(Android 13)
[3] 基于 Android 13 的 Activity 启动流程分析
[4] FART:ART环境下基于主动调用的自动化脱壳方案
[5] Android漏洞之战(11)——整体加壳原理和脱壳技巧详解
[6] Android脱壳之整体脱壳原理与实践
[7] Android DEX加壳
[8] dex壳简单分析与实现过程
[9] Android第一代加壳的验证和测试
整体思路
1、在壳程序dex末尾追加源程序所有dex
2、在壳程序Application的attachBaseContext方法释放源程序所有dex并替换mClassLoader
3、在壳程序Application的onCreate方法注册源程序Application并开始生命周期
源程序
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".SrcApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Shell1src"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
SrcApplication
package com.p1umh0.shell1src;
import android.app.Application;
public class SrcApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
}
MainActivity,注意这里一定是继承Activity而不是AppCompatActivity
package com.p1umh0.shell1src;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main);
}
}
壳程序
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".ShellApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Shell1shell"
tools:targetApi="31" >
<meta-data
android:name="SRCAPPLICATIONNAME"
android:value="com.p1umh0.shell1src.SrcApplication"/>
<activity
android:name="com.p1umh0.shell1src.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ShellApplication
package com.p1umh0.shell1shell;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dalvik.system.DexClassLoader;
public class ShellApplication extends Application {
private final String LOGTAG = "p1umh0";
private final String CLASS_NAME_ACTIVITYTHREAD = "android.app.ActivityThread";
private final String CLASS_NAME_LOADEDAPK = "android.app.LoadedApk";
// 私有路径
private String privateodexpath; //dex文件路径
private String privatelibspath; //lib文件路径
@Override
protected void attachBaseContext(Context base){
super.attachBaseContext(base);
try {
// 创建私有目录
File odex = this.getDir("myodex",MODE_PRIVATE);
File libs = this.getDir("mylibs",MODE_PRIVATE);
privateodexpath = odex.getAbsolutePath();
Log.e(LOGTAG,"privateodexpath => " + privateodexpath);
privatelibspath = libs.getAbsolutePath();
Log.e(LOGTAG,"privatelibspath => " + privatelibspath);
// odex目录为空,说明是安装后第一次启动
// 如果odex需要更新,必须卸载掉原程序,以删除/清空私有目录
if(odex.list().length==0){
// 提取壳Apk的classes.dex文件数据
byte[] dexdata = this.extractdexdata();
// 从dex数据末尾提取源Apk的所有.dex文件数据并另存到privateodexpath目录
this.parsedexdata(dexdata,privateodexpath);
}
// 获取当前ActivityThread实例
Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITYTHREAD, "currentActivityThread", new Class[]{}, new Object[]{});
// 获取已加载的所有包
ArrayMap mPackages = (ArrayMap) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mPackages");
// 获取当前包名
String packageName = this.getPackageName();
// 获取LoadedApk的弱引用
WeakReference weakReference = (WeakReference) mPackages.get(packageName);
// 获取LoadedApk属性mClassLoader
Object mClassLoader = RefInvoke.getField(CLASS_NAME_LOADEDAPK, weakReference.get(), "mClassLoader");
Log.e(LOGTAG,"mClassLoader => " + mClassLoader);
Log.e(LOGTAG,"mClassLoader.getParent => " + ((ClassLoader) mClassLoader).getParent());
// 拼接源Apk的所有.dex文件的绝对路径(privateodexpath+.dex文件名)
StringBuffer srcdexfilepath = new StringBuffer();
for(File srcdexfile : odex.listFiles()){
srcdexfilepath.append(srcdexfile.getAbsolutePath());
srcdexfilepath.append(File.separator);
}
srcdexfilepath.delete(srcdexfilepath.length()-1,srcdexfilepath.length());
Log.e(LOGTAG,"srcdexfilepath => " + srcdexfilepath.toString());
// 创建新的DexClassLoader
DexClassLoader myDexClassLoader = new DexClassLoader(srcdexfilepath.toString(),privateodexpath,privatelibspath,(ClassLoader) mClassLoader);
Log.e(LOGTAG,"myDexClassLoader => " + myDexClassLoader);
Log.e(LOGTAG,"myDexClassLoader.getParent => " + myDexClassLoader.getParent());
// 替换LoadedApk属性mClassLoader为新的DexClassLoader
// 相当于将新的DexClassLoader加入双亲委派加载链
RefInvoke.setField(CLASS_NAME_LOADEDAPK,"mClassLoader",weakReference.get(),myDexClassLoader);
// 尝试加载
Object srcMainActivity = myDexClassLoader.loadClass("com.p1umh0.shell1src.MainActivity");
Log.e(LOGTAG,"srcMainActivity => " + srcMainActivity);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public void onCreate() {
super.onCreate();
try {
// 从AndroidManifest的meta-data读取源apk的Application名称
String srcApplicationName = null;
ApplicationInfo applicationInfo = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = applicationInfo.metaData;
if (bundle != null && bundle.containsKey("SRCAPPLICATIONNAME")) {
srcApplicationName = bundle.getString("SRCAPPLICATIONNAME");
} else {
return;
}
// 获取当前ActivityThread实例
Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITYTHREAD, "currentActivityThread", new Class[]{}, new Object[]{});
// 获取当前绑定的应用
Object mBoundApplication = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mBoundApplication");
Log.e(LOGTAG,"mBoundApplication => " + mBoundApplication);
// 获取当前LoadedApk
Object loadedApk = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$AppBindData", mBoundApplication, "info");
Log.e(LOGTAG,"loadedApk => " + loadedApk);
// 将当前LoadedApk中的mApplication设置为null
RefInvoke.setField(CLASS_NAME_LOADEDAPK, "mApplication", loadedApk, null);
// 获取当前ActivityThread实例中注册的mInitialApplication
// 也就是壳apk的Application
Object shellApplication = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mInitialApplication");
Log.e(LOGTAG,"shellApplication => " + shellApplication);
// 获取当前ActivityThread实例中所有注册的Application,并将壳apk的Application从中移除
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mAllApplications");
mAllApplications.remove(shellApplication);
// 从LoadedApk中获取应用信息
ApplicationInfo appInfoInLoadedApk = (ApplicationInfo) RefInvoke.getField(CLASS_NAME_LOADEDAPK, loadedApk, "mApplicationInfo");
// 从AppBindData中获取应用信息
ApplicationInfo appInfoInAppBindData = (ApplicationInfo) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$AppBindData", mBoundApplication, "appInfo");
// 替换原来的Application
appInfoInLoadedApk.className = srcApplicationName;
appInfoInAppBindData.className = srcApplicationName;
// 注册源apk的Application
Application srcApplication = (Application) RefInvoke.invokeMethod(CLASS_NAME_LOADEDAPK, "makeApplication", loadedApk, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null});
Log.e(LOGTAG,"srcApplication => " + srcApplication);
// 替换当前ActivityThread实例中的mInitialApplication
// 也就是从shellApplication转为srcApplication
RefInvoke.setField(CLASS_NAME_ACTIVITYTHREAD, "mInitialApplication", currentActivityThread, srcApplication);
// 获取mProviderMap
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mProviderMap");
// 遍历
for (Object providerClientRecord : mProviderMap.values()) {
Object mLocalProvider = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$ProviderClientRecord", providerClientRecord, "mLocalProvider");
// 更新上下文mContext
RefInvoke.setField("android.content.ContentProvider", "mContext", mLocalProvider, srcApplication);
}
// 启动新的Application
srcApplication.onCreate();
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
private byte[] extractdexdata(){
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(this.getApplicationInfo().sourceDir)));
while(true){
ZipEntry zipEntry = zipInputStream.getNextEntry();
if(zipEntry==null){
zipInputStream.close();
break;
}
if(zipEntry.getName().equals("classes.dex")){
byte[] buffer = new byte[1024];
int byteRead;
while ( (byteRead = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, byteRead);
}
}
zipInputStream.closeEntry();
}
zipInputStream.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void parsedexdata(byte[] dexdata, String privateodexpath){
try {
// 壳Apk的classes.dex文件数据的总长度
int dexdatalen = dexdata.length;
Log.e(LOGTAG,"dexdatalen => " + dexdatalen);
// 读末尾4字节,获取源Apk的所有.dex文件数据的总长度
ByteArrayInputStream byteArrayInputStream;
DataInputStream dataInputStream;
byte[] srcdexdatalenbytearr = new byte[4];
System.arraycopy(dexdata,dexdatalen-4,srcdexdatalenbytearr,0,4);
byteArrayInputStream = new ByteArrayInputStream(srcdexdatalenbytearr);
dataInputStream = new DataInputStream(byteArrayInputStream);
int srcdexdatalen = dataInputStream.readInt();
Log.e(LOGTAG,"srcdexdatalen => " + srcdexdatalen);
// 拷贝出源Apk的所有.dex文件数据
byte[] srcdexdata = new byte[srcdexdatalen];
System.arraycopy(dexdata,dexdatalen-4-srcdexdatalen,srcdexdata,0,srcdexdatalen);
byteArrayInputStream = new ByteArrayInputStream(srcdexdata);
dataInputStream = new DataInputStream(byteArrayInputStream);
// 读开头2字节,获取源Apk包含的.dex文件的数目
short srcdexfilenum = dataInputStream.readShort();
Log.e(LOGTAG,"srcdexfilenum => " + srcdexfilenum);
// 借助srcdexfilenum完成循环,避免异常
short srcdexfilenamelen;
int srcdexfiledatalen;
while(srcdexfilenum!=0){
// 源Apk的.dex文件名称:2字节长度 + 不定长数据
srcdexfilenamelen = dataInputStream.readShort();
Log.e(LOGTAG,"srcdexfilenamelen => " + srcdexfilenamelen);
byte[] srcdexfilename = new byte[srcdexfilenamelen];
dataInputStream.read(srcdexfilename);
// 源Apk的.dex文件数据:4字节长度 + 不定长数据
srcdexfiledatalen = dataInputStream.readInt();
Log.e(LOGTAG,"srcdexfiledatalen => " + srcdexfiledatalen);
byte[] srcdexfiledata = new byte[srcdexfiledatalen];
dataInputStream.read(srcdexfiledata);
// 将源Apk的.dex文件另存到privateodexpath目录
File srcdexfile = new File(privateodexpath + File.separator + new String(srcdexfilename));
Log.e(LOGTAG,"srcdexfile => " + srcdexfile.getAbsolutePath());
if(!srcdexfile.exists()){
srcdexfile.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(srcdexfile);
fileOutputStream.write(srcdexfiledata);
fileOutputStream.close();
// 数量递减
srcdexfilenum -= 1;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
MainActivity
package com.p1umh0.shell1shell;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main);
}
}
加壳代码
dex追加参考[1],但略有不同
资源处理参考[8],直接用源程序的资源覆盖
笔者这里是从壳程序和源程序编译好的Apk包里提取出关键文件,处理后再打包成新的Apk,MT管理器签名安装
import hashlib
import os
import pathlib
import struct
import zlib
from zipfile import ZipFile
# 路径
thisDir = os.path.dirname(__file__)
srcApkPath = os.path.join(thisDir, "shell1src.apk")
shellApkPath = os.path.join(thisDir, "shell1shell.apk")
newShellApkPath = os.path.join(thisDir, "shell1newshell.apk")
# 文件
srcApk = ZipFile(srcApkPath, "r")
shellApk = ZipFile(shellApkPath, "r")
newShellApk = ZipFile(newShellApkPath, "w")
# 从源Apk中提取res文件夹、resources.arsc文件和所有.dex文件
srcApkUnzipTempDir = os.path.join(thisDir, "srcApkUnzipTempDir")
for srcFullName in srcApk.namelist():
if srcFullName.startswith("res") or srcFullName.endswith(".dex"):
srcApk.extract(srcFullName, srcApkUnzipTempDir)
# 从壳Apk中提取AndroidManifest.xml文件和classes.dex文件
shellApkUnzipTempDir = os.path.join(thisDir, "shellApkUnzipTempDir")
for shellFullName in shellApk.namelist():
if shellFullName == "AndroidManifest.xml" or shellFullName == "classes.dex":
shellApk.extract(shellFullName, shellApkUnzipTempDir)
# 为新壳Apk插入条目:源Apk的res文件夹、源Apk的resources.arsc文件
srcApkUnzipTempDirIns = pathlib.Path(srcApkUnzipTempDir)
for srcResFilePath in srcApkUnzipTempDirIns.rglob(r"*"):
if os.path.isfile(srcResFilePath) and not srcResFilePath.name.endswith(".dex"):
newShellApk.write(
srcResFilePath, srcResFilePath.relative_to(srcApkUnzipTempDirIns))
# 为新壳Apk插入条目:壳Apk的AndroidManifest.xml文件
newShellApk.write(os.path.join(shellApkUnzipTempDir,
"AndroidManifest.xml"), "AndroidManifest.xml")
# 拼接壳Apk的classes.dex文件以及源Apk的所有.dex文件
# 拼接结构:
# 壳dex数据
# 源Apk的.dex文件数量(2字节)
# 源dex1名称长度(2字节) + 源dex1名称(不定大小) + 源dex1数据长度(4字节) + 源dex1数据(不定大小)
# 源dexN名称长度(2字节) + 源dexN名称(不定大小) + 源dexN数据长度(4字节) + 源dexN数据(不定大小)
# 除壳dex数据外的数据长度(4字节)
# 新壳Apk的classes.dex文件数据
newShellDexData = b""
# 拼接壳Apk的classes.dex文件数据
with open(os.path.join(shellApkUnzipTempDir, "classes.dex"), "rb") as f:
newShellDexData += f.read()
# 壳dex数据长度
shellDexDataLen = len(newShellDexData)
# 源Apk的.dex文件数量(2字节占坑)
newShellDexData += b"??"
srcDexFileNum = 0
# 拼接源Apk的所有.dex文件数据
for srcDexFilePath in srcApkUnzipTempDirIns.rglob(r"*"):
if os.path.isfile(srcDexFilePath) and srcDexFilePath.name.endswith(".dex"):
srcDexFileNum += 1
srcDexFileRelaPath = srcDexFilePath.relative_to(
srcApkUnzipTempDirIns).name.encode()
# 大端序short,为了适应java中DataInputStream.readShort()
newShellDexData += struct.pack(">H", len(srcDexFileRelaPath))
newShellDexData += srcDexFileRelaPath
with open(srcDexFilePath, "rb") as f:
srcDexFileData = f.read()
# 大端序int,为了适应java中DataInputStream.readInt()
newShellDexData += struct.pack(">I", len(srcDexFileData))
newShellDexData += srcDexFileData
# 除壳dex数据外的数据长度,大端序int,为了适应java中DataInputStream.readInt()
newShellDexData += struct.pack(">I", len(newShellDexData)-shellDexDataLen)
# bytes转为list,用来item assignment
newShellDexData = list(newShellDexData)
# 设置源Apk的.dex文件数量,大端序short,为了适应java中DataInputStream.readShort()
newShellDexData[shellDexDataLen:shellDexDataLen +
2] = list(struct.pack(">H", srcDexFileNum))
# 修新壳Apk的classes.dex文件的file_size,小端序int,为了匹配dex文件结构
newFileSize = list(struct.pack("<I", len(newShellDexData)))
newShellDexData[32:32+len(newFileSize)] = newFileSize
# 修新壳Apk的classes.dex文件的signature,没有设置端序,直接添加
newSignature = hashlib.sha1(bytes(newShellDexData[32:])).hexdigest()
newSignature = list(bytes.fromhex(newSignature))
newShellDexData[12:12+len(newSignature)] = newSignature
# 修新壳Apk的classes.dex文件的checksum,小端序int,为了匹配dex文件结构
newChecksum = zlib.adler32(bytes(newShellDexData[12:]))
newChecksum = list(struct.pack("<I", newChecksum))
newShellDexData[8:8+len(newChecksum)] = newChecksum
# 另存到新壳Apk的classes.dex文件
with open(os.path.join(thisDir, "classes.dex"), "wb") as f:
f.write(bytes(newShellDexData))
# 为新壳Apk插入条目:新壳Apk的classes.dex文件
newShellApk.write(os.path.join(thisDir, "classes.dex"), "classes.dex")
srcApk.close()
shellApk.close()
newShellApk.close()
尾声
关于资源处理,参考资料[8]直接替换资源文件的方法可行,但要求源程序的MainActivity继承Activity
参考资料[9]的loadResources方法通过AssetManager.addAssetPath添加资源路径,按理说可行
但他是在壳程序dex末尾追加了源程序整个Apk,占用更多空间
笔者这里并没有复现出参考资料[9]中的资源处理方法
加壳时保留了壳程序的资源文件(否则程序启动崩溃)
在壳程序dex末尾追加源程序整个Apk(同参考资料[9])
源程序的MainActivity也是继承Activity而非AppCompatActivity(这样一比这种方法没有任何优势)
加壳后的程序可以顺利启动,也正常走到了源程序的MainActivity,但在onCreate中加载的布局却是壳程序的资源
看起来像是AssetManager.addAssetPath没起作用,笔者不太理解,还求大佬讲解~