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/