编辑
2025-02-10
工作知识
0

atexit

之前同事问了一个问题,在调试程序的时候,想要在程序退出的时候进行一些清理工作,但是不太清楚这个程序内应该怎样添加好。当时没有回答出来这个问题,最近在翻阅的时候找到了这个标准库函数atexit,本文介绍简单介绍这个函数功能。留作记忆

示例代码

#include <stdlib.h> #include <stdio.h> void atexit_func() { printf("atexit_func is called\n"); } void main() { atexit(atexit_func); return ; }

此代码通过atexit注册了一个退出回调函数atexit_func,此函数打印了一句话。

运行代码

为了能够证明这个atexit早就存在并可以使用,这里以c89来进行编译构建如下:

gcc test.c -std=c89 -o test

然后运行如下

# ./test atexit_func is called

总结

至此可以发现,此函数可以用作程序的退出回收工作,我们在调试操作系统上一些社区特别大型的c程序的时候,不是很方便在社区代码中添加回收逻辑的时候,这个atexit可以做到这点。

编辑
2025-01-22
工作知识
0

根据防破解之-完整性校验我们拿到了固定的摘要,我们需要针对这个摘要进行加密,否则其他人可以修改#这个摘要信息,将已经破解的文件的摘要放在指定节上。

一、什么是非对称加密

了解非对称加密的时候,我们需要先知道什么是对称加密,对称加密指的加密和解密时使用的密钥都是同一个,是“对称”的。图个网络图片例子如下:

image.png

这里我们虽然发现对称加密能够做到加密策略,但是如果对称密钥泄露,那加密也就被破解,也就是如何把密钥安全地传递给对方,这里非对称加密就出现了。

对于非对称加密,它有两个密钥,一个叫公钥,一个叫私钥。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有单向性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。

这样,非对称加密可以解决密钥被泄露的问题,也就是我们的所有的内容通过私钥加密,而我发布的公钥仅仅用来解密我私钥加密的内容。因为我私钥拿在手上,不会释放,所以没有办法破解密文

二、什么是RSA

RSA是比较著名的非对称加密,它的安全性基于“整数分解”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的,有兴趣了解rsa加密的可以查看此文章:RSA

我们可以实践如下:

2.1 生成公/私钥

为了更安全,这里选择2048长度

openssl genrsa -out rsa_private_key.pem 2048 openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

2.2 使用公钥加密

我们使用文件plaintext.bin作为待加密文件,ciphertext.bin是密文文件

openssl rsautl -encrypt -pubin -inkey rsa_public_key.pem -in plaintext.bin -out ciphertext.bin

2.3 使用私钥解密

openssl rsautl -decrypt -inkey rsa_private_key.pem -in ciphertext.bin -out out_plaintext.bin

此时我们可以发现"plaintext.bin"和"out_plaintext.bin"是完全相等的。

至此,我们可以在sha256的基础上,通过非对称加密将摘要信息进行加密,这样对方无法破解我们的摘要信息。从而保证了摘要信息被篡改的风险。

编辑
2025-01-22
工作知识
0

之前我们讲到了elf文件,通过解析elf文件,我们知道了对于关键文件需要保护哪些内容,这里主要针对是关键性文件的完整性校验的了解。

一、完整性计算

根据防破解之-elf文件格式我们知道了数据来源,为了实现关键文件防篡改,我们需要对这些内容进行完整性计算,针对此,我们应该满足下面三点:

  • 数据正向计算容易,逆向计算几乎不可能
  • 数据计算结果长度固定
  • 数据计算不易碰撞

根据上面的要求,结合当前已知的数据结构,我们可以选择hash,并且是单向hash。而常用的单向hash有哪些呢,如下:

  • md5
  • sha
  • sm3

对于此,我们可以如下假设,先定义一个数据源来自于.text

objcopy --dump-section .text=text.bin libhelloworld.so

这里我们提取了text.bin,我们先使用md5进行提取摘要

# md5sum text.bin 2d662e596919c294d7e3f274d75549b6 text.bin

