长亭百川云 - 文章详情

告别RegisterNatives获取JNI函数绑定的地址,迎接最底层的方式获取(3个案例)

看雪学苑

54

2024-08-08

很多小伙伴在逆向的时候定位到了Java层的Native函数,如果要进一步进行分析,就需要找到so中注册的Native函数。

第一种情况,函数静态注册,可以直接在so的导出符号表中找到静态注册的函数地址(这里使用的方法是dlsym)。

第二种情况,函数动态注册,在JNI_ONLOAD中使用RegisterNatives这个函数进行注册。

但是出现了一些特殊的情况,hook了这两个函数,却没有找到目标函数的注册方法。

文章结构:

本文章将分多个部分讲解:

1、从AOSP源码的角度讲解RegisterNatives函数具体的流程

2、从AOSP源码出发,探究Java的类加载时,如何注册自己的函数地址

3、讲解函数绑定的地址究竟在哪里,如何从根本上拿到绑定函数的地址

4、如何使用工具拿到属于自己唯一的偏移地址

5、小试牛刀,用学到的知识初步测试

6、利用两个群友遇到问题的例子,一个简单的,一个复杂的,来实战应用技术

◆群友提问

1 .首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数。

2.为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数。

参考资料

◆Fart脱壳王课程、看雪3w课程

脚本部分来源:Fart脱壳王课件

寒冰老师提出的这个方法,我并不是原创,我只是实现了一个小工具以及提供了两个具体案例来实现。

欢迎大家购买看雪2W、3W班,以及FART脱壳王课程来支持寒冰老师,并获得更加充分的售后指导。

RegisterNatives函数具体的流程

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    }

首先我们拿到RegisterNative的函数实现部分。

有两个重点关注的地方:

http://aospxref.com/android-10.0.0\_r47/xref/art/runtime/jni/jni\_internal.cc#2459

java对象转artmethod对象的过程:

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

从内存中遍历artmethod,匹配出符合条件的artmethod。

第二个重要的地方:

artmethod调用自己的RegisterNative方法。

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

ALWAYS\_INLINE void SetNativePointer(MemberOffset offset, T new\_value, PointerSize pointer\_size) {  
822      static\_assert(std::is\_pointer<T>::value, "T must be a pointer type");  
823      const auto addr = reinterpret\_cast<uintptr\_t>(this) + offset.Uint32Value();  
824      if (pointer\_size == PointerSize::k32) {  
825        uintptr\_t ptr = reinterpret\_cast<uintptr\_t>(new\_value);  
826        \*reinterpret\_cast<uint32\_t\*>(addr) = dchecked\_integral\_cast<uint32\_t>(ptr);  
827      } else {  
828        \*reinterpret\_cast<uint64\_t\*>(addr) = reinterpret\_cast<uintptr\_t>(new\_value);  
829      }  
830    }

在这里 对artmethod的指针进行设置,完成对jni函数的绑定。

总结一下:RegisterNative的核心就是调用SetNativePointer这个函数,将函数的地址保存到artmethod中。

reinterpret_cast<uintptr_t>(this) + offset.Uint32Value();

这一行正是他保存的偏移地址,artmethod指针的偏移32位在源码里体现出来了,当然我们可以通过计算的方式拿到偏移地址。

这个参数就是artmethod存储地址的地方。

可以根据结构体计算出data_的偏移。

看到这里,可以揭露下本文章的核心了,就是通过frida拿到artmethod结构体,在计算出当前机器的偏移数量,查看data_数据的内容,那么就是该jni地址绑定的artmehod的地址了。

Java的类加载时,如何注册自己的函数地址

在这个板块,我们将从LoadClass这个函数作为切入点。

在这个函数里有LoadMethod和Linkcode这两个核心函数。

每个函数第一次都要进行一次链接绑定。

在这里判断函数是否要在本地实现。

重点:根据函数类型走不同的分支,我们查看method->IsNative()

这个分支。

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

在函数链接的时候,所有的native函数都会调用一遍unregisternative

SetEntryPointFromJni

