应用侧漏洞与CTF题解:https://wnagzihxa1n.gitbook.io

保护技术开发07 - 纯Native层壳

之前我们把壳从Java层转到了Native层,但是我们依旧是通过调用Java层的loadDex()方法来加载Dex文件

jclass clazz_DexFile = env->FindClass("dalvik/system/DexFile");
if (clazz_DexFile != nullptr) {
    LOGI("---> Find Class dalvik.system.DexFile");
}

jmethodID methodID_loadDex = env->GetStaticMethodID(clazz_DexFile,
                                        "loadDex",
                                        "(Ljava/lang/String;Ljava/lang/String;I)Ldalvik/system/DexFile;");
if (methodID_loadDex != nullptr) {
    LOGI("---> Get methodID_loadDex");
}

auto jobj_dexFile = env->CallStaticObjectMethod(clazz_DexFile,
                                                methodID_loadDex,
                                                jstr_cachefilePath,
                                                jstr_cachefileOpt,
                                                false);

loadDex()函数开始

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

进入,其返回的是一个DexFile对象

static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {

    return new DexFile(sourcePathName, outputPathName, flags);
}

其构造函数,通过openDexFile()函数进行Dex文件的加载,返回一个mCookie,这个会加入一个全局Table,暂且不说

private DexFile(String sourceName, String outputName, int flags) throws IOException {
    if (outputName != null) {
        try {
            String parent = new File(outputName).getParent();
            if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                throw new IllegalArgumentException("Optimized data directory " + parent
                        + " is not owned by the current user. Shared storage cannot protect"
                        + " your application from code injection attacks.");
            }
        } catch (ErrnoException ignored) {
            // assume we'll fail with a more contextual error later
        }
    }

    mCookie = openDexFile(sourceName, outputName, flags);
    mFileName = sourceName;
    guard.open("close");
}

openDexFile()函数调用了一个Native函数openDexFileNative()

private static int openDexFile(String sourceName, String outputName,
    int flags) throws IOException {
    return openDexFileNative(new File(sourceName).getCanonicalPath(),
                             (outputName == null) ? null : new File(outputName).getCanonicalPath(),
                             flags);
}

openDexFileNative()函数定义

native private static int openDexFileNative(String sourceName, String outputName,
    int flags) throws IOException;

该函数在Native层使用动态注册的方式注册,函数注册表相应的Item如下

{ "openDexFileNative",  "(Ljava/lang/String;Ljava/lang/String;I)I",
        Dalvik_dalvik_system_DexFile_openDexFileNative },

终于找到关键的地方了

static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,
    JValue* pResult)
{
    StringObject* sourceNameObj = (StringObject*) args[0];
    StringObject* outputNameObj = (StringObject*) args[1];
    DexOrJar* pDexOrJar = NULL;
    JarFile* pJarFile;
    RawDexFile* pRawDexFile;
    char* sourceName;
    char* outputName;

    if (sourceNameObj == NULL) {
        dvmThrowNullPointerException("sourceName == null");
        RETURN_VOID();
    }

    sourceName = dvmCreateCstrFromString(sourceNameObj);
    if (outputNameObj != NULL)
        outputName = dvmCreateCstrFromString(outputNameObj);
    else
        outputName = NULL;

    if (dvmClassPathContains(gDvm.bootClassPath, sourceName)) {
        ALOGW("Refusing to reopen boot DEX '%s'", sourceName);
        dvmThrowIOException(
            "Re-opening BOOTCLASSPATH DEX files is not allowed");
        free(sourceName);
        free(outputName);
        RETURN_VOID();
    }

    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;
    } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {
        ALOGV("Opening DEX file '%s' (Jar)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = false;
        pDexOrJar->pJarFile = pJarFile;
        pDexOrJar->pDexMemory = NULL;
    } else {
        ALOGV("Unable to open DEX file '%s'", sourceName);
        dvmThrowIOException("unable to open DEX file");
    }

    if (pDexOrJar != NULL) {
        pDexOrJar->fileName = sourceName;
        addToDexFileTable(pDexOrJar);
    } else {
        free(sourceName);
    }

    free(outputName);
    RETURN_PTR(pDexOrJar);
}

我们要摆脱通过调用Java层函数进行Dex文件加载这种方式,必须是手动实现loadDex()Dalvik_dalvik_system_DexFile_openDexFileNative()之间所有调用过程,不一定要完整模拟出来,但是关键的参数什么的要到位,所以要研究libdvm.so,毕竟这些函数都编译进去了,我们使用dlopen()dlsym()来进行Native层函数的调用

由于这个还是要对应手机上的libdvm.so,我这里使用的是模拟器,从模拟器中导出

E:\>adb pull /system/lib/libdvm.so E:\OJ
/system/lib/libdvm.so: 1 file pulled. 1.1 MB/s (2629626 bytes in 2.368s)

记住这个方法是动态注册的,所以使用IDA反编译,找到Dalvik_dalvik_system_DexFile_openDexFileNative()

IMAGE

这个很明显是C++编译的,因为C++里有重载,所以函数前后会加上前缀和后缀,C语言就不存在这种问题了

我们看到Dalvik_dalvik_system_DexFile_openDexFileNative变成了_ZL46Dalvik_dalvik_system_DexFile_openDexFileNativePKjP6JValue

不过这样毕竟不是很靠谱,每个手机都不一样那还了得,所以我们使用一种通用的方法

还记得之前我们说这个函数是使用动态注册吗?

那么它必然有一个函数表,并且这个函数表的名字是不会变的,通过查看源码,确定是dvm_dalvik_system_DexFile

于是使用dlopen()dlsym()进行定位并循环遍历,对比名字及签名,并获取其函数指针

auto libdvm = dlopen("libdvm.so", RTLD_NOW);
auto dvm_dalvik_system_DexFile = (JNINativeMethod *) dlsym(libdvm, "dvm_dalvik_system_DexFile");
void (*fnOpenDexFileNative)(const u4* args, JValue* pResult) = nullptr;
for (auto p = dvm_dalvik_system_DexFile; p->fnPtr != nullptr; p++) {
    if (strcmp(p->name, "openDexFileNative") == 0
        && strcmp(p->signature, "(Ljava/lang/String;Ljava/lang/String;I)I") == 0) {
        fnOpenDexFileNative = (void (*)(const u4 *, JValue *)) p->fnPtr;
        break;
    }
}

if (fnOpenDexFileNative != nullptr) {
    LOGI("Found fnOpenDexFileNative");
}

跑起来看效果

03-07 12:28:02.172 2270-2270/com.wnagzihxa1n.protectapk I/wnagzihxa1n: Found fnOpenDexFileNative

函数指针获取到了,接着来观察参数并尝试构造,第一个参数是u4*的指针,第二个参数是JValue*类型的指针

static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,
    JValue* pResult)

继续看,args一共有两个值,第一个是待加载的Dex文件路径,第二个是优化后的ODex文件路径

StringObject* sourceNameObj = (StringObject*) args[0];
StringObject* outputNameObj = (StringObject*) args[1];

但是我们观察到这两个参数转换成了StringObject*类型,其定义如下

struct StringObject : Object {
    u4 instanceData[1];
    int length() const;
    int utfLength() const;
    ArrayObject* array() const;
    const u2* chars() const;
};

而且幸运的是,我们发现了一个函数,可以帮助我们直接将char*字符串转换成StringObject*

StringObject* dvmCreateStringFromCstr(const char* utf8Str) {
    assert(utf8Str != NULL);
    return dvmCreateStringFromCstrAndLength(utf8Str, dvmUtf8Len(utf8Str));
}

使用IDA找到这个函数的定义为_Z23dvmCreateStringFromCstrPKc

所以继续写代码来调用,其中需要强制转换类型

