前言
WebAssembly(Wasm)作为新一代的二进制指令格式,凭借其高性能、跨平台和沙箱隔离等特性,在安全攻防领域也展现出独特的价值。所以也趁着学习的机会把一些可能应用的场景实践一下。探讨Wasm在API网关安全、敏感信息监控、容器隔离等场景的创新应用,对云安全上的应用能力给出一些可以实践的方向。
本篇文章的目录如下:
- GoWasm 简单应用- Apisix 安全切面 - Apisix WAF 插件 - 云原生 HaE 敏感信息泄露监控- Container?Wasm!容器安全性大提升!- Golang+Rust 的分离- 创新应用拓展- Reference
(全文约 8000字,预计阅读时间:25分钟)
GoWasm简单应用
先编写一个Hello World来跑通流程
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
将其编译成wasm文件
GOOS=js GOARCH=wasm go build -o static/main.wasm
最后创建一个index.html来调用该wasm文件
<html><scriptsrc="static/wasm_exec.js"></script><script>const go = new Go(); WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject) .then((result) => go.run(result.instance));</script></html>
这里使用tomcat来测试,修改web.xml的内容,添加如下wasm支持
<mime-mapping><extension>wasm</extension><mime-type>application/wasm</mime-type></mime-mapping>
最后访问页面
从这里例子可以知道把常见的编程语言编程 WASM 的程序后,可以通过特定的某些接口直接在浏览器中调用。
Apisix安全切面
Apisix有着支持Wasm的插件,但需要用到TinyGo来编译。TinyGo不同于Golang的编译器,是专门为微型设备设计的编译器可以更好的支持WASM/WASI,TinyGo还有着可以生成更小的二进制文件的特性。
我这里使用Docker的方式来安装TinyGO
docker pull tinygo/tinygo:0.34.0
下载下来之后,用下面的命令来编译一个wasm文件
docker run -u root -e GO111MODULE=on -e GOPROXY=https://goproxy.cn -w /src --rm -v $(pwd):/src tinygo/tinygo:0.34.0 tinygo build -o response-rewrite.wasm -target=wasi response-rewrite/main.go
随后使用Apisix自带的wasm example来测试一下:https://github.com/apache/apisix/tree/master/t/wasm
进入example目录后,把编译生成好的response-rewrite.wasm放到这个目录
response-rewrite.wasm这个实现就是从配置中对应的参数值获取到ctx上下文中
修改好的配置会在OnHttpResponseHeaders和OnHttpResponseBody函数中对response内容进行处理。
同时需要修改apisix的config文件,引入wasm的插件
wasm:plugins:-name:wasm_rasponseBodypriority:7999file:/tmp/response-rewrite.wasm
然后在docker-compose.yml的apisix服务中添加一行volumes挂载到容器中
services:apisix:image:apache/apisix:${APISIX_IMAGE_TAG:-3.11.0-debian}restart:alwaysvolumes:-./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro-./response-rewrite.wasm:/tmp/response-rewrite.wasm:rodepends_on:-etcd
完成编辑之后就可以运行Apisix
docker-compose -p docker-apisix up -d
发送如下请求注册一个路由,同时给插件配置对应的参数
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
0
上面的API-KEY的值是在config.yaml中设置好的
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
1
创建好路由,访问对应的路径即可在response中看到对应的Header头添加成功
有了上述实现的逻辑,安全建设上就可以实现一些可观测性的操作,例如:
设计一个Waf,在请求的时候匹配request和response是否有恶意请求流量 配合HaE的规则,监控应用是否有敏感信息/数据泄露的接口、Ak/Sk泄露、明文传输、重要字段未加密等需求 统一认证网关,跨应用SSO单点登录
Apisix WAF插件
Apache APISIX因为可以很好的支持Wasm插件,既可以通过Coraza的Wasm插件来做这样一个Waf解决方案。Coraza是OWASP用Go语言开源的一款强大的WAF应用防火墙,支持OWASP CRS核心规则集来检测,而该CRS一开始的设计是用在ModSecurity上。
在Coraza的Github仓库中有专门针对Wasm做了集成支持的项目:https://github.com/corazawaf/coraza-proxy-wasm
只需在Release中下载最新的版本后,解压就是一个.wasm的文件,当作插件的方式加载进Apisix。
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
2
随后创建路由可以通过global_rules的方式来应用到全局上,使其可以处理全局的请求过我们的Coraza WAF
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
3
但如此添加全局路由后,访问任何route好像都是返回403,看日志显示:
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
4
是触发了@owasp_crs/REQUEST-920-PROTOCOL-ENFORCEMENT.conf这个文件ID为920350的规则
看规则内容就知道是通过IP来访问的资源不允许,需要设置一个域名来访问:http://wasm.demo.com:9080/wasmhello
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
5
这时候再加上恶意参数来攻击一下试试
可以看到成功拦截403页面,在apisix的日志中也输出了拦截成功的日志信息
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
6
这里再拓展一点同类方案:
而目前在云原生发展下,现在很多企业有用Higress来做云网关解决方案,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件。
在Istio官网文档内有如下介绍Wasm的目标:
正好在网上看到一个阿里云举办的一场Higress的云原生编程设计比赛:https://tianchi.aliyun.com/competition/entrance/532104/information
感兴趣可以看看CETC CST团队公布的解决方案:https://github.com/pingerr/sec-proxy-wasm
和2022 NIST ZTA大会上演示的云原生ZTA和DevSecOps实践:https://github.com/tetratelabs/zta-demo-2022
该仓库在waf这类解决方案上还是使用的proxy-wasm-go-sdk和Coraza的思路,感兴趣的读者可以在仓库中找到针对Higress的wasm插件使用和Coraza的搭建管理方法。
云原生 HaE 敏感信息泄露监控
因为前面apisix的waf实现章节有说过,wasm的沙盒目标是独立、隔离的特性,且有着高低资源消耗的优点。由于这个实现想法目前没有现成开源的轮子可以供参考,我就自己通过proxy-wasm-go-sdk和key师傅的HaE来实现一个demo版本,来达到Api安全这块的一个监控预警能力建设。
在 Envoy 中,VM 通常是在每个线程中创建的,可以创建多个,并且彼此隔离。
其中的Wasm Service是运行在Main Thread中的插件,通常做一些如聚合指标、日志等工作。
这里有个小插曲: 在用proxy-wasm-go-sdk开发的时候,发现该项目已经给出警告并存档了。可能的原因是在处理包含非ASCII字符的内存块时候可能存在内存泄露:https://github.com/wasilibs/nottinygc/issues/46
相同的,Coraza的wasm项目也存在内存泄露的情况,也是因为由TinyGO编译的项目:https://github.com/corazawaf/coraza-proxy-wasm/issues/249
访问proxy-wasm-go-sdk的repo,而其中给出的解决方法是使用非GC类的语言,如C++、Rust等,而非使用Go。正是因为TinyGO使用bdwgc作为垃圾回收的标准方案,该bdwgc在工作的时候时而会出现一些内存泄露的问题。
继续访问查看发现在如下Issue中,Higress的作者johnlanni有给出一个替代的方案:https://github.com/tinygo-org/tinygo/issues/3733 ,正是使用nottinygc来替代,且nottinygc是可以在TinyGO 0.28+版本中的可选替换方案。
在最后,johnlanni有fork到proxy-wasm-go-sdk的repo,并更换了默认的bdwgc为nottinygc,目前该解决方案也被作为Higress的Wasm-proxy官方解决方案。虽然这已经可以很大程度上避免内存泄露的发生,但除非有遇到上述处理包含非ASCII字符的内存块问题,现在nottinygc的作者也确认了该issue并等待官方golang给出支持wasm的集成方案,不过就目前的需求下来讲已经足够我们开发实现API网关版HaE插件。
正文: 所以后续的开发我们这里就使用Higress的SDK来开发:https://github.com/higress-group/proxy-wasm-go-sdk
因为HaE的项目本身就有一个config配置文件,里面记录了所有需要匹配的正则和目标Scope。为了让该配置也能拿来直接用,我在开发的时候也做了一定程度上的适配。
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
7
同时还对获取到的日志做一个监听更新的功能,这文件内容被修改的时候会触发重新加载到内存中
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
8
如此一来,在Apisix捕获到请求的代码片段里,再调用到正则匹配的函数,如果有对应匹配到则输出日志,方便后续通过日志采集工具同步到平台上。
package mainimport"syscall/js"funcmain() { alert := js.Global().Get("alert") alert.Invoke("Hello World!")}
9
大致代码编写的差不多了,就可以按照上述tinygo的方式编译该wasm-go程序
还是使用Docker来编译,由于tinygo的GC问题,Higress的Wasm-SDK是使用的nottinygc,需要在go文件上import
GOOS=js GOARCH=wasm go build -o static/main.wasm
0
并在编译的时候加上参数
GOOS=js GOARCH=wasm go build -o static/main.wasm
1
但是在编译后发现出现错误cannot use uintptr(size) (value of type uintptr) as int value in struct literal。经过查询发现是Higress的作者回复该Issue:“因为tinygo到0.33有不兼容的升级,对应的sdk还没有适配。”【https://github.com/alibaba/higress/discussions/1331】
因此解决方案只能是降级tinygo的版本为0.31.0的,最后的编译命令如下:
GOOS=js GOARCH=wasm go build -o static/main.wasm
2
但是编译还是出现了问题
提示“undefined: syscall.RLIMIT_NOFILE”
由于tinygo的版本比较低,因此对应的fsnotify也需要对应的降级下版本,这里我调整成了1.7.0。但是随之而来出现了一个新的问题:
GOOS=js GOARCH=wasm go build -o static/main.wasm
3
这个问题我在网上看了一圈,是属于tinygo的一个结构添加少了,golang在处理这个问题的情况是做了如下commit:https://go-review.googlesource.com/c/go/+/506175/3/src/syscall/tables_wasip1.go#79
这里我是有尝试过编译一个tinygo来解决这个Issue。但后续的viper由于调用了go goroutine(因为WASM前面说到是单线程的处理),所以这里只能修改程序监控逻辑,去掉viper的监控了。
GOOS=js GOARCH=wasm go build -o static/main.wasm
4
但这里还有一个坑,就是在编译的时候提示“runtime.alloc”:symbol multiply defined!的问题
我是根据https://github.com/higress-group/nottinygc的Usage里面参照的,但是后面发现其实已经在sdk里面引入了,所以无需在go文件中import对应的依赖
GOOS=js GOARCH=wasm go build -o static/main.wasm
2
直接编译即可
这里还踩了一个坑,运行后Apisix没有任何报错也没有日志打印出来,一致以为是插件哪里出了问题,后来才发现是proxywasm.LogInfof的函数打印不出来,得使用warning的日志函数,改成proxywasm.LogWarnf就可以了。
完整的代码为上传到gitee上:https://gitee.com/leiothrix/WasmHae.git 为了方便复现,上传的是demo,后续日志采集到功能没有完善可以自行修改。
Container?Wasm!
这个是我在搜寻资料的过程中看到一个有意思的,很好玩的项目工程。
由于Wasm的VM和安全性机制,这个可以用于什么方式中,估计可以大胆想象...蜜罐?本地化计算?恶意文件分析?IDE结合Wasm容器环境自动化部署?边缘计算Serverless?等...
https://github.com/ktock/container2wasm/blob/main/README.md
总的来说,container2wasm 能够将容器转换为 wasm 镜像,使其可以在 WASM 运行时上运行。
比如常见的github.dev,可以在浏览器中执行命令编译和调试代码
本机搭建环境测试的时候一直有点问题,就不深入了,纯当个题外话题引入想象下,或许有奇思妙想的思路可以实现下。
这一部分其实是想说下Wasm的应用直接运行在容器上带来的安全性,正因为WASM对于运行时有一套独立的VM,所以可以在使用的时候完全隔绝出一个新的环境出来,里面的所有虚拟出来的文件和系统信息和Docker容器获取到的完全不同。
以 PHP 为例,如果将 PHP 的程序运行在独立的 WASM VM 空间中,其沙箱虚拟化的技术可以增加隔离性和额外的虚拟环境信息被获取。如Jesús González发布的 WASM 减轻 PHP 中文件系统漏洞那样: https://wasmlabs.dev/articles/mitigating-php-vulnerabilities-with-webassembly ,WASM 能够有效的组织例如Archive_Tar库漏洞所带来的文件系统漏洞。
因为在 WASM 中有明确的deny-by-default的安全策略,这意味着没有经过明确设置,是不允许访问系统资源
接下来向你展示如何通过 WASM 在 Docker 容器中运行一个 PHP 应用脚本
因为PHP 的代码解释器底层是用 C 写的,因此可以通过 WASI-SDK 将其编译成 php.wasm 文件,方便之后直接替代原始的 php 二进制解释程序直接运行。
在编译之前,还需要准备一些环境:
WASI-SDK WasmEdge 运行时
访问 https://wasmedge.org/docs/start/install#generic-linux-and-macos 可以找到安装 WasmEdge 的方式,同时下载 WASI-SDK 安装到本地的目录并解压。
PHP 的wasm 运行时编译环境可以从 https://github.com/webassemblylabs/webassembly-language-runtimes 上clone
GOOS=js GOARCH=wasm go build -o static/main.wasm
6
运行 Make 之后会使用 Docker 的镜像构建编译一个php-cgi-8.1.11.wasm 如果想要构建AOT (ahead-of-time)以达成在云原生中快速运行和更小的内存占用,可以使用 wasmedge 工具来优化编译
GOOS=js GOARCH=wasm go build -o static/main.wasm
7
现在可以编写一个 Dockerfile 来构建你的基础镜像
GOOS=js GOARCH=wasm go build -o static/main.wasm
8
docroot 这里就放一个 test.php用来验证测试 WASM 的安全性
GOOS=js GOARCH=wasm go build -o static/main.wasm
9
因为 WASM 有独特的运行时,所以只需要包含 Host 主机和 Linux 内核部分的功能,因此就使用最小空间的 scratch 镜像来作为基础镜像。
<html><scriptsrc="static/wasm_exec.js"></script><script>const go = new Go(); WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject) .then((result) => go.run(result.instance));</script></html>
0
如果你是使用 Windows 的 Docker Desktop来运行的,记得把对用的 WASM 功能打开
编译好了之后,记得验证一下 Architecture 是不是 wasm 了
<html><scriptsrc="static/wasm_exec.js"></script><script>const go = new Go(); WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject) .then((result) => go.run(result.instance));</script></html>
1
之后就可以通过指定 runtime 来运行容器
<html><scriptsrc="static/wasm_exec.js"></script><script>const go = new Go(); WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject) .then((result) => go.run(result.instance));</script></html>
2
然后一直提示我如下错误,这个估计是 Docker Desktop 的版本太老了,更新一下应该就行
我更新到较新版本后不知道为啥还是没有解决,不过是演示,就用 WasmEdge 直接运行演示了。
用 WasmEdge 执行的时候需要使用--dir指定挂载进 WASM VM 的路径
<html><scriptsrc="static/wasm_exec.js"></script><script>const go = new Go(); WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject) .then((result) => go.run(result.instance));</script></html>
3
先来看看普通 PHP 的执行结果
可以看到环境变量和根目录下的文件内容
再来看看 wasm 的 PHP-CGI 执行结果
可以看到无法访问到环境变量和根目录下文件的内容了,包括系统的内核版本信息也无法正确获取到
这里写了一个简单的命令执行,测试结果一样也是没有任何输出,而传统的 PHP 命令则会输出内容
所以从某种程度上来说,WASM 的确减少了不少暴露的攻击面,让能够利用的攻击路径大大缩减。如果拿来跑简单的服务,适应云原生的快速启动和响应,的确是不错的选择。
Golang+Rust 的分离
之前有介绍过 WasmEdge 可以编写一个 AOT 的程序,让你能够脱离实际的 WASM 运行时而单独运行,无需安装任何依赖去解析你的 WASM 程序。 https://wasmedge.org/docs/embed/go/app/ 看到文档的介绍中能够直接嵌入
文档中的介绍也很简洁,就是直接通过 Rust 编译一个 WASM 的程序,最后再 Golang 中引用 WasmEdge 的库调用对应的 WASM 程序导出函数即可。可以做一个简单意义上的黑白分离。
创新应用拓展
通过上述所有的案例,可以看出 WASM 在安全能力提升中还是要不少潜在的创新应用场景,或许还有其他的有趣思路可以拓展:
或许还可以使用Wasmer JNI调用的方式加载恶意 wasm 绕过RASP? 升级云原生网关的 HaE 插件,实现动态加密/解密?或身份认证与访问控制 利用 WASM 的代码执行特性,部署轻量级的 AI 模型,用语义分析的方式监听数据库的执行内容? WAF 的模块中接入 STIX/TAXLL 客户端,同步最新 IOCs 指标?
Reference
[1].https://blog.csdn.net/alex_yangchuansheng/article/details/134566096
[2].https://apisix.apache.org/blog/2023/09/08/APISIX-integrates-with-Coraza/
[3].https://docs.api7.ai/apisix/how-to-guide/security/waf/integrate-with-coraza
[4].https://github.com/wasmerio/wasmer-go
[5].https://kwasm.sh/quickstart/
[6].https://github.com/pingerr/sec-proxy-wasm
[7].https://github.com/tetratelabs/zta-demo-2022
[8].https://developer.aliyun.com/article/1084904
[9].https://higress.cn/docs/latest/user/wasm-go/
[10].https://medium.com/trendyol-tech/extending-envoy-proxy-wasm-filter-with-golang-9080017f28ea
[11].https://apisix.incubator.apache.org/zh/blog/2022/01/26/apisix-integrate-forward-auth-plugin/
[12].https://istio.io/latest/zh/docs/concepts/wasm/
[13].https://higress.cn/zh-cn/blog/30-line-wasm.html
[14].https://mp.weixin.qq.com/s/lTVN3dIzIaGF2eRoBi_s_Q
[15].https://github.com/higress-group/proxy-wasm-go-sdk/
[16].https://github.com/ktock/container2wasm/blob/main/README.md
[17].https://developer.fermyon.com/wasm-languages/php
[18].https://github.com/vmware-labs/webassembly-language-runtimes
[19].https://wasmlabs.dev/articles/docker-without-containers/
[20].https://docs.docker.com/desktop/features/wasm/
[21].https://wasmedge.org/docs/embed/go/app/
[22].https://www.cnblogs.com/rongfengliang/p/17802326.html
[23].https://blog.51cto.com/u_16175517/7802271
[24].https://wasmedge.org/docs/embed/go/app/
[25].https://www.virusbulletin.com/virusbulletin/2018/10/dark-side-webassembly
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...