基于IDEA的自动化代码审计插件开发初探

2021-04-26 12:28:16 浏览数 (1)

想写这篇文章好久了,重度拖延症患者就是拖拖拖...

然后本文主要是给大家介绍一下怎么实现一个IDEA静态代码检测插件,现在都在讲安全左移嘛,我觉得静态代码检测插件就是一个安全左移很好的落地,于是就想着学习一下

我自己在写这个插件的起步阶段其实遇到了很多问题,这些问题肯定也会是每一个想要写插件的人都会遇到的,所以,我就简单记录下我的开发过程供来者参考

开发环境搭建

首先得确保你的Plugin DevKit插件成功安装并启用了,一般情况下idea都自带了这个插件,你只需要手动启用一下就行,启用过后记得重启idea

然后创建工程,我这里使用gradle创建一个IntelliJ platform plugin的工程,如果你没有启用devkit插件的话,这里应该是找不到intellij platform plugin这个选项的

通过gradle创建的工程,build.gradle文件中会有这么一项

gradle会根据这里的version字段去下载对应版本的intellij依赖包,这一阶段需要花费大量的时间(还有可能一直下载不下来,可以尝试挂代理),可能是因为源在国外吧

gradle项目构建好过后,项目结构大概如下

这里有个比较关键的文件plugin.xml,这是插件的配置文件,很重要,里面有如下常见字段

代码语言:javascript复制
<idea-plugin>
  <!-- 插件相关信息, 会展示在IDEA插件的描述中 -->
  
  <!-- 插件唯一id, 遵循使用包名的原则 -->
  <id>com.test.sast</id>
  <!-- 插件名称 -->
  <name>AxinSAST</name>
  <!-- 插件版本 -->
  <version>1.0</version>
  <!-- 开发者信息 -->
  <vendor email="wuhu@gmail.com" url="http://wuhu.top">$Company|$Name</vendor>
  <!-- 插件的描述 -->
  <description>my plugin description</description>
  <!-- 插件版本变更信息 -->
  <change-notes>Initial release of the plugin.</change-notes>
  <!-- 如果该插件还依赖了其他插件,则配置对对应的插件id -->
  <depends>com.intellij.modules.all</depends>
  <!-- 插件兼容IDEA的最大和最小build号,不配置则不做限制 -->
  <idea-version since-build="94.539" until-build="192"/>
  <!-- Actions: 如添加一个文件右击菜单按钮 -->
  <actions>
    <action id="FinderAction" class="com.test.finder.FinderAction" text="FileFinder" description="FileFinder">
      <add-to-group group-id="ProjectViewPopupMenu" anchor="first"/>
    </action>
  </actions>
  <!-- 插件定义的扩展点,以供其他插件扩展该插件,类似Java的抽象类的功能 -->
  <extensionPoints>
    ...
  </extensionPoints>
  <!-- 声明该插件对IDEA core或其他插件的扩展 -->
  <extensions xmlns="com.intellij">
    ...
  </extensions>
</idea-plugin>

通常来说,按照上面的操作应该就可以成功建立一个插件工程了,比较容易卡住的就是下载idea依赖那一块儿

当我们写好代码过后,可以使用gradle的runIde这个task测试自己的插件,这个task首次执行时会去下载一个jbr(jetbrains runtime)压缩包,然后利用这个runtime来启动我们的插件

runIde这个task会在一个沙箱环境里运行我们编写的插件,不会影响我们当前正在使用的idea环境,也就是会新起一个测试用的idea,然后我们的插件会在这个测试idea上跑起来

为了能够对idea插件有一个更加清晰的认识,我们来写一个小demo,这个demo也是网上最常见的一个demo

插件开发初体验

我们使用devkit 新建一个action

然后填写好对应的信息:

上述操作完成后,会在我们的项目目录下生成一个对应TestAction类文件,我们重写下该类的ationPerformed方法:

代码语言:javascript复制
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.ui.MessageType;

public class TestAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here
        // 这里的testid需要和你刚刚填写的action id保持一致
        NotificationGroup notificationGroup = new NotificationGroup("testid", NotificationDisplayType.BALLOON, false);
        /**
         * content :  通知内容
         * type  :通知的类型,warning,info,error
         */
        Notification notification = notificationGroup.createNotification("测试通知", MessageType.INFO);
        Notifications.Bus.notify(notification);
    }
}

完成了上面的操作,我们可以看下plugin.xml文件有什么变化

你会发现这里多了个action,这个action其实是devkit帮我们自动注册的,所以,到这里你也应该能够理解devkit有啥用了吧,它帮我们封装了一些方法,让我们更加便利的进行插件开发

如果我们没有使用devkit来创建刚刚那么一个action的话,我们就需要手动在plugin.xml文件里注册action,然后在对应的类实现对应的方法

ok,到此其实我们的一个小demo就算是完成了,到目前为止你都不需要去理解上面的代码有啥用,你只需要按照我的步骤走一遍就行,至于上面的代码能实现什么效果,有什么是比亲眼见证更好的呢?

接下来就是验证插件的效果了,直接执行intellij的runIde任务就好

运行这个会新起一个idea,然后我们的插件会自动安装到这个idea上,我们这个插件会在tools菜单下注册一个item,然后点击该item会在idea右下角弹出消息「测试通知」

然后我们到idea的plugins里也可以看到,我们的插件的确是成功安装并启用了的

上图中的就是我们的测试插件,红框中的展示文案都是可以在plugin.xml文件中进行配置的

插件的编写说白了还是调用各种api,想要写好一个插件,就需要清楚intellij sdk提供的各种方法以及接口的使用

而我们要编写一个静态代码审计插件的难点也就在「到底该使用哪个接口来进行代码检查」以及「SDK都提供了哪些工具或方法来方便我们完成代码审计」,只要克服了这两个问题,从AST里去找代码中可能存在的问题也就变得千篇一律了

在idea中进行静态代码审计

首先解决第一个问题,用SDK的哪个接口来进行代码审计

虽然intellij的文档写的很烂,但是还好他在github上放了一些代码示例,其中inspection_basics模块以及comparing_references_inspection模块(尤其是这个模块)给了我们提示,通过这两个模块我知道了在intellij platform上进行自动化代码检查需要用到的功能被称作inspection,那么在这个基础上我们就可以进一步查询inspection的使用文档了

既然写到这里了,那为了让屏幕前的大家更好地了解inspection的使用,我干脆来简单的给大家讲解下comparing_references_inspection这个模块里的一些知识点吧,这个简单的demo主要的功能就是检测java代码中有没有错误地进行引用类型的比较,比如对于String类型,错误的使用!=或者==而不是.equals()方法

如果发现有错误的用法插件会将对应的代码高亮,并且提供一键修复功能,除此之外这个demo里还给我们展示了怎么编写测试用例

这简直就是我们的SAST插件想要做的事情——发现漏洞对漏洞代码进行高亮,如果可以,提供一键修复功能

当然,除此之外,我们还希望发现漏洞代码及时上报到soc方便后续人工确认

首先,我们要对java做inspection,需要在build.gradle中的intellij配置好java的plugin

这个配好过后,我们就可以实现一个自己的inspection类啦,这个类需要继承AbstractBaseJavaLocalInspectionTool,然后我们自己实现的inspection类大体架构如下:

代码语言:javascript复制
public class ComparingReferencesInspection extends AbstractBaseJavaLocalInspectionTool{
 public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly){
   return new JavaElementVisitor() {
    public void visitBinaryExpression(PsiBinaryExpression expression) {
     doSomething...
     if(condition){
      holder.registerProblem(expression, "问题描述字符串")
     }
    }
    public void visitXXXXX(){
     xxxxx
    }
   }
 } 
}

其中最重要的方法就是visitXXX方法,上面代码中举了visitBinaryExpression这么个方法,这个方法有什么用呢?

当插件跑起来过后,这个方法就会检查代码中的BinaryExpression,你可能会问BinaryExpression是啥?

intellij platform中的BinaryExpression其实代表的就是代码中的二项式,例如:

代码语言:javascript复制
"select * from table where id="   id
1 2
1-2

idea中用BinaryExpression封装了上面这种表达式,代码中的每一个二项式都会作为参数传入visitBinaryExpression方法,然后你可以在这个方法里处理这些二项式,比如判断这个二项式是不是潜在的sql注入语句(例如上面第一条二项式那样)

除了visitBinaryExpression方法,intellij还提供了很多种visit方法,这些方法都是为了方便我们进行代码inspection的,例如visitMethodCallExpression是可以访问到所有的方法调用语句,visitNewExpression可以访问到所有的类似new Xxx()语句

有了上面的基础知识,我们尝试着想一想怎么实现一个简单的检测sql注入的插件,我们不考虑特别复杂的sql注入,我们就考虑怎么检测sql语句拼接就行,例如下面这些句子

代码语言:javascript复制
public test(String id){
 String id2 = '2';
 String sql1 = "select * from table where id=" id;
 String sql2 = "select * from table where id="   id2;
}

上面两个sql语句都是二项表达式,所以我们可以用visitBinaryExpression拿到他们,拿到他们过后,我们怎么确定他是一个sql语句呢?毕竟这两个二项表达式是被封装到binaryExpression这个对象里的,怎么通过这个对象判断他是一个sql语句这是一个需要我们解决的问题

这里我们借用一个插件「PsiViewer」,这个插件可以查看当前源码文件的AST树,看图

上图左侧是我们的源代码,右侧是psiviewer插件窗口,当我们在把光标停留在源码某处时,psiviewer窗口会对应展示我们正处在AST树的哪个位置,反过来,当我们在psiviewer窗口中选中ast树某处时,对应的源码也会高亮出来

这个插件可以让我们对intellij platform解析出来的AST树有更加清晰的认知

现在,让我们回到最初的问题,在拿到了BinaryExpression后,我们要怎么判断它到底是不是一个sql语句呢?

其实

如果你观察仔细,你可以从上图中发现psiviewer窗口的下半部分展示了当前PsiBinaryExpression对象所有的属性以及其值

可以看到前两个属性分别是LOperand(左操作数)以及ROperand(右操作数),除此之外还有个属性是operationTokenType,上面截图没有截到,这个属性指明了当前二项式的操作符是啥

常见的操作符有 -

所以,到目前为止怎么判断一个二项式是不是一个sql语句应该很明显了吧:

先判断当前二项式的操作符是不是加号,如果是加号,拿到左操作数以及右操作数,解析出他们的值,然后把他们的值拼接起来,最后用正则判断是不是一个sql语句

当然,这只是很理想的一个状况,如果你要检测sql注入,还需要考虑很多种情况,举一部分?:

代码语言:javascript复制
public test(String id){
 String id2 = '2';
 String sql1 = "select * from table where id="
 String sql2 = sql1   id;
 String sql1  = id;
 String sql3 = "select * from table where id="   id2;
 String sql4 = "select * from"   "table"   id2;
 String sql5 = "select * from table where id="   getId();
 String sql6 = "select * from table where id=%s";
 sql6.format(id);
}

ok,写到这里好像就已经差不多了,要是再细节一点,就是把代码直接贴出来,然后一句句注释了

然后下面是我写的demo插件运行的效果:

我自己在写了一些通过AST进行代码审计的demo后,发现AST能做的事情是很有限的,QL才是更正确的自动化代码审计姿势

代码已上传github:https://github.com/Maskhe/AxinSAST,点击阅读原文可跳转

还在开发中,代码很烂,希望大家别喷我,ball ball 大家?

我又来要赞?,如果你觉得本文写的不错就点个赞,有帮助就点个在看,如果你分享到朋友圈了,什么都别说了,好兄弟!你懂我!各位股东们的一键三连是我持续真诚写作的最大动力✌️

0 人点赞