Android加壳与脱壳(8)——Youpk脱壳机源码分析

admin 2024年7月4日16:15:20评论1 views字数 6086阅读20分17秒阅读模式

1.前言

今天来分析一下自动脱壳机Youpk的源码,Youpk是一个可以解决整体壳、抽取壳的脱壳机,可以解决自解密型的抽取壳。

2.源码分析

2.1 创建脱壳线程

frameworks/base/core/java/android/app/ActivityThread.java

函数:handleBindApplication

Android加壳与脱壳(8)——Youpk脱壳机源码分析

Android加壳与脱壳(8)——Youpk脱壳机源码分析

这里选择这个函数,是因为Android系统的执行流程,都要经过这个函数,且与FART的选择点有区别

art/runtime/unpacker/unpacker.cc

函数:unpack

Android加壳与脱壳(8)——Youpk脱壳机源码分析

我们可以依次看见4个功能模块:

1
2
3
4
(1)初始化
(2)dump所有dex
(3)主动调用所有方法
(4)还原

2.2 初始化init()

unpacker/unpacker.cc

Android加壳与脱壳(8)——Youpk脱壳机源码分析

data/data/process目录下新建三个文件夹dexmethodunpacker.json

getDexFiles:

Android加壳与脱壳(8)——Youpk脱壳机源码分析

内存中dump DEX,DEX文件在art虚拟机中使用DexFile对象表示, 而ClassLinker中引用了这些对象, 因此可以采用从ClassLinker中遍历DexFile对象并dump的方式来获取

注意:因为从内存中dump,所以无法针对动态加载的壳进行解决,因为这类壳需要完成类加载器的修正,而youpk就没有完成该部分的工作

另外, 为了避免dex做任何形式的优化影响dump下来的dex文件, 在dex2oat中设置 CompilerFilter 为仅验证

1
2
//dex2oat.cc
compiler_options_->SetCompilerFilter(CompilerFilter::kVerifyAtRuntime);

getAppClassLoader:

Android加壳与脱壳(8)——Youpk脱壳机源码分析

通过反射获得当前的类加载器

2.3 dump所有dex DumpAllDexs()

Android加壳与脱壳(8)——Youpk脱壳机源码分析

1
2
3
4
代码解析:
(1)对dexs文件进行Dump,根据begin和大小
(2)并且对抗了dex魔术字段被抹除的形式 可借鉴
(3)成功的保存 可改进

功能:对Dexfile的文件进行Dump

2.4 主动调用链invokeAllMethods()

Android加壳与脱壳(8)——Youpk脱壳机源码分析

1
2
代码解析:
(1)获得Dex的位置、dump路径、类大小

Android加壳与脱壳(8)——Youpk脱壳机源码分析

如果路径、位置、大小都相等,则说明是处理过后的dex

Android加壳与脱壳(8)——Youpk脱壳机源码分析

这里对init进行处理

Android加壳与脱壳(8)——Youpk脱壳机源码分析

将一些dex的信息保存到Json中

Android加壳与脱壳(8)——Youpk脱壳机源码分析

遍历dex的所有ClassDef

Android加壳与脱壳(8)——Youpk脱壳机源码分析

读取一些函数的状态

Android加壳与脱壳(8)——Youpk脱壳机源码分析

对函数初始化进行处理,对未经过初始化的函数进行初始化

Android加壳与脱壳(8)——Youpk脱壳机源码分析

开启虚假的调用

Android加壳与脱壳(8)——Youpk脱壳机源码分析

对方法进行遍历,对非抽象方法或非静态方法进行模拟调用

Android加壳与脱壳(8)——Youpk脱壳机源码分析

Android加壳与脱壳(8)——Youpk脱壳机源码分析

关闭模拟调用开关,然后将状态进行修改

2.5 系统源码

artmethod.cc

Invoke()函数:修改

Android加壳与脱壳(8)——Youpk脱壳机源码分析

如果是主动调用,并且不是native方法就强制走解释器

如果是主动调用并且是native方法则不执行

interpreter.cc

EnterInterpreterFromInvoke()函数

