比尔盖子 博客

Sticky post
程序员们浪费了大量了时间考虑、担忧一个程序非关键部分的速度问题,但如果把调试和维护工作也考虑进去,这些提高性能的企图事实上带来了严重的负面影响。我们必须忘记微小的性能收益,可以说在 97% 的时候:不成熟的优化是万恶之源。但我们也不要丢掉优化那关键 3% 的机会。

by Donald E. Knuth

6504 型无线电与电工计算尺使用说明书

为了在电磁脉冲发生后能为拯救人类科技作出贡献(滑稽),最近在二手市场购买了一把天津计算尺厂在 70 年代生产的 6504 型计算尺。和普通的计算尺不同,这把计算尺具有几项无线电与电工的专用功能,可以计算谐振频率、阻抗和功率,但我不知如何使用。幸好,又在另一家二手市场发现了纸质版的使用说明书。

据我所知,目前互联网上并不存在这把计算尺的任何资料。为保存历史,兼顾持有相同型号计算尺的网友们,我已将说明书数字化,完整 PDF 文件可以点击此处下载

说明书的排版和印刷质量都比较差——看起来,它是先用钢笔手写完成排版,再用复印机印刷——这应该是设施匮乏的文革年代条件所迫。因此,不少文字都已经看不清了。原件如此,请读者见谅。

说明书封面

卷首印有《毛主席语录》一则

为何 128 位对称加密不如“128 位安全”的公钥加密安全

对称加密的安全性可以直接使用密钥长度判断,如 128 位、256 位,只要密码学家同意算法本身是安全的。相比之下,衡量公钥加密的安全性则比较复杂:我们选择目前最高效的算法(如大步小步、Pollard rho 等)来攻击手中的问题(如椭圆曲线离散对数问题),估算攻击所需的资源的数量级,然后与对称加密的攻击相比较,得出其安全性“等效于 n 位对称加密”的结论。例如 NIST 认为:256 位有限域上的椭圆曲线公钥加密拥有”等效于 128 位对称加密“的安全性,因此推荐与 AES-128 配合使用。

但问题真的有那么简单吗?知名密码学家(兼信息安全界知名喷子与段子手) Daniel J. Bernstein 有不同意见。在口水战泛滥的”256 位还是 128 位加密”问题上,他的观点也是我听说过最有趣的。


人们常说破解 128 位对称加密(如 AES-128)所需的运算量约为 2^128,但往往忽略了一个前提:这个等级的运算量,是破解一个密钥与一个密文所需的运算量。如果攻击者能获得使用不同密钥加密的相同明文,那么它可以对使用不同密钥的大量用户(或连接)同时进行无差别攻击,而且这是一个可以完美并行化的问题。例如同时攻击 2^40 个密钥,能破解一个是一个,那么至少破解一个密钥的所需的运算量就降低为 2^88。这已经接近人类能想象的数字(2^80)。而且攻击者还可以碰运气:降低搜索次数,成功率线性递减。例如只搜索 25%,将运算量降低为 2^86。

随后,作者还设想了一台大规模并行密码破解机。机器具有 2^32 个芯片,攻击者进行 2^32 次运算(虽然极大,但是人类可以想象的),那么破解至少一个密钥的可能性就是 2^-24。虽然 0.00000006% 的概率可以忽略不计,但纯理论上讲,如此之低的安全性是许多密码学家不能接受的。

相比之下,破解 256 位的椭圆曲线(如 Curve25519,虽然严格意义是 255 位),攻击者必须至少解决一个椭圆曲线离散对数问题,破解多个密钥的难度不比破解一个密钥容易。无论同时破解多少个密钥,运算量都至少为 2^128(攻击更多密钥则需要更高的运算量)。而且攻击者也不太可能碰运气:降低搜索次数,成功率二次方递减。

因此得到几个结论:

  1. 同样被评估为“128 位安全”,对称加密和公钥加密的安全性却有极大的差别。虽然在实践中可能没有什么影响,但有明显的理论意义。DJB 说——Bottom line: 128-bit AES keys are not comparable in security to 255-bit elliptic-curve keys. Is 2^255 – 19 big enough? Yes. Is 128-bit AES safe? Unclear.
  2. 在密码分析中,串行计算的性能和并行计算的性能不能盲目互换。如果条件合适,大规模并行攻击的成本可能比类似的串行攻击低上亿倍。
  3. 在这种意义上,将 256 位对称加密和“128 位安全的公钥加密”搭配并非是不合理的。这也是主张使用 256 位加密而不是 128 位加密的一个有趣理由。Daniel J. Bernstein 设计的 ChaCha20 只使用 256 位加密,可能就是这个原因。此外,这个攻击之所以可行,是因为不同的密钥被用来加密相同的明文:AES_k1(x), AES_k2(x), … 但如果不同的密钥总是加密不同的明文:AES_k1(x1), AES_k2(x2), …,无差别攻击就不再可行。有研究者建议提高 nonce 大小,例如将 nonce 提高到 128 位,然后随机选取,但这个方案比加长密钥复杂,而且有许多缺点。

当然,上述讨论不适用于量子计算机。等大型量子计算机问世,所有现在使用的公钥加密和 128 位的对称加密均可被破解,只有 256 位对称加密还算安全。

