• 注册
  • SPTarkov 动态 SPTarkov 动态 关注:245 内容:59

    EFT internals research: Payload encryption

  • 查看作者
  • 打赏作者
  • 当前位置: ODDBA社区 > SPTarkov 动态 > 正文
  • 2
  • SPTarkov 动态
  • SPT主创
    初窥堂奥
    VIP2
    SPTarkov项目组

    Hello everyone!

    Today, I would like to cover 4 aspects of EFT's internals in 4 different articles:
    secured comminucation, payload encryption, response caching and file
    integrity scanning. These articles are mostly aimed at SuperMod author
    and anyone else interested in making modding more secure.

    This article will sadly be very technical since there is no other way around it. Hope the article will be enjoyable nonetheless!


    Payload encryption

    Since EFT 0.12.11, there is a new capability: the data from the server is encrypted. For AKI, this is not a problem; they hook deep into EFT's code to grab the data for the server, and up until now the encryption is advisatory, not mandatory. If it ever will become mandatory, AKI has a load of work to do. Luckily I did most of the work beforehand in my research server.

    This was an absolute massive pain to get working because EFT's code is a shitshow.

    The payload (the data send from the server to the client) has the following properties:

    – The data is encrypted using an AES-192 key in CBC mode
    – The encrypted data is ZLIB compressed (RFC1950)

    AES is an algorithm in which there is a key and a randomized hash (IV, initialization vector) to encrypt the payload. The key itself is easy enough to find within the game's code, but the IV is the annoying part. Since EFT rarely conforms to standards, the IV is not send the normal way. Instead, the first bytes of the payload contains the IV.

    Alright, with those requirements in mind, let's write the decryption code!

        public class AesEft
        {
            // AES blocksize is always 128 bits
            private const int _blockSize = 16;

            // AES-192, UTF-8 bytes, extracted from client
            private readonly byte[] _eftKey = new byte[24]
            {
                0x51, 0x6F, 0x2A, 0x6E, 0x70, 0x37, 0x2A, 0x79, 0x50, 0x48, 0x71,
                0x57, 0x58, 0x38, 0x5A, 0x42, 0x33, 0x5A, 0x4F, 0x40, 0x6D, 0x31,
                0x6B, 0x34
            };

            private async Task<byte[]> Run(byte[] data, byte[] iv, bool encrypt)
            {
                using (var ms = new MemoryStream())
                {
                    using (var aes = Aes.Create())
                    {
                        aes.Mode = CipherMode.CBC;
                        aes.Padding = PaddingMode.Zeros;
                        aes.Key = _eftKey;

                        if (encrypt)
                        {
                            // EFT sets IV as first block
                            aes.GenerateIV();
                            await ms.WriteAsync(aes.IV, 0, aes.IV.Length);
                        }
                        else
                        {
                            aes.IV = iv;
                        }

                        var transform = encrypt
                            ? aes.CreateEncryptor()
                            : aes.CreateDecryptor();

                        using (var cs = new CryptoStream(ms, transform, CryptoStreamMode.Write))
                        {
                            await cs.WriteAsync(data, 0, data.Length);
                        }
                    }

                    return ms.ToArray();
                }
            }

            public async Task<byte[]> Decrypt(byte[] bytes)
            {      
                using (var msIV = new MemoryStream())
                {
                    using (var msData = new MemoryStream())
                    {
                        // first block is IV
                        await msIV.WriteAsync(bytes, 0, _blockSize);

                        // encrypted data
                        var length = bytes.Length _blockSize;
                        await msData.WriteAsync(bytes, _blockSize, length);

                        return await Run(msData.ToArray(), msIV.ToArray(), false);
                    }
                }
            }
        }

    In this code, _eftKey is the key extracted from the client. Inside Decrypt we grab the IV from the beginning. The rest is standard.

    Sadly, this is only the first step. It's the second step I got really stuck on. You see, AES decrypts data in blocks of a fixed size. If the data is smaller than the block, it will insert 0x00 for each non-occupied byte left in the block. This is what we call a zero-bytes tail. These bytes are harmless, normally.

    BUT NOT TODAY.


    EFT uses ComponentAce.Compressions.libs.zlib partially with their own additional code on top for (de)compression. It is a library that can decompress RFC1950 formatted data to it's original form. ZLib is often confused with normal zip files, which generally use gzip or deflate. That's why I specify the standard here. Because EFT managed to use another non-standard method! Attempting to compress the data as deflate or gzip will cause EFT to decompress prematurely, soft-locking the game.

    You see, normally it's not a problem that EFT uses zlib. However, remember the zero-bytes tail? It causes componentace's code to hang indefinetly, hard locking the game. EFT is using a non-standard implementation of AES to get rid of these bytes. This was a problem that is very hard to debug.

    The fix is the following: force EFT to use my research server's implementation of zlib (de)compression, and remove the zero-bytes tail when decompressing.

    In addition, this solves another issue: EFT's (de)compression doesn't support multi-threaded scenarios (which you'll see in the last article of this series), as thing my research server heavily relies on. Attempting to use their code in such a scenario causes buffer overrides, and thus malformed data, which in turn crashes the game.

    namespace Haru.Client.Patches
    {
        public class ZlibDeflatePatch : APatch
        {
            private static readonly Zlib _zlib;

            static ZlibDeflatePatch()
            {
                _zlib = new Zlib();
            }

            public ZlibDeflatePatch() : base()
            {
                Id = “com.Haru.Client.zlibdeflate”;
                Type = EPatchType.Prefix;
            }

            protected override MethodBase GetOriginalMethod()
            {
                return typeof(Pooled9LevelZLib).GetMethod(nameof(Pooled9LevelZLib.CompressToBytesNonAlloc));
            }

            // note: EFT uses the resultBuffer by reference, ensure the result is stored in there!
            protected static bool Patch(ref int __result, string text, ref byte[] resultBuffer)
            {
                var bytes = Encoding.UTF8.GetBytes(text);
                resultBuffer = _zlib.Deflate(bytes);
                __result = resultBuffer.Length;

                return false;
            }
        }
    }

    Getting EFT to use my implementation is simple enough: find the method and override it to use my code

            public byte[] RemoveZeroTail(byte[] data)
            {
                var tail = 0;

                for (var i = data.Length 1; i > 0; i)
                {
                    if (data[i] == 0x00)
                    {
                        ++tail;
                    }
                    else
                    {
                        break;
                    }
                }

                using (var ms = new MemoryStream(data))
                {
                    ms.SetLength(ms.Length tail);
                    return ms.ToArray();
                }
            }

            public byte[] Inflate(byte[] data)
            {
                var bytes = RemoveZeroTail(data);
                return Run(bytes);
            }

    Removing the tail itself is also not too complicated; count from the end of the payload the amount of 0x00 bytes and remove it.

    This is not the first time the decompression library from BSG caused grief. At this point, the amount of comments in the code is larger than the actual functionality!

            private byte[] Run(byte[] data, int level = 0)
            {
                // ZOutputStream.Close() flushes itself.
                // ZOutputStream.Flush() flushes the target stream.
                // It's fucking stupid, but whatever.
                // — Waffle.Lord, 2022-12-01

                // Also apparenty ZOutputStream cannot use WriteAsync() because
                // “it does not support writing”. My brother in christ, we're
                // already capable of writing since we use Write().
                // — Senko-san, 2023-08-21

                // Also if you're trying to decompress data with a zero-padded
                // tail, it will just hang. Was bloody annoying to debug.
                // — Senko-san, 2023-11-06
       
                using (var msout = new MemoryStream())
                {
                    using (var zs = (level > 0)
                        ? new ZOutputStream(msout, level)
                        : new ZOutputStream(msout))
                    {
                        zs.Write(data, 0, data.Length);
                    }

                    return msout.ToArray();
                }
            }

    From all the challenges this caused, I hear you wondering:

    Was it really worth it?

    …and the answer is yes! I've never written encryption code like this before, so there was a lot to learn and get creative with. Overcoming challenges makes one a better person, and more the wiser. The hardest challenge is to keep things organized and readable, especially when the concepts are very technical and often difficult to read.

    I am really proud of my solution since the other options are vastly more complex. keeping things simple where it matters is very important.

    Why is this important?

    Securing a connection over SSL is often not enough to ensure secure comminucation. In order to prevent lowlife hackers from stealing mods, extra security is required. AES encryption is perfect from this. It is really hard to extract without the technical know-how, and conventional tools won't work due to EFT's implementation.

    自成一派
    赠送了礼物[赞]
    回复
    已臻大成
    VIP5

    666

    回复

    请登录之后再进行评论

    登录
  • 签到
  • 任务
  • 发布
  • 模式切换
  • 偏好设置
  • 帖子间隔 侧栏位置: