早期JAVA的安全模型被稱作”沙箱(sandbox)“,通過定義這樣一個使用者可配置的保護域來實現代碼的安全性管理,緊接著在JDK1.4中引入了健壯的全功能安全體系,該體系仍然基於沙箱這一概念的,不過新體系的安全性原則是向ProtectionDomain授權許可權而不是針對單一程式碼片段授權許可權
在最初的沙箱定義中,條件過於嚴格,導致善意的程式運行受限,新的安全平台體系中引入了程式碼簽署和認證的信任模式,在這種模式下,可以根據代碼數位簽章的可信度給與適當的執行許可權,使足夠可信度的代碼將充分發揮其作用。
沙箱由四個基本部分組成:
- 類裝載器結構
- class檔案驗證
- 內建與jvm的和語言本身的安全特性
- 安全管理器和java api
1.類裝載器結構
jvm通過類裝載器實作類別的載入並同時建立不同可信度的命名空間,也叫運行時包,只有被同一類載入器所載入的類相互之間才可見,因為他們在同一命名空間,不同的類載入器會建立不同的命名空間,不同命名空間之間是不允許訪問(不可見的。jvm在載入類的過程了使用雙親委託模式,既是發現需要裝載某一類的時候,首先將這個任務委託給自己的上級類裝載器,依次規律,一般情況下類裝載任務最後將委託給啟動類裝載器,只有在雙親無法完成裝載的情況下,才由自己完成裝載。雙親委託模式首先降低了惡意代碼試圖利用系統對java api的信任來達到目的的可能性。設想,一個惡意類被命名為java.lang.String,試圖使類載入器誤以為該惡意類是java api的一部分,但雙親委託將使用啟動類載入器首先尋找java.lang.String,啟動類載入器的搜尋範圍在java api中,當在api中找到這個String類後便會進行載入,而惡意的java.lang.String將不會被載入,也就是說惡意代碼無法偽造身份。若惡意代碼試圖將自己“混"入java API,比如,一個聲明為java.lang.IamHack的類,但由於啟動類裝載器在api中無法找到該類,它只可能被類路徑裝載器或使用者自訂的類裝載器裝載,所以與api處在不同的命名空間中,仍然無法獲得足夠的信任
2.class檔案驗證
這一過程分為四個階段
⑴檔案結構驗證
sun定義的class檔案具有特定的結構,以0xCAFEBABE開頭標識這是一份java特定的class檔案(cafe babe,聽說是對barista所作貢獻的感謝)。class檔案的每一個組成部分都聲明了其類型和長度,檢驗器可以以此計算出正確的class檔案總長度,從而判斷是否有刪減或附加額外的代碼
下面是一個介面的16進位標識:
- 0000000 : cafe babe 0000 0032 0009 0700 0707 0008 ....... 2 ........
- 0000010 : 0100 0564 6f53 7468 0100 0328 2956 0100 ...doSth...()V..
- 0000020 : 0a53 6f75 7263 6546 696c 6501 0009 446f .SourceFile...Do
- 0000030 : 6572 2e6a 6176 6101 0015 6575 746f 7069 er.java...eutopi
- 0000040 : 612f 7365 6375 7269 7479 2f44 6f65 7201 a/security/Doer.
- 0000050 : 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 ..java/lang/Obje
- 0000060 : 6374 0601 0001 0002 0000 0000 0001 0401 ct..............
- 0000070 : 0003 0004 0000 0001 0005 0000 0002 0006 ................
- 0000080 : 0a .
緊接cafe babe的是虛擬機器的主次版本號碼的16進位標識,然後是常量池中的資料個數,常量次的資料在後側可以看到,這裡包括了類名,方法名等,另外還有字串和特徵符。接下來的對類檔案中欄位和方法的複雜結構表示。
⑵類型資料語義檢查
這一過程需要驗證class檔案中個組成部分的所屬執行個體是否與宣告類型相符合,以及該類型是否符合java語言規定的特定條件,比如final類是否被子類化或是final方法是否被覆寫等。另外還有java所有類繼承自java.lang.Object,常量池中條目是否合法等。
⑶位元組碼驗證
型資料語義檢查並不檢驗class位元組碼,位元組碼的檢驗將在這個階段完成。JVM使用的是堆棧體繫結構,在使用指令運算元(Java為單位元組指令序列)必須將其裝入堆棧,JVM規範中又將其稱作作業碼,每一個作業碼後面都跟著一個或多個運算元,為JVM執行作業碼時提供額外的資料。JVM依次執行每個作業碼,這在JVM內部構成執行線程,每個線程被授予一個由不同棧幀構成的JAVA棧,每一個方法調用都對應記憶體中的一個棧幀,其中儲存了方法中的局部變數和中間結果(儲存中間結果的為運算元棧)。
這期間要驗證運算元棧中的資料是否合法,方法的調用過程是否使用了合法的參數,局部變數的訪問是否被允許(訪問必須在初始化之後),位元組碼驗證的驗證工作量是比較大的,其中還包括了跳轉命令的檢查等。經過位元組碼驗證後。可以確保class檔案的完整性。
⑷符號引用驗證
class檔案中對其引用的類,方法,變數等是以文字元號描述的,該描述保證準確定位至唯一的目標。類裝載器裝載一個類的時候會對其中包含的字元引用作出解析,首先是尋找被引用的類,但不一定會載入,因為JVM的消極式載入會確保被引用的類在第一次使用前被載入。若找到了對應的類,則將文字描述替換為直接引用,如指標。JVM會記住這些直接引用,所遇到同樣的描述,則直接替換為直接引用,從而避免重複尋找。
由於JAVA動態連結的特性,可能會導致二進位不相容,儘管在改動java檔案後的重新編譯過程中會檢查相容性,但考慮到在運行時載入的類檔案在編譯時間期並不會被檢查,所以需要在運行時執行二進位相容檢查。
3.JVM內建安全特性
在執行期,除了符號引用驗證,JVM還會對一些內建的安全特性進行檢查,大致如下:
- 型別安全的引用轉化
- 結構化的記憶體訪問(非指標演算法)
- GC
- 數組邊界檢查
- Null 參考檢查(NullPoint)
強制記憶體的結構話訪問,避免了惡意使用者在瞭解記憶體分布的情況下通過指標對JVM的內部結構進行破外。當然要瞭解JVM的記憶體分布也不是易事。JVM對運行時資料空間的分配是一個黑盒過程,完全由JVM自己決定如何分配,在class中沒有任何相關的資訊。
位元組碼檢查的局限就是對於那些不經過位元組碼檢查的方法(如本地方法:native method)無法驗證其安全性,所以這裡採用的是對動態連結程式庫的存取控制,對於那些足有足夠可信度的代碼才被允許訪問本地方法。具體的實現是,由安全管理器來決定代碼是否有調用動態連結程式庫的許可權,因為調用本地方法必須調用動態連結程式庫。這樣一來,不可信的代碼將無法通過調用本地方法來繞過位元組碼檢查。
4.安全管理器和java api
我們建立的java application基本上可以說是暢通無阻的,其原因是預設情況下所有JAVA API的操作都是被允許的。但
如果稍加限制一下
建立一個policy(實際上預設既是為read,這點從java.lang.Class中定義getProtectionDomain()方法中可以看出的)
java 代碼
- grant codeBase "file:///D:/JavaTestCode/security/securitymanager/" {
- permission java.io.FilePermission "D:/JavaTestCode/security/securitymanager/*" , "read" ;
- };
codeBase指定要限制的類所在目錄 :URL
FilePermission 中指定了要保護的檔案 :資源名稱
然後在運行時以
- java -Djava.security.manager -Djava.security.policy=policyURL appName
的形式指定
(也可以通過在java.security中以policy.url.n=policyURL 的形式指定)。java 代碼
- class TrustlessWriter
- {
- public static void main(String[] args) throws Exception
- {
- File data=new File( "data.txt" );
- FileWriter writer=new FileWriter(data);
- writer.write("this is a test" );
- writer.close();
- }
- }
在控制台輸入:
- java -Djava.security.manager -Djava.security.policy=myaccess.policy TrustlessWriter
結果將是拋出類似 java.security.AccessControlException: access denied (java.io.FilePermission data.txt write) 的錯誤資訊
這裡需要注意policy的格式
policy file syntax
- grant signedBy "signer_names" , codeBase "URL" ,
- principal principal_class_name "principal_name" ,
- principal principal_class_name "principal_name" ,
- ... {
- permission permission_class_name "target_name" , "action" ,
- signedBy "signer_names" ;
- permission permission_class_name "target_name" , "action" ,
- signedBy "signer_names" ;
- ...
- };
codeBase 需用聲明協議類型,若為本地檔案話,需以file:聲明,FilePermission裡面則直接使用目標檔案名或萬用字元
在JAVA2平台安全體系中,存取權限被類型化為對象,也既是說存取權限在Java api中被定義,所有存取權限對象繼
承自抽象類別java.security.Permission;
所以也可以在代碼中啟動SecurityManager,並執行檢查
java 代碼
- class TrustlessWriter
- {
- public static void main(String[] args) throws Exception
- {
- //SecurityManager sm=new SecurityManager();
- FilePermission filePermission=new FilePermission( "D:/JavaTestCode/security/securitymanager/data.txt" , "write" );
- System.out.println(TrustlessWriter.class .getProtectionDomain());
- AccessController.checkPermission(filePermission);//sm.checkPermission(filePermission)
- File data=new File( "data.txt" );
- Writer writer=new FileWriter(data, true );
- writer.write("hello!" );
- writer.close();
- }
- }
此時可省略-Djava.security.manage,執行命令變為:
- java -Djava.security.policy=myaccess.policy TrustlessWriter
(AccessController是在JAVA2中引入的,不過考慮到前向相容,新的平台安全體系中仍然可以使用SecurityManager
來做檢查,但SecurityManager的所有check方法均調用AccessController的checkPermission)
關於程式碼簽署和認證
新安全平台中對足夠信任度的代碼放寬了限制,要獲得充分信任須通過程式碼簽署來實現,若我們對某一簽名團體足夠信任,比如SUN,那麼具有該團體簽名的代碼將被給予充分信任。一個基本的思路大致為,代碼發行者建立私密金鑰/公開金鑰對,然後利用私密金鑰對發布代碼進行簽名,代碼使用方在獲得代碼發行者提供的公開金鑰後對代碼進行驗證,確認代碼確為該提供者提供以及在發布後未經非法修改。這其中存在一些潛在的危險,既是公開金鑰是否是該代碼發行者提供的,惡意使用者可能替換掉合法的公開金鑰,這會導致使用者將給與惡意使用者發布的代碼以充分信任,目前的常見做法是通過一些權威的認證機構來發布認證而不是公開金鑰,代碼發行者被認證發布機構認證合格後,可以將自己的公開金鑰交付認證發布機構,認證發布機構再通過私密金鑰加密該公開金鑰,從而產生認證序列。這樣替換公開金鑰的可能性就變得微乎其微。不過世上無絕對安全之事,通過認證機構發布的認證也不是絕對安全的途徑。
對代碼進行簽名需要將所有class檔案打包入jar,然後通過jarsigner對其進行散列計算。
一個簡單的樣本:
1.首先將CredibleDoer.class打包至 credible.jar:
- jar cvf credible.jar eutopia/security/CredibleDoer. class
2.建立別名為credible的金鑰組,將其儲存在至doerkeys:
- keytool -genkeypair -alias credible -keypass douyourwant -keystore doerkeys -validity 2000
2000為簽名有效期間(天)
3.為credible.jar進行簽名:
- jarsigner -keystore doerkeys -storepass 111111 -keypass douyourwant credible.jar credible
完成對一個jar檔案的簽名後,會在jar產生兩個檔案,此處為credible.SF和credible.DSA
.SF檔案儲存了一個所有類的列表以及了簽名過程中使用的摘要演算法,大致如下:
credible.sf
- Signature-Version: 1.0
- SHA1-Digest-Manifest-Main-Attributes: 9QUCJMHnILftziSD8agLig+iUN8=
- Created-By: 1.6 . 0 (Sun Microsystems Inc.)
- SHA1-Digest-Manifest: Z1288OdOqrP+Kstqxv0pS10Je7o=
- Name: eutopia/security/CredibleDoer.class
- SHA1-Digest: ODGgb1aB9Ht0iVwmfhGSU6pRneo=
DSA檔案是一個二進位密匙檔案。
驗證完整性實驗:
現在通過jarsigner -verify credible.jar對該檔案進行驗證。若檔案未經改動,一切正常
若簽名後再將TrustlessDoer.class加入credible.jar且不更新簽名,那麼驗證結果將會給出警告:
警告:
此 jar 包含尚未進行完整性檢查的未簽名條目。
若是替換原有class檔案,比如這裡替換CredibleDoer.class,將會有類似下面的錯誤提示:
jarsigner: java.lang.SecurityException: SHA1 digest error for eutopia/security/CredibleDoer.class
通過 keytool -exportcert -file credible.cer -alias credible -keystore doerkeys 可以匯出cer正式檔案,其中儲存了簽名的一些基本資料以及公開金鑰
建立金鑰組的演算法有DiffieHellman,DSA ,RSA, EC可以通過-sigalg <演算法名>指定演算法,比如:
- keytool -sigalg DSA -genkeypair -alias credible -keypass douyourwant -keystore doerkeys
參考:JavaTM Cryptography Architecture API Specification & Reference的Appendix A: Standard Names以及Appendix B: Algorithms
Related Data
深入java虛擬機器第二版
Permissions in the JavaTM 2 Standard Edition Development Kit (JDK)
JavaTM Cryptography Architecture API Specification & Reference
Java 安全性架構文檔
JavaTM 2 Platform Security Architecture
Default Policy Implementation and Policy File Syntax
Java 授權內幕
J2EE 探索者: 用 JAAS 和 JSSE 實現 Java 安全性