Разбираемся с поддержкой x64 в WPE Pro

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

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

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

image

Обратите внимание на код ошибки — 0×12B (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(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 Queue = new Stack();

public Main (RemoteHooking.IContext InContext, String InChannelName) { Interface = RemoteHooking.IpcConnectClient(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 Однако продемонстрированного здесь функционала было вполне достаточно для решения поставленной передо мной задачей, а остальное уже остаётся на усмотрение читателей.Послесловие Исследование и модификация бинарных файлов не всегда даёт моментальный результат, как это наблюдалось в предыдущих моих статьях (если интересно, читайте об этом тут и тут), так что иногда приходится вернуться в самое начало и изменить свой подход к решению проблемы. Но в этом нет абсолютно ничего ненормального — как известно, любой опыт полезен, а реверс-инжиниринг — не исключение.Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.

© Habrahabr.ru