插件化可以使一个工程的功能扩展更简单;代码的解耦,更易用维护,我们在很多工具中都能看到这个结构。同时插件化,也使得工程可以有更多人一起投入开发,不断去扩展更新工程的功能,也就是社区化开发。
像burpsuite中的插件使得该工具的功能被不断优化完善,大大提高我们的效率,像一些被动漏扫插件,验证码识别插件,越权测试插件等等。这些插件都是由burp使用者开发而非官方,不同人在工作学习中会有不同的idea,而如果仅仅依靠一个人或者一个小团队,可能是没那么多精力和想法去做到人人都能称手的工具,所以社区化很重要。
除此之外,像goby的poc、exp也是实现了插件化。而我看到的比较多的插件化是应用在java项目中的,这个主要原因是java强大的JVM,不需要重新编译主程序,即可动态加载class以及jar包,来实现功能扩展;而主程序只需要设计好插件的接口,插件开发者根据接口设计去实现特定功能就行了,这也是多态的表现。
参考一篇文章的解释
编译型语言的典型例子就是 汇编语言、C、C++、Objective-C、Go、Rust
等等。
解释型语言的典型例子就是 JavaScript、PHP、Shell、Python、Lua
等等。
至于 Java
,从 JVM 的角度,它是一个编译型语言,因为编译出来的二进制码可以直接在 JVM 上执行。但从 CPU 的角度,它依然是一个解释型语言,因为 CPU 并不直接运行代码,而是间接地通过 JVM 解释 Java 二进制码从而实现逻辑运行。
所谓的 “脚本语言” 则是另外的一个概念,这一般指的是设计初衷就是用来开发一段小程序或者是小逻辑,然后使用预设的解释器解释这段代码并执行的程序语言。这是一个程序语言功能上的定义,理论上所有解释型语言都可以很方便的作为脚本语言,但是实际上我们并不会这么做,比如说 PHP
和 JS
就很少作为脚本语言使用。
可以看到,解释型语言天生适合作为脚本语言,因为它们原本就需要使用运行时来解释和运行代码。将运行时稍作改造或封装,就可以实现一个动态拉起脚本的功能。
那么现在就有一个问题,像编译型语言,该如何实现这种插件化呢,那就是将编译型语言变成脚本语言。
由于我本身开发的一款工具是用基于go的,所以后续探索基于go的插件化。如上所言go也是一种编译型语言,而goby也是基于go开发的,所以我事先调研了goby是如何做的。
功能插件:主要是功能扩展,用js编写,类似chrome的插件方式。
exp插件:扩展漏洞poc/exp,有两种方案,json格式以及go代码。 官方文档https://cn.gobies.org/exp.html#
而我的目的也是做exp/poc插件,所以着重看了这部分
搜索了关键词
\"GobyQuery\"
,其实和xray的yaml差不多,分为ScanSteps和ExploitSteps,需要填写一些参数,和xray的yaml差不多,这个还复杂些,估计是因为考虑适用人群范围要更大些,所以需要更多信息填写。
go代码编写
关键词:*scanconfig.SingleScanConfig
这个会更灵活应对复杂漏洞,poc和exp对应如下签名,然后通过ExpManager.AddExploit添加,这个算是编译型语言里常用的一种注册方式。
func(exp *jsonvul.JsonVul, u *httpclient.FixUrl, ss *scanconfig.SingleScanConfig) bool func(expResult *jsonvul.ExploitResult, ss *scanconfig.SingleScanConfig) *jsonvul.ExploitResult
看了上面的两种方式,第二种会更灵活些,而且实际上手难度也不大,而第二种方式了解到是通过一个开源解析引擎实现的,yaegi是一个go解释器,它在go运行时之上,为嵌入式解释器或交互式shell提供可执行go脚本和插件。
https://github.com/traefik/yaegi
所以后续就开始研究这个引擎
https://github.com/traefik/yaegi
目前yaegi仅支持到1.17,因为1.18增加了泛型,还未做好支持。
先简单尝试了下,确实可以实现go代码的解释执行。
简单看一下这段go代码的,他实现了从代码中获取函数引用,并且实现调用,这非常友好。
func TestTest(t *testing.T) { // 测试 src := ` package plugins_test import "fmt" func printlog() { fmt.Println("test") } ` intp := interp.New(interp.Options{}) // 初始化一个 yaegi 解释器 intp.Use(stdlib.Symbols) // 允许脚本调用(几乎)所有的 Go 官方 package 代码 _, err := intp.Eval(src) // 执行go代码 if err != nil { panic(err) } v, _ := intp.Eval("plugins_test.printlog") // 由于上面以执行加载了一段go代码,所以这里可以通过package+funcname的方式来获取函数 fu := v.Interface().(func()) // 函数类型转换 fu() // 函数调用 }
现在还有一个问题,如果我需要在go脚本中导入第三方库,如何实现。其实可以注意到上面例子中intp.Use(stdlib.Symbols)
,yaegi解释器分析了go脚本的语法之后,会将其中的符号调用与符号表中的目标进行链接。而stdlib.Symbols
导出了go标准库中几乎所有的符号。
左侧就是解析的所有符号文件,并且可以看到他是通过yaegi extract提取生成的,结合了go generate
。
查看生成的其中一个符号,这个包下的函数、变量都做了链接,注意他的key是包路径+包名,所以会出现archive/tar/tar的格式。
那么要实现第三方库的支持,就需要生成第三方库的符号表。
创建一个基础代码文件,按照参考的格式使用extract来生成
在库目录上右键Go Tools-Go Generate File即可自动生成。
生成的和我们看到的标准库是一样的。
intp.Use
添加我们第三方库的符号表,然后我们再尝试在go脚本中导入第三方包执行,可成功执行
那么后面就可以投入插件设计了
其实如果到这,插件设计就应该没问题了,像goby那种通过一个管理包添加新的poc、exp,但因为我自己YY的漏洞框架不太一样,导致一些其他问题。
我是以一种类似java类的方式进行漏洞代码设计,每个漏洞,有一个专属的结构体,而漏洞的利用方式可能存在多种多样,我分为GetMsg/Cmd/Reverse/Upload
4类,并且每一类也可能会研究出不同的payload,就会存在Cmd1/Cmd2等等,这就是我对于一个漏洞的利用方式设计。
而注册方式使用的是将结构体的引用和漏洞信息通过一个通用的注册函数添加,后续利用就是通过reflect包反射调用结构体方法。
注册是放在init函数中,init函数在go程序启动时会优先于man函数加载,从而能自动进行注册,而不需要手动去调用,这样就使得漏洞编写和注册显得简单了许多。
但这种方法存在的问题就是,是通过结构体去反射调用,但通过yaegi拿到的结构体是无法获取到他的结构体方法,导致无法使用。
这个就很麻烦了,如果调整漏洞框架着实难受,也不符合我想要的。可能会说,插件化代码另外整一套逻辑不就行了,但这样我想把插件代码合并编译到项目里的话就会非常麻烦,两套逻辑维护也麻烦,另一方面考虑这种解释执行的不一定稳定,所以想给自己留条后路,能合并的代码还是考虑合并。
我本来都放弃插件化了,后面想到一种曲线救国的方式。代码还是这么写,但我可以patch,在读取脚本代码后patch后在调用yaegi执行,这样就能解决结构体方法无法调用的问题。
那么patch思路是啥,既然他能正常调用函数,而不能正常调用结构体方法,那就patch添加一个封装的函数不就好了,然后注册这个函数。
patch后,这个函数大致如下
通过解析到的已有的方法,然后做个switch就好了
而包名、结构体名、方法名通过在脚本里添加一个新的结构体注册不就好了。然后再调用yaegi执行,这样就将NewExp注册到管理器里了。
以上的patch通过go自带的AST解析修改。
后续调用的话,函数比结构体调用还简单些,在注册到管理器里的时候标记下是插件,调用的时候判断如果是插件就单独调用就好,这块没太多需要修改的。
patch逻辑就如下,AST解析后存放到一个管理map里,然后遍历执行,需要注意的是,因为每个文件都patch了一个相同名字的函数,所以需要每次创建一个新的解释器。
每次启动会自动加载,启动后,如果对代码文件做了修改,也支持热更新。
PS: 这里热更新做了个简单的hash校验,只会更新更改过的文件。
详细说明放到github,已实现poc/exp插件化,目前还在内测稳定性,后续会开放主程序。
https://github.com/lz520520/railgunlib
这工具写了很久了,一直想做插件化,受限于go做插件化太麻烦了,折腾了好久终于找到一种可行的方案,就插件化来讲,java更适合,他强大的运行时使得这一切变得简单了许多。其他编译型语言就捉襟见肘了。写了这么久go,yaegi算是一种解救吧,其实如果说到插件化,其实能应用的场景就可以发散下,比如用在马中,实现opsec