Dex起步探索
2021-08-04 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:38 收藏

本文为看雪论坛优秀文章
看雪论坛作者ID:misskings

部分内容摘自:android软件安全权威指南:丰生强
从根源上搞懂基础的原理是很有必要的,这样有助于我们更方便的利用它的特性,达到我们的目的。
我把内容主要分为二个部分:原理探索、案例分析。

1

原理探索

dex文件

我们在正向开发app编译时,编写的java代码,会编译成java字节码保存在.class后缀的文件中。然后再用dx工具将java字节码转换成dex文件(Dalvik字节码)。

在转换的过程中,会将所有java字节码中的所有冗余信息组成一个常量池。例如多个class文件中都存在的字符串"hello world"。转换后将单独存放在一个地方,并且所有类共享。包括方法的签名也会组成常量池。我们将编译好的apk文件解压后就能拿到classes.dex文件。

dex文件格式

1、DexFile结构

上面拿到的classes.dex文件包含了apk的可执行代码。Dalvik虚拟机会解析加载文件并执行代码。只要我们了解这个文件格式的组成,那么就可以自己解析这个文件获取到想要的数据。
 
首先是安卓源码中的dalvik/libdex/DexFile.h这里可以找到dex文件的数据结构。下面贴上源码部分:
struct DexFile {    /* odex的头 */    const DexOptHeader* pOptHeader;     /* dex文件头,指定了dex文件的一些数据,记录了其他数据结构在dex文件中的物理偏移 */    const DexHeader*    pHeader;      /* 索引结构区 */    const DexStringId*  pStringIds;    const DexTypeId*    pTypeIds;    const DexFieldId*   pFieldIds;    const DexMethodId*  pMethodIds;    const DexProtoId*   pProtoIds;      /* 真实的数据存放 */    const DexClassDef*  pClassDefs;      /* 静态链接数据区 */    const DexLink*      pLinkData;    /*     * These are mapped out of the "auxillary" section, and may not be     * included in the file.     */    const DexClassLookup* pClassLookup;    const void*         pRegisterMapPool;       // RegisterMapClassPool     /* points to start of DEX file data */    const u1*           baseAddr;     /* track memory overhead for auxillary structures */    int                 overhead;     /* additional app-specific data structures associated with the DEX */    //void*               auxData;};

为了更好的理解,dex文件格式,我们可以用010编辑器打开一个dex文件对照这个结构体来观察一下。
 
可以看到,这个dex文件由这8个部分组成。
 
dex_header:dex文件头,指定了dex文件的一些数据,记录了其他数据结构在dex文件中的物理偏移。
 
string_ids:字符串列表(前面说的去掉冗余信息组成的常量池,全局共享使用的)。
 
type_ids:类型签名列表(去掉冗余信息组成的常量池)。
 
proto_ids:方法声明列表(去掉冗余信息组成的常量池)。
 
field_ids:字段列表(去掉冗余信息组成的常量池)。
 
method_ids:方法列表(去掉冗余信息组成的常量池)。
 
class_def:类型结构体列表(去掉冗余信息组成的常量池)。
 
map_list:这里记录了前面7个部分的偏移和大小。
 
然后我们开始逐个的看各个部分的结构。

2、dex_header

先是贴上源码看看这个部分的结构体:
struct DexHeader {    u1  magic[8];           /* 表示是一个有效的dex文件。值一般固定为64 65 78 0A 30 33 35 00(dex.035) */    u4  checksum;           /* adler32 checksum dex文件的校验和,用来判断文件是否已经损坏或者篡改 */    u1  signature[kSHA1DigestLen]; /* SHA-1 hash 用来识别未经dexopt优化的dex文件*/    u4  fileSize;           /* length of entire file 记录了包括dexHeader在内的整个dex文件的大小*/    u4  headerSize;         /* offset to start of next section  dexHeader占用的字节数,一般都是0x70*/    u4  endianTag;                    /* 指定dex运行环境的cpu字节序。预设是ENDIAN_CONSTANT等于0x12345678,也就是默认小端字节序 */    u4  linkSize;                        /* 链接段的大小 */    u4  linkOff;                        /* 链接段的偏移 */    u4  mapOff;                            /* DexMapList结构的文件偏移 */    u4  stringIdsSize;            /* 下面都是数据段的大小和文件偏移 */    u4  stringIdsOff;    u4  typeIdsSize;    u4  typeIdsOff;    u4  protoIdsSize;    u4  protoIdsOff;    u4  fieldIdsSize;    u4  fieldIdsOff;    u4  methodIdsSize;    u4  methodIdsOff;    u4  classDefsSize;    u4  classDefsOff;    u4  dataSize;    u4  dataOff;};

这里可以看到,如果在DexHeader中,可以找到其他部分的偏移和大小,以及整个文件的大小,解析出这块数据,其他部分的任意数据,我们都可以获取到。然后再使用对应的结构体来解析。

另外留意这里DexHeader的结构体的大小是固定0x70字节的。所以有的脱壳工具中会将70 00 00 00来作为特征在内存中查找dex进行脱壳(比如FRIDA-DEXDump的深度检索)。

 
然后我们贴一下真实classes.dex文件的DexHeader数据是什么样的。
 

3、string_ids

先看看字符串列表的结构体,非常简单,就是字符串的偏移,但是并不是普通的ascii字符串,而是MUTF-8编码的。这个是一个经过修改的UTF-8编码。和传统的UTF-8相似。
struct DexStringId {    u4 stringDataOff;      /* 字符串数据偏移 */};

4、type_ids

类型签名列表的结构体也是非常简单,和上面字符串列表差不多:
struct DexTypeId {    u4  descriptorIdx;      /* index into stringIds list for type descriptor */};
真实数据图如下,可以看到值类型签名都在前面,后面都是引用类型签名。
‍‍

5、proto_ids

方法声明的列表的结构体较为复杂,因为方法签名必然是有几点信息构成:返回值类型、参数类型列表(就是每个参数是什么类型)。方法声明的结构体如下:
struct DexTypeList {    u4  size;               /* dexTypeItem的个数 */    DexTypeItem list[1];    /* entries */};struct DexTypeItem {    u2  typeIdx;            /* DexTypeId的索引 */};struct DexProtoId {    u4  shortyIdx;          /* DexStringId列表的索引,方法签名字符串,由返回值和参数类型列表组合*/    u4  returnTypeIdx;      /* DexTypeId的索引,返回值的类型 */    u4  parametersOff;      /* 指向DexTypeList的偏移,参数类型列表 */};

同样看看这个结构的真实数据

6、field_ids

字段描述的结构体,我们可以先想象一下,要找一个字段,我们需要些什么:字段所属的类,字段的类型,字段名称。有这些信息,就可以找到各自对应的字段了。接下来看看定义的结构体:
struct DexFieldId {    u2  classIdx;           /* 类的类型,指向DexTypeId的索引,字段所属的类 */    u2  typeIdx;            /* 字段类型,指向DexTypeId的索引,字段的类型 */    u4  nameIdx;            /* 字段名,指向DexStringId的索引,字段的名称 */};

然后看一段真实数据:
 

7、method_ids

方法描述‍的结构体,同样先了解找一个方法的几个必须项:方法所属的类,方法的签名(签名中有方法的返回值和方法的参数,也就是上面的proto_ids中记录的),方法的名称。然后下面看结构体:
struct DexMethodId {    u2  classIdx;           /* 类的类型,指向DexTypeId的索引,方法所属的类 */    u2  protoIdx;           /* 声明类型,指向DexProtoId的索引,方法的签名 */    u4  nameIdx;            /* 方法名,指向DexStringId索引,方法的名称 */};

看看一组方法的真实数据:

8、class_def

类定义的结构体,这个比较复杂。直接贴上结构体和原文的说明。这里大致可以看出来,和上面的原理差不多,通过这个结构体来描述类的内容。
struct DexClassDef {    u4  classIdx;           /* 类的类型,指向DexTypeId的索引 */    u4  accessFlags;                /* 访问标志 */    u4  superclassIdx;      /* 父类的类型,指向DexTypeId的索引 */    u4  interfacesOff;      /* 接口,指向DexTypeList的偏移,如果没有接口的声明和实现,值为0 */    u4  sourceFileIdx;      /* 类所在的源文件名,指向DexStringId的索引 */    u4  annotationsOff;     /* 注释,根据类型不同会有注解类,注解字段,注解方法,注解参数,没有注解值就是0,指向DexAnnotationsDirectoryItem的结构体 */    u4  classDataOff;       /* 类的数据部分,指向DexClassData结构的偏移 */    u4  staticValuesOff;    /* 类中的静态数据,指向DexEncodeArray结构的偏移 */};

下面同样展示一组真实数据:
 
 
上面数据看到里面的class_data也是一个结构体,然后继续看这个类数据的结构体:
/* expanded form of a class_data_item header */struct DexClassDataHeader {    u4 staticFieldsSize;                /* 静态字段的个数 */    u4 instanceFieldsSize;            /* 实例字段的个数 */    u4 directMethodsSize;                /* 直接方法的个数 */    u4 virtualMethodsSize;            /* 虚方法的个数 */}; /* expanded form of encoded_field */struct DexField {    u4 fieldIdx;    /* 指向DexFieldId的索引 */    u4 accessFlags;    /* 访问标志 */}; /* expanded form of encoded_method */struct DexMethod {    u4 methodIdx;    /* 指向DexMethodId的索引 */    u4 accessFlags;     /* 访问标志 */    u4 codeOff;      /* 指向DexCode结构的偏移 */}; struct DexClassData {    DexClassDataHeader header;                            /* 指定字段和方法的个数 */    DexField*          staticFields;                /* 静态字段 */    DexField*          instanceFields;            /* 实例字段 */    DexMethod*         directMethods;                /* 直接方法 */    DexMethod*         virtualMethods;            /* 虚方法 */};

到这里我们基本看到了在开发中,一个类的所有特征。完整的描述出了一个类的所有信息。
 
9、DexCode上面最后看到方法的代码是通过上面的DexCode结构体来找到的。最后看下这个结构体:
struct DexCode {    u2  registersSize;            /* 使用寄存器的个数 */    u2  insSize;                        /* 参数的个数 */    u2  outsSize;                        /* 调用其他方法时使用的寄存器个数 */    u2  triesSize;                    /* try/catch语句的个数 */    u4  debugInfoOff;       /* 指向调试信息的偏移 */    u4  insnsSize;          /* 指令集的个数,以2字节为单位 */    u2  insns[1];                        /* 指令集 */    /* 2字节空间用于对齐 */    /* followed by try_item[triesSize] DexTry结构体 */    /* followed by uleb128 handlersSize */    /* followed by catch_handler_item[handlersSize] DexCatchHandler结构体 */};

到了这里,存储的就是执行的指令集了。通过执行指令来跑这个方法。下面看一组真实数据:
 
 
这里看到这个类的具体描述字段和函数,还有访问标志等等信息。然后我们继续看里面函数的执行代码部分。看下面一组数据。
 
观察的函数是MainActivity类的DoTcp函数。DexCode(也就是code_item,也叫codeOff)的偏移地址是0x127ec。
 
下面观察到DexCode结构体的偏移到指令集insns字段的偏移是codeOff+2+2+2+2+4+4=0x127fc(这里+的2和4是看下面结构体insns前面的字段占了多少个字节计算的,可以当做固定+16个字节)。指令集的长度是0x93。
 
 
最后看看指令集的开始数据是0x62、0xe26、0x11a、0x232。但是我们要注意前面有说明,这里是两字节空间对齐。所以,这里的值我们应该前面填充一下。
 
前面四个字节我们要看做0x0062、0x0e26、0x011a、0x0232。但是我们还要注意,还有个端序问题会影响字节的顺序,这里是小端序,所以我们再调整下。
 
前面四个字节我们要看做0x6200、0x260e、0x1a01、0x3202。把这段指令集的数据看明白后,我们用gda打开这个dex文件。然后找到对应的方法,查看一下。
 
然后发现数据对上了。这里存储的果然就是我们dex分析方法的字节码了。

9、map_list

在书中的意思是,Dalvik虚拟机解析Dex后,将其映射成DexMapList的数据结构,然后在里面可以找到前面8个部分的偏移和大小。先看看结构体:
struct DexMapItem {    u2 type;              /* kDexType开头的类型 */    u2 unused;                        /* 未使用,用于字节对齐 */    u4 size;              /* 数据的大小 */    u4 offset;            /* 指定类型数据的文件偏移 */};/* * Direct-mapped "map_list". */struct DexMapList {    u4  size;               /* 有多少个DexMapItem */    DexMapItem list[1];     /* entries */}; enum {    kDexTypeHeaderItem               = 0x0000,    kDexTypeStringIdItem             = 0x0001,    kDexTypeTypeIdItem               = 0x0002,    kDexTypeProtoIdItem              = 0x0003,    kDexTypeFieldIdItem              = 0x0004,    kDexTypeMethodIdItem             = 0x0005,    kDexTypeClassDefItem             = 0x0006,    kDexTypeCallSiteIdItem           = 0x0007,    kDexTypeMethodHandleItem         = 0x0008,    kDexTypeMapList                  = 0x1000,    kDexTypeTypeList                 = 0x1001,    kDexTypeAnnotationSetRefList     = 0x1002,    kDexTypeAnnotationSetItem        = 0x1003,    kDexTypeClassDataItem            = 0x2000,    kDexTypeCodeItem                 = 0x2001,    kDexTypeStringDataItem           = 0x2002,    kDexTypeDebugInfoItem            = 0x2003,    kDexTypeAnnotationItem           = 0x2004,    kDexTypeEncodedArrayItem         = 0x2005,    kDexTypeAnnotationsDirectoryItem = 0x2006,};

每个DexMapItem对应了一块数据,例如type=kDexTypeHeaderItem则对应DexHeader的偏移地址和大小。下面看真实数据:
 
 
这里就能看到string_ids的偏移和大小。如此,根据这个map_list就能找到所有块的数据了。
 
那么在源码中是如何使用这个数据的呢,我好奇的翻了一下。然后再dex文件优化的流程中dexSwapAndVerify函数找到了使用的地方。
//字节排序优化int dexSwapAndVerify(u1* addr, size_t len){    ...    if (okay) {        /*         * Look for the map. Swap it and then use it to find and swap         * everything else.         */        if (pHeader->mapOff != 0) {            DexFile dexFile;            DexMapList* pDexMap = (DexMapList*) (addr + pHeader->mapOff);             okay = okay && swapMap(&state, pDexMap);            okay = okay && swapEverythingButHeaderAndMap(&state, pDexMap);             dexFileSetupBasicPointers(&dexFile, addr);            state.pDexFile = &dexFile;             okay = okay && crossVerifyEverything(&state, pDexMap);        } else {            ALOGE("ERROR: No map found; impossible to byte-swap and verify");            okay = false;        }    }        ...    return !okay;       // 0 == success}

这个函数中获取了DexMapList。然后交给swapMap函数来处理。
static bool swapMap(CheckState* state, DexMapList* pMap){    DexMapItem* item = pMap->list;    u4 count;    u4 dataItemCount = 0; // Total count of items in the data section.    u4 dataItemsLeft = state->pHeader->dataSize; // See use below.    u4 usedBits = 0;      // Bit set: one bit per section    bool first = true;    u4 lastOffset = 0;     SWAP_FIELD4(pMap->size);    count = pMap->size;    const u4 sizeOfItem = (u4) sizeof(DexMapItem);    CHECK_LIST_SIZE(item, count, sizeOfItem);     while (count--) {        SWAP_FIELD2(item->type);        SWAP_FIELD2(item->unused);        SWAP_FIELD4(item->size);        SWAP_OFFSET4(item->offset);         if (first) {            first = false;        } else if (lastOffset >= item->offset) {            ALOGE("Out-of-order map item: %#x then %#x",                    lastOffset, item->offset);            return false;        }         if (item->offset >= state->pHeader->fileSize) {            ALOGE("Map item after end of file: %x, size %#x",                    item->offset, state->pHeader->fileSize);            return false;        }         if (isDataSectionType(item->type)) {            u4 icount = item->size;             /*             * This sanity check on the data section items ensures that             * there are no more items than the number of bytes in             * the data section.             */            if (icount > dataItemsLeft) {                ALOGE("Unrealistically many items in the data section: "                        "at least %d", dataItemCount + icount);                return false;            }             dataItemsLeft -= icount;            dataItemCount += icount;        }         u4 bit = mapTypeToBitMask(item->type);         if (bit == 0) {            return false;        }         if ((usedBits & bit) != 0) {            ALOGE("Duplicate map section of type %#x", item->type);            return false;        }         if (item->type == kDexTypeCallSiteIdItem) {            state->pCallSiteIds = item;        } else if (item->type == kDexTypeMethodHandleItem) {            state->pMethodHandleItems = item;        }         usedBits |= bit;        lastOffset = item->offset;        item++;    }     if ((usedBits & mapTypeToBitMask(kDexTypeHeaderItem)) == 0) {        ALOGE("Map is missing header entry");        return false;    }     if ((usedBits & mapTypeToBitMask(kDexTypeMapList)) == 0) {        ALOGE("Map is missing map_list entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeStringIdItem)) == 0)            && ((state->pHeader->stringIdsOff != 0)                    || (state->pHeader->stringIdsSize != 0))) {        ALOGE("Map is missing string_ids entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeTypeIdItem)) == 0)            && ((state->pHeader->typeIdsOff != 0)                    || (state->pHeader->typeIdsSize != 0))) {        ALOGE("Map is missing type_ids entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeProtoIdItem)) == 0)            && ((state->pHeader->protoIdsOff != 0)                    || (state->pHeader->protoIdsSize != 0))) {        ALOGE("Map is missing proto_ids entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeFieldIdItem)) == 0)            && ((state->pHeader->fieldIdsOff != 0)                    || (state->pHeader->fieldIdsSize != 0))) {        ALOGE("Map is missing field_ids entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeMethodIdItem)) == 0)            && ((state->pHeader->methodIdsOff != 0)                    || (state->pHeader->methodIdsSize != 0))) {        ALOGE("Map is missing method_ids entry");        return false;    }     if (((usedBits & mapTypeToBitMask(kDexTypeClassDefItem)) == 0)            && ((state->pHeader->classDefsOff != 0)                    || (state->pHeader->classDefsSize != 0))) {        ALOGE("Map is missing class_defs entry");        return false;    }     state->pDataMap = dexDataMapAlloc(dataItemCount);    if (state->pDataMap == NULL) {        ALOGE("Unable to allocate data map (size %#x)", dataItemCount);        return false;    }     return true;}

通过这里的例子。我们可以看到他是如何使用这个map_list来访问所有的部分的。到这里dex文件的格式基本差不多了。
 
然后我们看看一个实战的项目。大佬写的fart中的py部分就的运用了dex文件格式相关的知识。

2

案例分析

案例一:fart

1、github

https://github.com/hanbinglengyue/FART

2、功能说明

这是一个脱壳工具,使用主动调用的方式来解决二代抽取壳。脱出来的数据不止是dex。还有一种.bin的数据,这种数据可以用来辅助我们修复dex的一些没有脱出来的函数。我们先看下.bin数据是什么,下面是.bin中的一组数据:
{name:ooxx,method_idx:1830,offset:180516,code_item_len:24,ins:AQABAAEAAAB+oAMABAAAAHAQwAsAAA4A};

name:是随意填的,因为并不是使用name来找对应函数,而是通过method_idx。
 
method_idx:函数的索引。
 
offset:函数的偏移。
 
code_item_len:code_item的大小。
 
ins:code_item结构体这段数据的base64编码。
 
另外贴一下这几个数据的dump来源的代码,以防有人把ins当成指令集了。
var base64ptr = funcBase64_encode(ptr(codeitemstartaddr), codeitemlength, ptr(base64lengthptr));var b64content = ptr(base64ptr).readCString(base64lengthptr.readInt());funcFreeptr(ptr(base64ptr));var content = "{name:ooxx,method_idx:" + dex_method_index_ + ",offset:" + dex_code_item_offset_ + ",code_item_len:" + codeitemlength + ",ins:" + b64content + "};";

这几个数据是一个函数最关键的片段。拿到就可以还原出最关键的code_item了。

3、使用

然后看看fart.py的使用./fart.py -d 431528_29868.dex -i 431528_29868.bin。
下面是执行结果,基本对这个dex进行完整的解析,打印出来大多数的数据。
 

4、源码分析

我们可以通过对这个项目的阅读,直观的了解到是如何进行dex文件格式进行解析的。下面开始看看具体的实现流程:
def main():    #加载dex文件    dex = dex_parser(filename)if __name__ == "__main__":    #获取到参数filename和insfilename    init()    methodTable.clear()    #加载.bin文件的内容,也就是insfilename设置的文件,给methodTable填充上值。每项的数据是:方法名称,方法id,方法偏移,方法大小,指令集    parseinsfile()    print "methodTable length:" + str(len(methodTable))    #开始处理    main()

然后看看是如何加载.bin文件的:
def parseinsfile():    global insfilename    insfile=open(insfilename)    content=insfile.read()    insfile.close()    #;{name:artMethod::dumpmethod DexFile_dumpDexFile'    # dexfile name:classes.dex--    # insfilepath:/data/data/com.wlqq/10668484_ins.bin--    # code_item_len:40,    # code_item_len:40,    # ins:AgABAAIAAABLnY4ADAAAACIAFwNwEPoOAABuIP4OEAAMAR8BFwMRAQ==};    insarray=re.findall(r"{name:(.*?),method_idx:(.*?),offset:(.*?),code_item_len:(.*?),ins:(.*?)}",content) #(.*?)最短匹配    #按照我们前面看到的格式进行匹配数据并遍历    for eachins in insarray:      #这里其实是固定的ooxx      methodname=eachins[0].replace(" ","")      number=(int)(eachins[1])      offset=(int)(eachins[2])      inssize=int(eachins[3])      ins=eachins[4]      tempmethod=CodeItem(number,methodname,inssize,ins)      methodTable[number]=tempmethod #添加method

可以看到就是遍历,组装好那些数据,最后保存到一个methodTable里面,索引就是method_id。
 
再看看最关键的dex_parse:
class dex_parser:     def __init__(self,filename):        #dex的标志        global DEX_MAGIC        #odex的标志        global DEX_OPT_MAGIC        self.m_javaobject_id = 0        #dex的文件路径        self.m_filename = filename        self.m_fd = open(filename,"rb")        self.m_content = self.m_fd.read()        self.m_fd.close()        self.m_dex_optheader = None        self.m_class_name_id = {}        self.string_table = []        #如果发现是odex文件,则填充opt_header,否则只填充dex_header        if self.m_content[0:4] == DEX_OPT_MAGIC:            self.init_optheader(self.m_content)            self.init_header(self.m_content,0x40)        elif self.m_content[0:4] == DEX_MAGIC:            self.init_header(self.m_content,0)        #上面填充dex_header的时候取到的string_ids的偏移位置和大小        bOffset = self.m_stringIdsOff        if self.m_stringIdsSize > 0:            #遍历字符串列表            for i in xrange(0,self.m_stringIdsSize):                #这里是取出每个字符串的偏移地址                offset, = struct.unpack_from("I",self.m_content,bOffset + i * 4)                #如果是第一个则直接存放到start,然后处理下一次                if i == 0:                    start = offset                else:                    #取出上一个偏移的字符串的偏移地址,这里由于存储格式是uleb128的。要转换成真正的偏移地址。                    skip, length = get_uleb128(self.m_content[start:start+5])                    #上面还原出了真实的偏移,这里取字符串的地址,保存起来                    self.string_table.append(self.m_content[start+skip:offset-1])                    start = offset            #处理最后的一条            for i in xrange(start,len(self.m_content)):                if self.m_content[i]==chr(0):                    self.string_table.append(self.m_content[start+1:i])                    break        #遍历classDef。填充m_class_name_id        for i in xrange(0,self.m_classDefSize):            str1 = self.getclassname(i)            self.m_class_name_id[str1] = i        #遍历classDef,填充classdef相关的属性,并且打印,合并.bin的内容是在print中进行的。        for i in xrange(0,self.m_classDefSize):            str1 = self.getclassname(i)            dex_class(self,i).printf(self)            pass

这里看到是通过init_header来解析dex文件中的dexHeader的。我就不贴代码了,整体就是偏移然后取数据。
 
这里有一点要说的是get_uleb128函数,uleb128在android里面是一个特殊的类型。是一个可变长度的类型。

大致意思就是例如第一个字节的最高位,如果是1,则第二个字节也是有效数据,如果第二个字节的最高位也是1,下一个字节也是有效数据,如果最高位不是1,就结束了。最后左移拼接就ok了。

 
这个类型有没有很眼熟?没错,特别像是protobuf里面的varint编码。所以我觉得完全可以用protobuf包自带的varint解码来获取这个数据。也可以用这种自己写的函数来处理。
def varint_encode(number):    buf = b''    while True:        towrite = number & 0x7f        number >>= 7        if number:            buf += struct.pack("B",(towrite | 0x80))        else:            buf +=  struct.pack("B",towrite)            break    return bufdef varint_decode(buff):    shift = 0    result = 0    idx=0    while True:        if idx>len(buff):            return ""        i = buff[idx]        idx+=1        result |= (i & 0x7f) << shift        shift += 7        if not (i & 0x80):            break    return result

继续看后面的关键函数dex_class,这里来处理每一个类的打印。
 
先看看初始化函数,基本和之前的initHeader的差不多,就是偏移取数据,然后保存下来。把classDef相关的数据都读取出来了。
class dex_class:    def __init__(self,dex_object,classid):        if classid >= dex_object.m_classDefSize:            return ""        offset = dex_object.m_classDefOffset + classid * struct.calcsize("8I")        self.offset = offset        format = "I"        self.thisClass,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.modifiers,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.superClass,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.interfacesOff,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.sourceFileIdx,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.annotationsOff,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.classDataOff,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.staticValuesOff,=struct.unpack_from(format,dex_object.m_content,offset)        offset += struct.calcsize(format)        self.index = classid        self.interfacesSize = 0        if self.interfacesOff != 0:            self.interfacesSize, = struct.unpack_from("I",dex_object.m_content,self.interfacesOff)        if self.classDataOff != 0:            offset = self.classDataOff            count,self.numStaticFields = get_uleb128(dex_object.m_content[offset:])            offset += count            count,self.numInstanceFields = get_uleb128(dex_object.m_content[offset:])            offset += count            count,self.numDirectMethods = get_uleb128(dex_object.m_content[offset:])            offset += count            count,self.numVirtualMethods = get_uleb128(dex_object.m_content[offset:])           else:            self.numStaticFields = 0            self.numInstanceFields = 0            self.numDirectMethods = 0            self.numVirtualMethods = 0

最后就是print函数,这里比较大,就只挑最关键的部分出来说下。
def printf(self,dex_object):    ...    print "=========numDirectMethods[%d]=numVirtualMethods[%d]=numStaticMethods[0]========="%(self.numDirectMethods,self.numVirtualMethods)    method_idx = 0# 遍历实例函数    for i in xrange(0,self.numDirectMethods):     #获取到method_id        n,method_idx_diff = get_uleb128(dex_object.m_content[offset:offset+5])        offset += n        n,access_flags = get_uleb128(dex_object.m_content[offset:offset+5])        offset += n        n,code_off = get_uleb128(dex_object.m_content[offset:offset+5])        offset += n  #这里看到method_idx实际上是累加的。我们可以找例子去看下。例如第一个函数的idx是0xd2。第二个的idx是1,实际上是0xd3。也就是上一个的值+1        method_idx += method_idx_diff        if code_off != 0:            methodname=dex_object.getmethodfullname(method_idx,True).replace("::",".").replace(" ","")            method=None            try:      #这里获取了之前的.bin文件中对应的数据                method = methodTable[method_idx]            except Exception as e:                pass            if method != None:      #如果bin文件中有这个函数,则先打印下dex中的对应函数                print "\nDirectMethod:" + dex_object.getmethodfullname(method_idx, True) + "\n"                try:                    print "before repire method+++++++++++++++++++++++++++++++++++\n"                    method_code(dex_object, code_off).printf(dex_object, "\t\t")                except Exception as e:                    print e      #然后再把bin文件中的给转换成method_code,然后打印                try:                    bytearray_str = base64.b64decode(method.insarray)                    print "after repire method++++++++++++++++++++++++++++++++++++\n"                    repired_method_code(dex_object, bytearray_str).printf(dex_object, "\t\t")                except Exception as e:                    print e    ...

最后我们看下怎么获取的bin文件数据的函数:
class repired_method_code:    dex_obj=None    content = ""    trylist = []    def __init__(self, dex_obj,content):        offset=0        format = "H"        self.dex_obj=dex_obj        self.content=content    # 这段数据我们之前看到就是存的DexCode的结构体,所以直接按格式进行读取数据出来        self.registers_size, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        self.ins_size, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        self.outs_size, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        self.tries_size, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        format = "I"        self.debug_info_off, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        self.insns_size, = struct.unpack_from(format, content, offset)        offset += struct.calcsize(format)        self.insns = offset        offset += 2 * self.insns_size        if self.insns_size % 2 == 1:            offset += 2        if self.tries_size == 0:            self.tries = 0            self.handlers = 0        else:            self.handlerlist_offset = offset + 8 * self.tries_size            self.tries = offset            for i in range(0, self.tries_size):                temptryitem = tryitem(self.dex_obj,content, self.handlerlist_offset, offset + 8 * i)                self.trylist.append(temptryitem)            self.handlers = offset + self.tries_size * struct.calcsize("I2H")  #

最后还有个打印指令集的部分就不贴了。感兴趣的可以自己看一看。

案例二:dex2jar

1、github

https://github.com/pxb1988/dex2jar

2、功能说明

这个工具基本大家都用过。功能非常强大,不过这里只分析下d2j_dex2jar功能。也就是把dex给转换成jar文件。

3、使用

下载release版本直接./d2j-dex2jar.sh classes.dex,就生成出了对应的classes-dex2jar.jar文件。然后我们直接用其他分析工具就能愉快的看java代码了。那么神奇的事情是如何做到的呢。下面跟踪分析一下大佬的作品。

4、源码分析

第一步是找到入口,根据我们上面的使用例子,先搜索下d2j-dex2jar。然后找到了下面的文件:
 
./dex2jar/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
 
先简单的看一下这里的代码,看来这里就是入口函数了。
@BaseCmd.Syntax(cmd = "d2j-dex2jar", syntax = "[options] <file0> [file1 ... fileN]", desc = "convert dex to jar")public class Dex2jarCmd extends BaseCmd {     public static void main(String... args) {        new Dex2jarCmd().doMain(args);    }    ...}

然后看看doMain的实现:
public void doMain(String... args) {        ...        doCommandLine();        ...    }

这里调用了抽象方法,doCommandLine,所以继续看看实现。
protected void doCommandLine() throws Exception {        ...        for (String fileName : remainingArgs) {            // long baseTS = System.currentTimeMillis();            String baseName = getBaseName(new File(fileName).toPath());              //这里看到默认使用的jar文件名            Path file = output == null ? currentDir.resolve(baseName + "-dex2jar.jar") : output;            System.err.println("dex2jar " + fileName + " -> " + file);                        //BaseDexFileReader这个类型就是dex的所有解析处理            BaseDexFileReader reader = MultiDexFileReader.open(Files.readAllBytes(new File(fileName).toPath()));              //用来处理异常信息的            BaksmaliBaseDexExceptionHandler handler = notHandleException ? null : new BaksmaliBaseDexExceptionHandler();              //这句就是最关键的转换部分,前面都是设置转换的一些参数,最后的to用来执行dex转换jar            Dex2jar.from(reader).withExceptionHandler(handler).reUseReg(reuseReg).topoLogicalSort()                    .skipDebug(!debugInfo).optimizeSynchronized(this.optmizeSynchronized).printIR(printIR)                    .noCode(noCode).skipExceptions(skipExceptions).to(file);                        //有异常的话,就通过异常处理handler保存信息            if (!notHandleException) {                if (handler.hasException()) {                    Path errorFile = exceptionFile == null ? currentDir.resolve(baseName + "-error.zip")                            : exceptionFile;                    System.err.println("Detail Error Information in File " + errorFile);                    System.err.println(BaksmaliBaseDexExceptionHandler.REPORT_MESSAGE);                    handler.dump(errorFile, orginalArgs);                }            }            // long endTS = System.currentTimeMillis();            // System.err.println(String.format("%.2f", (float) (endTS - baseTS) / 1000));        }    }

到这里就可以看出来两个最重要的部分了:
 
1、解析dex的 MultiDexFileReader.open方法;
 
2、执行转换的Dex2jar的to方法。
 
下面先看看解析的处理:
public static BaseDexFileReader open(byte[] data) throws IOException {              //dex文件太小就直接抛出异常        if (data.length < 3) {            throw new IOException("File too small to be a dex/zip");        }        if ("dex".equals(new String(data, 0, 3, StandardCharsets.ISO_8859_1))) {// dex            return new DexFileReader(data);        } else if ("PK".equals(new String(data, 0, 2, StandardCharsets.ISO_8859_1))) {// ZIP              //可以看到如果是zip文件,它会帮我们在里面找classes开头并且.dex结尾的文件来处理,也就是说直接参数使用apk也是可以的。            TreeMap<String, DexFileReader> dexFileReaders = new TreeMap<>();            try (ZipFile zipFile = new ZipFile(data)) {                for (ZipEntry e : zipFile.entries()) {                    String entryName = e.getName();                    if (entryName.startsWith("classes") && entryName.endsWith(".dex")) {                        if (!dexFileReaders.containsKey(entryName)) { // only the first one                            dexFileReaders.put(entryName, new DexFileReader(toByteArray(zipFile.getInputStream(e))));                        }                    }                }            }              //下面是单dex文件和多dex文件的处理            if (dexFileReaders.size() == 0) {                throw new IOException("Can not find classes.dex in zip file");            } else if (dexFileReaders.size() == 1) {                return dexFileReaders.firstEntry().getValue();            } else {                return new MultiDexFileReader(dexFileReaders.values());            }        }        throw new IOException("the src file not a .dex or zip file");    }

我们直接看参数为dex文件的情况就好了,所以继续看DexFileReader的构造函数:
//直接调用了另一个重载public DexFileReader(byte[] data) {        this(ByteBuffer.wrap(data));    }//解析dex的关键位置public DexFileReader(ByteBuffer in) {        in.position(0);        in = in.asReadOnlyBuffer().order(ByteOrder.BIG_ENDIAN);        int magic = in.getInt() & 0xFFFFFF00;         final int MAGIC_DEX = 0x6465780A & 0xFFFFFF00;// hex for 'dex ', ignore the 0A        final int MAGIC_ODEX = 0x6465790A & 0xFFFFFF00;// hex for 'dey ', ignore the 0A                //这里区分一下是dex还是odex。odex情况就直接抛出异常了        if (magic == MAGIC_DEX) {            ;        } else if (magic == MAGIC_ODEX) {            throw new DexException("Not support odex");        } else {            throw new DexException("not support magic.");        }              //下面是取出dexHeader相关的一系列数据了        int version = in.getInt() >> 8;        if (version < 0 || version < DEX_035) {            throw new DexException("not support version.");        }        this.dex_version = version;        in.order(ByteOrder.LITTLE_ENDIAN);         // skip uint checksum        // and 20 bytes signature        // and uint file_size        // and uint header_size 0x70              // 意思是偏移跳过上面的那些数据,跳过是根据上面的字段占的空间来直接偏移即可        skip(in, 4 + 20 + 4 + 4);                //这个是cpu字节序,非小端序就直接抛出异常了。        int endian_tag = in.getInt();        if (endian_tag != ENDIAN_CONSTANT) {            throw new DexException("not support endian_tag");        }         // skip uint link_size        // and uint link_off              //再跳过上面的两个字段        skip(in, 4 + 4);                //把下面那些重要部分的偏移全部取出来保存。        int map_off = in.getInt();        string_ids_size = in.getInt();        int string_ids_off = in.getInt();        type_ids_size = in.getInt();        int type_ids_off = in.getInt();        proto_ids_size = in.getInt();        int proto_ids_off = in.getInt();        field_ids_size = in.getInt();        int field_ids_off = in.getInt();        method_ids_size = in.getInt();        int method_ids_off = in.getInt();        class_defs_size = in.getInt();        int class_defs_off = in.getInt();        // skip uint data_size data_off         int call_site_ids_off = 0;        int call_site_ids_size = 0;        int method_handle_ids_off = 0;        int method_handle_ids_size = 0;              //如果是高版本的相关处理(大于DEX_037的版本我好像没见过。)        if (dex_version > DEX_037) {            in.position(map_off);            int size = in.getInt();            for (int i = 0; i < size; i++) {                int type = in.getShort() & 0xFFFF;                in.getShort(); // unused;                int item_size = in.getInt();                int item_offset = in.getInt();                switch (type) {                case TYPE_CALL_SITE_ID_ITEM:                    call_site_ids_off = item_offset;                    call_site_ids_size = item_size;                    break;                case TYPE_METHOD_HANDLE_ITEM:                    method_handle_ids_off = item_offset;                    method_handle_ids_size = item_size;                    break;                default:                    break;                }            }        }              //看这个意思是只有DEX_037以上的dex才有这个值,低版本默认0即可。        this.call_site_ids_size = call_site_ids_size;        this.method_handle_ids_size = method_handle_ids_size;                //直接从内存中把这些重要数据的块给切片出来单独存放,后面使用就不需要全部通过偏移来找了。              //这里的长度计算,是根据各个结构体的大小*列表长度计算。        stringIdIn = slice(in, string_ids_off, string_ids_size * 4);        typeIdIn = slice(in, type_ids_off, type_ids_size * 4);        protoIdIn = slice(in, proto_ids_off, proto_ids_size * 12);        fieldIdIn = slice(in, field_ids_off, field_ids_size * 8);        methoIdIn = slice(in, method_ids_off, method_ids_size * 8);        classDefIn = slice(in, class_defs_off, class_defs_size * 32);              //下面这两个不用在意把,DEX_037以上的版本才有        callSiteIdIn = slice(in, call_site_ids_off, call_site_ids_size * 4);        methodHandleIdIn = slice(in, method_handle_ids_off, method_handle_ids_size * 8);                //初始化下面这些数据,并且设置好字节序,这里还没设置偏移来着。感觉应该用一个变量就行了。        in.position(0);        annotationsDirectoryItemIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        annotationSetItemIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        annotationItemIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        annotationSetRefListIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        classDataIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        codeItemIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        stringDataIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        encodedArrayItemIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        typeListIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);        debugInfoIn = in.duplicate().order(ByteOrder.LITTLE_ENDIAN);    }

上面看了dexHeader的解析的核心部分,接下来我们看看转换实现to方法:
public void to(Path file) throws IOException {        if (Files.exists(file) && Files.isDirectory(file)) {            doTranslate(file);        } else {            try (FileSystem fs = createZip(file)) {                doTranslate(fs.getPath("/"));            }        }    }

继续看看doTranslate方法:
private void doTranslate(final Path dist) throws IOException {                ...        DexFileNode fileNode = new DexFileNode();        try {              //这里的reader就是我们前面看到的那个解析dexHeader的处理,这里就是完整解析dex转换成fileNode            reader.accept(fileNode, readerConfig | DexFileReader.IGNORE_READ_EXCEPTION);        } catch (Exception ex) {            exceptionHandler.handleFileException(ex);        }        ...                //.convertDex(fileNode, cvf);        最后调用的这个函数是把前面转换好的fileNode给转换成.class文件。最后打包成jar    }

我们主要观察的是dex的结构和解析,所以就不详细看转换部分了,继续看解析部分的后续accept方法。
//这里遍历所有class_def,然后调用另一个重载public void accept(DexFileVisitor dv, int config) {        dv.visitDexFileVersion(this.dex_version);        for (int cid = 0; cid < class_defs_size; cid++) {            accept(dv, cid, config);        }        dv.visitEnd();    } public void accept(DexFileVisitor dv, int classIdx, int config) {              //根据classdef索引找到偏移位置        classDefIn.position(classIdx * 32);              //解析出classdef的各项字段        int class_idx = classDefIn.getInt();        int access_flags = classDefIn.getInt();        int superclass_idx = classDefIn.getInt();        int interfaces_off = classDefIn.getInt();        int source_file_idx = classDefIn.getInt();        int annotations_off = classDefIn.getInt();        int class_data_off = classDefIn.getInt();        int static_values_off = classDefIn.getInt();         String className = getType(class_idx);              //这里看到可以设置忽略的classname,他这里是空的,固定返回false        if(ignoreClass(className)) return;        String superClassName = getType(superclass_idx);        String[] interfaceNames = getTypeList(interfaces_off);        try {              //visit这个是一个保存的功能,把值保存在了那个fileNode的里面。            DexClassVisitor dcv = dv.visit(access_flags, className, superClassName, interfaceNames);            if (dcv != null)// 不处理            {                  //拿到了classdef的相关数据的偏移位置,再根据这些数据对类进行详细解析                acceptClass(dcv, source_file_idx, annotations_off, class_data_off, static_values_off, config);                dcv.visitEnd();            }        } catch (Exception ex) {            DexException dexException = new DexException(ex, "Error process class: [%d]%s", class_idx, className);            if (0 != (config & IGNORE_READ_EXCEPTION)) {                niceExceptionMessage(dexException, 0);            } else {                throw dexException;            }        }    }

这里最关键的就是acceptClass来解析具体的类的内容。
private void acceptClass(DexClassVisitor dcv, int source_file_idx, int annotations_off, int class_data_off,            int static_values_off, int config) {        if ((config & SKIP_DEBUG) == 0) {            // 获取源文件            if (source_file_idx != -1) {                dcv.visitSource(this.getString(source_file_idx));            }        }                //字段的注释        Map<Integer, Integer> fieldAnnotationPositions;              //方法的注释        Map<Integer, Integer> methodAnnotationPositions;              //参数的注释        Map<Integer, Integer> paramAnnotationPositions;        if ((config & SKIP_ANNOTATION) == 0) {            // 获取注解            fieldAnnotationPositions = new HashMap<Integer, Integer>();            methodAnnotationPositions = new HashMap<Integer, Integer>();            paramAnnotationPositions = new HashMap<Integer, Integer>();              // 如果有注释的偏移,下面则解析出注释的数据,保存到上面的对应map中            if (annotations_off != 0) { // annotations_directory_item                 annotationsDirectoryItemIn.position(annotations_off);                 int class_annotations_off = annotationsDirectoryItemIn.getInt();                int field_annotation_size = annotationsDirectoryItemIn.getInt();                int method_annotation_size = annotationsDirectoryItemIn.getInt();                int parameter_annotation_size = annotationsDirectoryItemIn.getInt();                 for (int i = 0; i < field_annotation_size; i++) {                    int field_idx = annotationsDirectoryItemIn.getInt();                    int field_annotations_offset = annotationsDirectoryItemIn.getInt();                    fieldAnnotationPositions.put(field_idx, field_annotations_offset);                }                for (int i = 0; i < method_annotation_size; i++) {                    int method_idx = annotationsDirectoryItemIn.getInt();                    int method_annotation_offset = annotationsDirectoryItemIn.getInt();                    methodAnnotationPositions.put(method_idx, method_annotation_offset);                }                for (int i = 0; i < parameter_annotation_size; i++) {                    int method_idx = annotationsDirectoryItemIn.getInt();                    int parameter_annotation_offset = annotationsDirectoryItemIn.getInt();                    paramAnnotationPositions.put(method_idx, parameter_annotation_offset);                }                                // 如果有对类的注释偏移                if (class_annotations_off != 0) {                    try {                        read_annotation_set_item(class_annotations_off, dcv);                    } catch (Exception e) {                        throw new DexException("error on reading Annotation of class ", e);                    }                }            }        } else {            fieldAnnotationPositions = null;            methodAnnotationPositions = null;            paramAnnotationPositions = null;        }                //类详细数据的解析        if (class_data_off != 0) {            ByteBuffer in = classDataIn;            in.position(class_data_off);                        //静态字段            int static_fields = (int) readULeb128i(in);              //实例字段            int instance_fields = (int) readULeb128i(in);              //实例函数            int direct_methods = (int) readULeb128i(in);              //虚函数            int virtual_methods = (int) readULeb128i(in);            {                int lastIndex = 0;                {                    Object[] constant = null;                    if ((config & SKIP_FIELD_CONSTANT) == 0) {                        if (static_values_off != 0) {                            constant = read_encoded_array_item(static_values_off);                        }                    }                    for (int i = 0; i < static_fields; i++) {                        Object value = null;                        if (constant != null && i < constant.length) {                            value = constant[i];                        }                          // 解析并填充字段                        lastIndex = acceptField(in, lastIndex, dcv, fieldAnnotationPositions, value, config);                    }                }                lastIndex = 0;                for (int i = 0; i < instance_fields; i++) {                      // 解析并填充字段                    lastIndex = acceptField(in, lastIndex, dcv, fieldAnnotationPositions, null, config);                }                lastIndex = 0;                boolean firstMethod = true;                for (int i = 0; i < direct_methods; i++) {                      // 解析并填充方法                    lastIndex = acceptMethod(in, lastIndex, dcv, methodAnnotationPositions, paramAnnotationPositions,                            config, firstMethod);                    firstMethod = false;                }                lastIndex = 0;                firstMethod = true;                for (int i = 0; i < virtual_methods; i++) {                      // 解析并填充方法                    lastIndex = acceptMethod(in, lastIndex, dcv, methodAnnotationPositions, paramAnnotationPositions,                            config, firstMethod);                    firstMethod = false;                }            }         }    }

对类数据进行详细解析后,接着是对字段和方法的解析填充。先看看字段的解析处理:
//字段数据的解析获取private Field getField(int id) {        fieldIdIn.position(id * 8);        int owner_idx = 0xFFFF & fieldIdIn.getShort();        int type_idx = 0xFFFF & fieldIdIn.getShort();        int name_idx = fieldIdIn.getInt();        return new Field(getType(owner_idx), getString(name_idx), getType(type_idx));    } private int acceptField(ByteBuffer in, int lastIndex, DexClassVisitor dcv,            Map<Integer, Integer> fieldAnnotationPositions, Object value, int config) {        int diff = (int) readULeb128i(in);        int field_access_flags = (int) readULeb128i(in);        int field_id = lastIndex + diff;              // 取出字段的数据        Field field = getField(field_id);              // 下面是直接填充字段内容        // //////////////////////////////////////////////////////////////        DexFieldVisitor dfv = dcv.visitField(field_access_flags, field, value);        if (dfv != null) {            if ((config & SKIP_ANNOTATION) == 0) {                  //字段注释的相关处理                Integer annotation_offset = fieldAnnotationPositions.get(field_id);                if (annotation_offset != null) {                    try {                        read_annotation_set_item(annotation_offset, dfv);                    } catch (Exception e) {                        throw new DexException(e, "while accept annotation in field:%s.", field.toString());                    }                }            }            dfv.visitEnd();        }        // //////////////////////////////////////////////////////////////        return field_id;    }

字段填充完毕,然后看看方法是怎么解析填充的。
//方法数据的解析获取private Method getMethod(int id) {        methoIdIn.position(id * 8);        int owner_idx = 0xFFFF & methoIdIn.getShort();        int proto_idx = 0xFFFF & methoIdIn.getShort();        int name_idx = methoIdIn.getInt();        return new Method(getType(owner_idx), getString(name_idx), getProto(proto_idx));    } private int acceptMethod(ByteBuffer in, int lastIndex, DexClassVisitor cv, Map<Integer, Integer> methodAnnos,            Map<Integer, Integer> parameterAnnos, int config, boolean firstMethod) {        int offset = in.position();        int diff = (int) readULeb128i(in);        int method_access_flags = (int) readULeb128i(in);        int code_off = (int) readULeb128i(in);        int method_id = lastIndex + diff;        Method method = getMethod(method_id);                ...        try {              //填充方法数据            DexMethodVisitor dmv = cv.visitMethod(method_access_flags, method);            if (dmv != null) {                if ((config & SKIP_ANNOTATION) == 0) {                      //处理方法的注释                    Integer annotation_offset = methodAnnos.get(method_id);                    if (annotation_offset != null) {                        try {                            read_annotation_set_item(annotation_offset, dmv);                        } catch (Exception e) {                            throw new DexException(e, "while accept annotation in method:%s.", method.toString());                        }                    }                      //处理参数的注释                    Integer parameter_annotation_offset = parameterAnnos.get(method_id);                    if (parameter_annotation_offset != null) {                        try {                            read_annotation_set_ref_list(parameter_annotation_offset, dmv);                        } catch (Exception e) {                            throw new DexException(e, "while accept parameter annotation in method:%s.",                                    method.toString());                        }                    }                }                  //如果有code_item。还继续进行指令的解析                if (code_off != 0) {                    boolean keep = true;                    if (0 != (SKIP_CODE & config)) {                        keep = 0 != (KEEP_CLINIT & config) && method.getName().equals("<clinit>");                    }                    if(keep) {                        DexCodeVisitor dcv = dmv.visitCode();                        if (dcv != null) {                            try {                                  //解析并填充code_item的数据                                acceptCode(code_off, dcv, config, (method_access_flags & DexConstants.ACC_STATIC) != 0,                                        method);                            } catch (Exception e) {                                throw new DexException(e, "while accept code in method:[%s] @%08x", method.toString(),                                        code_off);                            }                        }                    }                }                dmv.visitEnd();            }        } catch (Exception e) {            throw new DexException(e, "while accept method:[%s]", method.toString());        }         return method_id;    }

继续看code_item的解析:
/* package */void acceptCode(int code_off, DexCodeVisitor dcv, int config, boolean isStatic, Method method) {        ByteBuffer in = codeItemIn;        in.position(code_off);              //取出code_item的字段数据        int registers_size = 0xFFFF & in.getShort();        in.getShort();// ins_size ushort        in.getShort();// outs_size ushort        int tries_size = 0xFFFF & in.getShort();        int debug_info_off = in.getInt();              //指令长度        int insns = in.getInt();                //这里是指令集,好像是因为指令是2个字节为一个单位的。所以要*2        byte[] insnsArray = new byte[insns * 2];        in.get(insnsArray);        dcv.visitRegister(registers_size);        BitSet nextInsn = new BitSet();        Map<Integer, DexLabel> labelsMap = new TreeMap<Integer, DexLabel>();        Set<Integer> handlers = new HashSet<Integer>();        // 处理异常处理        if (tries_size > 0) {            if ((insns & 0x01) != 0) {// skip padding                in.getShort();            }            if (0 == (config & SKIP_EXCEPTION)) {                findTryCatch(in, dcv, tries_size, insns, labelsMap, handlers);            }        }        // 处理debug信息        if (debug_info_off != 0 && (0 == (config & SKIP_DEBUG))) {            DexDebugVisitor ddv = dcv.visitDebug();            if (ddv != null) {                read_debug_info(debug_info_off, registers_size, isStatic, method, labelsMap, ddv);                ddv.visitEnd();            }        }         BitSet badOps = new BitSet();        findLabels(insnsArray, nextInsn, badOps, labelsMap, handlers, method);              //解析并填充指令集        acceptInsn(insnsArray, dcv, nextInsn, badOps, labelsMap);        dcv.visitEnd();    }

最后的acceptInsn指令集的解析方法太大了,我就不放上来了。整个解析填充的流程就完成了。后面就使用解析好的数据进行转换的操作。
 
可以看到两个案例的解析的方式差不多,总体都是根据结构体的大小偏移来取得想要的数据。最后一层一层的处理。
 
为了方便使用和测试,我将fart的解析整理了一下,改到了python3运行的。然后整合到了我的整合怪里面。感兴趣的可以看看。刚跑通,不知道有没啥问题,后面我再慢慢修复把。
 
github:https://github.com/dqzg12300/fridaUiTools
 
贴上效果图:
 

疑问

fart的修复方案仅仅是打印出了保存的指令数据。如果我们是想要直接转成.class文件或者是jar文件,是否可行呢?
 
我设想了两种方案来做,但是最后都被自己给否定了。
 
1、bin文件中的指令数据直接插入到dex文件的对应数据中,最后保存为一个新的文件。但是这样中间插一段数据,会有大量的偏移数据要修改。
 
2、和dex2jar的模式一样。构造好一个fileNode对象,里面就不存在偏移的问题了。最后把里面的类数据导出成.class文件。

新思路

疑问中的目的主要是想要fart.py跑完后直接输出一个新的dex文件,里面把bin文件的函数都填充到新的dex了,然后可以用jadx之类的工具直接打开查看。
 
最早先从修复的角度想,简单的处理就是把bin中的内容解析成codeitem后,插到method_idx的里面对应的函数去。但是由于指令集的大小和原来不一样。肯定会导致后面数据的偏移全部发生变化。
 
于是我分析了一下dex2jar,然后发现确实有更好的办法。
 
和原来的目前一样,但是把修复两个字调整一下说法,具体应该叫dex转换dex。

先看看dex2jar的做法是:解析dex,dex数据全部展开解析为java结构体,与偏移无关了,然后转换.class的结构,写入文件,最后打包成jar。
 
学习到人家的思路之后,我们可以变化下自己的思路,下面是我设想的两个解决方案。目前还没有做,但是感觉可行性非常高。
 
1、fart修改的解决方案

fart中定义dex的完整结构体,fart解析完dex后,直接将数据保存在结构体中,达到了偏移无关了。然后在遍历bin的数据。将code_item数据替换。然后再将整个dex结构体重新解析生成一个新的dex。
 
2、dex2jar的功能新增解决方案

就是给dex2jar新增一个功能。前面完全按照他的方式解析出fileNode,然后读取bin文件解析出code_item。接着替换fileNode中对应的code_item数据。最后再重新生成回dex文件。

 

看雪ID:misskings

https://bbs.pediy.com/user-home-659397.htm

*本文由看雪论坛 misskings 原创,转载请注明来自看雪社区

# 往期推荐

1. cocos2d逆向入门和某捕鱼游戏分析

2. Cocos2dlua手游 Lua解密与资源解密实战

3. Windows驱动编程之NetFilter SDK

4. GlobeImposter家族的病毒样本分析

5. CVE-2010-2553 堆溢出漏洞分析

6. 使用unidbg破解孤挺花字符串混淆并修复so

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458389482&idx=1&sn=53358382677e9b89124a5b8a0ce5dad2&chksm=b18f3b6086f8b276849d392325008e63e3419c999561c1d19bcc76825e6f0e2a95a3ec29bf24#rd
如有侵权请联系:admin#unsafe.sh