JNI指的是Java Native Interface,是Java提供的一种机制,用于在Java虚拟机(JVM)上运行的应用程序与本地操作系统或其他本地库进行交互。
通过JNI,Java应用程序可以调用本地代码,实现了Java语言与本地代码的无缝集成。使用JNI,开发人员可以编写C或C++代码,将其编译为本地共享库,并从Java程序中动态加载这些库并调用其中的函数。通常情况下,JNI被用来访问操作系统提供的底层接口,比如文件系统、网络等功能。同时,JNI也可以让Java程序直接调用本地语言编写的库,比如图形界面库、数学计算库等。
具体步骤如下:
1、.java文件声明一个native方法
2、对native方法的.java文件使用javah进行编译,生成.h头文件
3、引用.h头文件,编写对应的c代码
4、根据目标系统不同,通过gcc/g++编译成.so或.dll链接库文件
5、编写一个新的Java类使用System.loadLibrary
或System.load
方法,加载链接库文件并调用文件中的方法
根据以上步骤进行详细的实现
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.loadLibrary
或System.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
命令执行成功,这里不是调用java原生命令执行的方法,而是通过底层调用自写dll文件中封装的方法。
首先通过System.loadLibrary
跟进到Runtime.*getRuntime*().loadLibrary0(Reflection.*getCallerClass*(), libname)
,其中Reflection.getCallerClass()
方法会返回当前调用该方法的类对象,libname
则是要加载的cmd.dll本地库文件。这个方法会在运行时将cmd.dll加载到进程中,从而使得 Java 程序可以调用其中的函数。
loadLibrary0
中将调用ClassLoader.loadLibrary(fromClass, libname, false);
也是用来加载本地库文件的,其中的fasle
表示并不会使用System.loadLibrary()
方法进行本地库文件加载,该方法中将根据指定的类加载器sun.misc.Launcher$AppClassLoader
从特定位置加载本地库文件cmd.dll,避免了对系统全局状态的依赖。
1、将自写dll和恶意war包绑定,通过tomcat后台war包上传等任意文件上传漏洞部署应用后,jsp或者class调用自写dll文件,即可实现命令执行
2、上传jsp或注入class,将dll文件十六进制编码释放并调用,即可实现命令执行
"JNA" 是 Java Native Access 的缩写,也是Java调用本地库的一种方式,它允许Java应用程序通过本地方法调用(Native Method Invocation,NMI)来访问本地共享库(DLL、SO等)。但是相对JNI,JNA提供了一个易于使用的Java API,简化了手动编写映射代码的过程,可以自动将Java方法映射到本地库函数,允许Java应用程序直接访问本地库。
具体步骤如下:
1、引用com.sun.jna.Native
的jar包
2、编写一个新的Java类使用Native.load()
方法,即可在IDEA或应用上调用Native方法
根据以上步骤进行详细实现
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
函数,实现执行操作系统命令并获取返回值
直接跟进到com.sun.jna.Native.loadLibrary
,该方法用于加载JNA库的指定接口类的实现,并返回该实现的代理对象。它会创建一个名为”kernel32”的库处理程序handler
,并使用该处理程序创建一个动态代理对象proxy
,该代理对象实现了CLibrary
接口中定义的所有方法。
其中调用了cacheOptions
方法实现了创建并缓存Native
库的选项,其中将(cls, proxy)
添加到libraries Map
中,其中cls
为传入的类CommandExecutor$CLibrary
,proxy
为其代理对象
回到CLibrary.INSTANCE.WinExec(command);
,该方法中通过CommandExecutor$CLibrary.WinExec(java.lang.String)
反射调用相应Native库的WinExec
方法并传入参数,实现native方法的命令执行
pty4j是一个Java库,其提供了一个简单的API,可以轻松地启动一个子进程,并通过标准输入/输出流与其进行交互,就像在终端中一样。它还支持设置超时时间、绑定输出处理器以及获取子进程的退出状态等功能。
本身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();
}
}
效果如下
首先创建一个传入命令执行的参数的PtyProcessBuilder
对象,跟进到start()
方法,将看到创建新的PTY进程时需要指定的选项
接着来到私有方法WinPtyProcess
中,提取以上的配置选项,做下一步PTY执行的内容。在com.pty4j.windows.WinPty
类中可以看到加载了pty4j在临时文件夹产生的winpty.dll,调用INSTANCE.winpty_spawn
方法,后续的过程跟之前说的直接利用JNA一样,同样利用JNA反射调用了相应Native库的winpty_spawn
方法实现了命令执行
还有一种Native执行命令的技术叫JNR
,它其实是一个开源项目,也允许Java程序调用本地库,但与JNA不同的是,JNR采用了一种更快、更轻量级的实现方式。JNR使用Java字节码生成技术,在运行时动态生成本地方法的映射代码,避免了JNI调用过程中的不必要开销。
实则JNR的不同就是作者重写了jnr.ffi.Library.loadLibrary
方法,利用该方法去加载Windows的kernel32
库,接着利用kernel32
库的CreateProcess
函数创建命令管道和进程,从而达到命令执行的效果。(有兴趣的话可以自己研究一下)
以上内容主要出于”不依赖java原生命令执行类去实现命令执行“的想法出发,最为主流的技术非JNI莫属,同时发掘其它新的调用Native技术。不难发现,JNA和JNR都是为了简化Java与本地代码之间的交互而创建的工具库,它们都是基于JNI的思想进行扩展的。
12 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!