Youpk 脱壳机脱壳原理分析
Youpk 是一个针对整体加固和Dex抽取加固壳的脱壳机
主要是基于虚拟机的,也就是基于VA的脱壳机, 相对FART出来的更晚一些, 厂商针对少一些, 脱壳位置相对更底层一些,还提供了Dex修复的工具,简直棒棒
1. 先分析整体脱壳的原理
在ActivityThread 的 handleBindApplication 中增加了代码
也就是说,在应用的启动流程中,在makeApplication后,就开始干活
Unpacker.java -> unpack
public static void unpack() {
if (Unpacker.unpackerThread != null) {
return;
}
if (!shouldUnpack()) {
return;
}
//开启线程调用
Unpacker.unpackerThread = new Thread() {
@Override public void run() {
while (true) {
try {
Thread.sleep(UNPACK_INTERVAL);
}
catch (InterruptedException e) {
e.printStackTrace();
}
Unpacker.unpackNative();
}
}
};
Unpacker.unpackerThread.start();
}
这里开启一个线程,每一段时间就执行一下native的unpackNative
对应的是unpacker.cc
//注册native方法
static void Unpacker_unpackNative(JNIEnv*, jclass) {
Unpacker::unpack();
}
....
void Unpacker::unpack() {
ScopedObjectAccess soa(Thread::Current());
ULOGI("%s", "unpack begin!");
//1. 初始化
init();
//2. dump所有dex
dumpAllDexes();
//3. 主动调用所有方法
invokeAllMethods();
//4. 还原
fini();
ULOGI("%s", "unpack end!");
}
init()
主要是初始化工作,比如建立dump的目录,寻找需要dump的dex
void Unpacker::init() {
Unpacker_fake_invoke_ = false;
Unpacker_self_ = Thread::Current();
Unpacker_dump_dir_ = getDumpDir();
mkdir(Unpacker_dump_dir_.c_str(), 0777);
Unpacker_dex_dir_ = getDumpDir() + "/dex";
mkdir(Unpacker_dex_dir_.c_str(), 0777);
Unpacker_method_dir_ = getDumpDir() + "/method";
mkdir(Unpacker_method_dir_.c_str(), 0777);
Unpacker_json_path_ = getDumpDir() + "/unpacker.json";
Unpacker_json_fd_ = -1;
Unpacker_json_fd_ = open(Unpacker_json_path_.c_str(), O_RDWR | O_CREAT, 0777);
if (Unpacker_json_fd_ == -1) {
ULOGE("open %s error: %s", Unpacker_json_path_.c_str(), strerror(errno));
}
Unpacker_json_ = parseJson();
if (Unpacker_json_ == nullptr) {
Unpacker_json_ = createJson();
}
CHECK(Unpacker_json_ != nullptr);
Unpacker_dex_files_ = getDexFiles();
Unpacker_class_loader_ = getAppClassLoader();
}
Unpacker_dex_files_ 在这里进行了寻找和赋值的操作
std::list<const DexFile*> Unpacker::getDexFiles() {
std::list<const DexFile*> dex_files;
Thread* const self = Thread::Current();
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
ReaderMutexLock mu(self, *class_linker->DexLock());
const std::list<ClassLinker::DexCacheData>& dex_caches = class_linker->GetDexCachesData();
for (auto it = dex_caches.begin(); it != dex_caches.end(); ++it) {
ClassLinker::DexCacheData data = *it;
const DexFile* dex_file = data.dex_file;
const std::string& dex_location = dex_file->GetLocation();
if (dex_location.rfind("/system/", 0) == 0) {
continue;
}
dex_files.push_back(dex_file);
}
return dex_files;
}
这里通过RunTime 拿到class_linker,然后通过classLinker来获得所有的Dex的指针(看得出作者对虚拟机有比较深的研究)
dumpAllDexes();
就是我们整体dump的逻辑所在
void Unpacker::dumpAllDexes() {
for (const DexFile* dex_file : Unpacker_dex_files_) {
std::string dump_path = getDexDumpPath(dex_file);
if (access(dump_path.c_str(), F_OK) != -1) {
ULOGI("%s already dumped, ignored", dump_path.c_str());
continue;
}
const uint8_t* begin = dex_file->Begin();
size_t size = dex_file->Size();
int fd = open(dump_path.c_str(), O_RDWR | O_CREAT, 0777);
if (fd == -1) {
ULOGE("open %s error: %s", dump_path.c_str(), strerror(errno));
continue;
}
std::vector<uint8_t> data(size);
memcpy(data.data(), "dex\n035", 8);
memcpy(data.data() + 8, begin + 8, size - 8);
size_t written_size = write(fd, data.data(), size);
if (written_size < size) {
ULOGW("fwrite %s %zu/%zu error: %s", dump_path.c_str(), written_size, size, strerror(errno));
}
close(fd);
ULOGI("dump dex %s to %s successful!", dex_file->GetLocation().c_str(), dump_path.c_str());
}
}
整体dump最终把数据写入到了.dex文件中(还做了一个dex文件前缀魔数修复)
2. 再看对抽取壳的处理
首先是构建主动调用链,来欺骗壳,使壳进行函数指令填充
对应的就是 unpack方法中的第三步
//3. 主动调用所有方法
invokeAllMethods();
注意标志的六种状态
//dump类的六种status:
//Ready: 该类准备dump
//Resolved: ResolveClass成功
//ResolveClassFailed: ResolveClass失败
//Inited: EnsureInitialized成功
//EnsureInitializedFailed: EnsureInitialized失败
//Dumped: dump所有method成功
整体来说分两步,
一: 往unpacker.json里写每个方法的关键元数据
...
if (dex == nullptr) {
dex = cJSON_CreateObject();
cJSON_AddStringToObject(dex, "location", dex_file->GetLocation().c_str());
cJSON_AddStringToObject(dex, "dump_path", getDexDumpPath(dex_file).c_str());
cJSON_AddNumberToObject(dex, "class_size", dex_file->NumClassDefs());
current = cJSON_AddObjectToObject(dex, "current");
cJSON_AddNumberToObject(current, "index", class_idx);
cJSON_AddStringToObject(current, "descriptor", dex_file->GetClassDescriptor(dex_file->GetClassDef(class_idx)));
cJSON_AddStringToObject(current, "status", "Ready");
failures = cJSON_AddArrayToObject(dex, "failures");
cJSON_AddItemToArray(dexes, dex);
}
...
记录着dex的位置,dex整体dump下来的位置,有多少个class,class的id等等数据.方便后续codeitem.bin和整体dump的dex进行融合的操作
二: 构造参数发起主动调用
std::string Unpacker::getMethodDumpPath(ArtMethod* method) {
CHECK(method->GetDeclaringClass() != nullptr) << method;
const DexFile& dex_file = method->GetDeclaringClass()->GetDexFile();
std::string dex_location = dex_file.GetLocation();
size_t size = dex_file.Size();
//替换windows文件不支持的字符
for (size_t i = 0; i < dex_location.length(); i++) {
if (dex_location[i] == '/' || dex_location[i] == ':') {
dex_location[i] = '_';
}
}
std::string dump_path = Unpacker_method_dir_ + "/" + dex_location;
dump_path += StringPrintf("_%zu_codeitem.bin", size);
return dump_path;
}
从这里可以看出,函数的元数据写入到unpacker.json
,而函数的codeItem(即指令数据),写入到了xxx_codeitem.bin
的文件中,方便后续函数修复使用
三 获得 classDef后发起对class所有方法的主动调用()
// 前面还有一步主动初始化,
...
size_t pointer_size = class_linker->GetImagePointerSize();
auto methods = klass->GetDeclaredMethods(pointer_size);
Unpacker::enableFakeInvoke();
for (auto& m : methods) {
ArtMethod* method = &m;
if (!method->IsProxyMethod() && method->IsInvokable()) {
uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(method->GetShorty());
if (!method->IsStatic()) {
args_size += 1;
}
JValue result;
std::vector<uint32_t> args(args_size, 0);
if (!method->IsStatic()) {
mirror::Object* thiz = klass->AllocObject(self);
args[0] = StackReference<mirror::Object>::FromMirrorPtr(thiz).AsVRegValue();
}
// 重点这里
method->Invoke(self, args.data(), args_size, &result, method->GetShorty());
}
}
Unpacker::disableFakeInvoke();
cJSON_ReplaceItemInObject(current, "status", cJSON_CreateString("Dumped"));
writeJson();
...
四 发起invoke后,会走到java解释器中(youpk 强制走switch解释器), youpk修改了其中的一个宏
interpreter_switch_impl.cc
#define PREAMBLE() \
do { \
inst_count++; \
bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(), \
dex_pc, inst_count); \
if (dumped) { \
return JValue(); \
} \
if (UNLIKELY(instrumentation->HasDexPcListeners())) { \
instrumentation->DexPcMovedEvent(self, shadow_frame.GetThisObject(code_item->ins_size_), \
shadow_frame.GetMethod(), dex_pc); \
} \
} while (false)
这个宏在每个指令执行前都会调用,那么就一定会执行到 Unpacker::beforeInstructionExecute
,在这里发起了对method的codeitem的dump操作
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;
}
从这里可以看到,它即可一脱一般的抽取壳,也可以脱那种goto类型(ijiami, najia)的抽取壳,最终会走到
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));
}
}
这里就是把数据按照固定的格式把数据写入到.bin文件中
脱壳完成
3. dex修复
一 adb pull出dump文件, dump文件路径为 /data/data/包名/unpacker
adb pull /data/data/xxx.xxx.myxxxdemo/unpacker
二 调用修复工具 dexfixer.jar, 两个参数, 第一个为dump文件目录(必须为有效路径), 第二个为重组后的DEX目录(不存在将会创建)
youpk 比较爽的就是这里提供了修复的jar(还有源码),而fart的只是一个修复对比文件,未真正修复到dex中
java -jar dexfixer.jar /path/to/unpacker /path/to/output
完成dex的修复
Youpk 脱壳机脱壳原理分析的更多相关文章
- android脱壳之DexExtractor原理分析[zhuan]
http://www.cnblogs.com/jiaoxiake/p/6818786.html内容如下 导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的 ...
- android脱壳之DexExtractor原理分析
导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的原理,使用这种方法脱壳的有2个缺点: 1. 需要动态调试 2. 对抗反调试方案 为了提高工作效率, ...
- android黑科技系列——修改锁屏密码和恶意锁机样本原理分析
一.Android中加密算法 上一篇文章已经介绍了Android中系统锁屏密码算法原理,这里在来总结说一下: 第一种:输入密码算法 将输入的明文密码+设备的salt值,然后操作MD5和SHA1之后在转 ...
- DexHunter在Dalvik虚拟机模式下的脱壳原理分析
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78494671 在前面的博客<DexHunter的原理分析和使用说明(一)&g ...
- drizzleDumper的原理分析和使用说明
https://blog.csdn.net/qq1084283172/article/details/53561622 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog. ...
- android 脱壳 之 dvmDexFileOpenPartial断点脱壳原理分析
android 脱壳 之 dvmDexFileOpenPartial断点脱壳原理分析 导语: 笔者主要研究方向是网络通信协议的加密解密, 对应用程序加固脱壳技术很少研究, 脱壳壳经历更是经历少之甚少. ...
- Android FART脱壳机流程分析
本文首发于安全客 链接:https://www.anquanke.com/post/id/219094 0x1 前言 在Android平台上,程序员编写的Java代码最终将被编译成字节码在Androi ...
- DexHunter在ART虚拟机模式下的脱壳原理分析
本文博客地址: http://blog.csdn.net/qq1084283172/article/details/78494620 DexHunter脱壳工具在Dalvik虚拟机模式下的脱壳原理分析 ...
- APK加固之静态脱壳机编写入门
目录: 0x00APK加固简介与静态脱壳机的编写思路 1.大家都知道Android中的程序反编译比较简单,辛苦开发出一个APK轻易被人反编译了,所以现在就有很多APK加固的第三方平台,比如爱加密和梆梆 ...
- 菜鸟脱壳之脱壳的基础知识(二) ——DUMP的原理
菜鸟脱壳之脱壳的基础知识(二)——DUMP的原理当外壳的执行完毕后,会跳到原来的程序的入口点,即Entry Point,也可以称作OEP!当一般加密强度不是很大的壳,会在壳的末尾有一个大的跨段,跳向O ...
随机推荐
- [转帖]线上一个隐匿 Bug 的复盘
前言 之前负责的一个项目上线好久了,最近突然爆出一 Bug,最后评估影响范围将 Bug 升级成了故障,只因为影响的数据量有 10000 条左右,对业务方造成了一定的影响. 但因为不涉及到资金损失,Bu ...
- Redis scan等命令的学习与研究
Redis scan等命令的学习与研究 摘要 背景跟前几天说的一个问题类似. 为了验证自己的设想, 所以晚上继续写脚本进行了一轮次的验证. 不过上次讨论时,打击好像都没听懂我说的 所以这次准备从基础开 ...
- Oracle表数量对数据泵备份恢复速度的影响情况
Oracle表数量对数据泵备份恢复速度的影响情况 背景 随着公司产品交付后的时间越来越久. 数据库的备份恢复速度会越来越慢. 最开始一直认为是因为数据量导致的. 但是最近发现, 如果只是将数据库表的量 ...
- MySQL备份恢复简单处理方法
客户备份恢复的脚本处理简要如下: 首先登陆mysql服务器 方法如下: mysql -uroot -p 输入密码即可登陆 然后需要创建一个数据库, 个人感觉同名恢复最容易出问题 create data ...
- element-ui中Select 选择器value-key的使用
场景描述 很多时候我们都需要使用下拉框 Select 选择器. 在获取值的时候,通常只需要传递对应的id给后端就行了. 但是特殊情况,后端不想去查库,不仅需要我们id,还有name,code之类的. ...
- vuex4的简单使用
安装vuex cnpm install vuex@next --save 官网地址是 https://vuex.vuejs.org/zh/guide/#%E6%9C%80%E7%AE%80%E5%8D ...
- 手写Promise自定义封装 then 函数
Promise 自定义封装 then 函数 <script src="./Promise.js"></script> <script type=&qu ...
- 【JS 逆向百例】Fiddler 插件 Hook 实战,某创帮登录逆向
关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途 ...
- NextJs 与 Tailwind 入门开发笔记
前言 距离上次更新已经过去好久了,之前我在 StarBlog 博客2023年底更新一览的文章里说要使用 Next.js 来重构博客前端,最近也确实用 next.js 做了两个小项目,一个是单点认证项目 ...
- 根据TxID获取上链数据
根据TxID获取上链信息 前段时间应甲方爸爸的要求,需要在现有的业务系统中新增一个根据TxID来查询上链信息的接口.搜了一圈发现相关的信息很少,最后只能祭出终极大招:Read Source Code. ...