dpt-shell 抽取壳实现原理分析(执行逻辑)

发布时间 2023-04-12 18:30:44作者: 明月照江江

开源项目位置(为大佬开源精神点赞)
https://github.com/luoyesiqiu/dpt-shell

抽取壳分为两个步骤
加壳逻辑:
 一 对apk进行解析,将codeItem抽出到一个文件中,并进行nop填充
 二 对抽取后的apk进行加密
 三 注入壳程序相关文件即配置信息
 
执行逻辑:
  一 壳程序执行
  二 壳解密抽取后的dex,并完成classloader的替换
  三 hook住执行方法,在执行对应函数时进行指令填充,使程序正确执行

执行逻辑

执行逻辑在shell module中,毕竟本身就是先运行壳程序

知识点
ActivityThread.handleBindApplication() 按以下顺序加载 APK 并加载应用程序组件:


加载应用程序 AppComponentFactory 子类并创建一个实例。


调用 AppComponentFactory.instantiateClassLoader()。


调用 AppComponentFactory.instantiateApplication() 来加载应用程序 Application 子类并创建一个实例。


对于每个声明的 ContentProvider,按优先级顺序,调用 AppComponentFactory.instantiateProvider() 来加载它的类并创建一个实例,然后调用 ContentProvider.onCreate()。

调用 Application.attachBaseContext()。
调用 Application.onCreate()。

那么首先执行的是ProxyComponentFactory 的instantiateClassLoader

/**
     * This method add in Android 10
     */
    @Override
    public ClassLoader instantiateClassLoader(ClassLoader cl, ApplicationInfo aInfo) {
        Log.d(TAG, "instantiateClassLoader() called with: cl = [" + cl + "], aInfo = [" + aInfo + "]");
        ClassLoader classLoader = init(cl);

        shellClassLoader = cl;

        AppComponentFactory targetAppComponentFactory = getTargetAppComponentFactory(classLoader);

        Global.sIsReplacedClassLoader = true;

        if(targetAppComponentFactory != null) {
            try {
                Method method = AppComponentFactory.class.getDeclaredMethod("instantiateClassLoader", ClassLoader.class, ApplicationInfo.class);
                return (ClassLoader) method.invoke(targetAppComponentFactory, classLoader, aInfo);

            } catch (Exception e) {
            }
        }
        return super.instantiateClassLoader(classLoader, aInfo);
    }
	
	    private ClassLoader init(ClassLoader cl){
        if(!Global.sLoadedDexes){
            Global.sLoadedDexes = true;

            JniBridge.ia(null,cl);
            String apkPath = JniBridge.gap();
            String dexPath = JniBridge.gdp();
            Log.d(TAG, "init dexPath: " + dexPath + ",apkPath: " + apkPath);
            newClassLoader = ShellClassLoader.loadDex(apkPath,dexPath);
            Log.d(TAG,"ProxyComponentFactory init() shell classLoader = " + cl);
            Log.d(TAG,"ProxyComponentFactory init() app classLoader = " + newClassLoader);
            return newClassLoader;
        }
        Log.d(TAG,"ProxyComponentFactory init() tail shell classLoader = " + cl);
        Log.d(TAG,"ProxyComponentFactory init() tail app classLoader = " + newClassLoader);
        return newClassLoader;
    }

在init中完成了, 真是源dex的加载,和对应的classloader的生成,然后代位执行了原dex的instantiateClassLoader

从Android P开始,Android添加了android.app.AppComponentFactory类,它允许开发者覆盖Android的常用组件。

AppComponentFactory支持开发者对Application,Activity,Service,Receiver,Provider,ClassLoader(AndroidQ支持)等组件的替换。

这意味着开发者想替换Application等组件时不用写一堆反射代码了,对加固或者插件开发者带来极大的便利。

dpt在AppComponentFactory类的instantiateClassLoader和instantiateApplication函数中做了替换ClassLoader和Application的操作。

public Application instantiateApplication(ClassLoader cl, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Log.d(TAG, "instantiateApplication() called with: cl = [" + cl + "], className = [" + className + "]");
        ClassLoader appClassLoader = init(cl);

        AppComponentFactory targetAppComponentFactory = null;
        String applicationName = JniBridge.rapn(null);
        if(!Global.sIsReplacedClassLoader){
            JniBridge.mde(cl, appClassLoader);
            Global.sIsReplacedClassLoader = true;
            shellClassLoader = cl;
            targetAppComponentFactory = getTargetAppComponentFactory(cl);
        }
        else{
            targetAppComponentFactory = getTargetAppComponentFactory(appClassLoader);
        }

        ClassLoader apacheHttpLibLoader = ShellClassLoader.loadDex(Global.APACHE_HTTP_LIB);
        JniBridge.mde(cl, apacheHttpLibLoader);
        JniBridge.rde(cl, "base.apk");
        Global.sNeedCalledApplication = false;

....
    }

mde 用于两个classloader 的 dexElement 的合并, rde 用于删除目标element,在这里主要是将源dex的element 放到当前classloader的最前面,并删除base.apk(壳程序的)对应的element

