加壳与脱壳之一代壳dex保护

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

Dalvik4.4.4

dex加载源码分析

虽然加壳技术有所不同,第一步依然是dump内存中dex,虽然安卓4.4后Dalvik淡出视野,但是影响深远。Dalvik系统DexClassLoader加载dex具体流程:

libcore目录下搜索DexClassLoader源码只有一个构造函数

dexPath:需要加载的dex路径
optimizedDirectory:dex优化过程中产生的odex的存放路径
libraryPath:当前classloader需要加载so的路径
parent:双亲委派中的当前dexclassloader的父节点设置的classloader

1
2
3
4
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

由于只调用了父类的构造函数,我们进入父类BaseDexClassLoader封装大量的函数,真正逻辑存在BaseDexClassLoader的构造函数中实现,调用了父类的构造函数,该类存在与/libcore/libart/libcore/libdvm,说明在安卓4.4中已经开始引入art相关逻辑。

1
2
3
4
5
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

进入Dalvik的ClassLoader的构造函数,将当前的ClassLoader的父节点作为传入的parent。

1
2
3
4
5
6
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}

再进入new DexPathList再初始化pathList实例,进入DexPathList构造函数,前面对参数校验,真正起作用this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);,makeDexElements方法调用了loadDexFile返回的dex添加到element数组并返回数组。

loadDexFile中再度调用了DexFile.loadDex(file.getPath(), optimizedPath, 0),该方法新建了一个DexFile对象,return new DexFile(sourcePathName, outputPathName, flags);,在Dexfile中调用了openDexFile(sourceName, outputName, flags)完成了对dex的处理,openDexFile中调用了openDexFileNative,其中又调用openDexFileNative方法,跟进openDexFileNative发现是一个使用c/c++实现的native函数,native private static int openDexFileNative。该方法处理类/libcore/dalvik/src/main/java/dalvik/system/DexFile.java中,对应的实现文件就是dalvik_system_DexFile查看方法Dalvik_dalvik_system_DexFile_openDexFileNative,其中调用了dvmRawDexFileOpen实现对dex文件的打开。

1
2
3
4
5
6
7
8
9
if (hasDexExtension(sourceName)  // 对当前文件后缀的校验
&& dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
ALOGV("Opening DEX file '%s' (DEX)", sourceName);

pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
pDexOrJar->pDexMemory = NULL;
}

dvmRawDexFileOpendexFd = open(fileName, O_RDONLY);打开dex文件,verifyMagicAndGetAdler32(dexFd, &adler32) < 0对dex魔术字校验,cachedName = dexOptGenerateCacheFileName(fileName, NULL);生成优化后的odex文件路径,dvmOptimizeDexFile(optFd, dexOffset, fileSize,fileName, modTime, adler32, isBootstrap);优化当前的dex,跟进发现存在于DexPrepare.cpp中的dvmOptimizeDexFile(int fd, off_t dexOffset, long dexLength,const char* fileName, u4 modWhen, u4 crc, bool isBootstrap),该方法中pid = fork();fd为打开的dex文件的id,dexLength为dex文件的大小,pid=fork()新建了子进程用于调用/bin/dexopt对当前dex文件进行优化,结果生成odex文件。bin/dexopt的main方法中传入的文件校验是dex时,调用static int fromDex(int argc, char* const argv[])首先调用dvmPrepForDexOpt对当前优化环境准备,再调用dvmContinueOptimization对当前的fd文件进行优化

1
2
3
dexLength < (int) sizeof(DexHeader)  字节长度判断
mapAddr = mmap(NULL, dexOffset + dexLength, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); mmap对当前dex文件的内容进行内存映射
rewriteDex(((u1*) mapAddr) + dexOffset, dexLength, doVerify, doOpt, &pClassLookup, NULL); 重写dex(起始地址,长度,...)

查看static bool rewriteDex(u1* addr, int len, bool doVerify, bool doOpt,DexClassLookup** ppClassLookup, DvmDex** ppDvmDex)方法参数有加载到内存中dex的起始地址,字节数。该方法中暴露出在Dalvik中脱壳点的相关函数,dvmDexFileOpenPartial函数中出现dexFileParse,参数包含起始地址和字节数。

以上即是dexclassloader在动态加载dex时Dalvik实现的逻辑处理,很多时机点都出现了加载的dex的起始地址,就是最好的脱壳时机。很多脱壳方法都是对**dvmDexFileOpenPartialdexFileParse进行下断点或者进行hook,取出第一个参数(起始地址)和第二个参数(dex长度),在第一次DexPrepare.cpp中,其实在mmap对dex内存映射时包含dex文件,对当前映射区域进行dump也可以脱下从文件形式加载dex,在rewriteDex**时也出现了dex文件加载的起始地址和大小,自然也是可以进行dump等等。通过Cydia、xposed、frida都可以对实现对关键时机的hook,取出前两个参数,拿到起始地址和长度,dump下来内存区域即可实现脱壳。

定制源码脱壳

对新的一些加壳厂商的产品依然有效,除非厂商对这些函数进行hook修改,或者参考Dalvik修改实现自己的逻辑。进入Ubuntu 1604x64_4.4的编译环境虚拟机,tom/admin,4.4的源码为hammerhead及其驱动。通过编译源码的方式实现以下为部分脱壳点:

dvmDexFileOpenPartial

搜索 dvmDexFileOpenPartial 发现在/dalvik/vm/DvmDex.cpp文件中,使用Geany打开 ~/SourceCode/android-4.4.4_r1/dalvik/vm/DvmDex.cpp,找到dvmDexFileOpenPartial方法,只需要保存起始地址和大小即可。

修改int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
//新建保存路径用于拼接脱壳app产生的dex路径,对每个脱壳的app的脱下的dex单独起名
char dexfilepath[100]={0};
//当前进程的pid
int pid=getpid();
//文件路径拼接(文件大小,进程pid)
sprintf(dexfilepath,"/sdcard/%d_%d_dvmDexFileOpenPartial.dex",len,pid);
//写入sdcard保存的dex,很多壳对抗内存dump将fopen等方法hook,可以使用系统调用当中的写入,避免使用标准的文件写入函数导致dump不下来
int fd = open(dexfilepath,O_CREAT|O_RDWR,0666);
if(fd>0){
write(fd,addr,len);
close(fd);
}

dvmDexFileOpenPartial脱壳点

dexFileParse

搜索**dexFileParse**发现存在/dalvik/libdex/DexFile.cpp中,使用geany编辑器打开DexFile.cpp

修改DexFile* dexFileParse(const u1* data, size_t length, int flags)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
//新建保存路径用于拼接脱壳app产生的dex路径,对每个脱壳的app的脱下的dex单独起名
char dexfilepath[100]={0};
//当前进程的pid
int pid=getpid();
//文件路径拼接(文件大小,进程pid)
sprintf(dexfilepath,"/sdcard/%d_%d_dvmFileParse.dex",length,pid);
//写入sdcard保存的dex,很多壳对抗内存dump将fopen等方法hook,可以使用系统调用当中的写入,避免使用标准的文件写入函数导致dump不下来
int fd = open(dexfilepath,O_CREAT|O_RDWR,0666);
if(fd>0){
write(fd,data,length);
close(fd);
}

dexFileParse脱壳点

mmap

搜索 mmap 存在于/dalvik/vm/analysis/DexPrepare.cpp的中,

修改bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)

1
2
3
4
5
6
7
8
9
10
11
char dexfilepath[100]={0};
//当前进程的pid
int pid=getpid();
//文件路径拼接(文件大小,进程pid)
sprintf(dexfilepath,"/sdcard/%d_%d_dvmContinueOptimization.dex",dexLength,pid);
//写入sdcard保存的dex,很多壳对抗内存dump将fopen等方法hook,可以使用系统调用当中的写入,避免使用标准的文件写入函数导致dump不下来
int fd = open(dexfilepath,O_CREAT|O_RDWR,0666);
if(fd>0){
write(fd,mapAddr,dexOffset + dexLength);
close(fd);
}

mmap

第一次映射到内存中需要将dex通过010Editor修正,删除64 65 78 0A前的字符。

rewriteDex

搜索rewriteDex位于/dalvik/vm/analysis/DexPrepare.cpp

修改static bool rewriteDex(u1* addr, int len, bool doVerify, bool doOpt, DexClassLookup** ppClassLookup, DvmDex** ppDvmDex)

