2018年4月

一、逆向分析
有一个应用Fiddler抓包失败,其实对于这类问题:第一你是否安装Fiddler证书了。第二否用了Xposed框架JustTrustMe。的确有这个问题:
2018040417172352.jpg
看到了,这里就是这样的提示,也没看到具体的请求参数啥的,所以这个就引发了我的兴趣,那么不多说了直接爆破吧,突破口很简单,直接全局搜这个接口api.izuiyou.com即可:
20180405104301869.jpg
搜索结果很多,主要看纯接口那个,通过两三次的点击浏览最后定位到这一处:
20180404171910945.jpg
到这里就看到了,其实他内部用的是okhttp进行网络请求的,使用AsyncTask类进行操作,但是在构建okhttp的时候他做了很多设置操作,首先是参数设置:
20180405110324215.jpg
看到这个c类其实是设置okhttp的拦截器,这里简单看一下应该是post方式请求会把参数进行加密处理,我们看看那个加密代码:
20180404172121991.jpg
这个不多说了,依然采用底层加密操作,到这里我们就直接hook它:
20180405110518102.jpg
20180404172204484.jpg
然后安装模块运行即可,然后我们手机不挂代理访问,这样就能正常拉取数据了,看看加密的都是啥信息,我们在首页进行下拉刷新操作:
20180404172254898.jpg
看到了下拉数据接口是recommend的,然后参数就是请求参数信息,看到最后加密之后的只有一个sign字段了,应该是把参数放到so层进行加密处理了,那么这个加密逻辑不是本文的介绍重点,后续会继续单独介绍如何动态调试so来弄出加密算法。继续回到刚才设置了okhttp的拦截器之后,就设置okhttp的SSLSocketFactory和X509TrustManager值就是和证书相关的:
20180405104932102.jpg
然后这里他自定义了一个SSLSocketFactory类,这个类中可以自定义的Socket信息:
20180405105027235.jpg
一般都是在回调方法createSocket中处理通信的Socket信息:
20180405110750351.jpg
然后就是okhttp的设置证书的方法调用了,后面的解决方案就是hook这个方法来
20180404172513656.jpg
当然还有设置域名检查的方法HostnameVerifier,这里不多介绍了。那么现在可以看到抓包失败可能就是这三个值引起的,这个大家一定要熟悉okhttp框架的大致用法和原理,而且现在几乎很多应用都在用这个okhttp框架了,并且google官方已经把这个框架集成到系统中替换原来的apache的http框架了。所以后续如果抓包失败都可以直接分析应用中的这个框架即可。

到这里我们大致找到关键点了,不过这里需要说明一个现象就是抓包失败,在应用界面会看到有一个loading一直在转圈圈也就是这里有一个超时时间,诡异的是如果超时了,数据可以正常加载。但是这个超时太长了几乎几分钟。虽然最终可以看到数据也可以抓到包(文章开始说抓不到包是因为太慢了没心等待)。但是得解决这个问题不然等几分钟没法进行后面操作了。所以还得继续看这个问题吧。上面已经定位到问题大致就是okhttp设置了证书那些值导致的问题,那么是哪些值呢?我们先看看SSLSocketFactory这个类:

20180405110839630.jpg
这个类的createSocket方法中处理了很多逻辑,那么我们在hook这个方法打印参数信息:
20180405110935236.jpg
我们在方法调用前打印参数信息,方法调用结束之后打印结束信息,然后运行模块看日志信息:
20180404181901507.jpg
我们挂了Fiddler代理之后看到打印的参数值信息了,不过可惜的是结束日志没有打印出来,那么问题就出在这个方法了,但是上面有好几处代码怎么定位到是哪个代码等待呢?这个我们可以选择对每个方法调用进行hook然后查看日志,但是这样太费劲了,这时候就需要一个技巧就是如果想定位到哪个方法等待,那么可以进行插入日志代码,看打印信息,但是我们加入很多日志,所以要区分是哪行日志打印了,所以这里有一个技巧就是通过获取当前方法被调用的行数来作为打印的信息,代码大致如下:
20180404184852292.jpg
这样我们只需要调用log这个方法即可,因为这个方法是无参数的,在插入smali代码的时候有一条基本原则就是尽量把功能模块弄到一个static无参函数中,这样对于插入非常高效便捷也不会出错。有了这个java代码,然后用我之前写的java2smali工具直接运行即可,不了解这个工具的同学可以查看这里:Android中一键转化java2smali工具原理解析;然后就得到了对应的smali代码,然后我们对于这个app呢?不要去直接apktools反编译,因为反编译失败的,我们可以直接操作dex转化成smali即可,这个要用到baksmali和smali这两个工具,我们直接解压apk获取到classes.dex文件,然后执行命令:java -jar baksmali.jar -o classes classes.dex 其中classes是反编译dex之后的smali文件夹目录。然后把我们上面的MyLog.smali放到指定目录下,记住一定要有全路径,比如这里是cn.wjdiankong.log.MyLog,那么就要放到cn/wjdiankong/log/MyLog.smali,没有目录就自己手动新建即可。然后在上面想要插入代码的地方直接插入代码:invoke-static {}, Lcn/wjdiankong/log/MyLog;->log()V 这里我放了好几个:
20180404185628363.jpg
记住放的地方别瞎乱放入,一般都在move-result…语句之后也就是方法调用结束之后插入,不然回编译dex会报错的。插入之后就在用smali进行回编译:java -jar smali.jar classes -o classes.dex 其中classes是反编译的smali目录,classes.dex是回编译之后的dex。成功回编译得到新的dex之后,为了验证插入成功,可以用Jadx打开进行查看:
20180404185939991.jpg
我们看到好几处都插入日志了,并且日志是携带行号的,这样就能区分是哪里的日志打印结果了,好了把这个插入代码的dex在塞到原来的apk中,然后二次签名即可,因为这个应用没有签名校验,所以直接看打印日志:
20180404190322588.jpg
然后在回过头看看代码:
20180404190427836.jpg
但是这个createSocket方法是系统的了?得去看源码查看为何等待那么久?对到这里我没去深入看了,因为不是本文研究的重点了,不过可以告诉大家如果感兴趣可以自己写个demo然后用okhttp访问一下接口也是这个createSocket方法等待很久。
二、解决方案
我们问题找到了,解决办法就简单了,为了能够正常抓包,直接把这个okhttp的证书hook修改成系统默认的即可:
20180404192623700.jpg
然后拦截之前分析的那个设置okhttp的证书代码方法:
20180404192654327.jpg
然后运行模块之后就可以愉快的抓包啦:
20180404193513236.jpg
看到了这里就可以愉快的抓到数据包了,其实这里虽然我们知道了解决办法,但是其实内部的原因还是没弄清楚,就是知道原因是okhttp内部的问题,其实这个搜索可以发现内部实现机制的原因,不过这个对于我们不是重点,那么通过这个样本我们又可以总结一个遇到抓不到包的问题解决方案了,那就是全局搜SSLSocketFactory,然后看看应用是否设置了证书信息。如果有就拦截替换成系统默认的证书即可。

三、抓包失败解决方案
通过以往知道的知识以及本文的案例,那么我们就可以总结一下app中抓包失败的解决方案大致如下:

第一、确认Fiddler证书安装正确
第二、是否安装Xposed模块JustTrustMe信任所有证书
第三、查看应用中使用的okhttp中是否设置了SSLSocketFactory
其实除了这三个还有其他方式,还有其他方式,不过不在本文介绍了,后面会详细介绍如何防止自己的应用被被人恶意抓包,因为如果应用被人抓包其实很多破解操作就变的很简单了。把这个入口做的安全会对后面的防护有一定效果。

一般安卓加载代码都是通过classloader来装取本地代码到内存中去的。

classloader有两种加载方式:

1.通过路径寻找本地代码,然后载入内存:

基本的源码顺序是 BaseDexClassLoader->DexPathList->makeDexElements->loadDexFile->Dexfile.loadDex

最后DexFile中有一个native方法 OpenDexFileNative 对应了 DexFile_openDexFileNative 方法。其中的OpenDexFileFormOat便是最主要的方法了。

当然这里我们就跳过,需要了解classloader的机制可以自行去看xref,因为这里是路径加载的方式,一般加固不会选择这里的方法去加载的。

