脱壳基础 dvmDexFileOpenPartial

前置知识

http://www.wjdiankong.cn/android中的apk的加固加壳原理解析和实现/
http://blog.csdn.net/jiangwei0910410003/article/details/48104581

赛题链接

https://github.com/eternalsakura/ctf_pwn/blob/master/android逆向/jscrack.apk
可能是因为赛题是14年的,有点老了,所以在5.0.1上运行失败,用android 4.4.4就可以运行了。

原理

加固方式

现在市场中加固apk的方式一般就是两种:一种是对源apk整体做一个加固,放到指定位置,运行的时候在解密动态加载,还有一种是对so进行加固,在so加载内存的时候进行解密释放。我们今天主要看第一种加固方式,就是对apk整体进行加固。
当我们发现apk中主要的类都没有了,肯定是apk被加固了,加固的源程序肯定是在本地,一般会有这么几个地方需要注意的:
1、应用程序的asset目录,我们知道这个目录是不参与apk的资源编译过程的,所以很多加固的应用喜欢把加密之后的源apk放到这里
2、把源apk加密放到壳的dex文件的尾部,这个肯定不是我们这里的案例,但是也有这样的加固方式,这种加固方式会发现使用dex2jar工具解析dex是失败的,我们这时候就知道了,肯定对dex做了手脚
3、把源apk加密放到so文件中,这个就比较难了,一般都是把源apk进行拆分,存到so文件中,分析难度会加大的。

破解思路

不管上层怎么加固,最终加载到内存的dex肯定不是加固的,所以这个dex就是我们想要的.

为什么在dvmDexFileOpenPartial下断点就能dump出完全加载到内存的dex?

dalvik虚拟机会把dex文件优化为odex文件,而优化的源代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* Main entry point. Decide where to go.
*/
int main(int argc, char* const argv[])
{
...
if (argc > 1) {
if (strcmp(argv[1], "--zip") == 0)
return fromZip(argc, argv);
else if (strcmp(argv[1], "--dex") == 0)
return fromDex(argc, argv);
else if (strcmp(argv[1], "--preopt") == 0)
return preopt(argc, argv);
}
...
}

其中fromzip和preopt都会调用processZipFile先将dex文件提取出来,fromDex则直接调用dvmContinueOptimization优化.

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Common functionality for normal device-side processing as well as
* preoptimization.
*/
static int processZipFile(int zipFd, int cacheFd, const char* zipName,
const char *dexoptFlags)
{
...
int result = extractAndProcessZip(zipFd, cacheFd, zipName, isBootstrap,
bcp, dexoptFlags);
free(bcpCopy);
return result;
}

在extractAndProcessZip中处理zip文件并将dex提取出来,随后调用优化函数

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
55
56
57
58
59
60
61
/*
* Extract "classes.dex" from zipFd into "cacheFd", leaving a little space
* up front for the DEX optimization header.
*/
static int extractAndProcessZip(int zipFd, int cacheFd,
const char* debugFileName, bool isBootstrap, const char* bootClassPath,
const char* dexoptFlagStr)
{
...
/*
* Open the zip archive, find the DEX entry.
*/
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;
}
/*
* Extract some info about the zip entry.
*/
if (dexZipGetEntryInfo(&zippy, zipEntry, NULL, &uncompLen, NULL, NULL,
&modWhen, &crc32) != 0)
{
ALOGW("DexOptZ: zip archive GetEntryInfo failed on %s", debugFileName);
goto bail;
}
uncompLen = uncompLen;
modWhen = modWhen;
crc32 = crc32;
/*
* Extract the DEX data into the cache file at the current offset.
*/
if (dexZipExtractEntryToFile(&zippy, zipEntry, cacheFd) != 0) {
ALOGW("DexOptZ: extraction of %s from %s failed",
kClassesDex, debugFileName);
goto bail;
}
...
/*
* Prep the VM and perform the optimization.
*/
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode,
dexoptFlags) != 0)
{
ALOGE("DexOptZ: VM init failed");
goto bail;
}
//vmStarted = 1;
/* do the optimization */
if (!dvmContinueOptimization(cacheFd, dexOffset, uncompLen, debugFileName,
modWhen, crc32, isBootstrap))
{
ALOGE("Optimization failed");
goto bail;
}
}

dvmContinueOptimization函数位于/dalvik/vm/analysis/DexPrepare.cpp文件,其中调用了dvmDexFileOpenPartial.

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
/*
* Do the actual optimization. This is executed in the dexopt process.
*
* For best use of disk/memory, we want to extract once and perform
* optimizations in place. If the file has to expand or contract
* to match local structure padding/alignment expectations, we want
* to do the rewrite as part of the extract, rather than extracting
* into a temp file and slurping it back out. (The structure alignment
* is currently correct for all platforms, and this isn't expected to
* change, so we should be okay with having it already extracted.)
*
* Returns "true" on success.
*/
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
...
success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength,
doVerify, doOpt, &pClassLookup, NULL);
if (success) {
DvmDex* pDvmDex = NULL;
u1* dexAddr = ((u1*) mapAddr) + dexOffset;
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
ALOGE("Unable to create DexFile");
success = false;
} else {
...
}
}
...
}

dvmDexFileOpenPartial调用了dexFileParse,用来解析内存中优化过或未优化过的dex文件,返回dexFile结构.
所以此时dex文件已经被加载进内存,就可以dump出来了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* Create a DexFile structure for a "partial" DEX. This is one that is in
* the process of being optimized. The optimization header isn't finished
* and we won't have any of the auxillary data tables, so we have to do
* the initialization slightly differently.
*
* Returns nonzero on error.
*/
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)
{
DvmDex* pDvmDex;
DexFile* pDexFile;
int parseFlags = kDexParseDefault;
int result = -1;
/* -- file is incomplete, new checksum has not yet been calculated
if (gDvm.verifyDexChecksum)
parseFlags |= kDexParseVerifyChecksum;
*/
pDexFile = dexFileParse((u1*)addr, len, parseFlags);
if (pDexFile == NULL) {
ALOGE("DEX parse failed");
goto bail;
}
}

赛题分析

jeb打开apk,查看Manifest

反编译查看源代码

存在的问题

asset目录中的jar文件被处理了,打不开,也不知道处理逻辑
libs目录中的三个so文件,唯一加载了libmobisec.so文件了
所以说被加固的源程序可能存在于so文件,也可能存在于asset文件里。

脱壳

得到libdvm.so文件

adb pull /system/lib/libdvm.so

在dvmDexFileOpenPartial下断点

IDA attach并断下

adb shell am start -D -n com.ali.tg.testapp/.MainActivity

dvmDexFileOpenPartial函数的第一个参数就是dex内存起始地址,第二个参数就是dex大小。
R0:0x753EF008
R1:0x000941FC

dump脚本

1
2
3
4
5
6
7
import idaapi
start_addr=0x753EF008
fike_len=0x000941FC
data=idaapi.dbg_read_memory(start_addr,fike_len)
f=open(r'dump.dex','wb')
f.write(data)
f.close()

反编译dex为smali,进行分析