PAGalaxyLab / YAHFA

Yet Another Hook Framework for ART
GNU General Public License v3.0
1.56k stars 347 forks source link

Fix crash on Android 11 #133

Closed kotori2 closed 3 years ago

kotori2 commented 3 years ago

Fix #132 #130 (probably) 鉴于某谜语壬宁愿在issue里面讲故事也不发PR,那我自己研究下好了 Since someone figured out but won't send a PR, I'll do it.

From the crash trace, it tries to call artQuickGenericJniTrampoline and art_quick_generic_jni_trampoline, which will read data_ from ArtMethod instead of entry_point_from_quick_compiled_code_, which will get null pointer and it won't properly throw a Exception. But according to the Changelog, it might cause hook fail on Android O+, but it seems working for me with even debug build.

Credits: @yujincheng08

canyie commented 3 years ago

@rk700 进入jni方法时会切换线程状态到kNative,从jni直接跳转到method entry感觉会因为线程状态不对有问题 (能想到的场景就是moving gc时需要除native外的所有线程挂起,不会等这个看起来在native其实在running的线程,可能会造成各种奇奇怪怪的问题)

rk700 commented 3 years ago

嗯,感觉还是比想象的要复杂的多,如果方法flag改成fast native是否可以,这样会调用JniMethodFastStart而不是JniMethodStart,不知道是不是你说的那里。

我看FixupStaticTrampoline的时候,如果方法有oat code则更新入口为oat code,这种情况下也不会再走jni了,想通过jni还原的方式也就触发不了

如果主动调用MakeInitializedClassesVisiblyInitialized,是否就是从libart.so拿到符号就行,有什么坑需要注意吗

yujincheng08 commented 3 years ago

没有延迟hook的话,MakeInitializedClassesVisiblyInitialized 没有啥坑。

此外,最近yahfa把备份方案改为 只修改entrypoint,会导致 GetOatQuickMethodHeader 崩溃,@canyie 说这和 https://github.com/PAGalaxyLab/YAHFA/wiki/%E6%9B%B4%E6%96%B0%E8%AF%B4%E6%98%8E#a929fce-%E6%9B%B4%E6%96%B0%E8%AF%B4%E6%98%8E 这里有关系。看了一下确实类似。不知道还有没有什么其他思路?

yujincheng08 commented 3 years ago

@google-mirror https://github.com/PAGalaxyLab/YAHFA/pull/133#issuecomment-733874546 这里也说了是Fixup的问题,这都理解不了?

因为不是visiblyinitialized,所以R上会在调用若干次后,重新fixup这个static方法,导致entrypoint被reset。如果还设置了native,会导致reset成jni的trampoline然后读了一个null的data_然后crash。所以要手动调用makevisiblyinitialzed。懂了吗?

这里是技术讨论,不是让你问问题的地方。

还在edxp的issue里面点踩。。。

kotori2 commented 3 years ago

@google-mirror 其实你连epic的备注也没看

    /**
     * the static method is lazy resolved, when not resolved, the entry point is a trampoline of
     * a bridge, we can not hook these entry. this method force the static method to be resolved.
     */
google-mirror commented 3 years ago

@google-mirror 其实你连epic的备注也没看

    /**
     * the static method is lazy resolved, when not resolved, the entry point is a trampoline of
     * a bridge, we can not hook these entry. this method force the static method to be resolved.
     */

是的, 我就看了这个issue, 我错了, 大佬

rk700 commented 3 years ago

GetOatQuickMethodHeader的那个问题,我看下来似乎是这里造成的? https://cs.android.com/android/platform/superproject/+/master:art/runtime/art_method.cc;l=592

我们把 target 的 entry 替换成 hookTrampoline 了,从这个新的 entry 直接往前计算偏移得到 OatQuickMethodHeader https://cs.android.com/android/platform/superproject/+/master:art/runtime/oat_quick_method_header.h;l=47;drc=master

这个所谓的OatQuickMethodHeader肯定是不对的,导致计算 Contains(pc) 出错

那么我们是否可以在 hookTrampoline 前构造一个假的 OatQuickMethodHeader?让其 code_size 为0,使得Contains(pc) 不会挂掉且返回 false,继续从原始的 oat file 获取真正的 entry 再计算 dexpc https://cs.android.com/android/platform/superproject/+/master:art/runtime/art_method.cc;l=620

yujincheng08 commented 3 years ago

@rk700 我也是这个思路。在trampoline前面多放allocate一点空间,使得 OatQuickMethodHeader 指针正确并且能读出一个 0 的code_size。目前正在测试中

kotori2 commented 3 years ago

在trampoline前面多放allocate一点空间

其实可以构造一个mov r0, #0 这样的东西吧,不过我现在是直接偷懒把GetCodeSize给hook掉了

rk700 commented 3 years ago

在trampoline前面多放allocate一点空间

其实可以构造一个mov r0, #0 这样的东西吧,不过我现在是直接偷懒把GetCodeSize给hook掉了

mov r0, #0 是什么原理呢?

kotori2 commented 3 years ago

在trampoline前面多放allocate一点空间

其实可以构造一个mov r0, #0 这样的东西吧,不过我现在是直接偷懒把GetCodeSize给hook掉了

mov r0, #0 是什么原理呢?

我意思是,这样就可以在不炸掉程序的前提下获得几个字节的0,可以让GetCodeSize拿到0 Edit:是我理解错了,没注意到要-offset才是header

yujincheng08 commented 3 years ago

我觉得不如entrypoint前面多放几个0,这样还能保证 vmap_table_offset_ 是正确的,header的指针也正确

rk700 commented 3 years ago

嗯,感觉可以试下,好像前面就只有2个uint32,放8个0应该就可以?

yujincheng08 commented 3 years ago

@rk700 可以多放个,因为他还有 &~0x1

google-mirror commented 3 years ago

建议把整个class PACKED(4) OatQuickMethodHeader结构抄过来

yujincheng08 commented 3 years ago

@google-mirror 我就是这么做的。抄谷歌代码反算entrypoint的实际offset,保证最小的allocate

kotori2 commented 3 years ago

这个会有跨系统版本的问题,过两年你就要准备一大堆struct在代码里

Deleted user notifications@github.com 于 2020年12月18日周五 18:05写道:

建议把整个class PACKED(4) OatQuickMethodHeader结构抄过来

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/PAGalaxyLab/YAHFA/pull/133#issuecomment-747999100, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEGYNSNEGEY7M76IBAOMPLDSVMSPVANCNFSM4T54C2JA .

yujincheng08 commented 3 years ago

其实我感觉放9个0,然后抄个 GetEntryPoint 拿到真正的entrypoint地址放trampoline就行。

google-mirror commented 3 years ago

首先, 我没细看, 只是抛个问题, 假设带上了字段, 返回了真正的OatQuickMethodHeader, 在DoGetCalleeSaveMethodCaller里可能会调用NativeQuickPcOffset(caller_pc), 不是照样崩溃么

yujincheng08 commented 3 years ago

@google-mirror current_code->IsOptimized() == false

google-mirror commented 3 years ago

@yujincheng08 current_code是真实的header, 不一定为false

yujincheng08 commented 3 years ago

@google-mirror current_code是我们自己造的

那么我们是否可以在 hookTrampoline 前构造一个假的 OatQuickMethodHeader?让其 code_size 为0,使得Contains(pc) 不会挂掉且返回 false,继续从原始的 oat file 获取真正的 entry 再计算 dexpc

google-mirror commented 3 years ago

@yujincheng08 current_code是GetOatQuickMethodHeader返回的, 意思是返回假的? 如果返回假的, 问题更多吧, 就拿DoGetCalleeSaveMethodCaller来说, 原本应该走GetResolvedMethod的逻辑就永远不走了