延伸阅读

It is widely asserted that 128-bit AES and a strong 256-bit elliptic curve provide comparable security levels against known attacks. This assertion is false. Known attacks batch and scale much more effectively for 128-bit AES than they do for a strong 256-bit elliptic curve.

There is a widespread myth that parallelizing a computation cannot improve its price-performance ratio. The reality is that a parallel computer is often several orders of magnitude faster than a comparably priced serial computer.

4.3BSD 系统上手体验(从磁带获取 /usr/src 源代码)

前几天为了测试一个程序,在 SimH 模拟的 DEC VAX-11/780 计算机中安装了 4.3BSD 系统(大概是历史上最伟大的操作系统之一?)。本系统于 1986 年夏天在加州伯克利新鲜出炉,安装磁带绝赞发售中……

# dmesg | head
4.3 BSD UNIX #1: Fri Jun  6 19:55:29 PDT 1986
karels@monet.Berkeley.EDU:/usr/src/sys/GENERIC
real mem  = 8388608

系统的安装过程完全是按照一篇教程进行的。当完成教程后,就可以启动 VAX-11/780 并进入 BSD 系统,这里就不重复了。

登录系统

4.3 BSD UNIX (bsd.my.domain) (console)

login: root
Last login: Mon Jan 27 09:19:47 on console
Jan 27 10:20:58 bsd login: ROOT LOGIN console
4.3 BSD UNIX #1: Fri Jun  6 19:55:29 PDT 1986

Would you like to play a game?

Don't login as root, use su
bsd#

一切仿佛就在昨天,Unix 的 getty 这么多年都没有变化过。另外发现原来连 4.3BSD 都会嘲笑 root 敢死队——小心点,如果误操作,你会引起第三次世界核大战的……(”Would you like to play a game” 显然是 BSD 开发者在玩 1983 年好莱坞科幻《Wargames》的梗)。

系统中甚至有有个名为 /usr/games/wargames 的游戏……

# /usr/games/wargames
Would you like to play a game? yes
Funny, the only way to win is not to play at all

不管你回答什么,它都只会打印 “Funny, the only way to win is not to play at all”,纯粹是为了玩梗而存在。如今技术圈的社交媒体的人们经常玩科幻、电影、动画相关的各种梗,看来一直是历史传统了。

寻找源代码

不玩游戏了,回到测试工作。

众所周知,BSD 操作系统不只是一个内核,而是一套完整的操作系统。因此,所有程序的全套源代码都可以在 /usr/src 中找到。为了进行测试,我需要调试并修改某个系统工具的源代码。于是我使用了 find 快速递归列出文件。

# find /usr/src
/usr/src
...
/usr/src/bin/as
...
/usr/src/bin/awk
...
/usr/src/bin/cat.c
/usr/src/bin/cc.c
/usr/src/bin/chgrp.c
/usr/src/bin/chmod.c
/usr/src/bin/cmp.c
/usr/src/bin/cp.c
/usr/src/bin/csh
...
/usr/src/bin/date.c
/usr/src/bin/dd.c
/usr/src/bin/df.c
/usr/src/bin/diff
/usr/src/bin/diff/diff.c

源代码是应有尽有,一目了然。

见鬼的是这个目录居然不存在?!

# ls /usr/src
/usr/src not found

显然 find 对系统中存在的文件似乎有不同的理解。不过没有什么是看文档解决不了的问题,RTFM 一下。

# man find

SYNOPSIS
find pathname-list expression
find pattern

DESCRIPTION
...
The second form rapidly searches a database for all path-
names which match pattern.  Usually the database is recom-
puted weekly and contains the pathnames of all files which
are publicly accessible.

原来 BSD 的 find(1) 居然这么强大,还包含了文件索引的功能(GNU/Linux 用户可以理解为 updatedb + locate)。换句话说,现在列出的文件都是安装盘建立时的索引,并不实际存在。

磁带

显然,/usr/src 在这台机器上还没有被安装,需要从磁带安装。要使用磁带,必须先将磁带放入磁带机,并根据磁带机的型号在 /dev 下创建设备文件。在 SimH 模拟器的 .ini 配置文件中,我已经配置好了磁带机设备 ts0,它对应的是 DEC TS-11 型磁带机。从名字就可以看出,这台磁带机最初来自 PDP-11 计算机,后来在 VAX-11 时代沿用。

att ts 43.tap

开机时可以在 dmesg 中看到磁带机。

# dmesg | grep ts0
ts0 at zs0 slave 0

但是却找不到相关的设备文件。

# ls /dev/ts0 /dev/zs0
/dev/ts0 not found
/dev/zs0 not found

原来 ts0 和 zs0 都是内核驱动的硬件设备名,并非真正的设备文件。因此,首先创建设备文件。由于 Unix 支持大量不同的设备,创建设备文件是一项十分繁琐的任务。好在 BSD 的同学们为我们准备了一个名为 ./MAKEDEV 的 shell 脚本。

