換一個帥一點姿勢實現DexHunter

Editor發表於2018-03-28

DexHunter 需要刷機,感覺太麻煩了,所以用xposed弄了一份。然後大部分解析邏輯都放到了java層,這樣也有另一個好處,就是殼總不能修改java正常的api。所以 define class 之類的都可以直接呼叫 java 層的classloader 來初始化 class。


注意:目前我只在dalvik上面實現,art還沒有看。


1. 尋找兩個dalvik虛擬機器函式,dvmDecodeIndirectRef 和dvmThreadSelf


/**
* 函式名稱表根據4.4的Android版本設定的,不同Android版本對映可能存在差異,可以直接用ida檢視維護
*/
void initDvmFunctionTables() {
   void *libVMhandle = dlopen("libdvm.so", RTLD_GLOBAL | RTLD_LAZY);

   initDvmFunctionItem("_Z20dvmDecodeIndirectRefP6ThreadP8_jobject",
                       (void **) (&dvmFunctionTables.dvmDecodeIndirectRef), libVMhandle);
   initDvmFunctionItem("_Z13dvmThreadSelfv", (void **) (&dvmFunctionTables.dvmThreadSelf),
                       libVMhandle);


   dlclose(libVMhandle);
}



2. 定位dex檔案


其實我現在還不知道為啥dump記憶體需要那麼麻煩,直接就能在記憶體中找到啊。看程式碼


extern "C"
JNIEXPORT jobject JNICALL
Java_com_virjar_xposedhooktool_unshell_Dumper_originDex(JNIEnv *env, jclass type,
                                                       jclass loader) {
   //TODO check & throw exception
   ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(
           dvmFunctionTables.dvmThreadSelf(),
           loader);
   DvmDex *dvm_dex = clazz->pDvmDex;
   return env->NewDirectByteBuffer(dvm_dex->memMap.addr, dvm_dex->memMap.length);
}


這不就直接找到dex檔案了嘛。



3. 建立dexFile模型,使用baksmaliAPI


private static DexBackedDexFile createMemoryDexFile(Class loader) {
       ByteBuffer byteBuffer = originDex(loader);
       byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
       byte[] buffer = new byte[byteBuffer.capacity()];
       byteBuffer.get(buffer, 0, byteBuffer.capacity());

       if (HeaderItem.verifyMagic(buffer, 0)) {
           return new DexBackedDexFile(Opcodes.forApi(apiLevel()), buffer);
           //a normal dex file
       }
       if (OdexHeaderItem.verifyMagic(buffer, 0)) {
           //this is a odex file
           try {
               ByteArrayInputStream is = new ByteArrayInputStream(buffer);
               DexUtil.verifyOdexHeader(is);
               is.reset();
               byte[] odexBuf = new byte[OdexHeaderItem.ITEM_SIZE];
               ByteStreams.readFully(is, odexBuf);
               int dexOffset = OdexHeaderItem.getDexOffset(odexBuf);
               if (dexOffset > OdexHeaderItem.ITEM_SIZE) {
                   ByteStreams.skipFully(is, dexOffset - OdexHeaderItem.ITEM_SIZE);
               }
               return new DexBackedOdexFile(Opcodes.forApi(Dumper.apiLevel()), odexBuf, ByteStreams.toByteArray(is));
           } catch (IOException e) {
               //while not happen
               throw new RuntimeException(e);
           }
       }
       throw new IllegalStateException("can not find out dex image in vm memory");
   }



4. 使用smali APi rewrite功能,重構Method的指令資料


                   @Nonnull
                   @Override
                   public Method rewrite(@Nonnull final Method value) {
                       if (!(value instanceof DexBackedMethod)) {
                           return super.rewrite(value);
                       }

                       Class<?> definingClass;
                       try {
                           definingClass = classLoader.loadClass(value.getDefiningClass());
                       } catch (ClassNotFoundException e) {
                           return super.rewrite(value);
                       }
                       if (definingClass.getClassLoader() != classLoader) {
                           return super.rewrite(value);
                       }

                       final Class<?> searchClass = definingClass;
                       final String methodDescriptor = ReferenceUtil.getMethodDescriptor(value);
                       //覆蓋 getImplementation
                       return new RewrittenMethod(value) {

                           @Nullable
                           @Override
                           public MethodImplementation getImplementation() {
                               ByteBuffer byteBuffer = methodDataWithDescriptor(methodDescriptor, value.getName(), searchClass);
                               if (byteBuffer == null) {
                                   return super.getImplementation();
                               }
                               byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
                               byte[] buffer = new byte[byteBuffer.capacity()];
                               byteBuffer.get(buffer, 0, byteBuffer.capacity());
                               DexBackedMethodImplementation dexBackedMethodImplementation = new DexBackedMethodImplementation(new MethodSegmentDexFile(buffer, dexFile), (DexBackedMethod) value, 0);
                               return rewriters.getMethodImplementationRewriter().rewrite(dexBackedMethodImplementation);
                           }

                           @Override
                           public int getAccessFlags() {
                               int accessFlags = getMethodAccessFlagsWithDescriptor(methodDescriptor, value.getName(), searchClass);
                               if (accessFlags < 0) {
                                   //證明沒有找到這個方法
                                   return super.getAccessFlags();
                               }
                               return accessFlags;
                           }
                       };
                   }



