JNI安全基礎
Java語言是基于C語言實現的,Java底層的很多API都是通過JNI(Java Native Interface)來實現的。通過JNI接口C/C++和Java可以互相調用(存在跨平臺問題)。Java可以通過JNI調用來彌補語言自身的不足(代碼安全性、內存操作等)。這個看似非常炫酷的特性其實自JDK1.1開始就有了,但是我們不得不去考慮JNI調用帶來的一系列的安全問題!
本章節仍以本地命令執行為例講解如何構建動態鏈接庫供Java調用,也許很多人是第一次接觸這個概念會比較陌生但是如果你了學習過C/C++或者Android NDK那么本章節就會非常的簡單了。
JNI-定義native方法
首先在Java中如果想要調用native方法那么需要在類中先定義一個native方法。
CommandExecution.java演示
package com.anbai.sec.cmd;
/**
* 本地命令執行類
* Creator: yz
* Date: 2019/12/6
*/
public class CommandExecution {
public static native String exec(String cmd);
}
如上示例代碼,我們需要使用native關鍵字定義一個類似于接口的方法就行了,是不是感覺非常簡單?
JNI-生成類頭文件
如上,我們已經編寫好了CommandExecution.java,現在我們需要編譯并生成c語言頭文件。
完整的步驟如下:
cd ./javaweb-sec/javaweb-sec-source/javase/src/main/java/(換成自己本地的地址)。- vim或編輯器寫入
./com/anbai/sec/cmd/CommandExecution.java文件(該目錄已存了一個注釋掉的CommandExecution.java取消掉代碼注釋就可以用了)。 javac -cp . com/anbai/sec/cmd/CommandExecution.java。javah -d com/anbai/sec/cmd/ -cp . com.anbai.sec.cmd.CommandExecution
注意JDK版本:
JDK10移除了javah,需要改為javac加-h參數的方式生產頭文件,如果您的JDK版本正好>=10,那么使用如下方式可以同時編譯并生成頭文件。
javac -cp . com/anbai/sec/cmd/CommandExecution.java -h com/anbai/sec/cmd/
執行上面所述的命令后即可看到在com/anbai/sec/cmd/目錄已經生成了CommandExecution.class和com_anbai_sec_cmd_CommandExecution.h了。
com_anbai_sec_cmd_CommandExecution.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_anbai_sec_cmd_CommandExecution */
#ifndef _Included_com_anbai_sec_cmd_CommandExecution
#define _Included_com_anbai_sec_cmd_CommandExecution
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_anbai_sec_cmd_CommandExecution
* Method: exec
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
您可以使用IDE或者vim完成動態鏈接庫的編寫,如果您使用MacOS+CLion可能需要把#include <jni.h>改成#include "jni.h",不改也沒關系,編譯的時候帶上庫地址就行了。
頭文件命名強制性
javah生成的頭文件中的函數命名方式是有非常強制性的約束的,如Java_com_anbai_sec_cmd_CommandExecution_exec中Java_是固定的前綴,而com_anbai_sec_cmd_CommandExecution也就代表著Java的完整包名稱:com.anbai.sec.cmd.CommandExecution,_exec自然是表示的方法名稱了。(JNIEnv *, jclass, jstring)表示分別是JNI環境變量對象、java調用的類對象、參數入參類型。
如果您在不希望在命令行下編譯lib,可以參考:Mac IDEA+CLION jni Hello World。
JNI-基礎數據類型
需要特別注意的是Java和JNI定義的類型是需要轉換的,不能直接使用Java里的類型,也不能直接將JNI、C/C++的類型直接返回給Java。
參考如下類型對照表:

