最近看了一篇Android加殼相關的文章:http://blog.csdn.net/jiangwei0910410003/article/details/48415225
嘗試根據文章的步驟來實現Android加殼的功能,在發現文章實現的效果不大理想後,本人進行了一定的調整與改進
實現效果
實現效果如下:
reinforceTest是我們的加殼Android工程,我們把需要加殼的apk放置在其workspace目錄下
接着在reinforceTest工程下運行gradle的task:buildReinforceApk
在workspace目錄下,我們可以找到加殼後的output.apk
加殼後的apk可正常運行:
通過dex2jar以及jd-gui,我們可以看到dex中的殼代碼,但是看不到源apk的代碼:
原理簡介
public class ReinforceApplication extends Application {
private static final String TAG = ReinforceApplication.class.getSimpleName();
private static final String ACTIVITY_THREAD = "android.app.ActivityThread";
private static final String LOADED_APK = "android.app.LoadedApk";
private static final String APPLICATION_CLASS_NAME = "APPLICATION_CLASS_NAME";
private String mDexFileName;
private String mOdexPath;
private String mLibPath;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
initDexEnvironment();
decryptDex();
replaceDexLoader();
}
@Override
public void onCreate() {
// TODO provider onCreate?
String appClassName = null;
// 獲取Application名字
try {
ApplicationInfo ai = this.getPackageManager()
.getApplicationInfo(this.getPackageName(),
PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle != null && bundle.containsKey(APPLICATION_CLASS_NAME)) {
appClassName = bundle.getString(APPLICATION_CLASS_NAME);
} else {
return;
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, Log.getStackTraceString(e));
}
Log.i(TAG, appClassName);
Object sCurrentActivityThread = RefInvoke.invokeStaticMethod(
ACTIVITY_THREAD, "currentActivityThread",
new Class[]{}, new Object[]{});
Object mBoundApplication = RefInvoke.getFieldObject(
ACTIVITY_THREAD, "mBoundApplication", sCurrentActivityThread);
Object info = RefInvoke.getFieldObject(
ACTIVITY_THREAD + "$AppBindData", "info", mBoundApplication);
// 把當前進程的mApplication 設置成null
RefInvoke.setFieldObject(LOADED_APK, "mApplication", info, null);
// 刪除oldApplication
Object oldApplication = RefInvoke.getFieldObject(
ACTIVITY_THREAD, "mInitialApplication", sCurrentActivityThread);
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldObject(ACTIVITY_THREAD, "mAllApplications", sCurrentActivityThread);
mAllApplications.remove(oldApplication);
ApplicationInfo appInfoInLoadedApk = (ApplicationInfo) RefInvoke
.getFieldObject(LOADED_APK, "mApplicationInfo", info);
ApplicationInfo appInfoInAppBindData = (ApplicationInfo) RefInvoke
.getFieldObject(ACTIVITY_THREAD + "$AppBindData", "appInfo", mBoundApplication);
appInfoInLoadedApk.className = appClassName;
appInfoInAppBindData.className = appClassName;
// 執行 makeApplication(false,null),此功能需要把當前進程的mApplication 設置成null
Application app = (Application) RefInvoke.invokeMethod(
LOADED_APK, "makeApplication", info,
new Class[]{boolean.class, Instrumentation.class},
new Object[]{false, null});
RefInvoke.setFieldObject(ACTIVITY_THREAD, "mInitialApplication", sCurrentActivityThread,
app);
ArrayMap mProviderMap = (ArrayMap) RefInvoke
.getFieldObject(ACTIVITY_THREAD, "mProviderMap", sCurrentActivityThread);
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke
.getFieldObject(ACTIVITY_THREAD + "$ProviderClientRecord", "mLocalProvider",
providerClientRecord);
RefInvoke.setFieldObject("android.content.ContentProvider", "mContext", localProvider,
app);
}
Log.i(TAG, "app:" + app);
app.onCreate();
}
private void initDexEnvironment() {
mDexFileName = getApplicationInfo().dataDir + "/real.dex";
mOdexPath = getApplicationInfo().dataDir + "/odex";
File odexDir = new File(mOdexPath);
if (!odexDir.exists()) {
odexDir.mkdir();
}
mLibPath = getApplicationInfo().nativeLibraryDir;
}
private void decryptDex() {
byte[] dex = readDexFromApk();
if (dex != null) {
byte[] realDexBytes = decryption(dex);
if (realDexBytes != null) {
try {
File realDex = new File(mDexFileName);
if (realDex.exists()) {
realDex.delete();
}
realDex.createNewFile();
FileOutputStream fos = new FileOutputStream(realDex);
fos.write(realDexBytes);
fos.flush();
fos.close();
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
}
}
}
}
private byte[] readDexFromApk() {
File sourceApk = new File(getPackageCodePath());
try {
ZipInputStream zis = new ZipInputStream(new FileInputStream(sourceApk));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals("classes.dex")) {
byte[] bytes = new byte[1024];
int len;
while ((len = zis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
baos.flush();
}
return baos.toByteArray();
}
}
zis.close();
return null;
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
return null;
}
}
private byte[] decryption(byte[] dex) {
int totalLen = dex.length;
byte[] realDexLenBytes = new byte[4];
System.arraycopy(dex, totalLen - 4, realDexLenBytes, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(realDexLenBytes);
DataInputStream ins = new DataInputStream(bais);
int realDexLen;
try {
realDexLen = ins.readInt();
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
return null;
}
byte[] realDexBytes = new byte[realDexLen];
System.arraycopy(dex, totalLen - 4 - realDexLen, realDexBytes, 0, realDexLen);
return decrypt(realDexBytes);
}
private byte[] decrypt(byte[] bytes) {
// TODO
byte[] result = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
result[i] = (byte) (bytes[i] ^ 0x4598);
}
return result;
}
private void replaceDexLoader() {
Object sCurrentActivityThread = RefInvoke
.invokeStaticMethod(ACTIVITY_THREAD, "currentActivityThread", null, null);
String packageName = getPackageName();
ArrayMap mPackages = (ArrayMap) RefInvoke
.getFieldObject(ACTIVITY_THREAD, "mPackages", sCurrentActivityThread);
WeakReference weakReference = (WeakReference) mPackages.get(packageName);
Object loadedApk = weakReference.get();
ClassLoader mClassLoader = (ClassLoader) RefInvoke
.getFieldObject(LOADED_APK, "mClassLoader", loadedApk);
DexClassLoader dexClassLoader = new DexClassLoader(mDexFileName, mOdexPath, mLibPath,
mClassLoader);
RefInvoke.setFieldObject(LOADED_APK, "mClassLoader", loadedApk, dexClassLoader);
}
}
task buildReinforceApk(dependsOn: 'assembleDebug') << {
// 清理目錄
cleanDir();
// 解壓apk
decodeApk();
// 修改Manifest文件
modifyManifest();
// 加殼
reinforce();
// 重新打包apk並簽名
rebuildAndSign();
}
這裏的Gradle Task依賴了assembleDebug Task,用於獲取最新的殼apk
private void decodeApk() {
// 複製解殼apk
copy {
from 'build/outputs/apk/app-debug.apk'
into WORKSPACE
rename {
REIN_FORCE_APK
}
}
// 解壓解殼apk
exec {
workingDir WORKSPACE
commandLine 'java', '-jar', TOOLS_DIR + APK_TOOL, 'd', '-s', REIN_FORCE_APK, '-o', REIN_FORCE_DIR
}
// 解壓源apk
exec {
workingDir WORKSPACE
commandLine 'java', '-jar', TOOLS_DIR + APK_TOOL, 'd', '-s', SRC_APK, '-o', SRC_DIR
}
}
接着是修改源apk的AndroidManifest.xml文件,爲啥要修改呢?首先因爲源apk上有我們需要的資源文件,所以我們肯定是把加殼後的dex放入源apk中,而不是放入殼apk中。由上面的引導步驟圖我們得知,我們解殼時需要先執行殼apk的Application。如果加殼後的dex放入源apk中,我們的解殼Application由於沒有在源apk的AndroidManifest中註冊,因此無法率先執行。所以,我們需要修改源apk的AndroidManifest,把Application的name改爲殼apk的Application,同時添加一項meta-data,用於記錄源apk的Application的名字,讓我們在待會替換回真正的Application時知道它的名字:
private void modifyManifest() {
// 聲明命名空間
def android = new Namespace('http://schemas.android.com/apk/res/android', 'android')
// 獲取源apk application name
def parser = new XmlParser()
def srcManifest = parser.parse("${WORKSPACE}${SRC_DIR}/AndroidManifest.xml")
def srcApp = srcManifest.application[0].attribute(android.name)
// 獲取殼apk application name
def reinforceManifest = new XmlParser().parse("${WORKSPACE}${REIN_FORCE_DIR}/AndroidManifest.xml")
def reinforceApp = reinforceManifest.application[0].attribute(android.name)
// 合成新Manifest
// 新建meta-data節點,記錄源apk application Name
parser.createNode(
srcManifest.application[0],
new QName('http://schemas.android.com/apk/res/android', 'meta-data'),
[
(android.name):'APPLICATION_CLASS_NAME',
(android.value):srcApp
]
)
// 修改application節點,改爲殼apk application Name
srcManifest.application[0].attributes().put(android.name, reinforceApp)
println srcManifest.application[0].attribute(android.name)
// 寫入文件
Writer writer = new FileWriter("${WORKSPACE}${SRC_DIR}/AndroidManifest.xml")
writer.write(XmlUtil.serialize(srcManifest))
writer.flush()
}
接着就是加殼的步驟了,這裏主要調用了用Java寫的加殼工具:
private void reinforce() {
// 加殼
OutputStream os = new ByteArrayOutputStream();
exec {
workingDir TOOLS_DIR
// 參數爲 源dex 殼dex 輸出dex
commandLine 'java', '-cp', '.', 'DexTools', "${WORKSPACE}${SRC_DIR}/classes.dex", "${WORKSPACE}${REIN_FORCE_DIR}/classes.dex", WORKSPACE + OUTPUT_DEX
standardOutput = os;
}
println os.toString()
// 輸出dex替換源dex
file("${WORKSPACE}${SRC_DIR}/classes.dex").delete();
copy {
from WORKSPACE + OUTPUT_DEX
into WORKSPACE + SRC_DIR
}
}
工具Java代碼如下:
public class DexTools {
public static void main(String[] args) {
if (args.length != 3) {
System.out.println("plz enter srcDex , reinforceDex and outputDex");
System.exit(0);
}
try {
// 源dex
File srcDex = new File(args[0]);
// 殼dex
File reinForceDex = new File(args[1]);
// 對源dex進行加密
byte[] encryptSrcApkBytes = encrypt(readFileBytes(srcDex));
byte[] reinForceDexBytes = readFileBytes(reinForceDex);
// 新dex長度,4字節用於存放源apk長度
int totalLen = encryptSrcApkBytes.length + reinForceDexBytes.length
+ 4;
byte[] newDex = new byte[totalLen];
// 先拷貝殼dex
System.arraycopy(reinForceDexBytes, 0, newDex, 0,
reinForceDexBytes.length);
// 再拷貝源dex
System.arraycopy(encryptSrcApkBytes, 0, newDex,
reinForceDexBytes.length, encryptSrcApkBytes.length);
// 寫上源dex長度
System.arraycopy(int2byte(encryptSrcApkBytes.length), 0, newDex,
totalLen - 4, 4);
// 修改dex文件長度
fixHeaderFileSize(newDex);
// 修改dex簽名
fixHeaderSignature(newDex);
// 修改dex校驗和
fixHeaderCheckSum(newDex);
File outputDex = new File(args[2]);
if (!outputDex.exists()) {
outputDex.createNewFile();
}
FileOutputStream fos = new FileOutputStream(outputDex);
fos.write(newDex);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static byte[] readFileBytes(File file) {
if (file.canRead()) {
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
private static byte[] encrypt(byte[] bytes) {
// TODO
byte[] result = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
result[i] = (byte) (bytes[i] ^ 0x4598);
}
return result;
}
private static byte[] int2byte(int number) {
byte[] bytes = new byte[4];
for (int i = 3; i >= 0; i--) {
bytes[i] = (byte) (number % 256);
number >>= 8;
}
return bytes;
}
private static void fixHeaderFileSize(byte[] dex) {
byte[] newSize = int2byte(dex.length);
// 修改(32-35)file_size
System.arraycopy(changeBytesOrder(newSize), 0, dex, 32, 4);
}
private static void fixHeaderSignature(byte[] dex)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 計算從32位到文件尾的sha-1值
md.update(dex, 32, dex.length - 32);
byte[] newSignature = md.digest();
// 修改(12-31)signature
System.arraycopy(newSignature, 0, dex, 12, 20);
}
private static void fixHeaderCheckSum(byte[] dex) {
Adler32 adler32 = new Adler32();
// 計算從12位到文件尾的校驗和
adler32.update(dex, 12, dex.length - 12);
long checkSum = adler32.getValue();
byte[] checkSumBytes = int2byte((int) checkSum);
// 修改(8-11)checkSum
System.arraycopy(changeBytesOrder(checkSumBytes), 0, dex, 8, 4);
}
private static byte[] changeBytesOrder(byte[] bytes) {
int length = bytes.length;
byte[] result = new byte[length];
for (int i = 0; i < length; i++) {
result[i] = bytes[length - 1 - i];
}
return result;
}
}
具體的加殼原理可以參考文章頂部的鏈接,其中有提及,在此不做贅述
private void rebuildAndSign() {
// 打包apk
exec {
workingDir WORKSPACE
commandLine 'java', '-jar', TOOLS_DIR + APK_TOOL, 'b', SRC_DIR
}
// 複製打包完的apk
copy {
from "${WORKSPACE}${SRC_DIR}/dist/${SRC_APK}"
into WORKSPACE
rename {
OUTPUT_UNSIGNED_APK
}
}
exec {
workingDir WORKSPACE
commandLine 'jarsigner', '-sigalg', 'MD5withRSA',
'-digestalg', 'SHA1',
'-keystore', rootDir.getAbsolutePath() + '/reinforceTestKey.jks',
'-storepass', '123456',
'-signedjar', OUTPUT_APK, OUTPUT_UNSIGNED_APK, 'Dummy'
}
}
存在問題
- 不支持AppCompatActivity:當源apk使用AppCompatActivity時,會出現資源找不到的錯誤,具體原因未知,以後再做研究
- ContentProvider不清楚是否支持:根據執行順序,在APK中最先執行的幾個方法應該爲:Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate。這裏我們選擇在Application.attachBaseContext以及Application.onCreate進行解殼以及引導程序執行的操作,不清楚是否會對ContentProvider造成影響,打算以後再做測試