您现在的位置是:首页 > 文章详情

Android DEX加壳

日期:2018-06-25点击:494

1. APP加固

1). 原理
img_e248534e396e5333cda2d952b78111da.png
图1.png

加密过程的三个对象:

  • 1、需要加密的Apk(源Apk)
  • 2、壳程序Apk(负责解密Apk工作)
  • 3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)
2). DEX头内容
img_d289a9c4851826a36fd038e2285937f5.png
图2.png

需要关注的字段:

  • checksum 文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
  • signature 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
  • fileSize Dex 文件的大小 。
  • 在文件的最后,我们需要标注被加密的apk的大小,因此需要增加4个字节。


    img_007a9077111ce0ae98cd793052de426c.png
    图3.png
3). 解密过程

宿主Apk启动 -> 宿主Application中解密Apk -> 替换ClassLoader -> 替换资源路径 -> 替换Application对象

2. 源程序Module(source)

1). SourceApplication
/** * 源Apk的全局Application * Created by mazaiting on 2018/6/26. */ public class SourceApplication extends Application { private static final String TAG = SourceApplication.class.getSimpleName(); @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: --------"); } } 
2). MainActivity
/** * 应用主入口 */ public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tvContent = new TextView(this); tvContent.setText("I am Source Apk"); tvContent.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View arg0) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); }}); setContentView(tvContent); Log.i(TAG, "onCreate:app:"+getApplicationContext()); } } 
3). 第二个页面
/** * 第二个页面 */ public class SecondActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv_content = new TextView(this); tv_content.setText("I am Second Activity"); setContentView(tv_content); } } 
4). AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mazaiting.reinforcement"> <application android:name=".SourceApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".SecondActivity"> </activity> </application> </manifest> 
5). 签名

Build -> Generate Signed APK,对应用进行签名(如果没有keystore, 可以创建一个新的),将签名后的apk文件放置在项目根目录下的force文件夹下,并更名为source.apk


img_3948bda6f8e63bcf5d404762da1bb87e.png
图3.png

img_b0e862812416a65d6c93d5157757ecb5.png
图4.png

3. 脱壳Module(reforceapk)

1). 替换步骤
 * 代理Application * 步骤: * ------- 在attachBaseContext ------- * 1. 从当前APK中拿到classes.dex文件,拿到classes.dex文件的二进制数据 * 2. 从dex的二进制数据中分离出解密后的apk,及so文件 * 3. 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用 * 4. 创建一个新的DexClassLoader,从指定路径加载apk资源 * 5. 加载被加密的apk主Activity入口 * ------- 在onCreate方法 ---------- * 6. 获取配置在清单文件的源apk的Application * 7. 替换原有的Application * 8. 调用被加密app的Application 
2). 代码
/** * 代理Application * 步骤: * ------- 在attachBaseContext ------- * 1. 从当前APK中拿到classes.dex文件,拿到classes.dex文件的二进制数据 * 2. 从dex的二进制数据中分离出解密后的apk,及so文件 * 3. 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用 * 4. 创建一个新的DexClassLoader,从指定路径加载apk资源 * 5. 加载被加密的apk主Activity入口 * ------- 在onCreate方法 ---------- * 6. 获取配置在清单文件的源apk的Application * 7. 替换原有的Application * 8. 调用被加密app的Application * Created by mazaiting on 2018/6/26. */ public class ProxyApplication extends Application { private static final String TAG = ProxyApplication.class.getSimpleName(); /** * APP_KEY获取Activity入口 */ private static final String APP_KEY = "APPLICATION_CLASS_NAME"; /**ActivityThread包名*/ private static final String CLASS_NAME_ACTIVITY_THREAD = "android.app.ActivityThread"; /**LoadedApk包名*/ private static final String CLASS_NAME_LOADED_APK = "android.app.LoadedApk"; /** * 源Apk路径 */ private String mSrcApkFilePath; /** * odex路径 */ private String mOdexPath; /** * lib路径 */ private String mLibPath; /** * 加载资源 */ protected AssetManager mAssetManager; protected Resources mResources; protected Resources.Theme mTheme; /** * 最先执行的方法 */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Log.d(TAG, "attachBaseContext: --------onCreate"); try { // 创建payload_odex和payload_lib文件夹,payload_odex中放置源apk即源dex,payload_lib放置so文件 File odex = this.getDir("payload_odex", MODE_PRIVATE); File libs = this.getDir("payload_lib", MODE_PRIVATE); // 用于存放源apk释放出来的dex mOdexPath = odex.getAbsolutePath(); // 用于存放源apk用到的so文件 mLibPath = libs.getAbsolutePath(); // 用于存放解密后的apk mSrcApkFilePath = mOdexPath + "/payload.apk"; File srcApkFile = new File(mSrcApkFilePath); Log.d(TAG, "attachBaseContext: apk size: " + srcApkFile.length()); // 第一次加载 if (!srcApkFile.exists()) { Log.d(TAG, "attachBaseContext: isFirstLoading"); srcApkFile.createNewFile(); // 拿到dex文件 byte[] dexData = this.readDexFileFromApk(); // 取出解密后的apk放置在/payload.apk,及其so文件放置在payload_lib下 this.splitPayLoadFromDex(dexData); } // 配置动态加载环境 // 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用 // 获取主线程对象 Object currentActivityThread = RefInvoke.invokeStaticMethod( CLASS_NAME_ACTIVITY_THREAD, "currentActivityThread", new Class[]{}, new Object[]{} ); // 获取当前报名 String packageName = this.getPackageName(); // 获取已加载的所有包 ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mPackages" ); // 获取LoadApk的弱引用 WeakReference wr = (WeakReference) mPackages.get(packageName); // 创建一个新的DexClassLoader用于加载源Apk // 传入apk路径,dex释放路径,so路径,及父节点的DexClassLoader使其遵循双亲委托模型 // 反射获取属性ClassLoader Object mClassLoader = RefInvoke.getFieldObject( CLASS_NAME_LOADED_APK, wr.get(), "mClassLoader" ); // 定义新的DexClassLoader对象,指定apk路径,odex路径,lib路径 DexClassLoader dLoader = new DexClassLoader( mSrcApkFilePath, mOdexPath, mLibPath, (ClassLoader) mClassLoader ); // getClassLoader()等同于 (ClassLoader) RefInvoke.getFieldOjbect() // 但是为了替换掉父节点我们需要通过反射来获取并修改其值 Log.d(TAG, "attachBaseContext: 父ClassLoader: " + mClassLoader); // 将父节点DexClassLoader替换 RefInvoke.setFieldObject( CLASS_NAME_LOADED_APK, "mClassLoader", wr.get(), dLoader ); Log.d(TAG, "attachBaseContext: 子ClassLoader: " + dLoader); try { // 尝试加载源apk的MainActivity Object actObj = dLoader.loadClass("com.mazaiting.reinforcement.MainActivity"); Log.d(TAG, "attachBaseContext: SrcApk_MainActivity: " + actObj); } catch (ClassNotFoundException e) { e.printStackTrace(); Log.d(TAG, "attachBaseContext: LoadSrcActivityErr: " + Log.getStackTraceString(e)); } } catch (IOException e) { e.printStackTrace(); Log.d(TAG, "attachBaseContext: error: " + Log.getStackTraceString(e)); } } /** * 从Dex中分割出资源 * * @param dexData dex资源 */ private void splitPayLoadFromDex(byte[] dexData) throws IOException { // 获取dex数据长度 int len = dexData.length; // 存储被加壳apk的长度 byte[] dexLen = new byte[4]; // 获取最后4个字节数据 System.arraycopy(dexData, len - 4, dexLen, 0, 4); ByteArrayInputStream bais = new ByteArrayInputStream(dexLen); DataInputStream in = new DataInputStream(bais); // 获取被加密apk的长度 int readInt = in.readInt(); // 打印被加密apk的长度 Log.d(TAG, "splitPayLoadFromDex: Integer.toHexString(readInt): " + Integer.toHexString(readInt)); // 取出apk byte[] enSrcApk = new byte[readInt]; // 将被加密apk内容复制到二进制数组中 System.arraycopy(dexData, len - 4 - readInt, enSrcApk, 0, readInt); // 对源apk解密 byte[] srcApk = decrypt(enSrcApk); // 写入源APK文件 File file = new File(mSrcApkFilePath); try { FileOutputStream fos = new FileOutputStream(file); fos.write(srcApk); fos.close(); } catch (IOException e) { throw new RuntimeException(e); } // 分析源apk文件 ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new FileInputStream(file) ) ); // 遍历压缩包 while (true) { ZipEntry entry = zis.getNextEntry(); // 判断是否有内容 if (null == entry) { zis.close(); break; } // 依次取出被加壳的apk用到的so文件,放到libPath中(data/data/包名/paytload_lib) String name = entry.getName(); if (name.startsWith("lib/") && name.endsWith(".so")) { // 存储文件 File storeFile = new File( mLibPath + "/" + name.substring(name.lastIndexOf('/')) ); storeFile.createNewFile(); FileOutputStream fos = new FileOutputStream(storeFile); byte[] bytes = new byte[1024]; while (true) { int length = zis.read(bytes); if (-1 == length) break; fos.write(bytes); } fos.flush(); fos.close(); } zis.closeEntry(); } zis.close(); } /** * 解密二进制 * * @param srcData 二进制数 * @return 解密后的二进制数据 */ private byte[] decrypt(byte[] srcData) { for (int i = 0; i < srcData.length; i++) { srcData[i] ^= 0xFF; } return srcData; } /** * 从ApK文件中获取DEX文件 * * @return dex字节数组 */ private byte[] readDexFileFromApk() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new FileInputStream(this.getApplicationInfo().sourceDir) ) ); // 遍历压缩包 while (true) { ZipEntry entry = zis.getNextEntry(); if (null == entry) { zis.close(); break; } // 获取dex文件 if ("classes.dex".equals(entry.getName())) { byte[] bytes = new byte[1024]; while (true) { int len = zis.read(bytes); if (len == -1) break; baos.write(bytes, 0, len); } } zis.closeEntry(); } zis.close(); return baos.toByteArray(); } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: ---------------"); // 获取配置在清单文件的源apk的Application路径 String appClassName = null; try { // 创建应用信息对象 ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA); // 获取metaData数据 Bundle bundle = ai.metaData; if (null != bundle && bundle.containsKey(APP_KEY)) { appClassName = bundle.getString(APP_KEY); } else { Log.d(TAG, "onCreate: have no application class name"); return; } } catch (PackageManager.NameNotFoundException e) { Log.d(TAG, "onCreate: error: " + Log.getStackTraceString(e)); e.printStackTrace(); } // 获取当前Activity线程 Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITY_THREAD, "currentActivityThread", new Class[]{}, new Object[]{}); // 获取绑定的应用 Object mBoundApplication = RefInvoke.getFieldObject(CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mBoundApplication"); // 获取加载apk的信息 Object loadedApkInfo = RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD + "$AppBindData", mBoundApplication, "info" ); // 将LoadedApk中的ApplicationInfo设置为null RefInvoke.setFieldObject(CLASS_NAME_LOADED_APK, "mApplication", loadedApkInfo, null); // 获取currentActivityThread中注册的Application Object oldApplication = RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mInitialApplication" ); // 获取ActivityThread中所有已注册的Application, 并将当前壳Apk的Application从中移除 ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mAllApplications" ); mAllApplications.remove(oldApplication); // 从loadedApk中获取应用信息 ApplicationInfo appInfoInLoadedApk = (ApplicationInfo) RefInvoke.getFieldObject( CLASS_NAME_LOADED_APK, loadedApkInfo, "mApplicationInfo" ); // 从AppBindData中获取应用信息 ApplicationInfo appInfoInAppBindData = (ApplicationInfo) RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD + "$AppBindData", mBoundApplication, "appInfo" ); // 替换原来的Application appInfoInLoadedApk.className = appClassName; appInfoInAppBindData.className = appClassName; // 注册Application Application app = (Application) RefInvoke.invokeMethod( CLASS_NAME_LOADED_APK, "makeApplication", loadedApkInfo, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null} ); // 替换ActivityThread中的Application RefInvoke.setFieldObject(CLASS_NAME_ACTIVITY_THREAD, "mInitialApplication", currentActivityThread, app); ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mProviderMap" ); // 遍历 for (Object providerClientRecord : mProviderMap.values()) { Object localProvider = RefInvoke.getFieldObject( CLASS_NAME_ACTIVITY_THREAD + "$ProviderClientRecord", providerClientRecord, "mLocalProvider" ); RefInvoke.setFieldObject("android.content.ContentProvider", "mContext", localProvider, app); } Log.d(TAG, "onCreate: SrcApp: " + app); // 调用新的Application app.onCreate(); } 
3). RefInvoke类
/** * 反射类 * Created by mazaiting on 2018/6/26. */ public class RefInvoke { /** * 反射执行类的静态函数(public) * * @param className 类名 * @param methodName 方法名 * @param pareTypes 函数的参数类型 * @param pareValues 调用函数时传入的参数 * @return */ public static Object invokeStaticMethod(String className, String methodName, Class[] pareTypes, Object[] pareValues) { try { Class objClass = Class.forName(className); Method method = objClass.getMethod(methodName, pareTypes); return method.invoke(null, pareValues); } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } return null; } /** * 反射执行的函数(public) * * @param className 类名 * @param methodName 方法名 * @param obj 对象 * @param pareTypes 参数类型 * @param pareValues 调用方法传入的参数 * @return */ public static Object invokeMethod(String className, String methodName, Object obj, Class[] pareTypes, Object[] pareValues) { try { Class objClass = Class.forName(className); Method method = objClass.getMethod(methodName, pareTypes); return method.invoke(obj, pareValues); } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } return null; } /** * 反射得到类的属性(包括私有和保护) * * @param className 类名 * @param obj 对象 * @param fieldName 属性名 * @return */ public static Object getFieldObject(String className, Object obj, String fieldName) { try { Class objClass = Class.forName(className); Field field = objClass.getDeclaredField(fieldName); field.setAccessible(true); return field.get(obj); } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { e.printStackTrace(); } return null; } /** * 反射得到类的静态属性(包括私有和保护) * * @param className 类名 * @param fieldName 属性名 * @return */ public static Object getStaticFieldObject(String className, String fieldName) { try { Class objClass = Class.forName(className); Field field = objClass.getDeclaredField(fieldName); field.setAccessible(true); return field.get(null); } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { e.printStackTrace(); } return null; } /** * 设置类的属性(包括私有和保护) * * @param className 类名 * @param fieldName 属性名 * @param obj 对象 * @param fieldValue 字段值 */ public static void setFieldObject(String className, String fieldName, Object obj, Object fieldValue) { try { Class objClass = Class.forName(className); Field field = objClass.getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, fieldValue); } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { e.printStackTrace(); } } /** * 设置类的静态属性(包括私有和保护) * * @param className 类名 * @param fieldName 属性名 * @param fieldValue 属性值 */ public static void setStaticObject(String className, String fieldName, String fieldValue) { try { Class objClass = Class.forName(className); Field field = objClass.getDeclaredField(fieldName); field.setAccessible(true); field.set(null, fieldValue); } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { e.printStackTrace(); } } } 
4). MainActivity
public class MainActivity extends AppCompatActivity { private static final String TAG=MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG,"-------------onCreate"); } } 
5). AndroidManifest.xml文件

在这个文件中,需要配置meta-data结点和将源apk中四大组件进行配置,否则无法运行源apk中的内容

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mazaiting.reforceapk"> <application android:name=".ProxyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.mazaiting.reinforcement.SourceApplication"/> <activity android:name="com.mazaiting.reinforcement.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name="com.mazaiting.reinforcement.SecondActivity"> </activity> </application> </manifest> 
6). 打包

与源apk相同,对此Module进行打包,打包完成后,更改文件后缀名apk为zip,使用压缩工具解压,将解压后文件夹中的classes.dex文件复制到项目根目录force文件夹下


img_51cde423761d8cd07e5bee3f5b3dca82.png
图5.png

img_d49b44bbbdf60c2724bc6076a55a70ff.png
图6.png

img_ff84f3cb89ebddbb0f2ab1d6d66e6bc7.png
图7.png

4. 加密工程

1). 新建一个Java Module
2). 加密步骤
 * 步骤: * 1. 获取待加密的APK, 并对其二进制数据加密 * 2. 取出壳DEX, 并获取其二进制数据 * 3. 计算拼接后的DEX应用的大小, 并创建二进制数组 * 4. 依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的DEX * 5. 修改DEX的头,fileSize字段 * 6. 修改DEX的头,SHA1字段 * 7. 修改DEX的头,CheckNum字段 * 8. 输出新的DEX文件 
