Fart流程和原理分析

前言

Android应用保护发展的早期,通过对Dex整体进行加密或隐藏来完成对Dex的保护,也称之为一代壳.这时候侧重于对Dex的整体保护,相应的对Dex文件进行整体的Dump也就完成了脱壳.随着Android加固的发展,对Dex文件的保护也就提升到了函数粒度,通过对Dex中的函数指令进行抽取来抗衡Dex的整体Dump.在Android Dalvik环境下FUPK3通过引入主动调用思想,完美的解决了指令抽取型壳,但可惜的是该项目基于Android 4.4.4_r1进行开发,当下很多App已经不支持在Android4.4下安装运行.在Android ART环境下的今天,Fart基于主动调用的思想再加上将对Dex的Dump提升了到函数粒度,可有效来解决指令抽取型壳.

流程分析

Fart的入口函数在frameworks\base\core\java\android\app\ActivityThread.java的performLaunchActivity中.

1
2
3
4
5
public private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
	//---------
    //启动fart线程
    fartthread();
}

查看fartthread()函数,可以知道fart是在App启动1分钟后才开始调用fart()函数进行工作.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static void fartthread() {
    new Thread(new Runnable() {

        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                Log.e("ActivityThread", "start sleep......");
                Thread.sleep(1 * 60 * 1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Log.e("ActivityThread", "sleep over and start fart");
            fart();
            Log.e("ActivityThread", "fart run over");

        }
    }).start();
}

在fart()函数中,首先调用getClassloader()函数,拿到加固应用最终通过反射设置后的Classloader.

代码中提供了以下两种方式拿到Classloader对象:

  1. 通过反射调用ActivityThread类中的静态函数currentActivityThread(),拿到ActivityThread的静态实例对象sCurrentActivityThread,再通过反射拿到ActivityThread实例对象中的mInitialApplication成员.
  2. 通过反射拿到ActivityThread实例对象中的mBoundApplication成员,再反射拿到AppBindData对象的info成员,info的类型是LoadedApk,而在LoadedApk中存在一个成员mApplication,通过该成员可以拿到最终的Classloader对象.

通过对Android源码进行分析,可以知道上面两种方式拿到的Application对象mInitialApplication和mApplication,其实是一个值.

http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/ActivityThread.java

http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/LoadedApk.java

另外考虑到壳有两种情况来完成ClassLoader的替换,故下方的代码中也进行了两次fartwithClassloader函数的调用.

  1. 替换系统组件类加载器为壳的ClassLoader,同时设置壳ClassLoader的parent为系统组件类加载器.
  2. 打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入壳的ClassLoader.
 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
public static ClassLoader getClassloader() {
    ClassLoader resultClassloader = null;
    Object currentActivityThread = invokeStaticMethod(
            "android.app.ActivityThread", "currentActivityThread",
            new Class[]{}, new Object[]{});
    Object mBoundApplication = getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mBoundApplication");
    Application mInitialApplication = (Application) getFieldOjbect("android.app.ActivityThread",
            currentActivityThread, "mInitialApplication");
    Object loadedApkInfo = getFieldOjbect(
            "android.app.ActivityThread$AppBindData",
            mBoundApplication, "info");
    Application mApplication = (Application) getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplication");
    resultClassloader = mApplication.getClassLoader();
    return resultClassloader;
}

public static void fart() {
    ClassLoader appClassloader = getClassloader();
    ClassLoader tmpClassloader=appClassloader;
    ClassLoader parentClassloader=appClassloader.getParent();
    if(appClassloader.toString().indexOf("java.lang.BootClassLoader")==-1)
	{
		fartwithClassloader(appClassloader);
	}
    while(parentClassloader!=null){
		if(parentClassloader.toString().indexOf("java.lang.BootClassLoader")==-1)
		{
			fartwithClassloader(parentClassloader);
		}
        tmpClassloader=parentClassloader;
        parentClassloader=parentClassloader.getParent();
    }
}

