Fart脱壳-源码分析

Posted 梦过无声

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fart脱壳-源码分析相关的知识,希望对你有一定的参考价值。

源码分析

首先看java代码,在APP启动时候反射了DexFile.java中三个方法,分别是

  1. getClassNameList

  2. defineClassNative

  3. dumpMethodCode

其中前面两个方法是android自带的,dumpMethodCode方法是fart自己添加的

// frameworks/base/core/java/android/app/ActivityThread.java
public static void fart() 
        ...
        Class DexFileClazz = null;
        try 
            DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
         catch (Exception e) 
            e.printStackTrace();
        
        Method getClassNameList_method = null;
        Method defineClass_method = null;
        Method dumpDexFile_method = null;
        Method dumpMethodCode_method = null;

        for (Method field : DexFileClazz.getDeclaredMethods()) 
            if (field.getName().equals("getClassNameList")) 
                getClassNameList_method = field;
                getClassNameList_method.setAccessible(true);
            
            if (field.getName().equals("defineClassNative")) 
                defineClass_method = field;
                defineClass_method.setAccessible(true);
            
            if (field.getName().equals("dumpMethodCode")) 
                dumpMethodCode_method = field;
                dumpMethodCode_method.setAccessible(true);
            
        

重点: ArtMethod::FromReflectedMethod 获取 ArtMethod 指针

static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method) 
ScopedFastNativeObjectAccess soa(env);
  if(method!=nullptr)
  
      ArtMethod* artmethod = ArtMethod::FromReflectedMethod(soa, method);
      myfartInvoke(artmethod);
  

  return;




    extern "C" void myfartInvoke(ArtMethod * artmethod)
     SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) 
        JValue *result = nullptr;
        Thread *self = nullptr;
        uint32_t temp = 6;
        uint32_t *args = &temp;
        uint32_t args_size = 6;
        artmethod->Invoke(self, args, args_size, result, "fart");
    

注意第一个参数self,这里为null,会进入到dumpArtMethod中,也是fart添加的

    void ArtMethod::Invoke(Thread * self, uint32_t * args,
                   uint32_t args_size, JValue * result,
                   const char *shorty) 
 

        if (self == nullptr) 
            dumpArtMethod(this);
            return;
        
        ....
     

重点
artmethod->GetDexFile() 获取 DexFile
dex_file->Begin() 对应dex在内存中的起点
dex_file->size() 对应dex文件大小

DexFile::CodeItem 是什么目前还不清楚

    extern "C" void dumpArtMethod(ArtMethod * artmethod)
     SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) 
        char *dexfilepath = (char *) malloc(sizeof(char) * 2000);
        if (dexfilepath == nullptr) 
            LOG(INFO) <<
                "ArtMethod::dumpArtMethodinvoked,methodname:"
                << PrettyMethod(artmethod).
                c_str() << "malloc 2000 byte failed";
            return;
        
        int fcmdline = -1;
        char szCmdline[64] =  0 ;
        char szProcName[256] =  0 ;
        int procid = getpid();
        sprintf(szCmdline, "/proc/%d/cmdline", procid);
        fcmdline = open(szCmdline, O_RDONLY, 0644);
        if (fcmdline > 0) 
            read(fcmdline, szProcName, 256);
            close(fcmdline);
        

        if (szProcName[0]) 

            const DexFile *dex_file = artmethod->GetDexFile();
            const char *methodname = PrettyMethod(artmethod).c_str();
            const uint8_t *begin_ = dex_file->Begin();
            size_t size_ = dex_file->Size();

            memset(dexfilepath, 0, 2000);
            int size_int_ = (int) size_;

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath, "%s", "/sdcard/fart");
            mkdir(dexfilepath, 0777);

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath, "/sdcard/fart/%s", szProcName);
            mkdir(dexfilepath, 0777);

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath, "/sdcard/fart/%s/%d_dexfile.dex", szProcName, size_int_);
            int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
            if (dexfilefp > 0) 
                close(dexfilefp);
                dexfilefp = 0;
             else 
                dexfilefp = open(dexfilepath, O_CREAT | O_RDWR, 0666);
                if (dexfilefp > 0) 
                    write(dexfilefp, (void *) begin_, size_);
                    fsync(dexfilefp);
                    close(dexfilefp);
                
            
            const DexFile::CodeItem * code_item = artmethod->GetCodeItem();
            if (LIKELY(code_item != nullptr)) 
                int code_item_len = 0;
                uint8_t *item = (uint8_t *) code_item;
                if (code_item->tries_size_ > 0) 
                    const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item, code_item->tries_size_));
                    uint8_t *tail = codeitem_end(&handler_data);
                    code_item_len = (int) (tail - item);
                 else 
                    code_item_len = 16 + code_item->insns_size_in_code_units_ * 2;
                
                memset(dexfilepath, 0, 2000);
                int size_int = (int) dex_file->Size();  // Length of data
                uint32_t method_idx = artmethod->get_method_idx();
                sprintf(dexfilepath, "/sdcard/fart/%s/%d_%ld.bin", szProcName, size_int, gettidv1());
                int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
                if (fp2 > 0) 
                    lseek(fp2, 0, SEEK_END);
                    memset(dexfilepath, 0, 2000);
                    int offset = (int) (item - begin_);
                    sprintf(dexfilepath, "name:%s, method_idx:%d, offset:%d, code_item_len:%d, ins:",
                        methodname, method_idx, offset, code_item_len);
                    int contentlength = 0;
                    while (dexfilepath[contentlength] != 0)
                        contentlength++;
                    write(fp2, (void *) dexfilepath, contentlength);
                    long outlen = 0;
                    char *base64result = base64_encode((char *) item, (long)code_item_len, &outlen);
                    write(fp2, base64result, outlen);
                    write(fp2, ";", 2);
                    fsync(fp2);
                    close(fp2);
                    if (base64result != nullptr) 
                        free(base64result);
                        base64result = nullptr;
                    
                
            
        

        if (dexfilepath != nullptr) 
            free(dexfilepath);
            dexfilepath = nullptr;
        

    

dumpDexFileByExecute 是在Execute中被调用, 也就是是执行指令的时候

// art/runtime/interpreter/interpreter.cc
static inline JValue Execute(Thread* self, const DexFile::CodeItem* code_item,
                             ShadowFrame& shadow_frame, JValue result_register) 

  if(strstr(PrettyMethod(shadow_frame.GetMethod()).c_str(),"<clinit>") != nullptr)
  
    dumpDexFileByExecute(shadow_frame.GetMethod());
  
  ...

    extern "C" void dumpDexFileByExecute(ArtMethod * artmethod)
     SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) 
        char *dexfilepath = (char *) malloc(sizeof(char) * 2000);
        if (dexfilepath == nullptr) 
            LOG(INFO) <<
                "ArtMethod::dumpDexFileByExecute,methodname:"
                << PrettyMethod(artmethod).
                c_str() << "malloc 2000 byte failed";
            return;
        
        int fcmdline = -1;
        char szCmdline[64] =  0 ;
        char szProcName[256] =  0 ;
        int procid = getpid();
        sprintf(szCmdline, "/proc/%d/cmdline", procid);
        fcmdline = open(szCmdline, O_RDONLY, 0644);
        if (fcmdline > 0) 
            read(fcmdline, szProcName, 256);
            close(fcmdline);
        

        if (szProcName[0]) 

            const DexFile *dex_file = artmethod->GetDexFile();
            const uint8_t *begin_ = dex_file->Begin();  // Start of data.
            size_t size_ = dex_file->Size();    // Length of data.

            memset(dexfilepath, 0, 2000);
            int size_int_ = (int) size_;

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath, "%s", "/sdcard/fart");
            mkdir(dexfilepath, 0777);

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath, "/sdcard/fart/%s",
                szProcName);
            mkdir(dexfilepath, 0777);

            memset(dexfilepath, 0, 2000);
            sprintf(dexfilepath,
                "/sdcard/fart/%s/%d_dexfile_execute.dex",
                szProcName, size_int_);
            int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
            if (dexfilefp > 0) 
                close(dexfilefp);
                dexfilefp = 0;
                
             else 
                dexfilefp =
                    open(dexfilepath, O_CREAT | O_RDWR,
                     0666);
                if (dexfilefp > 0) 
                    write(dexfilefp, (void *) begin_,
                          size_);
                    fsync(dexfilefp);
                    close(dexfilefp);
                
            
        
        
        if (dexfilepath != nullptr) 
            free(dexfilepath);
            dexfilepath = nullptr;
        
    

测试实际效果

某平台加固后


fart之后

总结

Hook点有2个:

  1. 是app启动时候,在ActivityThread中利用反射,拿到ArtMethod,再通过ArtMethod->GetDexFile()拿到DexFile
  2. 在安卓虚拟机执行指令时候Execute中进行dump出DexFile,相较于早期的DexFile::OpenCommon(const uint8_t* base, size_t size, ...)中进行脱壳更加底层

源码分析 脱壳神器ZjDroid工作原理

0. 神器ZjDroid

Xposed框架的另外一个功能就是实现应用的简单脱壳,其实说是Xposed的作用其实也不是,主要是模块编写的好就可以了,主要是利用Xposed的牛逼Hook技术实现的,下面就先来介绍一下这个脱壳模块工具ZjDroid的原理,因为他是开源的,所以咋们直接分析源码即可,源码的下载地址:https://github.com/halfkiss/ZjDroid 不过可惜的时候他只公开了Java层的代码,而native层的代码并没有公开,但是分析源码之后会发现最重要的功能就在native层,不过也没关系,等分析到那里的时候我在给大家讲解底层的大致实现方案即可。


