<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    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語言頭文件。

    完整的步驟如下:

    1. cd ./javaweb-sec/javaweb-sec-source/javase/src/main/java/ (換成自己本地的地址)。
    2. vim或編輯器寫入./com/anbai/sec/cmd/CommandExecution.java文件(該目錄已存了一個注釋掉的CommandExecution.java取消掉代碼注釋就可以用了)。
    3. javac -cp . com/anbai/sec/cmd/CommandExecution.java
    4. 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.classcom_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_execJava_是固定的前綴,而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編譯:

    1. Visual Studio/cl命令編譯dll。
    2. 使用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來提升安全性。

    本文章首發在 網安wangan.com 網站上。

    上一篇 下一篇
    討論數量: 0
    只看當前版本


    暫無話題~
    亚洲 欧美 自拍 唯美 另类