引入
早在今年 8 月的时候 iuu6 就邀请我一起出 iuu6-ctf 的 CTF 题目 (消息记录找不到了 (懒得找))。然后我发现我能找到的、现有的 CTF 平台配置难度都比较高,于是就决定自己写个平台出来用。
想着能够让这个平台能够高性能一点 (折磨一下自己 (我绝对不是受虐狂)),于是用了 Rust 来写 (真的把自己折磨死了,我也不知道为什么我用 Rust 还想着用 SSR 服务端渲染方案)。
一开始也想着只是给这个 CTF 用的,本身也是作为这个平台的附属项目来做的,所以名字也是用这个 CTF 的名字来命名的。
直到今年我们工作室负责人 unknown 找到我说今年平台的事情我负责:
然后想起来手上刚好在写着一个 CTF 平台,于是加快了开发进度,并且给这个平台改了个名字,叫 attackr。
attackr 这个名字改自 attacker 一词,attacker 即 攻击者,用这个名字意为这个是为攻击者 (CTFer) 开发的平台。而去除 e 一字是参考了KAMITSUBAKI STUDIO (神椿 (裸春)) 母公司 THINKR 的命名方法 (唉,神椿人)。
开发过程
唉,一开始就不应该采用 Rust + SSR 的方案的,真的折磨死自己了 xwx。明知道自己不怎么会 Rust,还是第一次写 Web 应用。不过想一下也挺好的,给自己积累了一次写 Rust 和 Web 应用的经验,顺带让自己下次决定开发 Web 应用选择方案的时候能避下坑w。
要说其中最麻烦也是最折磨的部分就是怎么去实现构建并且是动态构建一个题目中的 Docker 容器,下面我将详细地讲一下这个平台的 Docker 构建策略。
Docker 容器构建
在设计 Docker 构建过程中,我想到了两个方案:
- 上传 Docker 镜像或者在创建题目的时候构建好 Docker 镜像,Flag 在启动容器的时候传入;
- 在用户触发构建的时候构建 Docker 镜像,并且 Flag 写死在 Docker 镜像里面。
最后我选择了第二个方案,其实兼顾了设计上的统一以及使用的便利性:
- 设计上可以不需要为题目构建中 Docker 的构建额外进行预构建操作;
- 编写者无需在启动时对相关 Flag 环境变量进行 unset;
- 参与者在使用时可以保证在每次启动 Docker 容器时环境不受改变,因为与 Flag 相关的内容将会持久化地保持在镜像中。
这个方案也得益于 Docker 有 Overlay2 镜像多层级存储结构,让每次镜像构建不需要重复非必要的过程和非必要的存储占用,用户触发构建时只需要进行对受 Flag 影响的步骤进行构建即可。
平台实现第二麻烦的东西就是动态分数和榜单的实现,下面我也会讲讲我是怎么实现这两个东西的。
动态分数
首先动态分数和前三血为了用户有更大的可调节空间,于是我采用了让用户去自己编写脚本实现的方案。
首先在脚本语言的选择上我花了大概一晚上去调查,一开始打算采用 Lua 方案的,但是考虑到 Lua 对平台有一定的依赖性,调用上也可能有一定的成本,用户也不一定方便配置。于是我着手去调查一下使用 Rust 实现的脚本语言,就找到了 Koto 这门轻量化的脚本语言,个人认为是一门不错的语言,并且提供的 API 也不难使用,然后敲定就用这个语言了。
动态积分和前三血都是给相应函数传入题目原分数和解出人数,动态积分计算的是题目分数,而前三血计算的是用户对当前分数的倍数。
榜单
然后就是来聊聊榜单。一开始数据库储存对分数相关的数据只有题目的当前分数和解出人的分数倍数,然后看到了其他平台的积分图表我意识到一个问题:我平台的图表不能显示分数的下降过程,也就是反映的并不是真实的变化过程,只能看出相应解出题目的时间点,对应的分数并不一定是当时所得到的分数。
随后就改了一下数据库和逻辑,在每次有人解出一道题的时候,数据库将会重新计算所有解出者在这道题获得的分数,并在数据库增加一条含有当前时间的一条记录。这样在渲染图表时,只需要创建一个 Map,对每个人的各记录按时间顺序进行遍历,以题目作为键并以分数作为值更新 Map 中的数据,取当前时间点更新后 Map 中的所有值进行求和即可得出当前时间点对应的分数,就可以绘制出带分数下降过程的得分图表了。
前置认证
这个功能是在比赛开始前一天弄出来的,主要是想到了有人去扫端口会有可能看到别人写的 exp 的风险,所以给加上了这个功能来预防一下。
实现思路就是使用 Docker 的 Bind 给 Docker 容器里面 /var/lib/attackr
路径绑定到宿主的临时文件夹上,在容器启动时执行一个脚本生成密钥输出到 /var/lib/attackr/passphrase.txt
,在认证时比较文件内容和用户输入的 Passphrase 即可。
所以总体也不难实现,就是前一天才想到要弄这个东西折磨死自己了 xwx。
这次平台用到的 HTTP Basic Auth 认证代理: https://github.com/angelbarrera92/basic-auth-reverse-proxy 。
其他部分的实现我就不展开叙述了,因为也没什么难点,可以去 HuajiTech GitLab 或者 GitHub 查看实现细节 www。
搭建过程
服务器选择
由于选择了 All in Boom 的思路来实现这套平台的,也就意味着这个平台只能够跑在一个性能足够的机子上。
上哪找性能这么强的机子呢?用云厂商的机子吧,性能要好的得贵死。最后还是决定了使用自己家里的台式机来搭建,然后想办法转发流量到公网。虽然说家宽经典 30M 带宽,不过我感觉应该也是够用了w。 (性能需求 >> 网络带宽需求 (确信))
流量转发
一开始选择 frp 进行转发流量,但是发现这个东西很奇怪,在网络掉线后并不能重新连回去就很诡异。虽然说我后面还配置了心跳包检测,但问题还是没有解决。最后还是采用了 WireGuard 通过隧道连接 (同时使用了 PSK (PreSharedKey) 对等加密),配合 wg-quick 的 PostUp 和 PostDown 支持使用 iptables 配置转发流量规则 (别问我为什么不用 nft,因为我懒得研究 nft 怎么配,反正现代 Linux 的 iptables 能够自动转换成 nftables 规则)。
HTTP 服务配置
HTTP 服务转发上使用了 caddy,配合了 Cloudflare DNS 的加速服务保证了访问速度。在 caddy 和 Cloudflare 之间采用 Cloudflare 签发的证书进行 HTTPS 通信,caddy 和服务端的 HTTP 之间也有 WireGuard 的加密,同时密码采用了 Argon2id 加密存储于服务器中,最大程度上保证了用户敏感数据 (包括邮箱、密码等信息) 的传输和存储安全。
最后也总算是顺利地把平台搭建起来给新生们用了 xwx。
结语
自开学以来就被 CTF 比赛塞满了,然后还要写平台搭建平台,同时还要给新生赛出题,属于是忙死了 xwx。不过也总算是熬过去了,给大家贡献还是挺开心的,大家能来光顾我做的平台、做我出的题目、给我平台提建议还是挺感谢的w。
最后记得来给我的平台给个 Star 鼓励一下我 www: https://github.com/ricky8955555/attackr 。