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点。

最新回复:
mb_ldbucrik 2024-06-14 22:59:07
向楼主学习
wx_你有帅得过黎明嘛 2024-06-17 18:07:10
tql