Obfuscated SSH 简要分析笔记

SSH翻墙的日子,和移动那5元30MB的流量包一样,已经一去不返了

现在跨境SSH流量倒是连得上,但是流量一大,分分钟就可以ban你IP(这就苦了用SFTP拉文件和Rsync同步的人了)。不过早期(Shadowsocks出现之前)的人们也没有坐以待毙,搞出了不少给SSH续命的方法,Obfuscated SSH(简称OSSH)就是其中的一种

前言

开始之前必须要提的一件事是:这玩意,据我所知,没有长期统一的标准,笔者也只能挑其中一个版本来学习记录:

这玩意算是一个打在OpenSSH上面的补丁,其GitHub页面在这里,有兴趣的读者可以到此阅读

配置

OSSH需要双端配置,也就是服务端和客户端都要是特制版OpenSSH。其服务端配置文件里,多了如下的配置项目

ObfuscatedPort 2222 #OSSH端口
ObfuscateKeyword some_text_as_password #【可选的】混淆密码

客户端也多了两个配置参数:

-z  #启用混淆
-Z some_text_as_password #【可选的】混淆密码

比较有趣的一点在于,这玩意的混淆密码是可选的,即使不填密码,后续的密钥派生步骤中也会为每一个SSH会话派生完全不同的密钥。但是要注意是是,不论加不加这个密码,OSSH的混淆层都不应该视为和用于完整的信息加密和身份验证,包括作者也明确表明这一点,混淆密码仅仅用于不让阿猫阿狗都能连上来混淆层下面的标准SSH服务器,仅此而已

至于为什么要这么做....应该说,这个混淆层一开始就没有也不必考虑可移植性,他不是Tor项目那些Plugable Transport插件,也就代表着混淆层下面就是SSH——一个『如果设计的没错的话,是在公网上运行』的协议,混淆层自然不用操心太多。而且,这个混淆层仅仅保护了SSH握手信息,让外界无法从握手信息判定这个是SSH连接而已,握手完毕之后,密钥已经协商好,就没有混淆层什么事了,直接跑裸SSH就行

但是要注意,如果没有设定混淆密码,就意味着监听者可以通过监听握手过程的方式,重建密钥,从而解密混淆前的内容(一般就是,SSH握手头),原作者对此的解决方案是让计算密钥的过程尽可能消耗算力,阻挡监听者大规模解密混淆层——但是嘛,15年过去了,SHA1早已被证明不安全,代码中的实现也只迭代了6000次。因为摩尔定律不是吹的,现在想要计算6000次SHA1真的是易如反掌,所以还是那句话,混淆层仅仅是混淆,千万不要用于保护通信机密性

密钥派生

会话建立前,混淆插件需要先协商一个混淆密钥,注意此处的混淆密钥并不是上面设定的混淆密码

为了便于理解(其实是作者偷懒.....)下面的伪代码采用比较像Golang的语法,但别忘了,原文件是用C写的,别傻乎乎抄过去就用了

首先,下面用到了几个常量:

const OBFUSCATE_KEY_LENGTH      = 16
const OBFUSCATE_SEED_LENGTH    = 16
const OBFUSCATE_HASH_ITERATIONS = 6000
const OBFUSCATE_MAX_PADDING    = 8192
const OBFUSCATE_MAGIC_VALUE    = 0x0BF5CA7E

第一步,需要一个种子(Seed),OSSH采用的是16字节的种子长度

seed := genSalt(OBFUSCATE_SEED_LENGTH)

第二步,计算种子和『方向常量』的SHA1值,如果用户指定了混淆密码,就计算种子和密码和方向常量的SHA1(这也就是为什么,混淆密码是可选的——因为它并不实际作用于加密)

方向常量,其作用是通过改变值,来计算一对密钥(客户端到服务器一个,反方向一个),所以两个方向上的方向常量取值分别问client_to_serverserver_to_clint

算完之后,为了『耗破解者的算力』(作者原话)起见,需要再迭代计算OBFUSCATE_HASH_ITERATIONS次SHA1,在公开实现中是6000次

h := sha1(seed + constant)
# 或者说是
h := sha1(seed + ObfuscateKeyword + constant)

# 迭代计算
for i := 0;i < OBFUSCATE_HASH_ITERATIONS; i++ {
    h = sha1(h)
}

迭代完成之后,h里面就是最终的通信密钥了

混淆层握手

算完密钥,并建立了TCP连接之后,首先需要对混淆层进行握手

第一步,客户端发送如下数据:

128(seed)+32(magic value)+32(Padding Length)+v(Padding)
info:一些表示法
笔者习惯用的一种表示法如上图所示,其格式为:

长度(内容)+长度(内容)+.....

长度单位为Bit,如果长度为v,则表示这是个可变长字段,一般来说是上一个字段确定这个字段的长度

Padding长度必须小于OBFUSCATE_MAX_PADDING,内容随机生成,因为并不重要

发送时,16字节seed是明文的,后续内容将会通过RC4加密(用『客户端到服务器』密钥进行加密),拼接到seed后面,一起发出去

第二步,服务器收到数据以后,用和客户端一样的方法计算两个密钥,并解密后续内容(因为是流加密,可以随到随解密)。在这一步里面,重点检查以下项目:

  • Magic Value是否正确
  • 填充长度是否正确(是否超限,是否对不上)

有读者可能察觉到了:如果使用了密码,服务器必然要校验客户端使用的混淆密码是否正确。问题是,这里用的是流加密,要怎么校验混淆密码是否正确呢?

其实,很简单,如果密码不一致,服务器虽然也能计算出来密钥,但是对流解密结果必然是不对的,这就让Magic Value校验过不去了。如果你真的那么欧皇,那么填充长度也会让你露出马脚,反正就是卡住你不让你继续下去

如果校验不通过,服务器会让这条连接进入『read forever』状态,也就是不论后续再发不发内容,服务器都不做出任何响应,等待自然超时(这里顺带吐槽一下,早期的Shadowsocks就是在这里栽了坑,校验失败立刻断开连接(发送RST),这就给主动探测留下了可趁之机)

SSH握手加密

混淆层握手完毕,双方已经协商出来一对混淆密钥,用这个密钥配合RC4算法,对SSH握手层加密就是了

而且因为这个混淆是做在OpenSSH上的,因此SSH握手完了,混淆层可以直接撤(至少理论上如此),接下来就是SSH的事情了

写在最后

个人笔记,水了一篇

但是再强调一遍:如果你想自己设计一个类似的算法,记住,RC4(包括其他流加密算法)和SHA1,在今天都已经算是安全性较弱了,非常不推荐使用,建议至少是AES-128-GCM + Sha256,有条件可以一步到位,Chacha20Poly1305/AES-256-GCM + BLAKE3

(完)

none
最后修改于:2024年11月05日 15:20

添加新评论