5. 使用 dalvik dvmFindXXXMethodByDescriptor 功能


尋找對應的 method 物件,進而扣出真實的指令,這裡指令長度計算,借用了 DexHunter 的程式碼


extern "C"
JNIEXPORT jobject JNICALL
Java_com_virjar_xposedhooktool_unshell_Dumper_methodDataWithDescriptor(JNIEnv *env, jclass type,
                                                                      jstring methodDescriptor_,
                                                                      jstring methodName_,
                                                                      jclass searchClass) {
   const char *methodDescriptor = env->GetStringUTFChars(methodDescriptor_, 0);
   const char *methodName = env->GetStringUTFChars(methodName_, 0);
   ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(
           dvmFunctionTables.dvmThreadSelf(),
           searchClass);

   jobject ret = NULL;
   Method *method = dvmFindDirectMethodByDescriptor(clazz, methodName, methodDescriptor);
   if (method == NULL) {
       method = dvmFindVirtualMethodByDescriptor(clazz, methodName, methodDescriptor);
   }
   if (method == NULL) {
       goto tail;
   }

   //check for native
   uint32_t ac = (method->accessFlags) & accessFlagsMask;
   if (method->insns == NULL || ac & ACC_NATIVE) {
       goto tail;
   }

   //why 16
   // 2 byte for registersSize
   // 2 byte for insSize
   // 2 byte for outsSize
   // 2 byte for triesSize
   // 4 byte for debugInfoOff
   // 4 byte for insnsSize
   // and then ,the insns address
   DexCode *code = (DexCode *) ((const u1 *) method->insns - 16);
   uint8_t *item = (uint8_t *) code;
   int code_item_len = 0;
   if (code->triesSize) {
       const u1 *handler_data = dexGetCatchHandlerData(code);
       const u1 **phandler = &handler_data;
       uint8_t *tail = codeitem_end(phandler);
       code_item_len = (int) (tail - item);
   } else {
       //正確的DexCode的大小
       code_item_len = 16 + code->insnsSize * 2;
   }

   ret = env->NewDirectByteBuffer(item, code_item_len);

   tail:
   env->ReleaseStringUTFChars(methodDescriptor_, methodDescriptor);
   env->ReleaseStringUTFChars(methodName_, methodName);
   return ret;
}



6. binggo,使用smali api,直接解碼


/**
    * 將指定loader的smali全部dump到硬碟,請非同步執行該函式
    *
    * @param loader loader
    */
   public static void dissembleAllDex(Object loader) {
       Class loaderClass = resolveLoaderClass(loader);
       DexBackedDexFile memoryMethodDexFile = createMemoryDexFile(loaderClass);
       File dumpDir = resolveDumpDir(memoryMethodDexFile);
       DexFile reWritedDexFile = rewrite(memoryMethodDexFile, loaderClass.getClassLoader());
       XposedBridge.log("脫殼目錄:" + dumpDir.getAbsolutePath());
       int jobs = Runtime.getRuntime().availableProcessors();
       if (jobs > 6) {
           jobs = 6;
       }
       if (memoryMethodDexFile instanceof DexBackedOdexFile) {
           baksmaliOptions.inlineResolver = InlineMethodResolver
                   .createInlineMethodResolver(((DexBackedOdexFile) memoryMethodDexFile).getOdexVersion());
       }
       Log.i("weijia", "開始進行脫殼");
       if (Baksmali.disassembleDexFile(reWritedDexFile, dumpDir, jobs, baksmaliOptions)) {
           Log.i("weijia", "脫殼完成,但是存在錯誤");
       } else {
           Log.i("weijia", "脫殼成功,請在" + dumpDir + "中檢視smali檔案");
       }
       Toast.makeText(SharedObject.context, "脫殼完成,請在" + dumpDir + "中檢視smali檔案", Toast.LENGTH_LONG).show();
   }


7.binggo,使用smali API,輸出dex檔案


/**
    * 將對應class對應的dex檔案的二進位制dump出來
    *
    * @param loader 該dex檔案定義的任何一個class,或者class定義的object
    * @return 一個byteBuffer,包含了二進位制資料
    */
   public static ByteBuffer dumpDex(Object loader) {
       Class loaderClass = resolveLoaderClass(loader);
       DexBackedDexFile memoryDexFile = createMemoryDexFile(loaderClass);
       byte[] buf = (byte[]) XposedHelpers.getObjectField(memoryDexFile, "buf");

       DexFile dexFile = rewrite(memoryDexFile, loaderClass.getClassLoader());
       final DexBuilder builder = new DexBuilder(Opcodes.forApi(apiLevel()));
       MemoryDataStore memoryDataStore = new MemoryDataStore(buf.length);
       for (ClassDef classDef : dexFile.getClasses()) {
           try {
               buildClassDef(classDef, builder);
           } catch (Exception e) {
               Log.i("weijia", "error when define class:" + classDef.getType() + " skipped for rebuild it");
           }
       }
       try {
           builder.writeTo(memoryDataStore);
       } catch (IOException ioe) {
           //the memory writer,no ioe happend
           throw new RuntimeException(ioe);
       }
       return ByteBuffer.wrap(memoryDataStore.getData());
   }



7. 如何呼叫


XposedHelpers.findAndHookConstructor(Activity.class, new XC_MethodHook() {
           @Override
           protected void afterHookedMethod(MethodHookParam param) throws Throwable {
               Object activity = param.thisObject;
               if (activity == null) {
                   return;
               }
               XposedBridge.log("hook class " + activity.getClass());
               if (StringUtils.equalsIgnoreCase(activity.getClass().getName(), "com.xxx.xxx.MainActivity")) {
                   Dumper.dumpDex(activity);
               }
           }
       });



8. 對了你需要移植dalvik的dexlib模組程式碼,還有/vm/oo包下面的程式碼,到你的jni環境下。要不然沒有dex的相關資料結構,也沒有ClassObject的資料結構。


然後,不願意放整個工程,不要求程式碼。論文放出來,還是自己實現一遍才能有收穫。


其他:


相比原始DexHunter的優點:


1. 定位dex更加精準,DexHunter用過filename來確定當前dex是不是需要處理,很容易被加殼平臺識別到這個特徵。而且每次都要在放Android系統push一些檔案,比較麻煩。我這個,直接通過class物件尋找,想找按個找那個。


2. 併發,可能沒有寫過Linux c語言程式,看那個pthread哪裡看不懂。DexHunter為了防止多次重複處理同一個dex檔案,寫了比較複雜的加鎖邏輯。這個放到java層,很簡單實現吧。


3. 多dex,如果一個apk多個dex都需要處理。DexHunter不好處理,因為他輸出就是whole.dex


4. dvmDefineClass失敗,就算正常情況,一個classLoader下面的class,也不是全部可以正常load成功。DexHunter的流程是刪除這些bad class,我還可以儘可能的使用原始dex資料進行解碼。


5. Dalvik_dalvik_system_DexFile_defineClassNative被替換,這可能導致defineClassNative函式不被呼叫,這樣DexHunter無法攔截到。我用classLoader.loadeClass,java標準介面,這個他永遠沒法替換。


6.其他資料結構加密調整,如果未來不光光是method的資料變化了。由於使用baksmali建立的模型,任何資料結構都很容易重構替換,畢竟是java,抽象封裝很好用。


7.脫殼時機,脫殼輸出控制更加方便。提供的是api,你呼叫就脫殼不呼叫就不脫殼。脫殼結果是java的二進位制流,你想怎麼編碼、加密、轉儲都很方便。java層的api太多了。


8.可移植性,是一個普通的xposed專案,任何一個有xposed環境的Android機器都可以(當然現在還沒有實現art,不過理論上沒問題)。也不需要編譯系統映象。等幾天再把xposed包裝一下,免root脫殼。


當然,我對dex檔案格式,並不是非常熟,反正沒有DexHunter玩兒的那麼溜,所以只有多用別人的api了。


關注看雪學院公眾號:ikanxue,更多幹貨等你來拿~

本文由看雪論壇 virjar 原創 轉載請註明來自看雪社群

相關文章