├── README.md ├── demo.keystore ├── images ├── wx.jpg └── zfb.jpg ├── requests.txt ├── shellApplicationSourceCode ├── AndroidManifest.xml ├── java │ └── cn │ │ └── yongye │ │ └── stub │ │ ├── MainActivity.java │ │ ├── R.java │ │ ├── StubApp.java │ │ └── common │ │ ├── Config.java │ │ ├── FileUtils.java │ │ └── RefInvoke.java └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── sheller.py └── tools ├── android.jar ├── apksigner.jar ├── apktool_2.5.0.jar ├── apktool_2.9.3.jar └── dx.jar /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | Java层DEX一键加固脚本 4 | 5 | # 使用说明 6 | 7 | ```shell 8 | python -f xxx.apk 9 | ``` 10 | 11 | # 加固原理 12 | 13 | 准备一个壳DEX文件(源码位置:shellApplicationSourceCode),将原APK的DEX文件加密保存到壳DEX尾部,然后将原APK文件中的原DEX文件替换为壳DEX,并修改原APK文件里AndroidManifest.xml的applicationandroid:name字段,实现从壳DEX启动。 14 | 15 | 解密和加载原DEX的任务交给壳DEX,这样就实现了**APK文件防编译保护** 16 | 17 | # 一键加固脚本实现步骤 18 | 19 | 1. 准备原DEX加密算法以及隐藏位置(壳DEX尾部) 20 | 21 | ```python 22 | """ 23 | 1. 第一步:确定加密算法 24 | """ 25 | inKey = 0xFF 26 | print("[*] 确定加密解密算法,异或: {}".format(str(inKey))) 27 | ``` 28 | 29 | 2. 生成壳DEX。(壳Application动态加载原application中需要原application的name字段) 30 | 31 | ```python 32 | """ 33 | 2. 第二步:准备好壳App 34 | """ 35 | # 反编译原apk 36 | decompAPK(fp) 37 | # print("[*] 反编译原的apk文件{}完成".format(fp)) 38 | # 获取Applicaiton name并保存到壳App源码中 39 | stSrcDexAppName = getAppName(fp) 40 | # print("[*] 获取原apk文件的Application Android:name=\"{}\" 完成".format(stSrcDexAppName)) 41 | save_appName(stSrcDexAppName) 42 | # print("[*] 保存原apk文件的Application Android:name=\"{}\" 到壳App源码的配置文件完成".format(stSrcDexAppName)) 43 | # 编译出壳DEX 44 | compileShellDex() 45 | print("[*] 壳App的class字节码文件编译为:shell.dex完成") 46 | ``` 47 | 48 | 3. 修改原APK文件中的AndroidManifest.xml文件的applicationandroid:name字段,实现从壳application启动 49 | 50 | ```python 51 | """ 52 | 3. 第三步:修改原apk AndroidManifest.xml文件中的Application name字段为壳的Application name字段 53 | """ 54 | # 替换壳Applicaiton name到原apk的AndroidManifest.xml内 55 | replaceTag(fp, "cn.yongye.stub.StubApp") 56 | print("[*] 原apk文件AndroidManifest.xml中application的name字段替换为壳application name字段完成") 57 | ``` 58 | 59 | 4. 加密原DEX到壳DEX尾部并将壳DEX替换到原APK中 60 | 61 | ```python 62 | """ 63 | 4. 替换原apk中的DEX文件为壳DEX 64 | """ 65 | replaceSDexToShellDex(os.path.join(stCurrentPt, "result.apk")) 66 | print("[*] 壳DEX替换原apk包内的DEX文件完成") 67 | ``` 68 | 69 | 5. 自动签名 70 | 71 | ```python 72 | """ 73 | 5. apk签名 74 | """ 75 | signApk(os.path.join(stCurrentPt, "result.apk"), os.path.join(stCurrentPt, "demo.keystore")) 76 | print("[*] 签名完成,生成apk文件: {}".format(os.path.join(stCurrentPt, "result.apk"))) 77 | ``` 78 | 79 | # 问题 80 | 81 | 【1】**libpng error: Not a PNG file**:apktool.jar编译smali项目时,如果出现png结尾的GIF文件时,会编译失败,这里我的解决方法时将GIF强行转换成PNG,解决问题。 82 | 83 | 中途解决该问题时,曾尝试使用[AXMLEditor](https://github.com/fourbrother/AXMLEditor)修改二进制Androidmanifest.xml的开源工具直接修改,然后想绕过编译资源步骤,实际不能成功,因为更改过后的application name字段在编译资源过程中,会被记录下来,而直接修改导致没有被记录,android系统是识别不到修改后的这个字段值 84 | 85 | # 定制版内容(付费) 86 | 87 | [x] 多DEX加密 88 | 89 | [x] APK包伪加密 90 | 91 | [x] 部分资源加密 92 | 93 | [x] 包名随机 94 | 95 | [x] 高级加密算法 96 | 97 | [x] ... 98 | 99 | 100 | # 参考 101 | 102 | [1] [Dex简单保护](https://xz.aliyun.com/t/5789) 103 | 104 | [2] [ DexShell](https://github.com/Herrrb/DexShell) 105 | 106 | # 联系 107 | 108 | vx: xcc1014885794 109 | 110 | # 赞助 111 | 112 |
113 | 114 | 微信捐赠 115 | 116 | 117 |
118 | 119 |
120 | 121 | 支付宝捐赠 122 | 123 | 124 |
125 | 126 | -------------------------------------------------------------------------------- /demo.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/demo.keystore -------------------------------------------------------------------------------- /images/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/images/wx.jpg -------------------------------------------------------------------------------- /images/zfb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/images/zfb.jpg -------------------------------------------------------------------------------- /requests.txt: -------------------------------------------------------------------------------- 1 | filetype 2 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/java/cn/yongye/stub/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.yongye.stub; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | public class MainActivity extends Activity { 7 | 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | setContentView(R.layout.activity_main); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/java/cn/yongye/stub/R.java: -------------------------------------------------------------------------------- 1 | /* AUTO-GENERATED FILE. DO NOT MODIFY. 2 | * 3 | * This class was automatically generated by the 4 | * aapt tool from the resource data it found. It 5 | * should not be modified by hand. 6 | */ 7 | 8 | package cn.yongye.stub; 9 | 10 | public final class R { 11 | public static final class attr { 12 | } 13 | public static final class color { 14 | public static final int colorAccent=0x7f050002; 15 | public static final int colorPrimary=0x7f050000; 16 | public static final int colorPrimaryDark=0x7f050001; 17 | } 18 | public static final class drawable { 19 | public static final int ic_launcher_background=0x7f020000; 20 | public static final int ic_launcher_foreground=0x7f020001; 21 | public static final int ic_launcher_foreground_1=0x7f020002; 22 | } 23 | public static final class layout { 24 | public static final int activity_main=0x7f040000; 25 | } 26 | public static final class mipmap { 27 | public static final int ic_launcher=0x7f030000; 28 | public static final int ic_launcher_round=0x7f030001; 29 | } 30 | public static final class string { 31 | public static final int app_name=0x7f060000; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/java/cn/yongye/stub/StubApp.java: -------------------------------------------------------------------------------- 1 | package cn.yongye.stub; 2 | 3 | import android.app.Application; 4 | import android.app.Instrumentation; 5 | import android.content.Context; 6 | import android.content.pm.ApplicationInfo; 7 | import android.content.res.AssetManager; 8 | import android.os.Environment; 9 | import android.util.Log; 10 | import java.lang.reflect.Array; 11 | import java.lang.reflect.Field; 12 | import dalvik.system.BaseDexClassLoader; 13 | import java.io.ByteArrayInputStream; 14 | import java.io.DataInputStream; 15 | import java.io.File; 16 | import java.io.FileInputStream; 17 | import java.io.FileNotFoundException; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.lang.ref.WeakReference; 22 | import java.util.List; 23 | import java.util.Map; 24 | import cn.yongye.stub.common.Config; 25 | import cn.yongye.stub.common.FileUtils; 26 | import cn.yongye.stub.common.RefInvoke; 27 | import dalvik.system.DexClassLoader; 28 | 29 | 30 | public class StubApp extends Application { 31 | 32 | private String TAG = "yongye"; 33 | private Application oApp = this; 34 | private String stDmpDexPt = null; 35 | 36 | @Override 37 | protected void attachBaseContext(Context base) { 38 | super.attachBaseContext(base); 39 | 40 | /** 41 | * 1.解密原始DEX到缓存目录下yongye 42 | */ 43 | byte[] baSrcDex = null; 44 | stDmpDexPt = oApp.getFilesDir().getAbsolutePath() + "/" + "/yongye.dex"; 45 | File libs = oApp.getDir("libs", Context.MODE_PRIVATE); 46 | byte[] baDex = null; 47 | try { 48 | baDex = FileUtils.readDexFileFromApk(oApp); 49 | FileUtils.splitPayLoadFromDex(stDmpDexPt, baDex); 50 | } catch (IOException e) { 51 | Log.e(TAG, "解密原始DEX失败。"); 52 | e.printStackTrace(); 53 | } 54 | Log.i(TAG, "解密原始DEXcompleted"); 55 | 56 | 57 | /** 58 | * 2.加载原始DEX 59 | */ 60 | String stActivityThread = "android.app.ActivityThread"; 61 | String stClassLoadedApk = "android.app.LoadedApk"; 62 | String stCurrentPkgName = this.getPackageName(); 63 | 64 | //bugfix libxxx.so not found in classloader 65 | File nativeLib = new File(this.getApplicationInfo().nativeLibraryDir); 66 | 67 | Object obCurrentActivityThread = RefInvoke.invokeStaticMethod(stActivityThread, 68 | "currentActivityThread", new Class[]{}, new Object[]{}); 69 | Map obmPackage = (Map)RefInvoke.getFieldObject(stActivityThread, 70 | obCurrentActivityThread, "mPackages"); 71 | WeakReference wr = (WeakReference) obmPackage.get(stCurrentPkgName); 72 | DexClassLoader oDexClassLoader = new DexClassLoader(stDmpDexPt, oApp.getCacheDir().getAbsolutePath(), 73 | nativeLib.getAbsolutePath(), (ClassLoader)RefInvoke.getFieldObject(stClassLoadedApk, 74 | wr.get(), "mClassLoader")); 75 | 76 | //bugfix class not found 77 | load(oApp, stDmpDexPt); 78 | load(oApp, oApp.getApplicationInfo().sourceDir); 79 | RefInvoke.setFieldObject(stClassLoadedApk, "mClassLoader", wr.get(), oDexClassLoader); 80 | RefInvoke.setFieldObject(stClassLoadedApk, "mResDir", wr.get(), oApp.getApplicationInfo().sourceDir); 81 | Log.i(TAG, "load source dex completed."); 82 | 83 | /** 84 | * 3.启用原始DEX文件的Application(更改环境值满足加载原始DEX的要求) 85 | */ 86 | String stSrcAppName = Config.MAIN_APPLICATION; 87 | Object mBoundApplication = RefInvoke.getFieldObject(stActivityThread, obCurrentActivityThread, "mBoundApplication"); 88 | Object loadedApkInfo = RefInvoke.getFieldObject(stActivityThread +"$AppBindData", mBoundApplication, "info"); 89 | RefInvoke.setFieldObject(stClassLoadedApk, "mApplication", loadedApkInfo, null); 90 | Object mInitApplication = RefInvoke.getFieldObject(stActivityThread, obCurrentActivityThread, "mInitialApplication"); 91 | List mAllApplications = (List) RefInvoke.getFieldObject(stActivityThread, obCurrentActivityThread, "mAllApplications"); 92 | mAllApplications.remove(mInitApplication); 93 | ((ApplicationInfo) RefInvoke.getFieldObject(stClassLoadedApk, loadedApkInfo, "mApplicationInfo")).className = stSrcAppName; 94 | ((ApplicationInfo) RefInvoke.getFieldObject(stActivityThread+"$AppBindData", mBoundApplication, "appInfo")).className = stSrcAppName; 95 | Application makeApplication = (Application) RefInvoke.invokeMethod(stClassLoadedApk, 96 | "makeApplication", loadedApkInfo, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null}); 97 | RefInvoke.setFieldObject(stActivityThread, "mInitialApplication", obCurrentActivityThread, 98 | makeApplication); 99 | Map mProviderMap = (Map) RefInvoke.getFieldObject(stActivityThread, obCurrentActivityThread, 100 | "mProviderMap"); 101 | for (Map.Entry entry : mProviderMap.entrySet()) { 102 | Object providerClientRecord = entry.getValue(); 103 | Object mLocalProvider = RefInvoke.getFieldObject(stActivityThread+"$ProviderClientRecord", providerClientRecord, "mLocalProvider"); 104 | if (mLocalProvider != null) { 105 | RefInvoke.setFieldObject("android.content.ContentProvider", "mContext", mLocalProvider, makeApplication); 106 | } 107 | } 108 | makeApplication.onCreate(); 109 | Log.i(TAG, "getClassLoader - " + getClassLoader()); 110 | Log.i(TAG, "unshell completed."); 111 | } 112 | 113 | public void load(Context oApp, String path) { 114 | try { 115 | // 已加载的dex 116 | Object dexPathList = getField(BaseDexClassLoader.class, "pathList", oApp.getClassLoader()); 117 | Object dexElements = getField(dexPathList.getClass(), "dexElements", dexPathList); 118 | 119 | // patchdex 120 | String dexOptDir = oApp.getCacheDir().getAbsolutePath(); 121 | DexClassLoader dcl = new DexClassLoader(path, dexOptDir, null, oApp.getClassLoader()); 122 | Object patchDexPathList = getField(BaseDexClassLoader.class, "pathList", dcl); 123 | Object patchDexElements = getField(patchDexPathList.getClass(), "dexElements", patchDexPathList); 124 | 125 | // 将patchdex和已加载的dexes数组拼接连接 126 | Object concatDexElements = concatArray(patchDexElements, dexElements); 127 | 128 | // 重新给dexPathList#dexElements赋值 129 | setField(dexPathList.getClass(), "dexElements", dexPathList, concatDexElements); 130 | } catch (NoSuchFieldException e) { 131 | e.printStackTrace(); 132 | } catch (IllegalAccessException e) { 133 | e.printStackTrace(); 134 | } 135 | } 136 | 137 | /** 138 | * @param cls 被访问对象的class 139 | * @param fieldName 对象的成员变量名 140 | * @param object 被访问对象 141 | * @return 142 | * @throws NoSuchFieldException 143 | * @throws IllegalAccessException 144 | */ 145 | public Object getField(Class cls, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException { 146 | Field field = cls.getDeclaredField(fieldName); 147 | field.setAccessible(true); 148 | return field.get(object); 149 | } 150 | 151 | 152 | /** 153 | * @param cls 被访问对象的class 154 | * @param fieldName 对象的成员变量名 155 | * @param object 被访问对象 156 | * @param value 赋值给成员变量 157 | * @throws NoSuchFieldException 158 | * @throws IllegalAccessException 159 | */ 160 | public void setField(Class cls, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException { 161 | Field field = cls.getDeclaredField(fieldName); 162 | field.setAccessible(true); 163 | field.set(object, value); 164 | } 165 | 166 | 167 | /** 168 | * 连接两个数组(指定位置) 169 | * 170 | * @param left 连接后在新数组的左侧 171 | * @param right 连接后在新数组的右侧 172 | * @return 173 | */ 174 | public Object concatArray(Object left, Object right) { 175 | int len1 = Array.getLength(left); 176 | int len2 = Array.getLength(right); 177 | int totalLen = len1 + len2; 178 | Object concatArray = Array.newInstance(left.getClass().getComponentType(), totalLen); 179 | for(int i = 0; i < len1; i++) { 180 | Array.set(concatArray, i, Array.get(left, i)); 181 | } 182 | for(int j = 0; j < len2; j++) { 183 | Array.set(concatArray, len1 + j, Array.get(right, j)); 184 | } 185 | return concatArray; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/java/cn/yongye/stub/common/Config.java: -------------------------------------------------------------------------------- 1 | package cn.yongye.stub.common; 2 | 3 | public class Config { 4 | public static final String MAIN_APPLICATION = "android.app.Application"; 5 | } -------------------------------------------------------------------------------- /shellApplicationSourceCode/java/cn/yongye/stub/common/FileUtils.java: -------------------------------------------------------------------------------- 1 | package cn.yongye.stub.common; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.io.ByteArrayInputStream; 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.DataInputStream; 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.util.zip.ZipEntry; 15 | import java.util.zip.ZipInputStream; 16 | 17 | public class FileUtils { 18 | 19 | /** 20 | * 获取父目录的绝对路径 21 | * @param f 文件对象 22 | * @return 父目录对象 23 | */ 24 | public static File getParent(File f) { 25 | String path = f.getAbsolutePath(); 26 | int separator = path.lastIndexOf(File.separator); 27 | return separator == -1 ? new File(path) : new File(path.substring(0, separator)); 28 | } 29 | 30 | /** 31 | * 获取应用classes.dex字节数据 32 | * @param context 33 | * @return 34 | * @throws IOException 35 | */ 36 | public static byte[] readDexFileFromApk(Context context) throws IOException { 37 | ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream(); 38 | ZipInputStream localZipInputStream = new ZipInputStream( 39 | new BufferedInputStream(new FileInputStream( 40 | context.getApplicationInfo().sourceDir))); 41 | while (true) { 42 | ZipEntry localZipEntry = localZipInputStream.getNextEntry(); 43 | if (localZipEntry == null) { 44 | localZipInputStream.close(); 45 | break; 46 | } 47 | if (localZipEntry.getName().equals("classes.dex")) { 48 | byte[] arrayOfByte = new byte[1024]; 49 | while (true) { 50 | int i = localZipInputStream.read(arrayOfByte); 51 | if (i == -1) 52 | break; 53 | dexByteArrayOutputStream.write(arrayOfByte, 0, i); 54 | } 55 | } 56 | localZipInputStream.closeEntry(); 57 | } 58 | localZipInputStream.close(); 59 | return dexByteArrayOutputStream.toByteArray(); 60 | } 61 | 62 | /** 63 | * 将原DEX从壳DEX上分离出来 64 | * @param stDexPt 65 | * @param apkdata 壳DEX数据 66 | * @throws IOException 67 | */ 68 | public static void splitPayLoadFromDex(String stDexPt, byte[] apkdata) throws IOException { 69 | String TAG = "yongye"; 70 | apkdata = decrypt(apkdata); 71 | int ablen = apkdata.length; 72 | byte[] dexlen = new byte[4]; 73 | System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4); 74 | ByteArrayInputStream bais = new ByteArrayInputStream(dexlen); 75 | DataInputStream in = new DataInputStream(bais); 76 | int readInt = in.readInt(); 77 | Log.i(TAG, "原始DEX长度:" + String.valueOf(readInt)); 78 | byte[] newdex = new byte[readInt]; 79 | System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt); 80 | File file = new File(stDexPt); 81 | if(file.exists()){ 82 | Log.i(TAG, file.getAbsolutePath() + "文件还存在"); 83 | } 84 | try { 85 | FileOutputStream localFileOutputStream = new FileOutputStream(file); 86 | localFileOutputStream.write(newdex); 87 | localFileOutputStream.close(); 88 | } catch (IOException localIOException) { 89 | Log.e(TAG, "dump 原始DEX失败"); 90 | throw new RuntimeException(localIOException); 91 | } 92 | } 93 | 94 | private static byte[] decrypt(byte[] baEncryted){ 95 | byte[] baRes = new byte[baEncryted.length]; 96 | for(int i=0; i 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/shellApplicationSourceCode/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ShellApp 3 | 4 | -------------------------------------------------------------------------------- /shellApplicationSourceCode/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /sheller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import struct 4 | import zlib 5 | import hashlib 6 | import re 7 | from subprocess import check_call 8 | import argparse 9 | import os 10 | from xml.dom.minidom import parse 11 | import zipfile 12 | from PIL import Image 13 | import shutil 14 | import sys 15 | 16 | 17 | stCurrentPt = os.path.abspath(__file__).replace(os.path.basename(__file__), "") 18 | stApkToolPt = os.path.join(stCurrentPt, 'tools', 'apktool_2.9.3.jar') 19 | stShellAppPt = os.path.join(stCurrentPt, 'shellApplicationSourceCode') 20 | staaptPt = os.path.join(stCurrentPt, 'tools', 'aapt.exe') 21 | stAndroidJarlibPt = os.path.join(stCurrentPt, 'tools', 'android.jar') 22 | stdxJarPt = os.path.join(stCurrentPt, 'tools', 'dx.jar') 23 | stApksignJarPt = os.path.join(stCurrentPt, 'tools', 'apksigner.jar') 24 | osType = sys.platform 25 | 26 | 27 | def add_srcDexToShellDex(srcDex, shellDex): 28 | 29 | liShellDt = [] 30 | liSrcDexDt = [] 31 | liAllDt = [] 32 | 33 | #将原始DEX和壳DEX数据放在一个列表中 34 | with open(shellDex, "rb") as f: 35 | shellData = f.read() 36 | liShellDt = list(struct.unpack(len(shellData)*'B', shellData)) 37 | with open(srcDex, 'rb') as f: 38 | srcDt = f.read() 39 | liSrcDexDt = list(struct.unpack(len(srcDt)*'B', srcDt)) 40 | liAllDt.extend(shellData) 41 | # 加密原DEX 42 | for i in liSrcDexDt: 43 | liAllDt.append(i ^ inKey) 44 | 45 | iSrcDexLen = len(liSrcDexDt) 46 | liSrcDexLen = intToSmalEndian(iSrcDexLen) 47 | liSrcDexLen.reverse() 48 | # 加密原DEX长度 49 | for i in liSrcDexLen: 50 | liAllDt.append(i ^ inKey) 51 | 52 | # 计算合成后DEX文件的checksum、signature、file_size 53 | # 更改文件头 54 | newFsize = len(liAllDt) 55 | liNewFSize = intToSmalEndian(newFsize) 56 | for i in range(4): 57 | liAllDt[32 + i] = liNewFSize[i] 58 | 59 | newSignature = hashlib.sha1(bytes(liAllDt[32:])).hexdigest() 60 | liNewSignature = re.findall(r'.{2}', newSignature) 61 | for i in range(len(liNewSignature)): 62 | liNewSignature[i] = ord(bytes.fromhex(liNewSignature[i])) 63 | for i in range(20): 64 | liAllDt[12 + i] = liNewSignature[i] 65 | 66 | newChecksum = zlib.adler32(bytes(liAllDt[12:])) 67 | liNewChecksum = intToSmalEndian(newChecksum) 68 | for i in range(4): 69 | liAllDt[8 + i] = liNewChecksum[i] 70 | 71 | with open(os.path.join(stCurrentPt, 'classes.dex'), 'wb') as f: 72 | f.write(bytes(liAllDt)) 73 | 74 | 75 | def intToSmalEndian(numb): 76 | liRes = [] 77 | 78 | stHexNumb = hex(numb)[2:] 79 | for i in range(8 - len(stHexNumb)): 80 | stHexNumb = '0' + stHexNumb 81 | liRes = re.findall(r'.{2}', stHexNumb) 82 | for i in range(len(liRes)): 83 | liRes[i] = ord(bytes.fromhex(liRes[i])) 84 | liRes.reverse() 85 | 86 | return liRes 87 | 88 | def decompAPK(fp): 89 | cmd = [] 90 | cmd.append('java') 91 | cmd.append('-jar') 92 | cmd.append(stApkToolPt) 93 | cmd.append('d') 94 | cmd.append('-o') 95 | cmd.append(fp + "decompile") 96 | cmd.append(fp) 97 | check_call(cmd) 98 | 99 | def getAppName(fp): 100 | stAppName = "android.app.Application" 101 | stDisassembleDp = fp + "decompile" 102 | stAMFp = os.path.join(stDisassembleDp, "AndroidManifest.xml") 103 | oDomTree = parse(stAMFp) 104 | manifest = oDomTree.documentElement 105 | apps = manifest.getElementsByTagName('application') 106 | 107 | if len(apps) != 1: 108 | print("存在多个Application") 109 | raise(Exception) 110 | if apps[0].getAttribute("android:name") != "": 111 | stAppName = apps[0].getAttribute("android:name") 112 | 113 | return stAppName 114 | 115 | def replaceTag(fp, stValue): 116 | stAXMLFp = os.path.join(stCurrentPt, fp + "decompile", "AndroidManifest.xml") 117 | dom = None 118 | with open(stAXMLFp, 'r', encoding='UTF-8') as f: 119 | dom = parse(f) 120 | root = dom.documentElement 121 | app = root.getElementsByTagName('application')[0] 122 | app.setAttribute("android:name", stValue) 123 | # bugfix INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2 124 | app.setAttribute("android:extractNativeLibs", 'true') 125 | app.setAttribute('android:debuggable', 'true') 126 | print('[*] replace android:extractNativeLibs -> ' + app.getAttribute("android:extractNativeLibs")) 127 | with open(stAXMLFp, "w", encoding='UTF-8') as f: 128 | dom.writexml(f, encoding='UTF-8') 129 | stDecompDp = os.path.join(stCurrentPt, fp + "decompile") 130 | # 修复PNG文件BUG 131 | PngBug(stDecompDp) 132 | 133 | cmd = [] 134 | cmd.append('java') 135 | cmd.append('-jar') 136 | cmd.append(stApkToolPt) 137 | cmd.append('b') 138 | cmd.append('-o') 139 | cmd.append("result.apk") 140 | cmd.append(stDecompDp) 141 | check_call(cmd) 142 | 143 | shutil.rmtree(stDecompDp) 144 | 145 | 146 | def save_appName(appName): 147 | stCfgFp = os.path.join(stShellAppPt, "java/cn/yongye/stub/common/Config.java") 148 | with open(stCfgFp, 'w') as f: 149 | f.write("package cn.yongye.stub.common;\n") 150 | f.write("\n") 151 | f.write("public class Config {\n") 152 | f.write(" public static final String MAIN_APPLICATION = \"{}\";\n".format(appName)) 153 | f.write("}") 154 | 155 | def compileSrcApk(dp): 156 | cmd = [] 157 | cmd.append('java') 158 | cmd.append('-jar') 159 | cmd.append(stApkToolPt) 160 | cmd.append('b') 161 | cmd.append('-f') 162 | cmd.append('-o') 163 | cmd.append(stCurrentPt + "\src.apk") 164 | cmd.append(dp) 165 | check_call(cmd) 166 | 167 | def compileShellDex(): 168 | 169 | licmd2 = [] 170 | licmd2.append("javac") 171 | licmd2.append("-encoding") 172 | licmd2.append("UTF-8") 173 | licmd2.append("-target") 174 | licmd2.append("1.8") 175 | licmd2.append("-bootclasspath") 176 | licmd2.append(stAndroidJarlibPt) 177 | licmd2.append("-d") 178 | licmd2.append(stCurrentPt) 179 | licmd2.append(os.path.join(stCurrentPt, 'shellApplicationSourceCode', 'java', 'cn', 'yongye', 'stub', '*.java')) 180 | licmd2.append(os.path.join(stCurrentPt, 'shellApplicationSourceCode', 'java', 'cn', 'yongye', 'stub', 'common', '*.java')) 181 | check_call(' '.join(licmd2), shell=True) 182 | 183 | licmd3 = [] 184 | licmd3.append("java") 185 | licmd3.append("-jar") 186 | licmd3.append(stdxJarPt) 187 | licmd3.append("--dex") 188 | licmd3.append("--output=" + stCurrentPt + r"\shell.dex") 189 | licmd3.append(os.path.join('cn', 'yongye', 'stub', '*.class')) 190 | licmd3.append(os.path.join('cn', 'yongye', 'stub', 'common', '*.class')) 191 | check_call(' '.join(licmd3), shell=True) 192 | 193 | shutil.rmtree("cn") 194 | 195 | 196 | def replaceSDexToShellDex(stSApkFp): 197 | # 提取原APK中的DEX到本地 198 | oFApk = zipfile.ZipFile(stSApkFp) 199 | oFApk.extract("classes.dex", stCurrentPt) 200 | oFApk.close() 201 | stSDexFp = os.path.join(stCurrentPt, "classes.dex") 202 | stShellDexFp = os.path.join(stCurrentPt, "shell.dex") 203 | # 将原DEX添加到壳DEX尾部,保存到本目录下为classes.dex 204 | add_srcDexToShellDex(stSDexFp, stShellDexFp) 205 | #将修改后的壳DEX添加到原DEX中 206 | 207 | deleteZipItem(stSApkFp, 'classes.dex') 208 | 209 | oNewApk = zipfile.ZipFile(stSApkFp, "a") 210 | oNewApk.write("classes.dex", "classes.dex") 211 | oNewApk.close() 212 | 213 | os.remove(stSDexFp) 214 | os.remove(stShellDexFp) 215 | 216 | 217 | def deleteZipItem(zp, pp): 218 | zoutp = '{}-out'.format(zp) 219 | zin = zipfile.ZipFile(zp, mode='r') 220 | zout = zipfile.ZipFile(zoutp, mode='w', compression=zipfile.ZIP_DEFLATED) 221 | for item in zin.infolist(): 222 | if item.filename == pp: continue 223 | zout.writestr(item, zin.read(item)) 224 | zin.close() 225 | zout.close() 226 | shutil.move(zoutp, zp) 227 | 228 | 229 | def signApk(fp, stKeystoreFp): 230 | cmd = [] 231 | cmd.append("java") 232 | cmd.append("-jar") 233 | cmd.append(stApksignJarPt) 234 | cmd.append("sign") 235 | cmd.append("--ks") 236 | cmd.append(stKeystoreFp) 237 | cmd.append("--ks-pass") 238 | cmd.append("pass:123456") 239 | cmd.append(fp) 240 | check_call(cmd) 241 | 242 | 243 | def PngBug(decompFp): 244 | import filetype 245 | liFiles = os.listdir(decompFp) 246 | for fn in liFiles: 247 | fp = os.path.join(decompFp, fn) 248 | if os.path.isdir(fp): 249 | PngBug(fp) 250 | if fp.endswith(".png") and filetype.guess(fp).MIME == "image/gif": 251 | im = Image.open(fp) 252 | current = im.tell() 253 | im.save(fp) 254 | 255 | 256 | if __name__ == "__main__": 257 | 258 | parser = argparse.ArgumentParser() 259 | parser.add_argument('-f', help="对指定文件进行加固", nargs=1, metavar='APK') 260 | args = parser.parse_args() 261 | 262 | if args.f: 263 | fp = args.__dict__.get('f')[0] 264 | """ 265 | 1. 第一步:确定加密算法 266 | """ 267 | inKey = 0xff 268 | print("[*] 确定加密解密算法,异或: {}".format(str(inKey))) 269 | 270 | """ 271 | 2. 第二步:准备好壳App 272 | """ 273 | # 反编译原apk 274 | decompAPK(fp) 275 | # print("[*] 反编译原的apk文件{}完成".format(fp)) 276 | # 获取Applicaiton name并保存到壳App源码中 277 | stSrcDexAppName = getAppName(fp) 278 | # print("[*] 获取原apk文件的Application Android:name=\"{}\" 完成".format(stSrcDexAppName)) 279 | save_appName(stSrcDexAppName) 280 | # print("[*] 保存原apk文件的Application Android:name=\"{}\" 到壳App源码的配置文件完成".format(stSrcDexAppName)) 281 | # 编译出壳DEX 282 | compileShellDex() 283 | print("[*] 壳App的class字节码文件编译为:shell.dex完成") 284 | 285 | """ 286 | 3. 第三步:修改原apk AndroidManifest.xml文件中的Application name字段为壳的Application name字段 287 | """ 288 | # 替换壳Applicaiton name到原apk的AndroidManifest.xml内 289 | replaceTag(fp, "cn.yongye.stub.StubApp") 290 | print("[*] 原apk文件AndroidManifest.xml中application的name字段替换为壳application name字段完成") 291 | 292 | """ 293 | 4. 替换原apk中的DEX文件为壳DEX 294 | """ 295 | replaceSDexToShellDex(os.path.join(stCurrentPt, "result.apk")) 296 | print("[*] 壳DEX替换原apk包内的DEX文件完成") 297 | 298 | """ 299 | 5. apk签名 300 | """ 301 | signApk(os.path.join(stCurrentPt, "result.apk"), os.path.join(stCurrentPt, "demo.keystore")) 302 | print("[*] 签名完成,生成apk文件: {}".format(os.path.join(stCurrentPt, "result.apk"))) 303 | 304 | 305 | else: 306 | parser.print_help() 307 | 308 | 309 | -------------------------------------------------------------------------------- /tools/android.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/tools/android.jar -------------------------------------------------------------------------------- /tools/apksigner.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/tools/apksigner.jar -------------------------------------------------------------------------------- /tools/apktool_2.5.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/tools/apktool_2.5.0.jar -------------------------------------------------------------------------------- /tools/apktool_2.9.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/tools/apktool_2.9.3.jar -------------------------------------------------------------------------------- /tools/dx.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongyecc/apksheller/4dbe3959680a461de737d85b28202118d092224a/tools/dx.jar --------------------------------------------------------------------------------