Android加壳与脱壳(8)——Youpk脱壳机源码分析
1.前言
今天来分析一下自动脱壳机Youpk的源码,Youpk是一个可以解决整体壳、抽取壳的脱壳机,可以解决自解密型的抽取壳。
2.源码分析
2.1 创建脱壳线程
frameworks/base/core/java/android/app/ActivityThread.java
函数:handleBindApplication
这里选择这个函数,是因为Android系统的执行流程,都要经过这个函数,且与FART的选择点有区别
art/runtime/unpacker/unpacker.cc
函数:unpack
我们可以依次看见4个功能模块:
(1)初始化
(2)dump所有dex
(3)主动调用所有方法
(4)还原
2.2 初始化init()
unpacker/unpacker.cc
在data/data/process
目录下新建三个文件夹dex
、method
、unpacker.json
getDexFiles:
内存中dump DEX,DEX文件在art虚拟机中使用DexFile对象表示, 而ClassLinker中引用了这些对象, 因此可以采用从ClassLinker中遍历DexFile对象并dump的方式来获取
注意:因为从内存中dump,所以无法针对动态加载的壳进行解决,因为这类壳需要完成类加载器的修正,而youpk就没有完成该部分的工作
另外, 为了避免dex做任何形式的优化影响dump下来的dex文件, 在dex2oat中设置 CompilerFilter 为仅验证
//dex2oat.cc
compiler_options_->SetCompilerFilter(CompilerFilter::kVerifyAtRuntime);
getAppClassLoader:
通过反射获得当前的类加载器
2.3 dump所有dex DumpAllDexs()
代码解析:
(1)对dexs文件进行Dump,根据begin和大小
(2)并且对抗了dex魔术字段被抹除的形式 可借鉴
(3)成功的保存 可改进
功能:对Dexfile的文件进行Dump
2.4 主动调用链invokeAllMethods()
代码解析:
(1)获得Dex的位置、dump路径、类大小
如果路径、位置、大小都相等,则说明是处理过后的dex
这里对init
进行处理
将一些dex的信息保存到Json中
遍历dex的所有ClassDef
读取一些函数的状态
对函数初始化进行处理,对未经过初始化的函数进行初始化
开启虚假的调用
对方法进行遍历,对非抽象方法或非静态方法进行模拟调用
关闭模拟调用开关,然后将状态进行修改
2.5 系统源码
artmethod.cc
Invoke()函数:修改
如果是主动调用,并且不是native方法就强制走解释器
如果是主动调用并且是native方法则不执行
interpreter.cc
EnterInterpreterFromInvoke()函数
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
uint32_t* args, JValue* result,
bool stay_in_interpreter) {
在这里面函数youpk并未进行处理
但是这里youpk强制修改了解释器:
Execute()函数:
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_) {
interpreter_switch_impl.cc
ExecuteSwitchImpl()函数:修改
JValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item,
ShadowFrame& shadow_frame, JValue result_register,
bool interpret_one_instruction) {
可以发现当程序进入Switch进行翻译后,翻译每条指令之前,都会执行PREAMBLE()方法,而Youpk就对这里进行了修改,这里与FARTExt区别的地方,就是这里是利用了解析器的判断:
PREAMBLE()函数:修改
在程序翻译之前都要使用beforeInstructionExecute
进行判断,这里就是Youpk深度调用链的地方,处理(NOP、Goto)等壳的地方,如果已经dump了就直接返回,这里程序结束,这里可以进行改进
unpacker.cc
beforeInstructionExecute()函数:修改
//继续解释执行返回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)对于一般的方法,直接进行Dump,然后返回ture,不需要继续运行
(2)对于特殊类型的壳,返回false,让其真正的运行起来
(3)运行过INVOKE_STATIC_RANGE,需要将程序真正的执行,然后再判断GOTO进行Dump
dumpMethod()函数:
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
switch解释完,这里再进行一次判断
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()
将虚拟执行和真实执行,以及json一些参数恢复到起始状态,到这里整个脱壳机的源码则分析完成
3.总结
本文分析了抽取壳的自动脱壳机youpk的源码,为后续进一步学习加壳和脱壳做铺垫,相关资料存放知识星球和微信公众号。