引子
通過《OpenRASP Java源碼分析與總結(一)——啟動與檢測》的分析,我們對OpenRASP Java部分的整體啟動和檢測流程有了大緻的了解。本文将對OpenRASP所支援的js插件、安全基線等幾類檢測方式中,選取幾個比較有代表性的檢測進行詳細分析。
準備
1
- agent Java代碼包結構
/+ |-boot // rasp.jar代碼所在目錄 | +-engine+ // rasp-engine.jar代碼所在目錄 +src+ +main+ +java+ +com.baidu.openrasp+ |-config // 配置代碼 |-hook+ // hoot代碼,主要分析這個目錄下的代碼 | |-file // 檔案hook | |-server // 伺服器hook | |-sql // sql hook | +-ssrf // ssrf hook | |-plugin+ // 插件代碼 | |-antlr // sql文法分析代碼 | |-checker+ // checker代碼 | | |-js // js checker代碼 | | |-local | | +-policy // 安全基線checker代碼 | +-js.engine // js引擎代碼 | |-tool +-transformer
- 《OpenRASP Java源碼分析與總結(一)——啟動與檢測》中介紹過檢測的基本流程,入口為hook,hook再委托給checker。是以,下文的分析也将按照此流程進行:先分析入口hook,再分析對應的checker。
- 分析過程中涉及一些正常Web伺服器、JDBC規範、SQL文法規則,本文不做詳細分析,請自行學習了解。
- 分析過程中涉及到的配置和代碼,均為精簡過,去掉了和分析無關的内容,是以和真正源碼并非一一對應,但對了解整個過程已經足夠了。
源碼分析
2
Web請求檢測
2.1
Tomcat是Java生态圈裡最常見和常用的Web容器,它實作了Servlet規範(版本不同實作的規範版本也不同),所有的Web請求均由Tomcat接收解析後,調用使用者程式進行處理。是以對Web請求檢測可以從Tomcat相關的Hook進行入手,ApplicationFilterHook就是其中一個:
代碼2-1
@HookAnnotationpublic class ApplicationFilterHook extends ServerRequestHook { // 1 @Override public boolean isClassMatched(String className) { return className.endsWith("apache/catalina/core/ApplicationFilterChain"); } @Override protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { // 2 String src = getInvokeStaticSrc(ServerRequestHook.class, "checkRequest", "$0,$1,$2", Object.class, Object.class, Object.class); // 3 insertBefore(ctClass, "doFilter", null, src); }}public abstract class ServerRequestHook extends AbstractClassHook { // 4 public static void checkRequest(Object filter, Object request, Object response) { HookHandler.checkRequest(filter, request, response); }}
- 判斷入參類是否需要被hoook。這裡檢查的是Tomcat對Servlet規範中FilterChain的實作類ApplicationFilterChain。通過Tomcat部署的Web應用的所有HTTP請求均會經過ApplicationFilterChain,是以以此類作為hook入口非常适合;
- 擷取靜态方法HookHandler.checkRequest()的源碼;
- 将2中的代碼插入到執行個體方法ApplicationFilterChain.doFilter()的開始處。ApplicationFilterChain.doFilter()為HTTP請求的入口方法,在這裡可以對所有的HTTP請求進行檢測;
- 調用HookHandler.checkRequest()進行請求檢測。
代碼2-2
public class HookHandler { public static void checkRequest(Object servlet, Object request, Object response) { // 1 HttpServletRequest requestContainer = new HttpServletRequest(request); // 2 HttpServletResponse responseContainer = new HttpServletResponse(response); // 3 responseContainer.setHeader(OPEN_RASP_HEADER_KEY, OPEN_RASP_HEADER_VALUE); // 4 responseContainer.setHeader(REQUEST_ID_HEADER_KEY, requestContainer.getRequestId()); // 5 requestCache.set(requestContainer); // 6 responseCache.set(responseContainer); // 7 doCheck(CheckParameter.Type.REQUEST, JSContext.getUndefinedValue()); }}
- 包裝原有的request對象為自定義的request;
- 包裝原有的response對象為自定義的response;
- 增加請求頭X-Protected-By,值為OpenRASP;
- 增加請求頭X-Request-ID,值為一個UUID;
- 儲存自定義的request到目前線程上下文中;
- 儲存自定義的response到目前線程上下文中;
- 檢測請求。
根據7中的第一個入參,查找CheckParameter.Type:
public class CheckParameter { public enum Type { ..., REQUEST("request", new JsChecker()), ...;}
可以得知,CheckParameter.Type.REQUEST對應的Checker實作為JsChecker。是以,最終檢測邏輯即委托給了JsChecker。在《OpenRASP Java源碼分析與總結(一)——啟動與檢測》中我們曾分析過,JsChecker的檢測邏輯實際又委托給了plugins目錄下的js實作。
OpenRASP官方僅提供了幾個請求檢測的Demo插件作為示範使用,如果要投入到實際生産環境,還需要做進一步的開發。下面,我們分别看下官方提供的兩個Demo插件,了解下請求檢測的大緻邏輯,分别是plugins/addons/001-xss-demo.js和plugins/addons/002-detect-scanner.js:
代碼2-3
var plugin = new RASP('offical')// 1plugin.register('request', function(params, context) { // 2 function detectXSS(params, context) { // 3 var xssRegex = /||javascript:(?!(?:history\.(?:go|back)|void\(0\)))/i var parameters = context.parameter; var message = ''; // 4 Object.keys(parameters).some(function (name) { parameters[name].some(function (value) { if (xssRegex.test(value)) { message = 'XSS 攻擊: ' + value; return true; } }); }); return message } // 5 var message = detectXSS(params, context) if (message.length) { return {action: 'block', message: message, confidence: 90} } return clean })
- 注冊一個request檢測函數;
- 定義一個XSS檢測函數;
- 定義XSS檢測的正規表達式;
- 循環請求裡的每個參數,判斷是否比對XSS正規表達式;
- 調用XSS檢測函數,如果命中,則傳回命中資訊。
代碼2-4
var plugin = new RASP('offical')plugin.register('request', function(params, context) { var foundScanner = false // 1 var scannerUA = [..., "bsqlbf", "sqlmap", "nessus", "arachni", "metis", ...] var headers = context.header // 2 var ua = headers['user-agent'] if (ua) { // 3 for (var i = 0; i < scannerUA.length; i++) { if (ua.indexOf(scannerUA[i].toLowerCase()) != -1) { foundScanner = true break } } } // 4 if (foundScanner) { return {action: 'block', message: '已知的掃描器探測行為,UA 特征為: ' + scannerUA[i], confidence: 90} } return clean})
- 定義掃描器可能産生的特定浏覽器User-Agent(以下簡稱為UA)請求頭;
- 從請求中擷取UA請求頭;
- 循環1中定義的掃描器UA,判斷是否和2中的UA進行比較;
- 如果命中掃描器UA,則傳回命中資訊。
通過以上Demo的分析,可以得知,如果要自行編寫Web請求檢測的插件,需要注冊一個request檢測函數,通過檢測函數的第二個入參context(其實就是OpenRASP自定義的HttpServletRequest對象)擷取請求頭、請求參數等資訊,對請求資訊進行檢測即可。通過自行編寫的請求檢測插件,可以完成諸如XSS、掃描器、異常請求頭等檢測。
基于文法分析的SQL檢測
2.2
Java通過JDBC來通路資料庫,而JDBC隻是一套規範和接口,不同的資料庫廠商會根據JDBC規範實作自己的通路邏輯。是以,RASP SQL檢測其實就是在不同資料庫的JDBC實作(主要是Statement、PrepareStatement接口的實作)中注入對SQL的檢測邏輯。
下面我們就來分析下OpenRASP是如何在Statement中完成SQL的檢測(PrepareStatement同理),分析的入口類為SQLStatementHook:
代碼2-5
@HookAnnotationpublic class SQLStatementHook extends AbstractSqlHook { // 1 public static LRUCache sqlCache = new LRUCache(); @Override public boolean isClassMatched(String className) { // 2 if ("com/mysql/jdbc/StatementImpl".equals(className) || "com/mysql/cj/jdbc/StatementImpl".equals(className)) { this.type = "mysql"; this.exceptions = new String[]{"java/sql/SQLException"}; return true; } ... return false; } @Override protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { // 3 String checkSqlSrc = getInvokeStaticSrc(SQLStatementHook.class, "checkSQL", "\"" + type + "\"" + ",$0,$1", String.class, Object.class, String.class); // 4 insertBefore(ctClass, "execute", checkSqlSrc, new String[]{"(Ljava/lang/String;)Z", "(Ljava/lang/String;I)Z", "(Ljava/lang/String;[I)Z", "(Ljava/lang/String;[Ljava/lang/String;)Z"}); ... } public static void checkSQL(String server, Object statement, String stmt) { // 5 if (!sqlCache.isContainsKey(stmt)) { JSContext cx = JSContextFactory.enterAndInitContext(); Scriptable params = cx.newObject(cx.getScope()); params.put("server", params, server); params.put("query", params, stmt); HookHandler.doCheck(CheckParameter.Type.SQL, params); } }}
- 定義一個SQL緩存,用于存放已經檢測過可放行的SQL;
- 檢查入參的類是否需要被hook。這裡檢查的是各個資料庫的JDBC驅動對Statement接口的具體實作類,比如MySQL的com.mysql.jdbc.StatementImpl;
- 擷取靜态方法SQLStatementHook.checkSQL()的源碼;
- 将4中的代碼插入到執行個體方法StatementImpl.execute()的開始處。StatementImpl.execute()總共有4個同名方法,是以insertBefore()的第四個入參提供了對應的4個不同的方法簽名;
- 判斷是否命中SQL緩存,命中則說明該SQL可放行,未命中則調用HookHandler.doCheck()。除了execute()之外,executeUpdate()、executeQuery()和addBatch()也被注入了SQLStatementHook.checkSQL()。
通過上述代碼,實作了類似如下的邏輯(以MySQL的StatementImpl.execute(String sql)為例):
public StatementImpl implements Statement { public boolean execute(String sql) { // 插入的SQLStatementHook.checkSQL(this, sql),展開後的代碼 if (!sqlCache.isContainsKey(sql)) { JSContext cx = JSContextFactory.enterAndInitContext(); Scriptable params = cx.newObject(cx.getScope()); params.put("server", params, server); params.put("query", params, sql); HookHandler.doCheck(CheckParameter.Type.SQL, params); } // 原有邏輯 ... }}
根據5中的第一個入參,查找CheckParameter.Type:
public class CheckParameter { public enum Type { ..., SQL("sql", new SqlStatementChecker()), ...;}
可以得知,CheckParameter.Type.REQUEST對應的Checker實作為SqlStatementChecker。是以,最終檢測邏輯即委托給了SqlStatementChecker,下面進行分析:
代碼2-6
public class SqlStatementChecker extends ConfigurableChecker { public List checkSql(CheckParameter checkParameter, Map parameterMap, JsonObject config) { List result = new LinkedList(); String query = (String) checkParameter.getParam("query"); String message = null; // 1 String[] tokens = TokenGenerator.detailTokenize(query, new TokenizeErrorListener()); // 2 for (Map.Entry entry : parameterMap.entrySet()) { String value = entry.getValue()[0]; // 3 int para_index = query.indexOf(value); if (para_index < 0) { continue; } // 4 int start = tokens.length, end = tokens.length, distance = 2; ... // 5 if (end - start > distance) { message = "SQLi - SQL query structure altered by user input, request parameter name: " + entry.getKey(); } } if (message != null) { result.add(AttackInfo.createLocalAttackInfo(checkParameter, action, message, "sqli_userinput", 90)); } else { // 6 for (String token : tokens) { if (token.equals("select")) { int nullCount = 0; // 7 for (int j = i + 1; j < tokens.length && j < i + 6; j++) { if (tokens[j].equals(",") || tokens[j].equals("null") || StringUtils.isNumeric(tokens[j])) { nullCount++; } else { break; } } // 8 if (nullCount >= 5) { message = "SQLi - Detected UNION-NULL phrase in sql query"; break; } } if (token.equals(";") && i != tokens.length - 1) { // 9 message = "SQLi - Detected stacked queries"; break; } else if (token.startsWith("0x")) { // 10 message = "SQLi - Detected hexadecimal values in sql query"; break; } else if (token.startsWith("/*!")) { // 11 message = "SQLi - Detected MySQL version comment in sql query"; break; } else if (i < tokens.length - 2 && tokens[i].equals("into") && (tokens[i + 1].equals("outfile") || tokens[i + 1].equals("dumpfile"))) { // 12 message = "SQLi - Detected INTO OUTFILE phrase in sql query"; break; } else if (i < tokens.length - 1 && tokens[i].equals("from"))) { // 13 String[] parts = tokens[i + 1].replace("`", "").split("\\."); if (parts.length == 2) { String db = parts[0].trim(); String table = parts[1].trim(); if (db.equals("information_schema") && table.equals("tables")) { message = "SQLi - Detected access to MySQL information_schema.tables table"; break; } } } if (message != null) { result.add(AttackInfo.createLocalAttackInfo(checkParameter, action, message, "sqli_policy", 100)); } } } return result; }}
1.對SQL進行文法分析(嚴格來說,這裡隻完成了詞法分析),擷取分析後的token(詞)。例如對SQL:select c2 from t1 where c1 = 'a',進行文法分析後,可以得到如下的token表:
2.循環使用者的輸入,檢測每個輸入項;
3.擷取使用者輸入項在SQL中的索引位置,如果未找到,則進入下一輪循環;
4.定義并計算使用者輸入項在token表中的start(開始索引)和end(結束索引),以及定義可能發生SQL注入時,使用者輸入項所占的最少token項,這裡為2。例如SQL:select c2 from t1 where c1 = '${input}',${input}為使用者輸入項:
- 正常的使用者輸入,${input} = a:
- 存在SQL注入的使用者輸入,${input} = ' or '1' = '1:
- 上面的例子中,正常的使用者輸入項隻會占7這一索引的token,而當使用者輸入項發生SQL注入的時候,占用了7~11總共4個索引的token。是以,可以通過計算使用者輸入項所占的token項(11-7=4)來判定使用者的輸入是否可能産生SQL注入。
5.判斷使用者輸入項所占的token是否大于2,大于2則可能為攻擊。為什麼沒有直接使用大于1來判定是否産生SQL注入?個人覺得可能的原因為,為了産生SQL注入,首先要對SQL中兩個字元引号(即where c1 = '${input}'中的兩個字元單引号')進行閉合(' or '1' = '1中的第一個和最後一個'分别用于閉合where c1 = '${input}'中的兩個引号),閉合後自然産生了兩個token。由此可知,為了産生SQL注入,使用者輸入項所占的token項必須大于2;
6.循環每個token,檢測token是否符合安全政策;
7.計算連續出現null、,或者數字的token。這裡的檢測是為了防止可能為UNION查詢攻擊,例如:select c1, c2, c3, c4 from t1 where c1 = 'a' union select null, null, null, c5 from t2 where c6 = 'b';
8.判斷連續出現的null、,或者數字的token是否大于5,大于5則可能為攻擊。選用5應該是出于經驗,null, null, null、1, 2, 3,這些token均為5,太小,容易産生誤判,因為偶爾也會有特殊場景下需要使用null占一個或兩個查詢字段。個人覺得可以再加入union這個token作為判斷依據會更精确;
9.判斷非最後一個token是否為;,如果是,則可能為攻擊。這裡的檢測是為了防止可能為堆疊查詢,例如select c1 from t1 where c1 = 'a'; select c2 from t2;
10.判斷token是否為十六進制符号,如果是,則可能為攻擊;
11.判斷token是否包含/*!,如果包含,則可能為攻擊。這裡的檢測是為了防止可能為内聯注釋攻擊;
12.判斷目前和下一個token是否為into outfile或者into dumpfile,如果是,則可能為攻擊。這裡的檢測是為了防止可能為檔案導出攻擊;
13.判斷目前和下一個token是否為from information_schema.tables,如果是,則可能為攻擊。
注:上述分析中涉及到的攻擊方式不做詳細解釋,請自行百度。
由上述分析可以得知,OpenRASP的SQL檢測是基于對SQL的文法分析。相比傳統的,特别是WAF,基于關鍵字、正規表達式比對的方式,基于文法分析由于從語言層面去分析和了解SQL,可以做到更加的精準,減少誤殺。當然,基于文法分析帶來的問題是,更大的性能損耗和記憶體占用(需要建構一棵完整的文法樹)。
安全基線檢查
2.3
代碼2-7
@HookAnnotationpublic class TomcatStartupHook extends ServerStartupHook { @Override public boolean isClassMatched(String className) { // 1 return "org/apache/catalina/startup/Catalina".equals(className); } @Override protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { // 2 String src = getInvokeStaticSrc(TomcatStartupHook.class, "checkTomcatStartup", ""); insertBefore(ctClass, "start", null, src); } public static void checkTomcatStartup() { // 3 HookHandler.doCheckWithoutRequest(CheckParameter.Type.POLICY_TOMCAT_START, CheckParameter.EMPTY_MAP); }}
- 判斷入參類是否需要被hook。這裡檢查的是Tomcat啟動的核心類Catalina;
- 擷取靜态方法TomcatStartupHook.checkTomcatStartup()的源碼,插入到執行個體方法Catalina.start()開始處;
- 調用靜态方法HookHandler.doCheckWithoutRequest()進行檢測。
根據3中的第一個入參,查找CheckParameter.Type:
public class CheckParameter { public enum Type { ..., POLICY_TOMCAT_START("tomcatStart", new TomcatSecurityChecker()), ...;}
可以得知,CheckParameter.Type.POLICY_TOMCAT_START對應的Checker實作為TomcatSecurityChecker。是以,最終檢測邏輯即委托給了TomcatSecurityChecker,下面進行分析:
代碼2-8
public abstract class ServerPolicyChecker extends PolicyChecker { @Override public List checkParam(CheckParameter checkParameter) { List infos = new LinkedList(); // 1 checkStartUser(infos); checkServer(checkParameter, infos); return infos; } private void checkStartUser(List infos) { String osName = System.getProperty("os.name").toLowerCase(); if (osName.startsWith("linux") || osName.startsWith("mac")) { // 2 if ("root".equals(System.getProperty("user.name"))) { infos.add(new SecurityPolicyInfo(SecurityPolicyInfo.Type.START_USER, "Java security baseline - should not start application server with root account", true)); } } else if (osName.startsWith("windows")) { // 3 Class ntSystemClass = Class.forName("com.sun.security.auth.module.NTSystem"); Object ntSystemObject = ntSystemClass.newInstance(); String[] userGroups = (String[]) ntSystemClass.getMethod("getGroupIDs").invoke(ntSystemObject); for (String group : userGroups) { // 4 if (group.equals("S-1-5-32-544")) { infos.add(new SecurityPolicyInfo(SecurityPolicyInfo.Type.START_USER, "Java security baseline - should not start application server with Administrator/system account", true)); } } } } public abstract void checkServer(CheckParameter checkParameter, List infos);}
- Server安全基線檢查的基類,包含使用者、Server自檢查,其中Server自檢查留給子類實作;
- 如果系統為Linux或者Mac,如果目前使用者為root,則記錄觸發安全基線;
- 加載com.sun.security.auth.module.NTSystem類,通過該類可以擷取Windows NT系統的安全資訊;
- 通過NTSystem.getGroupIDs()擷取目前使用者組資訊,如果為S-1-5-32-544(Windows NT系統上最高權限Administrators使用者組),則觸發安全基線。
代碼2-9
public class TomcatSecurityChecker extends ServerPolicyChecker { @Override public void checkServer(CheckParameter checkParameter, List infos) { // 1 String tomcatBaseDir = System.getProperty("catalina.base"); checkHttpOnlyIsOpen(tomcatBaseDir, infos); checkManagerPassword(tomcatBaseDir, infos); checkDirectoryListing(tomcatBaseDir, infos); checkDefaultApp(tomcatBaseDir, infos); } private void checkHttpOnlyIsOpen(String tomcatBaseDir, List infos) { // 2 File contextFile = new File(tomcatBaseDir + File.separator + "conf/context.xml"); Element contextElement = getXmlFileRootElement(contextFile); // 3 String httpOnly = contextElement.getAttribute("useHttpOnly"); boolean isHttpOnly = true; if (httpOnly != null && httpOnly.equals("false")) { isHttpOnly = false; } // 4 if (!isHttpOnly) { infos.add(new SecurityPolicyInfo(Type.COOKIE_HTTP_ONLY, "Tomcat security baseline - httpOnly should be enabled in " + contextFile.getAbsolutePath(), true)); } }}
- 擷取Tomcat安裝根目錄,我們先隻分析checkHttpOnlyIsOpen();
- 擷取根目錄下的conf/context.xml檔案,并解析XML;
- 擷取根元素的userHttpOnly屬性的值;
- 如果userHttpOnly為false,則觸發安全基線。
除了HttpOnly的檢查,TomcatSecurityChecker.checkServer()裡還包括管理者密碼、目錄清單等安全配置檢查。
從上述兩段代碼可以得知,OpenRASP Java的安全基線檢查和普通的Java程式并沒有任何差別。當然,也正因為和普通的Java程式沒有差別,是以OpenRASP也隻能完成運作目前應用程式的使用者所能完成的檢查,比如一些需要特殊使用者權限才能完成的檢查。
其他檢查
2.4
代碼2-10
public class SQLResultSetHook extends AbstractSqlHook { @Override public boolean isClassMatched(String className) { // 1 if ("com/mysql/jdbc/ResultSetImpl".equals(className) || "com/mysql/cj/jdbc/result/ResultSetImpl".equals(className)) { this.type = "MySQL"; return true; } ... return false; } @Override protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { // 2 String src = getInvokeStaticSrc(SQLResultSetHook.class, "checkSqlQueryResult", "\"" + type + "\"" + ",$0", String.class, Object.class); insertBefore(ctClass, "next", "()Z", src); } public static void checkSqlQueryResult(String server, Object sqlResultSet) { ResultSet resultSet = (ResultSet) sqlResultSet; // 3 int queryCount = resultSet.getRow(); HashMap params = new HashMap(4); params.put("query_count", queryCount); params.put("server", server); // 4 HookHandler.doCheck(CheckParameter.Type.SQL_SLOW_QUERY, params); }}
- 判斷入參類是否需要被hook。這裡檢查的是MySQL JDBC驅動ResultSet接口的實作類ResultSetImpl;
- 擷取靜态方法SQLResultSetHook.checkSqlQueryResult()的源碼,插入到執行個體方法ResultSetImpl.next()開始處,即擷取資料庫查詢結果的方法開始處;
- 擷取查詢結果傳回的總條數;
- 調用靜态方法HookHandler.doCheck()進行檢測。
根據3中的第一個入參,查找CheckParameter.Type:
public class CheckParameter { public enum Type { ..., SQL_SLOW_QUERY("sqlSlowQuery", new SqlResultChecker(false)), ...;}
可以得知,CheckParameter.Type.SQL_SLOW_QUERY對應的Checker實作為SqlResultChecker。是以,最終檢測邏輯即委托給了SqlResultChecker,下面進行分析:
代碼2-11
public class SqlResultChecker extends AttackChecker { @Override public List checkParam(CheckParameter checkParameter) { LinkedList result = new LinkedList(); // 1 Integer queryCount = (Integer) checkParameter.getParam("query_count"); // 2 int slowQueryMinCount = Config.getConfig().getSqlSlowQueryMinCount(); // 3 if (queryCount == slowQueryMinCount) { result.add(AttackInfo.createLocalAttackInfo(checkParameter, EventInfo.CHECK_ACTION_INFO, "慢查詢: 使用SELECT語句讀取了大于等于" + slowQueryMinCount + "條資料", "slow query")); } return result; }}
- 擷取查詢結果傳回的總條數;
- 從配置檔案(RELEASE版本的conf/rasp.properties)中的SQL慢查詢最小條數,配置項為sql.slowquery.min_rows,預設配置值為500;
- 判斷實際查詢傳回的總條數是否大于等于配置值(源碼中為==,BUG),大于等于則記錄安全檢查結果。
總結
3
經過上述的分析,我們了解到,OpenRASP:
- 通過往特定Web容器實作中注入檢測邏輯,可以完成對所有Web請求的檢測;
- 通過往特定JDBC驅動實作中注入檢測邏輯,可以完成對所有SQL的檢測。其中,SQL的檢測基于文法分析;
- 通過往特定類中注入檢測邏輯,可以完成安全基線的檢查,以及一些非安全相關的檢查。
因為RASP天然具備擷取應用運作過程中上下文,是以RASP具備了解應用上下文的能力。在了解應用上下文的前提下,可以擷取到精準的應用資料進行檢測,檢測的結果可以直接回報給應用,應用依據回報結果進行決策。