使用sha256进行提取摘要

# openssl dgst -sha256 text.bin SHA256(text.bin)= 6aaf37f9ef03aa1f06dab8083784a4b50acca524f9cf7476acc52bd23dc118f2

使用sm3进行提取摘要 通过openssl

# openssl dgst -sm3 text.bin SM3(text.bin)= d0e43ea849949decc4cf8deb557f4cb46f0f560563dc2f0a63f6cbfa5e27de18

至此,基于三种算法的完整性计算方案已经演示

二、md5的碰撞

我们可以知道,md5默认是256bit的摘要提取,但是根据当前的技术状态,md5是能够存在碰撞的,虽然是2^128的概率,如下是碰撞例子:

数据1:

STR1=d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70

数据2

STR2=d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70

此时我们做一下文本转换

echo $STR1 | xxd -r -p > str1 echo $STR2 | xxd -r -p > str2

此时我们对比一下即可

# md5sum str1 str2 79054025255fb1a26e4bc422aef54eb4 str1 79054025255fb1a26e4bc422aef54eb4 str2

可以看到,上面STR1和STR2进行碰撞了

我们通过hexdump进行转换16进制如下:

hexdump -C str1 > 1 hexdump -C str2 > 2

此时我们对比即可两个str的不同

image.png

参考:https://www.mscs.dal.ca/~selinger/md5collision/

故,根据此信息,md5存在碰撞问题,我们可以选择sha或sm3,对于sha,我们通常情况下选择更通用的sha256。

三、最终选择

根据上面我们可以知道,如果我们不在意md5的碰撞问题,那么我们可以选择md5,如果比较在意碰撞,那么我们可以选择更通用的sha256算法

当然sha还提供了其他的信息摘要算法,如下:

# openssl help Message Digest commands (see the `dgst' command for more details) blake2b512 blake2s256 gost md4 md5 rmd160 sha1 sha224 sha256 sha3-224 sha3-256 sha3-384 sha3-512 sha384 sha512 sha512-224 sha512-256 shake128 shake256 sm3

这些摘要算法我就不一一演示,对于当前方案,我们只需要知道选择了sha256。后续如果有需求使用sm3,会在重构的时候使用sm3。

对于sha256的原理,我也不了解,需要时间沉淀,这里提供文档,点击即可阅读,有兴趣的可以了解一下

image.png

关于演示,这里贴出一个网页,也可以了解一下:

https://sha256algorithm.com/

编辑
2025-01-22
工作知识
0

为了让系统的二进制文件避免被破解,我们先需要针对elf文件进行了解,本文以防破解为目的,来简单了解一下elf的组成,如果需要详细了解的,建议阅读《elf.pdf》和《elf-64-gen.pdf》,文件点击即可阅读。理解此文章需要一点elf知识,建议可以先搜索引擎简单了解一下。

一、ELF的结构

这里以网上的图片为例,ELF结构如下:

image.png 可以发现,这里列举了必要的ELF内容,这里解释如下:

ELF Header: ELF的头结构 .text: 程序的代码段 .rodata: 程序的只读数据区 .data: 程序的已初始化的数据区 .bss: 程序的未初始化数据(初始化为0算未初始化) .symtab: 链接符号表 .strtab: 字符串表 Section Header:节的头表

而实际上,我以一个helloworld.c的so编译一个libhelloworld.so为例,查看一下elf的结构:

抛开elf头和节头,剩下的段如下:

# readelf -S libhelloworld.so 节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.build-i NOTE 00000000000001c8 000001c8 0000000000000024 0000000000000000 A 0 0 4 [ 2] .gnu.hash GNU_HASH 00000000000001f0 000001f0 0000000000000024 0000000000000000 A 3 0 8 [ 3] .dynsym DYNSYM 0000000000000218 00000218 00000000000000d8 0000000000000018 A 4 3 8 [ 4] .dynstr STRTAB 00000000000002f0 000002f0 000000000000007b 0000000000000000 A 0 0 1 [ 5] .gnu.version VERSYM 000000000000036c 0000036c 0000000000000012 0000000000000002 A 3 0 2 [ 6] .gnu.version_r VERNEED 0000000000000380 00000380 0000000000000020 0000000000000000 A 4 1 8 [ 7] .rela.dyn RELA 00000000000003a0 000003a0 00000000000000a8 0000000000000018 A 3 0 8 [ 8] .rela.plt RELA 0000000000000448 00000448 0000000000000048 0000000000000018 AI 3 19 8 [ 9] .init PROGBITS 0000000000000490 00000490 0000000000000014 0000000000000000 AX 0 0 4 [10] .plt PROGBITS 00000000000004b0 000004b0 0000000000000050 0000000000000010 AX 0 0 16 [11] .text PROGBITS 0000000000000500 00000500 00000000000000fc 0000000000000000 AX 0 0 8 [12] .fini PROGBITS 00000000000005fc 000005fc 0000000000000010 0000000000000000 AX 0 0 4 [13] .eh_frame_hdr PROGBITS 000000000000060c 0000060c 0000000000000034 0000000000000000 A 0 0 4 [14] .eh_frame PROGBITS 0000000000000640 00000640 000000000000009c 0000000000000000 A 0 0 8 [15] .init_array INIT_ARRAY 0000000000010df0 00000df0 0000000000000008 0000000000000008 WA 0 0 8 [16] .fini_array FINI_ARRAY 0000000000010df8 00000df8 0000000000000008 0000000000000008 WA 0 0 8 [17] .dynamic DYNAMIC 0000000000010e00 00000e00 00000000000001c0 0000000000000010 WA 4 0 8 [18] .got PROGBITS 0000000000010fc0 00000fc0 0000000000000028 0000000000000008 WA 0 0 8 [19] .got.plt PROGBITS 0000000000010fe8 00000fe8 0000000000000030 0000000000000008 WA 0 0 8 [20] .data PROGBITS 0000000000011018 00001018 0000000000000016 0000000000000000 WA 0 0 8 [21] .bss NOBITS 0000000000011030 0000102e 0000000000000008 0000000000000000 WA 0 0 4 [22] .comment PROGBITS 0000000000000000 0000102e 000000000000002a 0000000000000001 MS 0 0 1 [23] .symtab SYMTAB 0000000000000000 00001058 0000000000000690 0000000000000018 24 64 8 [24] .strtab STRTAB 0000000000000000 000016e8 0000000000000236 0000000000000000 0 0 1 [25] .shstrtab STRTAB 0000000000000000 0000191e 00000000000000e5 0000000000000000 0 0 1

其他的节的解释,可以查看官方文档。这里不做解释了。

二、关注的节

我们带着问题来找答案:目前需要对一个elf文件进行保护,那我们保护什么呢?答案应该如下:

  1. 代码的实现
  2. 程序的数据

我们不需要关心的是

  1. 无需加载到内存中的节
  2. 参与动态链接的节
  3. 参与异常处理的节
  4. 特殊或用于标识的节

这里我们可以知道,下面节是我们无需关心的:

.rela.dyn .rela.plt .plt .got .got.plt .eh_frame_hdr .eh_frame .shstrtab .*gnu*

需要关心的是:

.init .init_array .fini .fini_array .text .rodata .data .bss

但是这里我们注意的是32位和64位的.init* 和.fini* 目前没有由程序参与,故可以排除掉,则我们关心如下节即可:

.text .rodata .data .bss

2.1 .text

我们知道.text是二进制的代码段,我们可以通过命令查看如下,以libhelloworld.so为例

