1.前言
这篇文章是基于上篇文章所总结知识点的抽取壳理论实践。
说到抽取壳脱壳机,FART算是很熟悉了,寒冰师傅的FART是ART环境下基于主动调用的自动化脱壳方案,FART关键的脱壳点是在ARTMethod::invoke,通过JNI函数主动遍历所有ArtMethod,在ARTMethod::invokedump下code_item中的数据。
通过之前对Dex加载流程,类加载流程,以及方法执行流程的总结,可以得知APP的Java方法的调用主要有两种,一是通过反射或JNI主动调用,二是通过自然调用路径,例如当APP被点击某个功能,Java方法就会被自然调用,FART的调用方法方式就是通过第一种方式,是所以是主动的方式。那存在被动调用吗,当APP被点击某个功能,Java方法就会被自然调用,如果脱壳机是在由系统触发的自然调用流程中进行被动脱壳,那么它就是基于被动调用的脱壳方案。
环境:Android 8.0
2.分析实现
有了之前的理论基础,其实实现起来就比较简单了,之前分析了自然调用流程:art_quick_to_interpreter_bridge->artQuickToInterpreterBridge() → interpreter::EnterInterpreterFromEntryPoint() → interpreter::Execute()在这个流程中找一个合适的脱壳点添加脱壳机代码即可,脱壳机的代码其实用FART的即可因为脱Code_item的流程都是一样的,少了主动调用的代码。
第一步:选择脱壳点
先看下这个流程涉及的几个函数的入参
__artQuickToInterpreterBridge__这个函数的入参有两个ArtMethod,第二个是SP指针用于构造解释器调用栈帧,栈帧包含了方法运行时的局部变量、操作数栈和其他必要的信息。通过调用栈我们就可以获取当当前运行的方法的ArtMethod
1 2 3 4 | extern "C" uint64_t artQuickToInterpreterBridge(ArtMethod* method, Thread* self, ArtMethod** sp) REQUIRES_SHARED(Locks::mutator_lock_) { ...} |
__EnterInterpreterFromEntryPoint__两个参数一个当前线程指针,一个栈帧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | JValue EnterInterpreterFromEntryPoint(Thread* self, const DexFile::CodeItem* code_item, ShadowFrame* shadow_frame) { DCHECK_EQ(self, Thread::Current()); bool implicit_check = !Runtime::Current()->ExplicitStackOverflowChecks(); if (UNLIKELY(__builtin_frame_address(0) < self->GetStackEndForInterpreter(implicit_check))) { ThrowStackOverflowError(self); return JValue(); } jit::Jit* jit = Runtime::Current()->GetJit(); if (jit != nullptr) { jit->NotifyCompiledCodeToInterpreterTransition(self, shadow_frame->GetMethod()); } return Execute(self, code_item, *shadow_frame, JValue());} |
__Execute__Execute这里的参数就比较多了可以看到code_item直接被传过来了,还有shadow_frame。
1 2 3 4 5 6 7 8 | 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_) { ...} |
看完这三个函数其实都可以做脱壳点,接下来开始添加脱壳代码把。
第二步 添加脱壳代码
之前说了脱壳代码可以直接使用FART中的dumpArtMethod函数,因为ArtMethod对象的获取并不难,直接传入ArtMethod即可
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 | extern "C" void dumpArtMethod(ArtMethod* artmethod) REQUIRES_SHARED(Locks::mutator_lock_) { char * dexfilepath = (char *) malloc(sizeof(char) * 1000); if (dexfilepath==nullptr) { LOG(INFO) << "ArtMethod::dumpArtMethod,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); szProcName[sizeof(szProcName) - 1] = '\0'; if (result < 0) { LOG(ERROR) << "ArtMethod::dumpArtMethod,open cmdline file file error"; return; } close(fcmdline); } ...} |
到这一步的话脱壳机已经可以运行起来了,因为被动脱壳方案是在代码自然调用路径上,所以不需要构造主动调用代码。不过这里还可以进一步优化一下,因为是在自然调用路径所以系统的其他代码也可能会被一起脱下来,这样很影响效率,所以可以添加一段配置读取代码,在某个文件夹下放一个配置文件,文件里写要脱壳APP的包名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | int fd=-1;char filepath[] = "/xxxxxx/config.txt";char line[256]={ 0 };char content[256]={ 0 };fd = open(filepath, O_RDONLY, 0666);if (fd == -1) { LOG(INFO) << "ArtMethod::dumpArtMethod,Error:" << strerror(errno);}if (fd > 0) { if (read(fd, line, sizeof(line)-1) != -1) { if (line[0] != '\n' && line[0] != '\r'&& line[0] != '\0') { strncpy(content, line, sizeof(content) - 1); } } close(fd);} |
第三步实战测试
这里选了一个企业版加固的APP来测试,企业版的加固抽取壳算是基本配置,这里不贴APP图片了。
先选一个要脱壳的地方,我选在了登录功能上,先把包名写到配置文件中去,然后启动APP,这里为了方便分析我打印了被调用函数,之后账号密码随便输入一下点登录,登录功能的脱壳就开始了,通过调用日志看一下,可以看到了名为LoginActivity的一些函数被调用比如LoginActivity.a(),从名字可以看出是用于登录的Activity组件,并且可以看到有的函数被混淆了,还有常用于发包的okhttp库的调用。

登录的主要逻辑主要是在OnClick()里看一下smali可以看到被抽取了codeitem

过滤一下包含LoginActivity的脱壳文件,可以看到3576752_ins_2543.bin,3576752_ins_2708.bin,3576752_classlist.txt以及3576752_dexfile.dex,这里还原抽取壳可以直接用FART的修复代码稍微修改一下。

修复前:

修复后:

可以看到我们点击登录功能后的函数被还原了。
3.优缺点
基于被动脱壳的脱壳方案有一个明显的缺点,脱壳脱不完整,只能脱系统调用了的方法,不过这也算是它的优点,在分析APP算法的Java层的流程时可以快速定位被调用了的函数,以及发包方式,排除可疑代码的干扰,快速定位Hook点。