越努力越幸运 —— 【看雪高研班】一颗金柚子访谈录
A:大家好,很高兴在这里与大家分享我的学习经历与收获。我目前是一名研究生,今天到报名高研班已经有一年多了,回想起来像是昨天刚发生过的事情(笑)。本科期间,我没有好好学习专业知识,基础不太扎实,好在研究生阶段才慢慢捡起来。
学习永远不怕晚,进步永远不迟到
越努力越幸运
书山有路勤为径,学海无涯苦作舟
本文为看雪论坛精华文章
看雪论坛作者ID:一颗金柚子
1
前言
②fartwithClassLoader-(反射获取mCookie)>
③loadClassAndInvoke-(dumpMethodCode将各种函数转化成ArtMethod类型并送入我们的fake_Invoke参数包装)>
④送入系统的Invoke-(调用dumpArtMethod实现第二个脱壳点)。
Fart主动调用前提:
②通过ClassLoader加载到所有类 ;
③通过每个类获取到该类下的所有方法【包括构造函数和普通函数】。
//interpreter_switch_impl.cc
// Code to run before each dex instruction.
#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)
2
实验
extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod) {
ScopedFastNativeObjectAccess soa(env);
ArtMethod* method = ArtMethod::FromReflectedMethod(soa, javaMethod);
//add
ObjPtr<mirror::Class> declaring_class = method->GetDeclaringClass();
if (UNLIKELY(!declaring_class->IsInitialized())) {
StackHandleScope<1> hs(soa.Self());
HandleWrapperObjPtr<mirror::Class> h_class(hs.NewHandleWrapper(&declaring_class));
if (!Runtime::Current()->GetClassLinker()->EnsureInitialized(soa.Self(), h_class, true, true)) {
return nullptr;
}
}
//addend
return method;
}
对其他壳还好,但是在实际某款App的测试中发现这个初始化会引起一些问题(经研究发现这个壳当看到如果对这些activities中的关键类进行初始化就对程序进行了一种退出保护)。
在这种情况下Youpk可能就失去了后续所有步骤的进行,因此初始化这里还需要借鉴FART的策略:通过壳最后的ClassLoader,经过反射思路使用不带初始化过程的loadClass加载类,并且这里要在Java层中进行,方便将类进行传递和储存。
根据Youpk的思路,构造完整的主动调用链。
从LinkCode源码中可以知道,无论一个类方法是通过解释器执行,还是直接以本地机器指令执行,均可以通过ArtMethod类的成员函数GetEntryPointFromCompiledCode获得其入口点,并且该入口不为NULL。
不过,Invoke并没有直接调用该入口点,而是通过Stub来间接调用。 这是因为ART需要设置一些特殊的寄存器。同时,ArtMethod::Invoke会在其中判断需要执行的函数时运行在什么模式的,①解释模式②Quick模式③JNI函数。
我们要恢复的函数肯定是会在解释模式下执行的【这个原因我会在后面讲到】,因此我们只关注这一部分区域。
首先判断是否是Native化的函数,如果是则主动调用没有意义,反之,则进入主动调用连。
//art/runtime/art_method.cc
//ArtMethod::Invoke
if (Callflag == 5201314){
const DexFile::CodeItem* code_item= this->GetCodeItem();
if(LIKELY(code_item != nullptr)){
ManagedStack fragment;
self->PushManagedStackFragment(&fragment);
if(IsStatic()){
art::interpreter::EnterInterpreterFromInvoke(
self, this, nullptr, args, result, false);
LOG(ERROR)<<"EnterInterpreterFromInvoke is ok!";
}else{
art::interpreter::EnterInterpreterFromInvoke(
self, this, nullptr, args+1, result, false);
LOG(ERROR)<<"EnterInterpreterFromInvoke is ok!";
}
self->PopManagedStackFragment(fragment);
}
return;
}
}
//addend
在经过构造失败多次后发现,EnterInterpreterInvoke这个函数在它的逻辑中有个不得不提的操作就是它会通过SetVRegReference去追溯参数内容,因此如果参数不合法,会造成异常。
271 void SetVReg(size_t i, int32_t val) {
272 DCHECK_LT(i, NumberOfVRegs());
273 uint32_t* vreg = &vregs_[i];
274 *reinterpret_cast<int32_t*>(vreg) = val;
275 // This is needed for moving collectors since these can update the vreg references if they
276 // happen to agree with references in the reference array.
277 if (kMovingCollector && HasReferenceArray()) {
278 References()[i].Clear();
279 }
280 }
bool ClassLinker::ShouldUseInterpreterEntrypoint(ArtMethod* method, const void* quick_code) {
if (UNLIKELY(method->IsNative() || method->IsProxyMethod())) {
return false;
}
if (quick_code == nullptr) {
return true;
}
Runtime* runtime = Runtime::Current();
instrumentation::Instrumentation* instr = runtime->GetInstrumentation();
if (instr->InterpretOnly()) {
return true;
}
if (runtime->GetClassLinker()->IsQuickToInterpreterBridge(quick_code)) {
// Doing this check avoids doing compiled/interpreter transitions.
return true;
}
if (Dbg::IsForcedInterpreterNeededForCalling(Thread::Current(), method)) {
// Force the use of interpreter when it is required by the debugger.
return true;
}
....
}
quick_code == nullptr
instr->InterpretOnly()
IsQuickToInterpreterBridge(quick_code)
……
当上面这几个条件有一个满足时,ShouldUseInterpreterEntrypoint 就会返回 true,使用 Interpreter 模式
到这里可能会有个疑问:quick_code那又是怎么获取到的呢?
const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
//机器码地址为空或者是调试状态等,需要解释模式
bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);
if (method->IsStatic() && !method->IsConstructor()) {
//静态方法且不是类初始化"<clinit>"方法,设置入口地址为art_quick_resolution_trampoline
//跳转到artQuickResolutionTrampoline函数。该函数和类的解析有关
//初始化完毕后会调用FixupStaticTrampolines()来更新入口地址为正确的机器码。
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
} else if (quick_code == nullptr && method->IsNative()) {
//jni方法,设置入口地址为art_quick_generic_jni_trampoline,跳转到artQuickGenericJniTrampoline函数。
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
} else if (enter_interpreter) {
//解释执行,设置入口地址为art_quick_to_interpreter_bridge,跳转到artQuickToInterpreterBridge函数。
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
}
在LinkCode中,第一句代码就是quick_code的获取方式,它使用了GetEntryPointFromQuickCompiledCode方法。ArtMethod对象第一次链接的时候肯定是需要运行在解释模式下的,因为在进入LinkCode之前没有其他函数会执行SetEntryPointFromQuickCompiledCode这个步骤,因此这里是设置入口点的起始位置。
那么,进入解释模式的判断点ShouldUseInterpreterEntrypoint中quick_code为nullptr只有可能是一个函数第一次执行的时候。因此当对象第二次经过这个判断时,就会返回false,并经过流程再次路过ArtMethod::Invoke然后进入Quick模式。
Fupk3中F8大佬曾在论坛提示过,针对“有的函数抽取加固,是在原函数中插入一些还原相关的新指令,要真正执行时才能真正还原”这样的情况时:预读取一条指令,判断是否是壳的解密代码,如果是的话需要执行完再dump method。
那么问题一就来了,什么才是已还原的指令该有的第一条指令呢?
可以看到,正常的指令正如上图所展示的那样,有很多的invoke-xxx指令,也有很多参数的赋值。由于有多样的特征我们不好定量,我们可以反其道而行之,可以观察壳中的第一条指令是什么,如果是像这样的壳:
我们就可以判断执行的时候第一条指令是否为goto相关指令,如果是则需要真正执行这个流程,若不是可以直接dump method。
那么问题二就来了:怎么真正执行goto以后的这个流程?
可以看到上图的goto 002f是强转到002f的位置上进行参数的赋值,随后进入invoke-static中调用,invoke-xxxx是开启下一个调用的“跳板”,我们针对这个指令做处理,令其执行完后dump,不再执行后面的goto 0001的操作。
下面是invoke-static指令相关代码:
case Instruction::INVOKE_STATIC: {
PREAMBLE();
bool success = DoInvoke<kStatic, false, do_access_check>(
self, shadow_frame, inst, inst_data, &result_register);
POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
break;
}
因此我们可以如下进行改动:
//针对每一种dex指令处理。每种dex之前,都有一个PREAMBLE宏,就是调用instrumentation的DexPcMovedEvent函数。
case Instruction::INVOKE_STATIC: {
PREAMBLE();//这里保留
//修改
//调用DoInvoke,这个函数是真正执行invoke-static逻辑的关键。
bool success = DoInvoke<kStatic, false, false>(
self, shadow_frame, inst, inst_data, &result_register);
//POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
//执行完就返回到主动调用链入口处等待下一个函数
return JValue();
}
而据Youpk作者的思路,PREAMBLE()所用到的beforeInstructionExecute在art_method.cc中我原本是这么写的:
//addYoupk
extern "C" bool beforeInstructionExecute(ArtMethod *method, uint32_t dex_pc, int count_ins, int Callflag ) REQUIRES_SHARED(Locks::mutator_lock_) {
//判断主动调用链标识
if (Callflag == 5201314 && count_ins > 2) {
dumpArtMethod(method);
return true;
}
return false;
}
//addYoupkend
我们现将这个代码“提”出来,放在ExecuteSwitchImpl进入Switch Opcode选择之前,做预读一条代码再做是否dump的决定。
但是在这之前我们可以学习一下源码是怎么提取指令的:
do {
dex_pc = inst->GetDexPc(insns);//表示计步器,等于几就是第几+1条指令,从0开始
shadow_frame.SetDexPC(dex_pc);
TraceExecution(shadow_frame, inst, dex_pc);
inst_data = inst->Fetch16(0);
switch (inst->Opcode(inst_data))
....
后面就是各种Opcode case语句
明白了,那就可以有【如果是针对goto指令->Invoke-static指令的话】
if(dex_pc == 0){
//直接把case紧挨着的Instruction指令粘贴过来
if(inst->Opcode(inst_data)!=Instruction::GOTO){
//如果不是,则直接执行beforeInstructionExecute的核心dump代码
if (Callflag == 5201314) {
dumpArtMethod(method);
return JValue();
}
}
①method_idx为方法B在dex文件里method_ids数组中的索引
②找到方法B对应的对象。它作为参数存储在方法A的ShadowFrame对象中。
③sf_method代表ArtMethod* A
④FindMethodFromCode查找目标方法B对应的ArtMethod对象,即ArtMethod* B。
template<InvokeType type, bool is_range, bool do_access_check>static inline bool DoInvoke(Thread* self,
ShadowFrame& shadow_frame,
const Instruction* inst,
uint16_t inst_data,
JValue* result) {
//method_idx为方法B在dex文件里method_ids数组中的索引
const uint32_t method_idx = (is_range) ? inst->VRegB_3rc() : inst->VRegB_35c();
//找到方法B对应的对象。它作为参数存储在方法A的ShadowFrame对象中。。
const uint32_t vregC = (is_range) ? inst->VRegC_3rc() : inst->VRegC_35c();
ObjPtr<mirror::Object> receiver =
(type == kStatic) ? nullptr : shadow_frame.GetVRegReference(vregC);
//sf_method代表ArtMethod* A
ArtMethod* sf_method = shadow_frame.GetMethod();
//FindMethodFromCode查找目标方法B对应的ArtMethod对象,即ArtMethod* B。
ArtMethod* const called_method = FindMethodFromCode<type, do_access_check>(
method_idx, &receiver, sf_method, self);
if (UNLIKELY(called_method == nullptr)) {
} else if (UNLIKELY(!called_method->IsInvokable())) {
} else {
...
return DoCall<is_range, do_access_check>(called_method, self, shadow_frame, inst, inst_data,
result);
}}
DoCall又进一步调用DoCallCommon,DoCallCommon又在return中调用PerformCall。
inline void PerformCall(Thread* self,
const CodeItemDataAccessor& accessor,
ArtMethod* caller_method,
const size_t first_dest_reg,
ShadowFrame* callee_frame,
JValue* result,
bool use_interpreter_entrypoint)
REQUIRES_SHARED(Locks::mutator_lock_) {
if (LIKELY(Runtime::Current()->IsStarted())) {
if (ShouldUseInterpreterEntrypoint) {
//解释模式,debug或者机器码不存在,调用ArtInterpreterToInterpreterBridge
interpreter::ArtInterpreterToInterpreterBridge(self, accessor, callee_frame, result);
} else {
//ArtMethod第二次经过时
//以机器码执行方法B,调用ArtInterpreterToCompiledCodeBridge
interpreter::ArtInterpreterToCompiledCodeBridge(
self, caller_method, callee_frame, first_dest_reg, result);
}
} else {
interpreter::UnstartedRuntime::Invoke(self, accessor, callee_frame, result, first_dest_reg);
}}
结合我上面贴的图分析,之后会经过ShouldUseInterpreterEntrypoint进行判断quick_code是否为null,由于当前的ArtMethod已经进入过一次解释器,因此再次碰到这个判断时,此时的quick_code已经被LinkCode设置好,会走else分支,调用ArtInterpreterToCompiledCodeBridge函数并重新进入ArtMethod::Invoke:
void ArtInterpreterToCompiledCodeBridge(Thread* self,
ArtMethod* caller,
ShadowFrame* shadow_frame,
uint16_t arg_offset,
JValue* result)
REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod* method = shadow_frame->GetMethod();
EnsureInitialized();
//又回到最初FART进来的Invoke,且此时Invoke最终需要调用到entry_point_from_quick_compiled_code_完成真实调用
method->Invoke(self, shadow_frame->GetVRegArgs(arg_offset),
(shadow_frame->NumberOfVRegs() - arg_offset) * sizeof(uint32_t),
result, method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetShorty());}
由于我们又回到了art_method.cc,因此很容易直接调用FART的最终dump的API(和之前写的做好区分工作):
if(fromExecuteSwitch){
if (!IsStatic()) {
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
}else{
(*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
}
//在真实调用结束后马上dump
dumpArtMethod(this);
return;
}
至此,结合Youpk启发的思路,加上寒冰大佬不断深入讲解的FART,一个完整的主动调用链就构造完成。之后的启发是无穷的,基本上又可以解决另一类函数抽取壳。
关于动态加载Dex的脱壳:
3
结论
学员优秀作品:
《单纯使用Frida书写类抽取脱壳工具的一些心路历程和实践》:https://bbs.pediy.com/thread-261197.htm
《将FART和Youpk结合来做一次针对函数抽取壳的全面提升》:https://bbs.pediy.com/thread-260052.htm
抖音搜索看雪学院,抖音号:ikanxue,观看更多高研班知识点片段分享,记得点赞关注收藏三连噢~~
《Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年秋季班开始招生!》:https://bbs.pediy.com/thread-267018.htm
看雪ID:一颗金柚子
https://bbs.pediy.com/user-879358.htm
*本文由看雪论坛 一颗金柚子 原创,转载请注明来自看雪社区。
﹀
球分享
球点赞
球在看