版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
DEF CON
DEF CON 是全球最大的计算机安全会议之一(极客的奥斯卡),自1993年6月起,每年在美国内华达州的拉斯维加斯举办。
官网:https://media.defcon.org/,DEF CON 黑客大会官方的媒体存档站点,提供历年 DEF CON 大会的公开演讲、幻灯片、视频、音频、代码示例和其他相关资源的免费下载。
在 DEF CON 25(2017 年)上,Check Point 的安全研究员 Slava Makkaveev 和 Avi Bashan 发表了题为《Unboxing Android: Everything You Wanted to Know About Android Packers》的演讲,深入探讨了 Android 应用程序中的加壳技术及其安全影响。
报告文件地址:
对于国内加壳厂商也有分析

DEF 的安全研究员选择的两个脱壳点:art::OpenAndReadMagic 和 DexFile::DexFile

Unboxing Android
在 DEF CON 25 (2017) 上,Avi Bashan 和 Slava Makkaveev 提出过一种非常实用的 Android 加壳脱壳技术:
通过修改 DexFile::DexFile() 构造函数和 OpenAndReadMagic() 方法,可以在应用运行时,拦截 DEX 文件加载过程,从而拿到已经解密后的内存数据,完成脱壳。
1. DexFile::DexFile 构造函数
可以看到 DexFile::DexFile() 的构造函数参数里包含了:
-
const uint8_t* base —— DEX 在内存中的起始地址
-
size_t size —— DEX 的内存大小
DexFile::DexFile(const uint8_t* base,
size_t size,
const uint8_t* data_begin,
size_t data_size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
std::unique_ptr<DexFileContainer> container,
bool is_compact_dex)
: begin_(base),
size_(size),
data_begin_(data_begin),
data_size_(data_size),
location_(location),
location_checksum_(location_checksum),
header_(reinterpret_cast<const Header*>(base)),
string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
method_handles_(nullptr),
num_method_handles_(0),
call_site_ids_(nullptr),
num_call_site_ids_(0),
hiddenapi_class_data_(nullptr),
oat_dex_file_(oat_dex_file),
container_(std::move(container)),
is_compact_dex_(is_compact_dex),
hiddenapi_domain_(hiddenapi::Domain::kApplication) {
CHECK(begin_ != nullptr) << GetLocation();
CHECK_GT(size_, 0U) << GetLocation();
// Check base (=header) alignment.
// Must be 4-byte aligned to avoid undefined behavior when accessing
// any of the sections via a pointer.
CHECK_ALIGNED(begin_, alignof(Header));
InitializeSectionsFromMapList();
}

插入脱壳代码示例
// 打印当前 DEX 文件的位置
LOG(WARNING) << "Dex File: Filename: " << location;
// 判断这个 DEX 是不是从 APP 自身私有目录 加载的。
// 因为系统自己的 framework、boot.oat 里的 DEX 都不是加壳 DEX,只想 dump 应用自己的 DEX。
if (location.find("/data/data/") != std::string::npos) {
LOG(WARNING) << "Dex File: OAT file unpacking launched";
// 创建一个新的文件,比如 /data/data/包名/xxx.dex__unpacked_oat。
std::ofstream dst(location + "__unpacked_oat", std::ios::binary);
// 把内存里的 DEX 数据完整写入磁盘。
dst.write(reinterpret_cast<const char*>(base), size);
// 保存文件,完成脱壳。
dst.close();
} else {
LOG(WARNING) << "Dex File: OAT file unpacking not launched";
}
2. DexFile::OpenAndReadMagic()
这是辅助检查 DEX 文件头的函数。
File OpenAndReadMagic(const char* filename, uint32_t* magic, std::string* error_msg) {
CHECK(magic != nullptr);
File fd(filename, O_RDONLY, /* check_usage= */ false);
if (fd.Fd() == -1) {
*error_msg = StringPrintf("Unable to open '%s' : %s", filename, strerror(errno));
return File();
}
if (!ReadMagicAndReset(fd.Fd(), magic, error_msg)) {
StringPrintf("Error in reading magic from file %s: %s", filename, error_msg->c_str());
return File();
}
return fd;
}

