APK加壳【1】初步方案实现详解
2016-03-14 04:07
330 查看
来源与原理
本文是尝试对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则是反射工具。许多童鞋表示反射不好理解,一开始我也是这么觉得。不过经过一行行注释下来、对比系统源码,其实也没有多难。这里要说,静下心来分析,不到三百行的代码,能有多复杂呢?protected void attachBaseContext(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 = new File(apkFileName); if (!dexFile.exists()) dexFile.createNewFile(); // 读取程序classes.dex文件 byte[] dexdata = this.readDexFileFromApk(); // 分离出解壳后的apk文件已用于动态加载 this.splitPayLoadFromDex(dexdata); // 配置动态加载环境 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); //换Loader操作 动态加载如被加密又装换回来的apk文件 DexClassLoader dLoader = new DexClassLoader(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(); } } public void onCreate() { 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", new Class[] {}, new Object[] {}); /** * 获取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<Application> mAllApplications = (ArrayList<Application>) 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, new Class[] { boolean.class, Instrumentation.class }, new Object[] { 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/bash ENCRYPT_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_PATH rm Demo-*.apk rm *.dex rm UnShell.apk echo "编译解壳工程..." cd $UNSHELL_PATH rm -rf gen bin android update project -p . ant clean debug echo "拷贝壳工程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_PATH ant clean compile jar run echo "解压待加密apk... 替换classes.dex文件为加壳.dex文件..." cd $ENCRYPT_PATH unzip -d $TEMP_PATH UnShell.apk rm $TEMP_PATH"/classes.dex" mv ./classes.dex $TEMP_PATH echo "删除签名文件夹 重新打包apk..." cd $TEMP_PATH rm -rf ./META-INF zip -r ../Demo-encrypt-unsign.apk ./* cd ../ echo "清理中间目录..." rm -rf $TEMP_PATH echo "为加壳后的apk重新签名..." jarsigner -verbose -keystore bfc.keystore -signedjar Demo-encrypt.apk Demo-encrypt-unsign.apk bfc.keystore
注意事项
无论是虚拟机还是手机、平板,测试时一定要统一使用一个签名,否则很容易出无签名的安装错误;由于本方案使用DexClassLoader作为动态加载的方案,从接口上看:
很明显,这货是需要一个文件路径的,这意味着如果直接使用该类,就必须要有个解密好的文件老老实实的躺在存储器上,这样一来无论你放在什么地方、该文件存在的时间有多短,破解者都有可能绕过壳、直接拿到解密的文件,这明显不科学;
资源加载,这里面我偷了个懒,壳工程的资源文件和源程序的资源文件是完全一致的,所以加载起来没有问题。但是这样一来整个加壳的APK实际上内部有两份资源文件了,示例APK还好,碰见图片多的那这个数据增量完全无法接受;
本文仅是个人理解,虽然跑通了但是也不免有瞎猫撞上死耗子的几率,错误加上错误产生正确也是可能的,仅供参考,欢迎质疑。
其实原文所述的方案是加壳的一个基本思路,具体要预防反编译实现起来肯定不会如此简略、加壳也只是预防破解的各路招式之一。但还是要感谢大牛的芸芸分享,使我辈菜鸟有了一条入门之路。
原文地址: http://taoyuanxiaoqi.com/2015/01/12/apkshell1/
相关文章推荐
- CUDA 7.0 速查手册
- dex注入实现详解
- cs231n assignment1 tips
- HDU 5047 Sawtooth 高精度
- 恒生电子
- OpenGL with PyOpenGL tutorial Python and PyGame p.1 - Making a rotating Cube Example
- 纯css3实现斑马线repeating-linear-gradient和linear-gradient的妙用
- Reorder Linked List
- JAVA获取公网IP地址与内网IP地址方法
- iOS_CNBlog项目开发 (基于博客园api开发)
- CRM IFRAME 显示地图
- 中软
- C# SQLite操作 特别注意事项
- 12C-OCP升级1z-060-001
- android 中theme和style的语法相关
- 通俗易懂多线程
- 1003. 我要通过!(20)——做题纪录
- 编程实现调出'运行'窗口
- hadoop环境搭建及修改配置文件(第四讲)
- 深度学习word2vec笔记之基础篇算法篇应用篇--写的非常到位