很多小伙伴在逆向的时候定位到了Java层的Native函数,如果要进一步进行分析,就需要找到so中注册的Native函数。
第一种情况,函数静态注册,可以直接在so的导出符号表中找到静态注册的函数地址(这里使用的方法是dlsym)。
第二种情况,函数动态注册,在JNI_ONLOAD中使用RegisterNatives这个函数进行注册。
但是出现了一些特殊的情况,hook了这两个函数,却没有找到目标函数的注册方法。
本文章将分多个部分讲解:
本帖子是转储notion的,下面如果格式跟不上请查看:
473K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3L8%4u0@1N6h3&6S2N6r3g2Q4x3X3c8V1k6h3y4A6L8h3q4D9i4K6u0V1y4K6x3H3i4K6u0W2L8X3!0@1K9h3!0F1i4K6u0W2M7$3W2@1k6g2)9J5c8W2u0W2k6$3W2K6N6r3g2J5e0X3q4@1K9i4k6W2M7#2)9J5k6p5A6z5d9g2)9J5k6r3t1^5x3$3j5%4x3h3t1@1j5e0W2V1j5K6c8T1x3K6m8S2x3o6l9I4y4K6N6T1y4K6q4U0k6h3f1J5y4o6u0U0i4K6y4r3M7s2k6K6i4K6y4p5y4l9`.`.
获取更好的阅读体验,谢谢大家~
1、从AOSP源码的角度讲解RegisterNatives函数具体的流程
2、从AOSP源码出发,探究Java的类加载时,如何注册自己的函数地址
3、讲解函数绑定的地址究竟在哪里,如何从根本上拿到绑定函数的地址
4、如何使用工具拿到属于自己唯一的偏移地址
5、小试牛刀,用学到的知识初步测试
6、利用两个群友遇到问题的例子,一个简单的,一个复杂的,来实战应用技术
群友提问
1 .首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
2.为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
脚本部分来源:Fart脱壳王课件
寒冰老师提出的这个方法,我并不是原创,我只是实现了一个小工具以及提供了两个具体案例来实现。
欢迎大家购买看雪2W、3W班,以及FART脱壳王课程来支持寒冰老师,并获得更加充分的售后指导。
首先我们拿到RegisterNative的函数实现部分
有两个重点关注的地方:
dbaK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4G2M7%4m8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6o6p5H3i4K6u0W2x3q4)9J5k6e0m8Q4y4h3k6J5y4o6N6Q4x3V1k6^5M7X3g2X3i4K6u0r3j5i4u0@1i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8X3A6F1K9g2)9J5c8X3A6F1K9g2)9#2k6X3W2F1N6r3g2J5L8X3q4D9i4K6u0W2j5$3y4Q4x3U0x3J5y4o6f1&6
java对象转artmethod对象的过程

在这里将java的class和签名都传入

从内存中遍历artmethod,匹配出符合条件的artmethod
第二个重要的地方

artmethod调用自己的RegisterNative方法
这里就有些厂商下沉到artmethod的注册方法,导致脚本hook不到。

在这里 对artmethod的指针进行设置,完成对jni函数的绑定
总结一下:RegisterNative的核心就是调用SetNativePointer这个函数,将函数的地址保存到artmethod中
reinterpret_cast<uintptr_t>(this) + offset.Uint32Value();
这一行正是他保存的偏移地址,artmethod指针的偏移32位在源码里体现出来了,当然我们可以通过计算的方式拿到偏移地址。

这个参数就是artmethod存储地址的地方
可以根据结构体计算出data_的偏移
看到这里,可以揭露下本文章的核心了,就是通过frida拿到artmethod结构体,在计算出当前机器的偏移数量,查看data_数据的内容,那么就是该jni地址绑定的artmehod的地址了
在这个板块,我们将从LoadClass这个函数作为切入点

在这个函数里有LoadMethod和Linkcode这两个核心函数
每个函数第一次都要进行一次链接绑定

在这里判断函数是否要在本地实现
重点:根据函数类型走不同的分支,我们查看method->IsNative()
这个分支

发现函数调用了
**UnregisterNative
这个方法**

