APK加壳【1】初步方案实现详解

来源与原理

本文是尝试对CSDN大牛 Jack_Jia 的博客Android APK加壳技术方案【2】进行实现的过程记录,该文介绍了一种对源程序APK加壳的思路并提供了对应的源码。

所谓加壳,就是通过给目标APK加一层保护程序,把需要保护的内容加密、隐藏起来,来防止反编译的一种方法。说到底我们要做的是这样一个事情,首先把要加壳的APK用自己的加密算法加个密(实验过程中这步可以省掉),然后藏在另一个APK中(就是壳工程)发布出去,这样防止破解者直接拿到源程序的APK去反编译。不好处理的是还需要壳工程在各种版本的Android系统里运行时,要把源程序解密出来还要跟直接装源程序有同样的运行效果才行。如何实现原文都已经写清楚了:

通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码;

找到源程序的Application,通过反射建立并运行;

方案

整个方案里面涉及到三种角色:

源程序——等待被加壳的目标程序,一个APK;【原文中的加密工具代码、DexShellTool中的g:/payload.apk】

加密工具——这是一个工具程序,用什么语言实现都是可以的。用来给源程序加密,这段功能对应的解密则在壳程序中实现;【原文中的DexShellTool代码,是一个java程序】

壳程序——它实际上也是一个Android工程,经过加壳发布出去的APK就是壳程序经过特殊处理之后生成的。它内部保存着已被加密的源程序(apk、dex或者odex),在启动后第一时间将加密后的源程序解出来,通过类加载器动态加载运行;【原文中Menifest、ProxyApplication.java、RefInvoke.java部分】

所以检查这个方案需要有个DEMO APK、有个加密工具JAVA工程DexShellTool 和一个Android壳工程UnShell,最后加壳后的APK实际上是壳工程编译出的、并且把其中的dex文件替换为经过加密工具处理生成的新dex、最后重新打包签名的APK。

源程序

源程序其实没什么好讲的,最好是有个带有服务、广播、网络操作什么的基础功能比较全面的示例程序,这样测试可行性更加有说服力一些。

加密工具

加密工具其实原文中给出的很容易看懂,因为没有涉及到加密算法,所以不到两百行。基本做了这样一件事:把源程序加密之后接到壳工程的dex文件尾,然后修改dex文件的文件长度、校验和什么的。这种隐藏方式略诡异。

壳工程

壳工程既是壳又要有解壳功能,原文只给了两个类,实际上也只需要这两个类。ProxyApplication里有解壳与反射实现动态加载源程序的代码逻辑、RefInvoke则是反射工具。许多童鞋表示反射不好理解,一开始我也是这么觉得。不过经过一行行注释下来、对比系统源码,其实也没有多难。这里要说,静下心来分析,不到三百行的代码,能有多复杂呢?

protectedvoidattachBaseContext(Context base) {super.attachBaseContext(base);        Log.d(TAG,"attachBaseContext hello world~");try{            File odex =this.getDir("payload_odex", MODE_PRIVATE);            File libs =this.getDir("payload_lib", MODE_PRIVATE);            odexPath = odex.getAbsolutePath();            libPath = libs.getAbsolutePath();            apkFileName = odex.getAbsolutePath() +"/payload.apk";            File dexFile =newFile(apkFileName);if(!dexFile.exists())                dexFile.createNewFile();// 读取程序classes.dex文件byte[] dexdata =this.readDexFileFromApk();// 分离出解壳后的apk文件已用于动态加载this.splitPayLoadFromDex(dexdata);// 配置动态加载环境Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",newClass[] {},newObject[] {});            String packageName =this.getPackageName();            HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mPackages");            WeakReference wr = (WeakReference) mPackages.get(packageName);//换Loader操作 动态加载如被加密又装换回来的apk文件DexClassLoader dLoader =newDexClassLoader(apkFileName, odexPath,                    libPath, (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(),"mClassLoader"));            RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",                    wr.get(), dLoader);        }catch(Exception e) {            e.printStackTrace();        }    }publicvoidonCreate() {        Log.d(TAG,"on create hello world~");// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。String appClassName =null;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(NameNotFoundException e) {            e.printStackTrace();        }        Log.d(TAG,"the app aplication name is "+ appClassName);/**

* 调用静态方法android.app.ActivityThread.currentActivityThread

* 获取当前activity所在的线程对象

*/Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",newClass[] {},newObject[] {});/**

* 获取currentActivityThread中的mBoundApplication属性对象,该对象是一个

*  AppBindData类对象,该类是ActivityThread的一个内部类

*/Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mBoundApplication");/**

* 获取mBoundApplication中的info属性,info 是 LoadedApk类对象

*/Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication,"info");/**

* loadedApkInfo对象的mApplication属性置为null

*/RefInvoke.setFieldOjbect("android.app.LoadedApk","mApplication",                loadedApkInfo,null);/**

* 获取currentActivityThread对象中的mInitialApplication属性

* 这货是个正牌的 Application

*/Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mInitialApplication");/**

* 获取currentActivityThread对象中的mAllApplications属性

* 这货是 装Application的列表

*/ArrayList mAllApplications = (ArrayList) RefInvoke                .getFieldOjbect("android.app.ActivityThread",                        currentActivityThread,"mAllApplications");//列表对象终于可以直接调用了 remove调了之前获取的application 抹去记录的样子mAllApplications.remove(oldApplication);/**

* 获取前面得到LoadedApk对象中的mApplicationInfo属性,是个ApplicationInfo对象

*/ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke                .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,"mApplicationInfo");/**

* 获取前面得到AppBindData对象中的appInfo属性,也是个ApplicationInfo对象

*/ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke                .getFieldOjbect("android.app.ActivityThread$AppBindData",                        mBoundApplication,"appInfo");//把这两个对象的className属性设置为从meta-data中获取的被加密apk的application路径appinfo_In_LoadedApk.className = appClassName;        appinfo_In_AppBindData.className = appClassName;/**

