手搓整体加密壳demo && 手撕安卓源码 为了学习一下脱壳所以学一下加壳(其实是看网上大佬的文章看晕了,我靠,怎么这么底层),其实写得我也挺晕的,正好整理一下思路。 本文可能偏向教程?大概吧。
一、环境准备 IDE: android studio 感觉gradle下载太慢可以修改镜像源,因为它默认下载谷歌的。但是不要异常退出,可能会崩掉,然后就只能重装了。 下面是我下的版本,根据自己需要下载就好了。
模拟器:mumu12 因为我没有真机,所以用的是模拟器,其实我感觉可能夜神会更好,因为可以调android的版本,不像mumu12只能用android12。 可不可以用android studio的呢,当然可以,主要是我没试过,所以就没这么干。
python,java,frida这些默认都有,没有就搜教程。
二、创建工程 1、选择工作目录 在一个你喜欢的地方,创建一个叫ShellDemo的目录(当然叫啥也随便)。
1 2 3 4 5 6 目录结构 ShellDemo ----target_app ----shell_file ----tool target_app和shell_file是android studio的project目录,tool则存放打包apk和脱壳脚本等杂七杂八的东西。
2、创建target_app 打开android studio -> 点击new project -> phone and tablet 选择 empty views activity -> next
1 2 3 4 5 6 Name : TargetApp PackageName : com.demo.target SaveLocation : 选择target_app目录 Language : Java Min SDK : 23 Build Configuration language : Groovy DSL
点击finish即创建成功。
创建完成后,选择ShellDemo\target_app\app\src\main\java\com\demo\target\MainActivity.java,将其修改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.demo.target;import android.app.Activity;import android.os.Bundle;import android.widget.TextView;import android.util.Log;public class MainActivity extends Activity { private static final String TAG = "TargetApp" ; private static final String SECRET = "I_AM_THE_REAL_DEX_FLAG_2024" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); TextView tv = new TextView (this ); tv.setText(String.format("目标应用正在运行\n%s" , SECRET)); setContentView(tv); Log.d(TAG, "onCreate: " + SECRET); realBusinessLogic(); } private void realBusinessLogic () { int result = 0 ; for (int i = 0 ; i < 100 ; i++) { result += i; } Log.d(TAG, "business result: " + result); } }
可以明显看出来这是ai写的(bushi
ShellDemo\target_app\app\src\main\res\layout\activity_main.xml修改为
1 2 3 4 <?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" />
3、创建shell_file 创建部分同上
创建ShellDemo\shell_file\app\src\main\java\com\demo\shell_file\ShellApplication.java文件,内容如下 因为代码比较长直接跳过也行,后面会有一个大章节用来分析的^o^/。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 package com.demo.shell_file;import android.app.Application;import android.content.Context;import android.content.res.AssetManager;import android.util.ArrayMap;import android.util.Log;import java.io.*;import java.lang.ref.WeakReference;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.security.Key;import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import dalvik.system.DexClassLoader;public class ShellApplication extends Application { private static final String TAG = "ShellFile" ; private static final byte [] AES_KEY = "ShellDemo1234567" .getBytes(); private static final byte [] AES_IV = "IV_ShellDemo1234" .getBytes(); @Override public void onCreate () { super .onCreate(); Log.d(TAG, "Application onCreate" ); try { Class<?> targetMain = getClassLoader() .loadClass("com.demo.target.MainActivity" ); Log.d(TAG, "找到目标 MainActivity: " + targetMain); } catch (ClassNotFoundException e) { Log.e(TAG, "找不到目标 MainActivity: " + e.getMessage()); } } @Override protected void attachBaseContext (Context base) { super .attachBaseContext(base); Log.d(TAG, "attachBaseContext: 开始落地版解密加载" ); try { int dexCount = getDexCount(base); Log.d(TAG, "dex 数量: " + dexCount); File[] decryptedFiles = decryptAndWriteToDisk(base, dexCount); StringBuilder classPath = new StringBuilder (); for (int i = 0 ; i < decryptedFiles.length; i++) { if (i > 0 ) classPath.append(File.pathSeparator); classPath.append(decryptedFiles[i].getAbsolutePath()); Log.d(TAG, "落地文件: " + decryptedFiles[i].getAbsolutePath()); } File optimizedDir = base.getDir("odex" , MODE_PRIVATE); DexClassLoader newClassLoader = new DexClassLoader ( classPath.toString(), optimizedDir.getAbsolutePath(), null , base.getClassLoader() ); Log.d(TAG, "新 ClassLoader 创建成功: " + newClassLoader); replaceClassLoader(base, newClassLoader); Log.d(TAG, "mClassLoader 替换完成" ); } catch (Exception e) { Log.e(TAG, "壳加载失败: " + e.getMessage(), e); } } private File[] decryptAndWriteToDisk(Context context, int dexCount) throws Exception { File[] files = new File [dexCount]; AssetManager am = context.getAssets(); for (int i = 0 ; i < dexCount; i++) { String assetName = i == 0 ? "encrypted_classes.dex" : "encrypted_classes" + (i + 1 ) + ".dex" ; byte [] encrypted = readAsset(am, assetName); Log.d(TAG, "读取加密 dex: " + assetName + " (" + encrypted.length + " bytes)" ); byte [] decrypted = decrypt(encrypted); Log.d(TAG, "解密完成: " + decrypted.length + " bytes" ); String fileName = i == 0 ? "classes.dex" : "classes" + (i + 1 ) + ".dex" ; File outFile = new File (context.getFilesDir(), fileName); writeFile(outFile, decrypted); files[i] = outFile; Log.d(TAG, "写入磁盘: " + outFile.getAbsolutePath()); } return files; } private byte [] decrypt(byte [] encrypted) throws Exception { Key keySpec = new SecretKeySpec (AES_KEY, "AES" ); IvParameterSpec ivSpec = new IvParameterSpec (AES_IV); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding" ); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); return cipher.doFinal(encrypted); } private byte [] readAsset(AssetManager am, String name) throws Exception { InputStream is = am.open(name); ByteArrayOutputStream bos = new ByteArrayOutputStream (); byte [] buf = new byte [4096 ]; int len; while ((len = is.read(buf)) != -1 ) { bos.write(buf, 0 , len); } is.close(); return bos.toByteArray(); } private void writeFile (File file, byte [] data) throws Exception { FileOutputStream fos = new FileOutputStream (file); fos.write(data); fos.flush(); fos.close(); } private int getDexCount (Context context) { try { byte [] data = readAsset(context.getAssets(), "dex_count" ); return Integer.parseInt(new String (data).trim()); } catch (Exception e) { return 1 ; } } private void replaceClassLoader (Context context, ClassLoader newClassLoader) throws Exception { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread" ); Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread" ); currentActivityThread.setAccessible(true ); Object activityThread = currentActivityThread.invoke(null ); Field mPackagesField = activityThreadClass.getDeclaredField("mPackages" ); mPackagesField.setAccessible(true ); ArrayMap mPackages = (ArrayMap) mPackagesField.get(activityThread); String packageName = context.getPackageName(); WeakReference wr = (WeakReference) mPackages.get(packageName); if (wr == null || wr.get() == null ) { Log.e(TAG, "LoadedApk 为空,跳过替换" ); return ; } Object loadedApk = wr.get(); Class<?> loadedApkClass = Class.forName("android.app.LoadedApk" ); Field mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader" ); mClassLoaderField.setAccessible(true ); Object oldClassLoader = mClassLoaderField.get(loadedApk); Log.d(TAG, "替换前 ClassLoader: " + oldClassLoader); mClassLoaderField.set(loadedApk, newClassLoader); Log.d(TAG, "替换后 ClassLoader: " + newClassLoader); } }
修改AndroidManifest.xml为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?xml version="1.0" encoding="utf-8" ?> <manifest xmlns:android ="http://schemas.android.com/apk/res/android" package ="com.demo.shell_file" > <application android:name =".ShellApplication" android:allowBackup ="true" android:icon ="@mipmap/ic_launcher" android:label ="ShellFile" android:theme ="@style/Theme.ShellFile" > <activity android:name =".MainActivity" android:exported ="false" /> <activity android:name ="com.demo.target.MainActivity" android:exported ="true" > <intent-filter > <action android:name ="android.intent.action.MAIN" /> <category android:name ="android.intent.category.LAUNCHER" /> </intent-filter > </activity > </application > </manifest >
添加pack.py文件,用于将target.apk和shell.apk打包到一起
代码很长,别看,后面也有解析<( ̄︶ ̄)↗[GO!]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 """ 一代壳打包工具 用法: python pack.py <target.apk> <shell.apk> <output.apk> [--memory] """ import subprocessimport osimport sysimport shutilimport zipfileimport argparsefrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padimport hashlibAES_KEY = b'ShellDemo1234567' AES_IV = b'IV_ShellDemo1234' def encrypt_dex (dex_data: bytes ) -> bytes : """AES-CBC 加密 dex 数据""" cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV) encrypted = cipher.encrypt(pad(dex_data, AES.block_size)) print (f" 原始大小: {len (dex_data)} bytes" ) print (f" 加密后: {len (encrypted)} bytes" ) print (f" MD5: {hashlib.md5(dex_data).hexdigest()} " ) return encrypted def get_dex_files (apk_path: str ) -> dict : """从 APK 中提取所有 dex 文件""" dex_files = {} with zipfile.ZipFile(apk_path, 'r' ) as zf: for name in zf.namelist(): if name.endswith('.dex' ): dex_files[name] = zf.read(name) print (f" 找到 dex: {name} ({len (dex_files[name])} bytes)" ) return dex_files def sign_apk (unsigned_apk: str , signed_apk: str ): """使用 debug.keystore 签名 APK""" apksigner = r"Z:\android_sdk\build-tools\36.0.0\apksigner.bat" keystore = r"C:\Users\win11\.android\debug.keystore" cmd = [ apksigner, "sign" , "--ks" , keystore, "--ks-key-alias" , "androiddebugkey" , "--ks-pass" , "pass:android" , "--key-pass" , "pass:android" , "--out" , signed_apk, unsigned_apk ] result = subprocess.run(cmd, capture_output=True , text=True ) if result.returncode == 0 : print (f"[+] 签名成功: {signed_apk} " ) else : print (f"[-] 签名失败: {result.stderr} " ) def pack (target_apk: str , shell_apk: str , output_apk: str , is_memory: bool ): print (f"\n[*] 开始打包" ) print (f" 目标APK: {target_apk} " ) print (f" 壳APK: {shell_apk} " ) print (f" 输出: {output_apk} " ) print (f" 模式: {'不落地(内存加载)' if is_memory else '落地(文件加载)' } " ) print (f"\n[*] 提取目标 dex..." ) dex_files = get_dex_files(target_apk) if not dex_files: print ("[-] 没有找到 dex 文件" ) return print (f"\n[*] 加密 dex..." ) encrypted_dexes = {} for name, data in dex_files.items(): print (f" 加密 {name} :" ) encrypted_dexes[name] = encrypt_dex(data) print (f"\n[*] 重新打包..." ) shutil.copy(shell_apk, output_apk) with zipfile.ZipFile(output_apk, 'a' , compression=zipfile.ZIP_STORED) as zf: for name, encrypted_data in encrypted_dexes.items(): asset_name = f"assets/encrypted_{name} " zf.writestr(asset_name, encrypted_data) print (f" 写入: {asset_name} ({len (encrypted_data)} bytes)" ) zf.writestr("assets/dex_count" , str (len (encrypted_dexes)).encode()) zf.writestr("assets/dex_mode" , b"memory" if is_memory else b"file" ) print (f"\n[+] 打包完成: {output_apk} " ) print (f" 文件大小: {os.path.getsize(output_apk)} bytes" ) signed_apk = output_apk.replace('.apk' , '_signed.apk' ) sign_apk(output_apk, signed_apk) print (f"[+] 最终产物: {signed_apk} " ) if __name__ == '__main__' : parser = argparse.ArgumentParser(description='一代壳打包工具' ) parser.add_argument('target' , help ='目标APK路径' ) parser.add_argument('shell' , help ='壳APK路径' ) parser.add_argument('output' , help ='输出APK路径' ) parser.add_argument('--memory' , action='store_true' , help ='使用不落地模式' ) args = parser.parse_args() pack(args.target, args.shell, args.output, args.memory)
三、原理解析 1、pack.py代码分析 按照个人习惯,先从程序执行的入口开始
(1)main() 1 2 if __name__ == '__main__': 函数的入口,也就是main函数
前面几段跟argument相关的是命令行调用的参数,不太重要,在main函数中主要是调用了pack()。 pack()是pack.py的核心逻辑。
(2)pack() get_dex_files() 首先调用了get_dex_files()提取apk的所有dex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 get_dex_files的作用是将apk以zip 的形式打开,然后提取所有dex文件的文件名,也就是路径。 此处的入参为target_apk,也就是正常的apk,得到的文件列表存储在dex_files变量中。 def get_dex_files (apk_path: str ) -> dict : """从 APK 中提取所有 dex 文件""" dex_files = {} with zipfile.ZipFile(apk_path, 'r' ) as zf: for name in zf.namelist(): if name.endswith('.dex' ): dex_files[name] = zf.read(name) print (f" 找到 dex: {name} ({len (dex_files[name])} bytes)" ) return dex_files
tips:在zip中文件是线性而不是树状结构,通过namelist()获取zip的集中目录,即使不递归查看目录,get_dex_file也会搜索到所有.dex文件。 但是在 Android APK 的标准规范中真正的可执行代码 dex 必须放在 apk 的根目录中,在此处并没有作出筛选,如果有需要可以加上。
encrypt_dex() 然后使用encrypt_dex()对获取的dex文件进行加密
1 2 3 4 5 6 7 8 9 10 加密函数没啥好说的,就是用AES的CBC模式加密,密钥还是固定的,加密的作用就是让破解者在最后的output.apk中只能看到shell的dex,但是找不到target的dex。 def encrypt_dex (dex_data: bytes ) -> bytes : """AES-CBC 加密 dex 数据""" cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV) encrypted = cipher.encrypt(pad(dex_data, AES.block_size)) print (f" 原始大小: {len (dex_data)} bytes" ) print (f" 加密后: {len (encrypted)} bytes" ) print (f" MD5: {hashlib.md5(dex_data).hexdigest()} " ) return encrypted
以壳 APK 为基础,替换 dex,塞入加密数据,这部分没有自定义的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 copy的作用是把shell_apk复制到output_apk中,在shell_apk中我们已经提前新建了一个assets目录。 shutil.copy(shell_apk, output_apk) encrypted_dexes就是加密后的dex文件,每一个encrypted_dex塞入一个assets/encrypted_class...的文件中, 等到之后使用ShellApplication的时候,就从assets中提取加密后的dex文件再解密。 同理,dex_count和dex_mode也是为了方便读取,is_memory用于区别文件落地与文件不落地。 for name, encrypted_data in encrypted_dexes.items(): asset_name = f"assets/encrypted_{name} " zf.writestr(asset_name, encrypted_data) zf.writestr("assets/dex_count" , str (len (encrypted_dexes)).encode()) zf.writestr("assets/dex_mode" , b"memory" if is_memory else b"file" )
sign_apk() 最后一步还要给apk签名,因为output_apk实际上是被修改过的shell_apk,需要签名才能安装。 不然就会报错。
1 2 3 4 报错如下 adb install .\output.apk Performing Streamed Install adb.exe: failed to install .\output.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Scanning Failed.: No signature found in package of version 2 or newer for package com.demo.shell_file]
实际上是调用的apksigner对apk签名,用的是android studio的,需要根据实际路径进行更改。
1 2 3 4 5 6 7 8 9 10 11 12 13 apksigner = r"Z:\android_sdk\build-tools\36.0.0\apksigner.bat" keystore = r"C:\Users\win11\.android\debug.keystore" cmd = [ apksigner, "sign" , "--ks" , keystore, "--ks-key-alias" , "androiddebugkey" , "--ks-pass" , "pass:android" , "--key-pass" , "pass:android" , "--out" , signed_apk, unsigned_apk ] result = subprocess.run(cmd, capture_output=True , text=True )
总结一下,pack.py主要做了一下几步:
(1)把target.apk的dex文件加密
(2)塞到shell.apk的assets文件夹中
(3)给output.apk重新签名
2、ShellApplication.java代码分析 由于target_app实际上就是一个普通的示例demo,也没什么用,就不分析了。
从入口开始,ShellApplication.java最开始执行的入口点在attachBaseContext(),至于为什么,详情请见[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解 ,大佬写得很清楚,但是我快啃晕了。(文末也会列出参考链接的)。
搜索android源码可以使用https://cs.android.com/
(1)attachBaseContext() super.attachBaseContext() 首先调用了super.attachBaseContext(base)。 super并不是ShellApplication的父类Application,而是Application的父类ContextWrapper。 Application并没有override这个函数。 进入ContextWrapper之后可以发现代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ContextWrapper extends Context { @UnsupportedAppUsage Context mBase; public ContextWrapper (Context base) { mBase = base; } ... protected void attachBaseContext (Context base) { if (mBase != null ) { throw new IllegalStateException ("Base context already set" ); } mBase = base; } ... }
那么base是从何而来呢,在ContextWrapper.java中可以发现ContextWrapper有一个有参构造函数,在Application.java的构造函数中,调用了super(null),此处的mBase为null。
1 2 3 4 5 public Application () { super (null ); }
想要知道base是从哪来的。还需要搞清一个问题,那就是谁调用了attachBaseContext()函数。
1 2 3 4 5 6 final void attach (Context context) { attachBaseContext(context); mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; }
其实是Application.attach()调用了attachBaseContext(),但是到这里其实还没办法解决疑惑(为什么调用Application.attach()中的attachBaseContext()反而会调用ShellApplication.attachBaseContext()呢)
再向上追溯
1 2 3 4 5 6 7 8 9 10 public Application newApplication (ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Application app = getFactory(context.getPackageName()) .instantiateApplication(cl, className); app.attach(context); return app; }
可以看到这里的app的类型是Application,但是实际上是根据AndroidManifest.xml中的application的android:name生成。 因此实际上的app是ShellApplication,但是被转换为了父类Application。在调用attach()时,由于ShellApplication没有attach(),调用了Application.attach(),但是在调用attachBaseContext时,又有override的ShellApplication.attachBaseContext。
到这里解决了attachBaseContext的调用问题,但是仍然不知道base是从何而来。
1 2 3 4 5 6 7 8 9 目前已知的调用链如下: Instrumentation.newApplication(cl,className,context) -> Application() -> ContextWrapper(null) -> Application.attach(context) -> ShellApplication.attachBaseContext(context)→ super.attachBaseContext(context) → ContextWrapper.attachBaseContext()→ mBase = base
再继续往上追溯,来到LoadedApk.java中
1 2 3 4 5 6 7 8 9 10 11 12 13 private Application makeApplicationInner (boolean forceDefaultAppClass, Instrumentation instrumentation, boolean allowDuplicateInstances) { ... ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this ); NetworkSecurityConfigProvider.handleNewApplication(appContext); app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext); appContext.setOuterContext(app); ... }
可以看到Instrumentation.newApplication(),mInstrumentation的类型就是Instrumentation,appContext也就是context,由ContextImpl.createAppContext()函数生成。至此,base的来源也清楚了。
其实这些在前面提到的大佬的文章也有写。
总结一下,调用链如下
1 2 3 4 5 6 7 8 9 10 11 # framework层 LoadedApk.makeApplicationInner(forceDefaultAppClass,instrumentation,allowDuplicateInstances) -> Instrumentation.newApplication(cl,className,context) -> Application() -> ContextWrapper(null) -> Application.attach(context) -> # APP层 ShellApplication.attachBaseContext(context)→ super.attachBaseContext(context) → ContextWrapper.attachBaseContext()→ mBase = base
getDexCount() 解决了前面的其实后面的基本都比较简单,可以说是先苦后甜了。
已知pack.py实际上的操作是将target.apk的加密dex文件塞到了shell.apk中,那么在ShellApplication中就需要解密。
1 int dexCount = getDexCount(base);
读取了dex_count文件中的内容,也就是dex文件的数量。
decryptAndWriteToDisk() 核心逻辑就是把解密出来的dex文件写入私有目录。私有目录由context.getFilesDir() 提供,它返回的一个 File 对象,指向App 私有数据目录下的 files文件夹。(一般只有本app可读,非root情况下) 由于代码中也有注释,这里就不多赘述了。
需要注意的是这里还对dex文件的命名也进行了复原,并且返回了解密后的文件路径,用于之后加载。
构建classpath和ClassLoader classpath就是用分隔符分隔开的绝对路径。 分隔符由File.pathSeparator生成,为了保证多系统下的兼容性,使用File.pathSeparator自动获取。
接下来创建一个新的classLoader用于加载解密后的dex文件。
1 2 3 4 5 6 7 File optimizedDir = base.getDir("odex", MODE_PRIVATE); DexClassLoader newClassLoader = new DexClassLoader( classPath.toString(), optimizedDir.getAbsolutePath(), null, base.getClassLoader() // 父加载器是原 PathClassLoader );
odex其实没什么用,高版本好像把这个取消了,这里为了兼容性可能才加了这个。
为什么要创建一个新的classLoader?
系统自带的PathClassLoader没有办法读取解密后的dex文件,这是运行过程中动态加载的,因此需要用一个新的classLoader来加载这些文件,或者将dex目录塞入原classLoader的pathList中(这个叫dex插桩,在热修复或者打补丁中比较常用),这样原来的pathClassLorder才能够成功识别。 可以尝试以下不换classLoader,大概率会报错:java.lang.ClassNotFoundException。
replaceClassLoader() 之前已经创建了新的classLoader,现在该替换掉原先的了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private void replaceClassLoader (Context context, ClassLoader newClassLoader) throws Exception { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread" ); Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread" ); currentActivityThread.setAccessible(true ); Object activityThread = currentActivityThread.invoke(null ); Field mPackagesField = activityThreadClass.getDeclaredField("mPackages" ); mPackagesField.setAccessible(true ); ArrayMap mPackages = (ArrayMap) mPackagesField.get(activityThread); String packageName = context.getPackageName(); WeakReference wr = (WeakReference) mPackages.get(packageName); if (wr == null || wr.get() == null ) { Log.e(TAG, "LoadedApk 为空,跳过替换" ); return ; } Object loadedApk = wr.get(); Class<?> loadedApkClass = Class.forName("android.app.LoadedApk" ); Field mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader" ); mClassLoaderField.setAccessible(true ); Object oldClassLoader = mClassLoaderField.get(loadedApk); Log.d(TAG, "替换前 ClassLoader: " + oldClassLoader); mClassLoaderField.set(loadedApk, newClassLoader); Log.d(TAG, "替换后 ClassLoader: " + newClassLoader); }
总结一下上述代码的作用 (1)通过反射获取了一个ActivityThread实例 ActivityThread 是 Android 应用进程的入口点和管理者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { private static volatile ActivityThread sCurrentActivityThread; public static void main (String[] args) { ... ActivityThread thread = new ActivityThread (); thread.attach(false , startSeq); ... } ... @UnsupportedAppUsage private void attach (boolean system, long startSeq) { sCurrentActivityThread = this ; ... } ... @UnsupportedAppUsage @RavenwoodKeep public static ActivityThread currentActivityThread () { return sCurrentActivityThread; } ... }
(2)获取 mPackages 字段 mPackages是ActivityThread的一个字段,类型为ArrayMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { ... final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap <>(); ... private LoadedApk getPackageInfo (ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage, boolean isSdkSandbox) { ... packageInfo = new LoadedApk (this , aInfo, compatInfo, baseLoader,securityViolation, includeCode&& (aInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0 , registerPackage); ... if (differentUser || isSdkSandbox) { } else if (includeCode) { mPackages.put(aInfo.packageName, new WeakReference <LoadedApk>(packageInfo)); } else { ... } ... } } public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { @UnsupportedAppUsage private void handleBindApplication (AppBindData data) { data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null , false , true , false , isSdkSandbox); try { app = data.info.makeApplicationInner(data.restrictedBackupMode, null ); } } class H extends Handler { public static final int BIND_APPLICATION = 110 ; public void handleMessage (Message msg) { switch (msg.what) { case BIND_APPLICATION: AppBindData data = (AppBindData)msg.obj; handleBindApplication(data); } } } final H mH = new H (); private void sendMessage (int what, Object obj, int arg1, int arg2, boolean async) { if (DEBUG_MESSAGES) { Slog.v(TAG,"SCHEDULE " + what + " " + mH.codeToString(what) + ": " + arg1 + " / " + obj); } Message msg = Message.obtain(); msg.what = what; msg.obj = obj; msg.arg1 = arg1; msg.arg2 = arg2; if (async) { msg.setAsynchronous(true ); } mH.sendMessage(msg); } private class ApplicationThread extends IApplicationThread .Stub { @Override public final void bindApplication (参数太多了,省略了) { AppBindData data = new AppBindData (); sendMessage(H.BIND_APPLICATION, data); } } public class ActivityManagerService extends IActivityManager .Stub implements Watchdog .Monitor, BatteryStatsImpl.BatteryCallback, ActivityManagerGlobalLock { ProcessRecord app; ... if (pid != MY_PID && pid >= 0 ) { synchronized (mPidsSelfLocked) { app = mPidsSelfLocked.get(pid); } } ... @GuardedBy("this") private void attachApplicationLocked (@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { if (app.getIsolatedEntryPoint() != null ) { ... } else { ... thread.bindApplication(processName,appInfo, app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,......) } } @Override public final void attachApplication (IApplicationThread thread, long startSeq) { if (thread == null ) { throw new SecurityException ("Invalid application interface" ); } synchronized (this ) { int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); attachApplicationLocked(thread, callingPid, callingUid, startSeq); Binder.restoreCallingIdentity(origId); } } }
(3)获取 LoadedApk 对象 WeakReference是Java提供的一种弱引用对象,用于引用一个对象。 已知mPackages存储的是LoadedApk的弱引用对象,那么通过WeakReference就能获取LoadedApk了。
1 2 3 4 5 6 7 String packageName = context.getPackageName(); WeakReference wr = (WeakReference) mPackages.get(packageName); if (wr == null || wr.get() == null) { Log.e(TAG, "LoadedApk 为空,跳过替换"); return; } Object loadedApk = wr.get();
需要注意的是WeakReference引用的对象可能已经被gc回收了。
(4)替换mClassLoader mClassLoader是LoadedApk的一个属性。
1 2 3 Class<?> loadedApkClass = Class.forName("android.app.LoadedApk"); Field mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true);
替换后的mClassLoader实际上是能够读取target_app的dex文件的classLoader,因而能够加载出target_app的内容。
(2)OnCreate() 1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void onCreate () { super .onCreate(); Log.d(TAG, "Application onCreate" ); try { Class<?> targetMain = getClassLoader().loadClass("com.demo.target.MainActivity" ); Log.d(TAG, "找到目标 MainActivity: " + targetMain); } catch (ClassNotFoundException e) { Log.e(TAG, "找不到目标 MainActivity: " + e.getMessage()); } }
ShellApplication.Create()调用链分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void onCreate () {} public void callApplicationOnCreate (Application app) { app.onCreate(); } private Application makeApplicationInner (boolean forceDefaultAppClass, Instrumentation instrumentation, boolean allowDuplicateInstances) { app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext); if (instrumentation != null ) { try { instrumentation.callApplicationOnCreate(app); } }
到这里可能就有细心的朋友发现了,实际上newApplication执行后最后调用了ShellApplication.attachBaseContext() (闭环了)
然后在同一个函数的下一段代码,执行了callApplicationOnCreate() -> app.onCreate()。 由上文可知,newApplication得到的其实是一个ShellApplication,根据多态的原则,实际上调用的是ShellApplication.OnCreate()(Application的OnCreate()是一个空函数)
(3)ActivityThread启动流程 将之前的内容贯通起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ActivityManagerService.attachApplication() -> ActivityManagerService.attachApplicationLocked() -> ApplicationThread.bindApplication() -> ActivityThread.sendMessage() --> handleMessage收到信息 H.handleMessage() -> ActivityThread.handleBindApplication() -> ActivityThread.getPackageInfo(); LoadedApk.makeApplicationInner() -> Instrumentation.newApplication();Instrumentation.callApplicationOnCreate() -> newApplication()和callApplicationOnCreate()是先后顺序执行 分支一: Instrumentation.newApplication() -> Application() -> ContextWrapper(null) -> Application.attach() -> ShellApplication.attachBaseContext() 分支二: Instrumentation.callApplicationOnCreate(app) -> app.onCreate() -> ShellApplication.Create()
3、MainActivity的执行 解决了Application的执行,接下来是Activity的执行。 还是从程序的入口开始,MainActivity.OnCreate()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 final void performCreate (Bundle icicle) { performCreate(icicle, null ); } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) final void performCreate (Bundle icicle, PersistableBundle persistentState) { if (persistentState != null ) { onCreate(icicle, persistentState); } else { onCreate(icicle); } } public Activity newActivity (ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException,ClassNotFoundException { String pkg = intent != null && intent.getComponent() != null ? intent.getComponent().getPackageName() : null ; return getFactory(pkg).instantiateActivity(cl, className, intent); } public void callActivityOnCreate (Activity activity, Bundle icicle) { prePerformCreate(activity); activity.performCreate(icicle); postPerformCreate(activity); } @Override public ClassLoader getClassLoader () { return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader()); } private Activity performLaunchActivity (ActivityClientRecord r, Intent customIntent) { Activity activity = null ; if (isSandboxedSdkContextUsed) { cl = activityBaseContext.getApplicationContext().getClassLoader(); } else { cl = activityBaseContext.getClassLoader(); } activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); if (r.isPersistable()) { mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); } else { mInstrumentation.callActivityOnCreate(activity, r.state); } }
调用链如下: ActivityThread.performLaunchActivity() -> Instrumentation.callActivityOnCreate() -> Activity.performCreate() -> MainActicity.onCreate()
有几个需要注意的点: (1)在ActivityThread.performLaunchActivity()中调用ContextImpl.getClassLoader()获得cl,而getClassLoader()实际上取的是mClassLoader,也就是前面修改的dexClassLoader。 (2)在ActivityThread.performLaunchActivity()中,Instrumentation.newActivity()虽然声明的返回类型是Activity,但是实际上是MainActivity,因此在调用onCreate()时执行了MainActicity.onCreate()。
4、dex加载过程 1 2 3 4 5 6 DexClassLoader newClassLoader = new DexClassLoader( classPath.toString(), optimizedDir.getAbsolutePath(), null, base.getClassLoader() // 父加载器是原 PathClassLoader );
在DexClassLoader生成的时候,大部分情况下dex就已经加载到内存中了。 由于分支较多,只写了适合本demo的调用链。
dex加载的调用链如下: DexClassLoader() -> BaseDexClassLoader() -> new DexPathList() -> DexPathList.makeDexElements() -> DexPathList.loadDexFile() -> new DexFile() -> DexFile.openDexFile() -> DexFile.openDexFileNative() -> DexFile_openDexFileNative() -> OatFileManager::OpenDexFilesFromOat() -> DexFileLoader::Open() -> DexFileLoader::OpenCommon()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader (String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super (dexPath, null , librarySearchPath, parent); } } public class BaseDexClassLoader extends ClassLoader { public BaseDexClassLoader (String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { this (dexPath, librarySearchPath, parent, null , null , false ); } public BaseDexClassLoader (String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,ClassLoader[] sharedLibraryLoadersAfter, boolean isTrusted) { super (parent); ... this .pathList = new DexPathList (this , dexPath, librarySearchPath, null , isTrusted); ... } } public final class DexPathList { DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { ... this .dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); ... } private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) { dex = loadDexFile(file, optimizedDirectory, loader, elements); } @UnsupportedAppUsage private static DexFile loadDexFile (File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { ... if (optimizedDirectory == null ) { return new DexFile (file, loader, elements); } ... } } public final class DexFile { DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException { mCookie = openDexFile(fileName, null , 0 , loader, elements); ... } private static Object openDexFile (String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException { return openDexFileNative(new File (sourceName).getAbsolutePath(),(outputName == null ) ? null : new File (outputName).getAbsolutePath(),flags,loader,elements); } @UnsupportedAppUsage private static native Object openDexFileNative (String sourceName, String outputName, int flags,ClassLoader loader, DexPathList.Element[] elements) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 static JNINativeMethod gMethods[] = { ... NATIVE_METHOD(DexFile, openDexFileNative, "(Ljava/lang/String;" "Ljava/lang/String;" "I" "Ljava/lang/ClassLoader;" "[Ldalvik/system/DexPathList$Element;" ")Ljava/lang/Object;" ), ... } static jobject DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, [[maybe_unused]] jstring javaOutputName, [[maybe_unused]] jint flags, jobject class_loader, jobjectArray dex_elements) { ... std ::vector <std ::unique_ptr <const DexFile>> dex_files = Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(), class_loader, dex_elements, &oat_file, &error_msgs); ... } std ::vector <std ::unique_ptr <const DexFile>> OatFileManager::OpenDexFilesFromOat( const char * dex_location, jobject class_loader, jobjectArray dex_elements, const OatFile** out_oat_file, std ::vector <std ::string >* error_msgs) { ... if (dex_files.empty()) { if (!dex_file_loader.Open(Runtime::Current()->IsVerificationEnabled(),kVerifyChecksum, &error_msg,&dex_files)) { } } return dex_files; ... } bool DexFileLoader::Open (bool verify, bool verify_checksum, bool allow_no_dex_files, DexFileLoaderErrorCode* error_code, std ::string * error_msg, std ::vector <std ::unique_ptr <const DexFile>>* dex_files) { ... std ::unique_ptr <const DexFile> dex_file = OpenCommon(root_container_, root_container_->Begin() + header_offset, root_container_->Size() - header_offset, multidex_location, {}, nullptr, verify, verify_checksum, error_msg, error_code); ... } std ::unique_ptr <DexFile> DexFileLoader::OpenCommon (std ::shared_ptr <DexFileContainer> container, const uint8_t * base, size_t app_compat_size, const std ::string & location, std ::optional<uint32_t > location_checksum, const OatDexFile* oat_dex_file, bool verify, bool verify_checksum, std ::string * error_msg, DexFileLoaderErrorCode* error_code) { std ::unique_ptr <DexFile> dex_file; auto header = reinterpret_cast<const DexFile::Header*>(base); if (size >= sizeof (StandardDexFile::Header) && StandardDexFile::IsMagicValid(base)) { uint32_t checksum = location_checksum.value_or(header->checksum_); dex_file.reset(new StandardDexFile(base, location, checksum, oat_dex_file, container)); } ... }
四、脱壳 力竭了,偷点懒,搬运一下大佬写的吧。大概写了两天X﹏X。
我要是写毕设有这么努力就好了(bushi
本来应该还有个不落地版的shell,但是感觉再多坐会要似了。
[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解 第五点的脱壳技巧归纳
五、参考链接 [原创]Android加壳脱壳学习(1)——动态加载和类加载机制详解
[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解
Android系统架构与系统源码目录
Android系统启动流程(一)解析init进程启动过程 (经典csdn自动收费这一块,路边也够)
Android系统启动流程(二)解析Zygote进程启动过程
Android系统启动流程(三)解析SyetemServer进程启动过程 (同上)
Android系统启动流程(四)Launcher启动过程与系统启动流程
拒绝“裸奔”!带你手搓一个 Android 加壳器(附源码解析)