在函数链接的时候,所有的native函数都会调用一遍unregisternative
SetEntryPointFromJni
3d0K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4G2M7%4m8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6o6p5H3i4K6u0W2x3q4)9J5k6e0m8Q4y4h3k6J5y4o6N6Q4x3V1k6K6i4K6y4r3k6r3g2X3M7#2)9K6c8q4y4W2N6p5g2F1N6s2u0&6f1r3!0A6L8Y4c8r3M7X3!0E0d9X3&6A6i4K6t1$3j5h3#2H3i4K6y4n7M7s2u0G2K9X3g2U0N6q4)9K6c8r3q4J5N6l9`.`.
和registernative一样 调用了设置入口函数 而入口函数来源于[GetJniDlsymLookupStub](0ccK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4G2M7%4m8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6o6p5H3i4K6u0W2x3q4)9J5k6e0m8Q4y4h3k6J5y4o6N6Q4x3V1k6K6i4K6y4r3k6r3g2X3M7#2)9K6c8p5N6W2N6p5A6F1K9f1c8D9M7%4W2E0e0r3!0G2K9%4g2H3f1%4c8#2j5W2)9J5y4X3q4E0M7q4)9K6b7Y4m8J5L8$3A6W2j5%4c8Q4x3@1c8S2M7Y4c8Q4x3U0W2Q4x3U0S2Q4x3U0V1`.

这个函数是一段内联汇编
其中内部调用了artFindNativeMethod这个方法
83fK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4G2M7%4m8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6o6p5H3i4K6u0W2x3q4)9J5k6e0m8Q4y4h3k6J5y4o6N6Q4x3V1k6^5M7X3g2X3i4K6u0r3j5i4u0@1i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8X3g2F1N6s2u0&6M7r3!0A6L8Y4c8K6i4K6u0r3K9X3&6A6i4K6u0r3K9X3&6A6i4K6g2X3k6h3&6@1M7Y4W2H3L8$3W2F1N6s2y4Q4x3X3g2U0j5#2)9K6c8X3k6A6i4K6y4p5j5i4u0@1c8X3W2F1k6p5&6S2N6r3W2$3k6f1#2W2N6r3S2G2k6q4)9J5x3$3q4J5N6p5k6A6L8X3c8z5j5i4c8A6N6X3g2y4k6i4c8Z5L8$3b7`.
最终这个函数调用了 真正的RegisterNative函数

在这个函数里

有着寻找函数符号的过程,可以看到静态注册的规则

将long_name和short_name做拼接去寻找符号,如果没找到则保留null,等待开发人员进行绑定
我们可以理解为,jni函数一开始都绑定在一个地址上,程序员需要在jni_onload再去二次绑定上自己的真实的地址(这里在后面有一个坑)
认真阅读的读者心中已经有了答案,就在Artmethod的data_这个属性里,我们只需要拿到函数的artmethod指针以及知道自己系统artmethod的储存绑定地址的偏移即可。
我们可以自己写一个小demo,手动调用registernative,绑定我们自己的地址到函数上,然后拿到对应的artmethod,对内存进行搜索,取出符合条件的index
在aosp8.0-aosp10的系统上,artmethod的指针就是jmethodid的数值,这里我们可以通过源码来查看 在aosp11的时候这一特性发生了变化,aosp为了安全,将artmetod指针建立了一个数组,并返回了一个id作为index


从这里看到,jmethoidid只是将artmethod强转了
所以在aosp10以下,可以直接通过

来直接获取到手机的偏移地址
在aosp10以上怎么办? 非常好办,frida就可以帮你做内存检索,虽然比app一键获取要来的慢
下面我们进入下一个篇章,如何用开发的demo获取到你手机目前的偏移地址
打开我们自己实现的app

我们可以看到是4个指针大小(并不是字节,上面打错了)
如果你的app运行在32位模式下,那么就是4x4(32位指针大小4字节)=16 字节
这样安装会让你的apk强制运行在32位模式下,其余手机基本默认都运行在64位下
不确定的可以调用frida的api Proces.pointersize
我的app是运行在64位模式下,那么就是4X8(64位指针大小8字节)=32字节
打开app是另外一个界面

我们首先获取目标类的artmethod地址
将frida挂载到demo app上面
不做任何修改的运行

拿到第一个值 也就是artmethod的地址 0x754d267ed8
接下来从界面上抄来第二个值,填入下面的脚本
注入脚本

就可以获取到你偏移的字节了这里是0x10 也就是16(64位下)
如果目标app比较老 运行在32位模式下
强制demo app强制运行在32位模式下,即可拿到32位的偏移
我们目标要获取的类名是
com.example.test_1.MainActivity
方法名是
public native String stringFromJNI();
首先启动好app,frida进行附加
运行脚本,获取到目标类的artmethod

之后阅读偏移的16个字节(上一个板块的获取到的)的信息

这就是这个art方法绑定的方法了 我们使用DebugSymbol.fromAddress查看具体符号信息

简单计算一下偏移

使用获取到的地址减去模块的base,得到偏移