2.直接映射到内存中去:

在4.x版本的DexFile.java有

    
native private static int openDexFile(byte[] fileContents);

这个函数在5.x中的java方法已经摒弃,在native层中依旧存在于/art/runtime/dex_file.cc中。

其方法为OpenMemory函数。我们只需要在此从内存中扣出文件就好了。
const DexFile DexFile::OpenMemory(const byte base,

                               size_t size,
                               const std::string& location,
                               uint32_t location_checksum,
                               MemMap* mem_map,
                               const OatFile* oat_file,
                               std::string* error_msg) {

CHECK_ALIGNED(base, 4); // various dex file structures must be word aligned
std::unique_ptr<DexFile> dex_file(

  new DexFile(base, size, location, location_checksum, mem_map, oat_file));

if (!dex_file->Init(error_msg)) {

return nullptr;

} else {

__android_log_print(ANDROID_LOG_DEBUG,"chason 's DexFile::OpenMemory",
                            "size is:%zu,location is:%s", size, location.c_str());
if (!strcmp(location.c_str(),"/data/data/com.autohome.mycar/.jiagu/classes.dex"))
{
     
    int fd = open("/data/data/com.autohome.mycar/classes.dex",O_CREAT|O_EXCL|O_WRONLY,S_IRWXU);
     __android_log_print(ANDROID_LOG_DEBUG,"chason's copy is starting!","hello");
    if (fd>0)
      write(fd,base,size);
    else
      __android_log_print(ANDROID_LOG_DEBUG,"chason's copy is failed!","codeis:%d",fd);
    close(fd);
}
return dex_file.release();

}
}

最后在路径里找到dex就ok了。。。

