8.0不落地加载&加密加载dex的实现

前言

阅读art源码,思考dex不落地&加密加载的实现,并含有oat过程

android提供的内存加载接口

分析内存加载dex,探索内存加载dex的可行性。

正好,InMemoryDexClassLoader是Android8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。

libcore\dalvik\src\main\java\dalvik\system\InMemoryDexClassLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class InMemoryDexClassLoader extends BaseDexClassLoader {
/**
* Create an in-memory DEX class loader with the given dex buffers.
*
* @param dexBuffers array of buffers containing DEX files between
* <tt>buffer.position()</tt> and <tt>buffer.limit()</tt>.
* @param parent the parent class loader for delegation.
* @hide
*/
public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent) {
super(dexBuffers, parent);
}
/**
* Creates a new in-memory DEX class loader.
*
* @param dexBuffer buffer containing DEX file contents between
* <tt>buffer.position()</tt> and <tt>buffer.limit()</tt>.
* @param parent the parent class loader for delegation.
*/
public InMemoryDexClassLoader(ByteBuffer dexBuffer, ClassLoader parent) {
this(new ByteBuffer[] { dexBuffer }, parent);
}
}

很直观,传入ByteBuffer数组,用父类创建。注意一个ByteBuffer对象代表了一个字节缓冲区,即一个对象代表一个dex文件

BaseDexClassLoader
libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Constructs an instance.
*
* dexFile must be an in-memory representation of a full dexFile.
*
* @param dexFiles the array of in-memory dex files containing classes.
* @param parent the parent class loader
*
* @hide
*/
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}

DexPathList
libcore\dalvik\src\main\java\dalvik\system\DexPathList.java

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
/**
* Construct an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
*
* @param dexFiles the bytebuffers containing the dex files that we should load classes from.
*/
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexFiles == null) {
throw new NullPointerException("dexFiles == null");
}
if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
throw new NullPointerException("dexFiles contains a null Buffer!");
}
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);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//创建文件节点
this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}

和普通的创建方式类似,创建节点数组,代表文件和目录

makeInMemoryDexElements
dalvik\src\main\java\dalvik\system\DexPathList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
List<IOException> suppressedExceptions) {
Element[] elements = new Element[dexFiles.length];
int elementPos = 0;
for (ByteBuffer buf : dexFiles) {
try {
DexFile dex = new DexFile(buf);
elements[elementPos++] = new Element(dex);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + buf, suppressed);
suppressedExceptions.add(suppressed);
}
}
if (elementPos != elements.length) {
elements = Arrays.copyOf(elements, elementPos);
}
return elements;
}

和普通方式相同,创建DexFile对象,表示加载到内存的dex文件,elements表示一个节点。

DexFile
libcore\dalvik\src\main\java\dalvik\system\DexFile.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DexFile(ByteBuffer buf) throws IOException {
mCookie = openInMemoryDexFile(buf);
mInternalCookie = mCookie;
mFileName = null;
}
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());
}
}
private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
private static native Object createCookieWithArray(byte[] buf, int start, int end);

调用native方法生成内存dex对象。参数:

  1. byte[],dex文件的字节数组
  2. int型,表示此缓冲区的位置(该位置可以设置)
  3. int,此缓冲区的限制

仔细一想….这个ByteBuffer不就是曾经研究javaNio的时候看的缓冲区类么……
position limit capacity分别代表读写当前位置、读限制、写限制
也就是说传入的就是byte[]数组,它的当前读取的位置,可读区的大小

接下来进入native层
art\runtime\native\dalvik_system_DexFile.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static jobject DexFile_createCookieWithArray(JNIEnv* env,
jclass,
jbyteArray buffer,
jint start,
jint end) {
//分配memmap内存
std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
if (dex_mem_map == nullptr) {
DCHECK(Thread::Current()->IsExceptionPending());
return 0;
}
auto destination = reinterpret_cast<jbyte*>(dex_mem_map.get()->Begin());
//GetByteArrayRegion的作用是byte数组的值拷贝,直接拷贝到destination中
env->GetByteArrayRegion(buffer, start, end - start, destination);
return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}

分配MemoryMap内存,通过jni拷贝dex进去,之后执行CreateSingleDexFileCookie

CreateSingleDexFileCookie
art\runtime\native\dalvik_system_DexFile.cc

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
constexpr size_t kOatFileIndex = 0;
constexpr size_t kDexFileIndexStart = 1;
static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
std::unique_ptr<const DexFile> dex_file(CreateDexFile(env, std::move(data)));
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));
//可见并没有oat文件被传入......直接执行的dex
return ConvertDexFilesToJavaArray(env, nullptr, dex_files);
}
static jlongArray ConvertDexFilesToJavaArray(JNIEnv* env,
const OatFile* oat_file,
std::vector<std::unique_ptr<const DexFile>>& vec) {
// Add one for the oat file.
//创建一个java层的long数组
jlongArray long_array = env->NewLongArray(static_cast<jsize>(kDexFileIndexStart + vec.size()));
if (env->ExceptionCheck() == JNI_TRUE) {
return nullptr;
}
jboolean is_long_data_copied;
//直接获取该java数组的引用
jlong* long_data = env->GetLongArrayElements(long_array, &is_long_data_copied);
if (env->ExceptionCheck() == JNI_TRUE) {
return nullptr;
}
//第0个位置存oatfile的指针
long_data[kOatFileIndex] = reinterpret_cast<uintptr_t>(oat_file);
for (size_t i = 0; i < vec.size(); ++i) {
//后面每个位置存dex文件的指针
long_data[kDexFileIndexStart + i] = reinterpret_cast<uintptr_t>(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();
}
return long_array;
}

调用了CreateDexFile去创建一个DexFile,之后调用和普通里一样的ConvertDexFilesToJavaArray方法返回jlongArray对象。注意oat_file为null。
可见返回的mCookie其实就是一个oat和多个dex的指针,但内存加载失去了oat

CreateDexFile
art\runtime\native\dalvik_system_DexFile.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map) {
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));
...
return dex_file.release();
}

调用open返回DexFile对象

Open
art\runtime\dex_file.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static constexpr OatDexFile* kNoOatDexFile = nullptr;
std::unique_ptr<const DexFile> DexFile::Open(const std::string& location,
uint32_t location_checksum,
std::unique_ptr<MemMap> map,
bool verify,
bool verify_checksum,
std::string* error_msg) {
...
std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
map->Size(),
location,
location_checksum,
kNoOatDexFile,
verify,
verify_checksum,
error_msg);
if (dex_file != nullptr) {
dex_file->mem_map_.reset(map.release());
}
return dex_file;
}

OpenCommon
art\runtime\dex_file.cc

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
std::unique_ptr<DexFile> 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<DexFile> dex_file(new DexFile(base,
size,
location,
location_checksum,
oat_dex_file));
...
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::kVerifySucceeded;
}
return dex_file;
}

调用DexFile的构造函数

DexFile
art\runtime\dex_file.cc

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
DexFile::DexFile(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file)
: begin_(base),
size_(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),
oat_dex_file_(oat_dex_file) {
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文件格式对其内存地址各项赋值
这里注意oat_dex_file传的是null,而在dexclassloader中,先经过dex2oat,再从磁盘中获取oat文件,之后从内存中的oat文件中获取dexfile。最后返回包装后的oat与dex文件

不落地加载参考

一直想解决怎么优化oat的问题….有个比较好的实现:https://github.com/xiaobaiyey/dexload

看了git上高星的:https://github.com/asLody/TurboDex
hook execv,发现dex2oat直接退出。可见fork的代码段是共享的。在fork前修可以影响子,fork后修改会拷贝副本修改。
调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
因此对dex2oat的过程也可以hook运行库去进行加密。

自己的实现

没有相关文章了,这部分必须自己看art加载dex源码理解透彻才能写。
我注释的art源码:
https://github.com/imbaya2466/art_read

项目地址:
https://github.com/imbaya2466/apksec

按文件偏移hook太不稳定了,应该从符号表、got。这样hook稳定点。
使用dlopen、dlsym来使用系统so的函数。
hook使用我的一次性hook正好,很稳定而且自动解除。
必须加载到内存再解密,因为读写大文件太费时了,不能在每次启动时进行。因此需要dex2oat的部分实现才可加密odex、vdex

杂记:

  1. 签名中的类名要以L开头以;结尾
  2. 保存全局对象在c++中要记得提升为全局,防止虚拟机delete,出错的话看报错的对象标号,debug找标号即可
  3. 模板的签名不用加<>如List< int>就用List就行,使用E的地方为Object。使用javap可以查看
  4. 构造函数的名字是
  5. dlopen返回的句柄是soinfo*
  6. native下log报错信息少时可以hook看栈的信息,存了函数调用路径
  7. xhook的plthook还挺好用的
  8. 用inline hook,hook mmap回调会有问题,猜测原因为mmalloc时使用的mmap有自己的处理

生成加密oat文件

比较容易被dump…但运行速度可以,之后结合dexvmp不太容易。
暂时先不动dex2oat进程。在主进程执行Dex2Oat方法时hook将文件解密,执行完后将dex加密,并加密odex与vdex。
在OatFileBase::OpenOatFile时解密odex与vdex,之后再加密

360的实现就是将加密的dex从内存放到磁盘,再进行dex2oat时解密加密-最好是到内存在解密-dlopen与mmap之后。

vdex是8.0新加的
google描述:

1
2
3
4
5
6
在 Android O 版本中,将会生成以下文件:
.vdex:其中包含 APK 的未压缩 DEX 代码,另外还有一些旨在加快验证速度的元数据。
.odex:其中包含 APK 中已经过 AOT 编译的方法代码。
.art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。
more:https://android-review.googlesource.com/c/platform/art/+/264514
odex与vdex配对,oat将dex写入vdex,其余输出到odex。

白名单:/system/etc/public.libraries.txt表示开放的so

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
libandroid.so
libaaudio.so
libc.so
libcamera2ndk.so
libdl.so
libEGL.so
libGLESv1_CM.so
libGLESv2.so
libGLESv3.so
libicui18n.so
libicuuc.so
libjnigraphics.so
liblog.so
libmediandk.so
libm.so
libnativewindow.so
libneuralnetworks.so
libOpenMAXAL.so
libOpenSLES.so
libRS.so
libstdc++.so
libsync.so
libvulkan.so
libwebviewchromium_plat_support.so
libz.so

注意没有art.so因此加载会失败。绕过so的限制最好就是自己实现dlopen与dlsym。

直接加载加密dex文件

容易被dump,需要dex函数运行时自解密,影响运行效率。如果实现了dexvmp这样直接使用dex效率可能更好点。

内存加载

8.0以上使用InMemoryDexClassLoader,hook解密。内存加载

reateCookieWithArray拷贝时是加密的。
DexFile::Open时解密map中内容。或者DexFile::OpenCommon时也可以,只要保证使用其中数据前解密即可。

落地加密

5.0以上使用hook阻止art生成oat。非内存加载。落地加密
hook阻止oat生成。
在DexFile::Open中获取句柄前解密即可。
DexFile::Open执行结束加密。