JNI、JNA、JNR的浅入浅出

如何不依赖java原生命令执行类去实现命令执行

JNI

JNI介绍

JNI指的是Java Native Interface,是Java提供的一种机制,用于在Java虚拟机(JVM)上运行的应用程序与本地操作系统或其他本地库进行交互。

通过JNI,Java应用程序可以调用本地代码,实现了Java语言与本地代码的无缝集成。使用JNI,开发人员可以编写C或C++代码,将其编译为本地共享库,并从Java程序中动态加载这些库并调用其中的函数。通常情况下,JNI被用来访问操作系统提供的底层接口,比如文件系统、网络等功能。同时,JNI也可以让Java程序直接调用本地语言编写的库,比如图形界面库、数学计算库等。

JNI实现过程

具体步骤如下:

1、.java文件声明一个native方法

2、对native方法的.java文件使用javah进行编译,生成.h头文件

3、引用.h头文件,编写对应的c代码

4、根据目标系统不同,通过gcc/g++编译成.so或.dll链接库文件

5、编写一个新的Java类使用System.loadLibrarySystem.load方法,加载链接库文件并调用文件中的方法

JNI具体实现

根据以上步骤进行详细的实现

1、.java文件声明一个native方法

package com.company;

public class Command {
    public native String exec(String cmd);
}

2、对native方法的.java文件使用javah进行编译,生成.h头文件

来到.java文件路径下,使用命令javah -cp . com.company.Command生成出对应的类头文件,头文件内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_company_Command */

#ifndef _Included_com_company_Command
#define _Included_com_company_Command
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_company_Command
 * Method:    exec
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_company_Command_exec
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

这里可以看到有个Java_com_company_Command_exec的字符,前面的Java是固定的前缀,CmdExec是类名,最后面的exec是类中定义的方法名,该方法接受一个jstring类型的参数,并返回一个jstring类型的值。由于该方法被声明为静态方法,因此可以通过类名直接调用

而括号中的三个参数分别代表:JNIEnv*表示指向JNI环境的指针,jclass表示对象的类,jstring表示Java字符串类型。

以上关于定义的类型转换,可参考:https://blog.csdn.net/qq_25722767/article/details/52557235

3、引用.h头文件,编写对应的c代码

以下c中实现了调用popen去执行系统命令,并将执行结果返回

#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <iostream>
#include "com_company_Command.h"
using namespace std;

JNIEXPORT jstring JNICALL Java_com_company_Command_exec(JNIEnv *env, jobject obj, jstring cmd) {
  const char *cmdStr = env->GetStringUTFChars(cmd, NULL); //将Java中的字符串转换为C风格的字符串

  FILE *fp;
  char buffer[256];
  fp = popen(cmdStr, "r"); // 执行系统命令
  std::string result = "";
  while (fgets(buffer, sizeof(buffer), fp)) {
    result += buffer; // 将命令输出添加到结果字符串中
  }
  pclose(fp);

  env->ReleaseStringUTFChars(cmd, cmdStr); //释放字符串资源

  return env->NewStringUTF(result.c_str()); // 将结果字符串转换为Java字符串
}

4、根据目标系统不同,通过gcc编译成.so或.dll链接库文件

注意x64位系统对应x64位gcc

来到c目录下,使用g++命令编译成动态链接库,前提是需要提前装好编译环境如:gcc/g++

需要指定jdk的include和win32文件

g++ -I "%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o cmd.dll Command.c

linux编译:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o cmd.so Command.cpp

编译完成我们就可以使用这个动态链接库了

5、编写一个新的Java类使用System.loadLibrarySystem.load方法,加载链接库文件并调用文件中的方法

package com.company;

public class Main {

    public static void main(String[] args) {
        System.loadLibrary("cmd");
        Command command = new Command();
        String ipconfig = command.exec("ipconfig");
        System.out.println(ipconfig);
    }
}

如果出现找不到链接库等报错,建议重新打开IDEA

Untitled.png

