- 本文轉自美團點評技術學院,未經做者許可,不容許私自轉載。
- 美團雲知乎機構帳號每日分享雲計算產品,技術內容。 歡迎關注!
- 加入美團雲技術交流羣(QQ羣:469243579),每日分享更多精彩技術文章。
目前Android應用代碼漏洞掃描工具種類繁多,效果參差不齊,這些工具備一個共同的特色,都是在應用打包完成後對應用進行解包掃描。這種掃描有很是明顯的缺點,掃描週期較長,不能向開發者實時反饋代碼中存在的安全問題,而且對於問題代碼的定位須要手動搜索匹配源碼,這樣就更不利於開發者對問題代碼進行及時的修改。Code Arbiter正是爲解決上述兩個問題而開發的,專門對Android Studio中的源碼進行安全掃描。
1 背景介紹
爲實現對Android Studio中的源碼進行掃描,最方便的方式即是將掃描工具以IDE插件的形式進行工做。此時一個很天然的想法即是從頭構建一個Android Studio插件,可是進行仔細的評估後會發現,這樣作難度並不小:
- 工做量大,許多知識須要學習,如IDE開放API接口、插件UI構建等,同時許多底層模塊須要從頭構建;
- 插件的穩定性、檢測問題的準確性上都不必定可以達到已有開源工具的效果。
所以咱們轉而考慮在已有漏洞檢測插件的基礎上進行擴展,以知足需求。通過調研,最終入圍的兩款檢測插件是PMD和FindBugs,其中PMD是對Java源碼進行掃描,而FindBugs則是對Java源碼編譯後的class文件進行掃描。考慮到可擴展性及檢測的準確性,最終選定了FindBugs。FindBugs是一個靜態分析工具,它檢查類或者JAR文件,將字節碼與一組缺陷模式進行對比來發現可能的問題,能夠以獨立的JAR包形式運行,也能夠做爲集成開發工具的插件形式存在。
擴展優化
那麼,怎麼擴展FindBugs呢?調研發現FindBugs插件具備着極強的可擴展性,只須要將擴展的JAR包導入FindBugs插件,重啓,便可完成相關功能的擴展。
下面的問題是如何構建可安裝的JAR包。繼續調研,發現FindBugs有一款專門對安全問題進行檢測的擴展插件Find Security Bugs,該插件主要用於對Web安全問題進行檢測,也有極少對Android相關安全問題的檢測規則。考慮如下幾個緣由,須要對該插件的源碼進行重構。
- 對Android安全問題的檢測太少,只包含外部文件使用、Webview、Broadcast使用等寥寥幾項;
- 檢測的細粒度上考慮不夠徹底,會形成大量的誤報,沒法知足檢測精度的要求;
- 檢測問題的上報只支持英文模式,且問題展現的邏輯性不夠嚴謹,不便於開發者進行問題排查。
基於以上三個緣由,咱們須要對Find Security Bugs的源碼進行重寫、優化,經過增長檢測項來檢測儘量多的安全問題,經過優化檢測規則來減小檢測的誤報,問題展現使用中文進行描述,同時優化問題描述的邏輯性,使得開發者可以更易理解並修改相關問題,至此插件實現及優化的方案肯定。
2 工具實現介紹
FindBugs檢測的是class文件,所以當待檢測的源碼未生成編譯文件時,FindBugs會先將源碼編譯生成.class文件,而後對這個class文件進行分析。FindBugs會完成對class文件的自動建模,在此模型的基礎上對代碼進行分析。按照在實際編寫檢測代碼過程當中的總結,把檢測的實現方式分紅四種方式,下面分別進行介紹。
2.1 逐行檢查
逐行檢查主要是針對代碼中使用的一些不安全方法或參數進行檢測,其實現方式是重寫sawOpcode()方法,下面以Android中使用外部存儲問題做爲示例進行講解。
Android中獲取外部存儲文件夾地址的方法主要包括下面這些方法:
getExternalCacheDir()
getExternalCacheDirs()
getExternalFilesDir()
getExternalFilesDirs()
getExternalMediaDirs()
Environment.getExternalStorageDirectory()
Environment.getExternalStoragePublicDirectory()複製代碼
檢測的方式即是,若是發現存在該方法的調用,則做爲一個問題進行上報,實現完整代碼以下所示:
public class ExternalFileAccessDetector extends OpcodeStackDetector {
private static final String ANDROID_EXTERNAL_FILE_ACCESS_TYPE = "ANDROID_EXTERNAL_FILE_ACCESS";
private BugReporter bugReporter;
public ExternalFileAccessDetector(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
@Override
public void sawOpcode(int seen) {
//printOpCode(seen);
if (seen == Constants.INVOKEVIRTUAL && (
getNameConstantOperand().equals("getExternalCacheDir") ||
getNameConstantOperand().equals("getExternalCacheDirs") ||
getNameConstantOperand().equals("getExternalFilesDir") ||
getNameConstantOperand().equals("getExternalFilesDirs") ||
getNameConstantOperand().equals("getExternalMediaDirs")
)) {
// System.out.println(getSigConstantOperand());
bugReporter.reportBug(new BugInstance(this, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY).addClass(this).addMethod(this).addSourceLine(this));
}
else if(seen == Constants.INVOKESTATIC && getClassConstantOperand().equals("android/os/Environment") && (getNameConstantOperand().equals("getExternalStorageDirectory") || getNameConstantOperand().equals("getExternalStoragePublicDirectory"))) {
bugReporter.reportBug(new BugInstance(this, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY).addClass(this).addMethod(this).addSourceLine(this));
}
}
}複製代碼
該類的實現是繼承OpcodeStackDetector類,是FindBugs中的一個抽象類,封裝了對於獲取代碼特定參數的方法調用。sawOpcode方法參數能夠理解爲待檢測代碼行的行號,經過printOpCode(seen)能夠打印該代碼行的具體內容。Constants.INVOKEVIRTUAL表示該行調用類的實例方法,Constants.INVOKESTATIC表示調用類的靜態方法。getNameConstantOperand方法表示獲取被調用方法的名稱,getClassConstantOperand方法表示獲取調用類的名稱,getSigConstantOperand方法表示獲取方法的全部參數。bugReporter.reportBug用於上報檢測到的漏洞信息,其中BugInstance的三個參數分別表示:檢測器、漏洞類型、漏洞等級,其中漏洞等級分爲五個級別,以下表所示:
名稱 參數 含義
HIGH_PRIORITY 1 高危風險
NORMAL_PRIORITY 2 中危風險
LOW_PRIORITY 3 低危風險
EXP_PRIORITY 4 安全提醒
IGNORE_PRIORITY 5 可忽略風險複製代碼
addClass、addMethod、addSourceLine用於指定該漏洞所在的類、方法、行,方便報告漏洞時定位關鍵代碼。
2.2 逐方法檢查
逐方法檢查首先獲取待檢測類的全部內容,而後對類中的方法進行逐個檢查,多用於對方法體進行檢測,其實現的方法主要是經過重寫visitClassContext方法,下面以對Android TrustManager的空實現的檢測爲例進行說明。
TrustManager的空實現,主要是指對於檢測Server端證書是否可信的方法checkServerTrusted,是不是空實現。下面展現問題代碼,若是是空實現那麼將致使客戶端接收任意證書,從而形成加密後的HTTPS消息被中間人解密。
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}複製代碼
檢測的方式是經過遍歷類中的全部方法,找到checkServerTrusted方法,對方法總體進行檢測,肯定其是否爲空實現,部分代碼以下所示:
public class WeakTrustManagerDetector implements Detector {
...
public WeakTrustManagerDetector(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass javaClass = classContext.getJavaClass();
//The class extends X509TrustManager
boolean isTrustManager = InterfaceUtils.isSubtype(javaClass,"javax.net.ssl.X509TrustManager");
boolean isHostnameVerifier = InterfaceUtils.isSubtype(javaClass,"javax.net.ssl.HostnameVerifier");
// if (!isTrustManager && !isHostnameVerifier) return;
if (!isTrustManager && !isHostnameVerifier){
for (Method m : javaClass.getMethods()) {
allow_All_Hostname_Verify(classContext, javaClass, m);
}
}
Method[] methodList = javaClass.getMethods();
for (Method m : methodList) {
MethodGen methodGen = classContext.getMethodGen(m);
if (DEBUG) System.out.println(">>> Method: " + m.getName());
if (isTrustManager &&
(m.getName().equals("checkClientTrusted") ||
m.getName().equals("checkServerTrusted") ||
m.getName().equals("getAcceptedIssuers"))) {
if(isEmptyImplementation(methodGen)) {
bugReporter.reportBug(new BugInstance(this, WEAK_TRUST_MANAGER_TYPE, Priorities.NORMAL_PRIORITY).addClassAndMethod(javaClass, m));
}
......複製代碼
classContext.getJavaClass用於獲取整個類的全部內容;javaClass.getMethods用於獲取該類中的全部方法,以一個方法列表的形式返回;classContext.getMethodGen用於獲取該方法的內容;isEmptyImplementation將方法的內容導入該函數進行檢測,用於肯定方法是不是空實現,該方法的代碼以下所示:
private boolean isEmptyImplementation(MethodGen methodGen){
boolean invokeInst = false;
boolean loadField = false;
for (Iterator itIns = methodGen.getInstructionList().iterator();itIns.hasNext();) {
Instruction inst = ((InstructionHandle) itIns.next()).getInstruction();
if (DEBUG)
System.out.println(inst.toString(true));
if (inst instanceof InvokeInstruction) {
invokeInst = true;
}
if (inst instanceof GETFIELD) {
loadField = true;
}
}
return !invokeInst && !loadField;
}複製代碼
該方法主要用於檢測方法中是否包含方法調用、域操做,若是沒有包含則認爲是一個空實現的方法。所以該方法對於只包含 return true/false 語句的方法體一樣認爲是一個空實現。
2.3 污點分析
數據流分析主要用於分析特定方法加載的參數是否可以被用戶控制,即進行污點分析。作污點分析首先須要定義污染源(source點),污染源能夠理解爲可以被用戶控制的輸入數據,這裏定義的Android污染源主要包括用戶的輸入、Intent傳入的數據,下面展現定義的部分污染源(source點):
- EditText
android/widget/EditText.getText()Landroid/text/Editable;:TAINTED
- Intent
android/content/Intent.getAction()Ljava/lang/String;:TAINTED
android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;:TAINTED
......
- Bundle
android/os/Bundle.get(Ljava/lang/String;)Ljava/lang/Object;:TAINTED
android/os/Bundle.getString(Ljava/lang/String;)Ljava/lang/String;:TAINTED
......複製代碼
定義好污染源後就須要肯定污染的觸發點(sink點),能夠理解爲會觸發危險操做的函數。定義sink點的方式有兩種,一種是直接從文件中導入,以命令注入爲示例,代碼以下:
public class CommandInjectionDetector extends BasicInjectionDetector {
public CommandInjectionDetector(BugReporter bugReporter) {
super(bugReporter);
loadConfiguredSinks("command.txt", "COMMAND_INJECTION");
}複製代碼
從代碼中能夠清楚的看到其導入方式是繼承BasicInjectionDetector類,而後再該類的構造方法中經過loadConfiguredSinks方法,導入包含sink點的文件,下面展現該示例文件中的內容:
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process;:0
java/lang/Runtime.exec([Ljava/lang/String;)Ljava/lang/Process;:0
java/lang/Runtime.exec(Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/Process;:0,1
java/lang/Runtime.exec([Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/Process;:0,1
java/lang/Runtime.exec(Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)Ljava/lang/Process;:1,2
java/lang/Runtime.exec([Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)Ljava/lang/Process;:1,2
java/lang/ProcessBuilder.<init>([Ljava/lang/String;)V:0
java/lang/ProcessBuilder.<init>(Ljava/util/List;)V:0
java/lang/ProcessBuilder.command([Ljava/lang/String;)Ljava/lang/ProcessBuilder;:0
java/lang/ProcessBuilder.command(Ljava/util/List;)Ljava/lang/ProcessBuilder;:0
dalvik/system/DexClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;:0複製代碼
另外一種是自定義導入,其實現是經過覆蓋BasicInjectionDetector類中的getInjectionPoint方法,以WebView.loadurl方法爲例,示例代碼以下所示:
@Override
protected InjectionPoint getInjectionPoint(InvokeInstruction invoke, ConstantPoolGen cpg, InstructionHandle handle) {
assert invoke != null && cpg != null;
String method = invoke.getMethodName(cpg);
String sig = invoke.getSignature(cpg);
// System.out.println(invoke.getClassName(cpg));
if(sig.contains("Ljava/lang/String;")) {
if("loadUrl".equals(method)){
if(sig.contains("Ljava/util/Map;")){
return new InjectionPoint(new int[]{1}, WEBVIEW_LOAD_DATA_URL_TYPE);
}else{
return new InjectionPoint(new int[]{0}, WEBVIEW_LOAD_DATA_URL_TYPE);
}
}else if("loadData".equals(method)){
return new InjectionPoint(new int[]{2}, WEBVIEW_LOAD_DATA_URL_TYPE);
}else if("loadDataWithBaseURL".equals(method)){
//BUG
return new InjectionPoint(new int[]{4}, WEBVIEW_LOAD_DATA_URL_TYPE);
}
}
return InjectionPoint.NONE;
}複製代碼
經過實例化InjectionPoint類構造新的sink點,其構造方法中的第一個參數表示該方法接收污染數據參數的位置,如方法爲webView.loadUrl(url),其第一個參數就是new int[]{0},其它的以此類推。
上報發現漏洞的狀況,則經過覆蓋getPriorityFromTaintFrame方法的實現,示例代碼以下所示:
@Override
protected int getPriorityFromTaintFrame(TaintFrame fact, int offset)
throws DataflowAnalysisException {
Taint stringValue = fact.getStackValue(offset);
// System.out.println(stringValue.getConstantValue());
if (stringValue.isTainted() || stringValue.isUnknown()) {
return Priorities.NORMAL_PRIORITY;
} else {
return Priorities.IGNORE_PRIORITY;
}
}
通複製代碼
過fact.getStackValue獲取檢測的函數變量,若是該變量被污染(isTainted)或 變量是否被污染未知(可是是可控制變量),那麼做爲一箇中危風險(Priorities.NORMAL_PRIORITY)進行上報,其它的狀況則上報爲可忽略風險(Priorities.IGNORE_PRIORITY)。
2.4 自定義代碼檢測
自定義代碼檢測實現的前半部分同2.2的逐方法檢測相似,均是獲取類的內容,而後遍歷全部的方法,對方法的內容進行檢測,可是在具體代碼檢測實現上是經過自定義分析進行。目前自定義檢測只應用到Android中本地拒絕服務的檢測。本地拒絕服務的被觸發的重要緣由在於對經過Intent獲取的參數未進行異常捕獲,所以檢測實現的方式即是檢測獲取參數的代碼行是否被try catch包裹(這個存在偏差,待改進)。對於其代碼分析,不能使用FindBugs模型進行分析,而是使用最原始的class代碼進行分析,原始class代碼的形式經過javap命令進行查看,下圖展現示例代碼。
對原始class文件進行分析存在的缺陷是沒法定位具體的代碼行,那麼在進行問題上報時沒法將問題定位到代碼行,所以第一步須要在原有模型的基礎上對全部包含Intent獲取參數的方法的位置存儲到一個Map結構中,方便後面對方法的定位,代碼實現以下所示,獲取方法所在的行,而後以方法名做爲Key值,以代碼行相關信息做爲Value值,存儲到Map中。
private Map<String, List<Location>> get_line_location(Method m, ClassContext classContext){
HashMap<String, List<Location>> all_line_location = new HashMap<>();
ConstantPoolGen cpg = classContext.getConstantPoolGen();
CFG cfg = null;
try {
cfg = classContext.getCFG(m);
} catch (CFGBuilderException e) {
e.printStackTrace();
return all_line_location;
}
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext(); ) {
Location loc = i.next();
Instruction inst = loc.getHandle().getInstruction();
if(inst instanceof INVOKEVIRTUAL) {
INVOKEVIRTUAL invoke = (INVOKEVIRTUAL) inst;
if(all_line_location.containsKey(invoke.getMethodName(cpg))){
all_line_location.get(invoke.getMethodName(cpg)).add(loc);
}else {
LinkedList<Location> loc_list = new LinkedList<>();
loc_list.add(loc);
all_line_location.put(invoke.getMethodName(cpg), loc_list);
}
// }
}
}
return all_line_location;
}複製代碼
以後獲取Exception包裹的範圍,FindBugs中包含對Exception的建模,所以可以經過其模型可以直接獲取其範圍並存儲到一個列表中,代碼以下所示,其中exceptionTable[i].getStartPC用於獲取try catch 的起始代碼行,exceptionTable[i].getEndPC用於獲取try catch 的結束代碼行。
public int[] getExceptionScope(){
try {
CodeException[] exceptionTable = this.code.getExceptionTable();
int[] exception_scop = new int[exceptionTable.length * 2];
for (int i = 0; i < exceptionTable.length; i++) {
exception_scop[i * 2] = exceptionTable[i].getStartPC();
exception_scop[i * 2 + 1] = exceptionTable[i].getEndPC();
}
return exception_scop;
}catch (Exception e){
}
return new int[0];
}複製代碼
在對代碼進行逐行檢查時,由於使用的是最原始class文件形式,所以須要限定其遍歷的範圍,限定的方式是經過代碼的行號,即上圖中每行代碼的第一個數值。首先須要獲取代碼總行數的大小,獲取的方式即是解析FindBugs建模後的第一行代碼,找到關鍵詞code-length後面的數值,即爲代碼的行數,解析代碼以下所示:
public int get_Code_Length(String firstLineCode){
try{
String[] split1 = firstLineCode.split("code_length");
// System.out.println(split1[split1.length-1]);
byte[] code_length_bytes = split1[split1.length-1].getBytes();
byte[] new_code_bytes = new byte[code_length_bytes.length];
for(int i=0; i<code_length_bytes.length; i++){
// System.out.println();
if(code_length_bytes[i]<48 || code_length_bytes[i]>57){
new_code_bytes[i] = 32;
}else{
new_code_bytes[i] = code_length_bytes[i];
}
}
return Integer.parseInt(new String(new_code_bytes).trim());
}catch(Exception e){
e.printStackTrace();
}
return 0;
}複製代碼
最後對代碼進行逐行遍歷,遍歷中爲防止try catch塊被遍歷到,使用行號來限制遍歷的範圍。檢測代碼行是否包含經過Intent獲取參數,及該行是否被try catch 包裹,若是上述兩個條件均被觸發,那麼就做爲一個問題進行上報。示例代碼以下,其中get_code_line_index方法用於獲取代碼的行號,獲取的方式是截取代碼行的首字符的數值,以肯定是否在代碼包裹的範圍內。
private void analyzeMethod(JavaClass javaClass, Method m, ClassContext classContext) throws CFGBuilderException {
HashMap<String, List<Location>> all_line_location = (HashMap<String, List<Location>>) get_line_location(m, classContext);
Code code = m.getCode();
StringCodeAnalysis sca = new StringCodeAnalysis(code);
String[] codes = sca.codes_String_Array();
int code_length = sca.get_Code_Length(sca.get_First_Code(codes));
int[] exception_scop = sca.getExceptionScope();
for(int i=1; i<codes.length; i++){
int line_index = sca.get_code_line_index(codes[i]);
if (line_index < code_length){
if(codes[i].toLowerCase().contains("invokevirtual") &&
(codes[i].contains("android.content.Intent.get") || codes[i].contains("android.os.Bundle.get"))){
if(exception_scop.length == 0){
......
}else{
boolean is_scope = false;
for(int j=0; j<exception_scop.length; j+=2){
int start = exception_scop[j];
int end = exception_scop[j+1];
if(line_index >= start && line_index <= end){
is_scope = true;
}
if(is_scope){
break;
}
}
if(!is_scope){
String method_name = get_method_name(codes[i]);
if(all_line_location.containsKey(method_name)){
for(Location loc : all_line_location.get(method_name)){
bugReporter.reportBug(new BugInstance(this, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY).addClass(javaClass).addMethod(javaClass, m).addSourceLine(classContext, m, loc));
}
}else {
bugReporter.reportBug(new BugInstance(this, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY).addClass(javaClass).addMethod(javaClass, m));
}
}
}
}
}
}
}
複製代碼
3 註冊打包
上面詳細敘述瞭如何構造本身的問題檢測代碼,完成檢測方法的書寫後,下一步就是在配置文件中對檢測方法進行註冊,才能使檢測代碼運轉起來。
須要在兩個文件中進行註冊,第一個是findbugs.xml,註冊示例以下:
<Detector class="com.h3xstream.findsecbugs.android.LocalDenialOfServiceDetector" reports="LOCAL_DENIAL_SERVICE"/>
<BugPattern type="LOCAL_DENIAL_SERVICE" abbrev="SECLDOS" category="Android安全問題" cweid="276"/>複製代碼
其中Detector用於註冊該檢測方法的位置及其惟一標識,BugPattern用於對檢測出的問題進行歸類,方便展現,如此處歸類到"Android安全問題"中,那麼在生成報告的時候問題也將被歸類到"Android安全問題"中。
第二個是messages.xml註冊,註冊示例以下,該註冊主要是對該問題進行說明,包括問題的危害及修復方法。
<Detector class="com.h3xstream.findsecbugs.android.LocalDenialOfServiceDetector">
<Details>Local複製代碼
一切完成就緒後使用Maven進行打包,就生產了供FindBugs集成開發工具插件使用的JAR包,完成安裝並重啓,便可使用自定義插件對特定問題進行檢測。
4 結語
本文介紹了Android集成開發環境Android Studio的代碼實時檢測工具Code Arbiter的產生緣由及代碼實現,最後展現了分析的效果。經過Code Arbiter在生產環境中的應用,其檢測效果仍是至關不錯,可以發現不少編碼過程當中存在的問題。可是Code Arbiter仍然存在許多不足,須要優化。後續將在如下兩個方面對工具進行改進:
- 擴大漏洞檢測範圍,使Code Arbiter可以囊括Android編碼常見安全問題;
- 優化漏洞檢測規則,提升檢測的準確性,減小誤報。