Fuzzing一直以来是漏洞挖掘非常有效的方式,我最喜欢的工具仍然是经久不衰的AFL,你可以基于AFL来打造各种fuzz工具,当然这得益于AFL中最具有魔性的遗传算法corpus变异,还记得吗,仅凭“hello”几个字符就能产生无数个有效的input输入,鹅妹子嘤!但现在有个问题,AFL基于LLVM、GCC等编译器的辅助一直发挥的不错,可如果遇到java这类基于VM的语言开发的软件还能派上用场吗?答案是肯定的,下面就祭出今天的主角大杀器 —— kelinci 。
进入主题之前先讨论个话题,java程序使用fuzz的方式去挖漏洞有效吗?其实个人认为,java语言开发的程序并不会fuzz出什么特别严重的漏洞,比如C\C++如果产生了数组越界读写等问题,那很有可能会给你一个舒服的abitrary memory R&W,接下来能干啥就看你的造化了。但是java的程序fuzz出这类问题,可能仅仅就是抛个IndexOutOfBoundsException,最多导致个拒绝服务啥的,危害程度要小得多。所以说如果你想直接在java语言层面fuzz出个什么高危漏洞,不能说不可能,只是概率非常的低。但是这不意味着java程序就不需要fuzz,fuzz仍然能够发现很多有价值的问题,例如资源泄露、RuntimeExceptions 、反序列化漏洞、XXE、逻辑bug、甚至jvm逃逸等等,每一类问题放在系统的业务逻辑中去仔细分析,都有可能是个大问题,千万不要忽视。
OK,下面我们言归正传,开始正式介绍kelinci。(友情提示,搞kelinci之前你最好是折腾过AFL,不然这个kelinci可能有很多地方你可能不会理解)
英文好的直接看这两篇文档:
https://github.com/isstac/kelinci/blob/master/docs/ccs17-kersten.pdf
https://github.com/isstac/kelinci/blob/master/docs/ccs2017-poster-small.pdf
我通俗的说一下kelinci实现的大概思路,首先对于AFL来说,它并不知道自己在fuzz java程序,这是因为kelinci做了一个java程序的C版interface ,这个interface程序负责从AFL中接收变异数据,然后通过TCP传给java侧,java侧的程序叫做instrumentor,它负责把从interface传过来的变异数据真正的传递给java原始目标程序,然后再把java的运行结果反馈给interface,如此形成了一个fuzz数据流闭环。
首先来个例行的
git clone https://github.com/isstac/kelincicd
Kelinci有两个组件,一个是C程序interface,它作为AFL的测试目标。你可以在fuzzerside目录里找到它。进入fuzzerside目录后执行make就可以编译生成interface可执行程序了。第二个组件是java侧的instrumentor,它是java侧的目标程序被工具化后的产物,负责和interface进行TCP通信,instrumentor运行后会起一个TCP server接收变异数据,每次接收到一个请求,instrumentor就会单独起一个线程来去运行java侧的目标程序,并且将变异数据传递进去。然后再把这个请求的运行结果发送回去,例如是success、timeout还是queue full等,注意任何可以跳出main的异常都会被认定是一个crash。你可以使用gradle来构建instrumentor,进入instrumentor目录,执行gradle build顺利的话,在./build/libs目录里会编译出一个kelinci.jar。
假定你的AFL和上面两个组件都已经编译好了,就可以按照下面的步骤来跑fuzz了。
1,创建一个driver:这个一个步骤是可选的,主要是因为AFL/Kelinci的输入预期是本地文件,但如果你的程序接受的参数不是文件,那就需要自己去把corpus文件和你程序的接口去做个适配了。这说的有点抽象,我举个栗子,比如你要fuzz的目标程序是从数据库中读取数据并做分析处理,那么AFL是无法直接对数据库中的某个字段数据做变异的,这时候就需要你自己去写个driver,把数据从数据库中读取出来组织成本地corpus文件,然后AFL就可以对这个本地的corpus文件进行变异了,变异完成后还需要你的driver程序把变异后的corpus文件的数据读出来,写回数据库让你的测试目标去读取。这些工作AFL无法自动化完成,因为它无法预先知道你要fuzz的程序接受什么样的输入,所以这个driver的工作你要自己去做,当然如果你要fuzz的程序本来就是接受文件的输入,那恭喜你这个步骤可以省了嘿嘿~
2,目标程序工具化:我们假定目标程序和driver你都已经构建好了,并且输出到了目录”bin”中。下面我们要将目标程序工具化,只有工具化以后的目标程序才可以使用AFL来进行fuzz。kelinci提供了一个工具类edu.cmu.sv.kelinci.instrumentor.Instrumentor来干这事。它使用-i选项来指定输入目录,在这里就是”bin”,使用-o选项指定输出目录,这里是“bin-instrumented”。我们需要确保kelinci.jar在classpath中,然后假定目标程序所依赖的jar包都在 /path/to/libs/中,那么执行命令示例如下:
java -cp /path/to/kelinci/instrumentor/build/libs/kelinci.jar:/path/to/libs/* edu.cmu.sv.kelinci.instrumentor.Instrumentor -i bin -o bin-instrumented
注意如果目标程序所依赖的库和Kelinci Instrumentor所依赖的相同,这时候如果他们之间的版本不同就会出现版本冲突问题。目前来说Kelinci所使用的版本如下:args4j version 2.32, ASM 5.2, Apache Commons IO 2.4。大多数情况下,如果出现这种问题,你可以不要去使用打包好的kelinci.jar,而是把Kelinci构建出来的classes目录放到classpath上,然后他们共同依赖的那些jar包你就只放一个版本到/path/to/libs/里就OK了。
3,创建输入样例:要对工具化后的目标程序进行fuzz还需要创建一个输入文件的目录”in_dir”。
mkdir in_dir
AFL会从这个in_dir里获取输入样本进行变异,注意这里的文件是变异最初的原始版本,要用心的构造。构造好输入样本后可以使用如下命令去测试一下:
java -cp bin-instrumented:/path/to/libs/* in_dir/
4,启动Kelinci server:现在我们可以启动Kelinci的server了。Kelinci需要目标java程序的main类作为第一个参数,然后使用@@来代替之前建立的”in_dir”中的输入文件,运行时Kelinci会使用每一个具体的输入文件来替换@@。运行的命令可以写成这个形式:
java -cp bin-instrumented:/path/to/libs/* edu.cmu.sv.kelinci.Kelinci @@
你也可以自己指定一个端口号,默认是7007,比如
java -cp bin-instrumented:/path/to/libs/* edu.cmu.sv.kelinci.Kelinci -port 6666 @@
5,通信测试:开始正式fuzz之前,可以运行interface程序来确认一下与java侧程序的联通性,命令如下:
/path/to/kelinci/fuzzerside/interface in_dir/
你还可以使用-s选项来指定一个远端server,(默认是连接localhost),
/path/to/kelinci/fuzzerside/interface -s yourdomain.com in_dir/
甚至提供一个server的列表server.txt
/path/to/kelinci/fuzzerside/interface-s servers.txt in_dir/
6, 开始正式fuzzing:如果之前做的都没啥毛病,现在可以启动AFL了,就像之前说过的interface就是AFL的测试目标,@@作为interface的输入文件,运行时从”in_dir”里取每一个文件代替@@,然后再指定个fuzz结果输出目录”out_dir”,那么AFL的运行命令可以组成如下:
/path/to/afl/afl-fuzz -i in_dir -o out_dir /path/to/kelinci/fuzzerside/interface [-s servers.txt] @@
不出意外的话,AFL interface过一会就会启动了,如果在AFL的展示窗口发现有了新的path,那么恭喜你,Kelinci运行成功了!
*本文作者:成王败寇,转载请注明来自FreeBuf.COM