书接上回, 我 暴打了我们 4090 48G 服务器的 BMC. 之后, 我觉得不爽, 凭什么我没法控制机箱的 GPU 风扇? 而且, 凭什么这垃圾 BMC 的 SSH 有 sysadmin 登录可以执行 shell 命令, 而我在系统里却只能憋屈地用 ipmitool 里面的一大坨? 这太气人了! 于是乎, 我让 Agent 整了点大活, 认真调教了一下服务器的风扇.

In-band Shell

虽然 Agent 抱怨了, 说这不就是开后门吗! 但是他还是干了.

What to Target

既然要做 In-band Shell, 那就意味着要在 ipmitool raw xxx 里面塞一条能用于执行 sh -c 的命令. 怎么做呢? 自然能联想到, 要么加一条命令, 要么把现有的命令改一条.

BMC 的文件系统是一个三个区的 SPI Flash, 总共 30M. 其中, 大头是 root, 只读, 有 20M 左右, 是 BMC 的本体; 另有一个 2M 左右的只读 www 分区存放 BMC 网页, 能持久化写入的只有一个 960KB 的 conf 分区, 挂载在 /conf 上.

BMC 用 SysV init, 启动的时候, 先挂载 /conf, 然后启动 IPMIMain, 之后进入 rc3 拉起 ssh, 最后拉起 cron. 整个启动链条中, cron conf 位于配置区, 可以持久化, 且允许通过 @reboot 执行任意自定义命令. 也就是说, 存在一条通路, 能持久化我们想要的修改.

IPMIMain 能静态和动态加载库. 其中, 底层的分发库 libipmimsghndlr 在编译期链接进 IPMIMain, 而 /usr/local/lib/ipmi/ 下存在不少动态链接库文件, IPMIMain 会依次加载, 然后根据 /conf/BMC/IPMI.conf 里面的配置决定是否要启用.

因此, 可以据此给出三条可能的技术路线: 首先是写一个新的 so 并注册模块; 修改一个现有的 so 增加一个命令, 或者修改一个现有的 so 覆盖一条命令.

我首先让 Agent 检查了新增动态库的可行性. Agent 指出, 由于 /usr/local/lib/ipmi/ 目录只读, 没办法新增文件; 而 bind mount 整个文件夹则需要往 shmem 里面扔整个文件夹, 不太优雅. 同时, rootfs 里面写死了一个 netfn --> handler 的表, 这个表不好改, 也不容易增加项.

于是问题转向了修改哪个现成链接库.

Agent 首先选定了 0x2a 0x00 SetDebugLevel, 位于 libipmipdkcmds.so. 但是当时发现了几个问题: 其一, 0x2a 上面还管着一些东西, 比如风扇啥的, 没那么好; 其二, 这个库的 LOAD#1 只读区间紧挨着 LOAD#2 读写区间, patch 起来有一些风险.

我当时觉得不够好. 考虑到有一个 netfn 0x39 的系列在 host 上压根没发调用, 位于 libipmiamioemscorpiocmds.so.2.1.0, 我当时提出把 0x39 换成 0x38 使用. 但是在检索后发现, 尽管在 host 上无法调用, 但是在 BMC 内部, 这些 netfn 还是有 caller 的.

于是, 我把目光放在另一组 0x36 APML 功能上. 由于 APML 是 AMD 平台的功能, 我们是 Intel 平台, 所以这一组功能无论如何也用不上. 我让 Agent 确认, 其给出, 整组功能没有任何 caller, 可以覆盖. 而且, libipmiapml.so.4.1.0 这个 so 的 0x1A74 - 0x1FFF 区间是全 0, 正好给出了一段 1420B 的可以 patch 的区间.

What to Build

我让 Agent 在这个区间里面塞了一个 “会话式 Shell 执行器”, 类似交换机 SNMP 的那种: 支持四种操作, START 启动一个新命令, READ 读出 stdout / stderr, KILL 停止一个命令, STATUS 列出所有当前追踪的命令. 然后, 为了提前测试这个程序, Agent 写了一个 c 的测试器, 和一个 Python 的客户端. 这之后, 我又让 Agent 写了一个 Python 脚本 bmc_sh 来获得与直接用 shell 调用命令差不多的体验.

What Agent Builds

Agent 首先手搓了一大段 ARM Assembly… 这玩意要是我写, 我至少得写一个星期:

然后它写了一个 Python 脚本 apply patch:

然后它写了一个 arm c, 可以在 BMC 上测试 patch:

然后是 BMC 重启的时候用于持久化改动的 shell script:

当然, Agent 还写了很多临时脚本, 这里就不赘述了.

最后, Agent 写了本地的客户端

和简单的 wrapper script bmc_sh:

The Outcome

于是, 我们可以在 Host 上愉快调试 BMC 了!

1
2
3
4
5
6
$ sudo bmc_sh id
uid=0(sysadmin) gid=0(sysadmin) groups=0(sysadmin)
$ sudo bmc_sh uptime
 06:46:19 up 12:56,  0 users,  load average: 2.38, 2.33, 2.34
$ sudo bmc_sh "uname -a"
Linux G3DE 3.14.17-ami #1 Tue Apr 21 09:20:40 GMT 2026 armv6l GNU/Linux

SYS FAN

系统风扇相对非常简单 – BMC 里面已经有现成的命令了. 直接让 Agent 写一个 wrapper 就行

FIB FAN

但是 GPU 风扇就没那么容易了. GPU 风扇在传感器列表里面有读数, 但是 IPMI 里面没有暴露可以设置的接口. 我让 Agent 去查查, 经过一番寻找, 他 probe 到, 位于 i2c bus 13 0x2d 位置一颗 NCT7904D 风扇控制芯片, 其 BANK 3 温度曲线为 25/30/34/37°C → 40/60/75/90%, 推测为机箱的出风温度.

由于前面已经实现了 Shell 访问, 我就懒得让他再 patch library 了. Agent 写了一个 c 文件 (对, 这所有的 c 文件都不依赖 stdlib, 全是 raw syscall… 太离谱了)

然后写了一个与 SYSFAN 类似的 Python 脚本来 invoke:

如此便实现了手动控制 GPU 风扇.

感受…

这年头 Agent 太能写了… 我只需要给出一些方向, 把 plan 看好了, 剩下的一切都不用管, 自然而然就能完成… 唯一的问题反倒是要定期让 Agent 清理大量 defensive 代码, 保持代码的简洁性.

Anyway, 我可以给 GPU Fan 拉满了!