加壳与脱壳之二代壳函数抽取

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

函数抽取宣告一代壳整体保护的结束,由此进入二代壳的时代,本文将对Dalvik和Art下函数抽取的加固方法原理介绍及实现函数抽取的代码保护方案。

Dalvik

源码分析

实现函数抽取壳需要保证对函数恢复的时机肯定早于函数被调用的时机,不然app逻辑被破坏了,即当函数被调用时指令流必须已经被修复了,否则app逻辑被破坏导致app崩了。再选择时机,Android中实现「类方法指令抽取方式」加固方案原理解析中选择了dexFindClass函数。

对于函数来说在被调用之前,首先dex加载(dexclassloader动态加载),对类加载的函数调用时需要进行一些准备,需要经过装载-链接-初始化,在这些加载过程中有非常多的时机供我们选择,Dalvik源代码编译生成的system/lib/libdvm.so通过ida打开,搜索文中的dexFindClass函数,获取导出的函数名可实现函数抽取。hook的时机点肯定是早于被调用的时机点,当dex被dexclassloader加载完后,需要加载dex其他类(隐式加载+显式加载)。搜索libcore库中loadClass加载一个dex中的类的流程:DexClassLoader->BaseDexClassLoader->ClassLoader的loadClass,完整体现了双亲委派的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
486    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {		   // 为了提高加载类效率,使用父节点加载如果已经加载直接返回,否则进行双亲委派的父节点加载
487 Class<?> clazz = findLoadedClass(className);
488
489 if (clazz == null) {
490 try {
491 clazz = parent.loadClass(className, false);
492 } catch (ClassNotFoundException e) {
493 // Don't want to see this.
494 }
495
496 if (clazz == null) {
497 clazz = findClass(className);
498 }
499 }
500
501 return clazz;
502 }

对于使用DexClassLoader第一次加载类的parent节点是pathClassLoader或被指定的bootClassLoader,因为类只由当前的classloader加载必然是找不到的,因此进入BaseDexClassLoader的findClass实现中。

1
2
3
4
5
6
7
8
9
10
11
12
52    protected Class<?> findClass(String name) throws ClassNotFoundException {
53 List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
54 Class c = pathList.findClass(name, suppressedExceptions);
55 if (c == null) {
56 ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
57 for (Throwable t : suppressedExceptions) {
58 cnfe.addSuppressed(t);
59 }
60 throw cnfe;
61 }
62 return c;
63 }

紧接着进入pathList.findClass,其中pathList是在BaseDexClassLoader构造函数中实例化化,跟着进入pathList.findClass,调用了dex.loadClassBinaryName查找类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
317    public Class findClass(String name, List<Throwable> suppressed) {
318 for (Element element : dexElements) {
319 DexFile dex = element.dexFile;
320
321 if (dex != null) {
// 尝试从每个dex中遍历找到类所在的dex中并返回类所在dex
322 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
323 if (clazz != null) {
324 return clazz;
325 }
326 }
327 }
328 if (dexElementsSuppressedExceptions != null) {
329 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
330 }
331 return null;
332 }

跟着进入loadClassBinaryName,其中调用了native层实现defineClassNative

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
214    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
215 return defineClass(name, loader, mCookie, suppressed);
216 }
217
218 private static Class defineClass(String name, ClassLoader loader, int cookie,
219 List<Throwable> suppressed) {
220 Class result = null;
221 try {
222 result = defineClassNative(name, loader, cookie);
223 } catch (NoClassDefFoundError e) {
224 if (suppressed != null) {
225 suppressed.add(e);
226 }
227 } catch (ClassNotFoundException e) {
228 if (suppressed != null) {
229 suppressed.add(e);
230 }
231 }
232 return result;
233 }

dalvik下Full Search搜索defineClassNative,进入/dalvik/vm/native/dalvik_system_DexFile.cppDalvik_dalvik_system_DexFile_defineClassNative方法,如注释中所说从一个dex文件中加载一个类

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
349static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args,
350 JValue* pResult)
351{
352 StringObject* nameObj = (StringObject*) args[0]; // 类名
353 Object* loader = (Object*) args[1]; // classloader
354 int cookie = args[2]; // java层cookie
355 ClassObject* clazz = NULL;
356 DexOrJar* pDexOrJar = (DexOrJar*) cookie; // 将int型cookie转为DexOrJar指针
357 DvmDex* pDvmDex;
358 char* name;
359 char* descriptor;
360
361 name = dvmCreateCstrFromString(nameObj);
362 descriptor = dvmDotToDescriptor(name);
363 ALOGV("--- Explicit class load '%s' l=%p c=0x%08x",
364 descriptor, loader, cookie);
365 free(name);
366
367 if (!validateCookie(cookie))
368 RETURN_VOID();
369
370 if (pDexOrJar->isDex)
371 pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile);
372 else
373 pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile);
374
375 /* once we load something, we can't unmap the storage */
376 pDexOrJar->okayToFree = false;
377
378 clazz = dvmDefineClass(pDvmDex, descriptor, loader);

进入dvmGetRawDexFileDex,只是取出指针中的pDvmDex

1
2
3
62INLINE DvmDex* dvmGetRawDexFileDex(RawDexFile* pRawDexFile) {
63 return pRawDexFile->pDvmDex;
64}

进入dvmDefineClass

1
2
3
4
5
6
7
1413ClassObject* dvmDefineClass(DvmDex* pDvmDex, const char* descriptor,
1414 Object* classLoader)
1415{
1416 assert(pDvmDex != NULL);
1417
1418 return findClassNoInit(descriptor, classLoader, pDvmDex);
1419}

进入findClassNoInit,首先调用了clazz = dvmLookupClass(descriptor, loader, true);对当前加载的类名进行计算查询,如没有则返回为空,对于第一次加载必然是没有的,接着进入pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor);,即Android中实现「类方法指令抽取方式」加固方案原理解析中选择的时机点,并通过hook掉类被加载时的时机,对抽取函数的恢复,实现函数还原,因此必然需要早于函数执行的时机,保证app正常运行。由于有些函数符号没有导出,就不是很容易进行hook,而在libdvm.so中的dexFindClass在IDA中是被导出的,方便hook。以上就是Dalvik下函数抽取壳的简单原理。

ART

Art下实现难点:dex2oat编译流程,dex2oat是可以进行脱壳,dex2oat完成了对抽取的dex进行编译生成了oat文件,后续的函数运行中,从oat中取出函数编译生成的二进制代码来执行,因此函数对dex填充后,如果时机不对,时机在dex2oat后,自然从dex2oat后那么我们动态修改的dex中的smali指令流就不会生效,因为后面app运行调用的真正的代码就会从dex2oat编译生成的oat文件,和以前的dex无关了。因此如果希望填充回去smali指令生效要么禁用dex2oat实现阻止编译,这样对加载到内存中的dex文件进行填充始终会保持生效,要么保持dex2oat编译,但是还原代码时机要早于dex2oat就ok了,保证dex2oat再次对dex编译的时候,dex已经是一个完整dex,不会影响我们填充的代码,但是肯定dex文件存在完整的时候,可以利用dex2oat编译的流程进行脱壳,一般加壳厂商都是牺牲掉app一部分的运行效率,干掉dex2oat的过程,因为google本身提倡dex2oat就是为了提升app运行效率。

禁用dex2oat编译

回到ART下DexClassLoader动态加载dex的流程,在art下搜索GenerateOatFileNoChecks,该函数完成调用dex2oat进行编译

搜索GenerateOatFileNoChecks

GenerateOatFileNoChecks中调用Dex2Oat函数