最后调用ProxyApplication 的onCreate (attachBaseContext里没干啥,都在之前干了)

    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "dpt onCreate");

        Log.d(TAG, "onCreate() classLoader = " + getApplicationContext().getClassLoader());

        String realApplicationName = FileUtils.readAppName(getApplicationContext());

        if (Global.sNeedCalledApplication && !TextUtils.isEmpty(realApplicationName)) {
            Log.d(TAG, "onCreate: " + realApplicationName);
            JniBridge.craa(getApplicationContext(), realApplicationName);
            JniBridge.ra(realApplicationName);
            JniBridge.craoc(realApplicationName);
            Global.sNeedCalledApplication = false;
        }
    }

craa 对应callRealApplicationAttach
ra 对应replaceApplication
craoc 对应callRealApplicationOnCreate

oCreate里完成了源dex的Application的替换了生命周期函数的调用,开始运行源dex的程序代码
到这里源dex(抽取过后的dex)就跑起来了,但更关键的是何时填充代码呢

2. 填充逻辑

dpt.h文件中

INIT_ARRAY_SECTION void init_dpt();

也就是说该函数被添加至INIT_ARRAY,会在so加载后立即执行

dpt.cpp

void init_dpt() {
    DLOGI("init_dpt call!");
    dpt_hook();
}

dpt_hook.cpp

void dpt_hook() {
    bytehook_init(BYTEHOOK_MODE_AUTOMATIC,false);
    g_sdkLevel = android_get_device_api_level();
    hook_mmap();
    hook_GetOatDexFile();
    hook_DefineClass();
}

在这里完成了对mmap的hook 和 DefineClass的hook
PS: 这里利用了Dobby和bhook两个hook框架,暂不清楚只用一个行不行(为啥要用两个呢)

mmap

void* fake_mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset){
    BYTEHOOK_STACK_SCOPE();
    int hasRead = (__prot & PROT_READ) == PROT_READ;
    int hasWrite = (__prot & PROT_WRITE) == PROT_WRITE;
    int prot = __prot;

    if(hasRead && !hasWrite) {
        prot = prot | PROT_WRITE;
        DLOGD("fake_mmap call fd = %p,size = %d, prot = %d,flag = %d",__fd,__size, prot,__flags);
    }
    if(g_sdkLevel == 30){
        char link_path[128] = {0};
        snprintf(link_path,sizeof(link_path),"/proc/%d/fd/%d",getpid(),__fd);
        char fd_path[256] = {0};
        readlink(link_path,fd_path,sizeof(fd_path));

        DLOGD("fake_mmap link path = %s",fd_path);

        if(strstr(fd_path,"base.vdex") ){
            DLOGE("fake_mmap want to mmap base.vdex");
            __flags = 0;
        }
    }

    void *addr = BYTEHOOK_CALL_PREV(fake_mmap,__addr,  __size, prot,  __flags,  __fd,  __offset);
    return addr;
}

void hook_mmap(){
    bytehook_stub_t stub = bytehook_hook_single(
            getArtLibName(),
            "libc.so",
            "mmap",
            (void*)fake_mmap,
            nullptr,
            nullptr);
    if(stub != nullptr){
        DLOGD("mmap hook success!");
    }
}

这里时为了加载进来的dex到内存,给这块内存添加写权限,为后面code合并进来做准备

hook_DefineClass

void patchMethod(uint8_t *begin,const char *location,uint32_t dexSize,int dexIndex,uint32_t methodIdx,uint32_t codeOff){
    if(codeOff == 0){
        DLOGI("[*] patchMethod dex: %d methodIndex: %d no need patch!",dexIndex,methodIdx);
        return;
    }
    auto *dexCodeItem = (dex::CodeItem *) (begin + codeOff);

    uint16_t firstDvmCode = *((uint16_t*)dexCodeItem->insns_);
    if(firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){
        NLOG("[*] this method has code no need to patch");
        return;
    }

    auto dexIt = dexMap.find(dexIndex);
    if (LIKELY(dexIt != dexMap.end())) {
        auto dexMemIt = dexMemMap.find(dexIndex);
        //没有放进去过,则放进去
        if(UNLIKELY(dexMemIt == dexMemMap.end())){
            change_dex_protective(begin,dexSize,dexIndex);
        }

        auto codeItemMap = dexIt->second;
        auto codeItemIt = codeItemMap->find(methodIdx);

        if (LIKELY(codeItemIt != codeItemMap->end())) {
            data::CodeItem* codeItem = codeItemIt->second;
            auto *realCodeItemPtr = (uint8_t *)(dexCodeItem->insns_);

#ifdef NOICE_LOG
            char threadName[128] = {0};
            getThreadName(threadName);
            NLOG("[*] patchMethod codeItem patch ,thread = %s, methodIndex = %d,insnsSize = %d >>> %p(0x%lx)",
                 threadName,codeItem->getMethodIdx(), codeItem->getInsnsSize(), realCodeItemPtr,(realCodeItemPtr - begin)
                );

#endif
            memcpy(realCodeItemPtr,codeItem->getInsns(),codeItem->getInsnsSize());
        }
        else{
            DLOGE("[*] patchMethod cannot find  methodId: %d in codeitem map, dex index: %d(%s)",methodIdx,dexIndex,location);
        }
    }
    else{
        DLOGE("[*] patchMethod cannot find dex: %d in dex map",dexIndex);
    }
}

