本文翻译自SCP - Familiar, Simple, Insecure, and Slow

标题里面把 SCP 的特点做了全面的概况,即常用,简单,但是不安全且速度慢。这篇文章分析了 SCP 的原理和特点。


1 What is SCP?

Secure Copy Protocol (SCP) 这个工具可以让我们在两台计算机之间移动文件,使用起来非常简单:

1
scp local_file.txt remote_host:/home

上面这个命令可以让我们把本地的 local_file.txt 文件复制到服务器的 /home 文件夹下面。这个命令的完美之处在于它支持几乎所有 的 Unix 类操作系统或者 Windows 的相关客户端上。这种普适性使得 SCP 大为流行。

不过文件复制的部分其实只是水面上的皮毛部分,SCP 这类文件传输应用的核心是两台计算机之间的相互认证以及输出阐述的加密(不然也不会叫 Secure 了)。使用 SCP 的过程中你会发现程序需要首先建立起到服务器的 SSH 链接 - 这是因为 SCP 是运行与 SSH 之上的。SCP 会使用 SSH 管道来传递明文数据。这意味着其实 SCP 将所有的安全性能保障都寄托到了 SSH 之上,而 SCP 自己只是做了简单的文件明文传输。

维基词条上梳理了 SCP 的历史。简而言之,最早在 BSD 系统上有一个工具名为 RCP,可以在两台计算机之间移动文件。这个命令诞生时,计算机系统还处于早期的所有计算机之间都可以信任,不需要考虑安全性问题的婴儿时代。后来人们认识到并非网络中的每个人都可以信任,所有有人就将 RCP 实现到 OpenSSH 之上,通过 SSH 会话来传递数据。至此问题解决,这样产生的 SCP 命令以后就都是构筑在 OpenSSH 之上了。

2 How does it work?

SSH RFC 的介绍非常直接、完整。不过这个文档并没有谈论通过 SSH 传递文件。那么 SCP 是如何实现的呢?

SCP 并非是一个标准化的协议,也并不存在一个 RFC 文档或者其他官方文档来描述其实现细节。OpenSSH 实现就是其正统的实现方法。在这个实现方法中有两个部分,分别是建立连接和线路协议(wire protocol)。

2.1 "Connection" establishment

实际上 SCP 里的这个链接建立过程并不是一个实际意义上的链接,而只是一个执行与 SSH 之上的命令的 STDIN/STDOUT,有点像一个 Unix pipe。OpenSSH 中有两个程序涉及这部分内容:sshdscpssh 为服务器端的 daemon 程序,总是在后台运行。这个程序可以接受新的 SSH 连接请求。scp 则是假装是 ssh 客户端,稍后负责传送文件。

scp 程序运行时,程序首先打开一个新的 SSH 链接。在连接建立以后,程序在服务器端执行另一个 scp 程序,这个远端的 scp 程序会使用一些特殊的,文档没有说明的 flags。你可以假定这些 flags 是 -t ("to",指目标),-f ("from",指来源),用来代表发送和接受。其他的 flag 还包括 d (目标是一个目录),-r (递归遍历目录)。

注意 scp 协议是单向的:只能是一端发送另一端接收。

当远端的 scp 程序运行起来之后,两边的 scp 命令就可以通过 STDIN/STDOUT 来交换数据。

2.2 Wire protocol

现在我们建立了安全的 I/O 管道,那我们就可以切换到 RCP 协议了。这个协议是串行(同时只能有一个操作)的同步(一个操作结束以后才能进行下一个操作)协议。协议内容的格式基本是 [command type][arguments]\n[optional data]

  • [command type] 总是一个 ASCII 字符:
    • C: 写一个文件
    • D: 进入一个目录
    • E: 推出上一个目录
    • T: 设置下一个文件或者目录的时间戳信息(创建/更新)
  • [arguments] 参数,可以是文件名、文件大小或者时间戳,E命令没有参数
  • [optional data] 当上一个命令是C时传输文件的内容,文件的大小有参数指定

除此之外还有其他的控制字节,这些控制字节单独发送,不创建新行(without new lines)

  • 0x00: 用于 ACK 信息。初始时接收方也会发送这个消息告知发送方已经就绪。
  • 0x01: 警告信息,后面跟一个新行(以新行作为结束符)作为展示给用户的警告信息。
  • 0x02: 错误信息,格式和警告信息类似。不过错误信息发送之后连接会中断了。

下图展示了协议交换数据过程:

3 SCP 协议的问题

3.1 性能

线路协议的串行特性以及每个信息的强制 ACK 要求产生了比较多的开销。如果有一个 ACK 丢失,那么整个连接会暂停,并对丢失的内容进行重传。另外,数据的传输也没有经过压缩,也为询问接收端是否已经接受到了完整的文件。

有经验的系统管理员会告诉你用tar命令来打包文件再传输,其效率会远高于使用scp来遍历地传递一个文件夹。实际上,你可以不用scp命令就能完成打包传输的过程(不过不用 scp 命令会复杂很多)。如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Copy a local folder with 10000 files
$ find /tmp/big_folder/ -type f | wc -l
10000

# Using scp
$ time scp -r -q /tmp/big_folder/ server:/tmp/big_folder

________________________________________________________
Executed in 882.99 millis fish external
usr time 114.09 millis 0.00 micros 114.09 millis
sys time 278.46 millis 949.00 micros 277.51 millis

# Using tar over ssh
$ time sh -c "tar cf - /tmp/big_folder | ssh server 'tar xC /tmp/ -f -'"
tar: Removing leading '/' from member names

________________________________________________________
Executed in 215.68 millis fish external
usr time 93.22 millis 0.00 micros 93.22 millis
sys time 66.51 millis 897.00 micros 65.62 millis

使用 scp 消耗了 882.99ms,而 tar 打包之后只需要 215.68ms。

3.2 安全性

虽然 scp 底层使用了 ssh 隧道,不过根据 OpenSSH 8.0 的 Release 文档

The scp protocol is outdated, inflexible and not readily fixed. We recommend the use of more modern protocols like sftp and rsync for file transfer instead.

SCP 协议是过时的、不灵活的,未修复的。我们推荐使用更加现代的传输协议,如sftp、rsync。

如果远端的shell打印出了任何针对非交互式会话的内容,那么这些内容也会被你的本地scp进行理解为远端发送的 SCP 内容(使用 STDIN/STDOUT的副作用)。一般来说这些内容会产生明显的格式性错误,导致 scp 失败,但是如果存在恶意侵入者,这个漏洞可以使得侵入者修改传输的文件的内容,带来严重的后果。

除了这些内在问题,实现上的 bug 也有重大的隐患。例如在 2018 年,Harry Sintonen 发现了 SCP 的流行实现中(包括 OpenSSH) 存在的诸多脆弱问题。这些问题导致的后果可能是:修改文件夹权限使得攻击者可以篡改任意的文件内容;注入系统命令且不留下任何踪迹。