1
2
3
4
5
6
7
8
9
10
11
char dexfilepath[100]={0};
//当前进程的pid
int pid=getpid();
//文件路径拼接(文件大小,进程pid)
sprintf(dexfilepath,"/sdcard/%d_%d_rewriteDex.dex",len,pid);
//写入sdcard保存的dex,很多壳对抗内存dump将fopen等方法hook,可以使用系统调用当中的写入,避免使用标准的文件写入函数导致dump不下来
int fd = open(dexfilepath,O_CREAT|O_RDWR,0666);
if(fd>0){
write(fd,addr,len);
close(fd);
}

rewriteDex

dexSwapVerify

搜索dexSwapVerify存在于/dalvik/libdex/DexSwapVerify.cpp

修改int dexSwapAndVerify(u1* addr, int len)

1
2
3
4
5
6
7
8
9
10
11
char dexfilepath[100]={0};
//当前进程的pid
int pid=getpid();
//文件路径拼接(文件大小,进程pid)
sprintf(dexfilepath,"/sdcard/%d_%d_dexSwapAndVerify.dex",len,pid);
//写入sdcard保存的dex,很多壳对抗内存dump将fopen等方法hook,可以使用系统调用当中的写入,避免使用标准的文件写入函数导致dump不下来
int fd = open(dexfilepath,O_CREAT|O_RDWR,0666);
if(fd>0){
write(fd,addr,len);
close(fd);
}

dexSwapVerify

编译

1
2
3
4
~/SourceCode/android-4.4.4_r1$ source build/envsetup.sh
~/SourceCode/android-4.4.4_r1$ lunch
7 选择aosp_hammerhead-userdebug
~/SourceCode/android-4.4.4_r1$ time make -j4 如果报错直接make单线程编译

编译源码

生成路径~/SourceCode/android/4.4.4_r1/out/target/product/hammerhead,将boot.img,cache.img,ramdisk.img,system.img,userdata.img拷出来

adb reboot bootloader
fastboot flash system system.img 常用的img有boot,cache,ramdisk,system,userdata

安装LoadDex.apk,并把ClassLoaderTest生成的classes.dex放入/sdcard。在Activity启动的时候加载/sdcard的classes.dex的TestActivity并打印I'm from TestActivity.onCreate,这就是一代壳的基本原理。