# head /dev/MAKEDEV
#!/bin/sh -
#
# Copyright (c) 1980 Regents of the University of California.
# All rights reserved.  The Berkeley software License Agreement
# specifies the terms and conditions for redistribution.
#
#       @(#)MAKEDEV     4.27 (Berkeley) 4/15/86

创建 ts0 磁带机的设备文件时,只需要输入如下命令(不知为何,./MAKEDEV 没有可执行权限,于是使用 sh ./MAKEDEV)

# cd /dev
# sh ./MAKEDEV ts0

但运行后发现 MAKEDEV 创建的好几个设备文件,包括 /dev/mt0, /dev/mt4, /dev/mt8, /dev/mt12, /dev/rmt0, /dev/rmt4, /dev/rmt8, /dev/rmt12, /dev/nmt0, /dev/mnt8, /dev/nrmt0, /dev/nmrt8,不知应使用哪个文件。

实在不会了,只能上网搜索看看。我首先了解到知道的是,BSD 控制磁带机的工具是 mt,所以先看看本机中 mt 的文档(不能看网络上的,版本都不一样。看来在文档难求的年代,man page 能解决不少问题)。

# man mt
...
Mt is used to give commands to a magnetic tape drive.  If a
tape name is not specified, the environment variable TAPE is
used;  if TAPE does not exist, mt uses the device
/dev/rmt12.

status
Print status information about the tape unit.

原来可以查看磁带机的状态,试一试:

# mt status
ts11 tape drive, residual=0
ds=0
er=152<onl,ies,ped,bot></onl,ies,ped,bot>

确实能识别。

接下来意识到了 Unix 从磁带读取文件的工具是 tar(这显然是废话),也许 tar 的文档里有什么说明,继续 RTFM。

# man tar

t       The names of the specified files are listed each
time they occur on the tape.  If no file argument is
given, all of the names on the tape are listed.

f       Tar uses the next argument as the name of the
archive instead of /dev/rmt?.

原来当 tar 没有指定任何文件时,它会去读取默认的磁带设备文件(不仅如今还有多少人使用过这个功能,反正我没有……)。这就试一试。

# tar t
tar: blocksize = 1
tar: directory checksum error (0 != 217)

tar 提示校验和错误。后来又使用 tar tf /dev/rmtX 把所有的设备文件都试了一遍,没有一个是能正常读取的。

彻底卡在这里了,只能从网络上搜索并重新阅读 SimH 的 4.3BSD 安装教程。原来 tar 不能读取磁带的原因是因为之前没有倒带到开头。

# mt rewind

倒带之后,还需要向后移动三条“空白计数文件”的距离。作为新手,我还并不清楚什么是空白计数文件,也不知道为什么是 3 个。不过先运行一下,日后慢慢了解。

# mt fsf 3

FSF 不是自由软件基金会,而 Forward Space Count Files 的缩写。

再试试 tar,成功!

# tar t
./
./cassette/
./dist/group
./dist/fstab.hp
./dist/motd
./dist/passwd
./dist/rc.local
./conf/touch.c
./h/mount.h
./h/signal.h
./h/quota.h
./netinet/tcp.h
./netinet/udp.h

能看到许多熟悉的文件,但并没有我要找的 /usr 或者 /usr/src。磁带后面还有更多文件,继续倒带。但运行 mt fsf 或者 mt fsf 3 没有效果。后来猜测之前的记录是 mt fsf 3,下一条可能是 mt fsf 6,果然如此。

# tar t
Makefile
...
bin/df.c
bin/hostid.c
bin/mail.c
bin/du.c
bin/strip.c
bin/stty.c
bin/cat.c
bin/ld.c
bin/login.c
bin/time.c
bin/ed.c
bin/nm.c
bin/dd.c
bin/su.c
bin/rm.c
bin/pr.c
bin/cp.c
bin/cmp.c
bin/true.sh
bin/false.sh
bin/ln.c
bin/grep.c
bin/write.c
bin/mv.c
bin/passwd.c
bin/echo.c
bin/hostname.c
bin/ps.c
bin/cc.c
bin/tar.c
bin/kill.c
bin/sync.c
bin/size.c
bin/date.c
bin/tee.c
bin/wall.c
bin/chmod.c
bin/test.c
bin/rmail.c
bin/od.c
bin/chgrp.c
bin/ar.c
bin/expr.y
bin/rcp.c
bin/ls.c
bin/Makefile
bin/mkdir.c
bin/mt.c
bin/nice.c
bin/pagesize.c
bin/pwd.c
bin/rmdir.c
bin/who.c

找到了,这就是 /usr/src。接下来只需要再重新倒带,向前快进并解压磁带上的tar 归档即可。

# mt rewind
# mt fsf 6
# mkdir /usr/src
# cd /usr/src
# tar x

大功告成。

# ls
Makefile  etc/      include/  local/    ucb/      usr.bin/
bin/      games/    lib/      old/      undoc/    usr.lib/

关机

输入 halt。

# halt
Jan 27 11:01:03 bsd halt: halted by root
Jan 27 11:01:04 bsd syslogd: going down on signal 15
syncing disks... done
halting (in tight loop); hit
^P
HALT

由于 Unix 的传统 halt 命令是只关机不断电的(您现在可以安全的关闭计算机了?),因此模拟器此时会报 HALT 指令死循环错误。

