背景
2022國家級護網行動即將開啟,根據阿里雲給出的安全建議,需要將登陸Linux的方式改為金鑰對方式。我這裡使用的遠端工具是自己開發的,能夠同時管理Windows和Linux,但是以前不支援金鑰對的登陸方式,所以需要改造一下。
護網行動是什麼?護網行動從2016年開始,是一場由公安部組織的網路安全攻防演練,目的是針對全國範圍的真實網路目標為物件的實戰攻防活動,旨在發現、暴露和解決安全問題,檢驗我國各大企事業單位、部屬機關的網路安全防護水平和應急處置能力。護網行動每年舉辦一次,為期2-3周。
我使用的遠端工具 RDManager:https://blog.bossma.cn/tools/new-version-of-rdmanager-replace-poderosa-with-putty/,這個工具訪問Linux使用了putty,putty的金鑰對登陸方式使用的是自有格式的ppk檔案,但是阿里雲上下載的是pem格式的金鑰檔案,所以需要將pem格式轉換為ppk格式。
思路
putty本身提供了一個工具,可以將其他格式的金鑰檔案轉換為自有的ppk檔案,這個工具的名字是puttygen。在linux上可以通過命令進行轉換,在Windows上則必須使用GUI工具手動操作,這多有不便。我期望的是能通過程式設計的方式進行這個轉換,這樣只需要在RDManger中上傳pem檔案,就可以自動轉換為putty的ppk格式的檔案,不需要再去使用puttygen。
首先查了下有沒有現成的輪子,經過多次尋找,在Github上找到了一個專案:pem2ppk (https://github.com/akira345/pem2ppk),這個專案看名字就知道很貼合我的需求,它的主要功能就是讀取pem檔案,然後輸出為ppk檔案。我最終的解決方案主體也是從此而來。不過這個程式有兩個問題:
- 1、不是所有的pem檔案都能轉換成功,網上也是有人說成功了,有人說不行。
- 2、不支援對金鑰進行加密,別人拿走了這個ppk檔案就可以直接使用。puttygen是有這個功能的。
除此之外,很難再找到比較貼合需求的資料了。怎麼辦?其實這個Github專案的很大一部分程式碼來源於另一篇文章:https://antonymale.co.uk/generating-putty-key-files.html,作者提到可以去看putty的原始碼。
受此啟發,我也可以去看putty的原始碼,然後將相關處理翻譯為C#的實現,這樣應該是可以解決問題的。
實現
putty的原始碼官網上就可以下載到,不過我看的是一個幾年前的版本:https://github.com/KasperDeng/putty,這個版本和新版本的主要邏輯都是一樣的,搞懂C語言的若干函式和資料型別就很容易理解,而且舊版本更原始,沒有那麼多的抽象,反而更容易理解。
輸出ppk內容不正確的問題
這個問題主要是由於填充(padding)使用不當造成的,pem2ppk專案在輸出金鑰的各個屬性時都使用了前置填充,而putty並不是固定的都加了填充。
看putty的程式碼實現:https://github.com/KasperDeng/putty/blob/037a4ccb6e731fafc4cc77c0d16f80552fd69dce/putty-src/sshrsa.c#L654
dlen = (bignum_bitcount(rsa->private_exponent) + 8) / 8;
plen = (bignum_bitcount(rsa->p) + 8) / 8;
qlen = (bignum_bitcount(rsa->q) + 8) / 8;
ulen = (bignum_bitcount(rsa->iqmp) + 8) / 8;
bloblen = 16 + dlen + plen + qlen + ulen;
這段程式碼是計算金鑰的各個屬性的值的位元組數,然後用於初始化一個大的位元組陣列,將這些資料寫進去。bignum_bitcount是計算值的位元位數,除以8就是得到位元組數,為什麼還要加8呢?這是因為C語言中除法的結果是向下取整的,比如數學計算結果是1.5,那麼C語言中得到的就是1,為了不讓任何一個位元丟失,所以這裡加了一個8,預留好充足的空間。
再來看pem2ppk中的實現:https://github.com/akira345/pem2ppk/blob/d2baee08064953280984607d1e4ae1183127e5ad/PEM2PPK/PuttyKeyFileGenerator.cs#L24
private const int prefixSize = 4;
private const int paddedPrefixSize = prefixSize + 1;
byte[] publicBuffer = new byte[3 + keyType.Length + paddedPrefixSize + keyParameters.Exponent.Length +
paddedPrefixSize + keyParameters.Modulus.Length + 1];
這裡keyParameters.Exponent和keyParameters.Modulus是公鑰的兩個屬性,可以看到前邊加了一個固定的長度paddedPrefixSize,這個paddedPrefixSize=prefixSize + 1,這裡邊的1就對應putty中的+8邏輯。
不過固定+1是有問題的,可以想一下C#和C語言在處理這些屬性值時的差別。
在putty中如果資料位元數不能被8整除,那麼+8之後再整除就可以得到正確的位元組數,否則就會少1個位元組;如果資料能被8整除,那麼+8就會多1個空的位元組,這個多的位元組就是padding了。所以能被8整除的時候才會有這個padding。
在C#中開始處理的時候就已經都是位元組了,所以C#中不需要處理位數不能被8整除的問題,但是需要在能被8整除的時候增加一個空位元組,C#中如何判斷資料的位數能被8整除呢?可以認為資料首byte的最高位是1的時候,位元數就能被8整數,此時最小二進位制數是10000000,比它小的數就可以被捨棄掉至少1位。10000000也就是128,因此凡是大於等於這個數的都是能被8整數的,也就是需要padding的。
所以可以這樣判斷是否需要增加padding:https://gist.github.com/bosima/ee6630d30b533c7d7b2743a849e9b9d0#file-puttykeyfilegenerator-cs-L190
private static bool CheckIsNeddPadding(byte[] bytes)
{
return bytes[0] >= 128;
}
private static int GetPrefixSize(byte[] bytes)
{
return CheckIsNeddPadding(bytes) ? paddedPrefixSize : prefixSize;
}
實現ppk加密
pem2ppk專案中沒有對key進行加密的實現,網上也沒有找到C#的原始碼可以實現這個功能。但是這個功能很關鍵,在RDManager中所有的密碼都是加密處理的,這樣伺服器賬號落盤的時候安全性才能有比較好的保障,但是阿里雲匯出的pem是沒有加密的,雖然puttygen也可以給pem加密,但是還不是不能將加密以程式設計的方式整合到RDManager中。
解決這個問題的方式還是搬運putty的實現方式,將C語言的實現轉換為C#的實現。其中有兩個關鍵的處理:一是要在計算Private-MAC的值時給私鑰增加padding,二是使用AES256進行加密處理。至於putty為什麼要這樣處理,我沒有研究,只是照搬過來。
主要看下AES256加密的處理,有些引數很關鍵:
byte[] passKey = new byte[40];
...
byte[] iv = new byte[16];
byte[] aesKey = new byte[32];
Buffer.BlockCopy(passKey, 0, aesKey, 0, 32);
using (RijndaelManaged rijalg = new RijndaelManaged())
{
rijalg.BlockSize = 128;
rijalg.KeySize = 256;
rijalg.Padding = PaddingMode.None;
rijalg.Mode = CipherMode.CBC;
rijalg.Key = aesKey;
rijalg.IV = iv;
ICryptoTransform encryptor = rijalg.CreateEncryptor(rijalg.Key, rijalg.IV);
return encryptor.TransformFinalBlock(bytes, 0, bytes.Length);
}
- iv是長度為16的位元組陣列,裡邊都是預設值0。
- aeskey是一個長度為32的位元組陣列,不過計算的時候準備的是長度為40的位元組陣列,需要截一下。
- Padding需要設定為PaddingMode.None,預設的不是這個。
其它就沒什麼好說的了。
為了不那麼單調,來一張RDManger的使用介面:
以上就是本文的主要內容了。
完整程式碼在Github,歡迎訪問:https://gist.github.com/bosima/ee6630d30b533c7d7b2743a849e9b9d0