1. 源码分析ZjDroid原理(

下面就来详细的分析一下ZjDroid工具的源码吧,他是一个Eclipse工程导入很简单,基于之前的Xposed模块编写的经验,我们知道找到入口代码也很简单,在assets目录下有一个xposed_init文件中就记录了模块的入口类:

然后我们直接进入到这个类查看即可:

看到了,遵循统一规则,实现了IXposedHookLoadPackage接口,实现handleLoadPackage回调方法即可,下面继续分析入口方法ModuleContext的initModuleContext

 

发现这开始拦截Application的onCreate方法了,而这个方法一般是每个应用程序的启动方法,在这里做拦截操作也是合情合理的,在看看拦截之后做了什么,也就是ApplicationOnCreateHook类的实现:

 在这里开始了真正的拦截操作了,主要是添加了一个广播,也就是说设备中每个应用在启动的时候都回去注册这个广播,而如果后续发送一个这样对应Action的广播的话,每个应用程序都会收到。所以这里可以看到,核心工作就在这个广播的接受之后做了,接下来继续去看这个广播的定义:

 果然在这里,可以看到了首先会通过发送广播的intent中携带一些数据过来,主要是两个数据:

一个是进程id:

这个作用主要是为了过滤其他应用,只处理本应用的逻辑,因为这个广播发送之后所有的应用都能接收到,但是我们脱壳有时候肯定只是针对于某一个应用,那么只需要在这个应用的广播接收中做处理即可。

一个是命令字符串:这个是为了发送广播可以支持多种功能,后面分析也可以看到的确有很多功能的。


然后这里得到命令之后就开始构造一个命令执行器类,这里用到了设计模式中的命令模式。下面继续看看有哪几种命令执行器类:

 

在这个方法中就开始分析了这里支持的哪几种命令类,下面来一一分析一下:

第一个命令:dump_dexinfo

获取应用运行时内存中dex的信息:DumpDexInfoCommandHandler

 

进入方法在详细查看一下:

 

看到了,这里的实现逻辑还是比较简单的,全部通过反射机制获取每个应用的dex文件对应的DexFile类型对象,这里的工作和我们之前介绍了Android中插件化开发已经很熟悉了,通过应用的默认类加载PathClassLoader类得到DexPathList类,然后在得到具体的DexFile对象即可。这里要说的就是这个dex文件对应的cookie值,这个值非常重要,是后续命令操作的基本信息,他代表的含义就是底层中每个应用的dex文件对应的唯一id值,系统会维护一个map结构来保存这些数据的,系统然后通过这个cookie值来找到对应的dex文件信息的。

命令用法:am broadcast -a com.zjdroid.invoke –ei target [pid] –es cmd \'{"action":"dump_dexinfo"}\'

这里使用的是命令方式发送一个广播,通过–ei携带目标进程id是一个int类型,通过–es携带命令字符串



第二个命令:dump_dexfile

这个命令也是后续脱壳的重要命令,就是dump出应用内存中的dex文件:DumpDexFileCommandHandler

这里可以看到dump出应用的内存数据,首先得需要传入源应用的dex数据也就是apk文件,这个一般都是存放在/data/app/xxx.apk目录下的,然后就是这里自己构建了一个dump之后的dex文件路径,通过源码查看是在/data/data/xxx/files/dexdump.odex中。接下来继续查看dump的核心代码:

 看到这里有一个核心的方法,但是可惜的是这个方法是native的,而这个工具并没有把native层的代码公开,但是通过这里传递的参数可以了解到
 

命令用法:am broadcast -a com.zjdroid.invoke –ei target [pid] –es cmd \'{"action":"dump_dexfile","dexpath":"*****"}\'

注意这里的dexpath参数是代表需要脱壳的dex文件,也就是应用程序文件。


第三个命令:backsmali

这个命令其实是和上面的命令差不多功能,只是这里的命令多了一层操作就是把dex文件转化成smali文件,所以这里不再详细说明了,咋们可以先得到dex文件,然后在通过工具得到smali文件也是可以的。

命令用法:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”backsmali”,”dexpath”:”*****”}’

注意这里的dexpath参数是代表需要脱壳的dex文件,也就是应用程序文件。而最终生成的smali文件夹是放在/data/data/xxx/smali下面的。


第四个命令:dump_mem

这个命令是用来dump出应用程序运行时内存中指定开始位置和长度的内存块数据的:DumpMemCommandHandler