HALT
Infinite loop, PC: 8002AFCD (BRB 8002AFCD)

输入 quit 退出 DEC VAX-11/780 模拟器。

Linode 旧东京服务器已永久退役

从上月起,比尔盖子的旧东京服务器已永久退役。至此,服务器的旧 IP 地址 106.187.x.x2400:8900 彻底作废。如果你在使用博主的服务器,请及时更新你的 IP 地址。新 IP 地址即 tomli.blog 的 A 记录和 AAAA 记录,可自行通过 pingdig 查询。

此外,由于多年来零零碎碎部署的许多服务,难免有疏漏。如果你发现我维护的某个网站或服务因忘记更新 IP 无法访问,请留言告知我,谢谢。

x86 的六种重启方法

31337 h4x0r 有一回对我说道,“你读过 x86 平台的底层代码么?”我略略点一点头。他说,“读过底层,……我便考你一考。x86 的电脑上,怎样重启的?”我想,脚本小子一样的人,也配考我么?便回过脸去,不再理会。

31337 h4x0r 等了许久,很恳切的说道,“不会重启电脑罢?……我教给你,记着!这些方法应该记着。将来做黑客的时候,开发要用。”我暗想我和黑客的等级还很远呢,而且我们编程序也不是用来重启机器;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不就是 ACPI 去写一个 ResetReg 么?”

31337 h4x0r 显出极高兴的样子,将两个指头的长指甲敲着键盘,点头说,“对呀对呀!……x86 有六样重启法,你知道么?”我愈不耐烦了,努着嘴走远。

31337 h4x0r 刚启动了 Emacs,想在编辑器上打字,见我毫不热心,便又叹一口气,显出极惋惜的样子。

Linux 内核支持全部六种方法……但为什么要支持六种?

有一些重启方法可能存在硬件实现问题,例如 8042 键盘控制器的方法,是支持最广泛,但也是电脑硬件 bug 最可能发生的方法。尽管 Linux 内核会逐一尝试每一种方法,很可能在尝试其中一种后导致硬件卡死。由于这些机器多半只在 Windows 上测试过,因此直到 2010 年左右,在 Linux 上重新启动机器时无法断电依然是比较常见的问题。此时,用户就需要逐一尝试这些重启方法,找到一种能用的,并向上游报告,上游将机器的型号和需要的重启方法加入表中。在现在出产的新机器上,这种问题已经非常罕见。如果你日后遇到了重启的问题,希望你能想起这篇文章,并对你有所帮助。

没错,六种方法重启,而且每种都不一定能工作,重启时需要逐一尝试,必然时候还需要查表,这就是充满历史遗产的 x86 世界的现状。

方法一:8042 键盘控制器的错误使用方法

8042 键盘控制器控制 x86 计算机上的 PS/2 键盘鼠标,但这大概也是 x86 计算机上能力最强大的硬件设备,因为它名义上是键盘控制器,但由于 IBM 工程师的脑洞,实际上还拥有控制 CPU 和内存的功能。历史上,8042 与 CPU 的 Reset 信号相连,还与内存总线上的一个逻辑门相连。

向 8042 键盘控制器(端口 0x64)写入 0xFE. 这会触发 CPU 的重置信号,并重启计算机。

ioperm(0x64, 1, 1);
outb(0xfe, 0x64);

在 Linux 上,可以使用内核参数 reboot=kbd,让 Linux 内核使用这种方式实现重新启动的功能。

方法二:向 PCI 的 0xCF9 端口发送重置信号

Intel 的 ICH/PCH 南桥芯片同时负责部分电源工作。向 PCI 的 0xCF9 端口发送重置信号,可以要求南桥芯片重启计算机。

ioperm(0xcf9, 1, 1);
uint8_t reboot_code = 0x06                 /* 热启动,冷启动 0x0E */
uint8_t cf9 = inb(0xcf9) & ~reboot_code;
outb(cf9|2, 0xcf9);                                    /* Request hard reset */
usleep(50);
outb(cf9|reboot_code, 0xcf9);            /* Actually do the reset */

在 Linux 上,可以使用内核参数 reboot=pci,让 Linux 内核使用这种方式实现重新启动的功能。

方法三:让 CPU 产生三重异常

如果代码触发了 CPU 的一个异常,并触发了事先注册的异常处理代码,但是异常处理代码本身也触发了异常,这就叫做双重异常。但这个异常处理代码本身也注册了异常处理代码,而且在执行这个异常处理代码的异常处理代码时又触发了异常,就产生了三重异常。从 80286 开始,三重异常会导致 CPU 进入 SHUTDOWN 状态,主板电路会重新启动系统。

例如:

load_idt(&no_idt);              /* 清空中断描述表 */
__asm__ __volatile__("int3");   /* 触发中断 */
/* 此时发生中断,由于 IDT 为空,无论是中断的异常处理代码还是双重异常处理代码都不存在,触发了三重异常
/* CPU 进入 SHUTDOWN 状态,主板重置 */

在 Linux 上,可以使用内核参数 reboot=triple,让 Linux 内核使用这种方式实现重新启动的功能。