那么我们是否可以在 hookTrampoline 前构造一个假的 OatQuickMethodHeader?让其 code_size 为0,使得Contains(pc) 不会挂掉且返回 false,继续从原始的 oat file 获取真正的 entry 再计算 dexpc

按上面的说法, GetOatQuickMethodHeader返回的难道不是原始的header?

canyie commented 3 years ago

@google-mirror

  static OatQuickMethodHeader* FromCodePointer(const void* code_ptr) {
    uintptr_t code = reinterpret_cast<uintptr_t>(code_ptr);
    uintptr_t header = code - OFFSETOF_MEMBER(OatQuickMethodHeader, code_);
    return reinterpret_cast<OatQuickMethodHeader*>(header);
  }
kotori2 commented 3 years ago

其实又想了一下,即使把code_size正确设置了也没有什么用 这东西是为了在崩溃的时候找崩溃点的,如果崩溃点本身是被hook的函数的话,那么这里的code_已经是跳板了,在跳板所在区域搜索没有什么意义,那样依旧是不知道PC在不在原函数里。 然后会获得一个这样的崩溃:

Abort message: 'Check failed: found_virtual Didn't find oat method index for virtual method: java.lang.String[] android.icu.impl.ICUResourceBundle.getStringArray()'

相关代码:

image
rk700 commented 3 years ago

这个报错信息是说没有找到匹配的method index?

是否有测试例子?

kotori2 commented 3 years ago

这个报错信息是说没有找到匹配的method index?

是否有测试例子?

按理来讲是任意oat的系统库里面丢异常就可以复现,但是我写了几个test case测复现不来。比较容易复现貌似的是氢OS的一加8P。 另外他跑到Hook的类里面去找这个方法,当然搜不到method index...

rk700 commented 3 years ago

我看它是从method的declaring class里遍历方法对比的,method的declaring class应该是没有修改的

yujincheng08 commented 3 years ago

他是用栈顶的方法去拿art_method指针的,所以调用backup时候会拿到target方法的art_method,然后用它的entrypoint去拿header,但是target的entrypoint变成了trampoline,自然会崩。

rk700 commented 3 years ago

我这里有一台android 9的一加6,是否可以用于复现?

yujincheng08 commented 3 years ago

可以尝试: hook一个系统函数,然后使得系统函数抛出异常,不catch,看看dump出来的stack有没有问题。

实际上edxp最近修复之后,已经比较稳定了,只有一个用户报问题( https://github.com/ElderDrivers/EdXposed/issues/740#issuecomment-748865758 ),而且是非hook函数下出问题,头大。

kotori2 commented 3 years ago

不catch

可能是因为我catch了所以才没复现。

zhoujunyou commented 2 years ago

yahfa又没有延迟hook,完全可以利用 MakeInitializedClassesVisiblyInitialized 的,这个不需要inline hook,直接 dlopen 拿到sym就行。但是edxp由于有延时hook,所以会死锁。

大佬 请教一下为什么会死锁呢。 我这边在MEIZU 18 Android 11 机型上必现。

"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 flags=1 obj=0x71ae1cc8 self=0xb4000072682b5010 | sysTid=32680 nice=-10 cgrp=top-app sched=0/0 handle=0x73eee8f4f8 | state=S schedstat=( 230325048 13994022 307 ) utm=19 stm=3 core=6 HZ=100 | stack=0x7fc22ae000-0x7fc22b0000 stackSize=8192KB | held mutexes= native: #00 pc 000000000004b48c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) native: #01 pc 00000000001ad92c /apex/com.android.art/lib64/libart.so (art::Mutex::ExclusiveLock(art::Thread)+412) native: #02 pc 00000000001bf66c /apex/com.android.art/lib64/libart.so (art::ClassLinker::MakeInitializedClassesVisiblyInitialized(art::Thread, bool)+104)