可惜这个方法也是native层的,但是这个操作就比较简单了,我们知道每个应用运行时的内存地址都在 /proc/[pid]/maps 文件中:

那么查找内存地址,然后在使用memcpy进行内存数据拷贝也是非常简单的。

命令用法:am broadcast -a com.zjdroid.invoke –ei target [pid] –es cmd ‘{“action”:”dump_mem”,”start”:111,”length”:23}’

注意这里的start和length都是十进制的,而不是十六进制的数据格式。


第五个命令:dump_heap

这个命令是可以dump出虚拟机的堆内存信息的,文件可以使用java heap工具进行分析,而对于这个命令我们想一下应该也知道实现逻辑应该是也是在native层的,而且这个代码逻辑应该和上面的那个命令差不多的,但是对于这个命令我还没有想到具体的思路,悲哀呀,如果有了解的同学就告知一下哈!

命令用法:am broadcast -a com.zjdroid.invoke –ei target [pid] –es cmd ‘{“action”:”dump_heap”}’

第六个命令:dump_class

这个命令主要是用于dump出dex文件中的类信息,这个操作也是非常简单的,因为在DexFile对象中有一个隐藏的方法可以把dex文件中的所有类名获取到:getClassNameList

这里可以看到这个方法的传入参数为一个dex文件对应的cookie值。

命令用法:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_class”,”dexpath”:”*****”}’

这里的dexpath是需要得到所有类信息的dex文件路径,也就是应用的apk文件路径。


第七个命令:invoke

这个命令是用于运行时动态调用Lua脚本,本人并没有看懂这个命令的作用,该功能可以通过Lua脚本动态调用java代码。使用场景:可以动态调用解密函数,完成解密。可以动态触发特定逻辑。代码就不进行分析了,因为我觉得这个命令应该不怎么会使用

命令用法:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”invoke”,”filepath”:”****”}’

这里的filepath是lua脚本文件的存放路径。


到这里就全部介绍完了ZjDroid的所有命令了,下面还有两个非常重要的打印日志的tag:

第一个:adb logcat -s zjdroid-shell-{package name}

这个tag可以查看上面每个命令执行的结果,便于查看命令执行的状态。

第二个:adb logcat -s zjdroid-apimonitor-{package name}

这个tag可以监听对应包名应用调用的哪些api信息,这个作用有点类似于运行时权限请求的作用。这个做起来就非常简单了,可以直接通过Xposed提供的方法进行系统的一些敏感api进行拦截然后添加监控代码即可。


三、命令总结

上面就从源码的角度完全分析完了ZjDroid工具的功能了,下面就来总结一下:

1、获取APK当前加载DEX文件信息
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dexinfo”}’

2、获取指定DEX文件包含可加载类名
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_class”,”dexpath”:”*****”}’

3、根据Dalvik相关内存指针动态反编译指定DEX,并以文件形式保存
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”backsmali”,”dexpath”:”*****”}’

4、Dump指定DEX内存中的数据并保存到文件(数据为odex格式,可在pc上反编译)
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dexfile”,”dexpath”:”*****”}’

5、Dump指定内存空间区域数据到文件
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_mem”,”start”:1234567,”length”:123}’

6、Dump Dalvik堆栈信息到文件,文件可以通过java heap分析工具分析处理
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_heap”}’

7、运行时动态调用Lua脚本
该功能可以通过Lua脚本动态调用java代码。使用场景:可以动态调用解密函数,完成解密。可以动态触发特定逻辑。
am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”invoke”,”filepath”:”****”}’

8、相关命令执行结果查看
1、命令执行结果
adb shell logcat -s zjdroid-shell-{package name}
2、敏感API调用监控输出结果
adb shell logcat -s zjdroid-apimonitor-{package name}


3、总结

好了,到这里我们就讲解完了基于Xposed框架的脱壳神器ZjDroid的实现原理以及具体用法。而这里也感受到了Xposed框架的强大之处,当然这也只是一部分,后面还可以利用这个框架编写游戏外挂等操作。


4、源码







附件列表

     

    以上是关于Fart脱壳-源码分析的主要内容,如果未能解决你的问题,请参考以下文章

    Android 逆向整体加固脱壳 ( 脱壳起点 : 整体加固脱壳 | Dalvik 脱壳机制 : 利用 DexClassLoader 加载过程进行脱壳 | 相关源码分析 )

    源码分析 脱壳神器ZjDroid工作原理

    Android 逆向整体加固脱壳 ( 脱壳点简介 | 修改系统源码进行脱壳 )

    DexHunter脱壳神器分析

    Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

    Android 逆向ART 脱壳 ( InMemoryDexClassLoader 脱壳 | DexFile 构造函数及相关调用函数 | Android 源码中查找 native 函数 )