尽管这个特性就在 80286 的手册附录里,而且 IBM 的工程师看到了而且还实现了必要的电路,然而 IBM 的工程师完全没有意识到这可以用来在切换到实模式时重置 CPU,而是将键盘控制器的一个端口接到 CPU 的 Reset 信号线上来进行重置 CPU 实现模式切换……

这个方法的成功率几乎是最高的,但大多数操作系统(包括 Linux 内核)不到万不得已,不会使用。虽然原则上这是为了正确进行电源管理,然而事实上的原因是,三重异常往往意味着操作系统出现了严重的故障,例如将内存中负责处理 Swap 的代码本身也移动到了 Swap 里,会立刻导致整个系统重启,数据荡然无存,开发者本来对三重异常感到恐惧,然而现在却要自己主动触发一个,实在是不能接受。此外,开发者也都认为触发个三重异常让 CPU 被迫重置实在是太残忍了,不忍心下手……因此,这种方法只会被开发者当作最终的手段。

方法四:切入实模式,跳转到 BIOS 代码重启

禁用各种东西,将 CPU 从保护模式切换回实模式,然后 ljmp $0xffff, $0x0000 跳转到 BIOS 的重置代码。 至于 BIOS 怎么重启,谁知道呢?不过只要 BIOS 实现正确,就可以工作(仅限于 32 位计算机)。

例如:

movl %cr0, %eax
andl $0x00000011, %eax
orl  $0x60000000, %eax
movl %eax, %cr0
movl %eax, %cr3
movl %cr0, %ebx
andl $0x60000000, %ebx
jz   f
invd
f:   andb $0x10, al
movl %eax,    %cr0
ljmp $0xffff, $0x0000

在 Linux 上,可以使用内核参数 reboot=bios,让 Linux 内核使用这种方式实现重新启动的功能。

方法五:通过 ACPI 重新启动

ACPI 是现代 x86 计算机必不可少的标准电源管理接口,自然也提供了重新启动计算机的功能。

在支持 ACPI 的机器固件提供的「固定 ACPI 描述表」(Fixed ACPI Description Table, FADT)中,保存有 reset_registerreset_value 两个数值,告知操作系统重启的方法。

rr = &acpi_gbl_FADT.reset_register;      /* 获取重启寄存器   */
reset_value = acpi_gbl_FADT.reset_value; /* 获取重启魔法数值 */

/* The reset register can only exist in I/O, Memory or PCI config space
 * on a device on bus 0. */

/* 在 PCI、内存和 IO 地址中寻找重启寄存器 */
switch (rr->space_id) {

/* 如果寄存器在 PCI,就写 PCI */
case ACPI_ADR_SPACE_PCI_CONFIG:
    /* The reset register can only live on bus 0. */
    bus0 = pci_find_bus(0, 0);
    if (!bus0)
        return;
    /* Form PCI device/function pair. */
    devfn = PCI_DEVFN((rr->address >> 32) & 0xffff,
              (rr->address >> 16) & 0xffff);
    printk(KERN_DEBUG "Resetting with ACPI PCI RESET_REG.");
    /* Write the value that resets us. */
    pci_bus_write_config_byte(bus0, devfn,
            (rr->address & 0xffff), reset_value);
    break;

/* 如果是内存或者 I/O 地址,直接用 writeb() / outb() 写内存或 I/O */
/* acpi_reset() 调用的是 acpi_os_write_port(), acpi_hw_write() */
/* 但只是多了一些检查,最后依然调用的是 writeb() / outb() */
case ACPI_ADR_SPACE_SYSTEM_MEMORY:
case ACPI_ADR_SPACE_SYSTEM_IO:
    printk(KERN_DEBUG "ACPI MEMORY or I/O RESET_REG.\n");
    acpi_reset();
    break;
}

写入数值后触发硬件重启。 在 Linux 上,可以使用内核参数 reboot=acpi,让 Linux 内核使用这种方式实现重新启动的功能。

方法六:通过 EFI/UEFI 重启

EFI/UEFI 是更现代计算机固件的接口标准,其中包括 UEFI 启动服务和 UEFI 运行服务,它们提供了一组标准的函数调用,提供一些和硬件相关的服务。好比系统调用为应用程序提供服务,UEFI 服务则为操作系统提供服务。在操作系统启动后,会运行 ExitBootServices 禁用 UEFI 启动服务,但是 UEFI 运行服务的代码会一直处于内存中,而且随时可以被系统调用执行。

efi_mode = EFI_RESET_WARM;
/* 或 efi_mode = EFI_RESET_COLD; */

/* reset_system 是 efi 结构体中的函数指针,其原型是:*/
/* void efi_reset_system_t (int reset_type, efi_status_t status, unsigned long data_size, efi_char16_t *data); */
/* 系统在通过 EFI 启动时,正确的内存地址会被初始化 */
efi.reset_system(efi_mode, EFI_SUCCESS, 0, NULL);

由于 EFI 的高度复杂性,可以直接阅读 EFI 相关文献。 在 Linux 上,可以使用内核参数 reboot=efi,让 Linux 内核使用这种方式实现重新启动的功能。

Linux 内核的逐一尝试

Linux 内核中,默认使用一个无限循环的 switch {} 语句逐一执行这些重启方法,如果成功了,那么机器就会重启,否则就会切换到下一种方法。如果硬件有已知问题,Linux 内核会查表,并根据机器型号选择合适的方式,并可能会进行一些 workaround。最后,有专门支持的特定计算机硬件会使用硬件自身的方法重启。

for (;;) {
    switch (reboot_type) {
    case BOOT_ACPI:
        // ...
        reboot_type = BOOT_KBD;
        break;

    case BOOT_KBD:
        /* 代码 */
        if (attempt == 0 && orig_reboot_type == BOOT_ACPI) {
            attempt = 1;
            reboot_type = BOOT_ACPI;
        } else {
            reboot_type = BOOT_EFI;
        }
        break;

    case BOOT_EFI:
        /* 代码 */
        reboot_type = BOOT_BIOS;
        break;

    case BOOT_BIOS:
        /* 代码 */
        reboot_type = BOOT_CF9_SAFE;
        break;

    case BOOT_CF9_FORCE:
        port_cf9_safe = true;
        /* Fall through */
    case BOOT_CF9_SAFE:
        /* 代码 */
        reboot_type = BOOT_TRIPLE;
        break;

    case BOOT_TRIPLE:
        /* 代码 */
        /* We're probably dead after this, but... */
        reboot_type = BOOT_KBD;
        break;
    }
}

为什么用键盘控制器能重启电脑?

随着个人计算机和半导体技术突飞猛进的发展,到了 1984 年,IBM 发表了具有划时代意义的 IBM PC/AT 计算机(即 IBM 5170)。PC/AT 搭载了强大的 80268 CPU,支持的内存也从 20 位的 1 MiB 变成了 24 位的 16 MiB。这一扩展地址可就惨了。在旧的 8086 上,尝试访问大于 1 MiB 的内存,会溢出回到内存的开头,后来有程序员也发现了这个现象,并利用它优化程序。但在 80286 上,由于大于 1 MiB 的内存确实存在,因此不会溢出也不会回到内存开头……本来 80286 默认使用的是实模式(而不是新的保护模式),就是为了完美兼容现有的 8086 程序的,但由于 Intel 并没有意识到这个问题,因此导致大量现有的程序停止工作。

IBM 的工程师为了解决这个兼容问题,他们在主板的内存地址总线的信号线上装了一个逻辑门,作为信号开关。当关闭时,就会一直产生信号 0,相当于关闭了超过 1 MiB 的内存,同时由于电路的特点,也能让这个溢出魔法重新恢复正常。但是,在哪里控制这个开关呢?PC AT 计算机使用 8042 芯片控制键盘,工程师看到芯片上正好有多余的端口,于是脑洞大开,用这个键盘芯片来控制 1 MiB 内存的开启。这就是著名的 A20。至今,让键盘控制器开启 A20 依然是进入保护模式不可或缺的一步。

同时,由于很多时候还需要在系统里兼容旧程序,有时还需要进入新的保护模式后,再切换回旧的实模式,但 80268 进行这个模式切换时,必须进行 CPU 重置。于是,IBM 的工程师把 8042 键盘控制器上另一个端口,连接到了CPU 的 Reset 信号线上……直到今天,x86 计算机上依然需要兼容 AT 机的 8042,因此,8042 键盘控制器能用来重启几乎一切 x86 电脑。

后来发现,键盘控制器的反应速度实在是太慢了,如果需要频繁 A20 切换会有性能问题,因为 IBM 当初的这个坑爹设计,日后系统开发者和 Intel 还花费了很大的代价解决。到了今天,x86 处理器里有一个专门模拟 A20 的 I/O 口……

如果说 8086 芯片是通往 x86 大坑的一把洛阳铲,那么这台 IBM 电脑则是造就了 x86 体系无数大坑的挖掘机。觉得 AT 很陌生?那在 AT 后面加个「X」呢?此外,你垃圾键盘右上角的 3 个刺眼的指示灯,Windows 用户不知道有什么用的 SysRq 按键,以及主板里一没电就掉时间日期,还时不时漏液损坏主板的纽扣电池,还有那经常出现诡异问题不能正常引导系统的 MBR,全都是 AT 的设计。更别说那个电源适配器负载一低就没有 +12V 输出,于是 IBM 在低端机器上安装了个 50 W 的功率电阻用来费电,幸好这一点上我们没有保持兼容……无论怎么说,这开启了一个时代,你现在的计算机很可能依然或多或少的兼容 AT。我们所说的「IBM 兼容机」指的就是 PC/AT 兼容。

GnuPG 2.1.18 不识别智能卡的解决方法

如果你通过 PC/SC 访问 OpenPGP Card,在升级到 GnuPG 2.1.18 后,你的智能卡将会停止工作。智能卡将无法被 gpg --card-status 正常识别,提示各类错误,例如:

gpg: selecting openpgp failed: No such device
gpg: OpenPGP card not available: No such device

在 GnuPG 中,本身就通过 ccid 支持了标准的 OpenPGP Card,一张标准的智能卡和兼容的读卡器可以直接和 GnuPG 一起工作,无需修改任何配置或安装任何程序。

但是,很多 OpenPGP Card 并非完全遵守 v2 标准,例如开发版本的 gnuk(遵守 v3 标准);或者 Yubikey(带有扩展特性),我们就需要通过 PC/SC 作为中间层访问智能卡。

这时,竟态条件就产生了。初次使用智能卡时,GnuPG 会和 PC-SC 抢占智能卡设备的控制权,过去 PC-SC 总是赢家,偶然,如果 GnuPG 赢了,智能卡就会无法正常工作。

因此,可以通过禁用 GnuPG 内建的智能卡支持,解决这个问题。

echo "disable-ccid" >> ~/.gnupg/scdaemon.conf

重启 pcscdgpg-agentscdaemon 后见效。如果你是 PC-SC 用户,建议总是禁用 ccid 避免冲突。

那么为何升级到 2.1.18 之后才会出现这个问题?日本 FSIJ 的 Niibe Yutaka,也就是 gnuk 的核心开发者主动跳出来表示,他把一个 bug 添加进了 scdaemon。以前,scdaemon 发现内建的 ccid 无法识别智能卡后,会顺便调用 PC/SC 再试试看,但重构代码时忘记保留这个特性,他已经编写了这个补丁修复了问题,而且已经被 Debian 采纳。

来源:https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=852702

最近的 OpenSSH 漏洞究竟有多大危害?

OpenSSH 刚刚更新到了 7.4 版,修复了已公开的 CVE-2016-10009, CVE-2016-10010, CVE-2016-10011, CVE-2016-10012 以及一个没有 CVE 的漏洞。其中,CVE-2016-10009 可以用来“任意执行代码”,因此,强烈建议大家立即更新所有的服务器,而且客户端也不能除外。但是,这些漏洞究竟有多大危害呢?是否可以利用这些漏洞控制服务器呢?我们一起来仔细看看:

CVE-2016-10009:sshd 服务器可以利用转发的 agent-socket 文件欺骗本机的 ssh-agent 加载一个恶意 PKCS#11 模块,任意执行代码。换句话说,是恶意服务器在客户端的机器上远程执行代码。

谁让你连恶意服务器的????

CVE-2016-10010:关闭 UsePrivilegeSeparation 权限隔离功能时,ssh 转发的 socket 文件将以 root 权限创建。

谁让你关闭权限隔离的????UsePrivilegeSeparation sandbox 是标准配置。

CVE-2016-10011:即使打开权限隔离,假如攻击者可以让 sshd 创建的子进程提权,那么服务器私钥理论上可能通过 realloc() 的过程泄漏给子进程。

泄漏私钥,听上去有点危险。前提是这攻击是理论上的,要使用,你得先有办法让子进程提权啊?????你有办法吗???再者,你们的 sshd 都应该开前向安全了吧……

CVE-2016-10012:sshd 服务器如果启用了认证前压缩(也就是 Compression yes),其中的某些边界检查可能会被编译优化去除,导致攻击者有可能从经过权限隔离的子进程中攻击高权限的父进程。

谁让你打开认证前压缩的?你当 CRIME 攻击是闹着玩的啊????CRIME 攻击之后 sshd 的默认配置都是 Compression delayed,也就是认证后压缩,如果你这都不知道那你的服务器就已经不安全了。再者,从子进程中怎么攻击父进程?目前还没有已知的方法。

无名漏洞AllowUsersDenyUsers 选项可以接受无效的 IP 段。假如管理员想拉黑用户,但却输入了无效的 IP 段,那么用户就不会被禁止登录,而管理员也不知道。

谁让你把 IP 段输错了?????再者,你拉黑恶意 IP 不使用 iptables 防火墙吗???


可见,这些漏洞确实不容忽视,强烈建议所有人立刻更新。然而,如果你想搞个大新闻说利用这些漏洞可以控制服务器,那还是图样图森破了。如果你平时在运维服务器的时候已经遵循了最佳安全实践,那么其实际危害是很低的。

新服务器密钥指纹

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

* RSA
    * SHA256 | DSsHk7JuE8iNPV9yVESiRptOPv4eepQKpWxaDoOk9m4
    * MD5 | 92:a7:2f:a8:07:76:3f:f7:03:f2:54:e9:06:ea:19:9e

