image

Думаю, что большинство из местных обитателей знакомы с понятием сниффера. Несмотря на то, что конечная цель у них одна и та же (перехват пакетов, соответствующих определённым критериям), достигают они её совершенно разным образом. Какой-то софт слушает указанный сетевой интерфейс (например, Wireshark, где это реализовано при помощи библиотеки Pcap), а какой-то — перехватывает вызовы ответственных за взаимодействие с сетью WinAPI-функций. И у того, и у другого метода есть свои плюсы и минусы, однако если по задаче необходим перехват пакетов от конкретного заранее известного приложения, то второй вариант, как правило, банально удобнее. В этом случае нет нужды узнавать IP-адреса и порты, которые использует данная программа (особенно учитывая тот факт, что их может быть довольно много), и можно просто сказать «я хочу перехватывать все пакеты вот этого приложения». Удобно, не правда ли?

Пожалуй, самым популярным на сегодняшний день сниффером, работающим по принципу перехвата вызовов определённых WinAPI-функций, является WPE Pro. Возможно, многие из вас слышали о нём на различных форумах, посвящённых онлайн-играм, ведь именно для получения преимуществ в различных играх этот сниффер в большинстве случаев и используется. Свою задачу он выполняет прекрасно, однако у него есть один неприятный недостаток — он не умеет работать с 64-битными приложениями. Так уж вышло, что по одной из возникших задач мне как раз понадобилось перехватывать пакеты от 64-битного приложения, и я посмотрел в сторону Wireshark. К сожалению, использовать его в данной ситуации было не очень удобно — исследуемое приложение отправляло данные на разные IP-адреса, каждый раз открывая новый порт. Погуглив немного, я обнаружил, что готовых аналогов WPE Pro с поддержкой x64 нет (если они всё же есть, буду признателен за ссылки в комментариях — обратите внимание, что речь идёт о Windows). Автор WPE Pro не оставил никаких контактных данных на официальном сайте и в самом сниффере, так что я принял решение разобраться в этом вопросе самостоятельно.

Как протекал процесс и что из этого вышло, читайте под катом.

Итак, что необходимо сделать в первую очередь? Верно, скачать сам WPE Pro. Сделать это можно на официальном сайте сниффера, где предлагаются для загрузки сразу две версии — 0.9a и 1.3. Мы будем рассматривать версию 0.9a, потому что именно она работает на последних версиях Windows.

Скачали? Теперь давайте проверим, не накрыт ли он каким-нибудь паковщиком или протектором:

image

image

Похоже, на этот раз снимать нам ничего не придётся. Тогда берём в руки OllyDbg и загружаем «WpePro.net.exe». Давайте для примера запустим 64-битную версию Dependency Walker'а и узнаем, почему WPE Pro не может отобразить его в списке доступных для перехвата процессов.

Получить список текущих процессов в WinAPI можно двумя основными путями:


При желании можете почитать о разнице между данными способами, например, тут.

Смотрим на intermodular calls модуля «WpePro.net.exe» и видим, что ни CreateToolhelp32Snapshot, ни EnumProcesses тут нет. Возможно, приложение получает их адрес в run-time при помощи WinAPI-функции GetProcAddress, так что давайте посмотрим на referenced text strings. На этот раз всё с точностью наоборот — нашлась как строка «CreateToolhelp32Snapshot», так и «EnumProcesses». Ставим бряки на места, где происходит обращение к данным строкам, нажимаем на кнопку «Target program» в WPE Pro и смотрим на место, на котором мы остановились:

image

Если понажимать F9, то мы увидим, что этот же бряк срабатывает ещё несколько раз, прежде чем наконец появится окно со списком текущих процессов. Давайте посмотрим, почему в нём не оказалось Dependency Walker'а. Закрываем окно, снова нажимаем на кнопку «Target program» и выполняем пошаговую отладку, начиная с того же самого бряка. Вскоре после вызовов GetProcAddress мы попадаем в цикл, в котором последовательно вызываются следующие функции — OpenProcess, EnumProcessModules, скрывающаяся за инструкцией CALL DWORD PTR DS:[EDI+10], GetModuleFileNameEx (инструкция CALL DWORD PTR DS:[ECX+14]) и CloseHandle:

image

Давайте поставим бряк по адресу 0x0042A910, где происходит занесение на стек последнего аргумента функции OpenProcess — PID, и попробуем дождаться момента, когда регистр EAX примет значение, равное идентификатору процесса Dependency Walker'а. На моей машине в этот момент он был равен 6600, т.е. 0x19C8.

Если пробежаться по коду, то мы увидим, что в случае Dependency Walker'а инструкция TEST EAX,EAX, находящаяся по адресу 0x0042A942 и следующая за вызовом WinAPI-функции EnumProcessModules, заставляет следующий за ней оператор условного перехода прыгнуть по адресу 0x0042A9E7, где находится вызов CloseHandle, что, разумеется, не является нормальным ходом работы приложения, ведь мы ещё даже не дошли до вызова функции GetModuleFileNameEx:

image

Обратите внимание на код ошибки — 0x12B (ERROR_PARTIAL_COPY). Давайте посмотрим, почему такое может происходить. Открываем документацию к функции EnumProcessModules и видим следующее:

If this function is called from a 32-bit application running on WOW64, it can only enumerate the modules of a 32-bit process. If the process is a 64-bit process, this function fails and the last error code is ERROR_PARTIAL_COPY (299)

Да, это как раз наш случай.

Несмотря на то, что в документации к функции GetModuleFileNameEx не сказано ничего похожего, ведёт она себя аналогичным образом, что описано, например, тут. Одним из вариантов решения этой проблемы является использование функции QueryFullProcessImageName, которая будет отрабатывать нормально даже в случае вызова из 32-битного приложения, запущенного в WOW64, в случае применения её к 64-битному процессу.

Теперь мы можем занопить вызов функции EnumProcessModules

0042A93F   .  FF57 10       CALL DWORD PTR DS:[EDI+10]               ;  EnumProcessModules
0042A942   .  85C0          TEST EAX,EAX
0042A944      90            NOP
0042A945      90            NOP
0042A946      90            NOP
0042A947      90            NOP
0042A948      90            NOP
0042A949      90            NOP
0042A94A   .  8B4424 14     MOV EAX,DWORD PTR SS:[ESP+14]
0042A94E   .  BE 00000000   MOV ESI,0

и написать code cave для замены GetModuleFileNameEx функцией QueryFullProcessImageName:

0042A971     /E9 DA3F0600   JMP WpePro_n.0048E950
0042A976     |90            NOP
0042A977     |90            NOP
0042A978     |90            NOP
0042A979     |90            NOP                                      ;  GetModuleFileNameEx
0042A97A     |90            NOP
0042A97B     |90            NOP
0042A97C     |90            NOP
0042A97D     |90            NOP
0042A97E     |90            NOP
0042A97F     |90            NOP
0042A980     |90            NOP
0042A981     |90            NOP
0042A982     |90            NOP
0042A983     |90            NOP
0042A984     |90            NOP
0042A985     |90            NOP
0042A986     |90            NOP
0042A987     |90            NOP
0042A988     |90            NOP
0042A989     |90            NOP
0042A98A     |90            NOP
0042A98B     |90            NOP
0042A98C     |90            NOP
0042A98D     |90            NOP
0042A98E   > |6A 14         PUSH 14
0042A990   . |E8 7FCF0300   CALL WpePro_n.00467914

0048E950      60            PUSHAD
0048E951      9C            PUSHFD
0048E952      8D5C24 AC     LEA EBX,DWORD PTR SS:[ESP-54]
0048E956      C74424 AC 040>MOV DWORD PTR SS:[ESP-54],104
0048E95E      53            PUSH EBX
0048E95F      50            PUSH EAX
0048E960      6A 00         PUSH 0
0048E962      55            PUSH EBP
0048E963      E8 777CB675   CALL KERNEL32.QueryFullProcessImageNameA
0048E968      9D            POPFD
0048E969      61            POPAD
0048E96A    ^ E9 1FC0F9FF   JMP WpePro_n.0042A98E

Теперь WPE Pro отображает множество новых процессов, в том числе и наш Dependency Walker:

image

Однако при попытке приаттачиться к данному процессу WPE Pro выдаёт неприятное для нас сообщение:

image

