深信服 VPN-1000 前面板 LED 控制逆向
1. 背景
公司去年的时候淘汰了一批旧设备,其中就包含这个叫做 VPN-1000 的机器,从本质来说这就是一个有着四个网口的 J1900 软路由。和市面上能买到的软路由不同,它的前面板多出了 ALARM(警告) 和 HA(双机热备/高可用) 指示灯,用来指示机器的运行状态。
- ALARM:告警状态,常亮表示异常
- HA:主备状态,常亮为主机,闪烁为从机
因为最近公司在考虑给已经用了 20 年的网络拓扑推倒重来,中间涉及大量的网络测试与调试,所以把这台 VPN-1000 的硬盘拆出来全盘镜像后直接装了 OpenWrt 系统来利旧。但在 OpenWrt 替换原厂固件后,上述指示灯控制逻辑当然也随之丢失,开机就亮的 ALARM 看着就非常的不安 desu。
还好我们备份了原始固件。
2. 静态分析: sinforLed.ko
直接在根文件系统中搜索 LED 找线索,本来没报任何希望但这个二进制文件的文件名看着就非常和蔼可亲。掏出 IDA 然后从导出表和符号表开始排查:
首先看字符串:
sinforLed
ssl_led
failed register char device!
license=GPL
vermagic=3.10.0 SMP mod_unload
内核版本够老,不过那个时候流行的 CentOS 7 也就是这样了。
再看符号表:
led_exit
cleanup_module
init_module
fd
led
led_init
status
这不就有戏了嘛!
先看看 led_init
public led_init
led_init proc near
call __fentry__
push rbp
mov rbp, rsp
pop rbp
retn
led_init endp
发现其实只是一个空函数,也就是说这个设备是不用初始化的,或者说初始化的程序没写在 init 里面。
此时我已经感觉有点力竭了。其他几个符号所对应的程序段的大段汇编对我来说天书别无二致。还好热心的 IDA 提供了一些辅助信息让事情变得简单了起来:
loc_58:
mov cs:g_nCount, 32h ; '2'
mov eax, 4
pop rbp
retn
device_write endp
text:0000000000000080 QosTimerProc proc near ; DATA XREF: QosTimerProc+59↓o
.text:0000000000000080 ; init_module+61↓o
.text:0000000000000080 ; __mcount_loc:00000000000002D0↓o
.text:0000000000000080 E8 0B 06 00 00 call __fentry__
.text:0000000000000080
.text:0000000000000085 8B 0D A5 05 00 00 mov ecx, cs:g_nCount
.text:000000000000008B 55 push rbp
.text:000000000000008C 48 89 E5 mov rbp, rsp
.text:000000000000008F 85 C9 test ecx, ecx
.text:0000000000000091 74 2D jz short loc_C0
.text:0000000000000091
.text:0000000000000093 8B 35 93 05 00 00 mov esi, cs:g_Temp
.text:0000000000000099 BA FC 02 00 00 mov edx, 2FCh
.text:000000000000009E 40 F6 C6 01 test sil, 1
.text:00000000000000A2 75 5C jnz short loc_100
.text:00000000000000A2
.text:00000000000000A4 31 C0 xor eax, eax
.text:00000000000000A6 EE out dx, al ; COM: modem control reg bits:
.text:00000000000000A6 ; 0: 1=activate -DTR (-data termnl ready), 0=deactivate
.text:00000000000000A6 ; 1: 1=activate -RTS (-request to send), 0=deactivate
.text:00000000000000A6 ; 2: 1=activate -OUT1 (spare, user-designated output)
.text:00000000000000A6 ; 3: 1=activate -OUT2
.text:00000000000000A6 ; 4: 1=activate loopback for diagnostic testing
COM: modem control reg bits 这行字是我唯一一眼就能看懂的东西。直接丢给 Claude 去分析:
几轮对话下来,确定了:
LED(DTR 引脚) 受到 COM2 的 MCR 寄存器控制,IO 端口
0x2FC(COM2 基址0x2F8+ 偏移4)
不过这个文件翻来覆去似乎只能控制一个指示灯(ALARM),另一个(HA)指示灯如何控制还是毫无头绪。
3. 控灯实验(ALARM)
在 OpenWrt 上写数据最简单的方法就是 dd了。
printf '\x01' | dd of=/dev/port bs=1 count=1 seek=$((0x2FC)) 2>/dev/null
写一个 0x01,再用 2>/dev/null 吃掉回显,从开机时一直常亮的 ALARM 灯果然熄灭了。
这种方法其实非常可靠。SuperIO 上电复位后 DTR就是低电平,不管 CPU 在不在工作,总能保证 ALARM 在没有正常运行的时候亮起来。
但在这之后我故伎重施尝试找 HA 的控制方法却一直没找到,不久后就放弃了。
4. 意外地找到了 HA 指示灯的控制方法
几个月后,一次网上冲浪的时候看到了这样的帖子

