程序員希望通過加密來提升程序的安全性性,但卻缺乏專業的密碼學背景知識,使得應用對數據的保護非常薄弱。本文將介紹可能出現在Android應用中的一些脆弱的加密方式,以及對應的攻擊方法。
造成脆弱加密的主要原因
Android應用中造成弱加密的原因多種多樣,OWASP Mobile Top 10 給出了幾個原因:
使用了脆弱的加密算法
使用了強健的加密算法,但加密的實現存在漏洞。
使用弱密碼算法實現加解密
我們先來看看如果如果一個應用使用弱加密算法會遭受怎樣的攻擊。
例如使用MD5作為Hash算法,MD5已經認為不安全了。如果應用使用了MD5這樣的脆弱算法,而攻擊者又能讀取到Hash,攻擊者就有相當大的機會能破解加密。假設應用使用MD5生成了這樣一段Hash:
8f6b983ad06881847a180f1398910891
攻擊者可以使用john之類的工具或在線的彩虹表破解服務:
如上圖,hash被成功破解。
除此之外,有的開發者也會使用編碼來保護敏感信息。編碼不是加密,可以很容易就解碼還原出明文。例如我們在反編譯了一個應用之后發現了如下的一段Base64編碼字符:
SGVsbG8gV29ybGQu
找一個提供在線base64解碼的網站就能還原出明文:
強密碼算法的不安全實現
另一種情況是使用了較強的加密,但使用方法不夠安全,例如使用了AES這樣的對稱加密算法,盡管該算法足夠強壯,但當其實現不當的時候也一樣容易受到攻擊。
1.AES加密使用javax.crypto.Cipher初始化時如果使用了ECB模式,將會使相同的明文在不同時候會產生相同的密文,容易遭到字典攻擊,安全性不夠高。
public void initAES() { try { this.cipher = javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding"); Exception v0_3 = new javax.crypto.spec.SecretKeySpec(this.keyBytes, "AES"); this.cipher.init(1, v0_3); this.decipher = javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding"); this.decipher.init(2, v0_3); com.easemob.util.EMLog.d("encrypt", "initital for AES"); } catch (Exception v0_5) { v0_5.printStackTrace(); } return; }
ECB加密的缺點在於同樣的明文組會得到同樣的密文組,相對於ECB模式來說,CBC模式則較安全,攻擊者不易發起主動攻擊,同時,CBC(密碼分組鏈接)適合於傳輸長度較長的報文,依據的是SSL、IPSEC的標准。
攻擊示例:http://www.freebuf.com/news/special/56506.html
2.使用SecureRandom時使用方法setSeed設置種子將會造成生成的隨機數不隨機。
通常我們都能意識到應該使用SecureRandom類來產生密鑰,用來對本地的敏感信息進行加密,實際上這種做法是欠妥的。
在這種方法中,不將密鑰作為一個字符串直接存儲在APK文件中,而是通過另外一個字符串來生成密鑰–有點類似於通過用戶口令生成加密密鑰。這種必要的混淆手段可以使攻擊者不容易破解加密信息,但是對於一個有經驗的攻擊者而言,這種策略很容易被繞過,因此我們不推薦這種方法。
事實上,Android現有的安全機制中已經為這種數據提供了很好的保護,敏感數據應該標記上MODE_PRIVATE,然后存儲在內部存儲中,請注意,千萬不能存儲在SD卡中,因為訪問控制沒法強制在外部存儲上起作用。
結合設備加密措施,這種方法可以杜絕絕大部分攻擊。
除此之外,像我們上面描述的那樣使用SecureRandom類還存在另外一個問題。從Android4.2開始,SecureRandom的默認實現是OpenSSL,開發者無法覆蓋SecureRandom的內部狀態信息,例如下面這段代碼:
SecureRandom secureRandom = new SecureRandom(); byte[] b = new byte[]{(byte)1}; secureRandom.setSeed(b); //在Android4.2上,下面這行代碼總是返回同一個數字。 System.out.println(secureRandom.nextInt());
(【譯者注】這段代碼可以看到通過程序覆蓋了random對象中的種子,造成每次生成的隨機數序列都是一樣的)
在以前的Android版本中,SecureRandom是基於Bouncy Castle實現的,它允許像上面代碼這樣的操作,每個SecureRandom類的實例產生偽隨機數時使用的種子是從/dev/urandom獲取的。(【譯者注】/dev/urandom是類Unix系統中根據當前計算機混亂狀態,如內存使用,CPU占用率等信息計算出來的隨機數,讀者可以在Linux下試試cat /dev/urandom,它會不停地輸出亂碼,一般用“熵”這個專業術語形容計算機的混亂狀態)。那些試圖使用產生隨機數的開發者通常是通過替換現有的種子來產生隨機數序列的(參考相關實現文檔),如果種子固定,那么產生的隨機數列就是可預測的,這一點是不安全的。現在通過OpenSSL實現,使得這種錯誤的行為不再可能出現。
不幸的是,那些依賴老的SecureRandom類的應用程序會發現每次程序啟動時產生的隨機數都不一樣了(事實上,這就應該是隨機數發生器的期望行為)。想要通過這種方法對加密密鑰混淆已經不可行了。
(【譯者注】原作者的意思是有些應用對敏感信息做加密,加密的話需要密鑰,但是密鑰如果直接存起來覺得很不放心,於是通過產生隨機數的方式來對密鑰混淆一下,那么每次程序啟動時都要用相同的密鑰去解密數據,於是通過一個固定的信息,比如一個密碼,或者記住某個隨機數字,通常很多人用當前時間,然后通過這個固定的信息作為種子產生隨機數,用這個隨機數做密鑰,相當於對這個固定信息做了一次混淆操作。實際上這種方法還是不安全,安全性就變成了如何保證這個種子的安全性,治標不治本。下面的部分作者的意思是應該直接將密鑰打上MODE_PRIVATE的標記存起來,通過系統的訪問控制機制保證密鑰的安全性。)
正確的方法
一種更加合理的解決方案很簡單,就是當應用程序第一次啟動時產生一個隨機的AES算法的密鑰:
public static SecretKey generateKey() throws NoSuchAlgorithmException { // 生成一個256位密鑰 final int outputKeyLength = 256; SecureRandom secureRandom = new SecureRandom(); // Do *not* seed secureRandom! Automatically seeded from system entropy. //不要給secureRandom一個固定的種子!通過系統熵值產生隨機的種子 KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(outputKeyLength, secureRandom); SecretKey key = keyGenerator.generateKey(); return key;
注意這種方法的安全性依賴於如何保證密鑰的安全性,這可以依賴於Android系統的內部存儲的安全性。將密鑰直接存放在文件中,標記為MODE_PRIVATE存在內部存儲器。(【譯者注】我們很多人的Android手機都被Root過的,好多應用也會取得Root權限,Root權限用戶是可以做任何事情的。。。這怎么辦呢?)
更加安全的方法
如果你的應用還需要額外的加密操作,那么一個推薦的方法是每次進入你的應用時需要輸入一個口令或者PIN碼。然后將這個口令傳給 PBKDF2(PBKDF2,基於口令的密鑰導出函數版本2,是RSA安全公司提出的密鑰導出算法,通常用來根據口令取得密鑰,通過一種叫做密鑰拉伸的專業技術),Android在SecretKeyFactory類中提供了一個叫做PBKDF2WithHmacSHA1的實現:
public static SecretKey generateKey(char[] passphraseOrPin, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { //PBKDF2算法執行輪數,這個數字越大,計算時間越長,你應該讓這個數字 //足夠大,以至於這個算法執行時間超過100毫秒以保證安全性 final int iterations = 1000; // 產生一個256位的密鑰 final int outputKeyLength = 256; SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength); SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); return secretKey; }
加密鹽應該是一個通過SecureRandom產生的隨機字符串,和加密密文一起存放在內部存儲器中。使用加密鹽很重要,它可以有效防止字典攻擊。
(【譯者注】看PBKDF2WithHmacSHA1這個名字也可以知道該算法是基於SHA1算法的,經常攻擊這種單向函數方法就是字典攻擊,預先計算好大量的明文對應的密文,就像是明文對應密文的字典,然后再進行逐一對比,如果明文字符串在加密前和一個隨機字符串做個連接操作,那么那些預先計算的字典就沒用了。)
檢查你的應用是不是正確的使用SecureRandom
如本文以及Jelly Bean的新安全特性所述,Android4.2的SecureRandom默認實現發生了變化,用它產生固定密鑰已經行不通了。
如果你也用了這種錯誤方法的話,我們建議現在就更新你的應用,防止當用戶升級到Android4.2或以上版本后發生一些莫名其妙的錯誤。
參考鏈接:
http://bobao.360.cn/learning/detail/174.html