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入口即可绕过我们的壳直接拉起游戏。
解决方案:
posted @ 2015-12-14 16:06  xadxxx  阅读(1521)  评论(0编辑  收藏  举报