3). DexShellTool
/** * 加密APK * 步骤: * 1. 获取待加密的APK, 并对其二进制数据加密 * 2. 取出壳DEX, 并获取其二进制数据 * 3. 计算拼接后的DEX应用的大小, 并创建二进制数组 * 4. 依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的DEX * 5. 修改DEX的头,fileSize字段 * 6. 修改DEX的头,SHA1字段 * 7. 修改DEX的头,CheckNum字段 * 8. 输出新的DEX文件 */ public class DexShellTool { public static void main(String[] args) { try { // 需要加壳的源APK, 以二进制形式读取,并进行加密处理 File srcApkFile = new File("force/source.apk"); System.out.println("apk path: " + srcApkFile.getAbsolutePath()); System.out.println("apk size: " + srcApkFile.length()); // 加密并返回元apk数据 byte[] enSrcApkArray = encrypt(readFileBytes(srcApkFile)); // 需要解壳的dex, 以二进制形式读出dex File unShellDexFile = new File("force/shell.dex"); byte[] unShellDexArray = readFileBytes(unShellDexFile); // 将源APK长度和需要解壳的DEX长度相加并加上存放源APK大小的四位得到总长度 int enSrcApkLen = enSrcApkArray.length; int unShellDexLen = unShellDexArray.length; // 多出的四位存放加密后的dex长度 int totalLen = enSrcApkLen + unShellDexLen + 4; // 依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的DEX byte[] newDex = new byte[totalLen]; // 复制加壳数据 System.arraycopy(unShellDexArray, 0, newDex, 0, unShellDexLen); // 复制加密apk数据 System.arraycopy(enSrcApkArray, 0, newDex, unShellDexLen, enSrcApkLen); // 赋值加壳后的dex大小 System.arraycopy(intToByte(enSrcApkLen), 0, newDex, totalLen - 4, 4); // 修改DEX file size 文件头 fixFileSizeHeader(newDex); // 修改DEX SHA1 文件头 fixSHA1Header(newDex); // 修改DEX CheckNum文件头 fixCheckSumHeader(newDex); // 写出新的DEX String str = "force/classes.dex"; File file = new File(str); if (!file.exists()) { file.createNewFile(); } FileOutputStream fos = new FileOutputStream(str); fos.write(newDex); fos.flush(); fos.close(); } catch (IOException | NoSuchAlgorithmException e) { e.printStackTrace(); } } /** * 修改DEX头,CheckSum校验码 * * @param dexBytes 要修改的二进制数据 */ private static void fixCheckSumHeader(byte[] dexBytes) { Adler32 adler = new Adler32(); // 从12到文件末尾计算校验码 adler.update(dexBytes, 12, dexBytes.length - 12); long value = adler.getValue(); int va = (int) value; byte[] newCs = intToByte(va); // 高低位互换位置 byte[] reCs = new byte[4]; for (int i = 0; i < 4; i++) { reCs[i] = newCs[newCs.length - 1 - i]; System.out.println("fixCheckSumHeader:" + Integer.toHexString(newCs[i])); } // 校验码赋值(8-11) System.arraycopy(reCs, 0, dexBytes, 8, 4); System.out.println("fixCheckSumHeader:" + Long.toHexString(value)); } /** * 修改DEX头, sha1值 * * @param dexBytes 要修改的二进制数组 */ private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-1"); // 从32位到结束计算sha-1 md.update(dexBytes, 32, dexBytes.length - 32); byte[] newDt = md.digest(); // 修改sha-1值(12-21) System.arraycopy(newDt, 0, dexBytes, 12, 20); // 输出sha-1值 StringBuilder hexStr = new StringBuilder(); for (byte aNewDt : newDt) { hexStr.append(Integer.toString((aNewDt & 0xFF) + 0x100, 16).substring(1)); } System.out.println("fixSHA1Header:" + hexStr.toString()); } /** * 修改DEX头, file_size值 * * @param dexBytes 二进制数据 */ private static void fixFileSizeHeader(byte[] dexBytes) { // 新文件长度 byte[] newFs = intToByte(dexBytes.length); System.out.println("fixFileSizeHeader: " + Integer.toHexString(dexBytes.length)); byte[] reFs = new byte[4]; // 高低位换位置 for (int i = 0; i < 4; i++) { reFs[i] = newFs[newFs.length - 1 - i]; System.out.println("fixFileSizeHeader: " + Integer.toHexString(newFs[i])); } // 修改32-35 System.arraycopy(reFs, 0, dexBytes, 32, 4); } /** * int 转 byte[] * * @param number 整型 * @return 返回字节数组 */ private static byte[] intToByte(int number) { byte[] b = new byte[4]; for (int i = 3; i >= 0; i--) { b[i] = (byte) (number % 256); number >>= 8; } return b; } /** * 加密二进制数据 * * @param srcData 字节数组 * @return 加密后的二进制数组 */ private static byte[] encrypt(byte[] srcData) { for (int i = 0; i < srcData.length; i++) { srcData[i] ^= 0xFF; } return srcData; } /** * 以二进制读出文件内容 * * @param file 文件 * @return 二进制数据 */ private static byte[] readFileBytes(File file) throws IOException { byte[] bytes = new byte[1024]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileInputStream fis = new FileInputStream(file); while (true) { int len = fis.read(bytes); if (-1 == len) break; baos.write(bytes, 0, len); } byte[] byteArray = baos.toByteArray(); fis.close(); baos.close(); return byteArray; } } 
4). 运行main函数

在项目的根目录的force文件夹下,生成一个classes.dex文件


img_d5c5f6d38e6c09baf457c14658f3ed34.png
图8.png

5. 合并

1). 项目结构
img_27933e46501c1591451311e1ac230c64.png
图9.png
2). 收集文件

将force/classes.dex与reforceapk Module生成的apk放在桌面


img_8e8b488c7b83db62f280c31e554ef0d8.png
图10.png
3). 替换

将reforceapk-release.apk直接使用压缩工具打开,将classes.dex复制并替换reforceapk-release.apk中原有的classes.dex文件.


img_4a25cf4fec7203a1d01436f6d2787b15.png
图11.png
4). 重签名
jarsigner -verbose -keystore E:\android\key\release-key.keystore -storepass mazaiting -keypass mazaiting -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar Reforce_des.apk reforceapk-release.apk key-alias del reforceapk-release.apk 

参数说明:

jarsigner -verbose -keystore 签名文件 -storepass 密码 -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 签名后的文件 签名前的apk alias名称 
5). 安装运行
adb install C:\Users\mazaiting\Desktop\Reforce_des.apk 

7. 参考文章及代码

8. 残留的问题

  • 源APP中Activity中的界面组件是由代码构建,xml文件中如何加载?
  • 宿主APP中ProxyApplication中使用到了源APP中的入口Application及MainActivity,如何动态获取?
  • 宿主APP中AndroidManifest.xml文件中需要配置源APP的四大组件,如何不进行不配置?

代码下载

原文链接:https://yq.aliyun.com/articles/663294
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章