在fartwithClassloader函数中,通过以下步骤遍历Dex中的所有函数并完成了主动调用.

  1. 通过反射拿到当前类加载器的DexPathList对象,进而拿到Element对象数组,Element类中存放着Dex的路径等信息.

  2. 遍历Element对象数组,拿到每一个Element对象的DexFile对象,接着通过反射拿到DexFile对象中的mCookie成员(指向当前Dex文件在内存的首地址).

  3. 利用Android内部封装好的函数getClassNameList,传入mCookie拿到Dex文件中所有的类名.

  4. 遍历Dex中所有类名,利用loadClassAndInvoke函数完成主动加载和调用.

需要注意的一点就是下方的dumpMethodCode函数至关重要,是fart新增的一个函数,且看后面对该函数的分析.

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public static void fartwithClassloader(ClassLoader appClassloader) {
    List<Object> dexFilesArray = new ArrayList<Object>();
    Field pathList_Field = (Field) getClassField(appClassloader, "dalvik.system.BaseDexClassLoader", "pathList");
    Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");
    Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");
    Field dexFile_fileField = null;
    try {
        dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");
    } catch (Exception e) {
        e.printStackTrace();
    } catch (Error e) {
        e.printStackTrace();
    }
    Class DexFileClazz = null;
    try {
        DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
    } catch (Exception e) {
        e.printStackTrace();
    } catch (Error 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("dumpDexFile")) {
            dumpDexFile_method = field;
            dumpDexFile_method.setAccessible(true);
        }
        if (field.getName().equals("dumpMethodCode")) {
            dumpMethodCode_method = field;
            dumpMethodCode_method.setAccessible(true);
        }
    }
    Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");
    Log.v("ActivityThread->methods", "dalvik.system.DexPathList.ElementsArray.length:" + ElementsArray.length);
    for (int j = 0; j < ElementsArray.length; j++) {
        Object element = ElementsArray[j];
        Object dexfile = null;
        try {
            dexfile = (Object) dexFile_fileField.get(element);
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
        if (dexfile == null) {
            Log.e("ActivityThread", "dexfile is null");
            continue;
        }
        if (dexfile != null) {
            dexFilesArray.add(dexfile);
            Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie");
            if (mcookie == null) {
                Object mInternalCookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mInternalCookie");
                if (mInternalCookie != null) {
                    mcookie = mInternalCookie;
                } else {
                    Log.v("ActivityThread->err", "get mInternalCookie is null");
                    continue;
                }

            }
            String[] classnames = null;
            try {
                classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            } catch (Error e) {
                e.printStackTrace();
                continue;
            }
            if (classnames != null) {
                for (String eachclassname : classnames) {
                    loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method);
                }
            }

        }
    }
    return;
}

在loadClassAndInvoke函数中,利用传入的ClassLoader对象对传入的类名进行主动加载,获取该类的所有函数,最后通过fart中新增的函数dumpMethodCode完成对目标类函数粒度的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
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
62
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
    Class resultclass = null;
    Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);
    try {
        resultclass = appClassloader.loadClass(eachclassname);
    } catch (Exception e) {
        e.printStackTrace();
        return;
    } catch (Error e) {
        e.printStackTrace();
        return;
    }
    if (resultclass != null) {
        try {
            Constructor<?> cons[] = resultclass.getDeclaredConstructors();
            for (Constructor<?> constructor : cons) {
                if (dumpMethodCode_method != null) {
                    try {
                        dumpMethodCode_method.invoke(null, constructor);
                    } catch (Exception e) {
                        e.printStackTrace();
                        continue;
                    } catch (Error e) {
                        e.printStackTrace();
                        continue;
                    }
                } else {
                    Log.e("ActivityThread", "dumpMethodCode_method is null ");
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
        try {
            Method[] methods = resultclass.getDeclaredMethods();
            if (methods != null) {
                for (Method m : methods) {
                    if (dumpMethodCode_method != null) {
                        try {
                            dumpMethodCode_method.invoke(null, m);
                        } catch (Exception e) {
                            e.printStackTrace();
                            continue;
                        } catch (Error e) {
                            e.printStackTrace();
                            continue;
                        }
                    } else {
                        Log.e("ActivityThread", "dumpMethodCode_method is null ");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
    }
}

接下来看下dumpMethodCode函数,该函数声明位于libcore/dalvik/src/main/java/dalvik/system/DexFile.java中,是一个Native函数.

1
private static native void dumpMethodCode(Object m);

其实现位于art/runtime/native/dalvik_system_DexFile.cc中.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
namespace art {
//add
extern "C" void myfartInvoke(ArtMethod* artmethod);
extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod);
//add
    
    //---
    
    //add
	static void DexFile_dumpMethodCode(JNIEnv* env, jclass clazz,jobject method) {
  	if(method!=nullptr)
  		{
		  ArtMethod* proxy_method = jobject2ArtMethod(env, method);
		  myfartInvoke(proxy_method);
	  	}	 
  	return;
	}
    //add
}

可以看到该函数十分简洁,调用jobject2ArtMethod将Java中的Method对象转换为C层中的ArtMethod*指针,然后调用myfartInvoke函数进行主动调用.

上方jobject2ArtMethod函数的实现在art/runtime/native/java_lang_reflect_Method.cc中.

1
2
3
4
5
6
7
8
9
namespace art {
//add
extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod) {
  ScopedFastNativeObjectAccess soa(env);
  ArtMethod* method = ArtMethod::FromReflectedMethod(soa, javaMethod);
  return method;
}
//add
}

myfartInvoke的实现在art/runtime/art_method.cc中.

1
2
3
4
5
6
7
8
extern "C" void myfartInvoke(ArtMethod* artmethod)  REQUIRES_SHARED(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");
}

在该函数中,通过传入的ArtMethod指针,进而调用其中的Invoke函数来完成函数的主动调用.

技巧
在fart中通过将self设置为nullptr来进行标记,用于后续判断是否我们自己进行的主动调用.

紧接着就要去看下art/runtime/art_method.cc中的Invoke函数,因为这是fart中的一个脱壳点同时也进行了函数粒度的指令Dump.

1
2
3
4
5
6
7
8
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
		if (self== nullptr) {
		dumpArtMethod(this);
		return;
		}
		//---
}

在dumpArtMethod函数中,主要做了以下几件事情;

  1. 利用ArtMethod类中的GetDexFile函数拿到DexFile对象,进而获取到与其对应的Dex文件在内存中的起始地址和大小,完成一次Dex文件的整体Dump.
  2. 通过对Dex文件格式进行解析拿到该Dex包含的所有类名并写出到文件xxx_classlist.txt文件中,方便我们后续通过类名来定位所在的Dex文件.
  3. 通过ArtMethod类中的GetCodeItem函数拿到该artmethod对应的CodeItem起始地址,然后又分为两种情况来计算CodeItem的大小,进而完成函数粒度的Dump,将其写出到xxx.bin文件中
  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
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
uint8_t *codeitem_end(const uint8_t **pData)
{
    //计算encoded_catch_handler_list中encoded_catch_handler结构的数量
    uint32_t num_of_list = DecodeUnsignedLeb128(pData);
    for (; num_of_list > 0; num_of_list--)
    {
        //计算encoded_catch_handler结构中encoded_type_addr_pair结构的数量
        int32_t num_of_handlers = DecodeSignedLeb128(pData);
        int num = num_of_handlers;
        if (num_of_handlers <= 0)
        {
            num = -num_of_handlers;
        }
        for (; num > 0; num--)
        {
            DecodeUnsignedLeb128(pData);
            DecodeUnsignedLeb128(pData);
        }
        if (num_of_handlers <= 0)
        {
            DecodeUnsignedLeb128(pData);
        }
    }
    return (uint8_t *)(*pData);
}

extern "C" void dumpArtMethod(ArtMethod *artmethod) REQUIRES_SHARED(Locks::mutator_lock_)
{
    char *dexfilepath = (char *)malloc(sizeof(char) * 1000);
    if (dexfilepath == nullptr)
    {
        LOG(ERROR) << "ArtMethod::dumpArtMethodinvoked,methodname:" << artmethod->PrettyMethod().c_str() << "malloc 1000 byte failed";
        return;
    }
    int result = 0;
    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)
    {
        result = read(fcmdline, szProcName, 256);
        if (result < 0)
        {
            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file file error";
        }
        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, 1000);
        int size_int_ = (int)size_;

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

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

        memset(dexfilepath, 0, 1000);
        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
        {
            int fp = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
            if (fp > 0)
            {
                result = write(fp, (void *)begin_, size_);
                if (result < 0)
                {
                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath file error";
                }
                fsync(fp);
                close(fp);
                memset(dexfilepath, 0, 1000);
                sprintf(dexfilepath, "/sdcard/fart/%s/%d_classlist.txt", szProcName, size_int_);
                int classlistfile = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
                if (classlistfile > 0)
                {
                    for (size_t ii = 0; ii < dex_file->NumClassDefs(); ++ii)
                    {
                        const DexFile::ClassDef &class_def = dex_file->GetClassDef(ii);
                        const char *descriptor = dex_file->GetClassDescriptor(class_def);
                        result = write(classlistfile, (void *)descriptor, strlen(descriptor));
                        if (result < 0)
                        {
                            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
                        }
                        const char *temp = "\n";
                        result = write(classlistfile, (void *)temp, 1);
                        if (result < 0)
                        {
                            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
                        }
                    }
                    fsync(classlistfile);
                    close(classlistfile);
                }
            }
        }
        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)
            {
                //跳过try_item tries结构,拿到encoded_catch_handler_list handlers结构的首地址
                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, 1000);
            int size_int = (int)dex_file->Size();
            uint32_t method_idx = artmethod->GetDexMethodIndexUnchecked();
            sprintf(dexfilepath, "/sdcard/fart/%s/%d_ins_%d.bin", szProcName, size_int, (int)gettidv1());
            int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
            if (fp2 > 0)
            {
                lseek(fp2, 0, SEEK_END);
                memset(dexfilepath, 0, 1000);
                int offset = (int)(item - begin_);
                sprintf(dexfilepath, "{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:", artmethod->PrettyMethod().c_str(), method_idx, offset, code_item_len);
                int contentlength = 0;
                while (dexfilepath[contentlength] != 0)
                    contentlength++;
                result = write(fp2, (void *)dexfilepath, contentlength);
                if (result < 0)
                {
                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
                }
                long outlen = 0;
                char *base64result = base64_encode((char *)item, (long)code_item_len, &outlen);
                result = write(fp2, base64result, outlen);
                if (result < 0)
                {
                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
                }
                result = write(fp2, "};", 2);
                if (result < 0)
                {
                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
                }
                fsync(fp2);
                close(fp2);
                if (base64result != nullptr)
                {
                    free(base64result);
                    base64result = nullptr;
                }
            }
        }
    }

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