1
2
3
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
uint32_t* args, JValue* result,
bool stay_in_interpreter) {

在这里面函数youpk并未进行处理

但是这里youpk强制修改了解释器:

Android加壳与脱壳(8)——Youpk脱壳机源码分析

Android加壳与脱壳(8)——Youpk脱壳机源码分析

Execute()函数:

1
2
3
4
5
6
static inline JValue Execute(
Thread* self,
const DexFile::CodeItem* code_item,
ShadowFrame& shadow_frame,
JValue result_register,
bool stay_in_interpreter = false) SHARED_REQUIRES(Locks::mutator_lock_) {

Android加壳与脱壳(8)——Youpk脱壳机源码分析

interpreter_switch_impl.cc

ExecuteSwitchImpl()函数:修改

1
2
3
JValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item,
ShadowFrame& shadow_frame, JValue result_register,
bool interpret_one_instruction) {

Android加壳与脱壳(8)——Youpk脱壳机源码分析

可以发现当程序进入Switch进行翻译后,翻译每条指令之前,都会执行PREAMBLE()方法,而Youpk就对这里进行了修改,这里与FARTExt区别的地方,就是这里是利用了解析器的判断:

PREAMBLE()函数:修改

Android加壳与脱壳(8)——Youpk脱壳机源码分析

在程序翻译之前都要使用beforeInstructionExecute进行判断,这里就是Youpk深度调用链的地方,处理(NOP、Goto)等壳的地方,如果已经dump了就直接返回,这里程序结束,这里可以进行改进

unpacker.cc

beforeInstructionExecute()函数:修改

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
//继续解释执行返回false, dump完成返回true
bool Unpacker::beforeInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) {
if (Unpacker::isFakeInvoke(self, method)) {
const uint16_t* const insns = method->GetCodeItem()->insns_;
const Instruction* inst = Instruction::At(insns + dex_pc);
uint16_t inst_data = inst->Fetch16(0);
Instruction::Code opcode = inst->Opcode(inst_data);

//对于一般的方法抽取(非ijiami, najia), 直接在第一条指令处dump即可
if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) {
Unpacker::dumpMethod(method);
return true;
}
//ijiami, najia的特征为: goto: goto_decrypt; nop; ... ; return; const vx, n; invoke-static xxx; goto: goto_origin;
else if (inst_count == 0 && opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
return false;
} else if (inst_count == 1 && opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16) {
return false;
} else if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)) {
//让这条指令真正的执行
Unpacker::disableFakeInvoke();
Unpacker::enableRealInvoke();
return false;
} else if (inst_count == 3) {
if (opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
//写入时将第一条GOTO用nop填充
const Instruction* inst_first = Instruction::At(insns);
Instruction::Code first_opcode = inst_first->Opcode(inst->Fetch16(0));
CHECK(first_opcode >= Instruction::GOTO && first_opcode <= Instruction::GOTO_32);
ULOGD("found najia/ijiami %s", PrettyMethod(method).c_str());
switch (first_opcode)
{
case Instruction::GOTO:
Unpacker::dumpMethod(method, 2);
break;
case Instruction::GOTO_16:
Unpacker::dumpMethod(method, 4);
break;
case Instruction::GOTO_32:
Unpacker::dumpMethod(method, 8);
break;
default:
break;
}
} else {
Unpacker::dumpMethod(method);
}
return true;
}
Unpacker::dumpMethod(method);
return true;
}
return false;
}
1
2
3
(1)对于一般的方法,直接进行Dump,然后返回ture,不需要继续运行
(2)对于特殊类型的壳,返回false,让其真正的运行起来
(3)运行过INVOKE_STATIC_RANGE,需要将程序真正的执行,然后再判断GOTO进行Dump

dumpMethod()函数:

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
void Unpacker::dumpMethod(ArtMethod *method, int nop_size) {
std::string dump_path = Unpacker::getMethodDumpPath(method);
int fd = -1;
if (Unpacker_method_fds_.find(dump_path) != Unpacker_method_fds_.end()) {
fd = Unpacker_method_fds_[dump_path];
}
else {
fd = open(dump_path.c_str(), O_RDWR | O_CREAT | O_APPEND, 0777);
if (fd == -1) {
ULOGE("open %s error: %s", dump_path.c_str(), strerror(errno));
return;
}
Unpacker_method_fds_[dump_path] = fd;
}

uint32_t index = method->GetDexMethodIndex();
std::string str_name = PrettyMethod(method);
const char* name = str_name.c_str();
const DexFile::CodeItem* code_item = method->GetCodeItem();
uint32_t code_item_size = (uint32_t)Unpacker::getCodeItemSize(method);

size_t total_size = 4 + strlen(name) + 1 + 4 + code_item_size;
std::vector<uint8_t> data(total_size);
uint8_t* buf = data.data();
memcpy(buf, &index, 4);
buf += 4;
memcpy(buf, name, strlen(name) + 1);
buf += strlen(name) + 1;
memcpy(buf, &code_item_size, 4);
buf += 4;
memcpy(buf, code_item, code_item_size);
if (nop_size != 0) {
memset(buf + offsetof(DexFile::CodeItem, insns_), 0, nop_size);
}

ssize_t written_size = write(fd, data.data(), total_size);
if (written_size > (ssize_t)total_size) {
ULOGW("write %s in %s %zd/%zu error: %s", PrettyMethod(method).c_str(), dump_path.c_str(), written_size, total_size, strerror(errno));
}
}

这里面进行了dex的重构,然后直接进行dump

Android加壳与脱壳(8)——Youpk脱壳机源码分析

switch解释完,这里再进行一次判断

1
2
3
4
5
6
7
8
9
10
11
12
bool Unpacker::afterInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) {
const uint16_t* const insns = method->GetCodeItem()->insns_;
const Instruction* inst = Instruction::At(insns + dex_pc);
uint16_t inst_data = inst->Fetch16(0);
Instruction::Code opcode = inst->Opcode(inst_data);
if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)
&& Unpacker::isRealInvoke(self, method)) {
Unpacker::enableFakeInvoke();
Unpacker::disableRealInvoke();
}
return false;
}

这里就看到每个指令都执行了PREAMBLE函数。然后每个指令执行完都执行了afterInstructionExecute这个函数。在这里就可以判断,如果执行完的指令是INVOKE_STATIC。就可以直接return结束掉函数执行了。

整体通过enableFakeInvoke和disableRealInvoke来控制下一个指令执行的时候来进行退出函数

2.6 还原fini()

Android加壳与脱壳(8)——Youpk脱壳机源码分析

将虚拟执行和真实执行,以及json一些参数恢复到起始状态,到这里整个脱壳机的源码则分析完成

3.总结

本文分析了抽取壳的自动脱壳机youpk的源码,为后续进一步学习加壳和脱壳做铺垫,相关资料存放知识星球和微信公众号。

- source:security-kitchen.com

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月4日16:15:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Android加壳与脱壳(8)——Youpk脱壳机源码分析https://cn-sec.com/archives/2919085.html

发表评论

匿名网友 填写信息