吾愛破解 - LCG - LSG |安卓破解|病毒分析|破解軟件|www.kvamco.live

 找回密碼
 注冊[Register]

QQ登錄

只需一步,快速開始

搜索
查看: 10671|回復: 65
上一主題 下一主題

[Android 原創] 撥云見日:安卓APP脫殼的本質以及如何快速發現ART下的脫殼點

  [復制鏈接]
跳轉到指定樓層
樓主
hanbinglengyue 發表于 2019-9-19 20:55 回帖獎勵
本帖最后由 hanbinglengyue 于 2019-9-25 20:14 編輯

撥云見日:安卓APP脫殼的本質以及如何快速發現ART下的脫殼點


這個貼依然是先發在了看雪,這里再在這里發一下。
我在文章《FART:ART環境下基于主動調用的自動化脫殼方案》中簡單對當前的幾個脫殼方法進行了總結,然而并沒有對每一種的具體原理進行分析。同時,在文章《FART正餐前甜點:ART下幾個通用簡單高效的dump內存中dex方法》中,通過對ART下類加載執行流程進行源碼分析,又給出了幾個新的ART下通用的簡單高效的脫殼點。但是,我在寫上述文章的時候,并沒有對當前安卓平臺加固APP的脫殼本質進行總結;同時,也沒有總結給出快速定位發現安卓中通用脫殼點的方法。這里要感謝看雪的邀請,讓我能夠有幸成為看雪的一名講師。我在看雪的線下培訓班:《安卓高級研修班》課程上已經對當前網上公開的一些脫殼方法的原理進行了深入的分析,同時總結出了脫殼的本質以及如何快速發現ART環境下的脫殼點。相信聽了我的《安卓高級研修班》的課程的大佬們目前也都已經發現了ART下新的通用脫殼點并付諸實際應用中,這也是我為什么說發現“海量”脫殼點的原因。這里,也相信在看懂了本文關于對脫殼的本質和快速發現ART下的脫殼點的方法后,大家也可以開始自己的“脫殼點”挖掘的工作中。這里先拋磚引玉,我在《安卓高級研修班》課程上也給出了ART下的一個未公開的通用脫殼點和脫殼鏡像。關于這部分內容將在本文的第三節,作為本文的結束彩蛋送給大家。
一、發現脫殼的本質1、現有脫殼方法原理分析
首先看下Dalvik下hook dvmdexopenpartal、dexfileparse函數來實現脫殼的原理。先看dexFileParse函數代碼:
[C] 純文本查看 復制代碼
/*
284 * Parse an optimized or unoptimized .dex file sitting in memory.  This is
285 * called after the byte-ordering and structure alignment has been fixed up.
286 *
287 * On success, return a newly-allocated DexFile.
288 */
289DexFile* dexFileParse(const u1* data, size_t length, int flags)
290{
291    DexFile* pDexFile = NULL;
292    const DexHeader* pHeader;
293    const u1* magic;
294    int result = -1;
295
296    if (length < sizeof(DexHeader)) {
297        ALOGE("too short to be a valid .dex");
298        goto bail;      /* bad file format */
299    }
300
301    pDexFile = (DexFile*) malloc(sizeof(DexFile));
302    if (pDexFile == NULL)
303        goto bail;      /* alloc failure */
304    memset(pDexFile, 0, sizeof(DexFile));
305
306    /*
307     * Peel off the optimized header.
308     */
309    if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) {
310        magic = data;
311        if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) {
312            ALOGE("bad opt version (0x%02x %02x %02x %02x)",
313                 magic[4], magic[5], magic[6], magic[7]);
314            goto bail;
315        }
316
317        pDexFile->pOptHeader = (const DexOptHeader*) data;
318        ALOGV("Good opt header, DEX offset is %d, flags=0x%02x",
319            pDexFile->pOptHeader->dexOffset, pDexFile->pOptHeader->flags);
320
321        /* parse the optimized dex file tables */
322        if (!dexParseOptData(data, length, pDexFile))
323            goto bail;
324
325        /* ignore the opt header and appended data from here on out */
326        data += pDexFile->pOptHeader->dexOffset;
327        length -= pDexFile->pOptHeader->dexOffset;
328        if (pDexFile->pOptHeader->dexLength > length) {
329            ALOGE("File truncated? stored len=%d, rem len=%d",
330                pDexFile->pOptHeader->dexLength, (int) length);
331            goto bail;
332        }
333        length = pDexFile->pOptHeader->dexLength;
334    }
335
336    dexFileSetupBasicPointers(pDexFile, data);
337    pHeader = pDexFile->pHeader;
338
339    if (!dexHasValidMagic(pHeader)) {
340        goto bail;
341    }
342
343    /*
344     * Verify the checksum(s).  This is reasonably quick, but does require
345     * touching every byte in the DEX file.  The base checksum changes after
346     * byte-swapping and DEX optimization.
347     */
348    if (flags & kDexParseVerifyChecksum) {
349        u4 adler = dexComputeChecksum(pHeader);
350        if (adler != pHeader->checksum) {
351            ALOGE("ERROR: bad checksum (%08x vs %08x)",
352                adler, pHeader->checksum);
353            if (!(flags & kDexParseContinueOnError))
354                goto bail;
355        } else {
356            ALOGV("+++ adler32 checksum (%08x) verified", adler);
357        }
358
359        const DexOptHeader* pOptHeader = pDexFile->pOptHeader;
360        if (pOptHeader != NULL) {
361            adler = dexComputeOptChecksum(pOptHeader);
362            if (adler != pOptHeader->checksum) {
363                ALOGE("ERROR: bad opt checksum (%08x vs %08x)",
364                    adler, pOptHeader->checksum);
365                if (!(flags & kDexParseContinueOnError))
366                    goto bail;
367            } else {
368                ALOGV("+++ adler32 opt checksum (%08x) verified", adler);
369            }
370        }
371    }
372
373    /*
374     * Verify the SHA-1 digest.  (Normally we don't want to do this --
375     * the digest is used to uniquely identify the original DEX file, and
376     * can't be computed for verification after the DEX is byte-swapped
377     * and optimized.)
378     */
379    if (kVerifySignature) {
380        unsigned char sha1Digest[kSHA1DigestLen];
381        const int nonSum = sizeof(pHeader->magic) + sizeof(pHeader->checksum) +
382                            kSHA1DigestLen;
383
384        dexComputeSHA1Digest(data + nonSum, length - nonSum, sha1Digest);
385        if (memcmp(sha1Digest, pHeader->signature, kSHA1DigestLen) != 0) {
386            char tmpBuf1[kSHA1DigestOutputLen];
387            char tmpBuf2[kSHA1DigestOutputLen];
388            ALOGE("ERROR: bad SHA1 digest (%s vs %s)",
389                dexSHA1DigestToStr(sha1Digest, tmpBuf1),
390                dexSHA1DigestToStr(pHeader->signature, tmpBuf2));
391            if (!(flags & kDexParseContinueOnError))
392                goto bail;
393        } else {
394            ALOGV("+++ sha1 digest verified");
395        }
396    }
397
398    if (pHeader->fileSize != length) {
399        ALOGE("ERROR: stored file size (%d) != expected (%d)",
400            (int) pHeader->fileSize, (int) length);
401        if (!(flags & kDexParseContinueOnError))
402            goto bail;
403    }
404
405    if (pHeader->classDefsSize == 0) {
406        ALOGE("ERROR: DEX file has no classes in it, failing");
407        goto bail;
408    }
409
410    /*
411     * Success!
412     */
413    result = 0;
414
415bail:
416    if (result != 0 && pDexFile != NULL) {
417        dexFileFree(pDexFile);
418        pDexFile = NULL;
419    }
420    return pDexFile;
421}