另外在fart中dumpArtMethod函数的上方,还有一个函数dumpdexfilebyExecute也值得引起我们的注意.

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
extern "C" void dumpdexfilebyExecute(ArtMethod *artmethod) REQUIRES_SHARED(Locks::mutator_lock_)
{
    char *dexfilepath = (char *)malloc(sizeof(char) * 1000);
    if (dexfilepath == nullptr)
    {
        LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,methodname:" << artmethod->PrettyMethod().c_str() << "malloc 1000 byte failed";
        return;
    }
    int result = 0;
    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)
    {
        result = read(fcmdline, szProcName, 256);
        if (result < 0)
        {
            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file error";
        }
        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, 1000);
        int size_int_ = (int)size_;

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

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

        memset(dexfilepath, 0, 1000);
        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
        {
            int fp = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
            if (fp > 0)
            {
                result = write(fp, (void *)begin_, size_);
                if (result < 0)
                {
                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath error";
                }
                fsync(fp);
                close(fp);
                memset(dexfilepath, 0, 1000);
                sprintf(dexfilepath, "/sdcard/fart/%s/%d_classlist_execute.txt", szProcName, size_int_);
                int classlistfile = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
                if (classlistfile > 0)
                {
                    for (size_t ii = 0; ii < dex_file->NumClassDefs(); ++ii)
                    {
                        const DexFile::ClassDef &class_def = dex_file->GetClassDef(ii);
                        const char *descriptor = dex_file->GetClassDescriptor(class_def);
                        result = write(classlistfile, (void *)descriptor, strlen(descriptor));
                        if (result < 0)
                        {
                            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
                        }
                        const char *temp = "\n";
                        result = write(classlistfile, (void *)temp, 1);
                        if (result < 0)
                        {
                            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
                        }
                    }
                    fsync(classlistfile);
                    close(classlistfile);
                }
            }
        }
    }

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

