Java Instrumentation插樁技術學習
Instrumentation基礎
openrasp中用到了Instrumentation技術,它的最大作用,就是類的動態改變和操作。
使用Instrumentation實際上也可以可以開發一個代理來監視jvm的上運行的程式,可以動態的替換類的定義,就可以達到虛擬機級別的AOP實現,隨時可以為應用增加新的功能。
基本功能和用法:
java.lang.instrument包提供的實現依賴於JVMTI,JVMTI(Java Virtual Machine Tool Interface)就是java虛擬機提供的一些本地變成的介面,通過代理的形式來訪問JVM。在instrument包當中通過jvmti代理程式來進行類的動態操作,還可以進行虛擬機記憶體管理、執行緒控制等
如何啟動代理?(有兩種方法)
1.啟動程式時指定代理(main之前運行)
2.程式啟動後用agentmain方法通過attach附加啟動代理(main之後運行)
代理何時起作用:
1.addTransformer,當class被裝載時(loadclasss),與ClassFileTransformer(我們需要實現其transform方法)結合,轉換單個class文件(可以根據拿到的classname進行匹配)
2.redefineClasses,支援多個class文件的轉換(自己指定好要轉換哪些類,比如類aaa,即aaa.class)
第一種:
在一個含有main函數的java類啟動時,通過指定–javaagent
參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啟動 Instrumentation 的代理程式。
Instrumentation是instrument包中的一個介面,jdk1.5引入
在jdk1.5中,可以通過premain方法來讓instrumentation在main函數之前執行
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs)
只需要將需要進行的操作(使用addtransformer或者redefineclasses)定義在premain方法中即可對類進行任意操作,agentArgs是命令行下啟動javaagent時傳入的參數,inst是insructation的實例(後面都靠它),由jvm傳入
之前在學習javacodeview時也做過一個類似的例子 //www.cnblogs.com/tr1ple/p/12260662.html,這裡拿來再分析一下
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.Arrays; public class testagent { private static byte[] relaceBytes(String classname,byte[] classbuffer) { String bufferStr = Arrays.toString(classbuffer).replace("[","").replace("]",""); System.out.println("classname:"+classname); System.out.println("byes:"+ bufferStr); byte[] findBytes = "hello world".getBytes(); String findStr = Arrays.toString(findBytes).replace("[","").replace("]",""); System.out.println("world"+findStr); byte[] replaceBytes = "hello agent".getBytes(); String replaceStr = Arrays.toString(replaceBytes).replace("[","").replace("]",""); System.out.println("agent"+replaceStr); bufferStr = bufferStr.replace(findStr,replaceStr); System.out.println(bufferStr); String[] bytearr = bufferStr.split("\\s*,\\s*"); byte[] bytes = new byte[bytearr.length]; for(int i=0;i < bytearr.length;i++) { bytes[i] = Byte.parseByte((bytearr[i])); } System.out.println("new byte :"+Arrays.toString(bytes)); return bytes; }
//主要還是premain方法的定義與agent相關,通過instrucmentation的實例inst來添加一個transformer public static void premain(String args,final Instrumentation inst){ inst.addTransformer(new ClassFileTransformer() { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/","."); if(className.equals("helloWorld")){ return relaceBytes(className,classfileBuffer); } return classfileBuffer; } },true); } }
先看一下instrumentation,其中addtransformer添加的transformer在每一次類被載入進jvm時都將被調用,並且轉換是可以有多個的(自己添加多個classfiletransformer),當一個類轉換報錯時,jvm將按順序調用其它的transformer進行類文件的轉換
addTransformer實際上添加的是一個classfileTransfoemer的實例,第二個參數則代表當前轉換的結果時候能夠被再次轉換
也存在addtransformer只傳一個transformer進來,則默認只轉換一次
在classfiletransformer中可以看到其可以對類文件進行轉換並返回一個替換後的文件(我們可以選擇只替換我們想要替換的部分)
這個類就只有一個transform方法,loader就是類文件被轉換時的類載入器
classname即jvm中定義的完整的類名或介面名,比如java/util/List,那麼在每個類都要進行載入時就需要進行一個判斷,只對滿足我們需求的類進行匹配
classBeingRedefined指transform是類載入時觸發還是類被重新轉換時觸發,感覺像個標誌的作用
保護域,這裡涉及到了類的載入,所以涉及到了Java Security(主要是一種定義了一些程式碼執行行為的約束)
java類載入時會形成sandbox,再根據security policy為沙盒生成安全策略,程式執行時再根據安全策略進行程式相應檢查,從而保護資源不被惡意操作。
還有最後一個參數即傳入的位元組碼文件所在的位元組數組,有了這個位元組數組我們就可以對想要轉換的class進行操作了
上面的的程式碼對應的操作實際上就是匹配類名為helloworld的類,然後直接暴力將位元組數組轉string,然後用等長的字元串替換對應的位元組碼world為agent,所以原理並不複雜,因為長度未發生變化,若是長度變化,則需要改的就不僅僅是簡單的替換,需要結合一些位元組碼操作類,比如javaassist操作位元組碼就很方便
第二種:
整個重新替換class的位元組碼(可以重新用javaassist重新構造一個新的類的各種方法體,成員變數值,然後用其位元組碼進行替換):
test_agent.java
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class testagent { public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException, ClassNotFoundException { File file; file = new File("hello_world"); FileInputStream fi = new FileInputStream(file); byte[] fb = new byte[fi.available()]; fi.read(fb); ClassDefinition cla = new ClassDefinition(hello_world.class, fb); inst.redefineClasses(new ClassDefinition[]{cla}); System.out.println("agent success"); } }
main_pro.java
public class main_pro { public static void main(String[] args) { System.out.println(new hello_world().print()); } }
hello_world.java
public class hello_world { public String print() { return "hello_world"; } }
如上面程式碼所示,main_pro.java嘗試調用hello_world類的print方法輸出,此時要用到hello_world這個類,那麼要涉及到該類的裝載,所以redefinedclass在這裡精確攔截的類即為hello_world.class,攔截並重新替換其位元組碼
更改後的hello_world.java如下所示,因為我們只需要其位元組碼即可,因此直接編譯為class位元組碼文件,然後該文件名可任意(讀取位元組碼時用)
public class hello_world { public String print() { return "hello_agent"; } }
然後編輯MANIFEST.MF文件,設置premain路徑以及能夠重定義位元組碼屬性,如果沒有該屬性將報錯如下圖所示:
MANIFEST.MF:
然後打包:
不加代理之前:
加上代理之後運行:
可以看到此時已經替換要載入的hello_world類的位元組碼文件,所以實際上載入進jvm的就是替換後的位元組碼文件,然後調用print方法時就輸入是我們替換之後的了,這裡的替換可以看到是整個文件的替換。如果單純只是插樁在指定的方法內,只要掌握好上面說的第一種addtransformer就好了,針對具體要hook某個類,感覺要針對某種具體的漏洞所經過的最終函數調用棧而言。(具體使用的時候針對我們不同的需求來定義,要是在某個類基礎上改,則採用addtransformer,如是新定義類,則用redefineclasses)
上面說的都是在實際的main方法之前載入相應的class位元組碼文件到jvm中,然後執行代理進行對指定的class進行相應的操作,那麼還有另一種就是main之後再用Instrumentation來做代理,openrasp中不僅用到了premain也用到了agentmain,所以兩種都得學學(agentmain是jdk1.6中提出來的)
agentmain和premain也有兩種定義方法:
public static void agentmain (String agentArgs, Instrumentation inst); //優先順序大
public static void agentmain (String agentArgs);
第一種優先順序高於第二種,agentmain通常和retransformClasses結合在一起用,當然addtransformer的時候要設定標誌位為true,允許再次轉換jvm已經載入的類
Transformer.java
import java.io.FileInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class Transformer implements ClassFileTransformer { public byte[] getByteFromFile() throws IOException { FileInputStream fi = new FileInputStream("hello_world"); byte[] fileByte = new byte[fi.available()]; fi.read(fileByte); return fileByte; } public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.equals("hello_world")) { try { return getByteFromFile(); } catch (IOException e) { e.printStackTrace(); } } return classfileBuffer; } }
main_pro.java
public class main_pro { public static void main(String[] args) throws InterruptedException { System.out.println(new hello_world().print()); int count = 0; while(true){ Thread.sleep(600); count++; System.out.println(new hello_world().print()); if(count==10){ break; } } } }
這裡具體的替換方法還是與premain一樣,我們替換hello_world為hello_agent,只需要在retransformClasses中指定要重新轉換的類hello_world,那麼main_pro在實例化hello_world的時候,被agent捕獲到,重新返回新的class位元組碼文件,因為agentmain是啟動後附加到應用程式的代理,所以需要定義一個附加過程(程式碼來自網上):
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; class AttachThread extends Thread { private final List<VirtualMachineDescriptor> listBefore; private final String jar; AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { listBefore = vms; // 記錄程式啟動時的 VM 集合 jar = attachJar; } @Override public void run() { VirtualMachine vm = null; List<VirtualMachineDescriptor> listAfter = null; try { int count = 0; while (true) { listAfter = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : listAfter) { if (!listBefore.contains(vmd)) { //新的jvm虛擬機啟動則認為是需要附加的進程(不夠精準) vm = VirtualMachine.attach(vmd); break; } } Thread.sleep(500); count++; if (null != vm || count >= 10) { break; } } vm.loadAgent(jar); vm.detach(); } catch (Exception e) { } } public static void main(String[] args) throws InterruptedException { new AttachThread("testagent1.jar", VirtualMachine.list()).start(); } }
這裡要用到virtualmachine這個類,默認沒在rt.jar裡面,所以bootstrap 和extend都沒把它載入進來,在idea里只要把jdk下lib下的tools.jar添加進來即可,先學習一下這個類:
virtualmachine這個類就是oracle提供給我們的在目標java虛擬機運行過程中,可以使用該類提供的方法附加到目標java虛擬機中,也就是滿足我們在應用運行過程中通過添加代理來探測應用運行的目的。關於什麼是java虛擬機,可以把它就當做一個運行的jvm進程。而VirtualMachineDescriptor就是具體去描述這java虛擬機的一個容器類,封裝著對目標java虛擬機的描述符和最終提供attach操作的AttachProvider的引用。那麼其對應的描述符一般就用作業系統的進程id來唯一標示該虛擬機,可以通過ps拿到進程id資訊,或者用jdk提供的工具jps.exe:
那麼通過virtualmachine的attach方法就能夠通過虛擬機描述符來獲得該進程對應的java 虛擬機的引用,關於拿到virtualmachine的實例,官方文檔有一句話:
Alternatively, a VirtualMachine instance is obtained by invoking the attach method with a VirtualMachineDescriptor obtained from the list of virtual machine descriptors returned by the list method.
那麼首先我們可以通過調用virtualmachine的list方法拿到返回的虛擬接描述符,可以看到其返回一個虛擬機描述符實例的list,那麼應該對應著一些進程id
上面的方法體中可以看到虛擬機描述符通過迭代attachProvider拿到的,所以有必要弄清attachProvider是什麼,它就是最終為我們提供attach到jvm虛擬機的,可以看到此時通過虛擬機描述符拿到對應的jvm虛擬機進程id作為最終attach的依據,然後到attachVirtualMachine方法進行附加,這個是個抽象方法,oracle中也說了該類是交給具體的平台去實現的(sun的providerAttach只能拿來實現sun平台jvm虛擬機程式的熱部署),不同的平台jvm虛擬機的實現和工作模式都是不一樣的,所以提供抽象方法,具體實現交給子類實現。
拿到目標要被附加的jvm實例後,此時要做的就是附加代理進程了,virtualmachine提供了三種方法:
第一種loadagent,也就是直接載入我們定義好的jar包中的代理類,比如上面用agentmain編的代理類,裡面寫好代理邏輯,和MANIFEST.MF一起打包成jar,那麼傳入該jar包的名字後,目標jvm將把該jar包添加到它的jvm的classpath中,其中options是提供給代理類agentmain方法的入口第一個參數
第二種即為loadAgentLibrary
這種是使用jvmti編寫本地的動態鏈接庫,然後將動態鏈接庫附加到目標jvm中,比如有一個libtr1ple.so的代理動態鏈接庫,此時只要傳一個tr1ple給該函數,它將到系統環境變數LD_LIBRARY_PATH的路徑下去找該so文件,並且會將傳入的tr1ple擴展為libtr1ple.so進行查找然後進行附加
第三種是loadAgentpath
這個和第二種很類似,也是載入so文件,不過不需要到環境變數中找so,而是直接提供一個代理so文件的絕對路徑來進行載入
其中MANIFEST.MF為:
運行結果如下(本來應該輸出hello_world,附加agent以後調用transform轉換class 位元組碼文件):
打包testagent1.jar作為要添加的代理:
然後同時運行附加程式,attack到java虛擬機
上面的監控jvm進程的來附加代理的程式碼感覺還不夠精準,因為只是針對選擇新增的jvm進程id來附加,那麼對於要被附加代理的java虛擬機,能否準確地獲得其虛擬機描述符?如下圖的方法是顯示虛擬機描述符
看VirtualMachineDescriptor的構造方法,也就是當我們創建虛擬機描述符時如果沒有指定displayname的時候,默認將進程id作為虛擬機描述符
那麼很容易來看一下現在機器上運行的java虛擬機的虛擬機描述符
virtual.java
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class virtual { public static void main(String[] args) { List<VirtualMachineDescriptor> vmList = VirtualMachine.list(); for(VirtualMachineDescriptor vmd:vmList){ System.out.println(vmd.displayName()); } } }
此時運行main_pro,作為要被附加的java虛擬機
並且運行virtual.java,此時由輸出可以看到目前有4個java虛擬機
virtual -> 當前運行的主程式
jetbrains -> idea
finalshell
main_pro 要被附加的java虛擬機程式
所以很容易看到此時jvm已經給運行的程式在初始化虛擬機描述符時傳入了diplayname,基本就是main方法所在的類名相關了,所有為了精確附加代理,所以可以通過displayname來進行捕獲目標java虛擬機:
那麼假設提前知道要捕獲的目標虛擬機的類名,則用以下程式碼:
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; class AttachThread extends Thread { private final String jar; AttachThread(String attachJar) { jar = attachJar; } @Override public void run() { VirtualMachine vm = null; try { int count = 0; while (true) { List<VirtualMachineDescriptor>list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { String vmDisplayName = vmd.displayName(); if (vmDisplayName.equals("main_pro")) { vm = VirtualMachine.attach(vmd); break; } } Thread.sleep(500); count++; if (null != vm || count >= 10) { break; } } vm.loadAgent(jar); vm.detach(); } catch (Exception e) { } } public static void main(String[] args) throws InterruptedException { new AttachThread("testagent1.jar").start(); } }
打包為jar
接著運行java -jar attach.jar去附加代理
此時就能看到第一次輸出是hello_world,接著新的執行緒裝載類hello_world時將被retransformClasses捕獲,此時附加testagent1.jar到main_pro對應的java虛擬機中實現程式運行中附加代理
java程式的執行過程
1.源碼編譯
首先java源文件要編譯生成.class文件肯定要經過一些強制性的語法規則檢測,那麼最常見的情況就是語法不正確將直接編譯不通過產生錯誤。
2.class位元組碼裝載、鏈接
classloader載入相應的class文件來生成class對象(loadclass->findclass->defineclass->resolveclass),而類的載入也要經過驗證,因為並不能保證載入進來的位元組碼文件是未經過篡改的,比如直接在更改class文件添加相應的後門,或位元組碼文件更改錯誤,所以位元組碼的驗證應該是class 位元組碼被載入到jvm中由jvm進行驗證
tips:jvm啟動時運行過程中已經載入到jvm的class文件的class對象獲取其classloader為null(rt.jar下的class,由bootstrap classloader載入),比如像maven中管理的第三方倉管中class文件,那麼此時要在程式中使用,獲取到的classloader也為appclassloader,這裡載入yyy和instruction屬於新的需要載入到jvm的類,因此默認也使用appclassloader。
3.應用程式訪問相關資源
資源包括文件資源、配置選項,我們可以檢查是否可讀、可寫、遠程埠是否可以進行連接等,具體的許可權包括以下這些
那麼這些檢查都是通過java SecurityManager來實現的,該類感覺是jdk為java 應用設計的一種安全檢查,其中內置了多種方法在當前應用要進行某種操作時可以調用checkxxx對應的方法來檢測是否允許執行
下圖中說的方法可以用來檢測是否擁有某種操作許可權
checkPermission如果只接受一個參數,那麼對應的只是當前執行的執行緒的上下文是否有某種操作許可權,那麼有時候需要在多執行緒下檢查許可權,所以使用getSecurityContext來拿到調用執行緒的context(默認都是AccessControlContext),然後再調用可以傳入context的checkPermission進行相應的許可權檢查
那麼最終的permission對比是由抽象類permission的具體某種操作的子類的implies方法來實現檢查,比如URLPermission,通過給定的url、指定的http方法(GET POST等)、http header進行匹配,來檢查能否訪問某個資源(檢查的前提當然要提前設置好相應的許可權new URLPermission)
大多數Permission都對應著一個action列表(初始化設置),後面進行check的時候將對應著進行檢查
比如通過以下方法就能定義一個URLPermission
這裡又學習了一下security manager的相關知識,這裡涉及到了jdk本身的安全機制,java sandbox,安全管理器只是其中一個組件,提供了一些jdk的api與作業系統之間聯繫的一些許可權限制,比如最常用的就是獲取作業系統的一些相關資訊,如下圖所示,%java_home%/jre/lib/securicy/java.policy中就包含了一些默認的安全限制選項,或者通常用的默認policy文件即為家目錄下的.java.policy,安全選項也就是之前所說的Perminssion的一些子類以及其對應的action操作
關於oracle的相關permission說明見此鏈接,//docs.oracle.com/javase/7/docs/technotes/guides/security/permissions.html
其中每一條就是許可權(就是相應的類名)+屬性+操作
其中security目錄下面還有一個java.security文件主要是對沙箱做一些配置的定義,比如如下兩個就是其定義的默認policy的尋找地址
因為默認情況下有下面的這個配置,所以是允許自定義策略文件的,還有一些其他配置等以後再單獨研究java 沙箱再詳細學習
使用policy方式:
java –Djava.security.policy=<URL> (這種一個等號還是會用java.policy)
java –Djava.security.policy==<URL> (這種兩個等號只用我們指定的policy來進行檢查)
如何使用security manager:
1.使用默認的manager
開啟方式:
java -Djava.security.manager
或者在程式碼中添加:
System.setSecurityManager(new SecurityManager());
使用默認的manager的話,最終調用checkxxx方法時則是如下所示,直接調用securitymanager的checkPermission來進行相應的檢查,然後調用AccessController的檢查許可權的方法來對要進行的操作能否執行進行檢查
所以也可以直接實例化某種許可權的實例,然後調用AccessController來進行許可權檢查,比如如下所示,如果對應的操作不允許,則拋出錯誤
2.自定義securityManager,複寫checkxxx,
import java.io.*; import java.lang.instrument.*; import java.security.*; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.regex.Pattern; public class Instruction { private static class MySecurityManager extends SecurityManager { @Override public void checkRead(String file) { Boolean match = Pattern.matches("/proc.*|/etc.*|/var.*","/proc/self/environ"); if (match) { throw new AccessControlException("cannot read file:" + file); } super.checkRead(file); } } public static void main(String[] args) throws IOException, PrivilegedActionException { final String file = "./com/1.txt"; System.setSecurityManager(new SecurityManager()); SecurityManager sm = System.getSecurityManager(); if(sm!=null){ sm.checkRead(file); } FileInputStream fi = new FileInputStream(file); int a =fi.available(); byte[] aa = new byte[a]; fi.read(aa); String out = new String(aa); System.out.println(out); } }
比如想在讀文件前,這裡不僅檢測你有沒有讀的許可權,還想檢測你一下你讀的文件是不是在應用想讓你讀的範圍之內,則可以直接定義自己的security manager來重寫checkread方法,上面的程式碼就比如想讀取/proc/self/environ文件,但是在正則中匹配到了,因此不允許進行讀取,那麼我們想要進行的操作進行完之後,只需再次調用父類的checkread方法接著後面的許可權檢查即可。
以checkread為例,對應其他的一些api我們也可以進行相應的限制,比如這裡是讀,那麼我們可以想到:
1.對應的輸出流,checkWrite,那麼就可以去檢查輸出的文件是否滿足我們的要求,比如限制寫文件的目錄,限制寫文件的後綴等
2.checkConnect可以檢查我們連接的主機和埠,那麼在發送連接請求時可以檢查是否在允許連接的ip和port範圍內等
3.checkExec可以拿到所要執行的命令,對命令來進行許可權檢查
…..
jdk提供的這一套api,我們都能夠在開啟security manager的情況下加以利用,使得應用更加安全,比如tomcat中就支援security manager的模式
tomcat cve裡面也有幾個security bypass的,不過都是低危,並且都是4年前的洞,大多數都是tomcat團隊發現的,由下面注釋可以看到-security模式將啟動security manager,默認情況下webapps下的目錄只有讀許可權,work下的對應的應用目錄具有讀、寫、刪除許可權
那麼如何指定自定義的pollicy來進行安全限制:
import java.io.FileInputStream; import java.io.FilePermission; import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedActionException; public class Instruction { public Instruction() { } public static <PriviliegedAction> void main(String[] args) throws IOException, PrivilegedActionException { String file = "./com/1.txt"; System.setSecurityManager(new SecurityManager()); //開啟security manager SecurityManager sm = System.getSecurityManager(); //拿到sm if (sm != null) { FilePermission fp = new FilePermission("./com/1.txt", "read"); //聲明對文件的讀許可權 FilePermission fp1 = new FilePermission("./com/1.txt", "write"); //聲明對文件的寫許可權 AccessController.checkPermission(fp); //直接檢查讀 AccessController.checkPermission(fp1); //直接檢查寫 System.out.println("sss"); sm.checkRead("./com/1.txt"); //用sm間接檢查讀 } FileInputStream fi = new FileInputStream("./com/1.txt"); int a = fi.available(); byte[] aa = new byte[a]; fi.read(aa); String out = new String(aa); System.out.println(out); } }
上面的文件執行編譯後運行,然後不制定策略文件的情況下,開啟sm,此時默認是對com/1.txt沒有讀寫許可權的,所以到read許可權檢查時直接報錯退出,第二次執行指定my.policy為策略文件,此時只指定讀不指定寫,此時將在寫許可權檢查時退出,即my.policy對當前運行環境生效了
tips:1.這裡將class打包成jar然後再執行比較好,如果要進行簽名的話也是jar包形式 2.policy中指定文件位置時要相對於執行的class位元組碼,文件路徑我用絕對路徑H:\JavaSecStudy\javasec-rasp\target\classes\kk\com\1.txt(單換雙杠)竟然不識別,坑了我好長時間,並且只支援下圖這種相對路徑定址方法
如下所示直接執行class位元組碼文件指定policy也可以,只要路徑對就行
總結
寫這篇總結寫看了不少文章,也將相關的鏈接放到下面,如果想多了解可以點開學習,不過大多數還是得去jdk源碼中的注釋或者jdk官方文檔去查看類的定義去理解某個類的設計意義及用法,文中不免有表述不清,若有不對,還請指出,歡迎師傅們找我交流~如果對大家有所幫助,點個推薦再走唄~ ^_^
參考
//www.ibm.com/developerworks/cn/java/j-lo-jse61/ Instrumentation 新功能
//www.jianshu.com/p/9f4e8dcb3e2f
//www.ibm.com/developerworks/cn/java/j-dyn0203/ 動態類轉換
//www.ibm.com/developerworks/cn/java/j-dyn0414/ 利用bcel涉及位元組碼
//www.cnblogs.com/f1194361820/p/4189269.html java security
//www.cnblogs.com/youxia/p/java004.html security manager
//www.cnblogs.com/MyStringIsNotNull/p/8268351.html java 沙箱機制
//www.blogjava.net/china-qd/archive/2006/04/25/42931.html 使用policy設置安全策略
//blog.spoock.com/2019/12/21/Getting-Started-with-Java-SecurityManager-from-Zero/ security manager
//www.infoq.cn/article/javaagent-illustrated/ javaagent解讀
//docs.oracle.com/javase/7/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html jvm虛擬機attach
//blog.csdn.net/qinhaotong/article/details/100693414 java agent 不錯的文章