0x1dd80
至此,我们的小试牛刀结束了,下面循序渐进的解决两位群友问题
问题:为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
app名称:人保e通
目标类型和函数
com.facebook.react.bridge.ReadableNativeMap

第一步,使用脚本拿到artmethod地址:
拿到了目标地址

第二步,阅读指针内容
成功拿到地址:

解析下符号:

问题:.首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
目标样本app:正保会计网校
老套路,获取到目标类型的artmethod:
0x7b3ff992c8

拿到目标函数地址:

奇怪? 为什么他绑定在了art里面呢,仔细一看
art_jni_dlsym_lookup_stub
这不就是第一次统一unregisternative的地址吗
具体原理请看上面的第三部分
我们该怎么办?
非常简单,主动调用一次即可!

调用成功后我们再次查看地址

果然 地址发生了变化
奇怪的事情来了,他并没有任何符号,仅仅是一个地址

难道我们的字节读取错误了吗
使用hexdump 查看一下artmethod在内存中的值
对比了下标横线的地址,我们获取的并没有错误,我们该怎么办?
当然是去map查找他所在的段,查看是不是可执行的,如果是,那么目标so就使用了动态释放内存的操作,将可执行代码用mmap释放到内存中并执行

获取到了目标进程的pid,我们在开启一个shel

找到了三个可以的段 连名字都没有
而且发现 目标地址正是在
7b3d228000-7b3d453000 rwxp 00000000 00:00 0
这个段中
并且这个段还有执行权限,非常可疑,我们来进行内存dump
有三种方式可以dump
第一种 使用dd命令 dd if = 具体可以问gpt如何操作
第二种 使用frida脚本 dump下memory 使用file写入文件
第三种 使用开源项目
0dcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6C8M7o6M7%4y4o6u0Q4x3V1k6y4k6h3#2p5N6h3#2H3k6i4t1`.
073K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0j5h3W2&6j5h3)9I4z5e0R3^5i4K6u0r3k6h3I4X3i4K6u0V1k6s2g2E0M7q4)9J5k6r3k6A6P5l9`.`.
文章结尾会打包好 所有需要的文件 下面我们开始dump

进行dump后 我们拿到目标文件查看

是一个elf文件
进行修复后我们导入ida
并计算偏移地址
base:7b3d228000
func ptr :0x7b3d2c2080

计算出偏移地址:
0x9a080
发现就是我们想要的函数

小彩蛋:
libproxy.so 在init_proc中 很奔放的写出了释放过程,大家可以去debug学习下