重启系统后/sdcard中已经出现一些系统中脱下的dex,启动LoadDex,grep -ril "TestActivity" ./*.dex 找到LoadDex中脱下的dex,gda可以看到优化后的odex的TestActivity中onCreate方法

真实案例

a.apk 快递100 《百度》加固

启动后进入/sdcard , grep -ril “SplashActivity” ./*.dex

b.apk 货拉拉司机版《爱加密》企业版本加固

启动后进入/sdcard , grep -ril “mvp/ui/SplashActivity” ./*.dex,内容都为null,说明用了函数抽取。

ART8.0.0

InMemoryDexClassLoader源码分析

加载内存中的解密字节流过程art的具体流程:

Android 8.0中libcore搜索InMemoryDexClassLoader两个构造函数,分别加载一个或多个dex,super(dexBuffers, parent);调用了父类的构造函数。进入父类BaseDexClassLoader.java的构造函数

1
2
3
4
5
6
7
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
// 设置parent
super(parent);
// 初始化DexPathList对象
this.pathList = new DexPathList(this, dexFiles);
}

跟进public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles)构造函数,前几步对传参判断,紧接着对传入的so库的处理

1
2
3
4
5
6
this.definingContext = definingContext;
// TODO It might be useful to let in-memory dex-paths have native libraries.
this.nativeLibraryDirectories = Collections.emptyList();
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);

传入的dex只需要看this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);摘除核心代码如下:

1
2
3
4
5
6
7
8
9
10
private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
List<IOException> suppressedExceptions) {
Element[] elements = new Element[dexFiles.length];
int elementPos = 0;
for (ByteBuffer buf : dexFiles) {
DexFile dex = new DexFile(buf);
elements[elementPos++] = new Element(dex);
}
return elements;
}

进入DexFile(buf)的构造函数中,发现openInMemoryDexFile将buf在内存中打开返回了mCookie

1
2
3
4
5
DexFile(ByteBuffer buf) throws IOException {
mCookie = openInMemoryDexFile(buf);
mInternalCookie = mCookie;
mFileName = null;
}

查看openInMemoryDexFile方法,分别根据条件创建了两次cookie

1
2
3
4
5
6
7
private static Object openInMemoryDexFile(ByteBuffer buf) throws IOException {
if (buf.isDirect()) {
return createCookieWithDirectBuffer(buf, buf.position(), buf.limit());
} else {
return createCookieWithArray(buf.array(), buf.position(), buf.limit());
}
}

分别查看createCookieWithDirectBuffercreateCookieWithArray两个方法,发现是两个native函数

1
2
private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
private static native Object createCookieWithArray(byte[] buf, int start, int end);

在art模块中Full Search createCookieWithDirectBuffer,进入DexFile_createCookieWithDirectBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static jobject DexFile_createCookieWithDirectBuffer(JNIEnv* env,
jclass, // 静态函数 第一个是jclass当前类
jobject buffer, // 传入内存中的字节流
jint start,
jint end) {
uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
if (base_address == nullptr) {
ScopedObjectAccess soa(env);
ThrowWrappedIOException("dexFileBuffer not direct");
return 0;
}

std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
if (dex_mem_map == nullptr) {
DCHECK(Thread::Current()->IsExceptionPending());
return 0;
}

size_t length = static_cast<size_t>(end - start);
memcpy(dex_mem_map->Begin(), base_address, length); // 对当前字节流进行内存拷贝memcpy,传入begin和length就是dex的起始地址,可以进行dump
return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}

createCookieWithDirectBuffercreateCookieWithArray都进行了CreateSingleDexFileCookie

1
2
3
4
5
6
7
8
9
10
static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
std::unique_ptr<const DexFile> dex_file(CreateDexFile(env, std::move(data))); // 根据传入dex文件在内存中信息创建了DexFile实例
if (dex_file.get() == nullptr) {
DCHECK(env->ExceptionCheck());
return nullptr;
}
std::vector<std::unique_ptr<const DexFile>> dex_files;
dex_files.push_back(std::move(dex_file));
return ConvertDexFilesToJavaArray(env, nullptr, dex_files); // 对该dex_files进行返回
}

通过CreateDexFile创建DexFile对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map) { // 返回dex文件在内存中映射的地址
std::string location = StringPrintf("Anonymous-DexFile@%p-%p",
dex_mem_map->Begin(),
dex_mem_map->End());
std::string error_message;
std::unique_ptr<const DexFile> dex_file(DexFile::Open(location,
0,
std::move(dex_mem_map),
/* verify */ true,
/* verify_location */ true,
&error_message));
if (dex_file == nullptr) {
ScopedObjectAccess soa(env);
ThrowWrappedIOException("%s", error_message.c_str());
return nullptr;
}

if (!dex_file->DisableWrite()) {
ScopedObjectAccess soa(env);
ThrowWrappedIOException("Failed to make dex file read-only");
return nullptr;
}

return dex_file.release();
}

进入DexFile::Open,其中又调用了OpenCommon函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
std::unique_ptr<const DexFile> DexFile::Open(const std::string& location, // 加载dex文件的路径
uint32_t location_checksum,
std::unique_ptr<MemMap> 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<DexFile> dex_file = OpenCommon(map->Begin(),
map->Size(),
location,
location_checksum,
kNoOatDexFile,
verify,
verify_checksum,
error_msg);

进入OpenCommon函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base, // 加载dex文件的起始地址
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<DexFile> dex_file(new DexFile(base,
size,
location,
location_checksum,
oat_dex_file)); // 创建新的DexFile实例,构造函数也包含起始地址和大小

说明InMemoryDexClassLoader在对内存中bytebuffer的dex信息进行加载流程中涉及很多函数逻辑都包含dex信息的起始地址和大小。InMemoryDexClassLoader并没有对内存中dex信息进行编译生成相应的oat文件,这是与DexClassLoader的不同。

InMemoryDexClassLoader通用脱壳点:

DexClassLoader加载dex源码分析

DexClassLoader只有一个构造函数

1
2
3
4
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

进入父类BaseDexClassLoader.java的构造函数

1
2
3
4
5
6
7
8
9
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}

同样的进入DexPathList的核心逻辑this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions, definingContext);,调用了makeDexElements,其中加载dex文件loadDexFile(file, optimizedDirectory, loader, elements);

1
2
3
4
5
6
7
8
9
10
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);
}
}

DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements)调用了5个参数的loadDex,进入loadDex函数

1
2
3
4
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements); // 创建DexFile实例
}

进入DexFile五参数构造函数

1
2
3
4
5
6
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中调用了native函数private static native Object openDexFileNative(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements);