插入脱壳代码示例
struct stat st; // 用于获取文件大小等信息
// 打印当前正在处理的文件路径,便于调试和观察加载的 DEX 来源
LOG(WARNING) << "File_magic: Filename: " << filename;
// 只处理 /data/data 路径下的文件(即应用私有目录中的 dex 文件)
// 这样可以避免处理系统 DEX,提高效率和准确性
if (strstr(filename, "/data/data") != NULL) {
LOG(WARNING) << "File_magic: DEX file unpacking launched";
// 构造输出文件路径,加上 "__unpacked_dex" 后缀
char* fn_out = new char[PATH_MAX];
strcpy(fn_out, filename);
strcat(fn_out, "__unpacked_dex");
// 创建输出文件,设置权限:用户可读写、用户组可读、其他人可读
int fd_out = open(fn_out, O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
// 如果获取原始 dex 文件信息成功(用于获取文件大小)
if (!fstat(fd.get(), &st)) {
// 使用 mmap 将整个 dex 文件映射到内存中
char* addr = (char*)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd.get(), 0);
// 将内存中的内容写入到新文件,完成磁盘级别的 dex dump
int ret = write(fd_out, addr, st.st_size);
// 可选防优化代码(保证 ret 被使用,防止编译器优化)
ret += 1;
// 解除映射,释放内存
munmap(addr, st.st_size);
}
// 关闭输出文件,清理路径内存
close(fd_out);
delete[] fn_out;
} else {
// 如果不是应用私有路径下的文件,跳过处理
LOG(WARNING) << "File_magic: DEX file unpacking not launched";
}
ART 下脱壳原理
ART 下常见的两个 dex 加载器:InMemoryDexClassLoader 和 DexClassLoader
InMemoryDexClassLoader 源码分析
InMemoryDexClassLoader 是 Android 8.0(API 级别 26)引入的一个类,用于动态加载内存中的 Dex。
调用示例:
// 假设 dexBytes 是你的 DEX 文件内容(可以通过解密获得)
ByteBuffer buffer = ByteBuffer.wrap(dexBytes);
// 创建 InMemoryDexClassLoader
ClassLoader loader = new InMemoryDexClassLoader(buffer, ClassLoader.getSystemClassLoader());
// 通过反射加载类并调用方法
Class<?> clazz = loader.loadClass("com.example.MyHiddenClass");
Method m = clazz.getDeclaredMethod("secretMethod");
m.invoke(null);
InMemoryDexClassLoader 支持加载 内存中 一个或多个 Dex。源码如下:
openInMemoryDexFilesNative
Dex 加载过程如下,最终调用到 native 方法 openInMemoryDexFilesNative
InMemoryDexClassLoader(ByteBuffer[] dexBuffers, String librarySearchPath, ClassLoader parent)
└── BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent)
└── DexPathList.initByteBufferDexPath(ByteBuffer[] dexFiles)
└── DexFile(ByteBuffer[] bufs, ClassLoader loader, DexPathList.Element[] elements)
└── DexFile.openInMemoryDexFiles(ByteBuffer[] bufs, ClassLoader loader, DexPathList.Element[] elements)
└── DexFile.openInMemoryDexFilesNative(ByteBuffer[] bufs, byte[][] arrays, int[] starts, int[] ends, ClassLoader loader, DexPathList.Element[] elements)
└── DexFile_openInMemoryDexFilesNative(JNIEnv* env, jclass, jobjectArray buffers, jobjectArray arrays, jintArray jstarts, jintArray jends, jobject class_loader, jobjectArray dex_elements)
DexFile_openInMemoryDexFilesNative 中 调用 OpenDexFilesFromOat 方法 加载 Dex :
static jobject DexFile_openInMemoryDexFilesNative(JNIEnv* env,
jclass,
jobjectArray buffers,
jobjectArray arrays,
jintArray jstarts,
jintArray jends,
jobject class_loader,
jobjectArray dex_elements) {
jsize buffers_length = env->GetArrayLength(buffers);
CHECK_EQ(buffers_length, env->GetArrayLength(arrays));
CHECK_EQ(buffers_length, env->GetArrayLength(jstarts));
CHECK_EQ(buffers_length, env->GetArrayLength(jends));
ScopedIntArrayAccessor starts(env, jstarts);
ScopedIntArrayAccessor ends(env, jends);
// Allocate memory for dex files and copy data from ByteBuffers.
std::vector<MemMap> dex_mem_maps;
dex_mem_maps.reserve(buffers_length);
for (jsize i = 0; i < buffers_length; ++i) {
jobject buffer = env->GetObjectArrayElement(buffers, i);
jbyteArray array = reinterpret_cast<jbyteArray>(env->GetObjectArrayElement(arrays, i));
jint start = starts.Get(i);
jint end = ends.Get(i);
MemMap dex_data = AllocateDexMemoryMap(env, start, end);
if (!dex_data.IsValid()) {
DCHECK(Thread::Current()->IsExceptionPending());
return nullptr;
}
if (array == nullptr) {
// Direct ByteBuffer
uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
if (base_address == nullptr) {
ScopedObjectAccess soa(env);
ThrowWrappedIOException("dexFileBuffer not direct");
return nullptr;
}
size_t length = static_cast<size_t>(end - start);
memcpy(dex_data.Begin(), base_address + start, length);
} else {
// ByteBuffer backed by a byte array
jbyte* destination = reinterpret_cast<jbyte*>(dex_data.Begin());
env->GetByteArrayRegion(array, start, end - start, destination);
}
dex_mem_maps.push_back(std::move(dex_data));
}
// Hand MemMaps over to OatFileManager to open the dex files and potentially
// create a backing OatFile instance from an anonymous vdex.
std::vector<std::string> error_msgs;
const OatFile* oat_file = nullptr;
std::vector<std::unique_ptr<const DexFile>> dex_files =
Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(std::move(dex_mem_maps),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs);
return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}
OpenDexFilesFromOat
OpenDexFilesFromOat 调用 OpenDexFilesFromOat_Impl 加载 Dex
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat_Impl(
std::vector<MemMap>&& dex_mem_maps,
jobject class_loader,
jobjectArray dex_elements,
const OatFile** out_oat_file,
std::vector<std::string>* error_msgs) {
ScopedTrace trace(__FUNCTION__);
std::string error_msg;
DCHECK(error_msgs != nullptr);
// [1] 提取 Dex Header,用于后续校验 checksum、生成路径等
const std::vector<const DexFile::Header*> dex_headers = GetDexFileHeaders(dex_mem_maps);
// [2] 生成临时匿名 dex/vdex 文件路径,获取 checksum 和路径
uint32_t location_checksum;
std::string dex_location;
std::string vdex_path;
bool has_vdex = OatFileAssistant::AnonymousDexVdexLocation(
dex_headers, kRuntimeISA, &location_checksum, &dex_location, &vdex_path);
// [3] 尝试打开 vdex 文件,并检查其中的 dex checksum 是否一致
std::unique_ptr<VdexFile> vdex_file = nullptr;
if (has_vdex && OS::FileExists(vdex_path.c_str())) {
vdex_file = VdexFile::Open(vdex_path, /*writable=*/false, /*low_4gb=*/false,
/*unquicken=*/false, &error_msg);
if (vdex_file == nullptr) {
LOG(WARNING) << "Failed to open vdex " << vdex_path << ": " << error_msg;
} else if (!vdex_file->MatchesDexFileChecksums(dex_headers)) {
LOG(WARNING) << "Dex checksum mismatch: " << vdex_path;
vdex_file.reset(nullptr);
}
}
// [4] 加载内存中的 dex。若存在 vdex 且校验成功,可跳过结构校验
std::vector<std::unique_ptr<const DexFile>> dex_files;
for (size_t i = 0; i < dex_mem_maps.size(); ++i) {
static constexpr bool kVerifyChecksum = true;
const ArtDexFileLoader dex_file_loader;
std::unique_ptr<const DexFile> dex_file(dex_file_loader.Open(
DexFileLoader::GetMultiDexLocation(i, dex_location.c_str()),
location_checksum,
std::move(dex_mem_maps[i]),
/*verify=*/(vdex_file == nullptr) && Runtime::Current()->IsVerificationEnabled(),
kVerifyChecksum,
&error_msg));
if (dex_file != nullptr) {
dex::tracking::RegisterDexFile(dex_file.get()); // 注册用于调试追踪
dex_files.push_back(std::move(dex_file));
} else {
error_msgs->push_back("Failed to open dex files from memory: " + error_msg);
}
}
// [5] 若 vdex 不存在、加载失败,或 class_loader 为空,直接返回 dex_files
if (vdex_file == nullptr || class_loader == nullptr || !error_msgs->empty()) {
return dex_files;
}
// [6] 创建 ClassLoaderContext,确保之后的 oat 加载上下文一致
std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::CreateContextForClassLoader(
class_loader, dex_elements);
if (context == nullptr) {
LOG(ERROR) << "Could not create class loader context for " << vdex_path;
return dex_files;
}
DCHECK(context->OpenDexFiles(kRuntimeISA, ""))
<< "Context created from already opened dex files should not attempt to open again";
// [7] 检查 boot class path checksum 和 class loader context 是否匹配
if (!vdex_file->MatchesBootClassPathChecksums() ||
!vdex_file->MatchesClassLoaderContext(*context.get())) {
return dex_files;
}
// [8] 从 vdex 创建 OatFile 实例并注册
std::unique_ptr<OatFile> oat_file(OatFile::OpenFromVdex(
MakeNonOwningPointerVector(dex_files),
std::move(vdex_file),
dex_location));
DCHECK(oat_file != nullptr);
VLOG(class_linker) << "Registering " << oat_file->GetLocation();
*out_oat_file = RegisterOatFile(std::move(oat_file));
return dex_files;
}


最低0.47元/天 解锁文章
2703

被折叠的 条评论
为什么被折叠?