if (fnOpenDexFileNative != nullptr) {
    LOGI("Found fnOpenDexFileNative");
    auto fndvmCreateStringFromCstr = (void* (*)(const char* utf8Str)) dlsym(libdvm, "_Z23dvmCreateStringFromCstrPKc");
    u4 args[2];
    args[0] = static_cast<u4>(reinterpret_cast<uintptr_t>(fndvmCreateStringFromCstr(cachefilePath.c_str())));
    args[1] = static_cast<u4>(reinterpret_cast<uintptr_t>(fndvmCreateStringFromCstr(cachefileOpt.c_str())));
    JValue result;
    fnOpenDexFileNative(args, &result);
}

这样就可以跑起来了

但是这仅仅是跑起来,我们接着要取出返回值并进行后续的调用,此处如果大家没有使用Dalvik虚拟机源码包的话,可以手动的一个个结构体添加,把不必要的注释去掉之后代码如下

#include <jni.h>
#include "Common.h"
#include "DexFile.h"

#ifndef PROTECTAPK_MANDROID_H
#define PROTECTAPK_MANDROID_H

#endif //PROTECTAPK_MANDROID_H

typedef struct MemMapping {
    void*   addr;           /* start of data */
    size_t  length;         /* length of data */

    void*   baseAddr;       /* page-aligned base address */
    size_t  baseLength;     /* length of mapping */
} MemMapping;

struct DvmDex {
    DexFile*            pDexFile;
    const DexHeader*    pHeader;
    struct StringObject** pResStrings;
    struct ClassObject** pResClasses;
    struct Method**     pResMethods;
    struct Field**      pResFields;
    struct AtomicCache* pInterfaceCache;
    bool                isMappedReadOnly;
    MemMapping          memMap;
    jobject dex_object;
    pthread_mutex_t     modLock;
};

struct RawDexFile {
    char*       cacheFileName;
    DvmDex*     pDvmDex;
};

typedef struct ZipHashEntry {
    const char*     name;
    unsigned short  nameLen;
} ZipHashEntry;

typedef struct ZipArchive {
    int         mFd;
    MemMapping  mMap;
    int         mNumEntries;
    int         mHashTableSize;
    ZipHashEntry* mHashTable;
} ZipArchive;

struct JarFile {
    ZipArchive  archive;
    char*       cacheFileName;
    DvmDex*     pDvmDex;
};

struct DexOrJar {
    char*       fileName;
    bool        isDex;
    bool        okayToFree;
    RawDexFile* pRawDexFile;
    JarFile*    pJarFile;
    u1*         pDexMemory; // malloc()ed memory, if any
};

我们获取返回的pDexOrJar指针

DexOrJar* pDexOrJar = nullptr;
if (fnOpenDexFileNative != nullptr) {
    LOGI("Found fnOpenDexFileNative");
    auto fndvmCreateStringFromCstr = (void* (*)(const char* utf8Str)) dlsym(libdvm, "_Z23dvmCreateStringFromCstrPKc");
    u4 args[2];
    args[0] = static_cast<u4>(reinterpret_cast<uintptr_t>(fndvmCreateStringFromCstr(cachefilePath.c_str())));
    args[1] = static_cast<u4>(reinterpret_cast<uintptr_t>(fndvmCreateStringFromCstr(cachefileOpt.c_str())));
    JValue result;
    fnOpenDexFileNative(args, &result);
    pDexOrJar = (DexOrJar*) result.l;
}

根据之前的分析,有一个地方是需要我们手动调用进行恢复的,就是下面这个mCookie

mCookie = openDexFile(sourceName, outputName, flags);

进行修改,把mCookie替换回去,这个值就是返回的pDexOrJar

jclass clazz_dalvik_system_DexFile = env->FindClass("dalvik/system/DexFile");
jobject jobj_dexFile = env->AllocObject(clazz_dalvik_system_DexFile);

jfieldID fieldID_mCookie = env->GetFieldID(clazz_dalvik_system_DexFile, "mCookie", "I");
env->SetIntField(jobj_dexFile, fieldID_mCookie, static_cast<jint>(reinterpret_cast<uintptr_t>(pDexOrJar)));

拿到mCookie后,再加到dexElements里就行了

跑起来的效果

IMAGE