http://aospxref.com/android-10.0.0\_r47/s?defs=SetEntryPointFromJni&project=art

和registernative一样 调用了设置入口函数 而入口函数来源于[GetJniDlsymLookupStub](http://aospxref.com/android-10.0.0_r47/s?defs=GetJniDlsymLookupStub&project=art)()

这个函数是一段内联汇编。

其中内部调用了artFindNativeMethod这个方法。

http://aospxref.com/android-10.0.0\_r47/xref/art/runtime/entrypoints/jni/jni\_entrypoints.cc?fi=artFindNativeMethod#artFindNativeMethod

最终这个函数调用了 真正的RegisterNative函数:

在这个函数里:

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

将long_name和short_name做拼接去寻找符号,如果没找到则保留null,等待开发人员进行绑定。

我们可以理解为,jni函数一开始都绑定在一个地址上,程序员需要在jni_onload再去二次绑定上自己的真实的地址(这里在后面有一个坑)。

函数绑定的地址究竟在哪里,如何拿到对应的偏移?

认真阅读的读者心中已经有了答案,就在Artmethod的data_这个属性里,我们只需要拿到函数的artmethod指针以及知道自己系统artmethod的储存绑定地址的偏移即可。

偏移地址如何优雅的获取?

我们可以自己写一个小demo,手动调用registernative,绑定我们自己的地址到函数上,然后拿到对应的artmethod,对内存进行搜索,取出符合条件的index。

demo开发原理

在aosp8.0-aosp10的系统上,artmethod的指针就是jmethodid的数值,这里我们可以通过源码来查看 在aosp11的时候这一特性发生了变化,aosp为了安全,将artmetod指针建立了一个数组,并返回了一个id作为index。

从这里看到,jmethoidid只是将artmethod强转了。

所以在aosp10以下,可以直接通过:

来直接获取到手机的偏移地址。

在aosp10以上怎么办?非常好办,frida就可以帮你做内存检索,虽然比app一键获取要来的慢。

下面我们进入下一个篇章,如何用开发的demo获取到你手机目前的偏移地址。

利用自写的工具,拿到你当前手机的ArtMethod偏移

aosp10.0以下:

打开我们自己实现的app。

我们可以看到是4个指针大小(并不是字节,上面打错了)。

如果你的app运行在32位模式下,那么就是4x4(32位指针大小4字节)=16 字节。

adb install --abi armeabi-v7a xxx.apk

这样安装会让你的apk强制运行在32位模式下,其余手机基本默认都运行在64位下。

不确定的可以调用frida的api Proces.pointersize。

我的app是运行在64位模式下,那么就是4X8(64位指针大小8字节)=32字节。

如果你的手机系统在安卓10以上

打开app是另外一个界面:

我们首先获取目标类的artmethod地址。

将frida挂载到demo app上面。

function getHandle(object) {  
    var handle = null;  
    try {  
        handle = object.$handle;  
    } catch (e) {  
    }  
    if (handle == null) {  
        try {  
            handle = object.$h;  
        } catch (e) {  
        }  
  
    }  
    if (handle == null) {  
        try {  
            handle = object.handle;  
        } catch (e) {  
        }  
  
    }  
    return handle;  
}  
  
Java.perform(function () {  
    let ReadableNativeMap = Java.use("com.example.getoffsite.MainActivity");  
    console.log(getHandle(ReadableNativeMap\["stringFromJNI"\]))  
  
});

不做任何修改的运行。

拿到第一个值,也就是artmethod的地址 0x754d267ed8。

接下来从界面上抄来第二个值,填入下面的脚本。

var startAddress = ptr('0x754d267ed8');  // artmethod地址  
var targetValue = ptr('0x74d82ebbb0');  // app界面上的值  
var scanLength = 1024;  // 扫描长度(字节数)  
  
function scanMemory(address, target, length) {  
    for (var i = 0; i < length; i++) {  
        var currentAddress = address.add(i);  
        var currentValue = Memory.readPointer(currentAddress);  
  
        if (currentValue.equals(target)) {  
            console.log('Found match at address: ' + currentAddress);  
            console.log("offsite",currentAddress.sub(startAddress));  
            return;  
        }  
    }  
    console.log('No match found within the specified range.');  
}  
  
scanMemory(startAddress, targetValue, scanLength);

注入脚本

就可以获取到你偏移的字节了这里是0x10 也就是16(64位下)。

如果目标app比较老,运行在32位模式下。

adb install --abi armeabi-v7a demo.apk

强制demo app强制运行在32位模式下,即可拿到32位的偏移。

小试牛刀,获取一个demoapp的jni绑定地址

我们目标要获取的类名是

com.example.test_1.MainActivity

方法名是

public native String stringFromJNI();

首先启动好app,frida进行附加。

运行脚本,获取到目标类的artmethod。

function getHandle(object) {  
    var handle = null;  
    try {  
        handle = object.$handle;  
    } catch (e) {  
    }  
    if (handle == null) {  
        try {  
            handle = object.$h;  
        } catch (e) {  
        }  
  
    }  
    if (handle == null) {  
        try {  
            handle = object.handle;  
        } catch (e) {  
        }  
  
    }  
    return handle;  
}  
  
Java.perform(function () {  
    let ReadableNativeMap = Java.use("com.example.test\_1.MainActivity");  
    console.log(getHandle(ReadableNativeMap\["stringFromJNI"\]))  
  
});

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

ptr(0x75480b3ed8).add(16).readPointer();

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

简单计算一下偏移:

Process.getModuleByName("libtest\_1.so")

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

0x1dd80

至此,我们的小试牛刀结束了,下面循序渐进的解决两位群友问题。

样本1:

问题:为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数?

app名称:人保e通

目标类型和函数

com.facebook.react.bridge.ReadableNativeMap

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

function getHandle(object) {  
    var handle = null;  
    try {  
        handle = object.$handle;  
    } catch (e) {  
    }  
    if (handle == null) {  
        try {  
            handle = object.$h;  
        } catch (e) {  
        }  
  
    }  
    if (handle == null) {  
        try {  
            handle = object.handle;  
        } catch (e) {  
        }  
  
    }  
    return handle;  
}  
Java.perform(function () {  
    let ReadableNativeMap = Java.use("com.facebook.react.bridge.ReadableNativeMap");  
    console.log(getHandle(ReadableNativeMap\["importValues"\]))  
  
});

拿到了目标地址。

第二步,阅读指针内容。

ptr(0x79b4c96368).add(32).readPointer();   //这里我使用的安卓8.0系统 32是4个指针乘8字节

成功拿到地址:

解析下符号:


样本2: 压轴戏(推荐观看)

问题:.首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数。

目标样本app:正保会计网校

老套路,获取到目标类型的artmethod:

function getHandle(object) {  
    var handle = null;  
    try {  
        handle = object.$handle;  
    } catch (e) {  
    }  
    if (handle == null) {  
        try {  
            handle = object.$h;  
        } catch (e) {  
        }  
  
    }  
    if (handle == null) {  
        try {  
            handle = object.handle;  
        } catch (e) {  
        }  
  
    }  
    return handle;  
}  
  
Java.perform(function() {  
    // 定位类  
    var targetClass = Java.use('com.cdel.encode.TSEncode');  
    console.log(getHandle(targetClass.de1))  
});

0x7b3ff992c8

拿到目标函数地址:

ptr(0x7b3ff992c8).add(32).readPointer();

奇怪?为什么他绑定在了art里面呢,仔细一看,

art_jni_dlsym_lookup_stub

这不就是第一次统一unregisternative的地址吗?

具体原理请看上面的第三部分。

我们该怎么办?

非常简单,主动调用一次即可!

Java.perform(function() {  
    // 定位类  
    var targetClass = Java.use('com.cdel.encode.TSEncode');  
  
    // 定义要传递的参数  
    var param = "7ZvLaMCWJPFQmQX87ZvLaMCWJPEFUzIwJGPZwXlCunyRfQ8xqyCsSt1ADfx3xI3LZkeb.w\_\_X8bMvisv";  
  
    // 调用目标方法并获取返回值  
    var result = targetClass.de1(param);  
  
    // 输出结果  
    console.log("Result: " + result);  
});

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

果然,地址发生了变化。

奇怪的事情来了,他并没有任何符号,仅仅是一个地址。

难道我们的字节读取错误了吗?

使用hexdump 查看一下artmethod在内存中的值。

\[Pixel::com.cdel.accmobile \]-> console.log(hexdump(ptr(0x7b3ff992c8)))  
             0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF  
7b3ff992c8  80 fd e4 14 09 01 00 00 00 00 00 00 2f 1e 00 00  ............/...  
7b3ff992d8  03 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00  ...........S{...  
7b3ff992e8  \*\*80 20 2c 3d 7b\*\* 00 00 00 60 0b b1 68 7b 00 00 00  . ,={...\`..h{...  
7b3ff992f8  80 fd e4 14 09 01 00 00 00 00 00 00 30 1e 00 00  ............0...  
7b3ff99308  04 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00  ...........S{...  
7b3ff99318  a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00  .m.h{...\`..h{...  
7b3ff99328  80 fd e4 14 09 01 00 00 00 00 00 00 31 1e 00 00  ............1...  
7b3ff99338  05 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00  ...........S{...  
7b3ff99348  a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00  .m.h{...\`..h{...  
7b3ff99358  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................  
7b3ff99368  00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00  ................  
7b3ff99378  00 00 00 00 00 00 00 00 a0 93 f9 3f 7b 00 00 00  ...........?{...  
7b3ff99388  70 08 b1 68 7b 00 00 00 00 00 00 00 00 00 00 00  p..h{...........  
7b3ff99398  00 00 00 00 00 00 00 00 30 a3 68 70 00 00 00 00  ........0.hp....  
7b3ff993a8  d8 2e 70 70 00 00 00 00 00 00 00 00 00 00 00 00  ..pp............  
7b3ff993b8  00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00  ................

对比了下标横线的地址,我们获取的并没有错误,我们该怎么办?

当然是去map查找他所在的段,查看是不是可执行的,如果是,那么目标so就使用了动态释放内存的操作,将可执行代码用mmap释放到内存中并执行。

获取到了目标进程的pid,我们再开启一个shell。

cat /proc/7129/maps >/data/local/tmp/map.txt

找到了三个可以的段,连名字都没有。

而且发现,目标地址正是在

7b3d228000-7b3d453000 rwxp 00000000 00:00 0

这个段中。

并且这个段还有执行权限,非常可疑,我们来进行内存dump。

有三种方式可以dump:

第一种 使用dd命令 dd if = 具体可以问gpt如何操作。

第二种 使用frida脚本 dump下memory 使用file写入文件。

第三种 使用开源项目。

https://github.com/kp7742/MemDumper

https://github.com/maiyao1988/elf-dump-fix

文章结尾会打包好所有需要的文件。下面我们开始dump。

255|sailfish:/data/local/tmp # ./memdumper64 -m -s 7b3d228000 -e 7b3d453000 -n 123.bin -i 7129 -o /sdcard

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

是一个elf文件。

进行修复后我们导入ida,并计算偏移地址。

base:7b3d228000

func ptr :0x7b3d2c2080

计算出偏移地址:

0x9a080

发现就是我们想要的函数。

小彩蛋:

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

===

尾言:

所有用到的文件打包地址:

链接:https://pan.baidu.com/s/1d3Ym-piDQe49A9-XcJVrhA?pwd=euwa提取码: euwa

欢迎大佬来指正,笔者会及时修改帖子内容。大家有问题可以点击阅读原文并于文末评论,笔者会每天看3-5次来解决大家的问题。

看雪ID:mb_qzwrkwda

https://bbs.kanxue.com/user-home-945390.htm

*本文为看雪论坛精华文章,由 mb_qzwrkwda 原创,转载请注明来自看雪社区



# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析

球分享

球点赞

球在看

点击阅读原文查看更多

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2