該函數主要就是對內存中的dex內容進行解析,最終返回一個DexFile結構體供虛擬機使用,函數的參數部分包含了內存中的dex文件的起始地址和大小,因此,在這里可以實現對app的脫殼。下面再看dvmDexFileOpenPartial函數:代碼如下

[C] 純文本查看 復制代碼
146int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)
147{
148    DvmDex* pDvmDex;
149    DexFile* pDexFile;
150    int parseFlags = kDexParseDefault;
151    int result = -1;
152
153    /* -- file is incomplete, new checksum has not yet been calculated
154    if (gDvm.verifyDexChecksum)
155        parseFlags |= kDexParseVerifyChecksum;
156    */
157
158    pDexFile = dexFileParse((u1*)addr, len, parseFlags);
159    if (pDexFile == NULL) {
160        ALOGE("DEX parse failed");
161        goto bail;
162    }
163    pDvmDex = allocateAuxStructures(pDexFile);
164    if (pDvmDex == NULL) {
165        dexFileFree(pDexFile);
166        goto bail;
167    }
168
169    pDvmDex->isMappedReadOnly = false;
170    *ppDvmDex = pDvmDex;
171    result = 0;
172
173bail:
174    return result;
175}


該函數里最后調用了dexFileParse函數來得到解析后的DexFile結構體,函數的參數部分也包含了內存中dex的起始地址和大小,因此這里也是可以脫殼的。事實上Dalvik下類似這種的脫殼點還有很多。接下來看下為什么通過對ART下OpenMemory函數hook或下斷能夠進行脫殼,當前很多殼通過對openmem函數進行hook來對抗該脫殼法,因此該方法針對某些殼可能已經失效。OpenMemory函數在DexFile類中被調用,相關源碼如下:

[C] 純文本查看 復制代碼
272std::unique_ptr<const DexFile> DexFile::OpenMemory(const std::string& location,
273                                                   uint32_t location_checksum,
274                                                   MemMap* mem_map,
275                                                   std::string* error_msg) {
276  return OpenMemory(mem_map->Begin(),
277                    mem_map->Size(),
278                    location,
279                    location_checksum,
280                    mem_map,
281                    nullptr,
282                    error_msg);
283}


可以看到OpenMemory函數的參數中包含了內存中dex的起始位置和大小,因此,能夠通過該函數進行脫殼。在17年的DEF CON 25 黑客大會中,Avi Bashan 和 SlavaMakkaveev 提出的通過修改DexFile的構造函數DexFile::DexFile(),以及OpenAndReadMagic()函數來實現對加殼應用的內存中的dex的dump來脫殼技術,下面我們就來看這兩個函數的具體代碼。首先看DexFile類的構造函數的源碼:

[C] 純文本查看 復制代碼
395DexFile::DexFile(const uint8_t* base, size_t size,
396                 const std::string& location,
397                 uint32_t location_checksum,
398                 MemMap* mem_map,
399                 const OatDexFile* oat_dex_file)
400    : begin_(base),
401      size_(size),
402      location_(location),
403      location_checksum_(location_checksum),
404      mem_map_(mem_map),
405      header_(reinterpret_cast<const Header*>(base)),
406      string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
407      type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
408      field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
409      method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
410      proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
411      class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
412      find_class_def_misses_(0),
413      class_def_index_(nullptr),
414      oat_dex_file_(oat_dex_file) {
415  CHECK(begin_ != nullptr) << GetLocation();
416  CHECK_GT(size_, 0U) << GetLocation();
417}



可以看到該構造函數的參數中依然是包含了內存中dex的起始位置和大小,因此,能夠通過修改該函數進行脫殼。下面為添加的代碼,在代碼中只需要調用write將內存中的dex寫入文件即完成脫殼。

[C] 純文本查看 復制代碼
+   LOG(WARNING) << "Dex File: Filename: "<< location;                                          
+   if (location.find("/data/data/") != std::string::npos) {                                    
+     LOG(WARNING) << "Dex File: OAT file unpacking launched";                                  
+     std::ofstream dst(location + "__unpacked_oat", std::ios::binary);                         
+     dst.write(reinterpret_cast<const char*>(base), size);                                     
+     dst.close();                                                                              
+   } else {                                                                                    
+     LOG(WARNING) << "Dex File: OAT file unpacking not launched";                              
+   }   



接下來再看OpenAndReadMagic函數,該函數打開了dex文件并進行魔數的校驗,源碼如下:

[C] 純文本查看 復制代碼
59static int OpenAndReadMagic(const char* filename, uint32_t* magic, std::string* error_msg) {
60  CHECK(magic != nullptr);
61  ScopedFd fd(open(filename, O_RDONLY, 0));
62  if (fd.get() == -1) {
63    *error_msg = StringPrintf("Unable to open '%s' : %s", filename, strerror(errno));
64    return -1;
65  }
66  int n = TEMP_FAILURE_RETRY(read(fd.get(), magic, sizeof(*magic)));
67  if (n != sizeof(*magic)) {
68    *error_msg = StringPrintf("Failed to find magic in '%s'", filename);
69    return -1;
70  }
71  if (lseek(fd.get(), 0, SEEK_SET) != 0) {
72    *error_msg = StringPrintf("Failed to seek to beginning of file '%s' : %s", filename,
73                              strerror(errno));
74    return -1;
75  }
76  return fd.release();
77}



因此添加如下代碼,即可完成脫殼。

[C] 純文本查看 復制代碼
+   struct stat st;
+   // let's limit processing file list
+
+   LOG(WARNING) << "File_magic: Filename: "<<filename;
+   if (strstr(filename, "/data/data") != NULL) {
+     LOG(WARNING) << "File_magic: DEX file unpacking launched";
+     char* fn_out = new char[PATH_MAX];
+     strcpy(fn_out, filename);
+     strcat(fn_out, "__unpacked_dex");
+
+     int fd_out = open(fn_out, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+
+     if (!fstat(fd.get(), &st)) {
+       char* addr = (char*)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd.get(), 0);
+       int ret=write(fd_out, addr, st.st_size);
+       ret+=1;
+       munmap(addr, st.st_size);
+     }
+
+     close(fd_out);
+     delete[] fn_out;
+   } else {
+     LOG(WARNING) << "File_magic: DEX file unpacking not launched";
+   }


接下來分析在java層進行脫殼的典型案例:Fdex2的原理以及如何對Fdex2進行拓展使用。先看Fdex2的關鍵代碼部分:

[Java] 純文本查看 復制代碼
XposedHelpers.findAndHookMethod("java.lang.ClassLoader", lpparam.classLoader, "loadClass", String.class, Boolean.TYPE, new XC_MethodHook() {
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                Class cls = (Class) param.getResult();
                if (cls == null) {
                    //XposedBridge.log("cls == null");
                    return;
                }
                String name = cls.getName();
                XposedBridge.log("當前類名:" + name);
                byte[] bArr = (byte[]) Dex_getBytes.invoke(getDex.invoke(cls, new Object[0]), new Object[0]);
                if (bArr == null) {
                    XposedBridge.log("數據為空:返回");
                    return;
                }
                XposedBridge.log("開始寫數據");
                String dex_path = "/data/data/" + packagename + "/" + packagename + "_" + bArr.length + ".dex";
                XposedBridge.log(dex_path);
                File file = new File(dex_path);
                if (file.exists()) return;
                writeByte(bArr, file.getAbsolutePath());
            }
            } );
    }
  
    public void initRefect() {
        try {
            Dex = Class.forName("com.android.dex.Dex");
            Dex_getBytes = Dex.getDeclaredMethod("getBytes", new Class[0]);
            getDex = Class.forName("java.lang.Class").getDeclaredMethod("getDex", new Class[0]);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
  
    }
  
    public  void writeByte(byte[] bArr, String str) {
        try {
            OutputStream outputStream = new FileOutputStream(str);
            outputStream.write(bArr);
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }


可以看到,fdex2通過對類"java.lang.ClassLoader”中的loadClass進行hook,進而獲取到該函數執行后返回的Class對象cls。對于"java.lang.ClassLoader”類中的loadClass函數是Android中加載Class流程中的一個關鍵函數,因此,Fdex2選擇hook這個函數。本來在java中來操作指針以及dump內存是非常困難的事情,然而,Android系統源碼卻恰好提供了java.lang.Class類中的getDex函數,用于通過一個Class對象來獲取到其所屬的dex對象,以及com.android.dex.Dex類中的getBytes函數,用于通過一個dex對象來獲取到該dex對象在內存中的字節流。正是有了這兩個api的支持,才能夠實現在java代碼中實現對內存中的dex的dump。因此,便可以利用這兩個api對Fdex2的脫殼原理進行拓展使用。比如,所有的app的組件信息都會在Manifest.xml文件中進行注冊,那么我們就可以知道該加殼app中dex必然包含的一些類,如Activity類名、Service類名等,那么我們就可以通過使用各種hook技術完成對該app的hook,獲取到這些類的Class對象,如首先通過反射獲取到app最終的Classloader,然后再通過classloader的loadClass獲取到某一個已知類的Class對象,然后再配合getDex和getBytes這兩個api便可以實現對該類所屬的dex的脫殼。最后,再提一下根據Classloader來脫殼,看雪上也有對這個方法的介紹。通過對源碼分析,可以看到app加載的所有的dex都依附在對應的Classloader中,那么,便可以通過一系列的反射最終獲取到每一個dex文件的DexFile對象,具體流程可以首先通過反射,獲取加固app最終的Classloader,然后再通過反射獲取到BaseDexClassloader類中的DexPathList pathList對象,再然后獲取pathList對象中的Element[]類型的dexElements對象,最后,便可以獲取到Element類型對象中的DexFile dexFile對象,進而再獲取到DexFile對象中的關鍵元素: mCookie,那么接下來就可以通過mCookie在C/C++層完成對dex的dump操作。

2、總結Android脫殼的本質上面對當前的一些主流的脫殼方法進行了簡單的原理分析。可以得出結論:Android APP脫殼的本質就是對內存中處于解密狀態的dex的dump。首先要區分這里的脫殼和修復的區別。這里的脫殼指的是對加固apk中保護的dex的整體的dump,不管是函數抽取、dex2c還是vmp殼,首要做的就是對整體dex的dump,然后再對脫殼下來的dex進行修復。要達到對apk的脫殼,最為關鍵的就是準確定位內存中解密后的dex文件的起始地址和大小。那么這里要達成對apk的成功脫殼,就有兩個最為關鍵的要素:1、內存中dex的起始地址和大小,只有拿到這兩個要素,才能夠成功dump下內存中的dex2、脫殼時機,只有正確的脫殼時機,才能夠dump下明文狀態的dex。否則,時機不對,及時是正確的起始地址和大小,dump下來的也可能只是密文。其中,通過上一小節對當前的一些脫殼方法的原理分析可以看到,Dalvik下的脫殼都是圍繞著一個至關重要的結構體:DexFile結構體,而到了ART以后,便演化為了DexFile類。可以說,在ART下只要獲得了DexFile對象,那么我們就可以得到該dex文件在內存中的起始地址和大小,進而完成脫殼。下面是DexFile結構體的定義以及DexFile類的定義源碼:Dalvik下DexFile結構體:

[C] 純文本查看 復制代碼
struct DexFile {
501    /* directly-mapped "opt" header */
502    const DexOptHeader* pOptHeader;
503
504    /* pointers to directly-mapped structs and arrays in base DEX */
505    const DexHeader*    pHeader;
506    const DexStringId*  pStringIds;
507    const DexTypeId*    pTypeIds;
508    const DexFieldId*   pFieldIds;
509    const DexMethodId*  pMethodIds;
510    const DexProtoId*   pProtoIds;
511    const DexClassDef*  pClassDefs;
512    const DexLink*      pLinkData;
513
514    /*
515     * These are mapped out of the "auxillary" section, and may not be
516     * included in the file.
517     */
518    const DexClassLookup* pClassLookup;
519    const void*         pRegisterMapPool;       // RegisterMapClassPool
520
521    /* points to start of DEX file data */
522    const u1*           baseAddr;
523
524    /* track memory overhead for auxillary structures */
525    int                 overhead;
526
527    /* additional app-specific data structures associated with the DEX */
528    //void*               auxData;
529};
530



ART下DexFile類,代碼較長,只貼出片段吧:

[C] 純文本查看 復制代碼
54class DexFile {
55 public:
56  static const uint8_t kDexMagic[];
57  static const uint8_t kDexMagicVersion[];
58  static constexpr size_t kSha1DigestSize = 20;
59  static constexpr uint32_t kDexEndianConstant = 0x12345678;
61  // name of the DexFile entry within a zip archive
62  static const char* kClassesDex;
64  // The value of an invalid index.
65  static const uint32_t kDexNoIndex = 0xFFFFFFFF;
67  // The value of an invalid index.
68  static const uint16_t kDexNoIndex16 = 0xFFFF;
70  // The separator charactor in MultiDex locations.
71  static constexpr char kMultiDexSeparator = ':';
73  // A string version of the previous. This is a define so that we can merge string literals in the
74  // preprocessor.
75  #define kMultiDexSeparatorString ":"77  // Raw header_item.
78  struct Header {
79    uint8_t magic_[8];
80    uint32_t checksum_;  // See also location_checksum_
81    uint8_t signature_[kSha1DigestSize];
82    uint32_t file_size_;  // size of entire file
83    uint32_t header_size_;  // offset to start of next section
84    uint32_t endian_tag_;
85    uint32_t link_size_;  // unused
86    uint32_t link_off_;  // unused
87    uint32_t map_off_;  // unused
88    uint32_t string_ids_size_;  // number of StringIds
89    uint32_t string_ids_off_;  // file offset of StringIds array
90    uint32_t type_ids_size_;  // number of TypeIds, we don't support more than 65535
91    uint32_t type_ids_off_;  // file offset of TypeIds array
92    uint32_t proto_ids_size_;  // number of ProtoIds, we don't support more than 65535
93    uint32_t proto_ids_off_;  // file offset of ProtoIds array
94    uint32_t field_ids_size_;  // number of FieldIds
95    uint32_t field_ids_off_;  // file offset of FieldIds array
96    uint32_t method_ids_size_;  // number of MethodIds
97    uint32_t method_ids_off_;  // file offset of MethodIds array
98    uint32_t class_defs_size_;  // number of ClassDefs
99    uint32_t class_defs_off_;  // file offset of ClassDef array
100    uint32_t data_size_;  // unused
101    uint32_t data_off_;  // unused
102
103   private:
104    DISALLOW_COPY_AND_ASSIGN(Header);
105  };
106
107  /
........


可以看到,ART下DexFile類中定義了兩個關鍵的變量: begin_、size_以及用于獲取這兩個變量的Begin()和Size()函數。這兩個變量分別代表著當前DexFile對象對應的內存中的dex文件加載的起始位置和大小。當然也有其他的方法來獲取到內存中的dex加載的起始位置和大小。可以說,只要有了這兩個值,我們就可以完成對這個dex的dump。

二、ART下基于關鍵字的快速定位脫殼點方法

在上一小節,我對當前Android加固app脫殼的本質進行了總結。其中,ART下影響脫殼的關鍵的一個類就是DexFile,那么我們便可以圍繞這個類,實現在Android龐大的系統源碼中快速定位脫殼點,從而能夠找到“海量”的脫殼點。這里再總結給出兩種快速定位脫殼點的方法。

1、直接查找法這里的直接查找法就是指以DexFile為關鍵字,在龐大的源碼庫中檢索定位可能的脫殼點。如參數中出現DexFile類型的、返回值為DexFile類型的、函數流程中出現DexFile類型的源碼位置。在獲取到DexFile對象以后,然后再通過該對象的Begin()和Size()函數獲取到該DexFile對象對應的內存中的dex的起始地址和大小即可進行dex的dump。如下圖的檢索過程,可以輕松定位出網上公開的基于dex2oat流程進行脫殼的脫殼點,同時,也可以看到那個脫殼點只是dex2oat流程中的一個脫殼點而已。





2、間接查找法

間接法就是指以DexFile為出發點,尋找能夠間接獲取到DexFile對象的。如通過ArtMethod對象的getDexFile()獲取到ArtMethod所屬的DexFile對象的這種一級間接法等;然后再在海量源碼中以ArtMethod為關鍵字進行檢索,檢索那些參數中出現ArtMethod類型的、返回值為ArtMethod類型的、函數流程中出現ArtMethod類型的源碼位置;再比如通過Thread的getCurrentMethod()函數首先獲取到ArtMethod或者通過ShadowFrame的getMethod獲取到ArtMethod對象,然后再通過getDexFile獲取到ArtMethod對象所屬的DexFile的二級間接法以及通過ShadowFrame對象的GetMethod()函數獲取到當前棧中執行的ArtMethod對象,然后再獲取DexFile對象等等的二級間接法。好了,上面已經給出了如何快速在龐大的源碼庫中定位可能的脫殼點的方法了,大家就可以開始自己的“挖寶”行動了!接下來,便進入了本文的彩蛋部分了。

三、彩蛋:送出一個未公開的脫殼點

1、原理分析

眾所周知,ART下引入了dex2oat來對dex進行編譯,生成每一個java函數對應的native代碼,來提高運行效率。但是,dex2oat并不是對dex中的所有函數進行編譯,通過對dex2oat的源碼進行分析,最終可以到達CompilerDriver類的CompileMethod函數,可以看到dex2oat對dex進行編譯的過程中是按照函數粒度進行編譯的。

[C] 純文本查看 復制代碼
2255void CompilerDriver::CompileMethod(Thread* self, const DexFile::CodeItem* code_item,
2256                                   uint32_t access_flags, InvokeType invoke_type,
2257                                   uint16_t class_def_idx, uint32_t method_idx,
2258                                   jobject class_loader, const DexFile& dex_file,
2259                                   DexToDexCompilationLevel dex_to_dex_compilation_level,
2260                                   bool compilation_enabled) {
2261  CompiledMethod* compiled_method = nullptr;
2262  uint64_t start_ns = kTimeCompileMethod ? NanoTime() : 0;
2263  MethodReference method_ref(&dex_file, method_idx);
2264
2265  if ((access_flags & kAccNative) != 0) {
2266    // Are we interpreting only and have support for generic JNI down calls?
2267    if (!compiler_options_->IsCompilationEnabled() &&
2268        InstructionSetHasGenericJniStub(instruction_set_)) {
2269      // Leaving this empty will trigger the generic JNI version
2270    } else {
2271      compiled_method = compiler_->JniCompile(access_flags, method_idx, dex_file);
2272      CHECK(compiled_method != nullptr);
2273    }
2274  } else if ((access_flags & kAccAbstract) != 0) {
2275    // Abstract methods don't have code.
2276  } else {
2277    bool has_verified_method = verification_results_->GetVerifiedMethod(method_ref) != nullptr;
2278    bool compile = compilation_enabled &&
2279                   // Basic checks, e.g., not <clinit>.
2280                   verification_results_->IsCandidateForCompilation(method_ref, access_flags) &&
2281                   // Did not fail to create VerifiedMethod metadata.
2282                   has_verified_method &&
2283                   // Is eligable for compilation by methods-to-compile filter.
2284                   IsMethodToCompile(method_ref);
2285    if (compile) {
2286      // NOTE: if compiler declines to compile this method, it will return null.
2287      compiled_method = compiler_->Compile(code_item, access_flags, invoke_type, class_def_idx,
2288                                           method_idx, class_loader, dex_file);
2289    }
2290    if (compiled_method == nullptr && dex_to_dex_compilation_level != kDontDexToDexCompile) {
2291      // TODO: add a command-line option to disable DEX-to-DEX compilation ?
2292      // Do not optimize if a VerifiedMethod is missing. SafeCast elision, for example, relies on
2293      // it.
2294      (*dex_to_dex_compiler_)(*this, code_item, access_flags,
2295                              invoke_type, class_def_idx,
2296                              method_idx, class_loader, dex_file,
2297                              has_verified_method ? dex_to_dex_compilation_level : kRequired);
2298    }
2299  }
2300  if (kTimeCompileMethod) {
2301    uint64_t duration_ns = NanoTime() - start_ns;
2302    if (duration_ns > MsToNs(compiler_->GetMaximumCompilationTimeBeforeWarning())) {
2303      LOG(WARNING) << "Compilation of " << PrettyMethod(method_idx, dex_file)
2304                   << " took " << PrettyDuration(duration_ns);
2305    }
2306  }
2307
2308  if (compiled_method != nullptr) {
2309    // Count non-relative linker patches.
2310    size_t non_relative_linker_patch_count = 0u;
2311    for (const LinkerPatch& patch : compiled_method->GetPatches()) {
2312      if (!patch.IsPcRelative()) {
2313        ++non_relative_linker_patch_count;
2314      }
2315    }
2316    bool compile_pic = GetCompilerOptions().GetCompilePic();  // Off by default
2317    // When compiling with PIC, there should be zero non-relative linker patches
2318    CHECK(!compile_pic || non_relative_linker_patch_count == 0u);
2319
2320    DCHECK(GetCompiledMethod(method_ref) == nullptr) << PrettyMethod(method_idx, dex_file);
2321    {
2322      MutexLock mu(self, compiled_methods_lock_);
2323      compiled_methods_.Put(method_ref, compiled_method);
2324      non_relative_linker_patch_count_ += non_relative_linker_patch_count;
2325    }
2326    DCHECK(GetCompiledMethod(method_ref) != nullptr) << PrettyMethod(method_idx, dex_file);
2327  }
2328
2329  // Done compiling, delete the verified method to reduce native memory usage. Do not delete in
2330  // optimizing compiler, which may need the verified method again for inlining.
2331  if (compiler_kind_ != Compiler::kOptimizing) {
2332    verification_results_->RemoveVerifiedMethod(method_ref);
2333  }
2334
2335  if (self->IsExceptionPending()) {
2336    ScopedObjectAccess soa(self);
2337    LOG(FATAL) << "Unexpected exception compiling: " << PrettyMethod(method_idx, dex_file) << "\n"
2338        << self->GetException()->Dump();
2339  }
2340}

可以看到在進行編譯前進行了判斷,最終可以發現,dex2oat對類的初始化函數并沒有進行編譯,那么也就是說類的初始化函數始終運行在ART下的inpterpreter模式,那么最終必然進入到interpreter.cc文件中的Execute函數,進而進入ART下的解釋器解釋執行。因此,我們便可以選擇在Execute或者其他解釋執行流程中的函數中進行dex的dump操作。事實上,當前一些殼通過阻斷dex2oat的編譯過程,導致了不只是類的初始化函數在解釋模式下執行,也讓類中的其他函數也運行在解釋模式下。

2、實現代碼



下面進入到代碼部分了,最終我們只需要在Execute函數中添加一些代碼即可,修改后的Execute函數代碼如下:

[C] 純文本查看 復制代碼
static inline JValue Execute(Thread* self, const DexFile::CodeItem* code_item,
                             ShadowFrame& shadow_frame, JValue result_register) {
      //addcodestart
    char *dexfilepath=(char*)malloc(sizeof(char)*1000);   
    if(dexfilepath!=nullptr)
    {
    ArtMethod* artmethod=shadow_frame.GetMethod();
    const DexFile* dex_file = artmethod->GetDexFile();
    const uint8_t* begin_=dex_file->Begin();  // Start of data.
    size_t size_=dex_file->Size();  // Length of data.
    int size_int_=(int)size_;
    int fcmdline =-1;
    char szCmdline[64]= {0};
    char szProcName[256] = {0};
    int procid = getpid();
    sprintf(szCmdline,"/proc/%d/cmdline", procid);
    fcmdline = open(szCmdline, O_RDONLY,0644);
    if(fcmdline >0)
    {
        read(fcmdline, szProcName,256);
        close(fcmdline);
    }
             
    if(szProcName[0])
    {
            memset(dexfilepath,0,1000);               
            sprintf(dexfilepath,"/sdcard/%s_%d_dexfile.dex",szProcName,size_int_);     
            int dexfilefp=open(dexfilepath,O_RDONLY,0666);
            if(dexfilefp>0){
                                close(dexfilefp);
                                dexfilefp=0;
                                       
                            }else{
                                        int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
                                        if(fp>0)
                                        {
                                            write(fp,(void*)begin_,size_);
                                            fsync(fp); 
                                            close(fp);  
                                            }  
                             
                                }
    }
 
    if(dexfilepath!=nullptr)
    {
        free(dexfilepath);
        dexfilepath=nullptr;
    }                        
   }
 //addcodeend
  DCHECK(!shadow_frame.GetMethod()->IsAbstract());
  DCHECK(!shadow_frame.GetMethod()->IsNative());
  shadow_frame.GetMethod()->GetDeclaringClass()->AssertInitializedOrInitializingInThread(self);
 
 
 
  bool transaction_active = Runtime::Current()->IsActiveTransaction();
  if (LIKELY(shadow_frame.GetMethod()->IsPreverified())) {
    // Enter the "without access check" interpreter.
    if (kInterpreterImplKind == kSwitchImpl) {
      if (transaction_active) {
        return ExecuteSwitchImpl<false, true>(self, code_item, shadow_frame, result_register);
      } else {
        return ExecuteSwitchImpl<false, false>(self, code_item, shadow_frame, result_register);
      }
    } else {
      DCHECK_EQ(kInterpreterImplKind, kComputedGotoImplKind);
      if (transaction_active) {
        return ExecuteGotoImpl<false, true>(self, code_item, shadow_frame, result_register);
      } else {
        return ExecuteGotoImpl<false, false>(self, code_item, shadow_frame, result_register);
      }
    }
  } else {
    // Enter the "with access check" interpreter.
    if (kInterpreterImplKind == kSwitchImpl) {
      if (transaction_active) {
        return ExecuteSwitchImpl<true, true>(self, code_item, shadow_frame, result_register);
      } else {
        return ExecuteSwitchImpl<true, false>(self, code_item, shadow_frame, result_register);
      }
    } else {
      DCHECK_EQ(kInterpreterImplKind, kComputedGotoImplKind);
      if (transaction_active) {
        return ExecuteGotoImpl<true, true>(self, code_item, shadow_frame, result_register);
      } else {
        return ExecuteGotoImpl<true, false>(self, code_item, shadow_frame, result_register);
      }
    }
  }
}


3、測試效果

在修改完代碼并完成脫殼鏡像的編譯后(這里提供一個已經編譯好的nexus5的6.0鏡像供大家體驗,鏈接:https://pan.baidu.com/s/1vt6roAFf_tdayp_QB1taZQ提取碼:wqn2),就可以愉快的開始測試脫殼效果了。這里要注意,我在代碼中對dex直接保存到了SD卡的根目錄下,因此在安裝完app后,記得到設置中授予app讀寫SD卡權限,不然無法寫入脫殼的dex。這里我就隨便選擇了幾個最新版的加固廠商的加固app進行測試了,下面是測試效果:數字殼測試效果



可以看到能夠dump成功:







某梆脫殼效果:




好了,就到這里吧,大家可以下載鏡像測試其他的殼。

鑒于很多人手里沒有nexus5手機,重新編譯又比較費勁,這里再同時提供arm、x86模擬器鏡像
鏈接:https://pan.baidu.com/s/1au7MShA1Ei_u9iDMM5JRoQ
提取碼:23d5

免費評分

參與人數 37吾愛幣 +42 熱心值 +33 收起 理由
zch333333 + 1 + 1 熱心回復!
xgx1988 + 1 + 1 [email protected]
AlexMahoneFBI + 1 我很贊同!
Liu0827 + 1 + 1 熱心回復!
888110 + 1 + 1 我有nexus5,續航不行了,LG換電池挺貴的。
hexio + 1 + 1 我很贊同!
siuhoapdou + 1 + 1 [email protected]
laoWMoel + 1 + 1 用心討論,共獲提升!
sunnylds7 + 1 + 1 熱心回復!
ytfh1131 + 1 + 1 我很贊同!
poisonbcat + 1 + 1 我很贊同!
gaosld + 1 + 1 [email protected]
狼貓 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
mengsiyiren + 1 + 1 用心討論,共獲提升!
xjlong86 + 1 + 1 用心討論,共獲提升!
yixi + 1 + 1 [email protected]
gqdsc + 1 + 1 真是高手,如果有打包的程序給小白就好
三胖胖胖 + 1 + 1 支持大佬
清炒藕片丶 + 1 + 1 撥云見日,膜拜了
jzmceo + 1 我很贊同!
CrazyNut + 3 + 1 不明覺厲 謝謝大佬
jnez112358 + 1 + 1 [email protected]
落塵6 + 1 + 1 [email protected]
youhen233 + 1 + 1 [email protected]
luochunyan + 1 + 1 [email protected]
yanwc + 1 + 1 [email protected]
h1ck + 1 用心討論,共獲提升!
LOLQAQ + 1 + 1 我很贊同!
小可愛~ + 2 + 1 用心討論,共獲提升!
丶咖啡貓丶 + 1 + 1 我很贊同!
小白在線 + 1 [email protected]
tang588 + 1 + 1 熱心回復!
笙若 + 2 + 1 [email protected]
濤之雨 + 1 + 1 看雪的大佬!
是張玄非阿 + 1 + 1 我很贊同!
yikuaiyingbi + 1 + 1 鼓勵轉貼優秀軟件安全工具和文檔!
Gentlewang + 3 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!

查看全部評分

本帖被以下淘專輯推薦:

發帖前要善用論壇搜索功能,那里可能會有你要找的答案或者已經有人發布過相同內容了,請勿重復發帖。

推薦
weilaikezhan 發表于 2019-11-7 15:03
嘗試了下nexus5 手機刷機之后,安裝運行了一個測試apk,  就是沒有在sdcard目錄下搜索到dex文件,      是鏡像刷完直接安裝app運行就能自動脫么?
推薦
thornfish 發表于 2019-11-18 14:45
weilaikezhan 發表于 2019-11-7 15:03
嘗試了下nexus5 手機刷機之后,安裝運行了一個測試apk,  就是沒有在sdcard目錄下搜索到dex文件,    ...

研究出來了嗎
4#
yikuaiyingbi 發表于 2019-9-19 21:18
5#
china0sen 發表于 2019-9-19 21:38
這個鏡像是要用真機刷機?
6#
夜步城 發表于 2019-9-19 22:31
有點生澀 但是還是要學習,謝謝老大
7#
xionghao 發表于 2019-9-20 00:07
沒電腦怎么玩???!
8#
chenjingyes 發表于 2019-9-20 01:12
希望樓主出下一篇  醍醐灌頂 謝謝分享
9#
丶咖啡貓丶 發表于 2019-9-20 08:47
支持大佬后續
10#
LOLQAQ 發表于 2019-9-20 12:18
感謝樓主分享!
11#
nws0507 發表于 2019-9-20 12:44
有點看不懂,感覺自己基礎太差了,鏡像要刷真機嗎
12#
破解project 發表于 2019-9-20 18:32
感謝分享,話說有很多應用加固之前連混淆都不干的。
您需要登錄后才可以回帖 登錄 | 注冊[Register]

本版積分規則 警告:禁止回復與主題無關內容,違者重罰!

快速回復 收藏帖子 返回列表 搜索

RSS訂閱|小黑屋|聯系我們|吾愛破解 - LCG - LSG ( 京ICP備16042023號 | 京公網安備 11010502030087號 )

GMT+8, 2019-12-13 21:57

Powered by Discuz!

© 2001-2017 Comsenz Inc.

快速回復 返回頂部 返回列表
3d开机号今天