从代码可以看到,该函数主要完成了对Dex文件的整体Dump以及输出Dex文件中包含的类名.另外对该函数的调用位于art\runtime\interpreter\interpreter.cc中的Execute函数中.这是fart中的第二个脱壳点,通过对dex2oat流程的分析,可以知道dex2oat对类的初始化函数并没有进行编译,也就是说类的初始化函数始终运行在ART下的inpterpreter模式,那么最终必然会进入到interpreter.cc文件中的Execute函数,这也就是fart选择此处作为第二个脱壳点的原因,可以与上面第一个脱壳点进行互补.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static inline JValue Execute(
    Thread* self,
    const DexFile::CodeItem* code_item,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_) {
	if(strstr(shadow_frame.GetMethod()->PrettyMethod().c_str(),"<clinit>"))
	{
		dumpdexfilebyExecute(shadow_frame.GetMethod());
	}
	//---
}

至此fart的流程分析完毕.

原理分析

从上面的流程分析中,仿佛没有感受到主动调用的存在,接下来进行fart主动调用原理层的讲解.

单个函数主动调用

这里我们从Android C层调用Java层方法的正常流程进行出发,通过跟踪Android源码来看下系统是如何完成在C层对Java层函数的调用.

1
2
3
4
5
6
7
8
9
JNIEXPORT jint luoJni(JNIEnv *env, jint n1, jint n2) {
    //①通过FindClass拿到类名
    jclass mainActivityClazz = env->FindClass("com/example/luodst/MainActivity");
    //②通过GetXXXMethodID拿到类中的方法
    jmethodID luoAdd = env->GetStaticMethodID(mainActivityClazz, "luoAdd", "(II)I");
    //③通过CallXXXMethod来调用方法.
    int nResult = env->CallStaticIntMethod(mainActivityClazz, luoAdd, n1, n2);
    return nResult;
}

FindClass源码跟踪如下:

1
2
3
4
5
6
//http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.cc
static jclass FindClass(JNIEnv* env, const char* name)
//http://androidxref.com/8.1.0_r33/xref/art/runtime/class_linker.cc
->mirror::Class* ClassLinker::FindClass(Thread* self,
                                      const char* descriptor,
                                      Handle<mirror::ClassLoader> class_loader)

FindClass

这里可以看到是通过调用ClassLinker类的FindClass完成对指定类的查找.

GetXXXMethodID源码跟踪如下:

1
2
3
4
5
//http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.cc
 static jmethodID GetStaticMethodID(JNIEnv* env, jclass java_class, const char* name,
                                     const char* sig)
->static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,
                              const char* name, const char* sig, bool is_static)   
1
2
3
4
//http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.cc
static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig)
->static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,
                              const char* name, const char* sig, bool is_static) 

FindMethodID

这里可以看到C层的GetXXXMethodID函数最终都是通过调用FindMethodID函数进行类方法的查找,这里我们同时也注意到了jmethodID其实是可以和ArtMethod*类型进行互转的.

