本文完整阅读约需 18 分钟,如时间较长请考虑收藏后慢慢阅读~

群晖的家用、商用NAS因其易用性与稳定性饱受好评,但众所周知买系统送主机的群晖NAS价格高昂.对于不想花那么多钱的用户,我们有第三方Bootloader:Jun’s Mod来实现所谓的『黑群晖』。Jun’s Mod V1.02b中存在一个BUG,会导致DSM系统文件损毁,故障表现为可以ping通,ssh可以连接,但无法打开网页控制面板,提示『您所指定的页面损坏』。本文将给出在保留用户数据情况下解决该故障的一种方法。

0x01 问题重现

我的机器是HP Gen 7服务器,使用Jun’s Mod V1.02b作为Bootloader安装了群晖基于DS3617xs硬件的的DSM 6.1.5-15284系统。

在一次停电重启后,我发现我的黑群晖无法访问,错误截图如下:

通过ping与ssh可以连接上机器,但任何网页均无法访问。重启后问题依旧。

0x02 问题排查

既然可以通过ssh登录,我便在机器上执行了ps -ef命令,得到的结果如下:

......
root      2948 10340  0 15:30 ?        00:00:00 [synoscgi_______] <defunct>
root     10280     1  0 15:30 ?        00:00:08 /usr/syno/sbin/synocgid -D
root     10340     1  0 15:30 ?        00:00:26 synoscgi
system   10470 10340  0 15:30 ?        00:00:00 synoscgi
system   29109 10340  0 15:30 ?        00:00:00 synoscgi
root     29180 29148  0 15:30 pts/5    00:00:00 grep --color=auto cgi
system   31659 10340  0 15:30 ?        00:00:00 synoscgi
system   31834 10340  0 15:30 ?        00:00:00 synoscgi
system   31924 10340  0 15:30 ?        00:00:00 synoscgi
......

乍一看似乎没有问题,但请注意第一个进程后面的<defunct>字样。

defunct的中文释义为损坏的、不复存在的,在这里指的是僵尸进程,synoscgi进程可以理解为群晖中的cgi-bin,用于实现Web服务。

从上面的执行结果我们可以看到出现僵尸进程的软件路径为/usr/syno/sbin/synocgid,那如果尝试执行它,会得到什么结果吗?

于是我kill掉了synoscgi进程,并尝试手动启动:

kill -9 10280
/usr/syno/sbin/synocgid

果不其然,终端中出现了以下错误信息:

synoscgi: error while loading shared libraries: /lib/libsynoshare.so.6: invalid ELF header

那么我们可以基本确定导致Web界面无法访问的罪魁祸首,就是这个损坏的文件。

0x03 问题分析

Linux下的so文件与Windows下的dll文件类似,都是便于应用程序调用的动态链接库,在Windows下我们遇到dll错误或dll缺失会从互联网上或启动盘中拷贝一个工作正常的dll到指定目录,那么对于Linux,我们也可以使用类似的解决方法,将故障文件进行拷贝。

我在群晖官方文件目录中下载了所需的镜像文件:DSM_DS3617xs_15284.pat

等等,pat文件?面对一个陌生的(专有的)文件格式,新手们可能会无从下手,甚至望而却步,但其实要知道这个文件如何打开还是很容易的。在这里我先卖个关子,容许大家读完本节再揭晓答案~

1. 准备工具

对于一个陌生的文件,我们通常会直接查看它的二进制数据来获取文件特征/结构,但二进制不够直观,而且太长、太不易读,因此我们通常会将其转换为16进制。

能阅读文件二进制数据并将其转换为16进制文本的软件一般叫做16进制查看器/编辑器。在Windows下我们可以使用WinHex进行编辑,而在Linux/MacOS下我们可以使用hexdump命令来输出一个文件的二进制格式。本文使用hexdump命令举例,其他软件大同小异。

我们在pat文件所在目录下输入以下命令:hexdump -v -C ./DSM_DS3617xs_15284.pat | more,可以看到如下图所示的界面,接下来我将讲解画面中『乱码』的含义。