# objdump -d -j .text libhelloworld.so libhelloworld.so: 文件格式 elf64-littleaarch64 Disassembly of section .text: 0000000000000500 <hello_world@@Base-0xd4>: 500: 90000080 adrp x0, 10000 <hello_world@@Base+0xfa2c> 504: f947ec00 ldr x0, [x0, #4056] 508: b4000040 cbz x0, 510 <puts@plt+0x20> 50c: 17fffff5 b 4e0 <__gmon_start__@plt> 510: d65f03c0 ret 514: d503201f nop 518: b0000080 adrp x0, 11000 <hello_world@@Base+0x10a2c> 51c: 9100c000 add x0, x0, #0x30 520: b0000081 adrp x1, 11000 <hello_world@@Base+0x10a2c> 524: 9100c021 add x1, x1, #0x30 528: eb00003f cmp x1, x0 52c: 540000c0 b.eq 544 <puts@plt+0x54> // b.none 530: 90000081 adrp x1, 10000 <hello_world@@Base+0xfa2c> 534: f947e421 ldr x1, [x1, #4040] 538: b4000061 cbz x1, 544 <puts@plt+0x54> 53c: aa0103f0 mov x16, x1 540: d61f0200 br x16 544: d65f03c0 ret 548: b0000080 adrp x0, 11000 <hello_world@@Base+0x10a2c> 54c: 9100c000 add x0, x0, #0x30 550: b0000081 adrp x1, 11000 <hello_world@@Base+0x10a2c> 554: 9100c021 add x1, x1, #0x30 558: cb000021 sub x1, x1, x0 55c: d37ffc22 lsr x2, x1, #63 560: 8b810c41 add x1, x2, x1, asr #3 564: eb8107ff cmp xzr, x1, asr #1 568: 9341fc21 asr x1, x1, #1 56c: 540000c0 b.eq 584 <puts@plt+0x94> // b.none 570: 90000082 adrp x2, 10000 <hello_world@@Base+0xfa2c> 574: f947f042 ldr x2, [x2, #4064] 578: b4000062 cbz x2, 584 <puts@plt+0x94> 57c: aa0203f0 mov x16, x2 580: d61f0200 br x16 584: d65f03c0 ret 588: a9be7bfd stp x29, x30, [sp, #-32]! 58c: 910003fd mov x29, sp 590: f9000bf3 str x19, [sp, #16] 594: b0000093 adrp x19, 11000 <hello_world@@Base+0x10a2c> 598: 3940c260 ldrb w0, [x19, #48] 59c: 35000140 cbnz w0, 5c4 <puts@plt+0xd4> 5a0: 90000080 adrp x0, 10000 <hello_world@@Base+0xfa2c> 5a4: f947e800 ldr x0, [x0, #4048] 5a8: b4000080 cbz x0, 5b8 <puts@plt+0xc8> 5ac: b0000080 adrp x0, 11000 <hello_world@@Base+0x10a2c> 5b0: f9400c00 ldr x0, [x0, #24] 5b4: 97ffffc7 bl 4d0 <__cxa_finalize@plt> 5b8: 97ffffd8 bl 518 <puts@plt+0x28> 5bc: 52800020 mov w0, #0x1 // #1 5c0: 3900c260 strb w0, [x19, #48] 5c4: f9400bf3 ldr x19, [sp, #16] 5c8: a8c27bfd ldp x29, x30, [sp], #32 5cc: d65f03c0 ret 5d0: 17ffffde b 548 <puts@plt+0x58> 00000000000005d4 <hello_world@@Base>: 5d4: a9be7bfd stp x29, x30, [sp, #-32]! 5d8: 910003fd mov x29, sp 5dc: 52800020 mov w0, #0x1 // #1 5e0: b9001fe0 str w0, [sp, #28] 5e4: b0000080 adrp x0, 11000 <hello_world@@Base+0x10a2c> 5e8: 91008000 add x0, x0, #0x20 5ec: 97ffffc1 bl 4f0 <puts@plt> 5f0: d503201f nop 5f4: a8c27bfd ldp x29, x30, [sp], #32 5f8: d65f03c0 ret

2.2 .data

我们知道.data是二进制的已初始化数据段,我们可以通过命令查看如下

# readelf -p .data libhelloworld.so String dump of section '.data': [ 8] Hello, World!

2.3 rodata

我们知道.rodata是二进制的只读数据段,我们需要添加一个变量让其存放在rodata,如下:

const char k[]="Hello, Kylin!";

此时我们查看如下

# readelf -p .rodata libhelloworld.so String dump of section '.rodata': [ 0] Hello, Kylin!

2.4 .bss

我们知道.bss是二进制的未初始化数据段,我们需要添加一个变量让其存放在.bss,如下:

char bss[64] = "";

此时我们查看如下

# objdump -d -j .bss libhelloworld.so libhelloworld.so: 文件格式 elf64-littleaarch64 Disassembly of section .bss: 0000000000011038 <completed.9189>: ... 0000000000011040 <bss>: ...

三、如何保护

我们抓住了elf的重点,就是

可加载文件的在运行时加载的text/data/rodata/bss段 如果我们下载hexedit工具,我们只需要对libhelloworld.so进行修改,就能左右代码的执行,数据的信息等等。为了简单易懂,我以修改rodata为例示例修改elf文件。

3.1 制造篡改

假设程序如下:

#include <stdio.h> void hello_world() { const char k[]="Hello, Kylin!"; printf("%s\n", k); } # LD_LIBRARY_PATH=./ ./test-helloworld Hello, Kylin!

已知rodata的内容是"Hello, Kylin!",我需要通过hexedit修改为"Hello, World!"如下:

hexedit libhelloworld.so

我们知道Hello, World!和Hello, Kylin!的二进制如下

# echo "Hello, World!" | xxd -g 1 00000000: 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 0a Hello, World!. # echo "Hello, Kylin!" | xxd -g 1 00000000: 48 65 6c 6c 6f 2c 20 4b 79 6c 69 6e 21 0a Hello, Kylin!.

这里我们打算把Kylin更换成World,我们使用hexedit编辑即可

hexedit libhelloworld.so

这里我们将4b 79 6c 69 6e修改成57 6f 72 6c 64即可。此时我们再运行程序

# LD_LIBRARY_PATH=./ ./test-helloworld Hello, World!

3.2 开展保护

为了针对elf程序的修改行为,我们计划是新增一个名字为.encrypt的加密节,里面存放上述关键节的hash加密。关于hash的选择后面文章会提,这里提供添加节的方法如下:

objcopy --add-section .encrypt=sha256_text.bin libhelloworld.so libhelloworld_en.so

此时,这个新的so会多出现一个节,如下:

readelf -S libhelloworld_en.so [23] .encrypt PROGBITS 0000000000000000 00001058 0000000000000100 0000000000000000 0 0 1

至此,我们根据elf的基本组成原理,为了安全性考量,新增了一个由程序自定义行为(PROGBITS)的节.encrypt来实现对核心文件防篡改的基本需求

编辑
2025-01-22
工作知识
0

为了开发调试方便,这里基于apache2搭建了一个adv的文件上传下载服务器站点,地址是172.25.130.31。本文总结搭建方法,和操作步骤,从而真正意义上实现文件中转站的基本功能

一、为什么要文件中转站

鉴于我们的开发方式是通过服务器上的chroot环境进行开发,而我们的烧录通常是在linux/windows上通过烧录工具的方式,这就导致了我们需要在chroot环境中通过scp的方式将文件复制过来,假设我主机的ip地址是124,那么我需要如下:

scp boot.img root@172.25.80.124:/home/kylin

如果我们的目标ip经常发生改变,那么这个124会经常修改,这就导致我们传输文件比较繁琐。

我们知道,解决任何问题,都可以添加一个中间者,这样,发送者只需要关心发给中间者即可,接受者只需要从中间者接受。

于是文件中转站的角色便诞生了。

二、搭建apache服务器

安装apache2

apt install apache2

使能dav fs

a2enmod dav a2enmod dav_fs

修改apache2配置

vim /etc/apache2/sites-available/000-default.conf <VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot /var/www/html/ Alias /file-hub /root/owner-workspace/file-hub <Directory /root/owner-workspace/file-hub> Options Indexes FollowSymLinks MultiViews AllowOverride None Require all granted DAV On AuthType Basic AuthName "WebDAV Restricted Area" AuthUserFile /etc/apache2/.htpasswd <LimitExcept GET OPTIONS> Require valid-user </LimitExcept> Order allow,deny allow from all </Directory> ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost>

重启apache2服务

systemctl restart apache2

因为我设置了/root/owner-workspace/file-hub作为我的中转站目录,所以需要使得其具有文件权限

chmod 775 /root chmod 775 /root/owner-workspace/ chmod 775 /root/owner-workspace/file-hub/

至此,apache服务器已经可以正常使用了,如下所示

image.png

三、中转站的上传,下载,删除

对于中转站的上传,下载和删除,可以通过命令如下:

3.1 上传

curl -s -u $USER:$PASSWD -T "{$filename}" "172.25.130.31/file-hub/" -o /dev/null

3.2 下载

curl -O -u $USER:$PASSWD "172.25.130.31/file-hub/$filename"

3.3 删除

curl -X DELETE -u $USER:$PASSWD http://172.25.130.31/file-hub/$filename

3.4 脚本

大家可以根据上面命令的变体,编写upload.sh和download.sh脚本,这样就实现了文件的上传下载,这里给出示范如下:

# upload.sh USER="root" PASSWD="Ky1inos" WEBDEV="dev.kpan.com" argv=$# is_dir(){ if [ -d $1 ] then for i in `ls $1` do is_dir $1/$i done else fn+="$1 " fi } main(){ if [ "$argv"x == "0"x ] then usage; exit 0 fi # 上传目录 fn= for i in $@ do is_dir $i done filename=`echo $fn | tr ' ' ','` curl -s -u $USER:$PASSWD -T "{$filename}" "$WEBDEV/file-hub/" -o /dev/null } main $@ # download main(){ if [ "$argv"x == "0"x ] then usage; exit 0 fi # 上传目录 fn= for i in $@ do is_dir $i done filename=`echo $fn | tr ' ' ','` curl -O -u $USER:$PASSWD "$WEBDEV/file-hub/$filename" } # delete.sh main(){ if [ "$argv"x == "0"x ] then usage; exit 0 fi # 上传目录 fn= for i in $@ do is_dir $i done filename=`echo $fn | tr ' ' ','` curl -X DELETE -u $USER:$PASSWD "$WEBDEV/file-hub/$filename" }

上面脚本完成了基本功能,但是很多边界并没有考虑,大家在自己实现的时候可以考虑更全面一点

四、定期整理文件

我们作为中转站,需要每天按照日期归档文档,脚本如下:

#!/bin/bash FILE_HUB_DIR="/root/owner-workspace/file-hub" cd ${FILE_HUB_DIR} DATE=`date +%Y-%m-%d` [ -d ${DATE} ] && mv ${DATE} "${DATE}_bak" mkdir ${DATE} for file in $(ls ${FILE_HUB_DIR}) do [ -d ${file} ] && continue mv ${file} ${DATE}/ done COUNT=`ls ${DATE} | wc -l` if [ "${COUNT}"x == "0"x ] then alias rmdir=rmdir rmdir ${DATE} fi

脚本临时写出来,逻辑比较简单,为了让其能够正常的按天运行,如下配置crontab

crontab -e 0 1 * * * /usr/local/bin/organize_files.sh

五、简单的提示界面

根据上面的操作,我们已经完成了中转站的基本功能,为了让别人登录服务器的时候,更清楚我们登录了中转站,所以我们需要编写一个index.html,如下

vim /var/www/html/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Hub</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; } .container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); text-align: center; } a { text-decoration: none; color: #007BFF; } a:hover { text-decoration: underline; } </style> </head> <body> <div class="container"> <h1>欢迎使用麒麟文件中转站</h1> <p>请登录到 <a href="http://172.25.130.31/file-hub">http://172.25.130.31/file-hub</a> 查看当前中转站内容.</p> </div> </body> </html>

此时页面如下:

image.png

至此,我们完成了一个文件中转站的基本功能。欢迎大家使用