Golang Hack: kunpeng代码框架学习笔记 2020-01-26 09:29:39 Steven Xeldax > 此项目作者为@ywolf 项目地址为 https://github.com/opensec-cn/kunpeng 最近正好尝试在使用golang来编写一些工具和项目,golang相对于python除了性能上的优势之外,对于我来说最为便利的就是静态编译。只要使用golang编译好二进制在目标平台不要安装任何依赖和库就能运行,再也不用pip,apt了。 ### 代码结构 ``` ├── config │ └── config.go ├── doc │ ├── ... ... ├── docall.py ├── example │ ├── ... .... ├── go.mod ├── go_plugin_template.txt ├── go.sum ├── kunpeng_c.h ├── kunpeng_c.so ├── KUNPEN_README.md ├── LICENSE ├── main.go ├── plugin │ ├── go │ ├── go.go │ ├── json │ ├── json.go │ └── plugin.go ├── README.md ├── util │ ├── aider.go │ ├── fun.go │ ├── log.go │ └── net.go └── web └── api.go ``` main.go: 作为程序的入口点,里面包含了对外so库所提供的调用接口函数。 util目录: 相当于工具类,里面有许多常用的函数。 web目录: gin快速开发的web api接口 plugin目录: 核心插件代码目录 #### main.go  其中Greeter为greeting的实例化结构,用来提供给golang语言进行动态库调用。 先定义一个greeting的申明,这样就可以编写greeting的成员函数了。 ``` type greeting string ``` 随后编写greeting的成员函数,最后实例化greeting,来给golang调用。 ``` func (g greeting) ShowLog() { config.SetDebug(true) } func (g greeting) GetVersion() string { return VERSION } ``` ``` var Greeter greeting //实例化 ``` 剩下的为c-like的导出函数,用来提供给c语言来调用动态库函数的。 ``` //export Check func Check(task *C.char) *C.char { } //export GetPlugins func GetPlugins() *C.char { } //export SetConfig func SetConfig(configJSON *C.char) { } ``` **Tips1 golang 生成go动态库** go build 可以指定buildmode。分为了多种模式,具体模式如下: **archive**:编译成二进制文件。一般是静态库文件。 xx.a **c-archive**:编译成C归档文件。C可调用的静态库。xx.a。注意要编译成此类文件需要import C 并且要外部调用的函数要使用 “//export 函数名” 的方式在函数上方注释。否则函数默认不会被导出。 **c-shared**:编译成C共享库。同样需要 import “C” 和在函数上方注释 // export xxx **default**:对于有main包的直接编译成可执行文件。没有main包的,编译成.a文件 **exe**:编译成window可执行程序 **plugin**:将main包和依赖的包一起编译成go plugin。非main包忽略。【类似C的共享库或静态库。插件式开发使用】 Example: plugin.so ``` package main import ( "fmt" ) func DCall(){ fmt.Println("plugin.so was called") } func DCallWithParam(msg string){ fmt.Println("参数内容为:",msg) } func main() { fmt.Println("goroute全部退出") } ``` pluginload.go ``` package main import ( "plugin" ) func main() { //加载动态库 p, err := plugin.Open("plugin.so") if err != nil { panic(err) } //查找函数 f, err := p.Lookup("DCall") if err != nil { panic(err) } //转换类型后调用函数 f.(func())() f2, err := p.Lookup("DCallWithParam") if err != nil { panic(err) } //带参函数的调用 f2.(func(string))("hello world,plugin.so") } ``` 代码写好之后我们先编译so动态库: > go build --buildmode=plugin plugin.go 然后运行调用so库代码: > go run pluginload.go golang以插件形式调用go so的方式,首先需要import plugin,然后打开so库,接着用Lookup函数寻找我们需要调用的函数,如果执行成功会返回这个的句柄就能直接用了。 **Tips2 golang生成c动态库** 直接看例子: 首先新建一个go文件来编写我们的动态库代码 ``` package main import "C" func main() {} //export Hello func Hello() string { return "Hello" } //export Test func Test() { println("export Test") } ``` 执行下列明 > go build -x -v -ldflags "-s -w" -buildmode=c-shared -o libhello.so main.go 下面看下在golang和c下分别调用该动态库的例子: 1.C调用C库 ``` #include <stdio.h> #include "libhello.h" void main() { _GoString str; str = Hello(); Test(); printf("%d\n",str.n); } ``` > gcc 1.c -I./ -L./ -lhello -I是大写的i,-L是大写的l,-l是小写的l 运行结果  2.golang调用C库 ``` package main /* #cgo CFLAGS: -I. #cgo LDFLAGS: -L. -lhello #include <stdio.h> #include <stdlib.h> #include "libhello.h" */ import "C" import ( "fmt" ) func main() { str := C.Hello() C.Test() fmt.Println(str) } ``` #### config模块 config包内包含了所有kunpeng需要的全局配置,可以供给各个模块使用。  其中struct定义了json tag(`json:"xxxx"`)可以针对传入json进行反序列化。 以下是json tag和不使用json tag的区别,使用json tag会在调用json.marshal转换的时候默认字段采用字段内部的tag。  #### web 模块 Package web 提供Web Api接口调用,在c-like导出库中可以通过调用StartWebServer来启动。 ``` //export StartWebServer func StartWebServer(bindAddr *C.char) { go web.StartServer(C.GoString(bindAddr)) } ``` #### util模块 提供了常用的工具函数,比如在net.go中定义了RequestDo来进行http请求。  #### plugin模块 plugin模块是整个kunpeng的核心包,plugin插件主要采用了json和go两种方式进行编写。 **<plugin.go>** 导入配置 ``` import ( . "github.com/opensec-cn/kunpeng/config" "github.com/opensec-cn/kunpeng/util" "sort" "strconv" "strings" ) ```  定义 ``` // GoPlugins GO插件集 var GoPlugins map[string][]GoPlugin // JSONPlugins JSON插件集 var JSONPlugins map[string][]JSONPlugin ``` 这个函数就很有意思,相当于实现了一个try的异常捕捉。 ``` func try(fun func(), handler func(interface{})) { defer func() { if err := recover(); err != nil { handler(err) } }() fun() } ```  func init函数初始化了goplugins和jsonplugins这两个分别存放了整个kunpeng载入的插件。 然后Scan函数处理由main.go传入内部的task任务,然后调用formatCheck进行检查。 作者在formatCheck中检查了如果是.gov.cn的话就终止扫描。 ``` func formatCheck(task Task) bool { if strings.Contains(strings.ToLower(task.Netloc), string([]byte{103, 111, 118, 46, 99, 110})) { return false } if task.Type == "web" { if strings.IndexAny(task.Netloc, "http") != 0 { return false } } else if strings.IndexAny(task.Netloc, "http") == 0 { return false } return true } ``` 再来仔细看下Scan函数,Scan函数采用遍历的方法逐一扫描GoPluigins和JsonPlugins中的插件取出插件的target和插件列表来进行扫描。 ``` for n, pluginList := range GoPlugins { if strings.Contains(strings.ToLower(task.Target), "cve-") {} else if strings.Contains(strings.ToLower(task.Target), "kp-") {} else if strings.Contains(strings.ToLower(task.Target), strings.ToLower(n)) || task.Target == "all" {} ``` pluginRun就是直接运行这个插件了 ``` func pluginRun(taskInfo Task, plugin GoPlugin) (result []map[string]interface{}) { if len(taskInfo.Meta.PassList) == 0 { taskInfo.Meta.PassList = Config.PassList } var hasVul bool try(func() { hasVul = plugin.Check(taskInfo.Netloc, taskInfo.Meta) }, func(e interface{}) { util.Logger.Println("panic", e) }) if hasVul == false { return } for _, res := range plugin.GetResult() { util.Logger.Info("hit plugin:", res.Name) result = append(result, util.Struct2Map(res)) } return result } ``` 下面看下json和go两种不同插件的实现方式。 **<json.go>** json插件的话由于已经格式固定所以定义了struct ``` type JSONPlugin struct { Target string `json:"target"` Meta Plugin `json:"meta"` Request struct { Path string `json:"path"` PostData string `json:"postdata"` } `json:"request"` Verify struct { Type string `json:"type"` Match string `json:"match"` } `json:"verify"` Extra bool } ``` 此外定义了jsonCheck来检查漏洞 ``` func jsonCheck(URL string, p JSONPlugin) (bool, Plugin) { ``` json格式 ``` { "target":"docker", "meta":{ "name": "Docker Remote API未授权访问", "remarks": "Docker Remote API未授权访问可导致代码泄露,严重可导致服务器被入侵控制。", "level": 0, "type": "RCE", "author": "wolf", "references": { "url":"https://github.com/vulhub/vulhub/blob/master/docker/unauthorized-rce/README.zh-cn.md", "cve":"", "kpid":"KP-0056" } }, "request":{ "path": "/info", "postData": "" }, "verify":{ "type": "string", "match": "ContainersRunning" } ``` 通过json文件来初始化http请求,哦对了,json只使用于http类型的plugin。 ``` var request *http.Request var result Plugin var vulURL = URL + p.Request.Path if p.Request.PostData != "" { request, _ = http.NewRequest("POST", vulURL, strings.NewReader(p.Request.PostData)) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { request, _ = http.NewRequest("GET", vulURL, nil) } ``` 最后对结果进行不同类型的比较  看下插件plugin/json内部分 每个实现的插件都需要在golang内部定义好,这个实现的方法就是用ecs来进行它被定义在JSONPlugin.go这个文件内部。 // Code generated by "esc -include=\.json$ -o plugin/json/JSONPlugin.go -pkg jsonplugin plugin/json/"; DO NOT EDIT.  然后在plugin/json/init.go内实现init在导入包的时候自动把所有的文件给注册进去。  init内部实现了 ``` func init() { // util.Logger.Println("init json plugin") loadJSONPlugin(false, "/plugin/json/") go loadExtraJSONPlugin() } ``` loadJSONPlugin载入/plugin/json下的json文件,loadExtraJSONPlugin异步载入其他路径下的插件。 ``` func loadJSONPlugin(useLocal bool, pluginPath string) { ``` loadJSONPlugin首先遍历/plugin/json ``` if useLocal { f, err = os.Open(pluginPath) if err != nil { util.Logger.Error(err.Error()) return } } else { f, err = FS(useLocal).Open(pluginPath) if err != nil { util.Logger.Error(err.Error()) return } } defer f.Close() fileList, err := f.Readdir(2000) ``` 然后依次调用readPlugin ``` for _, v := range fileList { p, ok := readPlugin(useLocal, pluginPath+v.Name()) ........ plugin.JSONPlugins[p.Target] = append(plugin.JSONPlugins[p.Target], p) extraPluginCache = append(extraPluginCache, p.Meta.Name) } ``` readPlugin很直接就是直接读取文件然后反序列化 ``` func readPlugin(useLocal bool, filePath string) (p plugin.JSONPlugin, ok bool) { // util.Logger.Println(filePath) var pluginBytes []byte var err error // util.Logger.Println(path.Ext(filePath)) if strings.ToLower(path.Ext(filePath)) != ".json" { return p, false } if useLocal { pluginBytes, err = ioutil.ReadFile(filePath) if err != nil { util.Logger.Error(err.Error(), filePath) return p, false } } else { pluginBytes = FSMustByte(useLocal, filePath) } err = json.Unmarshal(pluginBytes, &p) if err != nil { util.Logger.Error(err.Error(), string(pluginBytes)) return p, false } p.Extra = useLocal return p, true } ``` **<go.go>** go类型插件定义了一个interface,然后自动调用plugin/go下所有的插件,插件需要实现init,check以及getresult函数,最后调用Regist来注册插件把我们的插件放到插件列表里。 值得注意Regist(target string, plugin GoPlugin)它是通过形参来重载go/下的struct来初始化interface。 ``` package plugin // GoPlugin 插件接口 type GoPlugin interface { Init() Plugin Check(netloc string, meta TaskMeta) bool GetResult() []Plugin } // Regist 注册插件 func Regist(target string, plugin GoPlugin) { GoPlugins[target] = append(GoPlugins[target], plugin) // var pluginInfo = plugin.Init() // util.Logger.Println("init plugin:", pluginInfo.References.KPID, pluginInfo.Name) } ``` Eg:  ### 插件开发 Go类型插件 KunPeng定义了一个用于描述插件信息的公有结构体Plugin: ``` type References struct { URL string `json:"url"` CVE string `json:"cve"` } type Plugin struct { Name string `json:"name"` Remarks string `json:"remarks"` Level int `json:"level"` Type string `json:"type"` Author string `json:"author"` References References `json:"references"` Request string Response string } ``` 以及公有接口GoPlugin: ``` type GoPlugin interface { Init() Plugin Check(string, TaskMeta) bool GetResult() []Plugin } ``` 由于KunPeng未定义插件的相关基类及缺省字段和方法,所以我们需要在plugin/go/目录下创建一个新的.go文件,在其中自定义一个包含info和result字段的结构体来表示新的插件: ``` type pluginXXX struct { info plugin.Plugin result []plugin.Plugin } ``` 随后,为该结构体实现GoPlugin接口中所有的方法: ``` func (p *pluginXXX) Init() plugin.Plugin { p.info = plugin.Plugin{} } func (p *pluginXXX) Check(netloc string, meta TaskMeta) bool { // 自定义检测过程逻辑,成功返回true,失败返回false return false } func (p *pluginXXX) GetResult() []plugin.Plugin { return p.result } ``` 并在文件的init()方法中调用plugin.Regist()注册该插件即可: ``` func init() { plugin.Regist("xxx", new(plugin)) } ``` JSON类型插件 KunPeng同样为我们准备好了用于描述JSON插件信息的公有结构体JSONPlugin,并对它实现了统一的检测方法jsonCheck(): ``` type JSONPlugin struct { Target string `json:"target"` Meta Plugin `json:"meta"` Request struct { Path string `json:"path"` PostData string `json:"postdata"` } `json:"request"` Verify struct { Type string `json:"type"` Match string `json:"match"` } `json:"verify"` } func jsonCheck(URL string, p JSONPlugin) (bool, Plugin) { // 常规的HTTP发包和结果比较,略 return false, result } ``` 我们可以在plugin/json/目录下创建一个新的.json文件,写入我们需要的信息 (具体内容参考官方文档) 即可: ``` { "target": "xxx", "meta": { "name": "xxx", "remarks": "xxx", "level": 0, "type": "RCE", "author": "gyyyy", "references": { "url": "https://github.com/gyyyy/", "cve": "" } }, "request":{ "path": "/index.html", "postData": "" }, "verify":{ "type": "string", "match": "gyyyy" } } ``` ### 调用流程 最后我们看下整个调用的架构图  ### 参考资料 https://blog.csdn.net/github_33719169/article/details/84826983 https://github.com/lazybootsafe/Go-learning-With-Hack/blob/master/hack/kunpeng/kunpeng.md