2. 了解什么是文件头

请大家看一下上面的截图,截图中就是hexdump软件的输出,分为三部分:左侧是字节数量游标(十六进制),中间是空格分隔的字节(一个字节由两个16进制字符组成,一个16进制字符由4个二进制码组成,即2^4=16),每一行有16个字节,右侧则是将字节转换为ASCII码后的结果,用于更直观的帮助用户了解十六进制背后的含义。

文件头则是文件最前面帮助软件识别文件类型或是获取文件概要信息的一串二进制数据,这样软件就可以在扩展名错误的情况下依旧识别出正确的文件格式,也可以快速的了解一个大文件(例如几十G的视频文件)的概要信息(如视频长度、码率等)。

简而言之,想要知道pat文件是什么,我们只需要阅读这个文件开头的二进制结构即可分析出其真正格式。

3. 了解常用归档文件的文件头

最常用的归档文件是压缩卷(例如zip、rar、7z等),但不是所有归档文件都是压缩文件(如bin、iso、tar等),pat文件包含了群晖DSM操作系统的所有数据,那么它一定是某一种归档文件。这里我们就来看看所有归档文件的文件头,然后将其与我们获取的十六进制数据逐一匹配。

常用归档文件的文件头如下所示:

文件格式 文件头
Gzip 0x1F8B
Bzip 0x425A68
Zip 0x504B0304
LHA 0x2D6C68352D
RAR 0x52617221
7Zip 0x377ABCAF271C

篇幅所限,只列举了很少一部分归档文件的文件头,想要知道绝大多数常用文件的文件头可以看这里

但还有一个归档文件我们没有提到,就是tar。这个文件和其他的归档文件有一点不同,就是它并没有文件头。因为tar的全称是Tape Archive,是将数据打包存储在磁带上的一种方法,也就是说在其发明之初tar并非是一个文件,而是一张磁带(既然已经知道了拿的是磁带,就不需要文件头去辨别要用磁带机还是微波炉来读取它了)。

根据维基百科,tar格式文件尽管没有文件头,但其包含的每个文件头部都有着固定的结构,就像磁带上有标签可以帮助辨别磁带的录制时间、磁带的录制者是谁、所有者是谁、磁带里是什么内容一样,tar文件的头部结构也是起到这样一个作用。

tar中文件的头部结构如下表所示:

字节偏移 字节长度(10进制) 含义
0 100 文件名
100 8 文件的unix mode(权限)
108 8 拥有者的unix用户ID
116 8 所属unix组的用户ID
124 12 8进制的文件大小
136 12 8进制的上次修改时间(unix时间)
148 8 头部数据的校验码
156 1 是否是软链接
157 100 所链接的文件名(路径)

4. 扳手指数数,只不过这次是十六进制~

既然我们已经了解了常见归档文件的格式,那么一一对应即可了解这是什么文件。

这里我截取了hexdump的一部分输出(0x00000000~0x00000160):

00000000  56 45 52 53 49 4f 4e 00  00 00 00 00 00 00 00 00  |VERSION.........|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000060  00 00 00 00 30 30 30 30  37 35 35 00 30 30 30 30  |....0000755.0000|
00000070  30 30 30 00 30 30 30 30  30 30 30 00 30 30 30 30  |000.0000000.0000|
00000080  30 30 30 30 34 34 32 00  31 33 32 37 37 37 30 37  |0000442.13277707|
00000090  35 33 33 00 30 31 30 36  34 35 00 20 30 00 00 00  |533.010645. 0...|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000100  00 75 73 74 61 72 20 20  00 72 6f 6f 74 00 00 00  |.ustar  .root...|
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000120  00 00 00 00 00 00 00 00  00 72 6f 6f 74 00 00 00  |.........root...|
00000130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000150  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000160  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

可以看到,这个文件的头部是VERSION(56 45 52 53 49 4f 4e),并不能与以上表格中的任何文件头相匹配。但我们在VERSION后发现了大量的0,扳手指数一数,从0x000000000x00000064刚好是100个字节,对比上文所提到的tar文件头部格式,可以发现两者是一致的。