所有用到的文件打包地址:
链接: 2c7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5h3&6Q4x3X3g2T1j5h3W2V1N6g2)9J5k6h3y4G2L8g2)9J5c8Y4y4Q4x3V1j5I4k6o6y4k6L8g2)9J5k6s2m8A6c8q4q4W2y4o6W2m8z5g2)9J5k6q4S2U0d9W2k6J5K9p5q4Q4x3@1k6H3N6$3c8Q4x3@1c8W2N6i4N6S2 提取码: euwa
第二第三部分写的非常有瑕疵,欢迎大佬来指正,我会及时修改帖子内容!
希望大家能从我的帖子学到一些东西,现在的东西深度不是很够,我会努力学习给大家带来高质量的帖子~
大家有问题可以给我留言,我会每天看3-5次来解决大家的问题!
static jint RegisterNatives(JNIEnv*
env
,
2460 jclass java_class,
2461 const JNINativeMethod* methods,
2462 jint method_count) {
2463
if
(UNLIKELY(method_count < 0)) {
2464 JavaVmExtFromEnv(
env
)->JniAbortF(
"RegisterNatives"
,
"negative method count: %d"
,
2465 method_count);
2466
return
JNI_ERR;
//
Not reached except
in
unit tests.
2467 }
2468 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, java_class, JNI_ERR);
2469 ScopedObjectAccess soa(
env
);
2470 StackHandleScope<1> hs(soa.Self());
2471 Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
2472
if
(UNLIKELY(method_count == 0)) {
2473 LOG(WARNING) <<
"JNI RegisterNativeMethods: attempt to register 0 native methods for "
2474 << c->PrettyDescriptor();
2475
return
JNI_OK;
2476 }
2477 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, methods, JNI_ERR);
2478
for
(jint i = 0; i < method_count; ++i) {
2479 const char* name = methods[i].name;
2480 const char* sig = methods[i].signature;
2481 const void* fnPtr = methods[i].fnPtr;
2482
if
(UNLIKELY(name == nullptr)) {
2483 ReportInvalidJNINativeMethod(soa, c.Get(),
"method name"
, i);
2484
return
JNI_ERR;
2485 }
else
if
(UNLIKELY(sig == nullptr)) {
2486 ReportInvalidJNINativeMethod(soa, c.Get(),
"method signature"
, i);
2487
return
JNI_ERR;
2488 }
else
if
(UNLIKELY(fnPtr == nullptr)) {
2489 ReportInvalidJNINativeMethod(soa, c.Get(),
"native function"
, i);
2490
return
JNI_ERR;
2491 }
2492 bool is_fast =
false
;
2493
//
Notes about fast JNI calls:
2494
//
2495
//
On a normal JNI call, the calling thread usually transitions
2496
//
from the kRunnable state to the kNative state. But
if
the
2497
//
called native
function
needs to access any Java object, it
2498
//
will have to transition back to the kRunnable state.
2499
//
2500
//
There is a cost to this double transition. For a JNI call
2501
//
that should be quick, this cost may dominate the call cost.
2502
//
2503
//
On a fast JNI call, the calling thread avoids this double
2504
//
transition by not transitioning from kRunnable to kNative and
2505
//
stays
in
the kRunnable state.
2506
//
2507
//
There are risks to using a fast JNI call because it can delay
2508
//
a response to a thread suspension request
which
is typically
2509
//
used
for
a GC root scanning, etc. If a fast JNI call takes a
2510
//
long
time
, it could cause longer thread suspension latency
2511
//
and GC pauses.
2512
//
2513
//
Thus, fast JNI should be used with care. It should be used
2514
//
for
a JNI call that takes a short amount of
time
(eg. no
2515
//
long-running loop) and does not block (eg. no locks, I
/O
,
2516
//
etc.)
2517
//
2518
//
A
'!'
prefix
in
the signature
in
the JNINativeMethod
2519
//
indicates that it's a fast JNI call and the runtime omits the
2520
//
thread state transition from kRunnable to kNative at the
2521
//
entry.
2522
if
(*sig ==
'!'
) {
2523 is_fast =
true
;
2524 ++sig;
2525 }
2526
2527
//
Note: the right order is to try to
find
the method locally
2528
//
first, either as a direct or a virtual method. Then move to
2529
//
the parent.
2530 ArtMethod* m = nullptr;
2531 bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(
env
)->GetVm()->IsCheckJniEnabled();
2532
for
(ObjPtr<mirror::Class> current_class = c.Get();
2533 current_class != nullptr;
2534 current_class = current_class->GetSuperClass()) {
2535
//
Search first only comparing methods
which
are native.
2536 m = FindMethod<
true
>(current_class, name, sig);
2537
if
(m != nullptr) {
2538
break
;
2539 }
2540
2541
//
Search again comparing to all methods, to
find
non-native methods that match.
2542 m = FindMethod<
false
>(current_class, name, sig);
2543
if
(m != nullptr) {
2544
break
;
2545 }
2546
2547
if
(warn_on_going_to_parent) {
2548 LOG(WARNING) <<
"CheckJNI: method to register \""
<< name <<
"\" not in the given class. "
2549 <<
"This is slow, consider changing your RegisterNatives calls."
;
2550 warn_on_going_to_parent =
false
;
2551 }
2552 }
2553
2554
if
(m == nullptr) {
2555 c->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail);
2556 LOG(ERROR)
2557 <<
"Failed to register native method "
2558 << c->PrettyDescriptor() <<
"."
<< name << sig <<
" in "
2559 << c->GetDexCache()->GetLocation()->ToModifiedUtf8();
2560 ThrowNoSuchMethodError(soa, c.Get(), name, sig,
"static or non-static"
);
2561
return
JNI_ERR;
2562 }
else
if
(!m->IsNative()) {
2563 LOG(ERROR)
2564 <<
"Failed to register non-native method "
2565 << c->PrettyDescriptor() <<
"."
<< name << sig
2566 <<
" as native"
;
2567 ThrowNoSuchMethodError(soa, c.Get(), name, sig,
"native"
);
2568
return
JNI_ERR;
2569 }
2570
2571 VLOG(jni) <<
"[Registering JNI native method "
<< m->PrettyMethod() <<
"]"
;
2572
2573
if
(UNLIKELY(is_fast)) {
2574
//
There are a few reasons to switch:
2575
//
1) We don't support !bang JNI anymore, it will turn to a hard error later.
2576
//
2) @FastNative is actually faster. At least 1.5x faster than !bang JNI.
2577
//
and switching is super easy, remove !
in
C code, add annotation
in
.java code.
2578
//
3) Good chance of hitting DCHECK failures
in
ScopedFastNativeObjectAccess
2579
//
since that checks
for
presence of @FastNative and not
for
!
in
the descriptor.
2580 LOG(WARNING) <<
"!bang JNI is deprecated. Switch to @FastNative for "
<< m->PrettyMethod();
2581 is_fast =
false
;
2582
//
TODO:
make
this a hard register error
in
the future.
2583 }
2584
2585 const void* final_function_ptr = m->RegisterNative(fnPtr);
2586 UNUSED(final_function_ptr);
2587 }
2588
return
JNI_OK;
2589 }
static jint RegisterNatives(JNIEnv*
env
,
2460 jclass java_class,
2461 const JNINativeMethod* methods,
2462 jint method_count) {
2463
if
(UNLIKELY(method_count < 0)) {
2464 JavaVmExtFromEnv(
env
)->JniAbortF(
"RegisterNatives"
,
"negative method count: %d"
,
2465 method_count);
2466
return
JNI_ERR;
//
Not reached except
in
unit tests.
2467 }
2468 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, java_class, JNI_ERR);
2469 ScopedObjectAccess soa(
env
);
2470 StackHandleScope<1> hs(soa.Self());
2471 Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
2472
if
(UNLIKELY(method_count == 0)) {
2473 LOG(WARNING) <<
"JNI RegisterNativeMethods: attempt to register 0 native methods for "
2474 << c->PrettyDescriptor();
2475
return
JNI_OK;
2476 }
2477 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, methods, JNI_ERR);
2478
for
(jint i = 0; i < method_count; ++i) {
2479 const char* name = methods[i].name;
2480 const char* sig = methods[i].signature;
2481 const void* fnPtr = methods[i].fnPtr;
2482
if
(UNLIKELY(name == nullptr)) {
2483 ReportInvalidJNINativeMethod(soa, c.Get(),
"method name"
, i);
2484
return
JNI_ERR;
2485 }
else
if
(UNLIKELY(sig == nullptr)) {
2486 ReportInvalidJNINativeMethod(soa, c.Get(),
"method signature"
, i);
2487
return
JNI_ERR;
2488 }
else
if
(UNLIKELY(fnPtr == nullptr)) {
2489 ReportInvalidJNINativeMethod(soa, c.Get(),
"native function"
, i);
2490
return
JNI_ERR;
2491 }
2492 bool is_fast =
false
;
2493
//
Notes about fast JNI calls:
2494
//
2495
//
On a normal JNI call, the calling thread usually transitions
2496
//
from the kRunnable state to the kNative state. But
if
the
2497
//
called native
function
needs to access any Java object, it
2498
//
will have to transition back to the kRunnable state.
2499
//
2500
//
There is a cost to this double transition. For a JNI call
2501
//
that should be quick, this cost may dominate the call cost.
2502
//
2503
//
On a fast JNI call, the calling thread avoids this double
2504
//
transition by not transitioning from kRunnable to kNative and
2505
//
stays
in
the kRunnable state.
2506
//
2507
//
There are risks to using a fast JNI call because it can delay
2508
//
a response to a thread suspension request
which
is typically
2509
//
used
for
a GC root scanning, etc. If a fast JNI call takes a
2510
//
long
time
, it could cause longer thread suspension latency
2511
//
and GC pauses.
2512
//
2513
//
Thus, fast JNI should be used with care. It should be used
2514
//
for
a JNI call that takes a short amount of
time
(eg. no
2515
//
long-running loop) and does not block (eg. no locks, I
/O
,
2516
//
etc.)
2517
//
2518
//
A
'!'
prefix
in
the signature
in
the JNINativeMethod
2519
//
indicates that it's a fast JNI call and the runtime omits the
2520
//
thread state transition from kRunnable to kNative at the
2521
//
entry.
2522
if
(*sig ==
'!'
) {
2523 is_fast =
true
;
2524 ++sig;
2525 }
2526
2527
//
Note: the right order is to try to
find
the method locally
2528
//
first, either as a direct or a virtual method. Then move to
2529
//
the parent.
2530 ArtMethod* m = nullptr;
2531 bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(
env
)->GetVm()->IsCheckJniEnabled();
2532
for
(ObjPtr<mirror::Class> current_class = c.Get();
2533 current_class != nullptr;
2534 current_class = current_class->GetSuperClass()) {
2535
//
Search first only comparing methods
which
are native.
2536 m = FindMethod<
true
>(current_class, name, sig);
2537
if
(m != nullptr) {
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
最后于 2024-7-31 17:30
被棕熊编辑
,原因: 添加工具apk到附件,原来的aosp10不会直接输出指针数量,修复这个bug