jstring轉char*:env->GetStringUTFChars(str, &jsCopy)
char*轉jstring: env->NewStringUTF("Hello...")
字符串資源釋放: env->ReleaseStringUTFChars(javaString, p);
其他知識點參考:jni中java與原生代碼通信規則
JNI-編寫C/C++本地命令執行實現
如上,我們已經生成好了頭文件,接下來我們需要使用C/C++編寫函數的最終實現代碼。
com_anbai_sec_cmd_CommandExecution.cpp示例:
//
// Created by yz on 2019/12/6.
//
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "com_anbai_sec_cmd_CommandExecution.h"
using namespace std;
JNIEXPORT jstring
JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
(JNIEnv *env, jclass jclass, jstring str) {
if (str != NULL) {
jboolean jsCopy;
// 將jstring參數轉成char指針
const char *cmd = env->GetStringUTFChars(str, &jsCopy);
// 使用popen函數執行系統命令
FILE *fd = popen(cmd, "r");
if (fd != NULL) {
// 返回結果字符串
string result;
// 定義字符串數組
char buf[128];
// 讀取popen函數的執行結果
while (fgets(buf, sizeof(buf), fd) != NULL) {
// 拼接讀取到的結果到result
result +=buf;
}
// 關閉popen
pclose(fd);
// 返回命令執行結果給Java
return env->NewStringUTF(result.c_str());
}
}
return NULL;
}
使用vim com/anbai/sec/cmd/com_anbai_sec_cmd_CommandExecution.cpp或編輯器編寫好cpp文件。
首先切換到我們的C目錄:cd com/anbai/sec/cmd/然后使用g++命令編譯成動態鏈接庫,前提是您需要提前裝好編譯環境如:gcc/g++。
MacOSX編譯:
g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib com_anbai_sec_cmd_CommandExecution.cpp
Linux編譯:
g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp
Windows編譯:
Visual Studio/cl命令編譯dll。- 使用
min-gw/cygwin安裝gcc/g++,如:x86_64-w64-mingw32-g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll com_anbai_sec_cmd_CommandExecution.cpp。
如依舊無法編譯成,可參考:Java Programming Tutorial Java Native Interface (JNI),這篇文章講解了如何在不同的操作系統中使用C/C++來編寫JNI的HelloWorld。
如果您采用了C語言編寫(C和C++版本基本沒差別,也就在使用*env時的參數值一般會不一樣)那么請用gcc編譯,編譯完成我們就可以使用這個動態鏈接庫了。正常情況下我們需要嚴格按照JNI要求去命名文件名并且把鏈接庫放到Java的動態鏈接庫目錄,不然會無法加載。但是這都不是什么大問題我們完全可以通過自定義庫名稱和路徑。
com.anbai.sec.cmd.CommandExecutionTest示例:
package com.anbai.sec.cmd;
import java.io.File;
import java.lang.reflect.Method;
/**
* Creator: yz
* Date: 2019/12/8
*/
public class CommandExecutionTest {
private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";
/**
* JDK1.5編譯的com.anbai.sec.cmd.CommandExecution類字節碼,
* 只有一個public static native String exec(String cmd);的方法
*/
private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1,
0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1,
9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
};
public static void main(String[] args) {
String cmd = "ifconfig";// 定于需要執行的cmd
try {
ClassLoader loader = new ClassLoader(CommandExecutionTest.class.getClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
}
}
};
// 測試時候換成自己編譯好的lib路徑
File libPath = new File("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib");
// load命令執行類
Class commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");
// 可以用System.load也加載lib也可以用反射ClassLoader加載,如果loadLibrary0
// 也被攔截了可以換java.lang.ClassLoader$NativeLibrary類的load方法。
// System.load("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib/libcmd.jnilib");
Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
loadLibrary0Method.setAccessible(true);
loadLibrary0Method.invoke(loader, commandClass, libPath);
String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
System.out.println(content);
} catch (Exception e) {
e.printStackTrace();
}
}
}
CommandExecutionTest執行命令演示:

示例代碼中的CommandExecutionTest.java其實和load_library.jsp邏輯差不多,Demo實現了自定義ClassLoader重寫了findClass方法來加載com.anbai.sec.cmd.CommandExecution類的字節碼并實現調用,然后再通過JNI加載動態鏈接庫并調用了鏈接庫中的命令執行函數。
JNI安全基礎總結
本章節我們學習了如何通過JNI調用動態鏈接庫實現本地命令執行功能,我們應該深入的認識到通過編寫native方法我們可以做幾乎任何事(比如不使用Java自帶的FileInputStreamAPI讀文件、不使用forkAndExec執行系統命令等)。JNI為我們提供了如此強大的靈活性也為Java的安全性帶來了非常大的挑戰,所以某些情況下我們不得不考慮如何限制用戶調用JNI來提升安全性。
Java Web安全
推薦文章: