Думаю, что большинство из местных обитателей знакомы с понятием сниффера. Несмотря на то, что конечная цель у них одна и та же (перехват пакетов, соответствующих определённым критериям), достигают они её совершенно разным образом. Какой-то софт слушает указанный сетевой интерфейс (например, 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.
Скачали? Теперь давайте проверим, не накрыт ли он каким-нибудь паковщиком или протектором:
Похоже, на этот раз снимать нам ничего не придётся. Тогда берём в руки OllyDbg и загружаем «WpePro.net.exe». Давайте для примера запустим 64-битную версию Dependency Walker'а и узнаем, почему WPE Pro не может отобразить его в списке доступных для перехвата процессов.
Получить список текущих процессов в WinAPI можно двумя основными путями:
- При помощи связки функций CreateToolhelp32Snapshot, Process32First и Process32Next (пример можно найти, например, тут)
- При помощи функции EnumProcesses (пример можно найти, например, тут)
При желании можете почитать о разнице между данными способами, например, тут.
Смотрим на intermodular calls модуля «WpePro.net.exe» и видим, что ни CreateToolhelp32Snapshot, ни EnumProcesses тут нет. Возможно, приложение получает их адрес в run-time при помощи WinAPI-функции GetProcAddress, так что давайте посмотрим на referenced text strings. На этот раз всё с точностью наоборот — нашлась как строка «CreateToolhelp32Snapshot», так и «EnumProcesses». Ставим бряки на места, где происходит обращение к данным строкам, нажимаем на кнопку «Target program» в WPE Pro и смотрим на место, на котором мы остановились:
Если понажимать F9, то мы увидим, что этот же бряк срабатывает ещё несколько раз, прежде чем наконец появится окно со списком текущих процессов. Давайте посмотрим, почему в нём не оказалось Dependency Walker'а. Закрываем окно, снова нажимаем на кнопку «Target program» и выполняем пошаговую отладку, начиная с того же самого бряка. Вскоре после вызовов GetProcAddress мы попадаем в цикл, в котором последовательно вызываются следующие функции — OpenProcess, EnumProcessModules, скрывающаяся за инструкцией CALL DWORD PTR DS:[EDI+10], GetModuleFileNameEx (инструкция CALL DWORD PTR DS:[ECX+14]) и CloseHandle:
Давайте поставим бряк по адресу 0x0042A910, где происходит занесение на стек последнего аргумента функции OpenProcess — PID, и попробуем дождаться момента, когда регистр EAX примет значение, равное идентификатору процесса Dependency Walker'а. На моей машине в этот момент он был равен 6600, т.е. 0x19C8.
Если пробежаться по коду, то мы увидим, что в случае Dependency Walker'а инструкция TEST EAX,EAX, находящаяся по адресу 0x0042A942 и следующая за вызовом WinAPI-функции EnumProcessModules, заставляет следующий за ней оператор условного перехода прыгнуть по адресу 0x0042A9E7, где находится вызов CloseHandle, что, разумеется, не является нормальным ходом работы приложения, ведь мы ещё даже не дошли до вызова функции GetModuleFileNameEx:
Обратите внимание на код ошибки — 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:
Однако при попытке приаттачиться к данному процессу WPE Pro выдаёт неприятное для нас сообщение:
Тут мы уже, к сожалению, ничего не можем поделать. 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
Однако продемонстрированного здесь функционала было вполне достаточно для решения поставленной передо мной задачей, а остальное уже остаётся на усмотрение читателей.
Послесловие
Исследование и модификация бинарных файлов не всегда даёт моментальный результат, как это наблюдалось в предыдущих моих статьях (если интересно, читайте об этом тут и тут), так что иногда приходится вернуться в самое начало и изменить свой подход к решению проблемы. Но в этом нет абсолютно ничего ненормального — как известно, любой опыт полезен, а реверс-инжиниринг — не исключение.
Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.