Android加壳
网上找来的一片文章,要FQ的
android应用加壳
1、需求背景
在某些特殊场景下,我们需要在不修改原应用apk的情况下对其做些额外的事情,比方集成广告、增加鉴权、增加防止反编译的逻辑等。那么我们需要在应用外嵌套一层壳,该壳集成我们想要实现的功能。
2、方案一
首先想到的是将原apk作为一个完整包直接放入到壳工程中,然后在sdk鉴权成功后直接拉起应用(启动非安装应用)。
问题1、如果根据游戏包名来启动游戏,需要先对该应用进行显示安装(如果想跟手机助手等静默安装是需要获得root权限的),并且该应用可以作为正常应用运行,违背加壳的目的。
3、方案二
通过反射方法利用dexClassLoader来动态加载apk,从而拉起应用的入口activity,首先替换壳工程的manifest为待加壳应用的,修改主activity为新功能,应用原入口activity仅作为普通activity申明,代码如下:
File dexOutputDir = getDir("dex", Context.MODE_PRIVATE);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
Log.i(TAG, "dexOutputPath = " + dexOutputPath);
Object currentActivityThread =
RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[] {},new Object[] {});
String packageName = this.getPackageName();
HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread",
currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
DexClassLoader dLoader =
new DexClassLoader(apkFilePath, dexOutputPath,
getApplicationInfo().nativeLibraryDir,
(ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk",
wr.get(),
"mClassLoader")); RefInvoke.setFieldOjbect("android.app.LoadedApk",
"mClassLoader",
wr.get(),
dLoader);
// 启动应用主activity
try
{
Log.i(TAG, "start SDKProxyActivity");
Class localClass =
dLoader.loadClass("com.huawei.game.sdkshell.SDKProxyActivity");
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString(GAME_NAME_KEY, gameClasseName);
intent.putExtras(bundle);
intent.setClass(GameShellProxyActivity.this, localClass);
startActivity(intent);
}
catch (Exception e)
{
Log.i(TAG, "start SDKProxyActivity failed e = " + e.toString());
}
问题1、该种方式启动应用后无法加载到应用本身的资源,因为这些资源并未在当前壳应用的dexClassLoader中。
解决方案:将应用的apk利用apktool工具反编译,将反编译的除了smali目录剩下的所有目录copy至壳相应的目录下。
问题2、因为该方法需要用到应用原apk,所以将应用原apk放置assets目录下,在壳启动时将该apk拷贝至sd卡中,那么应用如果很大的话,这个copy读写操作太耗时并且可能overmemory,同时由于资源已经copy至壳应用中,导致加壳后的应用是原来游戏大小的近两倍。
解决方案:将反编译的应用apk目录除了smali文件其他的资源全部删除,然后再通过apktool重新打包生成没有资源的新应用apk,这样的apk一般就几百kb。
问题3、单独将应用apk放置assets目录下,这样对于应用完全性有隐患,因此需要将该apk隐藏并加密。
解决方案:将应用apk(后面更改为classes.dex)和壳的classes.dex合并在一起后通过修改dex文件的头信息达到将应用简化后的apk(classes.dex)隐藏在壳应用自身的classes.dex,这边生成的新dex文件必须修改一些文件信息使其可正常加载。
加壳source dex数据就是应用apk,下方增加其数据长度是用来在解壳时根据该长度来从dex中将应用apk取出来,在加入的过程中可以用一些加密算法进行加密。
具体实现部分代码如下:
/**
* 应用加壳方法<BR>
* @param apkPath apk路径
* @param unShellDexPath
* @param classDexPath
* @throws NoSuchAlgorithmException
* @throws IOException
*/
public static void dexShell(String apkPath, String unShellDexPath,
String classDexPath)
{
try
{
File apkFile = new File(apkPath);
File unShellFile = new File(unShellDexPath);
byte[] apkArray = readFileBytes(apkFile);
LogUtil.getInstance().i(TAG + "dexShell apkArray len = " + apkArray.length);
byte[] apkAesArray = encrpt(apkArray);
byte[] unShellArray = readFileBytes(unShellFile);
int apkFileLen = apkAesArray.length;
int unShellFileLen = unShellArray.length;
LogUtil.getInstance().i(TAG + "apkAesFileLen = " + apkFileLen + ", unShellFileLen = " + unShellFileLen);
int totalLen = apkFileLen + unShellFileLen + 4;
byte[] newDex = new byte[totalLen];
//添加解壳代码
System.arraycopy(unShellArray, 0, newDex, 0, unShellFileLen);
//添加加密后的解壳数据
System.arraycopy(apkAesArray, 0, newDex, unShellFileLen, apkFileLen);
//添加解壳数据长度
System.arraycopy(intToByte(apkFileLen), 0, newDex, totalLen - 4, 4);
//修改DEX file size文件头
fixFileSizeHeader(newDex);
//修改DEX SHA1 文件头
fixSHA1Header(newDex);
//修改DEX CheckSum文件头
fixCheckSumHeader(newDex);
File classDexFile = new File(classDexPath);
if (classDexFile.exists())
{
LogUtil.getInstance().i(TAG + "classDexFile exists");
classDexFile.delete();
}
classDexFile.createNewFile();
FileOutputStream localFileOutputStream =
new FileOutputStream(classDexPath);
localFileOutputStream.write(newDex);
localFileOutputStream.flush();
localFileOutputStream.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
问题4、如何将包含应用和壳的新dex文件替换壳的dex文件并能重新打包。
壳代码中包括解析dex拿到应用apk的方法。
解决方案:将已经copy了应用资源的壳应用编译,在bin目录下保留manifest和resources.ap_,然后将加壳的apk解压后的classes.dex文件替换成新的dex文件,然后通过命令apkbuilder D:\aaaa.apk -u -z C:\Users\g00216986\Desktop\coveredDemo\resources.ap_ -f C:\Users\g00216986\Desktop\coveredDemo\classes.dex -nf C:\Users\g00216986\Desktop\coveredDemo\lib
重新打包并签名即可生成最终加壳的apk
问题5、通过上面的方法的确可以实现应用加壳,但是步骤太繁琐,该方案作为备选方案暂放。
4、方案三
上面的方案都是将应用反编译通过各种手段用于生成新的加壳应用,我们是否可以不动用应用apk本身的情况下,将我们壳的相关东西直接copy至游戏,然后生成新的apk。该方案就是将应用apk反编译,然后将纯壳apk反编译生成的smali文件直接copy至应用目录,然后copy尽量少的资源。包括封装功能的apk、使用到的lib等。
问题1、壳不能包括太多的资源,否则在copy的时候可能会引起冲突。
解决方案:将鉴权逻辑等封装在底层中,然后底层通过一个activity(无view)调用底层的功能接口。
问题2、通过该方式动态加载底层的lib工程,底层封装新增功能的apk时会报错,无法在当前壳运行的classLoader中找到so。
解决方案:将这三部分作为资源copy至游戏相应目录下,在拷贝lib工程时注意应用本身lib是否有相应目录的so,如果没有就全部拷贝,如果已有so目录则只拷贝相对应目录下的so即可(比方armeabi-v7a目录)。否则应用运行时加载so可能会找到其他目录。
问题3、发现某些应用可能采用某些特殊平台,比方j2me的kjava,其中部分资源和class类并非像android应用放在res或者assets目录下,而apktool反编译工具只是针对android通用目录,从而导致这些资源和类丢失。
解决方案:判断应用除了通用的目录外是否还有其他文件,如果有将这些文件直接copy至解压后的加壳apk,然后利用jarsigner命令重新签名。
问题4、该方案因为未对游戏原代码和目录结构做任何修改,只是修改manifest的入口为壳的activity,同时增加部分activity申明和权限等,那么如果有人想破解这个加壳方案的话,只需要将manifest的activity入口修改回原来游戏的主activity入口即可绕过我们的壳直接拉起游戏。
解决方案:?