菜鸟刚刚学完了 dex_file.cc这个源码,大致搞明白了大佬们hook脱整体加固的原理了,原理在帖子最后
学习了大佬 angelToms 的帖子https://bbs.pediy.com/thread-252828.htm与https://bbs.pediy.com/thread-252284.htm,总结的很清晰,dex 加载到内存之后,只要想方设法找到 DexFile的实例,就可以通过它的数据结构搞出整体加固的dex了,下面我们看下 dex_file.cc 的源码,顺便理解下初步脱壳的基本原理。还是先贴一下流程。
DexFile::Open(OpenDexFilesFromOat里,通过oatfile获得dexfile走这个) { OpenCommon } DexFile::Open (OpenDexFilesFromOat里,通过oatfile获得dexfile失败,直接打开源dex获得dexfile走这个 ) { OpenAndReadMagic if zip OpenZip { OpenAllDexFilesFromZip { OpenOneDexFileFromZip { OpenCommon } } } else dex OpenFile { OpenCommon } }
1.先看DexFile::Open,看一下头文件,它在源码中有好几个重载函数 ,
第一个是OpenDexFilesFromOat里,通过oatfile获得dexfile成功的调用,看OpenDexFile函数中DexFile::Open的参数可以判断;
第三个是OpenDexFilesFromOat里,通过oatfile获得dexfile失败后,直接打开源dex的调用,看 OpenDexFilesFromOat 函数最后的DexFile::Open的参数可以判断
static std::unique_ptr<const DexFile> Open(const uint8_t* base, size_t size, const std::string& location, uint32_t location_checksum, const OatDexFile* oat_dex_file, bool verify, bool verify_checksum, std::string* error_msg);//这是OpenDexFilesFromOat里,通过oatfile获得dexfile走这个,看OpenDexFile函数中DexFile::Open的参数可以判断,不清楚的去看下上一篇 // Opens .dex file that has been memory-mapped by the caller. static std::unique_ptr<const DexFile> Open(const std::string& location, uint32_t location_checkum, std::unique_ptr<MemMap> mem_map, bool verify, bool verify_checksum, std::string* error_msg);//这是打开已经被调用者memory-mapped过的 // Opens all .dex files found in the file, guessing the container format based on file extension. static bool Open(const char* filename, const std::string& location, bool verify_checksum, std::string* error_msg, std::vector<std::unique_ptr<const DexFile>>* dex_files);//这是OpenDexFilesFromOat里,通过oatfile获得dexfile失败,直接打开源dex走这个,看OpenDexFilesFromOat函数中DexFile::Open的参数可以判断
1.1这里还是把 OpenDexFilesFromOat 这个函数贴一下,具体代码看前一篇
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat( const char* dex_location, jobject class_loader, jobjectArray dex_elements, const OatFile** out_oat_file, std::vector<std::string>* error_msgs) { 。。。 // Get the oat file on disk. std::unique_ptr<const OatFile> oat_file(oat_file_assistant.GetBestOatFile().release());//这句获得了oat_file,下面LoadDexFiles使用这个oat_file获得dex_files 。。。 if (accept_oat_file) { VLOG(class_linker) << "Registering " << oat_file->GetLocation(); source_oat_file = RegisterOatFile(std::move(oat_file));//这里把oat_file注册给source_oat_file *out_oat_file = source_oat_file; } } std::vector<std::unique_ptr<const DexFile>> dex_files; // Load the dex files from the oat file. 。。。 dex_files = oat_file_assistant.LoadDexFiles(*source_oat_file, dex_location);//这里通过加载source_oat_file获得dex_files,最终调用了DexFile::Open,这里的DexFile::Open是一个重载 。。。 // Fall back to running out of the original dex file if we couldn't load any // dex_files from the oat file. if (dex_files.empty()) { if (oat_file_assistant.HasOriginalDexFiles()) { if (Runtime::Current()->IsDexFileFallbackEnabled()) { static constexpr bool kVerifyChecksum = true; if (!DexFile::Open( dex_location, dex_location, kVerifyChecksum, /*out*/ &error_msg, &dex_files)) {//如果LoadDexFiles上面没有获得dex_files,直接DexFile::Open打开加载原始的dexfile,这里的DexFile::Open是另一个重载 LOG(WARNING) << error_msg; error_msgs->push_back("Failed to open dex files from " + std::string(dex_location) + " because: " + error_msg); } 。。。 return dex_files; }
1.2这个就是走oatfile获得dexfile路径的DexFile::Open,没啥花样,直接调用OpenCommon
std::unique_ptr<const DexFile> DexFile::Open(const uint8_t* base, size_t size, const std::string& location, uint32_t location_checksum, const OatDexFile* oat_dex_file,//这个参数就是上一篇的this,就是oat_dex_file,是从这里面找出dex_file哦,其他2个重载函数都不是从oat_dex_file里找到dex_file,所以肯定没有调用他们 bool verify, bool verify_checksum, std::string* error_msg) { ScopedTrace trace(std::string("Open dex file from RAM ") + location); return OpenCommon(base, size, location, location_checksum, oat_dex_file, verify, verify_checksum, error_msg); }
2.下面这个就是不通过oat_file直接打开dex文件的DexFile::Open,稍微复杂一点,先判断打开的是zip压缩包还是dex,最终其实也是调用了 OpenCommon
bool DexFile::Open(const char* filename, const std::string& location, bool verify_checksum, std::string* error_msg, std::vector<std::unique_ptr<const DexFile>>* dex_files) { ScopedTrace trace(std::string("Open dex file ") + std::string(location)); DCHECK(dex_files != nullptr) << "DexFile::Open: out-param is nullptr"; uint32_t magic; File fd = OpenAndReadMagic(filename, &magic, error_msg);//OpenAndReadMagic也是一个常用脱壳点,如果不是直接打开dex走这个函数,不会在这里被调用 if (fd.Fd() == -1) { DCHECK(!error_msg->empty()); return false; } if (IsZipMagic(magic)) { return DexFile::OpenZip(fd.Release(), location, verify_checksum, error_msg, dex_files);//如果是Zip,调用DexFile::OpenZip } if (IsDexMagic(magic)) { std::unique_ptr<const DexFile> dex_file(DexFile::OpenFile(fd.Release(), location, /* verify */ true, verify_checksum, error_msg));//如果是Dex,调用DexFile::OpenFile if (dex_file.get() != nullptr) { dex_files->push_back(std::move(dex_file)); return true; } else { return false; } } *error_msg = StringPrintf("Expected valid zip or dex file: '%s'", filename); return false; }
2.1先看OpenZip的逻辑,先通过fd文件描述符获得ZipArchive指针,在使用这个指针调用了OpenAllDexFilesFromZip处理 ZipArchive
bool DexFile::OpenZip(int fd, const std::string& location, bool verify_checksum, std::string* error_msg, std::vector<std::unique_ptr<const DexFile>>* dex_files) { ScopedTrace trace("Dex file open Zip " + std::string(location)); DCHECK(dex_files != nullptr) << "DexFile::OpenZip: out-param is nullptr"; std::unique_ptr<ZipArchive> zip_archive(ZipArchive::OpenFromFd(fd, location.c_str(), error_msg)); if (zip_archive.get() == nullptr) { DCHECK(!error_msg->empty()); return false; } return DexFile::OpenAllDexFilesFromZip(*zip_archive, location, verify_checksum, error_msg, dex_files); }
2.2再看 OpenAllDexFilesFromZip ,调用了OpenOneDexFileFromZip,因为可能有多个dex,依次打开
bool DexFile::OpenAllDexFilesFromZip(const ZipArchive& zip_archive, const std::string& location, bool verify_checksum, std::string* error_msg, std::vector<std::unique_ptr<const DexFile>>* dex_files) { ScopedTrace trace("Dex file open from Zip " + std::string(location)); DCHECK(dex_files != nullptr) << "DexFile::OpenFromZip: out-param is nullptr"; ZipOpenErrorCode error_code; std::unique_ptr<const DexFile> dex_file(OpenOneDexFileFromZip(zip_archive, kClassesDex, location, verify_checksum, error_msg, &error_code)); if (dex_file.get() == nullptr) { return false; } else { // Had at least classes.dex. dex_files->push_back(std::move(dex_file)); // Now try some more. // We could try to avoid std::string allocations by working on a char array directly. As we // do not expect a lot of iterations, this seems too involved and brittle. for (size_t i = 1; ; ++i) { std::string name = GetMultiDexClassesDexName(i); std::string fake_location = GetMultiDexLocation(i, location.c_str()); std::unique_ptr<const DexFile> next_dex_file(OpenOneDexFileFromZip(zip_archive, name.c_str(), fake_location, verify_checksum, error_msg, &error_code)); if (next_dex_file.get() == nullptr) { if (error_code != ZipOpenErrorCode::kEntryNotFound) { LOG(WARNING) << "Zip open failed: " << *error_msg; } break; } else { dex_files->push_back(std::move(next_dex_file)); } if (i == kWarnOnManyDexFilesThreshold) { LOG(WARNING) << location << " has in excess of " << kWarnOnManyDexFilesThreshold << " dex files. Please consider coalescing and shrinking the number to " " avoid runtime overhead."; } if (i == std::numeric_limits<size_t>::max()) { LOG(ERROR) << "Overflow in number of dex files!"; break; } } return true; } }
2.3最后在OpenOneDexFileFromZip里最终也是调用了OpenCommon,殊途同归,最后都来到这个关键函数
std::unique_ptr<const DexFile> DexFile::OpenOneDexFileFromZip(const ZipArchive& zip_archive, const char* entry_name, const std::string& location, bool verify_checksum, std::string* error_msg, ZipOpenErrorCode* error_code) { ScopedTrace trace("Dex file open from Zip Archive " + std::string(location)); CHECK(!location.empty()); std::unique_ptr<ZipEntry> zip_entry(zip_archive.Find(entry_name, error_msg)); if (zip_entry == nullptr) { *error_code = ZipOpenErrorCode::kEntryNotFound; return nullptr; } if (zip_entry->GetUncompressedLength() == 0) { *error_msg = StringPrintf("Dex file '%s' has zero length", location.c_str()); *error_code = ZipOpenErrorCode::kDexFileError; return nullptr; } std::unique_ptr<MemMap> map; if (zip_entry->IsUncompressed()) { if (!zip_entry->IsAlignedTo(alignof(Header))) { // Do not mmap unaligned ZIP entries because // doing so would fail dex verification which requires 4 byte alignment. LOG(WARNING) << "Can't mmap dex file " << location << "!" << entry_name << " directly; " << "please zipalign to " << alignof(Header) << " bytes. " << "Falling back to extracting file."; } else { // Map uncompressed files within zip as file-backed to avoid a dirty copy. map.reset(zip_entry->MapDirectlyFromFile(location.c_str(), /*out*/error_msg)); if (map == nullptr) { LOG(WARNING) << "Can't mmap dex file " << location << "!" << entry_name << " directly; " << "is your ZIP file corrupted? Falling back to extraction."; // Try again with Extraction which still has a chance of recovery. } } } if (map == nullptr) { // Default path for compressed ZIP entries, // and fallback for stored ZIP entries. map.reset(zip_entry->ExtractToMemMap(location.c_str(), entry_name, error_msg)); } if (map == nullptr) { *error_msg = StringPrintf("Failed to extract '%s' from '%s': %s", entry_name, location.c_str(), error_msg->c_str()); *error_code = ZipOpenErrorCode::kExtractToMemoryError; return nullptr; } VerifyResult verify_result; std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(), map->Size(), location, zip_entry->GetCrc32(), kNoOatDexFile, /* verify */ true, verify_checksum, error_msg, &verify_result); if (dex_file == nullptr) { if (verify_result == VerifyResult::kVerifyNotAttempted) { *error_code = ZipOpenErrorCode::kDexFileError; } else { *error_code = ZipOpenErrorCode::kVerifyError; } return nullptr; } dex_file->mem_map_ = std::move(map); if (!dex_file->DisableWrite()) { *error_msg = StringPrintf("Failed to make dex file '%s' read only", location.c_str()); *error_code = ZipOpenErrorCode::kMakeReadOnlyError; return nullptr; } CHECK(dex_file->IsReadOnly()) << location; if (verify_result != VerifyResult::kVerifySucceeded) { *error_code = ZipOpenErrorCode::kVerifyError; return nullptr; } *error_code = ZipOpenErrorCode::kNoError; return dex_file; }
3.最后我们认真分析OpenCommon这个函数,无论是通过oat_file获得的 oat_dex_file 获得 dex_file 也好,是直接打开zip或者dex文件获得 dex_file 也好,最终都得用到这个函数,所以它作为常用脱壳点的意义就很清楚了,它的前2个参数分别是dex的起始地址和大小,直接hook就可以dump出dex了。
其实只要有base地址,通过dex数据结构就可以定位size,加个偏移就行parseInt(base,16) + 0x20
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base,//这里是dex的开始 size_t size,//这里是dex的大小 const std::string& location,//这里是地址 uint32_t location_checksum, const OatDexFile* oat_dex_file,//如果直接打开文件而不是通过oat文件获得dex,这个参数是kNoOatDexFile,hook打印这个参数就可以判断一些壳是否放弃了oat文件强制以dex解释运行 bool verify, bool verify_checksum, std::string* error_msg, VerifyResult* verify_result) { if (verify_result != nullptr) { *verify_result = VerifyResult::kVerifyNotAttempted; } std::unique_ptr<DexFile> dex_file(new DexFile(base, size, location, location_checksum, oat_dex_file));//这里new了一个dex_file实例,至此dex_file加载结束 if (dex_file == nullptr) { *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(), error_msg->c_str()); return nullptr; } if (!dex_file->Init(error_msg)) {//init初始化 dex_file.reset(); return nullptr; } if (verify && !DexFileVerifier::Verify(dex_file.get(), dex_file->Begin(), dex_file->Size(), location.c_str(), verify_checksum, error_msg)) {//Verify验证 if (verify_result != nullptr) { *verify_result = VerifyResult::kVerifyFailed; } return nullptr; } if (verify_result != nullptr) { *verify_result = VerifyResult::kVerifySucceeded; } return dex_file; }
最后,仍然是画个及其丑陋的图,辅助自己理解。
4.下面贴一下frida hook脱壳脚本,很简单,每一步我都备注了,上次问我的同学仔细看一下,看看还有什么问题
/* static std::unique_ptr<DexFile> OpenCommon(const uint8_t* base, size_t size, const std::string& location, uint32_t location_checksum, const OatDexFile* oat_dex_file, bool verify, bool verify_checksum, std::string* error_msg, VerifyResult* verify_result = nullptr); */ //这里我是安卓8.1的,不同版本不一样,自己pull出libart.so,打开ida查 var OpenCommon = Module.findExportByName("libart.so", "_ZN3art7DexFile10OpenCommonEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_PNS0_12VerifyResultE"); console.log("[*] Opencommon method addr: " + OpenCommon); Interceptor.attach(OpenCommon, { onEnter: function (args) { console.log("[*] begin = " + args[1]);//dex文件begin的地址 console.log("[*] size = " + args[2]);//其实有了base就可以算出size了,这个参数不用也行,这里没有用 var begin = args[1]; console.log("magic : " + Memory.readUtf8String(begin)); //打印magic看下是不是dex var address = parseInt(begin,16) + 0x20;//通过begin计算size地址 var dex_size = Memory.readInt(ptr(address));//读出size大小 console.log("sizee : " + dex_size);//比较发现跟args[2]是一样的,证明有begin足够脱壳 var dex_file = new File("/data/data/com.xxx.xxx/" + dex_size.toString() + ".dex", "wb");//这里自己修改下路径,最好放在apk自己的data目录下,不然以后找不着了 dex_file.write(Memory.readByteArray(begin, dex_size)); dex_file.flush(); dex_file.close(); console.log("dump dex success"); }, onLeave: function (retval) { //这里也可以通过retval获得dex_file,通过dex数据结构找到begin和size,dump出来 } });
ps:菜鸟粗粗的阅读了下源码,又自己实践了一下,参考了Android万能脱壳机-- angelToms大佬的帖子https://bbs.pediy.com/thread-252284.htm,反思一下,如何做到对整体加固的脱壳,需要以下几个条件,以下纯属本人yy,大佬轻喷:
1.时机要对,一定要在dex完全加载到内存中才能完全脱下来(因为oat_file 包含dex_file,所以oat完全加载也可以,就是麻烦一点),如果在加载之前脱,脱出来的可能是壳,也可能是不完整的dex
2.脱壳最重要的就是dex文件的begin地址,拿到了这个地址,根据文件结构就可以找到size长度,就可以顺利的脱出来
3.只要脱壳时机对了,一切可以拿到 dex文件的begin地址 的地方都可以脱整体壳;
而 begin 存储在dex_file里, 所以一切可以拿到 dex_file 数据结构的地方都可以 脱整体壳 ;
而 dex_file 数据结构 存储在 oat_ dex_file 数据结构里, 所以一切可以拿到 oat_ dex_file 数据结构的地方都可以 脱整体壳 ;
而
oat_
dex_file
数据结构
存储在
oat_file 数据结构里,
所以一切可以拿到
oat_file
数据结构的地方都可以
脱整体壳
4.这个思路只能脱dex整体加固,抽取加固等菜鸟干完活,抽空阅读dex加载后类和方法的执行源码之后再看看能不能理解
5.下一篇菜鸟学习下oat文件加载的源码,再次增长自己的知识,争取靠大佬们更近一步