书接上回
想要了解如何dump出Dex,就要先了解Dex文件是如何加载进内存的。上文说到Application是通过LoadedApk#makeApplication完成的,那么我们看下相关实现。
本文源码为:android11_r1
系统源码:android.app.LoadedApk#makeApplication
@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
// 获取App的classloader
final java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
.........省略
// 内部通过classloader.findClass(appClass),并且进行实例化Application。
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
.........省略
return app;
}
getClassLoader内部流程过于复杂,感兴趣的自己去跟一下,简单点概括:根据ApplicationInfo中的信息,最终生成并返回PathClassloader
通过Classloader可以findClass我们Dex中的方法,由此可以看出,Classloader跟我们Dex是存在某种关系的。我们看一下PathClassloader是如何对Dex文件进行加载的。
源码:dalvik.system.PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
public PathClassLoader(
String dexPath, String librarySearchPath, ClassLoader parent,
ClassLoader[] sharedLibraryLoaders) {
super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
}
}
可以看到都是调用父类BaseClassloader的构造方法,我们主要关注参数dexPath参数,看他是如何打开我们的Dex文件。
源码:dalvik.system.BaseDexClassLoader
public BaseDexClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
boolean isTrusted) {
super(parent);
// Setup shared libraries before creating the path list. ART relies on the class loader
// hierarchy being finalized before loading dex files.
this.sharedLibraryLoaders = sharedLibraryLoaders == null
? null
: Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
reportClassLoaderChain();
}
继续跟进DexPathList,源码:dalvik.system.DexPathList
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
........省略
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
........省略
}
splitDexPath主要做的是检查dexPath是否合规,以及加载DEX,ZIP,JAR的不同处理方法,最后返回一个List
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
// 判断是否.dex结尾
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
........省略
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
以上可以看出,dexFile最终通过loadDexFile函数打开,返回dalvik.system.DexFile对象,最终通过dalvik.system.DexFile作为参数,new Element,然后返回Element[]。
继续跟进
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
源码:dalvik.system.DexFile
DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements)
throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
//System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
}
此处可以看出,通过openDexFile,最终返回mCookie,在Android 5.0的时候类型是long,6.0开始为Object,其实际内容是long[],此处了解即可。
继续跟进
private static native Object openDexFileNative(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements);
最终调用native中的openDexFileNative方法。
下面进入native的源码
源码:art/runtime/native/dalvik_system_DexFile.cc
static jobject DexFile_openDexFileNative(JNIEnv* env,
jclass,
jstring javaSourceName,
jstring javaOutputName ATTRIBUTE_UNUSED,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == nullptr) {
return nullptr;
}
std::vector<std::string> error_msgs;
const OatFile* oat_file = nullptr;
std::vector<std::unique_ptr<const DexFile>> dex_files =
Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs);
return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}
最终是通过Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat,打开了Dex,返回了Native中的DexFile,通过CreateCookieFromOatFileManagerResult返回到Java,也就是mCookie。
如果想进一步了解,可以看下其他博文:ClassLoader--02基于Android5.0的openDexFileNative
继续跟进
static jobject CreateCookieFromOatFileManagerResult(
JNIEnv* env,
std::vector<std::unique_ptr<const DexFile>>& dex_files,
const OatFile* oat_file,
const std::vector<std::string>& error_msgs) {
.......省略
jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files);
if (array == nullptr) {
ScopedObjectAccess soa(env);
for (auto& dex_file : dex_files) {
if (linker->IsDexFileRegistered(soa.Self(), *dex_file)) {
dex_file.release(); // NOLINT
}
}
}
return array;
}
继续跟进
static jlongArray ConvertDexFilesToJavaArray(JNIEnv* env,
const OatFile* oat_file,
std::vector<std::unique_ptr<const DexFile>>& vec) {
// Add one for the oat file.
jlongArray long_array = env->NewLongArray(static_cast<jsize>(kDexFileIndexStart + vec.size()));
if (env->ExceptionCheck() == JNI_TRUE) {
return nullptr;
}
jboolean is_long_data_copied;
jlong* long_data = env->GetLongArrayElements(long_array, &is_long_data_copied);
if (env->ExceptionCheck() == JNI_TRUE) {
return nullptr;
}
// 此处为核心转换
long_data[kOatFileIndex] = reinterpret_cast64<jlong>(oat_file);
for (size_t i = 0; i < vec.size(); ++i) {
// 将每个DexFile对象地址强转为long型,放入long_data中
long_data[kDexFileIndexStart + i] = reinterpret_cast64<jlong>(vec[i].get());
}
env->ReleaseLongArrayElements(long_array, long_data, 0);
if (env->ExceptionCheck() == JNI_TRUE) {
return nullptr;
}
// Now release all the unique_ptrs.
for (auto& dex_file : vec) {
dex_file.release(); // NOLINT
}
// 最终返回
return long_array;
}
以上可以看出来,art将dexfiles遍历,将dexfile强转为long型地址,最终返回到Java中。
所以可以这样认为,mCookie装载的,是art中的DexFile的对象的内存地址。
所以这跟脱壳有什么关系呢?让我们来看一下DexFile中的构造函数
源码:art/libdexfile/dex/dex_file.cc
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();
}
源码:art/libdexfile/dex/dex_file.h
........省略
// The base address of the memory mapping.
const uint8_t* const begin_;
// The size of the underlying memory allocation in bytes.
const size_t size_;
// The base address of the data section (same as Begin() for standard dex).
const uint8_t* const data_begin_;
// The size of the data section.
const size_t data_size_;
// Typically the dex file name when available, alternatively some identifying string.
//
// The ClassLinker will use this to match DexFiles the boot class
// path to DexCache::GetLocation when loading from an image.
const std::string location_;
const uint32_t location_checksum_;
// Points to the header section.
const Header* const header_;
// Points to the base of the string identifier list.
const dex::StringId* const string_ids_;
// Points to the base of the type identifier list.
const dex::TypeId* const type_ids_;
// Points to the base of the field identifier list.
const dex::FieldId* const field_ids_;
// Points to the base of the method identifier list.
const dex::MethodId* const method_ids_;
// Points to the base of the prototype identifier list.
const dex::ProtoId* const proto_ids_;
// Points to the base of the class definition list.
const dex::ClassDef* const class_defs_;
// Points to the base of the method handles list.
const dex::MethodHandleItem* method_handles_;
// Number of elements in the method handles list.
size_t num_method_handles_;
// Points to the base of the call sites id list.
const dex::CallSiteIdItem* call_site_ids_;
// Number of elements in the call sites list.
size_t num_call_site_ids_;
........省略
通过源码中的注释我们可以得知,Dex文件在内存中的映射地址是位于:begin_ ,Dex文件的大小:size_,后来实践中也证实了确实是这么回事。
知道DexFile对象与Dex文件的关系,这样一来就好办了,我们只要拿到所有的mCookie,就等于获取到了所有的DexFile,我们可以将其进行Dump,完成我们的脱壳。
0x2 流程简单梳理
- PathClassloader -> BaseClassloader -> DexPathList
- DexPathList里面通过 makeDexElements 获得Element[]
- Element[]是通过 loadDexFile返回的dalvik.system.DexFile对象创建的
- dalvik.system.DexFile最终通过openDexFileNative返回mCookie
反过来,即可得知获取mCookie的线路
- 先获取PathClassloader中的DexPathList对象
- 再获取DexPathList中的dexElements,也就是Element[]
- 再获取每个Element中的dexFile对象
- 再获取dexFile中的mCookie值
通过以上路线,即可获取到该Classloader中的所有mCookie
0x3 逻辑梳理
- 加固应用需要在最早的流程中进行解密并且加载Dex文件,否则应用将无法运行。
- 由于应用的Class被加固隐藏、保护起来了,如果不解密加载,应用会出现ClassNotFoundException。
- 既然我们应用能运行后,代表findClass可以找到该应用所需的Class,也同时代表了Classloader已经加载进内存了。
- 我们既然知道了Classloader与DexFile的关系,那么我们就可以根据Classloader,使用以上梳理的点获取该Classloader中的所有mCookie。
大体逻辑就如上面所说,我们再回头看看BlackDex时如何处理的。
0x4 BlackDex
书接上回
private void handleDumpDex(String packageName, DumpResult result, ClassLoader classLoader) {
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
try {
VMCore.cookieDumpDex(classLoader, packageName);
} finally {
mAppConfig = null;
File dir = new File(result.dir);
if (!dir.exists() || dir.listFiles().length == 0) {
BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpError("not found dex file"));
} else {
BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpSuccess());
}
BlackBoxCore.get().uninstallPackage(packageName);
}
}).start();
}
此处主要的工作是call
VMCore.cookieDumpDex(classLoader, packageName);
如果发生异常,则通知UI脱壳失败
继续跟进,一个大方法,我们逐步分析
public static void cookieDumpDex(ClassLoader classLoader, String packageName) {
// 核心:根据Classloader获取当前Classloader中所有的Cookie
List<Long> cookies = DexFileCompat.getCookies(classLoader);
File file = new File(BlackBoxCore.get().getDexDumpDir(), packageName);
DumpResult result = new DumpResult();
result.dir = file.getAbsolutePath();
result.packageName = packageName;
// 此处跟开启多线程进行脱壳
int availableProcessors = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(availableProcessors <= 0 ? 1 : availableProcessors);
CountDownLatch countDownLatch = new CountDownLatch(cookies.size());
AtomicInteger atomicInteger = new AtomicInteger(0);
BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpProcess(cookies.size(), atomicInteger.getAndIncrement()));
// 此处遍历每一个cookie
for (int i = 0; i < cookies.size(); i++) {
long cookie = cookies.get(i);
........省略通知UI操作
executorService.execute(() -> {
// 调用native方法进行真正脱壳
cookieDumpDex(cookie, file.getAbsolutePath(), BlackBoxCore.get().isFixCodeItem());
........省略通知UI操作
});
}
File[] files = file.listFiles();
if (files != null) {
for (File dex : files) {
if (dex.isFile() && dex.getAbsolutePath().endsWith(".dex")) {
// 如果脱壳成功,修复Dex的signature与checksum
DexUtils.fixDex(dex);
}
}
}
}
使用方法
List<Long> cookies = DexFileCompat.getCookies(classLoader);
获取Classloader所有的Cookie,原理我们上面也讲过,感兴趣的可以自行查看代码,此处不再细说。
以下是最核心的脱壳操作,我们逐步分析
void DexDump::cookieDumpDex(JNIEnv *env, jlong cookie, jstring dir, jboolean fix) {
// 如果环境没有初始化则去初始化,初始化内容我们后续分析
if (beginOffset == -2) {
init(env);
}
// 如果初始化失败,我们则无法脱壳
if (beginOffset == -1) {
ALOGD("dumpDex not support!");
return;
}
// 以上初始化核心工作为:获取DexFile->begin_的偏移量,知道了begin_我们才知道Dex在内存中的什么位置,才可以将它Dump出来,否则将无法脱壳。
// Dex magic,某数字加固为了防止内存搜索脱壳,会将Dex magic抹除变成00 00 00 00 00 00 00,使得内存扫描无法找到Dex文件从而无法脱壳,BlackDex脱壳后会将此修复。
char magic[8] = {0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00};
// 将cookie转换成内存地址
auto base = reinterpret_cast<char *>(cookie);
// 根据初始化beginOffset,通过base + 偏移,得知begin_所在的位置
auto begin = *(size_t *) (base + beginOffset * sizeof(size_t));
// 此处经常会出现bad point,所以检查一下是否野指针
if (!PointerCheck::check(reinterpret_cast<void *>(begin))) {
return;
}
auto dirC = env->GetStringUTFChars(dir, 0);
// 此处根据Dex + 0x20获取Dex文件的总大小,方便我们对Dex进行Dump
auto dexSizeOffset = ((unsigned long) begin) + 0x20;
int size = *(size_t *) dexSizeOffset;
void *buffer = malloc(size);
if (buffer) {
// 将内存中的Dex复制出来
memcpy(buffer, reinterpret_cast<const void *>(begin), size);
// 修复 magic
memcpy(buffer, magic, sizeof(magic));
// 以下是检验Dex是否完整,符合格式
const bool kVerifyChecksum = false;
const bool kVerify = true;
const art_lkchan::DexFileLoader dex_file_loader;
std::string error_msg;
std::vector<std::unique_ptr<const art_lkchan::DexFile>> dex_files;
if (!dex_file_loader.OpenAll(reinterpret_cast<const uint8_t *>(buffer),
size,
"",
kVerify,
kVerifyChecksum,
&error_msg,
&dex_files)) {
// Display returned error message to user. Note that this error behavior
// differs from the error messages shown by the original Dalvik dexdump.
ALOGE("Open dex error %s", error_msg.c_str());
return;
}
// 是否需要深度修复,此处下文再讲。
if (fix) {
fixCodeItem(env, dex_files[0].get(), begin);
}
// 最终我们将Dex写出到指定的目录
char path[1024];
sprintf(path, "%s/cookie_%d.dex", dirC, size);
auto fd = open(path, O_CREAT | O_WRONLY, 0600);
ssize_t w = write(fd, buffer, size);
fsync(fd);
if (w > 0) {
ALOGE("cookie dump dex ======> %s", path);
} else {
remove(path);
}
close(fd);
free(buffer);
env->ReleaseStringUTFChars(dir, dirC);
}
}
我们分析一下init方法,看看BlackDex是如何获取begin_偏移量的
void init(JNIEnv *env) {
// 此处使用PLT HOOK处理掉kill、killpg方法,避免在脱壳过程中应用kill掉自己。
const char *soName = ".*\\.so$";
xhook_register(soName, "kill", (void *) new_kill,
(void **) (&orig_kill));
xhook_register(soName, "killpg", (void *) new_killpg,
(void **) (&orig_killpg));
xhook_refresh(0);
// 此处出现了一个loadEmptyDex方法
jlongArray emptyCookie = VmCore::loadEmptyDex(env);
.......省略
}
我们脱壳的第一步就是想要获取begin_的偏移量,但是偏偏这个begin_在DexFile中不一定是固定的位置,如果我们根据AOSP的代码,根据每个Android版本区分写死也没有问题,但是如果说某个手机厂商中间加了一个字段,减了一个字段。那么这里必定出现脱壳失败,此处我使用了一个 预测法 ,这种方法在许多Hook框架上也是常见的,比如需要预测ArtMethod的flags偏移的。
下面将说一下是怎么进行预测,首先我们观察每个Android版本的,可以发现。
// The base address of the memory mapping.
const uint8_t* const begin_;
// The size of the underlying memory allocation in bytes.
const size_t size_;
begin_与size_都是保持相对位置的,我们可以先这样决定,我们就认为在实际情况中,他们两个的值就是保持相对位置。
既然这样的话,我们不知道begin_,那么size_,也就是Dex文件的大小,我们是肯定能知道的,我们根据这个大小,来获取size_,然后通过 begin_与size_都是保持相对位置 这个特性,我们减去一个uint8_t的大小,那就能获取到begin_的偏移量。
继续看代码是如何操作的
void init(JNIEnv *env) {
// 此处loadEmptyDex方法,就是加载一个我们指定的Dex文件,这个Dex文件是BlackDex里面准备好的,我已经知道了这个Dex的大小,此处我们加载这个Dex并且获取他的Cookie
jlongArray emptyCookie = VmCore::loadEmptyDex(env);
jsize arrSize = env->GetArrayLength(emptyCookie);
if (env->ExceptionCheck() == JNI_TRUE) {
return;
}
jlong *long_data = env->GetLongArrayElements(emptyCookie, nullptr);
// 此处遍历cookie
for (int i = 0; i < arrSize; ++i) {
jlong cookie = long_data[i];
if (cookie == 0) {
continue;
}
// 此处我们从cookie的内存地址,也就是DexFile的内存地址,开始往下搜索。
// 最多搜索10个size_t的大小
auto dex = reinterpret_cast<char *>(cookie);
for (int ii = 0; ii < 10; ++ii) {
auto value = *(size_t *) (dex + ii * sizeof(size_t));
// 如果此时,搜索到值等于1872,我们的Dex文件大小也是1872,那么可以确定这个内存地址为size_
if (value == 1872) {
// 那么我们将size_的偏移 - 1,就取得了begin_的所在内存地址。
beginOffset = ii - 1;
env->ReleaseLongArrayElements(emptyCookie, long_data, 0);
return;
}
}
}
env->ReleaseLongArrayElements(emptyCookie, long_data, 0);
beginOffset = -1;
}
这样,我们就可以获取到begin_与size_的偏移量,就可以配合cookieDumpDex进行Dex的Dump操作。至此,BlackDex的脱壳工作就此结束。但是想要真正了解BlackDex的还远远不够,更核心提供脱壳能力的还是虚拟化技术。
BlackDex还有一项加强功能为深度修复,此模式下可以对抗一些指令抽取壳,详情可见Github,有空之后将继续分析是如何对抗指令抽取壳并且实现指令还原。
以上技术点和对于系统源代码分析纯属个人见解,如有不对的地方请指出,感谢。
哥,什么时候更深度脱壳
牛逼,大佬
膜拜大佬,我连代码都看不明白(
大佬 长见识了
好棒,求更深度修复文章
写的真好 多写点 大佬
大佬这是java和c都通啊
非常好的,脑回路
妙不可言:预测法
都是前辈们的经验,哈哈哈哈