├── README.md └── dexDump.js /README.md: -------------------------------------------------------------------------------- 1 | # fridroid-unpacker 2 | 3 | Defeat Java packers via Frida instrumentation 4 | 5 | 6 | Description 7 | ----------- 8 | Use the method `OpenMemory` or `OpenCommon` (after Android N) in `libart.so`/`libdexfile.so` to get the address of the dex in memory, calculate the size of the dex file, and dump the dex from memory. 9 | 10 | Usage 11 | ----- 12 | 13 | ```sh 14 | $ frida -U -f com.package.target -l dexDump.js --no-pause 15 | ``` 16 | 17 | References 18 | ---------- 19 | - https://www.frida.re/docs/home/ 20 | - frida-unpack (dstmath) https://github.com/dstmath/frida-unpack 21 | - Frida-Android-unpack (xiaokanghub) https://github.com/xiaokanghub/Frida-Android-unpack 22 | 23 | Supported OS: Android 4.4 - Android 11 24 | 25 | Tested Packers 26 | --------------- 27 | 28 | - Jiagu 29 | - DexProtector 30 | - DexGuard 31 | - Yidun 32 | - Tencent Legu 33 | - Mobile Tencent Protect 34 | -------------------------------------------------------------------------------- /dexDump.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Author: guoqiangck & enovella 5 | * Created: 2019/6/11 6 | * Dump dex file for packed apks 7 | * Hook art/runtime/dex_file.cc OpenMemory or OpenCommon 8 | * Support Version: Android 4.4 up to Android 11.0 9 | */ 10 | 11 | /* Read a C++ std string (basic_string) to a nomal string */ 12 | function readStdString(ptr_str) { 13 | const isTiny = (ptr_str.readU8() & 1) === 0; 14 | if (isTiny) { 15 | return ptr_str.add(1).readUtf8String(); 16 | } 17 | 18 | return ptr_str.add(2 * Process.pointerSize).readPointer().readUtf8String(); 19 | } 20 | 21 | 22 | function logPrint(log) { 23 | var theDate = new Date(); 24 | var hour = theDate.getHours(); 25 | var minute = theDate.getMinutes(); 26 | var second = theDate.getSeconds(); 27 | var mSecond = theDate.getMilliseconds() 28 | 29 | hour < 10 ? hour = "0" + hour : hour; 30 | minute < 10 ? minute = "0" + minute : minute; 31 | second < 10 ? second = "0" + second : second; 32 | mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond; 33 | 34 | var time = hour + ":" + minute + ":" + second + ":" + mSecond; 35 | console.log("[" + time + "] " + log); 36 | } 37 | 38 | function getAndroidVersion(){ 39 | var version = 0; 40 | 41 | if(Java.available){ 42 | var version = parseInt(Java.androidVersion); 43 | }else{ 44 | logPrint("Error: cannot get android version"); 45 | } 46 | logPrint("[*] Android version: " + version); 47 | 48 | return version; 49 | } 50 | 51 | function getFunctionName(){ 52 | var i = 0; 53 | var functionName = ""; 54 | 55 | // Android 4: hook dvmDexFileOpenPartial 56 | // Android 5: hook OpenMemory 57 | // after Android 5: hook OpenCommon 58 | if (g_AndroidOSVersion > 4){ // android 5 and later version 59 | // OpenCommon is in libdexfile.so in android 10 and later 60 | var soName = g_AndroidOSVersion >= 10 ? "libdexfile.so" : "libart.so"; 61 | var artExports = Module.enumerateExportsSync(soName); 62 | for(i = 0; i< artExports.length; i++){ 63 | if(artExports[i].name.indexOf("OpenMemory") !== -1){ 64 | functionName = artExports[i].name; 65 | logPrint("[*] Export index: " + i + " -> "+ functionName); 66 | break; 67 | }else if(artExports[i].name.indexOf("OpenCommon") !== -1){ 68 | if (g_AndroidOSVersion >= 10 && artExports[i].name.indexOf("ArtDexFileLoader") !== -1) 69 | continue; 70 | functionName = artExports[i].name; 71 | logPrint("[*] Export index: " + i + " -> "+ functionName); 72 | break; 73 | } 74 | } 75 | }else{ //android 4 76 | var dvmExports = Module.enumerateExportsSync("libdvm.so"); 77 | if (dvmExports.length !== 0) { 78 | for(i = 0; i< dvmExports.length; i++){ 79 | if(dvmExports[i].name.indexOf("dexFileParse") !== -1){ 80 | functionName = dvmExports[i].name; 81 | logPrint("[*] Export index: " + i + " -> "+ functionName); 82 | break; 83 | } 84 | } 85 | }else { 86 | dvmExports = Module.enumerateExportsSync("libart.so"); 87 | for(i = 0; i< dvmExports.length; i++){ 88 | if(dvmExports[i].name.indexOf("OpenMemory") !== -1){ 89 | functionName = dvmExports[i].name; 90 | logPrint("[*] Export index: " + i + " -> "+ functionName); 91 | break; 92 | } 93 | } 94 | } 95 | } 96 | return functionName; 97 | } 98 | 99 | function getg_processName(){ 100 | var g_processName = ""; 101 | 102 | var fopenPtr = Module.findExportByName("libc.so", "fopen"); 103 | var fgetsPtr = Module.findExportByName("libc.so", "fgets"); 104 | var fclosePtr = Module.findExportByName("libc.so", "fclose"); 105 | 106 | var fopenFunc = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']); 107 | var fgetsFunc = new NativeFunction(fgetsPtr, 'int', ['pointer', 'int', 'pointer']); 108 | var fcloseFunc = new NativeFunction(fclosePtr, 'int', ['pointer']); 109 | 110 | var pathPtr = Memory.allocUtf8String("/proc/self/cmdline"); 111 | var openFlagsPtr = Memory.allocUtf8String("r"); 112 | 113 | var fp = fopenFunc(pathPtr, openFlagsPtr); 114 | if(fp.isNull() === false){ 115 | var buffData = Memory.alloc(128); 116 | var ret = fgetsFunc(buffData, 128, fp); 117 | if(ret !== 0){ 118 | g_processName = Memory.readCString(buffData); 119 | logPrint("[*] ProcessName: " + g_processName); 120 | } 121 | fcloseFunc(fp); 122 | } 123 | return g_processName; 124 | } 125 | 126 | function arraybuffer2hexstr(buffer) 127 | { 128 | var hexArr = Array.prototype.map.call( 129 | new Uint8Array(buffer), 130 | function (bit) { 131 | return ('00' + bit.toString(16)).slice(-2) 132 | } 133 | ); 134 | return hexArr.join(' '); 135 | } 136 | 137 | function checkMagic(dataAddr) { // Throws access violation errors, not handled at all. 138 | let dexMagic = 'dex\n'; // [0x64, 0x65, 0x78, 0x0a] 139 | let dexVersions = ['035', '037', '038', '039', '040']; // Same as above (hex -> ascii) 140 | let odexVersions = ['036']; 141 | let kDexMagic = 'cdex'; // [0x63, 0x64, 0x65, 0x78] 142 | let kDexVersions = ['001']; 143 | let magicTrailing = 0x00; 144 | 145 | let readData 146 | try { 147 | readData = ptr(dataAddr).readByteArray(8) 148 | } catch (e) { 149 | logPrint('Error reading memory at address' + dataAddr); 150 | return {found: false, wrongMagic: 0xDEADBEEF}; 151 | } 152 | let magic = Array.from( new Uint8Array( readData ) ); 153 | 154 | let foundStart = magic.slice(0,4).map(i => String.fromCharCode(i)).join(''); 155 | let foundVersion = magic.slice(4,7).map(i => String.fromCharCode(i)).join(''); 156 | let foundMagicString = foundStart.replace('\n', '') + foundVersion; // Printable string 157 | 158 | if (foundStart === dexMagic && dexVersions.includes(foundVersion) && magic[7] === magicTrailing) { 159 | // Found a dex 160 | return {found: true, ext: 'dex', sizeOffset: 0x20, magicString: foundMagicString}; 161 | } else if (foundStart === dexMagic && odexVersions.includes(foundVersion) && magic[7] === magicTrailing) { 162 | // Found an odex (only version number differs, same magic) 163 | return {found: true, ext: 'odex', sizeOffset: 0x1C, magicString: foundMagicString}; 164 | } else if (foundStart === kDexMagic && kDexVersions.includes(foundVersion) && magic[7] === magicTrailing) { 165 | // Found a compact dex 166 | return {found: true, ext: 'cdex', sizeOffset: 0x20, magicString: foundMagicString}; 167 | } else { 168 | return {found: false, wrongMagic: magic}; 169 | } 170 | } 171 | 172 | function dumpDexToFile(begin, dexInfo, processName, location) { 173 | let dexSize = ptr(begin).add(dexInfo.sizeOffset).readInt(); 174 | let dexPath = "/data/data/" + processName + "/" + dexSize + "." + dexInfo.ext; 175 | var dexFile = new File(dexPath, "wb"); 176 | 177 | dexFile.write(ptr(begin).readByteArray(dexSize)); 178 | dexFile.flush(); 179 | dexFile.close(); 180 | 181 | logPrint("magic : " + dexInfo.magicString); 182 | logPrint("size : " + dexSize); 183 | logPrint("orig location: " + location); 184 | logPrint("dumped " + dexInfo.ext + " @ " + dexPath + "\n"); 185 | } 186 | 187 | function dumpDex(moduleFuncName, g_processName){ 188 | if (moduleFuncName == "") { 189 | logPrint("Error: cannot find correct module function."); 190 | return; 191 | } 192 | 193 | var hookFunction; 194 | if (g_AndroidOSVersion > 4) { 195 | hookFunction = Module.findExportByName("libart.so", moduleFuncName); 196 | } else { 197 | hookFunction = Module.findExportByName("libdvm.so", moduleFuncName); 198 | if(hookFunction == null) { 199 | hookFunction = Module.findExportByName("libart.so", moduleFuncName); 200 | } 201 | } 202 | 203 | Interceptor.attach(hookFunction,{ 204 | onEnter: function(args){ 205 | let begin, dexInfo, location; 206 | 207 | dexInfo = checkMagic(args[0]); 208 | begin = args[0]; 209 | if (!dexInfo.found) { 210 | wrongMagic0 = dexInfo.wrongMagic 211 | dexInfo = checkMagic(args[1]); 212 | begin = args[1]; 213 | } 214 | if (!dexInfo.found) { 215 | throw new Error( 216 | 'Could not identify magic, found invalid values ' + 217 | wrongMagic0.map(i => i.toString(16).padStart(2, '0')).join('') + 218 | ' ' + 219 | dexInfo.wrongMagic.map(i => i.toString(16).padStart(2, '0')).join('') 220 | ) 221 | } 222 | 223 | for (let i = 0; i < 10; i++) { 224 | // Try all parameters 225 | try { 226 | location = readStdString(ptr(args[i])); 227 | } catch {} // Illegal memory access 228 | if (location != null && location.length > 0 && location.includes('/')) { 229 | // != null catches both undefined and null 230 | break; 231 | } 232 | } 233 | 234 | dumpDexToFile(begin, dexInfo, g_processName, location); 235 | }, 236 | }); 237 | } 238 | 239 | // Main code 240 | var g_AndroidOSVersion = getAndroidVersion(); 241 | var g_moduleFunctionName = getFunctionName(); 242 | var g_processName = getg_processName(); 243 | 244 | if(g_moduleFunctionName !== "" && g_processName !== ""){ 245 | dumpDex(g_moduleFunctionName, g_processName); 246 | } 247 | --------------------------------------------------------------------------------