一、前言
其實有臺fart的脫殼機挺好的,很多時候就沒有這么多麻煩。
手上有兩臺設備一個Android 7 一個Android 8,沒脫殼需求的時候還沒發現它兩脫殼點完全不一樣,為此有了今天的研究。
源碼在線:http://aospxref.com/
二、源碼解析
源碼閱讀是一件痛苦的事情,我相信不止我這么覺得,所以我會在不影響大家理解的情況下,以最簡單的方式,以最少的源碼量讓大家讀懂理解源碼。
2.1 dex_file
從Android 5.0(Lollipop)開始,ART取代了之前的Dalvik虛擬機作為Android的主要運行時環境。
在/art/runtime/dex_file.cc中提供了對DEX文件的解析和訪問功能,dex_file.cc的主要作用如下:
1、DEX文件解析:dex_file.cc提供了解析DEX文件的功能,可以讀取DEX文件的結構和內容。它能夠解析DEX文件的頭部信息、字符串表、類定義、方法定義等,將這些信息轉化為可供程序使用的數據結構。
2、類加載:dex_file.cc可以根據DEX文件中的類定義加載指定的類。它能夠根據類的全限定名查找并獲取類的相關信息,如父類、接口、字段、方法等。
3、方法調用和執行:通過dex_file.cc,可以獲取DEX文件中方法的字節碼指令,以及方法的參數和返回值類型等信息。這使得程序能夠調用和執行DEX文件中定義的方法。
4、字符串處理:dex_file.cc還提供了對DEX文件中字符串表的操作。它可以讀取DEX文件中的字符串,比如類名、方法名、字段名等,并提供相關的字符串處理功能。
5、資源訪問:在DEX文件中,可能包含一些資源文件(如圖片、音頻等),dex_file.cc可以讀取這些資源文件,以便程序進行相應的操作和使用。
dex_file.h中提供了兩個重要的函數:
const uint8_t* Begin() const {
return begin_;
}
size_t Size() const {
return size_;
}
Begin()獲取了dex在內存中的相對位置,Size()獲取了dex的大小。
2.2 dex 格式規范
Dex文件的版本號標識著Dex文件的格式和兼容性。
每個Dex文件版本都對應著不同的Android系統版本,并且隨著時間的推移,新的Dex文件版本會隨著新版本的Android系統發布而引入。
打開源碼網站找到對應版本源碼,在File Path中輸入dex_file.cc后點搜索,即可跳轉到/art/runtime/dex_file.cc源碼界面。
在Android 7.1.2 源碼界面首先會發現這么一段代碼:
namespace art {
const uint8_t DexFile::kDexMagic[] = { 'd', 'e', 'x', '' };
const uint8_t DexFile::kDexMagicVersions[DexFile::kNumDexVersions][DexFile::kDexVersionLen] = {
{'0', '3', '5', '\0'},
// Dex version 036 skipped because of an old dalvik bug on some versions of android where dex
// files with that version number would erroneously be accepted and run.
{'0', '3', '7', '\0'}
};
其中明確規定了dex唯一允許的值是 035 和 037,Android 8.0 中為 038。注釋中聲明:
由于舊版 Android 中存在 Dalvik 錯誤,Dex 版本 036 已被跳過。Dex 036 版不適用于任何版本的 Android,而且永遠不會。
Android 8.0 中:
const uint8_t DexFile::kDexMagic[] = { 'd', 'e', 'x', '' };
const uint8_t DexFile::kDexMagicVersions[DexFile::kNumDexVersions][DexFile::kDexVersionLen] = {
{'0', '3', '5', '\0'},
// Dex version 036 skipped because of an old dalvik bug on some versions of android where dex
// files with that version number would erroneously be accepted and run.
{'0', '3', '7', '\0'},
// Dex version 038: Android "O" and beyond.
{'0', '3', '8', '\0'}
};
037 和 038 屬于較新的格式規范,并未被廣泛應用。
2.3 Android 7.1.2
2.3.1 DexFile::Open
首先看dex_file.cc中第一處 DexFile::Open 函數,通過OatFile取得DexFile走這個。
感興趣的可以從/art/runtime/oat_file_manager.cc中OatFileManager::OpenDexFilesFromOat往下看:
std::unique_ptr DexFile::Open(const uint8_t* base, size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
bool verify,
std::string* error_msg) {
ScopedTrace trace(std::string("Open dex file from RAM ") + location);
std::unique_ptr dex_file = OpenMemory(base,
size,
location,
location_checksum,
nullptr,
oat_dex_file,
error_msg);
if (verify && !DexFileVerifier::Verify(dex_file.get(),
dex_file->Begin(),
dex_file->Size(),
location.c_str(),
error_msg)) {
return nullptr;
}
return dex_file;
}
解析:
1、參數列表
const uint8_t* base 是一個指向 uint8_t 類型的指針,表示Dex文件的內存基地址。
size_t size 是一個無符號整數,表示Dex文件的大小。
const std::string& location 是一個常量引用,表示Dex文件的位置。
uint32_t location_checksum 是一個無符號 32 位整數,表示Dex文件位置的校驗和。
const OatDexFile* oat_dex_file 是一個指向 OatDexFile 類對象的指針,用于關聯Dex與Oat文件。
bool verify 是一個布爾值,表示是否要驗證Dex文件的有效性。
std::string* error_msg 是一個指向 std::string 類對象的指針,用于存儲錯誤消息(如果有)。
2、函數體
調用另一個成員函數 OpenMemory,傳遞了相同的參數,并返回其結果作為函數的返回值。
在調用中,mem_map->Begin() 返回內存映射的起始地址,mem_map->Size() 返回映射區域的大小,然后將它們以及其他參數傳遞給 OpenMemory 函數。
看第二處 DexFile::Open 函數,通過OatFile取得DexFile失敗走這個,從Zip存檔中打開Dex文件。
可從/art/runtime/oat_file_manager.cc中OatFileManager::OpenDexFilesFromOat函數中DexFile::Open入參參數判斷出:
std::unique_ptr DexFile::Open(const ZipArchive& zip_archive, const char* entry_name,
const std::string& location, std::string* error_msg,
ZipOpenErrorCode* error_code) {
ScopedTrace trace("Dex file open from Zip Archive " + std::string(location));
CHECK(!location.empty());
std::unique_ptr zip_entry(zip_archive.Find(entry_name, error_msg));
if (zip_entry.get() == nullptr) {
*error_code = ZipOpenErrorCode::kEntryNotFound;
return nullptr;
}
std::unique_ptr map(zip_entry->ExtractToMemMap(location.c_str(), entry_name, error_msg));
if (map.get() == 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;
}
std::unique_ptr dex_file(OpenMemory(location, zip_entry->GetCrc32(), map.release(),
error_msg));
if (dex_file.get() == nullptr) {
*error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(),
error_msg->c_str());
*error_code = ZipOpenErrorCode::kDexFileError;
return nullptr;
}
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 (!DexFileVerifier::Verify(dex_file.get(), dex_file->Begin(), dex_file->Size(),
location.c_str(), error_msg)) {
*error_code = ZipOpenErrorCode::kVerifyError;
return nullptr;
}
*error_code = ZipOpenErrorCode::kNoError;
return dex_file;
}
// Technically we do not have a limitation with respect to the number of dex files that can be in a
// multidex APK. However, it's bad practice, as each dex file requires its own tables for symbols
// (types, classes, methods, ...) and dex caches. So warn the user that we open a zip with what
// seems an excessive number.
static constexpr size_t kWarnOnManyDexFilesThreshold = 100;
解析:
1、參數列表
const ZipArchive& zip_archive 是一個常量引用,表示包含Dex文件的Zip存檔對象。
const char* entry_name 是一個指向字符的指針,表示Dex文件在Zip存檔中的入口名稱。
const std::string& location 是一個常量引用,表示Dex文件的位置。
std::string* error_msg 是一個指向 std::string 類對象的指針,用于存儲錯誤消息(如果有)。
ZipOpenErrorCode* error_code 是一個指向 ZipOpenErrorCode 枚舉類型對象的指針,用于存儲錯誤碼。
2、函數體
ScopedTrace 類的實例化,初始化了一個 ScopedTrace 對象,用于跟蹤函數執行過程。同時,構造函數會生成一條與Zip存檔位置相關的跟蹤信息。
使用 CHECK 宏檢查 location 是否為空,如果為空則觸發斷言。
調用 zip_archive 對象的 Find 函數查找指定名稱的Zip文件條目,并使用 std::unique_ptr 管理返回的 ZipEntry 對象。
如果找不到Zip文件條目,則將錯誤碼設置為 ZipOpenErrorCode::kEntryNotFound,并返回空指針。
調用 zip_entry 對象的 ExtractToMemMap 函數,將Zip文件條目提取到內存映射對象 MemMap 中,并使用 std::unique_ptr 管理返回的 MemMap 對象。
如果提取失敗,則根據錯誤消息生成詳細錯誤信息,并將錯誤碼設置為 ZipOpenErrorCode::kExtractToMemoryError,然后返回空指針。
調用另一個成員函數 OpenMemory,傳遞了相同的參數,并返回一個指向 const DexFile 對象的智能指針。
如果打開Dex文件失敗,則根據錯誤消息生成詳細錯誤信息,并將錯誤碼設置為 ZipOpenErrorCode::kDexFileError,然后返回空指針。
調用 DisableWrite 函數將Dex文件設置為只讀模式。如果設置失敗,則根據錯誤消息生成詳細錯誤信息,并將錯誤碼設置為 ZipOpenErrorCode::kMakeReadOnlyError,然后返回空指針。
使用 CHECK 宏檢查Dex文件是否已被設置為只讀模式,如果未設置則觸發斷言。
調用 DexFileVerifier 類的靜態成員函數 Verify 驗證Dex文件的有效性。如果驗證失敗,則將錯誤碼設置為 ZipOpenErrorCode::kVerifyError,然后返回空指針。
如果所有步驟執行成功,將錯誤碼設置為 ZipOpenErrorCode::kNoError,并返回包含Dex文件的智能指針。
此外,還定義了一個常量 kWarnOnManyDexFilesThreshold,用于設置在多dex APK中存在過多Dex文件時發出警告的閾值。
這兩個open函數,只要成功都走了成員函數 OpenMemory 只是其入參不同:
第一處入參:
std::unique_ptr dex_file = OpenMemory(base, size, location, location_checksum, nullptr, oat_dex_file, error_msg);
第二處入參:
std::unique_ptr dex_file(OpenMemory(location, zip_entry->GetCrc32(), map.release(),error_msg));
2.3.2 DexFile::OpenMemory
查看兩個重載的OpenMemory成員函數:
第一處:
std::unique_ptr DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file,
std::string* error_msg) {
CHECK_ALIGNED(base, 4); // various dex file structures must be word aligned
std::unique_ptr dex_file(
new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
if (!dex_file->Init(error_msg)) {
dex_file.reset();
}
return std::unique_ptr(dex_file.release());
}
第二處:
std::unique_ptr DexFile::OpenMemory(const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
std::string* error_msg) {
return OpenMemory(mem_map->Begin(),
mem_map->Size(),
location,
location_checksum,
mem_map,
nullptr,
error_msg);
}
不難發現第二處最后還是走了第一個OpenMemory函數,殊途同歸了。
2.3.3 脫殼點分析
經過上面的分析,可得DexFile::Open后只要打開了dex文件,最終都是走OpenMemory函數,其重要意義不必多說,dddd,這也是其作為常用脫殼點的原因!
參數解析:
const uint8_t* base 是一個指向 uint8_t 類型的指針,表示Dex文件在內存中的基地址。
size_t size 表示Dex文件的大小。
const std::string& location 是一個常量引用,表示Dex文件的位置。
uint32_t location_checksum 表示Dex文件位置的校驗和。
MemMap* mem_map 是一個指向 MemMap 類對象的指針,表示內存映射對象。
const OatDexFile* oat_dex_file 是一個指向 OatDexFile 類對象的常量指針。
std::string* error_msg 是一個指向 std::string 類對象的指針,用于存儲錯誤消息(如果有)。
函數體:
用 CHECK_ALIGNED 宏檢查 base 是否按字對齊,因為Dex文件的結構必須按字對齊。
使用 std::unique_ptr 創建一個指向 DexFile 對象的智能指針 dex_file。DexFile 的構造函數接收多個參數,用于初始化 DexFile 對象。
如果 dex_file->Init(error_msg) 返回 false,表示初始化失敗,將 dex_file 重置為 nullptr。
返回一個指向 dex_file 的常量 std::unique_ptr 對象。
前兩個參數分別是dex的起始地址和大小,想dumpdex直接hook該函數,在加上libart.so的基址即可完整脫下dex。
注意這里的base需加上固定偏移parseInt(base,16) + 0x20。
2.4 Android 8.1.0
其實和Android 7.1.2的分析流程并無太多區別,甚至可以說大差不差,只是最后的脫殼函數變成了OpenCommon函數。
2.4.1 DexFile::Open
看dex_file.cc中第一處 DexFile::Open 函數,通過OatFile取得DexFile走這個。
感興趣的可以從/art/runtime/oat_file_manager.cc中OatFileManager::OpenDexFilesFromOat往下看:
std::unique_ptr 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) {
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);
}
解析:
1、參數列表
const uint8_t* base 是一個指向 uint8_t 類型的指針,表示Dex文件在內存中的基地址。
size_t size 表示Dex文件的大小。
const std::string& location 是一個常量引用,表示Dex文件的位置。
uint32_t location_checksum 表示Dex文件位置的校驗和。
const OatDexFile* oat_dex_file 是一個指向 OatDexFile 類對象的常量指針。
bool verify 表示是否進行驗證。
bool verify_checksum 表示是否驗證校驗和。
std::string* error_msg 是一個指向 std::string 類對象的指針,用于存儲錯誤消息(如果有)。
2、函數體
創建一個 ScopedTrace 對象 trace,并將信息字符串與 location 相連接,用于跟蹤和記錄Dex文件的打開操作。
調用 OpenCommon 函數,將參數傳遞給 OpenCommon,并返回其返回值。
看第二處 DexFile::Open 函數,通過OatFile取得DexFile失敗走這個,從Zip存檔中打開Dex文件。
可從/art/runtime/oat_file_manager.cc中OatFileManager::OpenDexFilesFromOat函數中DexFile::Open入參參數判斷出:
std::unique_ptr DexFile::Open(const std::string& location,
uint32_t location_checksum,
std::unique_ptr map,
bool verify,
bool verify_checksum,
std::string* error_msg) {
ScopedTrace trace(std::string("Open dex file from mapped-memory ") + location);
CHECK(map.get() != nullptr);
if (map->Size() < sizeof(DexFile::Header)) {
*error_msg = StringPrintf(
"DexFile: failed to open dex file '%s' that is too short to have a header",
location.c_str());
return nullptr;
}
std::unique_ptr dex_file = OpenCommon(map->Begin(),
map->Size(),
location,
location_checksum,
kNoOatDexFile,
verify,
verify_checksum,
error_msg);
if (dex_file != nullptr) {
dex_file->mem_map_ = std::move(map);
}
return dex_file;
}
解析:
1、參數列表
const std::string& location 是一個常量引用,表示Dex文件的位置。
uint32_t location_checksum 表示Dex文件位置的校驗和。
std::unique_ptr map 是一個指向 MemMap 類對象的獨占指針,表示內存映射區域。
bool verify 表示是否進行驗證。
bool verify_checksum 表示是否驗證校驗和。
std::string* error_msg 是一個指向 std::string 類對象的指針,用于存儲錯誤消息(如果有)。
2、函數體
創建一個 ScopedTrace 對象 trace,并將信息字符串與 location 相連接,用于跟蹤和記錄Dex文件的打開操作。
使用 CHECK 宏檢查 map 是否為非空指針。如果為空指針,則產生一個斷言失敗并終止程序的錯誤。
如果內存映射區域的大小小于 DexFile::Header 的大小,說明Dex文件太短而無法包含頭部信息,將錯誤消息賦值給 error_msg,然后返回空指針。
調用 OpenCommon 函數,將內存映射區域的起始地址、大小、位置、校驗和等參數傳遞給 OpenCommon 函數,并將返回值賦給 dex_file。
如果 dex_file 不為空指針,將內存映射區域的所有權轉移給 dex_file 的 mem_map_ 成員變量。
返回 dex_file。
這兩個open函數,只要成功都走了成員函數 OpenCommon ,且該函數并無重載。
2.4.2 DexFile::OpenCommon
查看其函數:
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) {
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifyNotAttempted;
}
std::unique_ptr dex_file(new DexFile(base,
size,
location,
location_checksum,
oat_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)) {
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)) {
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifyFailed;
}
return nullptr;
}
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifySucceeded;
}
return dex_file;
}
解析:
1、參數列表
size_t size 表示Dex文件的大小。
const std::string& location 是一個常量引用,表示Dex文件的位置。
uint32_t location_checksum 表示Dex文件位置的校驗和。
const OatDexFile* oat_dex_file 是一個指向 OatDexFile 對象的常量指針。
bool verify 表示是否進行驗證。
bool verify_checksum 表示是否驗證校驗和。
std::string* error_msg 是一個指向 std::string 對象的指針,用于存儲錯誤消息(如果有)。
VerifyResult* verify_result 是一個指向 VerifyResult 枚舉類型對象的指針,用于存儲驗證結果(如果有)。
2、函數體
如果 verify_result 不為空指針,則將其值設置為 VerifyResult::kVerifyNotAttempted,表示驗證尚未嘗試。
使用 new 運算符創建一個 DexFile 對象,并將參數傳遞給構造函數,然后將返回的指針賦給 dex_file。
如果 dex_file 為空指針,則將錯誤消息設置為無法打開Dex文件的錯誤信息,并返回空指針。
調用 Init 函數初始化 dex_file,如果初始化失敗,則將 dex_file 重置為空指針,并返回空指針。
如果需要進行驗證而且驗證失敗,則將驗證結果設置為 VerifyResult::kVerifyFailed,表示驗證失敗,并返回空指針。
如果 verify_result 不為空指針,則將其值設置為 VerifyResult::kVerifySucceeded,表示驗證成功。
返回 dex_file。
和上個版本一樣,前兩個參數分別是dex的起始地址和大小,想dumpdex直接hook該函數,在加上libart.so的基址即可完整脫下dex。
注意這里的base需加上固定偏移parseInt(base,16) + 0x20。
2.5 總結
總結來看在art模式下,無論哪個Android版本下其dex的加載流程總是類似的,都可以從/art/runtime/oat_file_manager.cc源代碼中找出往下的流程,其hook點也并非是一成不變的。
在Android 9版本后從原來的/art/runtime/dex_file.cc變成了/art/libdexfile/dex/dex_file_loader.cc文件中查找hook函數,一樣可以從/art/runtime/oat_file_manager.cc代碼中得出結論。
三、frida dump dex方案
3.1 hook 函數獲取
需要hook的so文件為:libart.so
需要hook的函數為:OpenCommon、OpenMemory
但在C++編譯過程中函數名會進行名稱修飾,用以支持函數重載和命名空間。這種修飾將函數名轉換為一串帶有類型信息的字符串,以便在鏈接和調用過程中進行區分。
所以想直接通過給定OpenCommon、OpenMemory函數名進行hook是行不通的。
那總不能,每換一個設備就需要去找對應的so文件查看其對應的特征函數名吧?
frida enumerateExportsSync() 函數很好的解決了這個問題,可用于枚舉給定模塊(或進程)中的導出函數。
具體來說,它的作用如下:
接收一個模塊名或進程名作為參數,可以是字符串形式的名稱或整數形式的進程ID。
枚舉出指定模塊(或進程)中所有的導出函數。
返回一個包含導出函數信息的數組,每個元素都包含函數名及其對應的內存地址。
那么就通過該函數進行hook函數獲取:
function find_hook_fun() {
var fun_Name = ""; // 聲明一個變量用于存儲函數名
var libart = Module.findBaseAddress('libart.so'); // 查找 libart.so 的基地址
var exports = Module.enumerateExportsSync("libart.so"); // 枚舉 libart.so 導出函數
for (var i = 0; i < exports.length; i++) {
if (exports[i].name.indexOf("OpenMemory") !== -1) { // 如果導出函數名包含 "OpenMemory"
fun_Name = exports[i].name; // 將函數名賦值給 fun_Name
console.log("導出模塊名: " + exports[i].name + "\t\t偏移地址: " + (exports[i].address - libart - 1)); // 打印導出函數名和偏移地址
break; // 跳出循環
} else if (exports[i].name.indexOf("OpenCommon") !== -1) { // 如果導出函數名包含 "OpenCommon"
fun_Name = exports[i].name; // 將函數名賦值給 fun_Name
console.log("導出模塊名: " + exports[i].name + "\t\t偏移地址: " + (exports[i].address - libart - 1)); // 打印導出函數名和偏移地址
break; // 跳出循環
}
}
return fun_Name; // 返回找到的函數名
}
3.2 DEX 校驗
但根據DEX格式的官方規范,唯一允許的值是035和037,Android 8.0中為038。
根據其給定的規則,可編寫如下代碼進行DEX文件校驗:
function DexFileVerifier(Verify) {
var magic_03x = true; // 聲明一個變量用于標識是否符合特定條件,默認為 true
var magic_Hex = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00]; // 給定最常見dex魔數序列
for (var i = 0; i < 8; i++) { // 遍歷魔數序列的每個字節
if (Memory.readU8(ptr(Verify).add(i)) !== magic_Hex[i]) { // 判斷 Verify 內存地址偏移 i 處的字節是否與魔數序列相等
if (Memory.readU8(ptr(Verify).add(i)) === 0x37 || 0x38) { // 如果當前字節等于 0x37 或者 0x38 新版本的dex
console.log('new dex'); // 打印輸出 'new dex'
} else {
magic_03x = false; // 將 magic_03x 置為 false
break; // 跳出循環
}
}
}
return magic_03x; // 返回 magic_03x 的值,表示是否符合特定條件
}
3.3 dump dex
前面已經把要將的都講了,直接上代碼吧,注釋也很詳細:
function dump_Dex(fun_Name, apk_Name) {
if (fun_Name !== '') { // 判斷傳入的參數 fun_Name 是否為空字符串
var hook_fun = Module.findExportByName("libart.so", fun_Name); // 在 libart.so 動態庫中查找指定的導出函數地址
Interceptor.attach(hook_fun, { // 使用 Interceptor 對導出函數進行掛鉤
onEnter: function (args) { // 進入函數之前的回調函數
var begin = 0; // 初始化一個變量用于保存 Dex 文件的起始地址
var dex_flag = false; // 初始化一個標志位,表示是否符合 Dex 文件的魔數
dex_flag = DexFileVerifier(args[0]); // 調用 DexFileVerifier 函數對傳入的第一個參數進行驗證
if (dex_flag === true) { // 如果驗證通過
begin = args[0]; // 保存 Dex 文件的起始地址
}
if (begin === 0) { // 如果起始地址為 0
dex_flag = DexFileVerifier(args[1]); // 再次調用 DexFileVerifier 函數對傳入的第二個參數進行驗證
if (dex_flag === true) { // 如果驗證通過
begin = args[1]; // 保存 Dex 文件的起始地址
}
}
if (dex_flag === true) { // 如果驗證通過
console.log("magic : " + Memory.readUtf8String(begin)); // 打印輸出 Dex 文件的魔數
var address = parseInt(begin,16) + 0x20; // 計算 Dex 文件大小字段的地址
var dex_size = Memory.readInt(ptr(address)); // 讀取 Dex 文件大小
console.log("dex_size :" + dex_size); // 打印輸出 Dex 文件的大小
var dex_path = "/data/data/" + apk_Name + "/" + dex_size + ".dex"; // 構建 Dex 文件保存路徑
var dex_file = new File(dex_path, "wb"); // 創建一個文件對象用于寫入 Dex 文件
dex_file.write(Memory.readByteArray(begin, dex_size)); // 將 Dex 文件內容寫入文件
dex_file.flush(); // 刷新文件緩沖區
dex_file.close(); // 關閉文件
}
},
onLeave: function (retval) { // 函數返回之后的回調函數
}
});
} else {
console.log("Error: no hook function."); // 如果傳入的參數 fun_Name 為空字符串,則打印錯誤消息
}
}
3.4 調用方法
var fun_Name = find_hook_fun(); var apk_Name = 'com.xxx.xxx' // 給定要hook的apk dump_Dex(fun_Name, apk_Name); // frida -U -f com.xxx.xxx -l dumpdex.js --no-pause
在手機上啟動 fs 執行 frida -U -f com.xxx.xxx -l dumpdex.js --no-pause 脫殼后的dex保存在/data/data/應用包名/目錄下。
四、結論
缺點:普通加固可以脫殼,對于類抽取等加固脫出的只是個空殼,需要做指令Dump以及Patch到脫出的Dex文件中,后續如有遇到該類型apk再給大家做對應的分享。
無論是工具脫殼,還是自行脫殼,最根本的思想還是在dex加載流程中進行dump,整個流程中可脫殼時機有很多,但脫殼的時機不同,效果也會不同,我無非是站在前人的肩膀上快速的定位了較好的脫殼點。
合天網安實驗室
雷石安全實驗室
看雪學苑
聚銘網絡
看雪學苑
中國信息安全
看雪學苑
看雪學苑
信息安全與通信保密雜志社
商密君
E安全
D1Net