深信服 VPN-1000 前面板 LED 控制逆向

约 5 分钟

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:

LDNActiveIO0功能
0x00010x03F0FDC(软盘)
0x01010x0378LPT(并口)
0x02010x03F8COM1
0x03010x02F8COM2 ← ALARM
0x05010x0060PS/2 键鼠
0x07DFFFFFGPIO(间接访问)
0x08FE0x0280待查
0x0B010x0A10hwmon

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 控制。

  1. 写 0x87 × 2 到 0x2E,进入 Extended Function Mode
  2. 写 LDN 选择寄存器(0x07)= 0x07,切换至 LDN7
  3. 读 0xE8,将 bit4 置 1,写回(方向设为 output)
  4. 读 0xE9,按需置位/清零 bit4,写回(设置数据值)
  5. 写 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 首发于知乎,此处文章为镜像

这篇文章对你有帮助吗?欢迎通过关于页面里的方式告诉我。