void* DefineClass(void* thiz,void* self,
                 const char* descriptor,
                 size_t hash,
                 void* class_loader,
                 const void* dex_file,
                 const void* dex_class_def) {

    if(LIKELY(g_originDefineClass != nullptr)){

        if(LIKELY(dex_file != nullptr)){
            std::string location;
            uint8_t *begin = nullptr;
            uint64_t dexSize = 0;
            int dexIndex = 0;
            if(g_sdkLevel >= 28){
                auto* dexFileV28 = (V28::DexFile *)dex_file;
                location = dexFileV28->location_;
                begin = (uint8_t *)dexFileV28->begin_;
                dexSize = dexFileV28->size_;
                dexIndex = parse_dex_number(&location);
            }
            else{
                auto* dexFileV23 = (V23::DexFile *)dex_file;
                location = dexFileV23->location_;
                begin = (uint8_t *)dexFileV23->begin_;
                dexSize = dexFileV23->size_;
                dexIndex = parse_dex_number(&location);
            }

            if(location.find(DEXES_ZIP_NAME) != std::string::npos){
                NLOG("DefineClass location: %s", location.c_str());
                if(dex_class_def){
                    auto* class_def = (dex::ClassDef *)dex_class_def;
                    NLOG("[+] DefineClass class_idx_ = 0x%x,class data off = 0x%x",class_def->class_idx_,class_def->class_data_off_);

                    size_t read = 0;
                    auto *class_data = (uint8_t *)((uint8_t *)begin + class_def->class_data_off_);

                    uint64_t static_fields_size = 0;
                    read += DexFileUtils::readUleb128(class_data, &static_fields_size);
                    NLOG("[-] DefineClass static_fields_size = %lu,read = %zu",static_fields_size,read);

                    uint64_t instance_fields_size = 0;
                    read += DexFileUtils::readUleb128(class_data + read, &instance_fields_size);
                    NLOG("[-] DefineClass instance_fields_size = %lu,read = %zu",instance_fields_size,read);

                    uint64_t direct_methods_size = 0;
                    read += DexFileUtils::readUleb128(class_data + read, &direct_methods_size);
                    NLOG("[-] DefineClass direct_methods_size = %lu,read = %zu",direct_methods_size,read);

                    uint64_t virtual_methods_size = 0;
                    read += DexFileUtils::readUleb128(class_data + read, &virtual_methods_size);
                    NLOG("[-] DefineClass virtual_methods_size = %lu,read = %zu",virtual_methods_size,read);

                    dex::ClassDataField staticFields[static_fields_size];
                    read += DexFileUtils::readFields(class_data + read,staticFields,static_fields_size);

                    dex::ClassDataField instanceFields[instance_fields_size];
                    read += DexFileUtils::readFields(class_data + read,instanceFields,instance_fields_size);

                    dex::ClassDataMethod directMethods[direct_methods_size];
                    read += DexFileUtils::readMethods(class_data + read,directMethods,direct_methods_size);

                    dex::ClassDataMethod virtualMethods[virtual_methods_size];
                    read += DexFileUtils::readMethods(class_data + read,virtualMethods,virtual_methods_size);

                    for(int i = 0;i < direct_methods_size;i++){
                        auto method = directMethods[i];
                        NLOG("[-] DefineClass directMethods[%d] methodIndex = %d,code_off = 0x%x",i,method.method_idx_delta_,method.code_off_);
                        patchMethod(begin, location.c_str(), dexSize, dexIndex, method.method_idx_delta_,method.code_off_);
                    }

                    for(int i = 0;i < virtual_methods_size;i++){
                        auto method = virtualMethods[i];
                        NLOG("[-] DefineClass virtualMethods[%d] methodIndex = %d,code_off = 0x%x",i,method.method_idx_delta_,method.code_off_);
                        patchMethod(begin, location.c_str(), dexSize, dexIndex, method.method_idx_delta_,method.code_off_);
                    }

                }
            }
        }
        return g_originDefineClass( thiz,self,descriptor,hash,class_loader, dex_file, dex_class_def);

    }

    return nullptr;
}

void hook_DefineClass(){
    void* defineClassAddress = DobbySymbolResolver(GetClassLinkerDefineClassLibPath(),getClassLinkerDefineClassSymbol());

    DobbyHook(defineClassAddress, (void *) DefineClass,(void**)&g_originDefineClass);
}
这里的逻辑时,通过hook DefineClass, 而DefineClass 是执行流程中的必经一环,当执行到这里时,执行patchMethod, 开始对dex对应的位置执行代码填充,最后执行原始的DefineClass,实现正确运行