在现阶段Android环境下,开发者为了保护自身APP不被反编译以及逻辑的解读、应用重打包,许多开发者都会选择对APP进行加固,APP经过加固,一定程度上保护了APP,使APP没那么容易被反编译。当然,有加固技术的出现,也会有反加固技术的出现,即本文要分析的一种反加固的原理。
BlackDex是一个运行在Android手机上的脱壳工具,支持5.0~12,无需依赖任何环境任何手机都可以使用,包括模拟器。只需几秒,即可对已安装包括未安装的APK进行脱壳。
项目开源地址:https://github.com/CodingGay/BlackDex
本项目现仅供参考,因开源的原因,已经被许多加固厂商Anti,点到为止,现已停止维护。
本项目涉及知识点,按照重要程度排序
BlackDex核心原理是利用虚拟化技术,使APP进程运行,让加固壳进行自解密,最后完成DexFile的Dump,得益于依赖于虚拟化技术,不需要任何手机拥有任何逆向环境,甚至连APP都不用安装到手机上就可以进行脱壳。
本系列将逐步剖析BlackDex是如何进行运作。
前篇 说到,BlackDex是基于虚拟化技术进行的,本项目实际上是基于 BlackBox 的基础下进行开发,很可惜的是由于某些原因此项目没能继续下去。
本系列将选定发表时最后的提交,朋友们可以Clone下来跟着本文走:https://github.com/CodingGay/BlackDex/tree/5580fa8f5d658afae4eb667f8c8d6632be5b9aaf
本文主要分析如何从点击BlackDex图标之后,是如何进行启动APP的进程,此处不会过度深究虚拟化的实现过程,有机会更完这个系列之后会再出一系列虚拟化技术的文章。
核心启动dump方法:top.niunaijun.blackbox.BlackDexCore#dumpDex(java.lang.String)
public InstallResult dumpDex(String packageName) {
// 将软件安装至BlackBox中
InstallResult installResult = BlackBoxCore.get().installPackage(packageName);
if (installResult.success) {
// 安装成功则启动,否则卸载并且返回失败。
boolean b = BlackBoxCore.get().launchApk(packageName);
if (!b) {
BlackBoxCore.get().uninstallPackage(installResult.packageName);
return null;
}
return installResult;
} else {
return null;
}
}
两步事情:
内部实现原理涉及虚拟化相关知识,此处不多深究,应用启动时,程序感知到的第一个方法就是Application#attachBaseContext,在虚拟环境内也是一样的,在经过一系列调度后。我们直接跟进到虚拟化进程的启动的方法:top.niunaijun.blackbox.app.BActivityThread#handleBindApplication
此函数比较长,代码内注释逐步解析
private synchronized void handleBindApplication(String packageName, String processName) {
// 初始化Dump的返回的信息
DumpResult result = new DumpResult();
result.packageName = packageName;
result.dir = new File(BlackBoxCore.get().getDexDumpDir(), packageName).getAbsolutePath();
try {
// 以下是获取需要多开应用的信息,然后对当前进程进行重新设置,因为本进程信息是宿主的。
PackageInfo packageInfo = BlackBoxCore.getBPackageManager().getPackageInfo(packageName, PackageManager.GET_PROVIDERS, BActivityThread.getUserId());
if (packageInfo == null)
return;
.........
// 以上省略部分虚拟化的代码。
// 此处清除dump目录,防止多次脱壳的dex文件乱窜
// clear dump file
FileUtils.deleteDir(new File(BlackBoxCore.get().getDexDumpDir(), packageName));
// 初始化native层代码
VMCore.init(Build.VERSION.SDK_INT);
// 启用IO重定向,支持虚拟应用运行环境
IOCore.get().enableRedirect(packageContext);
......
// 以上省略部分虚拟化代码
// 反射LoadedApk获取多开应用的classloader,并且反射LoadedApk#makeApplication函数,makeApplication中会初始化Application,调用其attachBaseContext、onCreate函数,完成Application的初始化。
try {
ClassLoader call = LoadedApk.getClassloader.call(loadedApk);
application = LoadedApk.makeApplication.call(loadedApk, false, null);
} catch (Throwable e) {
Slog.e(TAG, "Unable to makeApplication");
e.printStackTrace();
}
// 如果走到此处没有发生异常,说明Application已经完成启动,一般在这种时候从理论上来说,应用已经运行起来了,那么自然加固也已经解密完成,我们接下来进行核心的dex的dump工作。
if (Objects.equals(packageName, processName)) {
ClassLoader loader;
// 此处获取需要脱壳的app的classloader
if (application == null) {
loader = LoadedApk.getClassloader.call(loadedApk);
} else {
// 实际上走到这里,理论是启动失败了。不过还可以挣扎一下。
loader = application.getClassLoader();
}
// 调用核心DumpDex方法,进行dex的dump工作
handleDumpDex(packageName, result, loader);
}
} catch (Throwable e) {
// 如果发生异常,通知UI并且从BlackBox中卸载该应用
e.printStackTrace();
mAppConfig = null;
BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpError(e.getMessage()));
BlackBoxCore.get().uninstallPackage(packageName);
}
}
其实从上述代码分析下来,整体的流程已经非常的顺畅了,汇总整理一下
本系列将慢慢梳理每一个流程,本篇讲了BlackDex是如何启动且执行脱壳的,剩下的在后续的文章內继续深扒。
想要了解如何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,里面装载着Dex文件File对象,继续跟进makeDexElements
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,完成我们的脱壳。
反过来,即可得知获取mCookie的线路
通过以上路线,即可获取到该Classloader中的所有mCookie
大体逻辑就如上面所说,我们再回头看看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,有空之后将继续分析是如何对抗指令抽取壳并且实现指令还原。
以上技术点和对于系统源代码分析纯属个人见解,如有不对的地方请指出,感谢。