命令执行成功,这里不是调用java原生命令执行的方法,而是通过底层调用自写dll文件中封装的方法。

加载链接库的经过

首先通过System.loadLibrary跟进到Runtime.*getRuntime*().loadLibrary0(Reflection.*getCallerClass*(), libname),其中Reflection.getCallerClass()方法会返回当前调用该方法的类对象,libname则是要加载的cmd.dll本地库文件。这个方法会在运行时将cmd.dll加载到进程中,从而使得 Java 程序可以调用其中的函数。

Untitled (1).png

loadLibrary0中将调用ClassLoader.loadLibrary(fromClass, libname, false);也是用来加载本地库文件的,其中的fasle表示并不会使用System.loadLibrary()方法进行本地库文件加载,该方法中将根据指定的类加载器sun.misc.Launcher$AppClassLoader从特定位置加载本地库文件cmd.dll,避免了对系统全局状态的依赖。

Untitled (2).png

JNI利用场景

1、将自写dll和恶意war包绑定,通过tomcat后台war包上传等任意文件上传漏洞部署应用后,jsp或者class调用自写dll文件,即可实现命令执行

2、上传jsp或注入class,将dll文件十六进制编码释放并调用,即可实现命令执行

JNA

JNA介绍

"JNA" 是 Java Native Access 的缩写,也是Java调用本地库的一种方式,它允许Java应用程序通过本地方法调用(Native Method Invocation,NMI)来访问本地共享库(DLL、SO等)。但是相对JNI,JNA提供了一个易于使用的Java API,简化了手动编写映射代码的过程,可以自动将Java方法映射到本地库函数,允许Java应用程序直接访问本地库。

JNA实现过程

具体步骤如下:

1、引用com.sun.jna.Native的jar包

2、编写一个新的Java类使用Native.load()方法,即可在IDEA或应用上调用Native方法

JNA具体实现

根据以上步骤进行详细实现

import com.sun.jna.Library;
import com.sun.jna.Native;

public class CommandExecutor {
    public interface CLibrary extends Library {
                // windows下的共享库
        CLibrary INSTANCE = (CLibrary) Native.loadLibrary("kernel32", CLibrary.class);
                int WinExec(String command);

                // linux下的共享库
                // CLibrary INSTANCE = (CLibrary) Native.load("libc.so.6", CLibrary.class);
        // int system(String command);
    }

    public static void main(String[] args)   {
        // 执行命令并获取返回值
        String command = "calc";
                int returnValue = CLibrary.INSTANCE.WinExec(command);
        // int returnValue = CLibrary.INSTANCE.system(command);

        // 打印返回值
        System.out.println("Return Value: " + returnValue);
    }
}

在本例中,"kernel32"代表C共享库,由于是windows操作系统提供的共享库,linux则使用"libc.so.6"共享库

1、在类中定义原生函数接口

public interface CLibrary extends Library语句定义了一个代理接口CLibrary,它继承自Library类。接口中声明了一个名为INSTANCE的常量,其类型为CLibrary,并使用Native.loadLibrary("kernel32", CLibrary.class)方法加载了名为"kernel32"的本地共享库,并将结果赋值给INSTANCE常量。

2、同时声明需要调用的函数

CLibrary接口中声明了一个WinExec函数,该函数与C共享库中的WinExec函数具有相同的参数类型(即输入参数为字符串类型,返回值为整型),表示将要执行的系统命令command作为输入参数,函数将返回执行此命令后的状态码。

3、调用共享库函数

main函数中,先定义了一个变量command用于存储将要执行的命令。使用CLibrary.INSTANCE.system(command)方法调用了C共享库中的WinExec函数,并将command作为输入参数传递给该函数。最后输出、打印执行命令后的返回值即状态码returnValue

综上所述,这段代码的主要功能是使用JNA库直接调用kernel32共享库中的WinExec函数,实现执行操作系统命令并获取返回值

Untitled (5).png

加载链接库的经过

