Android 加殼嘗試(一)

最近看了一篇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的代碼:



原理簡介

加殼的原理大致如下圖所示:



殼apk(即reinforceTest工程)的主要作用用是提供殼classes.dex,用於在啓動app時解析出源classes.dex,並引導程序執行classes.dex的代碼
引導程序執行真正classes.dex文件的步驟如下:


引導的Application如下:
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);
    }
}



reinforceTest工程具體的加殼步驟如下所示:
task buildReinforceApk(dependsOn: 'assembleDebug') << {
    // 清理目錄
    cleanDir();
    // 解壓apk
    decodeApk();
    // 修改Manifest文件
    modifyManifest();
    // 加殼
    reinforce();
    // 重新打包apk並簽名
    rebuildAndSign();
}

這裏的Gradle Task依賴了assembleDebug Task,用於獲取最新的殼apk

清理目錄就不多說了,解壓apk使用的是apktool工具,把源apk與殼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;
	}
}

具體的加殼原理可以參考文章頂部的鏈接,其中有提及,在此不做贅述
加殼後的dex如下所示:


加殼完後把新的dex覆蓋舊的源apk的dex,由於文件進行了改動,因此apk在重新打包後需要重新簽名:
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'
    }
}

存在問題

目前這種加殼方式在測試過程中發現仍存在一些問題:
  1. 不支持AppCompatActivity:當源apk使用AppCompatActivity時,會出現資源找不到的錯誤,具體原因未知,以後再做研究
  2. ContentProvider不清楚是否支持:根據執行順序,在APK中最先執行的幾個方法應該爲:Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate。這裏我們選擇在Application.attachBaseContext以及Application.onCreate進行解殼以及引導程序執行的操作,不清楚是否會對ContentProvider造成影響,打算以後再做測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章