* 调用LoadedApk中的makeApplication 方法 造一个application

* 前面改过路径了

*/Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk","makeApplication", loadedApkInfo,newClass[] {boolean.class, Instrumentation.class },newObject[] {false,null});        RefInvoke.setFieldOjbect("android.app.ActivityThread","mInitialApplication", currentActivityThread, app);        HashMap mProviderMap = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mProviderMap");        Iterator it = mProviderMap.values().iterator();while(it.hasNext()) {            Object providerClientRecord = it.next();            Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord",                    providerClientRecord,"mLocalProvider");            RefInvoke.setFieldOjbect("android.content.ContentProvider","mContext", localProvider, app);        }if(null== app){            Log.e(TAG,"application get is null !");        }else{            app.onCreate();        }    }

辅助源码看实际上还是很好理解的,不多说。

顺手推荐个android在线源码浏览网址http://androidxref.com/

为了方便后续调试代码,弄了个shell脚本,同时也可以基本解释整个加壳的流程:

#!/bin/bashENCRYPT_PATH="/home/kf2lc/develop/apk_encrypt"UNSHELL_PATH="/home/kf2lc/develop/workspace/BFC/UnShell"DEX_SHELL_TOOL_PATH="/home/kf2lc/develop/workspace/BFC/DexShellTool"DEMO_TEMP_PATH="./demopac"TEMP_PATH="./apk"ARM_SO_PATH="/libs/armeabi-v7a/libNativeTool.so"ARM_MIPS_PATH="/libs/mips/libNativeTool.so"echo"清理中间文件..."cd$ENCRYPT_PATHrm Demo-*.apkrm *.dexrm UnShell.apkecho"编译解壳工程..."cd$UNSHELL_PATHrm -rf gen binandroid update project -p .ant clean debugecho"拷贝壳工程dex文件到工作目录..."cp$UNSHELL_PATH"/bin/classes.dex"$ENCRYPT_PATH"/unshell.dex"cp$UNSHELL_PATH"/bin/BlankActivity-debug-unaligned.apk"$ENCRYPT_PATH"/UnShell.apk"echo"编译加壳工程... 生成新的classes.dex文件到工作目录..."cd$DEX_SHELL_TOOL_PATHant clean compile jar runecho"解压待加密apk... 替换classes.dex文件为加壳.dex文件..."cd$ENCRYPT_PATHunzip-d$TEMP_PATHUnShell.apkrm$TEMP_PATH"/classes.dex"mv ./classes.dex$TEMP_PATHecho"删除签名文件夹 重新打包apk..."cd$TEMP_PATHrm -rf ./META-INFzip -r ../Demo-encrypt-unsign.apk ./*cd../echo"清理中间目录..."rm -rf$TEMP_PATHecho"为加壳后的apk重新签名..."jarsigner -verbose -keystore bfc.keystore -signedjar Demo-encrypt.apk Demo-encrypt-unsign.apk bfc.keystore

注意事项

无论是虚拟机还是手机、平板,测试时一定要统一使用一个签名,否则很容易出无签名的安装错误;

由于本方案使用DexClassLoader作为动态加载的方案,从接口上看:

很明显,这货是需要一个文件路径的,这意味着如果直接使用该类,就必须要有个解密好的文件老老实实的躺在存储器上,这样一来无论你放在什么地方、该文件存在的时间有多短,破解者都有可能绕过壳、直接拿到解密的文件,这明显不科学;

资源加载,这里面我偷了个懒,壳工程的资源文件和源程序的资源文件是完全一致的,所以加载起来没有问题。但是这样一来整个加壳的APK实际上内部有两份资源文件了,示例APK还好,碰见图片多的那这个数据增量完全无法接受;

本文仅是个人理解,虽然跑通了但是也不免有瞎猫撞上死耗子的几率,错误加上错误产生正确也是可能的,仅供参考,欢迎质疑。

其实原文所述的方案是加壳的一个基本思路,具体要预防反编译实现起来肯定不会如此简略、加壳也只是预防破解的各路招式之一。但还是要感谢大牛的芸芸分享,使我辈菜鸟有了一条入门之路。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容