这不直接印证了之前的分析。而且还爆出了之前没研究到的 HA 灯是由 GPIO 控制的情报。
5. 让 AI 直接帮我寻找 GPIO 端口
其实研究 ALARM 灯后我就怀疑过 HA 这个灯是 GPIO 控制的,但测试一些 J1900 自带的 GPIO 都没作用,后面 SuperIO 的 GPIO 因为陌生又菜便没什么心力去继续测试了。官方论坛的这篇帖子无疑是为我指明了方向,所以可以专攻之前没研究的 SuperIO 芯片了。
还好这个时候已经到了龙虾满天飞的时代。分析起来交给 AI 即可。它负责写脚本收集平台信息,我负责观察 LED 状态就把这件事办了:
6.1 确认 Super I/O 型号
通过 dmesg 确认系统已加载 nct6775 驱动:
nct6775: Found NCT6106D or compatible chip at 0x2e:0xa10
通过 0x2E/0x2F index/data port 读取 Chip ID:
Chip ID: C452
查阅内核补丁记录(lm-sensors,Guenter Roeck,2013):NCT6106D 在真实硬件上的 Chip ID 为 0xC452,数据手册所载 0x1061 有误。芯片型号确认为 NCT6106D。
6.2 LDN 枚举
逐一探测 LDN 0x00–0x0F 的激活状态与 I/O base:
| LDN | Active | IO0 | 功能 |
|---|---|---|---|
| 0x00 | 01 | 0x03F0 | FDC(软盘) |
| 0x01 | 01 | 0x0378 | LPT(并口) |
| 0x02 | 01 | 0x03F8 | COM1 |
| 0x03 | 01 | 0x02F8 | COM2 ← ALARM |
| 0x05 | 01 | 0x0060 | PS/2 键鼠 |
| 0x07 | DF | FFFF | GPIO(间接访问) |
| 0x08 | FE | 0x0280 | 待查 |
| 0x0B | 01 | 0x0A10 | hwmon |
LDN 0x07 激活(active=DF)但无 IO base(FFFF),说明 GPIO 通过 index/data 间接访问,不做直接端口映射。
6.3 LDN7 寄存器 dump(cold boot 状态)
进入 Extended Function Mode(连续写两次 0x87 到 0x2E),切换至 LDN7,dump 0xE0–0xFF:
E0: FF 00 00 00 FF 00 00 00
E8: EF EF 00 FF FF 00 00 00
F0: FF 00 00 00 FF FF FF FF
F8: FF 80 00 00 03 03 00 00
6.4 寄存器结构推断
观察 E0 区段的规律:
E0=FF E1=00 → 方向寄存器=FF(全 output),数据寄存器=00(全低)
E2=00 E3=00 → 方向=00(全 input),数据=00
E4=FF E5=00 → 方向=FF(全 output),数据=00
E6=00 E7=00 → 方向=00(全 input),数据=00
E8=EF E9=EF → 方向=EF,数据=EF
GPIO 寄存器为成对结构:奇地址为数据,偶地址为方向。
0xEF = 1110 1111,bit4=0:方向寄存器0xE8 的 bit4 为 input。
6.5 逐 bit 翻转验证
从已知熄灭状态(0xEF)出发,仅将 bit4 置 1:
0xEF | (1 << 4) = 0xFF
写入 0xE8(方向寄存器)后,0xE9(数据寄存器)bit4 置 1,HA LED 点亮。
单独验证:从熄灭基准值逐 bit 置位,仅 bit4=1 时 LED 响应,其余 bit 无效。
结论:HA LED 由 LDN7 寄存器 0xE8(方向)/ 0xE9(数据)的 bit4 控制。
- 写
0x87× 2 到0x2E,进入 Extended Function Mode - 写 LDN 选择寄存器(
0x07)=0x07,切换至 LDN7 - 读
0xE8,将 bit4 置 1,写回(方向设为 output) - 读
0xE9,按需置位/清零 bit4,写回(设置数据值) - 写
0xAA到0x2E,退出 Extended Function Mode
7. final
理清两颗指示灯底层控制逻辑后,终于可以在 OpenWrt 或者其他第三方系统中实现ALARM 告警灯与HA 主备灯的正常控制了:
#!/usr/bin/env python3
import struct
import sys
import time
# ALARM LED — COM2 MCR (DTR), active-low
ALARM_PORT = 0x2FC
# HA LED — NCT6106D Super I/O LDN7
# E8 是唯一控制寄存器(E9 为只读镜像),bit4=1 点亮,bit4=0 熄灭
SIO_INDEX = 0x2E
SIO_DATA = 0x2F
HA_LDN = 0x07
HA_REG = 0xE8
HA_BIT = 4
def port_read(port):
with open('/dev/port', 'r+b', buffering=0) as p:
p.seek(port)
return struct.unpack('B', p.read(1))[0]
def port_write(port, val):
with open('/dev/port', 'r+b', buffering=0) as p:
p.seek(port)
p.write(struct.pack('B', val))
def sio_read(reg):
port_write(SIO_INDEX, reg)
return port_read(SIO_DATA)
def sio_write(reg, val):
port_write(SIO_INDEX, reg)
port_write(SIO_DATA, val)
def sio_enter():
port_write(SIO_INDEX, 0x87)
port_write(SIO_INDEX, 0x87)
time.sleep(0.01)
sio_write(0x07, HA_LDN)
def sio_exit():
sio_write(0xAA, 0x00)
def alarm_set(on: bool):
# active-low: ON=0x00, OFF=0x01
val = port_read(ALARM_PORT)
if on:
val = val & ~0x01
else:
val = val | 0x01
port_write(ALARM_PORT, val)
def ha_set(on: bool):
sio_enter()
val = sio_read(HA_REG)
if on:
val = val | (1 << HA_BIT)
else:
val = val & ~(1 << HA_BIT)
sio_write(HA_REG, val)
time.sleep(0.01)
sio_exit()
USAGE = "Usage: sangfor-led {ha|alarm} {on|off}"
def main():
if len(sys.argv) != 3:
print(USAGE)
sys.exit(1)
led = sys.argv[1]
cmd = sys.argv[2]
if cmd not in ('on', 'off'):
print(USAGE)
sys.exit(1)
on = (cmd == 'on')
if led == 'ha':
ha_set(on)
elif led == 'alarm':
alarm_set(on)
else:
print(USAGE)
sys.exit(1)
if __name__ == '__main__':
main()
把脚本保存在 /usr/bin/sangfor-led,然后 chmod +x 此后就能方便地使用
sangfor-led ha on 或者 sangfor-led alarm off进行指示灯切换了!
2026-05-17 首发于知乎,此处文章为镜像