深入淺出SagaECO RPC篇(四) “不滅之握”
以下討論基於SagaECO svn 900版本
注意: 被分發SagaECO源代碼並不代表你被授權使用、修改、發布衍生的程式
源代碼可以從這裡下載
換言之SagaECO團隊仍然保留一切權利(雖然SagaECO團隊早就消失了…
這篇主要覆蓋SagaECO中的NetIO和Encryption類,比較沉悶,是失眠的良藥。
// SagaECO/SagaLib/NetIO.cs | |
namespace SagaLib | |
{ | |
public class NetIO | |
{ | |
public enum Mode | |
{ | |
Server, | |
Client | |
} | |
private byte[] buffer = new byte[4]; | |
private AsyncCallback callbackSize; | |
private AsyncCallback callbackData; | |
private AsyncCallback callbackKeyExchange; | |
public Socket sock; | |
public Encryption Crypt; | |
private NetworkStream stream; | |
private Client client; | |
private ushort firstLevelLenth = 4; | |
private bool isDisconnected; | |
private bool disconnecting; | |
private int lastSize; | |
private int alreadyReceived; | |
private ClientManager currentClientManager; | |
public ushort FirstLevelLength | |
{ | |
get | |
{ | |
return firstLevelLenth; | |
} | |
set | |
{ | |
firstLevelLenth = value; | |
} | |
} | |
/// <summary> | |
/// Create a new netIO class using a given socket. | |
/// </summary> | |
/// <param name="sock">The socket for this netIO class.</param> | |
public NetIO(Socket sock, Dictionary<ushort, Packet> commandTable, Client client ,ClientManager manager) | |
{ | |
this.sock = sock; | |
this.stream = new NetworkStream(sock); | |
this.commandTable = commandTable; | |
this.client = client; | |
this.currentClientManager = manager; | |
Crypt = new Encryption(); | |
this.callbackSize = new AsyncCallback(this.ReceiveSize); | |
this.callbackData = new AsyncCallback(this.ReceiveData); | |
this.callbackKeyExchange= new AsyncCallback(this.ReceiveKeyExchange); | |
this.isDisconnected = false; | |
} | |
private void StartPacketParsing() | |
{ | |
if (sock.Connected) | |
{ | |
try { stream.BeginRead(buffer, 0, 4, this.callbackSize, null); } | |
// 省略catch部分 | |
} | |
else { this.Disconnect(); return; } | |
} | |
public void SetMode(Mode mode) | |
{ | |
byte[] data; | |
switch (mode) | |
{ | |
case Mode.Server : | |
try | |
{ | |
data = new byte[8]; | |
stream.BeginRead(data, 0, 8, this.callbackKeyExchange, data); | |
} | |
// 省略catch部分 | |
break; | |
case Mode.Client : | |
try | |
{ | |
data = new byte[529]; | |
stream.BeginRead(data, 0, 529, this.callbackKeyExchange, data); | |
} | |
// 省略catch部分 | |
break; | |
} | |
} | |
private void ReceiveKeyExchange(IAsyncResult ar) | |
{ | |
try | |
{ | |
if (this.isDisconnected) | |
{ | |
return; | |
} | |
if (!sock.Connected) | |
{ | |
ClientManager.EnterCriticalArea(); | |
this.Disconnect(); | |
ClientManager.LeaveCriticalArea(); | |
return; | |
} | |
try { stream.EndRead(ar); } | |
// 省略catch部分 | |
byte[] raw = (byte[])ar.AsyncState; | |
if (raw.Length == 8) | |
{ | |
Packet p1 = new Packet(529); | |
p1.PutUInt(1, 4); | |
p1.PutByte(0x32, 8); | |
p1.PutUInt(0x100, 9); | |
Crypt.MakePrivateKey(); | |
string bufstring = Conversions.bytes2HexString(Encryption.Module.getBytes()); | |
p1.PutBytes(System.Text.Encoding.ASCII.GetBytes(bufstring.ToLower()), 13); | |
p1.PutUInt(0x100, 269); | |
bufstring = Conversions.bytes2HexString(Crypt.GetKeyExchangeBytes()); | |
p1.PutBytes(System.Text.Encoding.ASCII.GetBytes(bufstring), 273); | |
SendPacket(p1, true, true); | |
try | |
{ | |
byte[] data = new byte[260]; | |
stream.BeginRead(data, 0, 260, this.callbackKeyExchange, data); | |
} | |
// 省略catch部分 | |
} | |
else if (raw.Length == 260) | |
{ | |
Packet p1 = new Packet(); | |
p1.data = raw; | |
byte[] keyBuf = p1.GetBytes(256, 4); | |
Crypt.MakeAESKey(System.Text.Encoding.ASCII.GetString(keyBuf)); | |
StartPacketParsing(); | |
} | |
else if (raw.Length == 529) | |
{ | |
Packet p1 = new Packet(); | |
p1.data = raw; | |
byte[] keyBuf = p1.GetBytes(256, 273); | |
Crypt.MakePrivateKey(); | |
Packet p2 = new Packet(260); | |
p2.PutUInt(0x100, 0); | |
string bufstring = Conversions.bytes2HexString(Crypt.GetKeyExchangeBytes()); | |
p2.PutBytes(System.Text.Encoding.ASCII.GetBytes(bufstring), 4); | |
SendPacket(p2, true, true); | |
Crypt.MakeAESKey(System.Text.Encoding.ASCII.GetString(keyBuf)); | |
StartPacketParsing(); | |
} | |
} | |
// 省略catch部分 | |
} | |
/// <summary> | |
/// Disconnect the client | |
/// </summary> | |
public void Disconnect() | |
{ | |
try | |
{ | |
if (this.isDisconnected) | |
return; | |
try | |
{ | |
if (!disconnecting) | |
this.client.OnDisconnect(); | |
disconnecting = true; | |
} | |
try { stream.Close(); } | |
try { sock.Close(); } | |
this.isDisconnected = true; | |
} | |
// 省略catch部分 | |
//this.nlock.ReleaseWriterLock(); | |
} | |
private void ReceiveSize(IAsyncResult ar) | |
{ | |
try | |
{ | |
if (this.isDisconnected) | |
{ | |
return; | |
} | |
if (buffer[0] == 0xFF && buffer[1] == 0xFF & buffer[2] == 0xFF && buffer[3] == 0xFF) | |
{ | |
// if the buffer is marked as "empty", there was an error during reading | |
// normally happens if the client disconnects | |
// note: this is required as sock.Connected still can be true, even the client | |
// is already disconnected | |
ClientManager.EnterCriticalArea(); | |
this.Disconnect(); | |
ClientManager.LeaveCriticalArea(); | |
return; | |
} | |
if (!sock.Connected) | |
{ | |
ClientManager.EnterCriticalArea(); | |
this.Disconnect(); | |
ClientManager.LeaveCriticalArea(); | |
return; | |
} | |
try { stream.EndRead(ar); } | |
Array.Reverse(buffer); | |
uint size = BitConverter.ToUInt32(buffer, 0) + 4; | |
if (size < 4) | |
{ | |
Logger.ShowWarning(sock.RemoteEndPoint.ToString() + " error: packet size is < 4",null); | |
return; | |
} | |
byte[] data = new byte[size + 4]; | |
// mark buffer as "empty" | |
buffer[0] = 0xFF; | |
buffer[1] = 0xFF; | |
buffer[2] = 0xFF; | |
buffer[3] = 0xFF; | |
lastSize = (int)size; | |
if (sock.Available < lastSize) size = (uint)sock.Available; | |
if (size > 1024) | |
{ | |
size = 1024; | |
alreadyReceived = 1024; | |
} | |
else | |
{ | |
alreadyReceived = (int)size; | |
} | |
// Receive the data from the packet and call the receivedata function | |
// The packet is stored in AsyncState | |
try | |
{ | |
stream.BeginRead(data, 4, (int)(size), this.callbackData, data); | |
} | |
// 省略catch部分 | |
} | |
} | |
private void ReceiveData(IAsyncResult ar) | |
{ | |
try | |
{ | |
if (this.isDisconnected) | |
{ | |
return; | |
} | |
if (!sock.Connected) | |
{ | |
ClientManager.EnterCriticalArea(); | |
this.Disconnect(); | |
ClientManager.LeaveCriticalArea(); | |
return; | |
} | |
try { stream.EndRead(ar); } | |
byte[] raw = (byte[])ar.AsyncState; | |
if (alreadyReceived < lastSize) | |
{ | |
int left = lastSize - alreadyReceived; | |
if (left > 1024) | |
left = 1024; | |
if (left > sock.Available) left = sock.Available; | |
try | |
{ | |
stream.BeginRead(raw, 4 + alreadyReceived, left, this.callbackData, raw); | |
} | |
alreadyReceived += left; | |
return; | |
} | |
raw = Crypt.Decrypt(raw, 8); | |
Packet p = new Packet(); | |
p.data = raw; | |
uint length = p.GetUInt(4); | |
uint offset = 0; | |
while (offset < length) | |
{ | |
uint size; | |
if(firstLevelLenth ==4) | |
size= p.GetUInt((ushort)(8 + offset)); | |
else | |
size = p.GetUShort((ushort)(8 + offset)); | |
offset += firstLevelLenth; | |
if (size + offset > length) | |
break; | |
Packet p2 = new Packet(); | |
p2.data = p.GetBytes((ushort)size, (ushort)(8 + offset)); | |
offset += size; | |
ProcessPacket(p2); | |
} | |
try | |
{ | |
stream.BeginRead(buffer, 0, 4, this.callbackSize, null); | |
} | |
} | |
// 省略catch部分 | |
} | |
private void ProcessPacket(Packet p) | |
{ | |
// 省略... | |
} | |
public string DumpData(Packet p) | |
{ | |
// 省略.. | |
} | |
/// <summary> | |
/// Sends a packet, which is not yet encrypted, to the client. | |
/// </summary> | |
/// <param name="p">The packet containing all info.</param> | |
public void SendPacket(Packet p, bool nolength, bool noWarper) | |
{ | |
if (!noWarper) | |
{ | |
byte[] buf = new byte[p.data.Length + firstLevelLenth]; | |
Array.Copy(p.data, 0, buf, firstLevelLenth, p.data.Length); | |
p.data = buf; | |
if (firstLevelLenth == 4) | |
p.SetLength(); | |
else | |
p.PutUShort((ushort)(p.data.Length - 2), 0); | |
buf = new byte[p.data.Length + 4]; | |
Array.Copy(p.data, 0, buf, 4, p.data.Length); | |
p.data = buf; | |
p.SetLength(); | |
buf = new byte[p.data.Length + 4]; | |
Array.Copy(p.data, 0, buf, 4, p.data.Length); | |
p.data = buf; | |
} | |
if (!nolength) | |
{ | |
int mod = 16-((p.data.Length - 8) % 16); | |
if (mod != 0) | |
{ | |
byte[] buf = new byte[p.data.Length + mod]; | |
Array.Copy(p.data, 0, buf, 0, p.data.Length); | |
p.data = buf; | |
} | |
p.PutUInt((uint)(p.data.Length - 8), 0); | |
} | |
try | |
{ | |
byte[] data; | |
data = Crypt.Encrypt(p.data, 8); | |
sock.BeginSend(data, 0, data.Length, SocketFlags.None, null, null); | |
} | |
catch (Exception ex) | |
{ | |
Logger.ShowError(ex); | |
this.Disconnect(); | |
} | |
} | |
public void SendPacket(Packet p, bool noWarper) | |
{ | |
SendPacket(p, false, noWarper); | |
} | |
public void SendPacket(Packet p) | |
{ | |
SendPacket(p, false); | |
} | |
} | |
} |
已經去掉無關代碼,但是還是太長了。
要表達的其實是客戶端和伺服器確認連線的流程:
- 客戶端發送長度為8的封包
- 伺服器啟動鑰匙交換 ,準備私鑰 ,向客戶端發送含有公鑰、長度為529的封包
- 客戶端收到伺服器的公鑰,準備私鑰,製作共享鑰匙,並向伺服器發送含有 公鑰、長度為260的封包
- 伺服器收到客戶端的公鑰製作共享鑰匙
- 連接成立

來源
想當年我自行斷斷續續去湊關鍵字花了幾個月最後才得出是DH交換
書本和大神的文章是可以省掉以年為單位的時間可謂不誇張…每每醍醐灌頂
每天生活在他們的陰影中實在不要太幸福
具體可以自行參照wiki對應交換所需要的資料。(謎之聲: 再說一次! Ecore是日本的網站非指ECO-Re私服)



DH鑰匙交換的計算 對應的SagaECO中半個Encryption類別(emmm…作者你確定?)
// SagaECO/SagaLib/Encryption.cs | |
public class Encryption | |
{ | |
public static BigInteger Two = new BigInteger((uint)2); | |
public static BigInteger Module = new BigInteger("f488fd584e49dbcd20b49de49107366b336c380d451d0f7c88b31c7c5b2d8ef6f3c923c043f0a55b188d8ebb558cb85d38d334fd7c175743a31d186cde33212cb52aff3ce1b1294018118d7c84a70a72d686c40319c807297aca950cd9969fabd00a509b0246d3083d66a45d419f9c7cbd894b221926baaba25ec355e92f78c7"); | |
BigInteger privateKey = Two; | |
// 製造私鑰 | |
public void MakePrivateKey() | |
{ | |
// 具體是用DateTime.Now還是用其他方法去產生亂數 不影響結果 | |
SHA1 sha = SHA1.Create(); | |
byte[] tmp = new byte[40]; | |
sha.TransformBlock(System.Text.Encoding.ASCII.GetBytes(DateTime.Now.ToString() + DateTime.Now.ToUniversalTime() + DateTime.Now.ToLongDateString()), 0, 40, tmp, 0); | |
privateKey = new BigInteger(tmp); | |
} | |
// 製造公鑰 | |
public byte[] GetKeyExchangeBytes() | |
{ | |
if (privateKey == Two) | |
return null; | |
// 重點... | |
return Two.modPow(privateKey, Module).getBytes(); | |
} | |
public void MakeAESKey(string keyExchangeBytes) | |
{ | |
BigInteger A = new BigInteger(keyExchangeBytes); | |
// 取得共享鑰匙 | |
byte[] R = A.modPow(privateKey, Module).getBytes(); | |
// ... | |
} | |
} |
在知道是Diffie–Hellman key exchange之後,其弱點同時暴露:
一個中間人在信道的中央進行兩次迪菲-赫爾曼金鑰交換,一次和Alice另一次和Bob,就能夠成功的向Alice假裝自己是Bob,反之亦然。 攻擊者可以解密(讀取和儲存)任何一個人的資訊並重新加密資訊,然後傳遞給另一個人。
取自wikipedia
幾乎所有ECO封包工具皆基於中間人攻擊。翻查記載,原來曾經有個工具叫作Ecoxy(不用pm我了我也沒有~_~),具體實現依靠路由表來設置代理,從而作出中間人攻擊。
クライアントからサーバへの接続をecoxyに中継させる
そのため、パケットのIPアドレスを適切に書き換えるクライアント→サーバのあて先IPアドレスをecoxyのアドレスに変換
ecoxy代理客戶端與伺服器之間的連接
ecoxy→サーバはそのまま通す
ecoxy→クライアントの送信元IPアドレスをサーバのIPアドレスに変換する
為此,會替換掉封包中的IP地址
客戶端->伺服器 傳送之前變換成ecoxy的地址
ecoxy->伺服器 就這樣傳遞
ecoxy->客戶端 發包人IP地址變換成伺服器的地址
計算出共享金鑰了,還差最後一步計算: 利用D-H交換中的共享金鑰計算出AES金鑰
通過兩邊一致算法和原料,計算出相等的AES金鑰進行對稱加密。
很多現有機制也是先採用非對稱加密來建立連接,其後進行對稱加密。
這個也是先看了Ecore的注解比較好,否則心臟有危險


特意挑選了有二進制的
看來是我多事了…並沒有用

對應Encryption.cs的另一半
// SagaECO/SagaLib/Encryption.cs | |
public class Encryption | |
{ | |
byte[] aesKey; | |
Rijndael aes; | |
public Encryption() | |
{ | |
aes = Rijndael.Create(); | |
aes.Mode = CipherMode.ECB; | |
aes.KeySize = 128; | |
aes.Padding = PaddingMode.None; | |
} | |
public void MakeAESKey(string keyExchangeBytes) | |
{ | |
BigInteger A = new BigInteger(keyExchangeBytes); | |
byte[] R = A.modPow(privateKey, Module).getBytes(); | |
// 128位 | |
aesKey = new byte[16]; | |
Array.Copy(R, aesKey, 16); | |
// input example: C8 76 31... | |
for (int i = 0; i < 16; i++) | |
{ | |
// 筆者也好久沒有面對位元運算了 腦漿炸裂 > < | |
// e.g. 0xC8 = 200d = 11001000 = 高位1100 低位1000 | |
// 取高4位 高4位把低4位推走了 此時0d <= tmp2 <= 15d | |
byte tmp = (byte)(aesKey[i] >> 4); | |
// 取低4位 0xF=1111 更高位的全部強制為0 此時0d <= tmp2 <= 15d | |
byte tmp2 = (byte)(aesKey[i] & 0xF); | |
if (tmp > 9) // 高位1100 = 12 | |
tmp = (byte)(tmp - 9); // tmp = 3d | |
if (tmp2 > 9) // 低位1000 = 8d | |
tmp2 = (byte)(tmp2 - 9); | |
// 重新由高位位元和低位位元組合位元組 = 0x38 | |
// 可見'c'變成了'3' 就結果而言是相等的 | |
aesKey[i] = (byte)( tmp << 4 | tmp2); | |
} | |
} | |
public bool IsReady | |
{ | |
get | |
{ | |
return aesKey != null; | |
} | |
} | |
public byte[] Encrypt(byte[] src, int offset) | |
{ | |
if (aesKey == null) return src; | |
if (offset == src.Length) return src; | |
ICryptoTransform crypt = aes.CreateEncryptor(aesKey, new byte[16]); | |
int len = src.Length - offset; | |
byte[] buf = new byte[src.Length]; | |
src.CopyTo(buf, 0); | |
crypt.TransformBlock(src, offset, len, buf, offset); | |
return buf; | |
} | |
public byte[] Decrypt(byte[] src, int offset) | |
{ | |
if (aesKey == null) return src; | |
if (offset == src.Length) return src; | |
ICryptoTransform crypt = aes.CreateDecryptor(aesKey, new byte[16]); | |
int len = src.Length - offset; | |
byte[] buf = new byte[src.Length]; | |
src.CopyTo(buf, 0); | |
crypt.TransformBlock(src, offset, len, buf, offset); | |
return buf; | |
} | |
} |
不得不說這寫法實在是….九陰真經
亂入一下 這是上面基於Ecore思路的實作
// 特別嘉賓 | |
// virtualeco/lib/general.py | |
def get_rijndael_key(share_key_bytes): | |
rijndael_key_hex = "" | |
for s in share_key_bytes[:32].lower(): | |
#if ord(s) > 57: rijndael_key_bytes += chr(ord(s)-48) | |
#else: rijndael_key_bytes += s | |
if s == "a": rijndael_key_hex += "1" | |
elif s == "b": rijndael_key_hex += "2" | |
elif s == "c": rijndael_key_hex += "3" | |
elif s == "d": rijndael_key_hex += "4" | |
elif s == "e": rijndael_key_hex += "5" | |
elif s == "f": rijndael_key_hex += "6" | |
else: rijndael_key_hex += s | |
return rijndael_key_hex.decode("hex") |
謎之聲: C#的版本port到C語言的效率比這段python靠字串不停疊加可能快上50倍…
謎之聲2: C的版本解讀花上100倍時間 甚至沒有了python版的輔助就能入選世界文化遺產了 T_T
完成加解密後近乎通信協議的完全體了,自此封包工具廣泛應用在遊戲中… 例如用作保存遺體XD
想帶出的一點是,一個遊戲的通訊加解密方法被破解也就意味著脫機外掛和私服的基礎,沒有和客戶端的溝通方法的話是不可能實現私服的。(先不要被這篇嚇跑啦,通信層的知識在其他部分幾乎不會用到,它是存在感零同時相當重要的根基。有一天,你會再次回到這裡跟困難打交道,當你有所求之時。
此篇算是勉強完成了ECO RPC通信握手和加密層的解說,特別需要感謝前人的努力,他們鍥而不捨的駭客精神總算是攻破了這一道大關。
後記:
這篇文章沒有修改NetIO。邏輯和線程夾雜在一起,既然能用,又不是犯下滔天大罪,何必處處留難人家。 對於中間人攻擊無效的遊戲客戶端還有DLL注入這一手,只是難度相當大。
[…] RPC篇終於來到第五篇… 上回提要是每當與伺服器連接時首要的鑰匙交換事項,換言之通信確立後馬上開始通信(傳送門)。 […]