Как я перехватывал трафик покер рума или «Пишем свой MitM SSL прокси на C#»

Однажды у меня появилась навязчивая идея: посмотреть, а что же там такого покерный клиент отправляет на сервер. Как Вы понимаете, крупные покерные румы используют SSL для передачи данных. Протоколы, основанные на асимметричном шифровании, подвержены только одному известному мне виду атак — MitM (Man in the middle — человек посередине).

Помаявшись с тонной софта, предназначенного для реализации MitM на SSL соединение, я пришел к выводу, что руки растут не из того места либо у разработчиков данных инструментов, либо у меня. Но идея была жутко навязчивая, и было принято решение сделать всё вручную. Если интересно, что же из всего этого вышло, прошу под кат.

http://habrastorage.org/getpro/habr/post_images/da9/cde/e8d/da9cdee8d285cc304fdbbe10ac76e66f.jpg

Данная статья просвещена написанию простого инструмента для реализации MitM-атаки. Те, кто не знакомы с тем, что такое MitM, могут почитать об этом тут.

Цель

В качестве подопытного был выбран клиент Cake Poker по причине моего длительного знакомства с ним. Началось все с того, что я просто запустил покерный клиент и полез в старый добрый менеджер ресурсов, в котором нашел с десяток подключений от него.

http://habrastorage.org/getpro/habr/post_images/27c/6cf/e82/27c6cfe82800e7b7f41695035622a259.png

Поэкспериментировав, я смог выделить соединение, которое держится всегда. Его я и взял на прицел для проведение MitM атаки.

http://habrastorage.org/getpro/habr/post_images/8fa/109/b0a/8fa109b0a7fb511ab5d6de4c686d1d10.png

Источник соединения известен, конечная цель известна — lb6.playdata.co.uk.

Перенаправление трафика

Теперь необходимо было вклиниться между клиентом и сервером. Нечего особо хитрого изобретать не стал, ибо незачем — просто добавил в host доменные имена, соотнесённые с 127.0.0.1. Не сложно догадаться, что если есть lb6.playdata.co.uk, то есть и lb1.playdata.co.uk, и lb8.playdata.co.uk. С ними поступил аналогично, занеся в host, т.к. конечные инстансы, как я понял, выбираются по расположению звёзд. При запуске покерного клиента он повисает в ожидании подключения. Замечательно. Это означает что трафик был перенаправлен на нашу машинку. Идём дальше.

http://habrastorage.org/getpro/habr/post_images/e9a/6b0/3df/e9a6b03dfeca220fb12c90c07cc424ae.png

Прокси

Следующей задачей было написание прокси на C#. Да-да, простой прокси, для заготовки будущей программы. Чтобы не заниматься изобретением велосипеда, я по-быстрому нагуглил подходящее для меня решение:TCP Proxy in C# using Task Parallel Library.
Немного его отрефакторил (ненавижу, когда много вложенности), захардкодил конечную точку подключения и запустил. Запускаю покерного клиента — всё работает. В диспетчере ресурсов можем видеть, что трафик от покерного клиента идёт к моему прокси, а от него на сервер и обратно.

MitM на SSL

Далее нам предстоит реализовать MitM-атаку на SSL. Разделим её на два этапа: первый — соединение клиента с прокси; второй — прокси с сервером.
Для реализации первого этапа, когда клиент подключится к прокси, мы не будем отправлять пришедшие данные дальше, а начнём, так называемую, процедуру рукопожатия. На C# это можно сделать с помощью экземпляра класса SslStream, построенного поверх уже созданного NetworkStream. В момент создания передаётся информация о протоколе и прочая специфическая информация.
После этого, передаём клиенту свой сертификат. Это делается с помощью метода AuthenticateAsServer класса SslStream, куда мы должны передать путь до файла сертификата.

Файл x509 сертификата был сгенерирована с помощью утилиты Makecert, которую можно вызвать из девелоперской консоли Visual Studio. Пришлось немного помучиться с параметрами, но всё получилось. Вот неплохое описание того, как ей можно пользоваться: SSL communication in C#. В качестве имени укажем *.playdata.co.uk. Это имя покрывает все домены, которые используются покерным клиентом.

makecert -n CN=MyCA -cy authority -a sha1 -sv “MyCA.pvk” -r “MyCA.cer” //Создаём сертификат ЦА
certmgr -add -all -c “MyCA.cer” -s -r LocalMachine Root //Добавляем в довереные коневые центры сертификаты
makecert -n CN=*.playdata.co.uk -ic MyCA.cer -iv MyCA.pvk -a sha1 -sky exchange -pe -sr currentuser -ss my SslServer.cer
//Создаём серверный сертификат


Мы сгенерировали серверный закрытый ключ и ключ УЦ (удостоверительного центра), которым подписан наш ключ. Ключ УЦ помещаем в «Доверенные корневые центры сертификации», и вуаля! Если посмотреть на наш серверный ключик средствами просмотра ключей Windows, то мы увидим, что система считает его валидным, как и покерный клиент, который запущен в нашей системе.

http://habrastorage.org/getpro/habr/post_images/c71/50b/1a3/c7150b1a3f7126a8446a34fc8419eb9a.png

Путь до полученого сертификата мы передаём в метод AuthenticateAsServer. Если всё прошло хорошо, то у нас получится SSL соединение от клиента до прокси, в которое клиент будет посылать данные. Теперь необходимо давать клиенту адекватные ответы на его запросы. Для этого нам потребуется реализовать второй этап MitM-атаки, а именно построить SSL соединение от прокси до сервера. Так же строим SslStream поверх NetworkStream до сервера и авторизовываемся с помощью метода AuthenticateAsClient. Данные, приходящие из SSL соединения клиента и сервера отправляем друг другу.

Процес рукопожатия C#

var certificate = new X509Certificate("SslServer.cer", "123");
var clientStream = new SslStream(client.GetStream(), false);
clientStream.AuthenticateAsServer(certificate, false, System.Security.Authentication.SslProtocols.Default, false);

var server = new TcpClient("200.26.205.63", 4520);
var serverSslStream = new SslStream(server.GetStream(), false, SslValidationCallback, null);
serverSslStream.AuthenticateAsClient("lb3.playdata.co.uk");

Через некоторое время работы можно будет заметить в менеджере ресурсов расхождение количества отправленных байт на прокси и от него. Это обусловлено тем, что при шифровании разные ключи дают разный по размеру результат.

http://habrastorage.org/getpro/habr/post_images/2e0/30e/36d/2e030e36d580c262c586320f747acbbe.png

Заключение

Что же дальше? А дальше мы добавим код для сохранения данных в текстовый файл, чтобы их можно было проанализировать то, что передаёт покерный клиент серверу. Собственно, всё, MitM Proxy написан.

Осталось добавить в него немного блекджека. Например, для разбора идущего через него трафика, выдирания карт пользователя и отправки нам и т. д…
Я написал разборщик трафика на лету, что бы можно было удобно мониторить, что отправляет клиент, и соотносить это с моими действиями. Демонстрация того, что получилось у меня:

Исходники mitm proxy

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using ConnectionAnalizer;

namespace MITMProxy
{
    class Program
    {

        static readonly TcpListener Listener = new TcpListener(IPAddress.Any, 4520);
        const int BufferSize = 4096;

        static void Main()
        {
            Listener.Start();
            new Task(() =>
            {
                while (true)
                {
                    var client = Listener.AcceptTcpClient();
                    new Task(() => AcceptConnection(client)).Start();
                }
            }).Start();
            Debug.WriteLine("Server listening on port 4502.  Press enter to exit.");
            Console.ReadLine();
            Listener.Stop();
        }

        private static void AcceptConnection(TcpClient client)
        {
            try
            {
                var certificate = new X509Certificate("SslServer.cer", "123");
                var clientStream = new SslStream(client.GetStream(), false);
                clientStream.AuthenticateAsServer(certificate, false, System.Security.Authentication.SslProtocols.Default, false);

                var server = new TcpClient("200.26.205.63", 4520);
                var serverSslStream = new SslStream(server.GetStream(), false, SslValidationCallback, null);
                serverSslStream.AuthenticateAsClient("lb3.playdata.co.uk");
             
                new Task(() => ReadFromClient(client, clientStream, serverSslStream)).Start();
                new Task(() => ReadFromServer(serverSslStream, clientStream)).Start();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }

        }

        private static bool SslValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslpolicyerrors)
        {
            return true;
        }

        private static void ReadFromServer(Stream serverStream, Stream clientStream)
        {
            var message = new byte[BufferSize];
            while (true)
            {
                int serverBytes;
                try
                {
                    serverBytes = serverStream.Read(message, 0, BufferSize);
                    clientStream.Write(message, 0, serverBytes);
                }
                catch
                {
                    break;
                }
                if (serverBytes == 0)
                {
                    break;
                }
            }
        }

        private static void ReadFromClient(TcpClient client, Stream clientStream, Stream serverStream)
        {
        var message = new byte[BufferSize];
            var fileInfo = new FileInfo("client");
            if (!fileInfo.Exists)
                fileInfo.Create().Dispose();
            using (var stream = fileInfo.OpenWrite())
            {
                while (true)
                {
                    int clientBytes;
                    try
                    {
                        clientBytes = clientStream.Read(message, 0, BufferSize);
                    }
                    catch
                    {
                        break;
                    }
                    if (clientBytes == 0)
                    {
                        break;
                    }
                    serverStream.Write(message, 0, clientBytes);
                    memoryStream.Write(message, 0, clientBytes);
                    stream.Write(message, 0, clientBytes);

                }
                client.Close();
            }
        }
    }
}

(c) habra/post/213397/