一、 概述
相信各位读者对so分析都采用静态和动态相结合的方式,静态分析常用readelf、objdump、ida等工具,这些工具对so文件的分析都会使用到Section信息。从这篇帖子中http://bbs.pediy.com/showthread.php?t=191649 知道,程序并不需要section信息。现很多so文件对section信息都进行了处理,导致常用分析工具无法使用。以下讨论前段时间对section修复的一些思考,若有不足或错误之处,请各位大大指正,小弟感激不尽!
二、 仅处理so文件头
在上文提到的帖子中,给出了一种section处理的一种简单方式。这里在罗嗦下,即将Elf32_Ehdr中的e_shoff, e_shnum, e_shstrndx, e_shentsize字段处理。修复公式:e_shstrndx = e_shnum -1; e_shnum = (file_size – e_shoff) / sizeof(Elf32_Shdr)。在那篇帖子中作为修复的数字so文件,并未处理e_shoff字段,故用上式修复可行。那如果都处理掉,则上式中存在两个未知数,无法利用。
一种简单的思路是,手动查找so文件中一些稳定且标志性的数据作为参考来修复。这里,我选择shstrtab表,这样比较简单。因为shshtrtab后面就是section头信息,这样就间接找到e_shoff位置,即可利用上式修复。
手动查找当然可行,毕竟麻烦。作为程序猿,应该通过程序来解决问题。借鉴手动修复的思路,只要程序能找到shstrtab即可实现修复。从观察或e_shstrndx知道,shstrtab section为最后一个section,即处于文件末尾。那直接移动到末尾读取到shstrtab section,则e_shoff = sh_offset + sh_zize(这里还需对e_shoff 4字节对齐处理)。
三、 无section信息
现阶段,我遇到的很多so文件的section修复都采用上述方法,还未遇到无section信息的so,即直接将so文件中的section直接删除,或者替换section内容(比如填充隐藏代码或者垃圾数据之类的)。直接删除section信息,即可节省空间,又可让静态工具“蛋疼”(有点好奇为什么不对so作如此处理)。另外,直接从内存中dump出来的so文件,也是没有section信息的(因为section没有被加载到内存中)。当然,从内存中dump的so文件,由于内存对齐的原因,需要作下简单处理,这里就不赘述。
从内存dump出的so文件已经经过解密,直接拿来分析,能获得事半功倍的效果。但没有section信息,静态分析很是不爽。如果原so文件有section信息,则只需要把原so文件中的section信息复制过来并修复Elf32_Ehdr即可。那如果原so无section信息,我的理解是需要对section信息进行重建(虽然现阶段还没用到,处于问题思考的完整性,讨论这种情况)。
使用readelf –S 查看一个完整的so文件section如下图所示:
627520_tlo8o0755bhtj31.png
使用readelf –l 如图所示:
627520_psx2z2kev1eln5s.png
从segment信息可以看出, 对.dynamic和.arm_exidx的section重建很简单,即读取即可。
通过.dynamic,可以对大部分section进行重建,具体如下:
1. 通过DT_SYMTAB,DT_STRTAB,DT_STRSZ,DT_REL,DT_RELSZ,DT_JMPREL, DT_PLTRELSZ,DT_INIT_ARRAY,DT_INIT_ARRAYSZ,DT_FINI_ARRAY,DT_FINI_ARRAYSZ 得到.dynsym,.dynstr, rel.dyn, rel.plt, init_array, fini_array 相应的section vaddr 和 size信息,完成对上述section的重建。这里需要注意,处于load2中的section,offset = vaddr – 0x1000
2. 通过DT_HASH得到hash section的vaddr,然后读入前两项得到nbucket和nchain的值,得到hashsz = (nbucket + nchain + 2) * sizeof(int), 完成对hash表重建
3. Plt的起始位置即为rel.plt的末尾,通过1中的对rel.plt的处理,即可得到plt的offset和vaddr信息。通过plt的结构知道,plt由固定16字节 + 4字节的__global__offset_table变量和n个需要重定位的函数地址构成,函数地址又与rel.plt中的结构一一对应。故size = (20 + 12 * (rel.plt.size) / sizeof(Elf32_Rel)。
4. 从DT_PLTGOT可以得到__global_offset_table的偏移位置。由got表的结构知道,__global_offset_table前是rel.dyn重定位结构,之后为rel.plt重定位结构,都与rel一一对应。则got表的重建具体为:通过已重建的.dynamic得到got起始位置,通过__global_offset_table 偏移 + 4 * (rel.plt.size) / sizeof(Elf32_Rel)(这里还需要添加2个int的填充位置)得到got的末尾,通过首尾位置得到got的size,完成重建
5. 通过got的末尾,得到data的起始位置,再通过load2_vaddr + load2_filesz得到load2的末尾(load2即第二个LOAD),即data的末尾位置,计算长度,完成修正。可能读者会问,bss才是load2的最后一个section。的确,但bss为NOBITS,即可把data看作load2最后一个section。
6. 对bss的修正就很简单,offset和vaddr即为load2末尾。由于未NOBITS类型,长度信息无关紧要。
7. 到这里,读者可能已经发现,还没对text和ARM.extab修正。限于本人水平,还没能找到方法区分开这两个section。现处理是将之合并,作为text & ARM.extab节。具体修正:offset和vaddr通过plt末尾得到,长度通过ARM.exidx的起始位置和plt末尾位置计算得到。
至此,绝大部分section信息已经重建完成。最后,在将shstrtab添加,并修正Elf32_Ehdr,完成section重建。虽然未100%重建,但已经能够帮助分析了。重建后的如图所示,图中红色部分即是未分离的test & ARM.extab section。
627520_w8iyncqz9yap2ku.png
使用ida也能正常打开,只是会将ARM.extab的数据转换成错误代码,其他均正常。
627520_ixtllg9gc9b7aw4.png

https://cat-in-136.github.io/2010/12/aadecode-decode-encoded-as-aaencode.html

var AADecode = {

decode: function(text) {
    var evalPreamble = "(\uFF9F\u0414\uFF9F) ['_'] ( (\uFF9F\u0414\uFF9F) ['_'] (";
    var decodePreamble = "( (\uFF9F\u0414\uFF9F) ['_'] (";
    var evalPostamble = ") (\uFF9F\u0398\uFF9F)) ('_');";
    var decodePostamble = ") ());";

    // strip beginning/ending space.
    text = text.replace(/^\s*/, "").replace(/\s*$/, "");

    // returns empty text for empty input.
    if (/^\s*$/.test(text)) {
        return "";
    }
    // check if it is encoded.
    if (text.lastIndexOf(evalPreamble) < 0) {
        throw new Error("Given code is not encoded as aaencode.");
    }
    if (text.lastIndexOf(evalPostamble) != text.length - evalPostamble.length) {
        throw new Error("Given code is not encoded as aaencode.");
    }

    var decodingScript = text.replace(evalPreamble, decodePreamble)
                             .replace(evalPostamble, decodePostamble);
    return eval(decodingScript);
},
doDecode: function() {
    var oEncoded = document.getElementById("aadecode_encoded");
    var oDecoded = document.getElementById("aadecode_decoded");

    try {
        oDecoded.value = AADecode.decode(oEncoded.value);
    } catch (ex) {
        oDecoded.value = "****Error:\n" + ex.toString();
    }
},
dummy: null

};

openmemory 的地方,腾讯的是打开对应app的oat/arm/xxx.odex dump出来的
这篇文章是记录本人在学习Legu脱壳的心得,分析的样本是Legu libshella-2.10.so的版本。
本文分为几个部分:
修复So文件
第一次解密
第二次解密
解密Dex
Dalvik下加载Dex原理分析

Art下加载Dex原理分析

Dalvik下脱壳机编写
Art下脱壳机编写

修复So文件

Legu的核心加固代码都是在libshella-x.so里面,
我们用IDA打开libshella-2.10.1.so,意料之中的是,IDA打开什么也看不到。
629060_5z9v5clkg3uvp36.png
在这里我用ThomasKing的ELF修复工具试着修复一下,修复之后已经可以看到很多函数了
但是修复后的So文件在IDA还是看不到init_array段,JNI_OnLoad函数也是加密状态。
629060_f6g0tv4mteys0jo.png
由于So加载完之后会调用init_array函数,我们从Android源码入手来获取init_array的地址,在Android源码 linker.cpp的代码中有这么一段代码就是来调用init和init_aray函数的
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
将手机中的linker拖入ida,找到DT_INIT_ARRAY字符串,可以获取调用CallArray的地址,在我的linker中,0x295E处就是调用init段的地址
629060_aolb74w2tk0aezm.png
我用修复后的So文件替换掉原始的Libshela-2.10.1.so文件后,App还可以成功运行,在这里就直接用修复后的So来动态调试

第一次解密

在init_array的函数中,首先会解密出JNI_OnLoad和反调试代码等等
解密是一个while循环,会从libshella.so 0x2000处开始解密,到0x3954处结束解密,解密完成之后,就只是创建了一个反调试线程,检测到反调试就执行raise(9),最简单的方法可以将call raise函数的代码nop掉
629060_wxv9u9uqrim6211.png
我没有关注init_array的解密算法是什么样的,当它解密完成之后,我将0x2000-0x3954偏移处的代码dump出来了,然后替换掉上面修复后的libshella_fix.so对应的字节,这样生成的So就包含解密后的Jni_OnLoad函数了

第二次解密
629060_ixfia7e5wu0f5yp.png
咋一看,很奇怪的是在JNI_OnLoad函数中,还会自身再调用JNI_OnLoad函数,关键地方在于sub_1968()函数,
这个函数比较庞大,我分析了半天,没分析处到底是怎么解密的,但是这里并不影响后面的分析,
当这个函数执行完之后,通过dlsym 获取JNI_OnLoad函数的地址已经不是开始的JNI_OnLoad函数地址,我们这里成为new_JNI_Onload,而是处于动态分配的debug内存区域,其实这些debug内存区域的代码就是前面sub_1968()函数解密出来的
629060_qzox1plty1n264g.png
为了方便分析,我将libshella.so debug区域以及libc.so libdvm.so进行了内存快照拍摄
629060_zgpmtz5pkzkfftj.png
我们用上面内存快照拍摄的生成的idb来分析new_JNI_OnLoad函数,在new_OnLoad函数中,看到了久违的registerNative注册操作,注册了Java层的load,runCreate等native函数
629060_xq9b09t83sr2n0m.png
解密Dex

在Java层的attachBaseContext函数中,首先执行的是native层的load函数,
在load函数中,首先会判断当前是dalvik还是art虚拟机,然后执行对应的加载Dex方法
629060_l7sxucdskl5u27z.png
在解密真正的dex之前,首先是获取解密前dex的存储位置,很简单的是,解密前的Dex存储地址=内存odex地址+DexHeade->dataOff+DexHeade->dataSize
629060_hsns3fqiz919tu4.png
获取到解密前dex的内存地址,就会执行decrypt函数对dex进行解密,手动脱壳的话,在这里就可以dump处真正的dex了
629060_oeo40tifcfw4cet.png

Dalvik下加载Dex

在我的那篇阿里早期加固代码还原的帖子中,在Dalvik下的Dex加载方式是通过Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个方式进行内存加载的,但是Legu在这里使用了一种更加高级的方法
首先Legu会通过loadDex函数加载mix.dex,从而得到一个表示mix.dex的mCookie对象
629060_oqhuby4i1uarua0.png
然后Legu会构造一个0x34字节的结构体DexHeaderBak,用来保存真正Dex的DexHeader信息
629060_aej1yjgzk52u9kz.png
然后用mmap分配一段内存mmap_buffer,在其中填充一些真正Dex的信息,我画了一张图描述mmap_bufffer的内存结构
629060_xaa956o5k1a8lcg.png
在Android4.4中,mCookie对象是指向DexOrJar结构的指针,
struct DexOrJar {

char*       fileName;
bool        isDex;
bool        okayToFree;
RawDexFile* pRawDexFile;
JarFile*    pJarFile;
u1*         pDexMemory; // malloc()ed memory, if any

};
struct RawDexFile {

char*       cacheFileName;
DvmDex*     pDvmDex;

};
Legu会通过mixdex_cookie获取pRawDexFile指针,再来获取pDvmDex指针,
629060_fhbjh44jobe9o8m.png
最后将pDvmDex指向的内容全部替换为mmap_buffer结构体中的内容,这样mix.dex的mCookie对象已经表示为真正的Dex,而不是原本的mix.dex了,关于Legu为什么知道这么做,可能要分析Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数的原理了。
629060_77yd9svh7vmsydt.png
Dalvik下Dex的加载大部分都完成了,后面Legu加载的方法其实也就是MultiDex的多Dex加载方法,这里不做分析。

Art(Android 6.0)下加载Dex分析

Android4.4下有Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数加载Dex,但是Android N以上就没有了这个函数,
Legu在Art下会hook几个系统函数,并且获取libart.so中的art::dexFile::OpenFile函数的地址
629060_kuq2y6j8ekc4giz.png
让我开始很疑惑的是,Legu用OpenFile函数打开内存中的oat文件,并没有任何加载dex的操作
我分析了下Android 6.0下的OpenFile函数
629060_yfirol7gjll6duv.png
OpenFile首先会call fstat函数获取location的大小,但是此时执行的却是hook后的fstat,Legu会替换真正的location的大小,而是返回真正的Dex大小,
然后根据前面的大小调用MapFile函数,MapFile最终调用了mmap函数,此时执行的还是hook后的mmap,
fake_mmap代码如下,首先会解密处真正的dex内容,然后用真正的Dex地址替换mmap返回值
629060_i674u8y7o2ejb5m.png

OpenFile进行了mmap操作后,会进行OpenMemory加载Dex,经过上面的hook,表面上是打开base.odex oat文件,实际上OpenMemory真正的Dex文件,因此我们可以Hook OpenMemory达到dump dex的目的,所以可以看出Android Art下使用OpenMemory函数来加载dex文件。

Dalvik下脱壳机编写
根据上面的分析,在Dalivk下可以很容易地获取到真正Dex的内存位置:真正Dex存储地址=内存odex地址+DexHeade->dataOff+DexHeade->dataSize
App通过运行之后,注入后虽然可以Dump Dex,但是dump 的Dex跟原始的字节有几个字节不同,导致重打包运行出错,Legu在加载的时候是真正的Dex文件,但是脱离Legu代码运行起来后发现有几个字节变了,刚开始老以为是Legu对方法字节码做了处理,后来调试发现是Dalik自己改变的,字节码的变化不知道是不是Dalvik对字节码进行优化导致的。
Art下Dump 得到的Dex是完成正确的,推荐大家在Art下进行Dump Dex
void DumpDex_kitkat(char* pkgName)
{

int pid=getpid();
printf("pid:%d\n",pid);
char filename[100]={0};
char dumpfilepath[256]={0};
char* s;
unsigned int startAddr=NULL;
unsigned int endAddr=NULL;
unsigned int mainDexAddr;
char* oatPath[256]={0};
errno=0;
sprintf(dumpfilepath,"/data/data/%s/dump.dex",pkgName);
sprintf(filename,"/proc/%d/maps",pid);
FILE *fp;
fp = fopen(filename, "r");
if(fp!=NULL)
{
    char line [2048];
    while (fgets(line, sizeof(line), fp ) != NULL ) /* read a line */
    {
        if (strstr(line, pkgName) != NULL)
        {
            if (strstr(line, "classes.dex") != NULL)
            {
                LOGI("dvm-found odex address");
                s = strchr(line, '-');
                if (s == NULL)
                    LOGI(" Error: string NULL");
                *s++ = '\0';
                //strtoul:将字符串转化成无符号整型
                startAddr = (void *)strtoul(line, NULL, 16);
                endAddr = (void *)strtoul(s, NULL, 16);
                LOGI(" dvm classes.odex addr %x-%x", startAddr,endAddr);
                break;
            }
        }
    }
    fclose ( fp);
}
else
{
    LOGI("fopen maps failed");
    return;
}
if(startAddr==NULL || endAddr==NULL)
{
    LOGI("found odex or oat file failed");
    return;
}
mainDexAddr=startAddr+0x28;
LOGI("dexAddr:%s",(unsigned char*)mainDexAddr);
int magic=*(unsigned int*)mainDexAddr;
if(magic!=0x0A786564)
{
    LOGI("not find main Dex");
    return ;
}
unsigned int OrgDexOffset=getOrgDexOffset(mainDexAddr);
LOGI("OrgDexOffset:%d",OrgDexOffset);
unsigned int realDexAddr=mainDexAddr+OrgDexOffset;
magic=*(unsigned int*)realDexAddr;
if(magic!=0x0A786564)
{
    LOGI("not find real Dex");
    return ;
}
unsigned int dexSize=*(unsigned int*)(realDexAddr+0x20);
LOGI("dexSize:%d",dexSize);
void* buffer=malloc(dexSize);
if(buffer==0)
{
    LOGI("malloc dexsize buffer failed");
    return;
}
memcpy(buffer,(void*)realDexAddr,dexSize);
FILE* fd_dump=fopen(dumpfilepath,"wb+");
if(fd_dump==NULL)
{
    LOGI("fopen dumpfile error:%s",strerror(errno));
    return;
}
fwrite(buffer,dexSize,1,fd_dump);
free(buffer);
fflush(fd_dump);
fclsoe(fd_dump);

}
Art下脱壳机编写

我这里是针对Android 6.0下hook OpenMemory函数,在5.1下OpenMemory函数可能参数有所改变要另外做处理
注入zygote Hook OpenMemory函数dump dex
uint32_t new_art_dexFile_openMemory(void DexFile_thiz,char base,int size,void* location,

                                void* location_checksum,void* mem_map,void* oat_dex_file,void* error_meessage )

{

if(*((uint32_t*)base)==0x0A786564)
{
    int pid=getpid();
    const char* proc_name=get_process_name(pid);
    if(strstr(proc_name,g_szTargetName))
    {
        LOGI("openMemory found target dex");
        char szDumpPath[256]={0};
        sprintf(szDumpPath,"/data/data/%s/dump_marsha_%x.dex",g_szTargetName,size);
        FILE* fd_dump=fopen(szDumpPath,"wb+");
        if(fd_dump==NULL)
        {
            LOGI("fopen dumpfile error:%s",strerror(errno));
            return;
        }
        fwrite(base,size,1,fd_dump);
        fflush(fd_dump);
        fclose(fd_dump);
        LOGI("dex file save at %s size:%x",szDumpPath,size);
    }
    return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);
}
else
{
    return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);
}

}
最后测试发现libshella 2.7-2.10版本的Dex都可以成功Dump出来