1
2
616OatFileAssistant::ResultOfAttemptToUpdate OatFileAssistant::GenerateOatFileNoChecks(
617 OatFileAssistant::OatFileInfo& info, CompilerFilter::Filter filter, std::string* error_msg) {

Dex2Oat最终调用return Exec(argv, error_msg);Exec中调用了int status = ExecAndReturnCode(arg_vector, error_msg);,其中ExecAndReturnCode通过pid_t pid = fork();进行fork一个子进程,在子进程中调用了execve(program, &args[0], envp);完成对dex2oat这个二进制程序的调用。整个流程中任意一个环节被打断,dex2oat将会被干掉,无法继续运行。

TurboDex就是干掉dex2oat为了让dex在第一次动态加载时快速加载完成,因为不干掉dex2oat,art虚拟机就会调用dex2oat对当前的dex进行编译,编译过程非常耗时,可以很大地提升dexclassloader加载dex 的效率,该项目就是通过hook了execv方法实现。

众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.

TurboDex 就是为了解决这一问题而生, 就像是给AndroidVM开启了上帝模式, 在引入TurboDex后, 无论你加载了多大的Dex文件,都可以在毫秒级别内完成.

接下来通过hook execve实现干掉dex2oat,可以通过爱奇艺xhook的GOT进行表hook干掉dex2oat,也可以用inline库hook。

SecondShell_80项目中:hooklibc中的execve的函数,干掉dexclassloader加载dex过程中dex2oat的流程。

myexecve

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
// 系统函数(在libc库中定义)被调用进入替换原始逻辑
void* *myexecve(const char *__file, char *const *__argv, char *const *__envp) {
LOGD("process:%d,enter execve:%s", getpid(), __file);
if (strstr(__file, "dex2oat")) {
return NULL;
} else {
return oriexecve(__file, __argv, __envp);
}
}
// 实现干掉dex2oat的逻辑,加载原始的dex文件
void hooklibc() {
LOGD("go into hooklibc");
//7.0 命名空间限制 libc有直接权限调用
void *libc_addr = dlopen_compat("libc.so", RTLD_NOW);
void *execve_addr = dlsym_compat(libc_addr, "execve");
if (execve_addr != NULL) {
// 需要hook函数地址,替换的地址,保存原函数地址在自己函数逻辑中进行调用
if (ELE7EN_OK == registerInlineHook((uint32_t) execve_addr, (uint32_t) myexecve,
(uint32_t **) &oriexecve)) {
if (ELE7EN_OK == inlineHook((uint32_t) execve_addr)) {
LOGD("inlineHook execve success");
} else {
LOGD("inlineHook execve failure");
}
}
}
}

启动SecondShell_80项目,并授予sdcard权限,查看logcat,检索dex2oat

FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法找一个好时机点实现art下二代函数抽取壳对抽空函数的还原,LoadClassMembers完成对要加载的class的准备工作,准备SetSFields,SetDirectMethodsPtr,要对当前已经加载的dex内容进行修改某一个被抽空的函数,首先定位到当前函数的CodeItem的地址进行修复,这过程中出现的时机点可以hook掉。LoadMethod设置了CodeItem的偏移,ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);初始化了method对象,此时内容还没有被填充,ArtMethod中关键变量dex_code_item_offset,在我们class被加载完后,准备好当前class每个函数对应的ArtMethod对象,禁用掉dex2oat以后,自然类中所有的函数都在解释模式下运行,必然找到当前ArtMethod的CodeItem在内存中的偏移,进行取出一条条smali指令流解释执行。LoadMethod(self, dex_file, it, klass, method);传入的dex_file为当前的dex对象,method为当前要准备的ArtMethod对象,每个ArtMethod对象都和java层的函数一一对应,这些函数都可以被hook掉,因为都在函数被调用前执行,都能实现对其的填充。

