声明:案例分析仅供学习交流使用,勿用于任何非法用途。如学习者进一步逆向并对版权方造成损失,请自行承担法律后果,本人概不负责。
案例与目标
案例下载
jadx打开以后可以看到很明显的壳特征。
目标:使用工具和自行实现脱壳。
工具脱壳
FRIDA-DEXDump
FRIDA-DEXDump确实很轻松就拿到了dex文件,可惜文件数据有异常,无法正常打开分析。
frida-fart
通过frida-fart同样很轻松的拿到了dex文件,以首屏SplashActivity为例进行分析。
jadx可以正常打开,但里面的函数都是nop。
Fart脱壳机镜像
pixel刷入Fart脱壳机镜像,运行后在/sdcard/fart找到dex。
jadx打开,发现SplashActivity中函数是完整的。
Youpk脱壳机镜像
pixel刷入Youpk脱壳机镜像。 执行:
adb shell "echo com.ninemax.ncsearchnew >> /data/local/tmp/unpacker.config"
启动app等待10秒后在 /data/data/com.ninemax.ncsearchnew/unpacker 可找到dex。
jadx打开,发现SplashActivity中函数是完整的。
自行脱壳
OpenCommon
无论是InMemoryDexClassLoader还是DexClasslLoader,在动态加载dex时都会经过OpenCommon()与DexFile()两个函数,那么这两个函数都会是合适的脱壳点,这里以OpenCommon()为例进行修改:
// /aosp810r1/art/runtime/dex_file.cc
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
bool verify,
bool verify_checksum,
std::string* error_msg,
VerifyResult* verify_result) {
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifyNotAttempted;
}
std::unique_ptr<DexFile> dex_file(new DexFile(base,
size,
location,
location_checksum,
oat_dex_file));
if (dex_file == nullptr) {
*error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(),
error_msg->c_str());
return nullptr;
}
if (!dex_file->Init(error_msg)) {
dex_file.reset();
return nullptr;
}
if (verify && !DexFileVerifier::Verify(dex_file.get(),
dex_file->Begin(),
dex_file->Size(),
location.c_str(),
verify_checksum,
error_msg)) {
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifyFailed;
}
return nullptr;
}
if (verify_result != nullptr) {
*verify_result = VerifyResult::kVerifySucceeded;
}
//this is zyc code , dump method
//--------------------------------------------
const char* dirPath = "/sdcard/com.ninemax.ncsearchnew_zycdump";
if (access(dirPath, 0) == -1){ //dir is not exists
mkdir(dirPath, 0777);
}
char dexPath[100] = {0};
sprintf(dexPath, "%s/%d_%d_OpenCommon.dex",dirPath, getpid(),(int)dex_file->Size());
int fd = open(dexPath, O_CREAT|O_RDWR,666);
if(fd>0){
int ret = write(fd, dex_file->Begin(), (int)dex_file->Size());
if(ret>0){
ALOGD("write %s success!",dexPath);
}
close(fd);
}
//--------------------------------------------
return dex_file;
}
LoadClassMembers
如果不执行dex2oat流程,那么ClassLinker过程中的诸多函数(如LoadClassMembers()、LoadMethod()等)将变为脱壳点,这里以LoadClassMembers()为例进行修改:
// /aosp810r1/art/runtime/class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
// Note: We cannot have thread suspension until the field and method arrays are setup or else
// Class::VisitFieldRoots may miss some fields or methods.
ScopedAssertNoThreadSuspension nts(__FUNCTION__);
// Load static fields.
// We allow duplicate definitions of the same field in a class_data_item
// but ignore the repeated indexes here, b/21868015.
LinearAlloc* const allocator = GetAllocatorForClassLoader(klass->GetClassLoader());
ClassDataItemIterator it(dex_file, class_data);
LengthPrefixedArray<ArtField>* sfields = AllocArtFieldArray(self,
allocator,
it.NumStaticFields());
......
//this is zyc code , dump method
//--------------------------------------------
const char* dirPath = "/sdcard/com.ninemax.ncsearchnew_zycdump";
if (access(dirPath, 0) == -1){ //dir is not exists
mkdir(dirPath, 0777);
}
char dexPath[100] = {0};
sprintf(dexPath, "%s/%d_%d_LoadClassMembers.dex",dirPath, getpid(),(int)dex_file.Size());
int fd = open(dexPath, O_CREAT|O_RDWR,666);
if(fd>0){
int ret = write(fd, dex_file.Begin(), (int)dex_file.Size());
if(ret>0){
ALOGD("write %s success!",dexPath);
}
close(fd);
}
//--------------------------------------------
DCHECK(!it.HasNext());
}
// Ensure that the card is marked so that remembered sets pick up native roots.
Runtime::Current()->GetHeap()->WriteBarrierEveryFieldOf(klass.Get());
self->AllowThreadSuspension();
}
这里设置一下dex2oat中 CompilerFilter::Filter 为仅验证,可以避免dex做任何形式的优化影响dump下来的dex文件:
// /aosp810r1/art/dex2oat/dex2oat.cc
dex2oat::ReturnCode Setup() {
TimingLogger::ScopedTiming t("dex2oat Setup", timings_);
......
// If we need to downgrade the compiler-filter for size reasons.
if (!IsBootImage() && IsVeryLarge(dex_files_)) {
// Disable app image to make sure dex2oat unloading is enabled.
compiler_options_->DisableAppImage();
// If we need to downgrade the compiler-filter for size reasons, do that early before we read
// it below for creating verification callbacks.
if (!CompilerFilter::IsAsGoodAs(kLargeAppFilter, compiler_options_->GetCompilerFilter())) {
LOG(INFO) << "Very large app, downgrading to verify.";
// Note: this change won't be reflected in the key-value store, as that had to be
// finalized before loading the dex files. This setup is currently required
// to get the size from the DexFile objects.
// TODO: refactor. b/29790079
compiler_options_->SetCompilerFilter(kLargeAppFilter);
}
}
//this is zyc code , dump method
//--------------------------------------------
compiler_options_->SetCompilerFilter(CompilerFilter::kExtract);
//--------------------------------------------
......
return dex2oat::ReturnCode::kNoFailure;
}
值得注意的是Android各版本CompilerFilter::Filter有所不同,具体参考:
// /aosp810r1/art/runtime/compiler_filter.h
class CompilerFilter FINAL {
public:
// Note: Order here matters. Later filter choices are considered "as good
// as" earlier filter choices.
enum Filter {
kAssumeVerified, // Skip verification but mark all classes as verified anyway.
kExtract, // Delay verication to runtime, do not compile anything.
kVerify, // Only verify classes.
kQuicken, // Verify, quicken, and compile JNI stubs.
kSpaceProfile, // Maximize space savings based on profile.
kSpace, // Maximize space savings.
kSpeedProfile, // Maximize runtime performance based on profile.
kSpeed, // Maximize runtime performance.
kEverythingProfile, // Compile everything capable of being compiled based on profile.
kEverything, // Compile everything capable of being compiled.
};
......
}
Execute
ART模式下如不经过dex2oat编译,将使用Interpreter解释器(类的初始化函数默认始终由该解释器编译),Interpreter将会执行Execute(),而且该函数是内联函数,不容易被hook,结合上面阻断dex2oat流程部分,作出修改如下:
// /aosp810r1/art/runtime/interpreter/interpreter.cc
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_) {
DCHECK(!shadow_frame.GetMethod()->IsAbstract());
DCHECK(!shadow_frame.GetMethod()->IsNative());
//this is zyc code , dump method
//--------------------------------------------
ArtMethod* artMethod = shadow_frame.GetMethod();
if(strstr(artMethod->PrettyMethod().c_str(),"<clinit>")){ //这里要对函数或者进程进行一定的筛选,否则系统会非常卡!
const DexFile* dex_file = artMethod->GetDexFile();
const char* dirPath = "/sdcard/com.ninemax.ncsearchnew_zycdump";
if (access(dirPath, 0) == -1){ //dir is not exists
mkdir(dirPath, 0777);
}
char dexPath[100] = {0};
sprintf(dexPath, "%s/%d_%d_Execute.dex",dirPath, getpid(),(int)dex_file->Size());
int fd = open(dexPath, O_CREAT|O_RDWR,666);
if(fd>0){
int ret = write(fd, dex_file->Begin(), (int)dex_file->Size());
if(ret>0){
ALOGD("write %s success!",dexPath);
}
close(fd);
}
}
//--------------------------------------------
if (LIKELY(shadow_frame.GetDexPC() == 0)) { // Entering the method, but not via deoptimization.
if (kIsDebugBuild) {
self->AssertNoPendingException();
}
instrumentation::Instrumentation* instrumentation = Runtime::Current()->GetInstrumentation();
ArtMethod *method = shadow_frame.GetMethod();
if (UNLIKELY(instrumentation->HasMethodEntryListeners())) {
instrumentation->MethodEnterEvent(self, shadow_frame.GetThisObject(code_item->ins_size_),
method, 0);
if (UNLIKELY(self->IsExceptionPending())) {
instrumentation->MethodUnwindEvent(self,
shadow_frame.GetThisObject(code_item->ins_size_),
method,
0);
return JValue();
}
}
......
}
结果
刷入脱壳镜像,运行可以看到各脱壳点都dump出了dex:
对dump出的dex进行分析,发现LoadClassMembers和Execute两个点能正常获取到大部分源码(小部分类依旧nop),原因是我们在上面的代码中实现的是被动脱壳,没有调用到的地方依旧可能为nop,要达到更好的效果需要构建主动调用链。而OpenCommon在得到DexFile时早于App对dex的回填,内容依旧全是nop。
结论
无论是工具脱壳,还是自行脱壳,最根本的思想还是在dex加载流程中进行dump,但脱壳的时机不同,效果也会不同。由此次脱壳也不难看出,对于函数粒度的壳,结合了“脱壳点+主动调用”的Fart脱壳机镜像与Youpk脱壳机镜像效果较好,在dex2oat流程中被动调用脱壳效果其次,而在获取dex却早于回填时整体脱壳几乎无效。
相关资料
ART环境下基于主动调用的自动化脱壳方案
Youpk: 又一款基于ART的主动调用的脱壳机
AUPK:基于Art虚拟机的脱壳机