hook native方法and bypass
前言
介绍
众所周知,Java 的 RASP 是没有办法直接 Hook Native 方法的,但确实有这个场景的需求。于是 JDK 官方出了一个 java.lang.instrument.Instrumentation#setNativeMethodPrefix ,在 Java 方法映射 C++ 方法的时候加一个前缀,这样相当于创建一个了原有 Native 方法的代理,从而实现了 Hook Native 方法。
native方法解析原理
Java 无法直接访问到操作系统底层如硬件系统,为此 Java 提供了 JNI 来实现对于底层的访问。JNI,Java Native Interface,它是 Java 的 SDK 一部分,JNI 允许 Java 代码使用以其他语言编写的代码和代码库,本地程序中的函数也可以调用 Java 层的函数,即 JNI 实现了 Java 和本地代码间的双向交互。
在 Java 命令执行中,Java 执行操作系统命令实际上需要调用操作系统的系统函数( Windows 平台为 CreateProcess API ,*nix 平台是通过 fork 和 exec 函数),而 Java 不能直接调用系统函数,而是通过 forkAndExec 这个 native 函数调用其用本地 C 代码实现的方法。
那某个 native 方法怎么知道调用本地的哪个 C 代码方法呢?
参考:
https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/native/java/lang/UNIXProcess_md.c
可以看出 native 方法解析到本地方法函数是由 Java 类的包名称和方法名称组成,这个规则这称之为:standard resolution(标准解析)。
通过 setNativeMethodPrefix 函数对 ClassFileTransformer 设置 native prefix ,JVM 将会使用动态解析方式。
比如,现有一个 native 方法在标准解析下为:
1 | native boolean foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x); |
通过 setNativeMethodPrefix 函数设置了 native prefix ,且 prefix 为 "wrapped_" ,那么解析关系就会变为:
1 | native boolean wrapped_foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x); |
这是因为当设置为动态解析方式后,在不设置 JNI RegisterNatives 显式解析的情况下,JVM尝试:
1 | method(wrapped_foo) -> nativeImplementation(wrapped_foo) |
当解析失败,会从 nativeImplementation 中删除 prefix 前缀,继续进行解析:
1 | method(wrapped_foo) -> nativeImplementation(foo) |
这个时候如果想 hook 一个 native 方法,那么就可以通过 ClassFileTransformer 对类进行修改:
移除想要
hook的native方法。增加一个
native方法 ,这个方法和hook的native方法除了方法名增加prefix,其他相同。增加一个和
hook native方法同名的java方法(除了native modifier之外其他和hook native方法相同),其中返回时调用prefix native方法。
实验测试
完整代码放到了
https://github.com/justdoit-cai/javaAgent-learn/blob/main/native-rasp/
我们先启动具有 jackson 漏洞环境的 docker 。

然后把 native-rasp-agent.jar 上传到这个容器中,并注入 agent 到目标 java 进程中。

然后我们模拟攻击方通过 jackson 的漏洞打入内存马并利用内存马执行命令:

看容器的日志可以发现已经成功 hook 了 forkAndExec 这个方法,并记录了其入参。这里没有直接返回来完全禁止调用这个方法,为了避免影响程序的其它部分。

参考前言中分析过了的
forkAndExec方法,我们可以知道执行的命令会被分到这个方法的第三个参数和第四个参数中,并且第四个参数中是用%00分割更多的参数的,这里new String()不会显示出来这里的%00,这就导致cp /home/flag /tmp/flag.txt的后两个参数会连到一起。这里为了方便只是测试就没有处理这里的%00,只是记得如果真的要做rasp记录这里的执行的命令的化要注意处理这里的%00。

补充说明(很关键!)
注意上面注入 agent 的代码中用到的是 GenericAgentTools.jar 并非 jdk 中提供的 tools.jar ,GenericAgentTools.jar 是经过了加强的 Tools.jar ,同时兼容 Windows 版本和 Linux 版本(这个 jar 包我也不知道出自于哪里,是在别人的项目中找到的)。因为开发往往用的是 Windows ,所以我们打 agent 的 jar 包时如果打入的是 Windows 版本的 tools.jar 会导致 agent.jar 在 Linux 下无法运行。
报错信息如下:

这里 tools.jar 不能兼容不同环境是因为 tools.jar 是属于 jdk 的工具,在 jre 中不存在,而不同环境用的 jdk 是不同的,比如 Linux 用的是 Linux 的 jdk ,Windows 用的是 Windows 的 jdk ,两者是不一样的。参考 https://stackoverflow.com/questions/45053288/virtualmachine-list-returns-empty-list
此外,要注意代码中的 Class.forName("sun.tools.attach.HotSpotAttachProvider"); 很关键,没有这行代码虽然不会报错,但是就会在使用 attach 模式的时候(使用 agent 模式是没问题的)无法正确返回当前正在运行的 VirtualMachine (会发现 VirtualMachine.list() 一直为空),也就无法注入 agent 到目标程序中。这是因为目标程序的 jvm 在启动的时候并不会加载这个类,需要我们手动加载。
绕过RASP NativeMethodPrefix
但是即使我们前面 hook 了 native 的代码,还是存在手段绕过的。因为我们可以通过反射拿到真正的 native 方法,即可绕过 RASP 对于命令执行的 hook 。使用 NativeMethodPrefix 实现的 native agent rasp 真正的 native 方法的名字格式为: <前缀>_forkAndExec ,因此我们找 UNIXProcess 中方法名字以 forkAndExec 结尾,但是不等于 forkAndExec 的方法就是真正的原来的 forkAndExec 方法。
代码参考:
参考文章
https://www.jrasp.com/guide/technology/native_method.html
https://github.com/turn1tup/JvmRaspBypass/tree/main
https://www.secrss.com/articles/49044
https://yzddmr6.com/posts/rasp-nativemethodprefix-bypass/