1
2
3
4
5
6
7
8
9
10
11
12
3305void ClassLinker::LoadMethod(const DexFile& dex_file,
3306 const ClassDataItemIterator& it,
3307 Handle<mirror::Class> klass,
3308 ArtMethod* dst) {
3309 uint32_t dex_method_idx = it.GetMemberIndex();
3310 const DexFile::MethodId& method_id = dex_file.GetMethodId(dex_method_idx);
3311 const char* method_name = dex_file.StringDataByIdx(method_id.name_idx_);
3312
3313 ScopedAssertNoThreadSuspension ants("LoadMethod");
3314 dst->SetDexMethodIndex(dex_method_idx); // 设置MethodIndex
3315 dst->SetDeclaringClass(klass.Get()); // 设置类
3316 dst->SetCodeItemOffset(it.GetMethodCodeItemOffset()); // 指向smali指令在内存中的偏移

如果hook了LoadMethod函数,被调用完后ArtMethod对象的CodeItemOffset就完成了设置,且第五个参数就是ArtMethod对象的指针,可以很容易取出ArtMethod参数的CodeItemOffset,定位到了当前要填充的函数的smali指令流在内存中的偏移位置,避免了大量代码解析dex在内存中的映射。

实现函数抽取壳

案例用loadDex.apk和4.dex。

定位函数要抽取的位置com.kanxue.test02.TestClass.testFunc的函数置空。使用010Editor打开4.dex,方法太多了导出csv

image-20210601213439021

导出的csv用010打开,检索TestClass,出现索引号1028

image-20210601213656008

回过头来再看4.dex中的TestClass的位置

image-20210601213827131

code_item占了32个字节

image-20210601213950873

GDA查看4.dex该方法,右键-Show Hex

image-20210601214744838

image-20210601220617803

对应010中选中部分全部改成0,即完成函数抽取。

image-20210601214904829

dex header本身对dex校验,改完后还需要修改校验头部分

python checksum.py 返回CheckSum = 0x2785815e,则修改校验头部分,使其成为一个合法的dex文件

image-20210601215218271

gda再次打开该dex,完成函数抽取。

image-20210601215350064

接下来完成对testFunc的填充,并正常调用testFunc中的代码。

  1. 通过hooklibc的execve禁用掉dex2oat
  2. 选中了LoadMethod函数的hook,原型是oriloadmethod,并添加了自己的逻辑。需要当testFunc被初始化时需要将原有的smali指令流利用myloadmethod填充回去,
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
void hookART() {
LOGD("go into hookART");
void *libart_addr = dlopen_compat("/system/lib/libart.so", RTLD_NOW);
if (libart_addr != NULL) {
void *loadmethod_addr = dlsym_compat(libart_addr,
"_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE");
if (loadmethod_addr != NULL) {
if (ELE7EN_OK == registerInlineHook((uint32_t) loadmethod_addr, (uint32_t) myloadmethod,
(uint32_t **) &oriloadmethod)) {
if (ELE7EN_OK == inlineHook((uint32_t) loadmethod_addr)) {
LOGD("inlineHook loadmethod success");
} else {
LOGD("inlineHook loadmethod failure");
}
}
}
}
}
void *myloadmethod(void *a, void *b, void *c, void *d, void *e) {
LOGD("process:%d,before run loadmethod:", getpid());
struct ArtMethod *artmethod = (struct ArtMethod *) e;
struct DexFile *dexfile = (struct DexFile *) b;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d", getpid(), dexfile->begin,
dexfile->size);//0,57344
char dexfilepath[100] = {0};
sprintf(dexfilepath, "/sdcard/%d_%d.dex", dexfile->size, getpid());
int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
if (fd > 0) {
// 得到dex file的起始地址和大小,可以将此刻dex dump下来,用来对比
write(fd, dexfile->begin, dexfile->size);
close(fd);
}
// oriloadmethod未被调用时,artmethod未被初始化,值为空。当调用了原始的oriloadmethod关键变量被初始化好,如dex_method_index_和dex_code_item_offset_,取出artmethod
void *result = oriloadmethod(a, b, c, d, e);
LOGD("process:%d,enter loadmethod:code_offset:%d,idx:%d", getpid(),
artmethod->dex_code_item_offset_, artmethod->dex_method_index_);

byte *code_item_addr = static_cast<byte *>(dexfile->begin) + artmethod->dex_code_item_offset_;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
dexfile->begin, dexfile->size, code_item_addr);

// 当地址为15203时,即testFunc函数(010中),进行原有指令数组进行填充
if (artmethod->dex_method_index_ == 15203) {//TestClass.testFunc->methodidx
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,start repire method", getpid(),
dexfile->begin, dexfile->size);
byte *code_item_addr = (byte *) dexfile->begin + artmethod->dex_code_item_offset_;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
dexfile->begin, dexfile->size, code_item_addr);

int result = mprotect(dexfile->begin, dexfile->size, PROT_WRITE);
byte *code_item_start = static_cast<byte *>(code_item_addr) + 16; // 16字节后才是函数填充全0的指令流
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,code_item_start:%p", getpid(),
dexfile->begin, dexfile->size, code_item_start);
byte inst[16] = {0x1a, 0x00, 0xed, 0x34, 0x1a, 0x01, 0x43, 0x32, 0x71, 0x20, 0x91, 0x05,
0x10, 0x00, 0x0e, 0x00};
for (int i = 0; i < sizeof(inst); i++) {
// 逐个赋值
code_item_start[i] = inst[i];
}
//2343->i am from com.kanxue.test02.TestClass.testFunc
code_item_start[2] = 0x43;//34ed->kanxue
code_item_start[3] = 0x23;
memset(dexfilepath, 0, 100);
sprintf(dexfilepath, "/sdcard/%d_%d.dex_15203_2", dexfile->size, getpid());
fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
if (fd > 0) {
write(fd, dexfile->begin, dexfile->size);
close(fd);
}
}
LOGD("process:%d,after loadmethod:code_offset:%d,idx:%d", getpid(),
artmethod->dex_code_item_offset_, artmethod->dex_method_index_);//0,57344
return result;

}

将修复后的4_chouqu.dex推送到/sdcard中

image-20210601204950420

通过初始化后的code_item的偏移直接定位dex在内存中地址,并进行修正,实现art下函数抽取的解决方案,并没有通过大量代码art中的头文件去解析dex结构信息。

文章作者: J
文章链接: http://onejane.github.io/2021/03/25/加壳与脱壳之二代壳函数抽取/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