最近研究有关odex的相关资料,目的是想在user模式下,修改system.img中framework的odex,因为绕不过签名检测这关总是自动重启。
因为看了很多,也想了很多,这里只是抛砖引玉,说的不好请各位见谅。
分析步骤:
1、寻找dvm初始化代码,弄明白dvm初始化流程。
2、寻找dexopt优化和签名流程。
dalvik虚拟机里面我们主要分析两个目录:“vm”和“dexopt”
他们构建后分别生成:“libdvm.so”和“dexopt”
1、libdvm.so 初始化流程分析
vm/Init.cpp ->dvmStartup
/*
* VM initialization. Pass in any options provided on the command line.
* Do not pass in the class name or the options for the class.
*
* Returns 0 on success.
*/
std::string dvmStartup(int argc, const char* const argv[],
bool ignoreUnrecognized, JNIEnv* pEnv)
{
ScopedShutdown scopedShutdown;
assert(gDvm.initializing);
ALOGV("VM init args (%d):", argc);
for (int i = 0; i < argc; i++) {
ALOGV(" %d: '%s'", i, argv[i]);
}
setCommandLineDefaults();
/*
* Process the option flags (if any).
*/
int cc = processOptions(argc, argv, ignoreUnrecognized);
if (cc != 0) {
if (cc < 0) {
dvmFprintf(stderr, "\n");
usage("dalvikvm");
}
return "syntax error";
}
#if WITH_EXTRA_GC_CHECKS > 1
/* only "portable" interp has the extra goodies */
if (gDvm.executionMode != kExecutionModeInterpPortable) {
ALOGI("Switching to 'portable' interpreter for GC checks");
gDvm.executionMode = kExecutionModeInterpPortable;
}
#endif
/* Configure group scheduling capabilities */
if (!access("/dev/cpuctl/tasks", F_OK)) {
ALOGV("Using kernel group scheduling");
gDvm.kernelGroupScheduling = 1;
} else {
ALOGV("Using kernel scheduler policies");
}
/* configure signal handling */
if (!gDvm.reduceSignals)
blockSignals();
/* verify system page size */
if (sysconf(_SC_PAGESIZE) != SYSTEM_PAGE_SIZE) {
return StringPrintf("expected page size %d, got %d",
SYSTEM_PAGE_SIZE, (int) sysconf(_SC_PAGESIZE));
}
/* mterp setup */
ALOGV("Using executionMode %d", gDvm.executionMode);
dvmCheckAsmConstants();
/*
* Initialize components.
*/
dvmQuasiAtomicsStartup();
if (!dvmAllocTrackerStartup()) {
return "dvmAllocTrackerStartup failed";
}
if (!dvmGcStartup()) {
return "dvmGcStartup failed";
}
if (!dvmThreadStartup()) {
return "dvmThreadStartup failed";
}
if (!dvmInlineNativeStartup()) {
return "dvmInlineNativeStartup";
}
if (!dvmRegisterMapStartup()) {
return "dvmRegisterMapStartup failed";
}
if (!dvmInstanceofStartup()) {
return "dvmInstanceofStartup failed";
}
if (!dvmClassStartup()) {
return "dvmClassStartup failed";
}
/*
* At this point, the system is guaranteed to be sufficiently
* initialized that we can look up classes and class members. This
* call populates the gDvm instance with all the class and member
* references that the VM wants to use directly.
*/
if (!dvmFindRequiredClassesAndMembers()) {
return "dvmFindRequiredClassesAndMembers failed";
}
if (!dvmStringInternStartup()) {
return "dvmStringInternStartup failed";
}
if (!dvmNativeStartup()) {
return "dvmNativeStartup failed";
}
if (!dvmInternalNativeStartup()) {
return "dvmInternalNativeStartup failed";
}
if (!dvmJniStartup()) {
return "dvmJniStartup failed";
}
if (!dvmProfilingStartup()) {
return "dvmProfilingStartup failed";
}
/*
* Create a table of methods for which we will substitute an "inline"
* version for performance.
*/
if (!dvmCreateInlineSubsTable()) {
return "dvmCreateInlineSubsTable failed";
}
/*
* Miscellaneous class library validation.
*/
if (!dvmValidateBoxClasses()) {
return "dvmValidateBoxClasses failed";
}
/*
* Do the last bits of Thread struct initialization we need to allow
* JNI calls to work.
*/
if (!dvmPrepMainForJni(pEnv)) {
return "dvmPrepMainForJni failed";
}
/*
* Explicitly initialize java.lang.Class. This doesn't happen
* automatically because it's allocated specially (it's an instance
* of itself). Must happen before registration of system natives,
* which make some calls that throw assertions if the classes they
* operate on aren't initialized.
*/
if (!dvmInitClass(gDvm.classJavaLangClass)) {
return "couldn't initialized java.lang.Class";
}
/*
* Register the system native methods, which are registered through JNI.
*/
if (!registerSystemNatives(pEnv)) {
return "couldn't register system natives";
}
/*
* Do some "late" initialization for the memory allocator. This may
* allocate storage and initialize classes.
*/
if (!dvmCreateStockExceptions()) {
return "dvmCreateStockExceptions failed";
}
/*
* At this point, the VM is in a pretty good state. Finish prep on
* the main thread (specifically, create a java.lang.Thread object to go
* along with our Thread struct). Note we will probably be executing
* some interpreted class initializer code in here.
*/
if (!dvmPrepMainThread()) {
return "dvmPrepMainThread failed";
}
/*
* Make sure we haven't accumulated any tracked references. The main
* thread should be starting with a clean slate.
*/
if (dvmReferenceTableEntries(&dvmThreadSelf()->internalLocalRefTable) != 0)
{
ALOGW("Warning: tracked references remain post-initialization");
dvmDumpReferenceTable(&dvmThreadSelf()->internalLocalRefTable, "MAIN");
}
/* general debugging setup */
if (!dvmDebuggerStartup()) {
return "dvmDebuggerStartup failed";
}
if (!dvmGcStartupClasses()) {
return "dvmGcStartupClasses failed";
}
/*
* Init for either zygote mode or non-zygote mode. The key difference
* is that we don't start any additional threads in Zygote mode.
*/
if (gDvm.zygote) {
if (!initZygote()) {
return "initZygote failed";
}
} else {
if (!dvmInitAfterZygote()) {
return "dvmInitAfterZygote failed";
}
}
#ifndef NDEBUG
if (!dvmTestHash())
ALOGE("dvmTestHash FAILED");
if (false /*noisy!*/ && !dvmTestIndirectRefTable())
ALOGE("dvmTestIndirectRefTable FAILED");
#endif
if (dvmCheckException(dvmThreadSelf())) {
dvmLogExceptionStackTrace();
return "Exception pending at end of VM initialization";
}
scopedShutdown.disarm();
return "";
}
下一步:
vm/oo/Class.cpp->dvmClassStartup->processClassPath->prepareCpe
/*
* Prepare a ClassPathEntry struct, which at this point only has a valid
* filename. We need to figure out what kind of file it is, and for
* everything other than directories we need to open it up and see
* what's inside.
*/
static bool prepareCpe(ClassPathEntry* cpe, bool isBootstrap)
{
struct stat sb;
if (stat(cpe->fileName, &sb) < 0) {
ALOGD("Unable to stat classpath element '%s'", cpe->fileName);
return false;
}
if (S_ISDIR(sb.st_mode)) {
ALOGE("Directory classpath elements are not supported: %s", cpe->fileName);
return false;
}
char suffix[10];
getFileNameSuffix(cpe->fileName, suffix, sizeof(suffix));
if ((strcmp(suffix, "jar") == 0) || (strcmp(suffix, "zip") == 0) ||
(strcmp(suffix, "apk") == 0)) {
JarFile* pJarFile = NULL;
if (dvmJarFileOpen(cpe->fileName, NULL, &pJarFile, isBootstrap) == 0) {
cpe->kind = kCpeJar;
cpe->ptr = pJarFile;
return true;
}
} else if (strcmp(suffix, "dex") == 0) {
RawDexFile* pRawDexFile = NULL;
if (dvmRawDexFileOpen(cpe->fileName, NULL, &pRawDexFile, isBootstrap) == 0) {
cpe->kind = kCpeDex;
cpe->ptr = pRawDexFile;
return true;
}
} else {
ALOGE("Unknown type suffix '%s'", suffix);
}
ALOGD("Unable to process classpath element '%s'", cpe->fileName);
return false;
}
下一步:
vm/JarFile.cpp -> dvmJarFileOpen
/*
* Open a Jar file. It's okay if it's just a Zip archive without all of
* the Jar trimmings, but we do insist on finding "classes.dex" inside
* or an appropriately-named ".odex" file alongside.
*
* If "isBootstrap" is not set, the optimizer/verifier regards this DEX as
* being part of a different class loader.
*/
int dvmJarFileOpen(const char* fileName, const char* odexOutputName,
JarFile** ppJarFile, bool isBootstrap)
{
/*
* TODO: This function has been duplicated and modified to become
* dvmRawDexFileOpen() in RawDexFile.c. This should be refactored.
*/
ZipArchive archive;
DvmDex* pDvmDex = NULL;
char* cachedName = NULL;
bool archiveOpen = false;
bool locked = false;
int fd = -1;
int result = -1;
/* Even if we're not going to look at the archive, we need to
* open it so we can stuff it into ppJarFile.
*/
if (dexZipOpenArchive(fileName, &archive) != 0)
goto bail;
archiveOpen = true;
/* If we fork/exec into dexopt, don't let it inherit the archive's fd.
*/
dvmSetCloseOnExec(dexZipGetArchiveFd(&archive));
/* First, look for a ".odex" alongside the jar file. It will
* have the same name/path except for the extension.
*/
fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
if (fd >= 0) {
ALOGV("Using alternate file (odex) for %s ...", fileName);
if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) {
ALOGE("%s odex has stale dependencies", fileName);
free(cachedName);
cachedName = NULL;
close(fd);
fd = -1;
goto tryArchive;
} else {
ALOGV("%s odex has good dependencies", fileName);
//TODO: make sure that the .odex actually corresponds
// to the classes.dex inside the archive (if present).
// For typical use there will be no classes.dex.
}
} else {
ZipEntry entry;
tryArchive:
/*
* Pre-created .odex absent or stale. Look inside the jar for a
* "classes.dex".
*/
entry = dexZipFindEntry(&archive, kDexInJarName);
if (entry != NULL) {
bool newFile = false;
/*
* We've found the one we want. See if there's an up-to-date copy
* in the cache.
*
* On return, "fd" will be seeked just past the "opt" header.
*
* If a stale .odex file is present and classes.dex exists in
* the archive, this will *not* return an fd pointing to the
* .odex file; the fd will point into dalvik-cache like any
* other jar.
*/
if (odexOutputName == NULL) {
cachedName = dexOptGenerateCacheFileName(fileName,
kDexInJarName);
if (cachedName == NULL)
goto bail;
} else {
cachedName = strdup(odexOutputName);
}
ALOGV("dvmJarFileOpen: Checking cache for %s (%s)",
fileName, cachedName);
fd = dvmOpenCachedDexFile(fileName, cachedName,
dexGetZipEntryModTime(&archive, entry),
dexGetZipEntryCrc32(&archive, entry),
isBootstrap, &newFile, /*createIfMissing=*/true);
if (fd < 0) {
ALOGI("Unable to open or create cache for %s (%s)",
fileName, cachedName);
goto bail;
}
locked = true;
/*
* If fd points to a new file (because there was no cached version,
* or the cached version was stale), generate the optimized DEX.
* The file descriptor returned is still locked, and is positioned
* just past the optimization header.
*/
if (newFile) {
u8 startWhen, extractWhen, endWhen;
bool result;
off_t dexOffset;
dexOffset = lseek(fd, 0, SEEK_CUR);
result = (dexOffset > 0);
if (result) {
startWhen = dvmGetRelativeTimeUsec();
result = dexZipExtractEntryToFile(&archive, entry, fd) == 0;
extractWhen = dvmGetRelativeTimeUsec();
}
if (result) {
result = dvmOptimizeDexFile(fd, dexOffset,
dexGetZipEntryUncompLen(&archive, entry),
fileName,
dexGetZipEntryModTime(&archive, entry),
dexGetZipEntryCrc32(&archive, entry),
isBootstrap);
}
if (!result) {
ALOGE("Unable to extract+optimize DEX from '%s'",
fileName);
goto bail;
}
endWhen = dvmGetRelativeTimeUsec();
ALOGD("DEX prep '%s': unzip in %dms, rewrite %dms",
fileName,
(int) (extractWhen - startWhen) / 1000,
(int) (endWhen - extractWhen) / 1000);
}
} else {
ALOGI("Zip is good, but no %s inside, and no valid .odex "
"file in the same directory", kDexInJarName);
goto bail;
}
}
/*
* Map the cached version. This immediately rewinds the fd, so it
* doesn't have to be seeked anywhere in particular.
*/
if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) {
ALOGI("Unable to map %s in %s", kDexInJarName, fileName);
goto bail;
}
if (locked) {
/* unlock the fd */
if (!dvmUnlockCachedDexFile(fd)) {
/* uh oh -- this process needs to exit or we'll wedge the system */
ALOGE("Unable to unlock DEX file");
goto bail;
}
locked = false;
}
ALOGV("Successfully opened '%s' in '%s'", kDexInJarName, fileName);
*ppJarFile = (JarFile*) calloc(1, sizeof(JarFile));
(*ppJarFile)->archive = archive;
(*ppJarFile)->cacheFileName = cachedName;
(*ppJarFile)->pDvmDex = pDvmDex;
cachedName = NULL; // don't free it below
result = 0;
bail:
/* clean up, closing the open file */
if (archiveOpen && result != 0)
dexZipCloseArchive(&archive);
free(cachedName);
if (fd >= 0) {
if (locked)
(void) dvmUnlockCachedDexFile(fd);
close(fd);
}
return result;
}
找到关键函数就在 dvmJarFileOpen 这里。。。。
//我们可以看到它会优先判断该jar或者apk是否有odex文件,如果有就用dvmCheckOptHeaderAndDependencies函数检测odex的头
fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
if (fd >= 0) {
ALOGV("Using alternate file (odex) for %s ...", fileName);
if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) {
ALOGE("%s odex has stale dependencies", fileName);
free(cachedName);
cachedName = NULL;
close(fd);
fd = -1;
goto tryArchive;
} else {
ALOGV("%s odex has good dependencies", fileName);
//TODO: make sure that the .odex actually corresponds
// to the classes.dex inside the archive (if present).
// For typical use there will be no classes.dex.
}
} else {
ZipEntry entry;
最后我们来看看dvmCheckOptHeaderAndDependencies干了些什么?
vm/analysis/DexPrepare.cpp -> dvmCheckOptHeaderAndDependencies
附带odex头结构:
struct DexOptHeader {
u1 magic[8]; /* includes version number */
u4 dexOffset; /* file offset of DEX header */
u4 dexLength;
u4 depsOffset; /* offset of optimized DEX dependency table */
u4 depsLength;
u4 optOffset; /* file offset of optimized data tables */
u4 optLength;
u4 flags; /* some info flags */
u4 checksum; /* adler32 checksum covering deps/opt */
/* pad for 64-bit alignment if necessary */
};
bool dvmCheckOptHeaderAndDependencies(int fd, bool sourceAvail, u4 modWhen,
u4 crc, bool expectVerify, bool expectOpt)
{
DexOptHeader optHdr; //opt(odex简称)的头结构
u1* depData = NULL;//opt依赖文件数据(从opt头结构中获取)
const u1* magic;//魔术头
off_t posn;//记录偏移量
int result = false;//最终结果返回
ssize_t actual;
/*
* 将fd指针移动到头部做好读取准备
*/
if (lseek(fd, 0, SEEK_SET) != 0) {
ALOGE("DexOpt: failed to seek to start of file: %s", strerror(errno));
goto bail;
}
/*
* 读取头部数据到结构optHdr
*/
actual = read(fd, &optHdr, sizeof(optHdr));
if (actual < 0) {
ALOGE("DexOpt: failed reading opt header: %s", strerror(errno));
goto bail;
} else if (actual != sizeof(optHdr)) {
ALOGE("DexOpt: failed reading opt header (got %d of %zd)",
(int) actual, sizeof(optHdr));
goto bail;
}
//魔术头判断,保证必须是dey\n036\0的odex魔术头
magic = optHdr.magic;
if (memcmp(magic, DEX_MAGIC, 4) == 0) {
/* somebody probably pointed us at the wrong file */
ALOGD("DexOpt: expected optimized DEX, found unoptimized");
goto bail;
} else if (memcmp(magic, DEX_OPT_MAGIC, 4) != 0) {
/* not a DEX file, or previous attempt was interrupted */
ALOGD("DexOpt: incorrect opt magic number (0x%02x %02x %02x %02x)",
magic[0], magic[1], magic[2], magic[3]);
goto bail;
}
if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) {
ALOGW("DexOpt: stale opt version (0x%02x %02x %02x %02x)",
magic[4], magic[5], magic[6], magic[7]);
goto bail;
}
//确保deps大小在系统规定范围内
if (optHdr.depsLength < kMinDepSize || optHdr.depsLength > kMaxDepSize) {
ALOGW("DexOpt: weird deps length %d, bailing", optHdr.depsLength);
goto bail;
}
/*
* 根据opt头部flags验证,所以odex的flags必须修复,才可能绕过odex这一步。
*/
{
const u4 matchMask = DEX_OPT_FLAG_BIG;
u4 expectedFlags = 0;
#if __BYTE_ORDER != __LITTLE_ENDIAN
expectedFlags |= DEX_OPT_FLAG_BIG;
#endif
if ((expectedFlags & matchMask) != (optHdr.flags & matchMask)) {
ALOGI("DexOpt: header flag mismatch (0x%02x vs 0x%02x, mask=0x%02x)",
expectedFlags, optHdr.flags, matchMask);
goto bail;
}
}
// 指针移动到deps数据段
posn = lseek(fd, optHdr.depsOffset, SEEK_SET);
if (posn < 0) {
ALOGW("DexOpt: seek to deps failed: %s", strerror(errno));
goto bail;
}
/*
* 读取deps数据到depData
*/
depData = (u1*) malloc(optHdr.depsLength);
if (depData == NULL) {
ALOGW("DexOpt: unable to allocate %d bytes for deps",
optHdr.depsLength);
goto bail;
}
actual = read(fd, depData, optHdr.depsLength);
if (actual < 0) {
ALOGW("DexOpt: failed reading deps: %s", strerror(errno));
goto bail;
} else if (actual != (ssize_t) optHdr.depsLength) {
ALOGW("DexOpt: failed reading deps: got %d of %d",
(int) actual, optHdr.depsLength);
goto bail;
}
/*
* 简单的对dep的数据进行匹配
*/
const u1* ptr;
u4 val;
ptr = depData;
val = read4LE(&ptr);
//源文件时间匹配
if (sourceAvail && val != modWhen) {
ALOGI("DexOpt: source file mod time mismatch (%08x vs %08x)",
val, modWhen);
goto bail;
}
val = read4LE(&ptr);
//CRC匹配
if (sourceAvail && val != crc) {
ALOGI("DexOpt: source file CRC mismatch (%08x vs %08x)", val, crc);
goto bail;
}
val = read4LE(&ptr);
//VM构建版本匹配
if (val != DALVIK_VM_BUILD) {
ALOGD("DexOpt: VM build version mismatch (%d vs %d)",
val, DALVIK_VM_BUILD);
goto bail;
}
/*
* 其他依赖文件必须来自bootclasspath的定义,其中还有签名匹配
*/
ClassPathEntry* cpe;
u4 numDeps;
//依赖库数量
numDeps = read4LE(&ptr);
ALOGV("+++ DexOpt: numDeps = %d", numDeps);
for (cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++) {
//获取其中一个cpe(来自bootclasspath)缓存文件
const char* cacheFileName =
dvmPathToAbsolutePortion(getCacheFileName(cpe));
assert(cacheFileName != NULL); /* guaranteed by Class.c */
//获取cpe的签名
const u1* signature = getSignature(cpe);
//获取cpe文件名的字符长度+1
size_t len = strlen(cacheFileName) +1;
u4 storedStrLen;
//deps数量为0也无法通过
if (numDeps == 0) {
/* more entries in bootclasspath than in deps list */
ALOGI("DexOpt: not all deps represented");
goto bail;
}
//读取deps指针的storedStrLen(存储字符长度)
storedStrLen = read4LE(&ptr);
//如果dep的字符长度于cpe必须相等,再者dep和cpe的字符比较也必须相等
if (len != storedStrLen ||
strcmp(cacheFileName, (const char*) ptr) != 0)
{
ALOGI("DexOpt: mismatch dep name: '%s' vs. '%s'",
cacheFileName, ptr);
goto bail;
}
//dep指针位移到当前数据末尾,也就是dep签名开始处
ptr += storedStrLen;
//开始检测cpe的签名与ptr签名是否相等,比较的签名长度kSHA1DigestLen
if (memcmp(signature, ptr, kSHA1DigestLen) != 0) {
ALOGI("DexOpt: mismatch dep signature for '%s'", cacheFileName);
goto bail;
}
//dep指针位移到签名结尾,也就是下一个dep stored(数据段)处。
ptr += kSHA1DigestLen;
ALOGV("DexOpt: dep match on '%s'", cacheFileName);
//dep num递减
numDeps--;
}
//如果numDeps不等于0,相当于deps的处理没完。
if (numDeps != 0) {
/* more entries in deps list than in classpath */
ALOGI("DexOpt: Some deps went away");
goto bail;
}
// 所有的dep数据指针是否都处理完了这里还检测一遍,可以说检测是相当严格的
// 避免了一些简单的IDA修改工作,因为ptr=depData已经是指针操作了,如果指针没处理完,这是绕不过去的。想想办法应该可以绕过。
if (ptr != depData + optHdr.depsLength) {
ALOGW("DexOpt: Spurious dep data? %d vs %d",
(int) (ptr - depData), optHdr.depsLength);
assert(false);
}
result = true;
bail:
free(depData);
return result;
}
2、dexopt 执行流程分析
dexopt是一个将dex优化成odex的工具,分析它能够掌握优化的原理。
OptMain.cpp->extractAndProcessZip
static int extractAndProcessZip(int zipFd, int cacheFd,
const char* debugFileName, bool isBootstrap, const char* bootClassPath,
const char* dexoptFlagStr)
{
ZipArchive zippy;
ZipEntry zipEntry;
size_t uncompLen;
long modWhen, crc32;
off_t dexOffset;
int err;
int result = -1;
int dexoptFlags = 0; /* bit flags, from enum DexoptFlags */
DexClassVerifyMode verifyMode = VERIFY_MODE_ALL;
DexOptimizerMode dexOptMode = OPTIMIZE_MODE_VERIFIED;
memset(&zippy, 0, sizeof(zippy));
/* 确保输出文件cacheFd指针指向文件开始处 */
if (lseek(cacheFd, 0, SEEK_END) != 0) {
ALOGE("DexOptZ: new cache file '%s' is not empty", debugFileName);
goto bail;
}
/*
* 创建一个空文件,并写入odex头结构
*/
err = dexOptCreateEmptyHeader(cacheFd);
if (err != 0)
goto bail;
/* 记录当前文件位置,上一步创建完后,指针指向了末尾,也就是dexOffset指定的位置 */
dexOffset = lseek(cacheFd, 0, SEEK_CUR);
if (dexOffset < 0)
goto bail;
/*
* 打开zip工具,找到dex文件实体
*/
if (dexZipPrepArchive(zipFd, debugFileName, &zippy) != 0) {
ALOGW("DexOptZ: unable to open zip archive '%s'", debugFileName);
goto bail;
}
zipEntry = dexZipFindEntry(&zippy, kClassesDex);
if (zipEntry == NULL) {
ALOGW("DexOptZ: zip archive '%s' does not include %s",
debugFileName, kClassesDex);
goto bail;
}
/*
* 提取zip压缩包的一些基本信息 uncompLen,modWhen,crc32
*/
if (dexZipGetEntryInfo(&zippy, zipEntry, NULL, &uncompLen, NULL, NULL,
&modWhen, &crc32) != 0)
{
ALOGW("DexOptZ: zip archive GetEntryInfo failed on %s", debugFileName);
goto bail;
}
// 从上面分析odex初始化流程里可以发现,modWhen和crc验证是我们需要关注的。
uncompLen = uncompLen;//这个值最终不做验证
modWhen = modWhen;//这个需要验证
crc32 = crc32;//这个需要验证
/*
* 提取zip压缩包内的dex文件实体到cacheFd
*/
if (dexZipExtractEntryToFile(&zippy, zipEntry, cacheFd) != 0) {
ALOGW("DexOptZ: extraction of %s from %s failed",
kClassesDex, debugFileName);
goto bail;
}
/* 优化选项 */
if (dexoptFlagStr[0] != '\0') {
const char* opc;
const char* val;
opc = strstr(dexoptFlagStr, "v="); /* 验证 */
if (opc != NULL) {
switch (*(opc+2)) {
case 'n': verifyMode = VERIFY_MODE_NONE; break;
case 'r': verifyMode = VERIFY_MODE_REMOTE; break;
case 'a': verifyMode = VERIFY_MODE_ALL; break;
default: break;
}
}
opc = strstr(dexoptFlagStr, "o="); /* 优化 */
if (opc != NULL) {
switch (*(opc+2)) {
case 'n': dexOptMode = OPTIMIZE_MODE_NONE; break;
case 'v': dexOptMode = OPTIMIZE_MODE_VERIFIED; break;
case 'a': dexOptMode = OPTIMIZE_MODE_ALL; break;
case 'f': dexOptMode = OPTIMIZE_MODE_FULL; break;
default: break;
}
}
opc = strstr(dexoptFlagStr, "m=y"); /* 寄存器映射 */
if (opc != NULL) {
dexoptFlags |= DEXOPT_GEN_REGISTER_MAPS;
}
opc = strstr(dexoptFlagStr, "u="); /* 单处理器的目标 */
if (opc != NULL) {
switch (*(opc+2)) {
case 'y': dexoptFlags |= DEXOPT_UNIPROCESSOR; break;
case 'n': dexoptFlags |= DEXOPT_SMP; break;
default: break;
}
}
}
/*
* 准备好初始化VM,并执行优化
*/
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode,
dexoptFlags) != 0)
{
ALOGE("DexOptZ: VM init failed");
goto bail;
}
//vmStarted = 1;
/* 执行优化 */
if (!dvmContinueOptimization(cacheFd, dexOffset, uncompLen, debugFileName,
modWhen, crc32, isBootstrap))
{
ALOGE("Optimization failed");
goto bail;
}
/* we don't shut the VM down -- process is about to exit */
result = 0;
bail:
dexZipCloseArchive(&zippy);
return result;
}
上面大概明白了具体的操作流程,下一步我们直接锁定到有关的一些步骤进行分析。
#创建空odex文件,以及写入0xff到头部信息结构中
libdex/OptInvocation.cpp->dexOptCreateEmptyHeader
int dexOptCreateEmptyHeader(int fd)
{
DexOptHeader optHdr;
ssize_t actual;
assert(lseek(fd, 0, SEEK_CUR) == 0);
/*
* 创建一个odex(后面简称opt)的头信息,并全部填充0xFF
*/
assert((sizeof(optHdr) & 0x07) == 0);
memset(&optHdr, 0xff, sizeof(optHdr));
//dex文件偏移量,从opt头信息后开始
optHdr.dexOffset = sizeof(optHdr);
actual = write(fd, &optHdr, sizeof(optHdr));
if (actual != sizeof(optHdr)) {
int err = errno ? errno : -1;
ALOGE("opt header write failed: %s", strerror(errno));
return errno;
}
return 0;
}
初始化vm根据dexopt工具选项值。设备VM不同,选项不同,所以才会出现不同设备间的odex不能通用的原因。
vm/Init.cpp->dvmPrepForDexOpt
...
执行优化,这就是关键函数了
vm/analysis/DexPrepare.cpp -> dvmContinueOptimization
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
DexClassLookup* pClassLookup = NULL;//类查询对象
RegisterMapBuilder* pRegMapBuilder = NULL;//映射表构建对象
assert(gDvm.optimizing);
ALOGV("Continuing optimization (%s, isb=%d)", fileName, isBootstrap);
assert(dexOffset >= 0);
/* 验证头不大小是否符合规格,以及dex偏移量是否正确 */
if (dexLength < (int) sizeof(DexHeader)) {
ALOGE("too small to be DEX");
return false;
}
if (dexOffset < (int) sizeof(DexOptHeader)) {
ALOGE("not enough room for opt header");
return false;
}
bool result = false;
/*
* Drop this into a global so we don't have to pass it around. We could
* also add a field to DexFile, but since it only pertains to DEX
* creation that probably doesn't make sense.
*/
gDvm.optimizingBootstrapClass = isBootstrap;
{
/*
* mmap映射dex到内存,并获取dex在map表的起始地址
*/
bool success;
void* mapAddr;
mapAddr = mmap(NULL, dexOffset + dexLength, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if (mapAddr == MAP_FAILED) {
ALOGE("unable to mmap DEX cache: %s", strerror(errno));
goto bail;
}
//读取gDvm的验证模式
bool doVerify, doOpt;
if (gDvm.classVerifyMode == VERIFY_MODE_NONE) {
doVerify = false;
} else if (gDvm.classVerifyMode == VERIFY_MODE_REMOTE) {
doVerify = !gDvm.optimizingBootstrapClass;
} else /*if (gDvm.classVerifyMode == VERIFY_MODE_ALL)*/ {
doVerify = true;
}
//读取gDvm的优化模式
if (gDvm.dexOptMode == OPTIMIZE_MODE_NONE) {
doOpt = false;
} else if (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED ||
gDvm.dexOptMode == OPTIMIZE_MODE_FULL) {
doOpt = doVerify;
} else /*if (gDvm.dexOptMode == OPTIMIZE_MODE_ALL)*/ {
doOpt = true;
}
/*
* 重写文件. 字节重新排序, 结构重新调整, 类验证,字节码优化。有兴趣的朋友可以看看rewriteDex就在当前类,因为水太深了,篇章有限不想搞太复杂。
* 类优化就跟之前的dexopt优化选项有关,只要我们将jar和apk有关验证的信息节点弄清楚了,就可以放入手机进行优化。
* 记得原数据不管jar,apk,odex都很重要,里面都潜藏着我们需要修复的信息节点。
* 其次,该方法中还能学到如何映射内存,修改映射空间,以及回写到磁盘等操作。
*/
success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength,
doVerify, doOpt, &pClassLookup, NULL);
if (success) {
DvmDex* pDvmDex = NULL;//虚拟的dex对象
u1* dexAddr = ((u1*) mapAddr) + dexOffset;//因为优化过,所以mapAddr地址的dex实体偏移了dexOffset,这里得到的是实体dex偏移后的地址
//打开优化后的dex,将解析的dex创建到DvmDex附加结构上,
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
ALOGE("Unable to create DexFile");
success = false;
} else {
/*
* 如果该配置启用,将所有验证类输出map表。
*/
if (gDvm.generateRegisterMaps) {
pRegMapBuilder = dvmGenerateRegisterMaps(pDvmDex);
if (pRegMapBuilder == NULL) {
ALOGE("Failed generating register maps");
success = false;
}
}
//更新dex校验和到pHeader
DexHeader* pHeader = (DexHeader*)pDvmDex->pHeader;
updateChecksum(dexAddr, dexLength, pHeader);
//释放pDvmDex,完成虚拟dex步骤
dvmDexFileFree(pDvmDex);
}
}
/* 将mmap映射空间的修改写回到磁盘文件(mmap中的参数fd就是磁盘文件) */
if (msync(mapAddr, dexOffset + dexLength, MS_SYNC) != 0) {
ALOGW("msync failed: %s", strerror(errno));
// weird, but keep going
}
#if 1
/*
* 清理映射空间
*/
if (munmap(mapAddr, dexOffset + dexLength) != 0) {
ALOGE("munmap failed: %s", strerror(errno));
goto bail;
}
#endif
if (!success)
goto bail;
}
/* 获取odex包的各种偏移量起点 */
off_t depsOffset, optOffset, endOffset, adjOffset;
int depsLength, optLength;
u4 optChecksum;
depsOffset = lseek(fd, 0, SEEK_END);
if (depsOffset < 0) {
ALOGE("lseek to EOF failed: %s", strerror(errno));
goto bail;
}
adjOffset = (depsOffset + 7) & ~(0x07);
if (adjOffset != depsOffset) {
ALOGV("Adjusting deps start from %d to %d",
(int) depsOffset, (int) adjOffset);
depsOffset = adjOffset;
lseek(fd, depsOffset, SEEK_SET);
}
/*
* 追加依赖文件,这个方法必须好好读一遍,刚好是上一段文里,关于验证odex相关数据段的,正操作。还是把代码附加在下面给大家看吧。
*/
if (writeDependencies(fd, modWhen, crc) != 0) {
ALOGW("Failed writing dependencies");
goto bail;
}
/* 计算deps长度开始64位对齐 */
optOffset = lseek(fd, 0, SEEK_END);
depsLength = optOffset - depsOffset;
adjOffset = (optOffset + 7) & ~(0x07);
if (adjOffset != optOffset) {
ALOGV("Adjusting opt start from %d to %d",
(int) optOffset, (int) adjOffset);
optOffset = adjOffset;
lseek(fd, optOffset, SEEK_SET);
}
/*
* 追加任意的优化数据结构
*/
if (!writeOptData(fd, pClassLookup, pRegMapBuilder)) {
ALOGW("Failed writing opt data");
goto bail;
}
endOffset = lseek(fd, 0, SEEK_END);
optLength = endOffset - optOffset;
/* 计算deps的总校验和,这个节点在上面验证过程中貌似没看到过。 */
if (!computeFileChecksum(fd, depsOffset,
(optOffset+optLength) - depsOffset, &optChecksum))
{
goto bail;
}
/*
* 重新计算了一下odex头的相关信息
*/
DexOptHeader optHdr;
memset(&optHdr, 0xff, sizeof(optHdr));
memcpy(optHdr.magic, DEX_OPT_MAGIC, 4);
memcpy(optHdr.magic+4, DEX_OPT_MAGIC_VERS, 4);
optHdr.dexOffset = (u4) dexOffset;
optHdr.dexLength = (u4) dexLength;
optHdr.depsOffset = (u4) depsOffset;
optHdr.depsLength = (u4) depsLength;
optHdr.optOffset = (u4) optOffset;
optHdr.optLength = (u4) optLength;
#if __BYTE_ORDER != __LITTLE_ENDIAN
optHdr.flags = DEX_OPT_FLAG_BIG;
#else
optHdr.flags = 0;
#endif
optHdr.checksum = optChecksum;
fsync(fd); /* 同步内存中已修改的文件到存储设备,这里的fd只是磁盘文件的内存指针,上面有过一次回写到磁盘文件,但是并未关闭文件指针 */
lseek(fd, 0, SEEK_SET);
// 调整指针,将头部信息写入文件
if (sysWriteFully(fd, &optHdr, sizeof(optHdr), "DexOpt opt header") != 0)
goto bail;
ALOGV("Successfully wrote DEX header");
result = true;
//dvmRegisterMapDumpStats();
bail:
dvmFreeRegisterMapBuilder(pRegMapBuilder);
free(pClassLookup);
return result;
}
追加依赖文件函数:writeDependencies 此代码必看,看过后就知道每个dep项的数据结构是如何的。解析deps和修复它就不是问题了。
static int writeDependencies(int fd, u4 modWhen, u4 crc)
{
u1* buf = NULL;
int result = -1;
ssize_t bufLen;
ClassPathEntry* cpe;
int numDeps;
/*
* 计算bootclasspath依赖文件的条目数 numDeps
*/
numDeps = 0;
bufLen = 0;
for (cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++) {
const char* cacheFileName =
dvmPathToAbsolutePortion(getCacheFileName(cpe));
assert(cacheFileName != NULL); /* guaranteed by Class.c */
ALOGV("+++ DexOpt: found dep '%s'", cacheFileName);
numDeps++;
bufLen += strlen(cacheFileName) +1;
}
//缓冲大小计算
bufLen += 4*4 + numDeps * (4+kSHA1DigestLen);
//申请对应的缓冲区
buf = (u1*)malloc(bufLen);
//字节高低位转换
set4LE(buf+0, modWhen);
set4LE(buf+4, crc);
set4LE(buf+8, DALVIK_VM_BUILD);
set4LE(buf+12, numDeps);
// 循环处理bootclasspath依赖文件
u1* ptr = buf + 4*4;
for (cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++) {
//获取依赖文件
const char* cacheFileName =
dvmPathToAbsolutePortion(getCacheFileName(cpe));
assert(cacheFileName != NULL); /* guaranteed by Class.c */
// 取得文件签名
const u1* signature = getSignature(cpe);
// 计算文件大小
int len = strlen(cacheFileName) +1;
if (ptr + 4 + len + kSHA1DigestLen > buf + bufLen) {
ALOGE("DexOpt: overran buffer");
dvmAbort();
}
// 追加文件名称和签名到odex deps列表
set4LE(ptr, len);
ptr += 4;
memcpy(ptr, cacheFileName, len);
ptr += len;
memcpy(ptr, signature, kSHA1DigestLen);
ptr += kSHA1DigestLen;
}
assert(ptr == buf + bufLen);
result = sysWriteFully(fd, buf, bufLen, "DexOpt dep info");
free(buf);
return result;
}
终于写完了。希望能对大家有所帮助!!
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界