按照tar文件的格式,在这100个字节后存放的是文件的权限,占八个字节,从上面的16进制中,我们同样可以看到从0x000000650x0000006c刚好是八个字节,其内容为0000755\0。如果还是不确定,可以继续往下对比,可以发现tar文件的头部结构均可以在以上16进制代码中找到对应。

5. 验证我们的猜测

既然我们通过阅读16进制文本的方式了解了这个pat文件的格式是tar,那我们就来验证一下上文所述是否正确。将pat后缀名修改为tar之后,再在命令行中执行tar xvf ./DSM_DS3617xs_15284.tar,我们可以发现文件成功解压了出来。这证明我们的猜测是准确的,通过16进制阅读的方式了解到了该文件的真实格式与后缀名,就是tar归档格式。

0x04 问题解决

这个时候,我们获取到了pat文件解压后的内容,可以从里面hda1.tgz文件解压后的hda1目录看到一个Linux系统的基础目录结构。那么接下来要做的就是将出现故障的文件用正常的文件进行替换。

1. 使用diff命令对比文件

在第二节中,我们通过报错信息了解到导致synoscgi进程无法启动的直接原因在于/lib/libsynoshare.so.6文件的损坏,但该目录下还有大量的其他动态链接库,且之间相互关联,如果只是拿正常的文件替换该文件,问题可能还是无法得到根本解决。在覆盖前,我们不妨先拿安装包内的目录与服务器上的目录做一个比对。

比对文件差异的软件很多,这里我们以Linux/MacOS下默认提供的diff为例进行讲解。

在这里,我将解压出来的目录拷贝到了服务器上的/mnt/image,然后执行以下命令:

diff -c -a -b -B -r -q /mnt/image/hda1/lib /lib

这里diff命令带了很多的参数,其含义如下:

-c     使用上下文输出格式.
-a     所有的文件都视为文本文件来逐行比较,甚至他们似乎不是文本文件.
-b     忽略空格引起的变化.
-B     忽略插入删除空行引起的变化.
-r     当比较目录时,递归比较任何找到的子目录.
-q     仅报告文件是否相异,不报告详细的差异.

执行后,该命令输出了以下结果:

Files lib/libsynoshare.so.6 and lib/libsynoshare.so.6 differ
Files lib/libsynopkg.so.1 and lib/libsynopkg.so.1 differ

果不其然,除了上文报错的/lib/libsynoshare.so.6以外,还有/lib/libsynopkg.so.1文件也出现了损坏。

2. 覆盖损坏的文件

既然已经知道了损坏的文件,同时也拿到了原文件,我们要做的就只是覆盖之。

但需要注意的是,在覆盖结束后我们还要重置一下这两个文件的权限,避免出现权限错误导致依旧无法启动。

介于群晖官方服务器速度过慢,这里我提供了这两个文件的下载:链接,需要的读者可以直接下载所需文件,无需下载巨大的系统镜像。

sudo mv -f /mnt/image/hda1/lib/libsynopkg.so.1 /lib/
sudo mv -f /mnt/image/hda1/lib/libsynoshare.so.6 /lib/
chmod 644 /lib/libsynopkg.so.1
chmod 644 /lib/libsynoshare.so.6

3. 验证是否成功

确定文件覆盖成功后,我们执行sudo reboot进行重启,稍等片刻访问web界面,发现此前提示『您所指定的页面不存在』的错误已经消失,显示出了正常的登录页面。


至此,我们成功解决了这一问题,在保留所有数据/设置的情况下对群晖DSM系统的故障进行了修复。如果遇到类似的问题(甚至是不小心误删系统文件),只要还能通过Telnet/SSH的方式登录进服务器,都可以借助本文的方法来进行紧急修复。

实际上,就算是无法登录进服务器,通过PE启动盘+DiskGenius或LiveCD的方式,只要能访问到硬盘分区,也是可以进行文件替换的。限于篇幅,不再赘述,愿读者在读完本文后能够举一反三。