1
2
3
4
5
6
7
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);

Full Search搜索art目录下的openDexFileNative

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static jobject DexFile_openDexFileNative(JNIEnv* env,
jclass, // 静态函数
jstring javaSourceName, // 加载的dex路径
jstring javaOutputName ATTRIBUTE_UNUSED,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == nullptr) {
return 0;
}

Runtime* const runtime = Runtime::Current();
ClassLinker* linker = runtime->GetClassLinker();
std::vector<std::unique_ptr<const DexFile>> dex_files;
std::vector<std::string> error_msgs;
const OatFile* oat_file = nullptr; // 出现oat

dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs); // 调用OpenDexFilesFromOat编译生成oat流程

if (!dex_files.empty()) {
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();
}
}
}
return array;
}
return nullptr;
}
}

DexClassLoader第一次动态加载解密的dex时必然没有进行编译生成oat,查看OpenDexFilesFromOat,首先OatFileAssistant oat_file_assistant(dex_location,kRuntimeISA,!runtime->IsAotCompiler());新建了OatFileAssistant 对象,if (!oat_file_assistant.IsUpToDate())由于没有生成oat对象的,进入判断中的MakeUpToDate函数,其中调用了return GenerateOatFileNoChecks(info, target, error_msg);GenerateOatFileNoChecks最终进入调用dex2oat编译生成oat的流程

1
2
3
4
5
6
7
8
9
if (!Dex2Oat(args, error_msg)) {
// Manually delete the oat and vdex files. This ensures there is no garbage
// left over if the process unexpectedly died.
vdex_file->Erase();
unlink(vdex_file_name.c_str());
oat_file->Erase();
unlink(oat_file_name.c_str());
return kUpdateFailed;
}

Dex2Oat中准备相关二进制程序参数的相关信息,最终调用return Exec(argv, error_msg);实现dex2oat编译的过程,进入Exec中调用了ExecAndReturnCode,其中首次pid_t pid=fork()进行了进程fork,在子进程当中使用execve(program, &args[0], envp);执行dex2oat实际执行流程。

说明我们在整个流程中其中某个函数进行修改或者hook都会导致dex2oat流程结束,强制结束dex2oat流程,可以让我们在DexClassLoader在加载dex时过程变的很有效率,减少dex2oat编译的流程,要想实现art下的函数抽取技术,也是要阻断dex2oat的流程。当我们阻断了dex2oat会导致openDexFileNativeoat_file 文件无法生成,在调用OatFileManager::OpenDexFilesFromOat中进入尝试判断原始dex文件oat_file_assistant.HasOriginalDexFiles()并通过DexFile::Open进行加载dex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)) {
LOG(WARNING) << error_msg;
error_msgs->push_back("Failed to open dex files from " + std::string(dex_location)
+ " because: " + error_msg);
}
} else {
error_msgs->push_back("Fallback mode disabled, skipping dex files.");
}
} else {
error_msgs->push_back("No original dex files found for dex location "
+ std::string(dex_location));
}
}

进入DexFile::Open中可看到File fd = OpenAndReadMagic(filename, &magic, error_msg);到此出现了第一个脱壳点OpenAndReadMagic,此时dex还未加载到内存当中。紧接着开始判断dex魔术头,并进入DexFile::OpenFile函数

1
2
3
4
5
6
7
8
9
10
11
12
13
if (IsDexMagic(magic)) {
std::unique_ptr<const DexFile> dex_file(DexFile::OpenFile(fd.Release(), // 文件描述符,可以作为脱壳点
location,
/* verify */ true,
verify_checksum,
error_msg));
if (dex_file.get() != nullptr) {
dex_files->push_back(std::move(dex_file));
return true;
} else {
return false;
}
}

进入DexFile::OpenFile函数发现通过MemMap::MapFile将dex进行了内存映射

1
2
3
4
5
6
7
8
map.reset(MemMap::MapFile(length,
PROT_READ,
MAP_PRIVATE,
fd,
0,
/*low_4gb*/false,
location.c_str(),
error_msg));

再进入OpenCommon函数中,参数中也包含了dex文件的映射区域的起始地址,出现了第二个脱壳点。

1
2
3
4
5
6
7
8
std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
map->Size(),
location,
dex_header->checksum_,
kNoOatDexFile,
verify,
verify_checksum,
error_msg);

跟进DexFile::OpenCommon中,发现其中调用了DexFile的构造函数