直接跟进到com.sun.jna.Native.loadLibrary,该方法用于加载JNA库的指定接口类的实现,并返回该实现的代理对象。它会创建一个名为”kernel32”的库处理程序handler,并使用该处理程序创建一个动态代理对象proxy,该代理对象实现了CLibrary接口中定义的所有方法。

Untitled (4).png

其中调用了cacheOptions方法实现了创建并缓存Native库的选项,其中将(cls, proxy)添加到libraries Map中,其中cls为传入的类CommandExecutor$CLibraryproxy为其代理对象

Untitled (5).png

回到CLibrary.INSTANCE.WinExec(command);,该方法中通过CommandExecutor$CLibrary.WinExec(java.lang.String)反射调用相应Native库的WinExec方法并传入参数,实现native方法的命令执行

Untitled (6).png

Untitled (7).png

基于JNA实现的pty4j

pty4j介绍

pty4j是一个Java库,其提供了一个简单的API,可以轻松地启动一个子进程,并通过标准输入/输出流与其进行交互,就像在终端中一样。它还支持设置超时时间、绑定输出处理器以及获取子进程的退出状态等功能。

pty4j命令执行

本身pty4j主要用于远程连接,做远程终端管理的工具。但是意外发现它具备了基于JNA实现本地命令执行或处理文件系统的能力,并且不依赖jdk原生的runtime等命令执行类。

其利用环境比较苛刻,我下载的pty4j版本由于其本身采用了JDK11进行编译,那我就将Java运行版本也切换成JDK11,并且还需要结合其它的第三方库才能正常实现命令执行(根据报错,缺什么装什么)。

import com.pty4j.PtyProcess;
import com.pty4j.PtyProcessBuilder;
import java.io.*;

public class ptyTest {
    public static void main(String[] args) throws IOException {
        PtyProcessBuilder builder = new PtyProcessBuilder(new String[]{"cmd.exe","/c","calc.exe"});
        //PtyProcessBuilder builder = new PtyProcessBuilder(new String[]{"/bin/bash","-c","ifconfig"});
        PtyProcess process = builder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

        String line;
        while((line = reader.readLine()) != null){
            System.out.println(line);
        }
        try {
            process.waitFor();
            process.destroy();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        process.destroy();
    }
}

效果如下

Untitled (8).png

具体调用过程

首先创建一个传入命令执行的参数的PtyProcessBuilder对象,跟进到start()方法,将看到创建新的PTY进程时需要指定的选项

Untitled (9).png

接着来到私有方法WinPtyProcess中,提取以上的配置选项,做下一步PTY执行的内容。在com.pty4j.windows.WinPty类中可以看到加载了pty4j在临时文件夹产生的winpty.dll,调用INSTANCE.winpty_spawn方法,后续的过程跟之前说的直接利用JNA一样,同样利用JNA反射调用了相应Native库的winpty_spawn方法实现了命令执行

Untitled (10).png

Untitled (11).png

题外话

还有一种Native执行命令的技术叫JNR,它其实是一个开源项目,也允许Java程序调用本地库,但与JNA不同的是,JNR采用了一种更快、更轻量级的实现方式。JNR使用Java字节码生成技术,在运行时动态生成本地方法的映射代码,避免了JNI调用过程中的不必要开销。

实则JNR的不同就是作者重写了jnr.ffi.Library.loadLibrary方法,利用该方法去加载Windows的kernel32库,接着利用kernel32库的CreateProcess函数创建命令管道和进程,从而达到命令执行的效果。(有兴趣的话可以自己研究一下)

小结

以上内容主要出于”不依赖java原生命令执行类去实现命令执行“的想法出发,最为主流的技术非JNI莫属,同时发掘其它新的调用Native技术。不难发现,JNA和JNR都是为了简化Java与本地代码之间的交互而创建的工具库,它们都是基于JNI的思想进行扩展的。

  • 发表于 2023-05-11 10:09:25
  • 阅读 ( 7552 )
  • 分类:安全工具

0 条评论

请先 登录 后评论
w1nk1
w1nk1

12 篇文章

站长统计