Тут мы уже, к сожалению, ничего не можем поделать. DLL-инъекция осуществляется путём внедрения своей DLL в адресное пространство target-процесса, а на MSDN сказано следующее:

On 64-bit Windows, a 64-bit process cannot load a 32-bit dynamic-link library (DLL). Additionally, a 32-bit process cannot load a 64-bit DLL

Несложно догадаться, что WPE Pro поставляется только с 32-битной библиотекой.

Что же делать? Писать свой перехватчик WinSock? А почему бы и нет?

Но как мы это будем осуществлять? Первое, что приходит на ум — это воспользоваться Detours, однако на официальном сайте сразу же сообщается, что поддержка 64-битных приложений есть только в Professional Edition:

Detours Express is limited to 32-bit processes on x86 processors

Тогда давайте попробуем EasyHook. Среди всего прочего, эта библиотека как раз позволяет работать с 64-битными приложениями:

You will be able to write injection libraries and host processes compiled for AnyCPU, which will allow you to inject your code into 32- and 64-Bit processes from 64- and 32-Bit processes by using the very same assembly in all cases

С небольшими изменениями примера, продемонстрированного в официальном туториале, получаем код, перехватывающий вызовы функций recv и send из WinSock и выводящий перехваченные сообщения побайтово в шестнадцатеричном виде:

Код программы, совершающей DLL-инъекцию

using EasyHook;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Remoting;
using System.Text;
using System.Threading.Tasks;

namespace Sniffer
{
    public enum MsgType { Recv, Send };

    [Serializable]
    public class Message
    {
        public byte[] Buf;
        public int Len;
        public MsgType Type;
    }

    public class InjectorInterface : MarshalByRefObject
    {
        public void OnMessages(Message[] messages)
        {
            foreach (Message message in messages)
            {
                switch (message.Type)
                {
                    case MsgType.Recv:
                        Console.WriteLine("Received {0} bytes via recv function", message.Len);
                        break;

                    case MsgType.Send:
                        Console.WriteLine("Sent {0} bytes via send function", message.Len);
                        break;

                    default:
                        Console.WriteLine("Unknown action");
                        continue;
                }

                foreach (byte curByte in message.Buf)
                {
                    Console.Write("{0:x2} ", curByte);
                }

                Console.WriteLine("=====================");
            }
        }

        public void Print(string message)
        {
            Console.WriteLine(message);
        }

        public void Ping()
        {

        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                Console.WriteLine("Usage: Sniffer.exe [pid]");
                return;
            }

            int pid = Int32.Parse(args[0]);

            try
            {
                string channelName = null;
                RemoteHooking.IpcCreateServer<InjectorInterface>(ref channelName, WellKnownObjectMode.SingleCall);

                RemoteHooking.Inject(
                   pid,
                   InjectionOptions.DoNotRequireStrongName,
                   "WinSockSpy.dll",
                   "WinSockSpy.dll",
                    channelName);

                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("An error occured while connecting to target: {0}", ex.Message);
            }
        }
    }
}

Код самой DLL, которая будет инжектиться в целевой процесс

using EasyHook;
using Sniffer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WinSockSpy
{
    public class Main : EasyHook.IEntryPoint
    {
        public Sniffer.InjectorInterface Interface;
        public LocalHook RecvHook;
        public LocalHook SendHook;
        public LocalHook WSASendHook;
        public Stack<Message> Queue = new Stack<Message>();

        public Main(RemoteHooking.IContext InContext, String InChannelName)
        {
            Interface = RemoteHooking.IpcConnectClient<Sniffer.InjectorInterface>(InChannelName);
        }

        public void Run(RemoteHooking.IContext InContext, String InChannelName)
        {
            try
            {
                RecvHook = LocalHook.Create(
                    LocalHook.GetProcAddress("Ws2_32.dll", "recv"),
                    new DRecv(RecvH),
                    this);

                SendHook = LocalHook.Create(
                    LocalHook.GetProcAddress("Ws2_32.dll", "send"),
                    new DSend(SendH),
                    this);

                RecvHook.ThreadACL.SetExclusiveACL(new Int32[] { 0 });
                SendHook.ThreadACL.SetExclusiveACL(new Int32[] { 0 });
            }
            catch (Exception ex)
            {
                Interface.Print(ex.Message);
                return;
            }

            // Wait for host process termination...
            try
            {
                while (true)
                {
                    Thread.Sleep(500);

                    if (Queue.Count > 0)
                    {
                        Message[] messages = null;
                        lock (Queue)
                        {
                            messages = Queue.ToArray();
                            Queue.Clear();
                        }
                        
                        Interface.OnMessages(messages);
                    }
                    else
                    {
                        Interface.Ping();
                    }
                }
            }
            catch (Exception)
            {
                // NET Remoting will raise an exception if host is unreachable
            }
        }

        //int recv(
        //  _In_  SOCKET s,
        //  _Out_ char   *buf,
        //  _In_  int    len,
        //  _In_  int    flags
        //);
        [DllImport("Ws2_32.dll")]
        public static extern int recv(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags
        );

        //int send(
        //  _In_       SOCKET s,
        //  _In_ const char   *buf,
        //  _In_       int    len,
        //  _In_       int    flags
        //);
        [DllImport("Ws2_32.dll")]
        public static extern int send(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags
        );

        [UnmanagedFunctionPointer(CallingConvention.StdCall,
            CharSet = CharSet.Unicode,
            SetLastError = true)]
        delegate int DRecv(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags
        );

        [UnmanagedFunctionPointer(CallingConvention.StdCall,
            CharSet = CharSet.Unicode,
            SetLastError = true)]
        delegate int DSend(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags
        );

        static int RecvH(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags)
        {
            Main This = (Main)HookRuntimeInfo.Callback;
            lock (This.Queue)
            {
                byte[] message = new byte[len];
                Marshal.Copy(buf, message, 0, len);
                This.Queue.Push(new Message { Buf = message, Len = len, Type = MsgType.Recv });
            }

            return recv(s, buf, len, flags);
        }

        static int SendH(
            IntPtr s,
            IntPtr buf,
            int len,
            int flags)
        {
            Main This = (Main)HookRuntimeInfo.Callback;
            lock (This.Queue)
            {
                byte[] message = new byte[len];
                Marshal.Copy(buf, message, 0, len);
                This.Queue.Push(new Message { Buf = message, Len = len, Type = MsgType.Send });
            }

            return send(s, buf, len, flags);
        }
    }
}

Конечно, тут ещё есть, над чем поработать:
  • Во-первых, отсутствует перехват WSARecv, WSASend и некоторых других функций, которые могут использоваться в целевых приложениях
  • Во-вторых, отсутствие GUI и «строкового» представления перехваченных сообщений
  • etc

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

Послесловие


Исследование и модификация бинарных файлов не всегда даёт моментальный результат, как это наблюдалось в предыдущих моих статьях (если интересно, читайте об этом тут и тут), так что иногда приходится вернуться в самое начало и изменить свой подход к решению проблемы. Но в этом нет абсолютно ничего ненормального — как известно, любой опыт полезен, а реверс-инжиниринг — не исключение.

Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.

Комментарии (5)


  1. MichaelBorisov
    05.06.2015 22:45

    Спасибо, интересная и полезная статья. Кстати, перехват пакетов путем перехвата WinAPI может быть единственным решением, если сетевое соединение замкнуто в рамках локального компьютера. Передачу таких пакетов WireShark не видит. Локальные соединения часто возникают в случае отладки собственных сетевых приложений.


    1. resetnow
      06.06.2015 00:20

      Можно добавить маршрут для 127.0.0.1 до ближайшего роутера.


      1. Plone
        07.06.2015 12:56

        Что-то мне подсказывает, что адресаты в пределах прямой видимости в подсети не маршрутизируются. И потом, ну отправите вы пакет для 127. наружу, ну прибъет его роутер. Дальше что?


        1. resetnow
          07.06.2015 15:35

          Да, дальше ничего, вы правы, я вот с этим советом wiki.wireshark.org/CaptureSetup/Loopback попутал. Теоретически можно в программе заменить локальный адрес на адрес сетевого адаптера, чтобы таким способом перехватывать, но не думаю, что это всегда возможно.


    1. naum
      06.06.2015 00:57

      RawCap + Cygwin (tail) + WireShark вам в помощь