1
2
3
4
5
std::unique_ptr<DexFile> dex_file(new DexFile(base,
size,
location,
location_checksum,
oat_dex_file));

至此出现了第三个脱壳点DexFile::DexFile

1
2
3
4
5
DexFile::DexFile(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file)

通用脱壳点:

通过对比:InMemoryDexClassLoader和DexClassLoader的脱壳点,发现OpenCommon是两者通用脱壳点。

定制源码脱壳

DexFile::OpenCommon

修改/art/runtime/dex_file.ccDexFile::OpenCommon方法

openCommon

1
2
3
4
5
6
7
8
9
10
 int pid=getpid();
char dexfilepath[100]={0};
sprintf(dexfilepath,"/sdcard/%d_%d_OpenCommon.dex",(int)size,pid);

int fd=open(dexfilepath,O_CREAT|O_RDWR,666);
if (fd>0){
int number=write(fd,base,size);
if(number>0){}
close(fd);
}

DexFile::DexFile

修改/art/runtime/dex_file.ccDexFile::DexFile方法

DexFile

1
2
3
4
5
6
7
8
9
10
 int pid=getpid();
char dexfilepath[100]={0};
sprintf(dexfilepath,"/sdcard/%d_%d_DexFile.dex",(int)size,pid);

int fd=open(dexfilepath,O_CREAT|O_RDWR,666);
if (fd>0){
int number=write(fd,base,size);
if(number>0){}
close(fd);
}

编译

1
2
3
4
~/Android8.0/android-8.0.0_r1$ source build/envsetup.sh
~/Android8.0/android-8.0.0_r1$ lunch
23 选择aosp_sailfish-userdebug
~/Android8.0/android-8.0.0_r1$ time make -j4 如果报错直接make单线程编译

生成路径~/Android8.0/android-8.0.0_r1/out/target/product/sailfish,将boot.img,ramdisk.img,system.img,system_other.img,userdata.img,vendor.img拷出来

1
2
3
4
5
6
fastboot flash boot boot.img
fastboot flash vendor vendor.img
fastboot flash system_a system.img
fastboot flash system_b system_other.img
fastboot flash userdata userdata.img
fastboot reboot

安装LoadDex.apk,并把ClassLoaderTest生成的classes.dex放入/sdcard。并在设置中授予该app读写sdcard的权限。在Activity启动的时候加载/sdcard的classes.dex的TestActivity并打印I'm from TestActivity.onCreate,这就是一代壳的基本原理。

重启系统后/sdcard中已经出现一些系统中脱下的dex,启动LoadDex,grep -ril "TestActivity" ./*.dex 找到LoadDex中脱下的dex,DexClassLoader加载的插件dex已经被dump下来,gda可以看到优化后的odex的TestActivity中onCreate方法。

真实案例

a.apk 《百度》加固

启动后进入/sdcard , grep -ril “SplashActivity” ./*.dex

b.apk 《爱加密》企业版本加固

启动后进入/sdcard , grep -ril “mvp/ui/SplashActivity” ./*.dex,内容都为null,说明用了函数抽取,这就是需要fart解决的问题。

以上方案都是针对于没有dex2oat的情况,实际上对于一些壳没有禁用dex2oat的编译过程,且使用dexclassloader进行编译,最终会进入dex2oat流程,这个流程也是可以进行脱壳的。

ExecAndReturnCode中调用execve(program, &args[0], envp);调用dex2oat二进制程序对dex的文件加载。,dex2oat流程也可以脱壳,main函数中调用了Dex2oat,int result = static_cast<int>(art::Dex2oat(argc, argv));,跟进Setup()方法最后会出现对要编译dex文件的处理,如下代码对当前要编译的文件进行遍历,逐个进行注册,这个地方可以完成dex的脱壳。CompileApp也出现了DexFile对象等等非常多的流程出现Dexfile对象,都可以成为脱壳点

dex2oat

除了对dex加载过程中还有其他脱壳点,比如对class进行load过程中,对art method的准备阶段,甚至每个函数的执行过程中都可以进行脱壳,这就是art下众多脱壳点的原因,因为非常多的流程都可以获取到dex文件的位置信息。


  • Dalvik下一代壳通用解决方案
  • ART下一代壳通用解决方案
文章作者: J
文章链接: http://onejane.github.io/2021/03/16/加壳与脱壳之一代壳dex保护/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