上篇关于《SpringInspector源码分析文章》中提到过“污点传播过程中清洗函数怎么定义?能否通过自然语言处理提取其特征来自动化检测?”,近段时间才得空能接着上次的想法继续思考,由于SpringInspector检测工具对规则的依赖性比较强,因此检测污点分析中的过滤函数也是精进工具必不可少的一环。
常用的程序代码检测技术主要分为两类,即:
属性计数(Attribute Counting,AC):针对程序代码的各种统计属性进行处理,从程序代码中提取多个软件度量特征,然后再用这些特征比较程序从而得到程序相似度。该方法也是目前为止软件科学度量方法中最早的典型属性计数法。
基于结构度量(Structural Metrics,SM):该方法加入了程序内部结构信息的比较。
而目前的匹配技术来实现相似度计算的方法有:
点阵图(dotplot)
余弦相似度
Levenshtein距离法
最长公共子序列(Longest Common Subsequence,LCS)
求最长公共子串RKR-GST法
而对此代码相似度的检测方案也有两个思路:
权重比较法,获取代码中的关键词来比较权重。
解析AST,并使用深度优先遍历DFT来生成文本向量,之后再对文本向量进行相似度分析。
关键词权重比较方法是传统意义上的相似度比较方法,是通过捕获源代码中是否存在某些关键词,然后更具优先程度来计算余弦相似度的一种方式。
其中正则匹配阶段有使用JavaParase的方法读取其方法的属性,进一步准确的提取相关特征。
删除注释的方式可以通过正则匹配,过滤掉单行和多行注释
String code = n.getBody().get().toString().replaceAll("(?<!:)\\/\\/.*", "").replaceAll("\\/\\*(\\s|.)*?\\*\\/", "");
System.out.println(code);
有了过滤完的代码段之后,就只需要对其提取特征并判断
float isName = (n.getName().toString().toLowerCase().contains("xss"))? 1 : 0; //方法名称中是否有Xss关键词
Parameter p = n.getParameters().get(0);
float isParam = (p.getTypeAsString().contains("String"))? 1 : 0; //参数中是否有String类型
String code = n.getBody().get().toString().replaceAll("(?<!:)\\/\\/.*", "").replaceAll("\\/\\*(\\s|.)*?\\*\\/", ""); //过滤方法体中的注释
Pattern pat = Pattern.compile("([a-zA-Z0-9_.&%]+)+"); //实例化Pattern对象
Matcher mat = pat.matcher(code); //实例化Matcher对象
float isReplace = 0;
float isScript = 0;
float isOnEvent = 0;
float isUnEscapeHtml4 = 0;
while(mat.find()) {
String str = mat.group().toLowerCase();
if(str.contains("replace")) { //方法体是否有调用Replace方法
isReplace = 1;
}
if(str.contains("script")) { //常见Script标签
isScript = 1;
}
if(str.equals("onmouseover") || str.equals("onclick") || str.equals("onunload") || str.equals("onerror")) { //常见的事件
isOnEvent = 1;
}
if(str.contains("StringEscapeUtils") || str.contains("unescapeHtml4")) { //HTML实体化方法
isUnEscapeHtml4 = 1;
}
}
flag = new float[]{(float) isName, isParam, isReplace, isScript, isOnEvent, isUnEscapeHtml4};
上述代码片段展示了是如何提取相关方法特征的,最后通过余弦相似度算法计算
package javaTest;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
public class SimilarityMain {
public static ArrayList<Float> vb = new ArrayList() {{
add(1);
add(1);
add(1);
add(1);
add(1);
add(1);
}};
public static float flag[] = {(float) 0, 0, 0, 0, 0, 0};
public static ArrayList<String> vitem = new ArrayList() {{
add("方法名称中有Xss关键词");
add("参数中有String类型");
add("有调用Replace方法");
add("常见Script标签");
add("常见的事件");
add("HTML实体化方法");
}};
public static double similarity(ArrayList va, ArrayList vb) {
if (va.size() > vb.size()) {
int temp = va.size() - vb.size();
for (int i = 0; i < temp; i++) {
vb.add(0);
}
} else if (va.size() < vb.size()) {
int temp = vb.size() - va.size();
for (int i = 0; i < temp; i++) {
va.add(0);
}
}
int size = va.size();
double simVal = 0;
double num = 0;
double den = 1;
double powa_sum = 0;
double powb_sum = 0;
for (int i = 0; i < size; i++) {
double a = Double.parseDouble(va.get(i).toString());
double b = Double.parseDouble(vb.get(i).toString());
num = num + a * b;
powa_sum = powa_sum + (double) Math.pow(a, 2);
powb_sum = powb_sum + (double) Math.pow(b, 2);
}
double sqrta = (double) Math.sqrt(powa_sum);
double sqrtb = (double) Math.sqrt(powb_sum);
den = sqrta * sqrtb;
simVal = num / den;
return simVal;
}
public static void generateArrayList(String file) throws FileNotFoundException {
FileInputStream in = new FileInputStream(file);
CompilationUnit cu = StaticJavaParser.parse(in);
new MethodVisitor().visit(cu, null);
}
/**
* Simple visitor implementation for visiting MethodDeclaration nodes.
*/
private static class MethodVisitor extends VoidVisitorAdapter {
@Override
public void visit(MethodDeclaration n, Object arg) {
float isName = (n.getName().toString().toLowerCase().contains("xss"))? 1 : 0; //方法名称中是否有Xss关键词
Parameter p = n.getParameters().get(0);
float isParam = (p.getTypeAsString().contains("String"))? 1 : 0; //参数中是否有String类型
String code = n.getBody().get().toString().replaceAll("(?<!:)\\/\\/.*", "").replaceAll("\\/\\*(\\s|.)*?\\*\\/", ""); //过滤方法体中的注释
Pattern pat = Pattern.compile("([a-zA-Z0-9_.&%]+)+"); //实例化Pattern对象
Matcher mat = pat.matcher(code); //实例化Matcher对象
float isReplace = 0;
float isScript = 0;
float isOnEvent = 0;
float isUnEscapeHtml4 = 0;
while(mat.find()) {
String str = mat.group().toLowerCase();
if(str.contains("replace")) { //方法体是否有调用Replace方法
isReplace = 1;
}
if(str.contains("script")) { //常见Script标签
isScript = 1;
}
if(str.equals("onmouseover") || str.equals("onclick") || str.equals("onunload") || str.equals("onerror")) { //常见的事件
isOnEvent = 1;
}
if(str.contains("StringEscapeUtils") || str.contains("unescapeHtml4")) { //HTML实体化方法
isUnEscapeHtml4 = 1;
}
}
flag = new float[]{(float) isName, isParam, isReplace, isScript, isOnEvent, isUnEscapeHtml4};
ArrayList<Float> va = new ArrayList();
for (int i = 0; i < flag.length; i++)
{
va.add(new Float(flag[i]));
}
SimilarityMain sim = new SimilarityMain();
double simVal = sim.similarity(va, vb);
System.out.println("Method Name:" + n.getName() + ", The sim value is:" + simVal);
for(float a : flag) {
System.out.print(a+" ");
}
System.out.println();
}
}
public static void main(String[] args) throws FileNotFoundException {
System.out.print("特征列表");
System.out.println(vitem);
generateArrayList("C:\\Users\\DELL\\eclipse-workspace\\javaTest\\src\\javaTest\\Hello.java"); //0.81
}
}
首先对Java代码进行预处理,减少冗余信息。然后对预处理后的代码进行词法分析、语义分析等步骤,最终生成AST语法树。之后再对生成好的AST转换成字符串序列,再对字符串序列进行相似度计算,检测代码是否相似。
AST解析工具我这里用的是ANTLR4的库类,使用专门的词法分析和语法分析来解析Java源代码。
Antlr (ANother Tool for Language Recognition) 是一个强大的跨语言语法解析器,可以用来读取、处理、执行或翻译结构化文本或二进制文件。它被广泛用来构建语言,工具和框架。Antlr可以从语法上来生成一个可以构建和遍历解析树的解析器。
添加如下依赖
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.9.3</version>
</dependency>
我这里的开发环境IDE是用的eclipse,因此还需要安装一个ANTLR的工具antlr4ide-eclipse-release[3]
安装之后就可以通过*.g4的文件规则生成对应的ANTLR4的词法分析和语法分析类了
插件正常安装之后,可以对*.g4的文件直接生成对应的类了。这里我给出Java8语法下的代码分析规则:
Java8Lexer(词法分析):https://github.com/antlr/grammars-v4/blob/master/java/java8/Java8Lexer.g4
Java8Parser(语法分析):https://github.com/antlr/grammars-v4/blob/master/java/java8/Java8Parser.g4
生成好了之后,可以用ANTLR4生成Java代码的字符串序列,方便之后进行相似度分析。
public static String getAntlr4String(String path) throws IOException {
FileInputStream inputStream = new FileInputStream(path); //获取java源代码文件的路径
Lexer lexer = new Java8Lexer(CharStreams.fromStream(inputStream)); //利用自定义的词法分析器
TokenStream tokenStream = new CommonTokenStream(lexer); //获取词法分析器生成的Token
Java8Parser parser = new Java8Parser(tokenStream); //语法解析器
ParserRuleContext t = parser.compilationUnit();
return t.toStringTree(parser); //最后讲生成的AST转换为字符串序列组,方便后续进行相似度计算
}
在这之后该方法会返回字符串序列
这里我用到文本相似度分析框架:https://github.com/shibing624/similarity
采用的是similarity框架的段落文本相似度分析计算
public static void main(String[] args) throws ExitException {
System.out.println("Antlr4 Examples");
try {
String text1 = getAntlr4String("C:\\Users\\DELL\\eclipse-workspace\\javaTest\\src\\javaTest\\Hello.java");
String text2 = getAntlr4String("C:\\Users\\DELL\\eclipse-workspace\\javaTest\\src\\javaTest\\Hello2.java");
// System.out.println(text1);
// System.out.println(text2);
TextSimilarity cosSimilarity = new CosineSimilarity();
double score1 = cosSimilarity.getSimilarity(text1, text2);
System.out.println("cos相似度分值:" + score1);
TextSimilarity editSimilarity = new EditDistanceSimilarity();
double score2 = editSimilarity.getSimilarity(text1, text2);
System.out.println("edit相似度分值:" + score2);
} catch (IOException e) {
e.printStackTrace();
}
}
这里用常见的XSS修复代码案例来做个测试:
https://github.com/ChenViVi/XssFilter/blob/ec4dc983efd4fe80d8c6ff07dc29dc5f8523964d/XssHttpServletRequestWrapper.java
https://github.com/agarwal-arjun/XSSFilter/blob/537bc086ce700608e435546fe673c537f79f67af/src/main/java/filters/wrapper/XSSRequestWrapper.java
上述两个Xss过滤代码是我从GitHub上检索到的公开源码,对其进行相似度查询:
首先是Cos余弦相似度分值:
然后是Edit编辑距离相似度分值:
可以看到两个分值一个在0.99,一个在0.49。相差还是蛮大的,而且Edit编辑距离算法用到递归的特性,所以运行时间要比余弦相似度更久,
经过几次比对之后发现,就算用两种不相干的方法片段,其Cos余弦相似度还是会一样,因此在这里就只能选择Edit编辑距离算法。当然读者也可以在这里替换其他准确度更高的相似度比较算法。
最后,不管是上述哪种方法,都有一定的阈值需要经过多次测试才能确保检出的稳定性。