本文为2015年移动安全挑战赛第三题的writeup。

链接:http://msc.pediy.com/timu.htm
平台:Android
类型:CrackMe

利用JEB反编译APK后发现只有一个继承Application类的StubApplication类,该类加载了动态库。很明显,dex加壳了,那么在SO里肯定回解壳的,不过SO有反调试和混淆,分析起来比较困难(好吧,至少我不想分析了)。
我注意到在lib/armeabi目录中的libmobisecy.so实际上是一个zip文件,解压后得到一个classes.dex文件,拖到JEB里:
1
所有的函数实体都被替换成了throw new RuntimeException();注意到的是除了函数实体,其他地方都是正确的,用010的dex模板解析也没有报错,那么我们只需要将真正的函数实体恢复就可以了,也就是修复codeOff,修复codeOff对应的字节码。(对dex结构不熟悉的童鞋,面壁去吧)

那么问题来了,怎么不通过调试SO就得到codeOff和其对应的字节码呢?O(∩_∩)O比忘了Android是开源的,直接修改源码,从底层那里搞出来!
还有一个问题,什么时候才知道该APK已经解壳了呢?这个就需要了解这种加壳方式的原理了:
戳这里:http://blogs.360.cn/blog/proxydelegate-application/
再戳这里:http://blog.csdn.net/androidsecurity/article/details/8809542
通过上面两篇文章我们可以知道(你不知道我也没办法),最后必定是要通过调用Application.onCreate来启动真正的逻辑的。也就是说当执行Application.onCreate时,codeOff已经修正完毕。好了现在我们开始改源码,在dalvik/vm/interp/Stack.cpp文件的dvmCallMethodV函数(因为这个函数调用了onCreate函数,函数原型:void dvmCallMethodV(Thread self, const Method method, Object obj, bool fromJni, JValue pResult, va_list args)),添加自己的逻辑:

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
if (strcmp(method->name, "onCreate") == 0)
{
ALOGD("dvmCallMethodV: methodName: %s\n", method->name);
const Method* thisMethod = dvmGetCurrentJNIMethod();
assert(thisMethod != NULL);
ClassObject* mainClazz = dvmFindClassNoInit(dvmNameToDescriptor("crackme/a3/Main"), thisMethod->clazz->classLoader);
ALOGD("className: %s", mainClazz->descriptor);
int i;
DvmDex *pDvmDex = mainClazz->pDvmDex;
DexFile *pDexFile = pDvmDex->pDexFile;
for (i = 0; i < (int)pDexFile->pHeader->classDefsSize; i++)
{
const DexClassDef* pClassDef;
const u1* pEncodedData;
DexClassData* pClassData;
pClassDef = dexGetClassDef(pDexFile, i);
pEncodedData = dexGetClassData(pDexFile, pClassDef);
pClassData = dexReadAndVerifyClassData(&pEncodedData, NULL);
if (pClassData == NULL)
{
ALOGE("Trouble reading class data\n");
return;
}
int j;
for (j = 0; j < (int)pClassData->header.directMethodsSize; j++)
{
DexMethod *pDexDirectMethod = &pClassData->directMethods[j];
ALOGD("direct: classIdx: %x, methodIdx: %x, codeOff: %x", pClassDef->classIdx, pDexDirectMethod->methodIdx, pDexDirectMethod->codeOff);
}
for (j = 0; j < (int)pClassData->header.virtualMethodsSize; j++)
{
DexMethod *pDexVirtualMethod = &pClassData->virtualMethods[j];
ALOGD("virtual: classIdx: %x, methodIdx: %x, codeOff: %x", pClassDef->classIdx, pDexVirtualMethod->methodIdx, pDexVirtualMethod->codeOff);
}
}
}

这里面涉及到dalvik源码里的很多结构体,请自行研究。我这里只是打印出了codeOff,接下来dump和修复的工作就不再详述了,关键是思路!(ps:我的办法不是很优雅,应该有更好的思路)

经过上一步骤,得到了修复过好的dex,原以为这样就结束了,结果又遇到一个问题,我们先看jeb反编译出的几个关键代码吧:
将输入作为map的key,得到对应的value:
2
前两个字符的hashCode等于3618且字符ascii值和为168,这里推出前两个字符为s5:
3
然后就是读取两个annotation,与后面几位比较,相同则验证成功:
4
问题就出在最后这个验证,我们之前修复的是codeOff,但是没有修复annotationsOff,怎么办呢?还是改源码,直接让源码帮我们把两个annotation的值打印出来:
在dalvik/vm/reflect/Annotation.cpp的processAnnotationValue函数处添加自己的逻辑:

  • 在函数头部打印出className:
    ALOGD("***processAnnotationValue*** className: %s", clazz->descriptor);
    
  • 在case kDexAnnotationString处打印出值:
    ALOGD("elemObj: %s", dvmCreateCstrFromString((const StringObject *)elemObj));
    

得到后面的字符为:7e1p
最终密码为:… _____ ____. . ..___ .__.