CallXXXMethod源码跟踪如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.cc
static jint CallStaticIntMethod(JNIEnv* env, jclass, jmethodID mid, ...)
//http://androidxref.com/8.1.0_r33/xref/art/runtime/reflection.cc
->JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa, jobject obj, jmethodID mid,
                         va_list args)
static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
//http://androidxref.com/8.1.0_r33/xref/art/runtime/art_method.cc
->void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.cc
static void CallVoidMethod(JNIEnv* env, jobject obj, jmethodID mid, ...)\
//http://androidxref.com/8.1.0_r33/xref/art/runtime/reflection.cc
->JValue InvokeVirtualOrInterfaceWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,
                                           jobject obj, jmethodID mid, va_list args)
->static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
//http://androidxref.com/8.1.0_r33/xref/art/runtime/art_method.cc
->void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) 

invokeWithinvokeWith2

这里可以看到,原来平时我们在C层使用的CallXXXMethod函数进行Java层函数调用的底层是由该函数对应的ArtMethod对象的Invoke函数来完成的.

至此我们可以写出自己的主动调用函数了,在fart中该部分的体现主要在art/runtime/art_method.cc中的myfartInvoke函数中.另外有一点比较巧妙的就是在构造函数参数的时候通过将函数的第一个参数self置空,然后在ArtMethod类的Invoke函数中进行判断若是我们主动调用的就进行该函数方法体的Dump,不让其继续执行下去.同时在这里我们也注意到了fart对于函数主动调用的深度也就停留在了ArtMethod类的Invoke函数中,假设壳在Invoke函数之后才进行函数指令的解密以及方法体的填充,那么这是fart目前所不能解决的.

遍历Dex所有函数完成主动调用

加固应用的运行流程大致如下:

加固应用运行流程

当壳在函数attachBaseContext和onCreate中执行完加密Dex文件的解密后,通过自定义的Classloader在内存中加载解密后的dex文件.为了解决后续应用在加载执行解密后的dex文件中的Class和Method的问题,接下来就是通过利用java的反射修复一系列的变量.其中最为重要的一个变量就是应用运行中的Classloader,这里面包含着App真实的业务代码.因此,只要获取到加固应用最终通过反射设置后的Classloader,我们就可以通过一系列反射完成对加密代码的解密.

Fart的入口时机选在ActivityThread类的performLaunchActivity函数中,这个函数是用来响应Activity相关的操作,当App在响应Activity消息时,壳已经完成了ClassLoader的替换.因此,此时通过反射利用fart内部的函数getClassloader拿到的就是加固应用最终通过反射设置后的Classloader.然后利用fartwithClassloader函数,对Classloader进行处理,经过一系列的反射拿到DexFile对象,利用android内部函数getClassNameList获取到Dex文件中所有的类名,最后通过主动加载的形式,获取每个类中的所有函数,进行函数的主动调用.

注意
在Fart中处理的Classloader是加固应用最终通过反射设置后的Classloader,但App在运行过程中也可以动态加载Dex文件,此时的Classloader并不在fart处理的范围内.对于这部分代码的处理,fart提供了一个函数fartwithClassloader,需要结合frida进行使用.

总结

Fart的流程图如下:

Fart流程图

其脱壳步骤主要分为以下三步:

  1. 内存中DexFile结构体完整Dex的Dump
  2. 主动调用类中的每一个方法,并实现对应CodeItem的Dump
  3. 通过主动调用Dump下来方法的CodeItem进行Dex中被抽取方法的修复

在Android ART环境下,Fart提出了一种针对指令抽取型壳的解决方案,很值得我们进行研究学习.同样我们可以站在巨人的肩膀上,进行更深层次的主动调用链构造以及将Fart与Frida相结合为我们破开Android应用分析的第一层壁垒,降低Android应用分析的难度.

参考链接

FART:ART环境下基于主动调用的自动化脱壳方案


相关内容

0%