* Ed25519
    * SHA256 | MTJ6TdQezosVmHLR258Fp967mSZlfK96V4oQprP96qw
    * MD5 | 6f:b2:9b:0d:05:51:12:25:23:02:ea:74:33:3d:28:d5
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEJVIRsjlaWj4OSKDx+tPrBeiOjW0FAlhLgf8ACgkQ+tPrBeiO
jW2raw/9HKZ2c9tjs/u001P1TL7+sLriK12SzBkXR7jfTCRjXAR4Qxh9QnU5hZ8/
tnXH5qmKQxQXTrZqZtkbsNNQbwLBT5c0N7ZfYoJAh1occveAOaE9z6W7CT/8y1H5
1KiYXKhWs0ShbFs+VT9yasvgwGInTjERvXUk9JBa5DCjz1tFBDAzE51h5fQD8CA2
5USJHMbx4wDVAskZ5uSeXWV+xMY1U9JgDbZeSefFJLV+rzd1tt9rCWrgz7lQRDmT
ufQ8hMOX3Dkp6Zllyi1OVrxbep8zMSZgRPaIhcdnIG9A+9dBRQ5QJV6uBzMocXlP
FlSjqtlGqjCdcdqQg7ka9fMBhox/hE34lKeNdubT9TVv7jWy8EmUxUhIKd+72wUC
9sdt6g/yLeboPa88YwKH9LvmOlsJS69P4m40i4rJprZSaERyNTkGP1GoFl167v1k
nDIrwkmI6Os53H1rt7e3jYbgg/cJxOF7+734T+RbVsHO+t/GvgoD9OCP3avpzbF/
SNl4rGBlNbbqN7Trw4a8w+SRl3FLiH6HSt64xrNqhttb/wJHO0SmR/aaXqYVa3Og
VKCaXgIuOqaOw3aDuoSt4Qh//BDRInC/89QLEbHBxjvFxnGF+BVIg3Kij3lVIlXi
cJmkh+a4j94KfTvV3ZQkM61jTkeASAUc2UP10cZTPABinUT1fdk=
=0t8H
-----END PGP SIGNATURE-----

使用 cryptsetup 加密 swap

Swap 分区不利于系统安全。当 RAM 中的数据被写入到硬盘等永久存储设备时,就会面临更高的数据泄漏风险。如果内存中的密钥恰好被写入到了 Swap 中,就大大降低了攻击者的难度,只需要简单得进行数据恢复,无论是什么加密算法都不在话下。不过,随着现在的计算机内存容量越来越来,Swap 的使用已经没有那么广泛了。

不过,很多情况下 Swap 依然是非常有用的,这时候我们可以使用 cryptsetup 对 Swap 加密一下消除安全隐患,整个过程非常简单,也不需要记忆任何的密钥。作为副作用,这意味着系统无法在关机后解密 Swap,因此无法休眠了,此外,写入 Swap 时的 CPU 占用率也会有所上升,但如果你的 CPU 支持 AES 指令集,影响会十分有限。

思路非常简单,首先,我们使用 cryptsetup 使用一个随机密钥打开 Swap 分区或文件,然后,再将 cryptsetup 在 /dev/mapper/ 中创建的抽象设备作为 Swap 使用。由于密钥是随机的,因此每次抽象设备中的数据也都是完全随机的,我们在使用之前还需要重新 mkswap 创建 Swap。

我们假设已经有一个 Swap 文件位于 /swap,因此抽象设备将是 /dev/mapper/swap。如果你使用其他文件或分区自然没有问题,记得手工执行 cryptsetup 并观察一下 /dev/mapper 里的设备叫什么名字。如果你想自己创建 /swap,别忘了在创建后使用 chmod 600 /swap 保护原始文件。

cryptsetup open --type plain /swap swap --key-file /dev/urandom 
chmod 600 /dev/mapper/swap 
mkswap /dev/mapper/swap
swapon /dev/mapper/swap

创建此脚本,赋予可执行权限,并执行此脚本,Swap 立刻可以使用了。由于这是一个一次性 Swap,我们并不需要关闭它,但如果测试需要,可以这样:

swapoff /dev/mapper/swap
cryptsetup close /dev/mapper/swap

为了方便,我使用了一个 Systemd 的 Unit 文件来做到自动挂载。

[Unit]
Requires=swap.target

[Service]
ExecStart=/usr/local/sbin/encrypted-swap
Type=oneshot

[Install]
WantedBy=multi-user.target

其中 Requires=swap.target 代表此脚本在 Systemd 执行内建的 swap 服务后执行,避免可能的竞态条件;/usr/local/sbin/encrypted-swap 代表脚本的位置;Type=oneshot 代表此服务只运行一次,然后立刻退出。首先关闭现有的 Swap,把此文件命名为 encrypted-swap.service,并 systemd start encrypted-swap 看看是否正常,若正常便可 systemctl enable encrypted-swap 开机自启动了。如果你有现有的自动挂载 Swap 配置,如 /etc/fstab,你需要手工取消,避免冲突。

此外,cryptsetup 加密 plain 文件的默认算法是 aes-cbc-essiv:sha256,没有安全问题。你可以使用 cryptsetup --help 查看默认的 plain 类型加密算法。

Linode Xen 下 grsecurity >= 4.3 崩溃的解决

更新:官方已在 grsecurity-3.1-4.7.4-201609152234.patch 中修复问题,不再需要此 workaround。

自从 Linux 4.3 开始,在 Linode 上使用 PaX/grsecurity 时,内核会在被 pv-grub 执行后不久立即崩溃。由于崩溃是在启动后极早期立刻发生的,没有任何可以用来调试的日志,同时公司也不是盖子开的,也没有办法得到母机上有意义的调试信息。这导致了盖子的 VPS 内核从去年 12 月开始被锁定在 4.2.7。由于不知什么时候产生了 Linode 东京机房会在 2016 年 6 月从 Xen 迁移到 KVM 的错觉,也没有花精力去尝试调试这个问题。

Continue reading

« Older posts

Copyright © 2023 比尔盖子 博客

Theme by Anders NorenUp ↑