Understanding x64 support in WPE Pro

    image

    I think that most of the local inhabitants are familiar with the concept of sniffer . Despite the fact that their ultimate goal is the same (intercepting packets that meet certain criteria), they achieve it in completely different ways. Some software listens to the specified network interface (for example, Wireshark , where it is implemented using the Pcap library), and some intercepts the calls of those responsible for interacting with the WinAPI network. Both this and the other method have their pros and cons, however, if the task requires packet capture from a specific well-known application, then the second option is usually more convenient. In this case, there is no need to find out the IP addresses and ports that this program uses (especially considering the fact that there can be quite a lot of them), and you can simply say "I want to intercept all the packets of this application." Convenient, isn't it?

    Perhaps the most popular sniffer to date, working on the principle of intercepting calls to certain WinAPI functions, is WPE Pro. Perhaps many of you have heard about him in various forums devoted to online games, because it is precisely in order to gain advantages in various games that this sniffer is used. He performs his task perfectly, but he has one unpleasant drawback - he does not know how to work with 64-bit applications. It just so happened that for one of the problems that arose, I just needed to intercept packets from a 64-bit application, and I looked in the direction of Wireshark. Unfortunately, using it in this situation was not very convenient - the application under study sent data to different IP addresses, each time opening a new port. Googling a little, I found that there are no ready-made analogues of WPE Pro with x64 support (if they do exist, I would be grateful for the links in the comments - please note that this is about Windows).

    How the process went and what came of it, read under the cut.

    So what needs to be done first? That's right, download WPE Pro itself. You can do this on the official sniffer website , where two versions are offered for download at once - 0.9a and 1.3. We will consider version 0.9a, because it works on the latest versions of Windows.

    Have you downloaded? Now let's check if it is covered with some kind of packer or tread:

    image

    image

    It seems that this time we don’t have to shoot anything. Then we take OllyDbg into our hands and load “WpePro.net.exe”. For example, let's launch the 64-bit version of Dependency Walker and find out why WPE Pro cannot display it in the list of processes available for interception.

    There are two main ways to get the list of current processes in WinAPI:


    If you wish, you can read about the difference between these methods, for example, here .

    We look at the intermodular calls of the WpePro.net.exe module and see that neither CreateToolhelp32Snapshot nor EnumProcesses are here. Perhaps the application gets their address in run-time using the WinAPI function GetProcAddress , so let's look at referenced text strings. This time everything is exactly the opposite - there was found both the line “CreateToolhelp32Snapshot” and “EnumProcesses”. We put the breaks on the places where these lines are accessed, click on the “Target program” button in WPE Pro and look at the place we stopped at:

    image

    If you press F9, then we will see that the same crack works several more times before finally a window appears with a list of current processes. Let's see why Dependency Walker wasn’t in it. Close the window, click on the “Target program” button again and perform step-by-step debugging, starting from the same crack. Shortly after the GetProcAddress calls, we find ourselves in a loop in which the following functions are called sequentially - OpenProcess , EnumProcessModules , which is hidden behind the CALL DWORD PTR DS statement : [EDI + 10] , GetModuleFileNameEx ( CALL DWORD PTR DS instruction : [ECX + 14] ) and CloseHandle :

    image

    Let's put the break at the address0x0042A910 , where the last argument of the OpenProcess function , PID, is pushed onto the stack and try to wait for the moment when the EAX register takes a value equal to the identifier of the Dependency Walker process. On my machine at that moment it was equal to 6600, i.e. 0x19C8.

    If we run through the code, we will see that in the case of Dependency Walker, the TEST EAX, EAX instruction located at 0x0042A942 and following the call of the EnumProcessModules WinAPI function makes the conditional jump operator next to it jump to 0x0042A9E7 , where the call is located Closehandle, which, of course, is not the normal course of the application’s work, because we haven’t even reached the call to the GetModuleFileNameEx function :

    image

    Pay attention to the error code - 0x12B (ERROR_PARTIAL_COPY). Let's see why this can happen. Open the documentation for the EnumProcessModules function and see the following:

    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)

    Yes, this is just our case.

    Despite the fact that the documentation for the GetModuleFileNameEx function does not say anything similar, it behaves in a similar way, as described, for example, here . One of the solutions to this problem is to use the QueryFullProcessImageName function , which will work fine even in case of a call from a 32-bit application running in WOW64, if it is applied to a 64-bit process.

    Now we can flood the call to the EnumProcessModules function

    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
    

    and write code cave to replace GetModuleFileNameEx with the QueryFullProcessImageName function :

    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
    

    Now WPE Pro displays a lot of new processes, including our Dependency Walker:

    image

    However, when you try to attach to this process, WPE Pro gives an unpleasant message for us:

    image

    Here, unfortunately, there is nothing we can do. DLL injection is done by embedding your DLL in the address space of the target process, and the following is said on 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

    It’s easy to guess that WPE Pro only comes with a 32-bit library.

    What to do? Write your WinSock interceptor? Why not?

    But how will we do this? The first thing that comes to mind is to use Detours , however, it is immediately reported on the official website that support for 64-bit applications is available only in Professional Edition:

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

    Then let's try EasyHook . Among other things, this library just allows you to work with 64-bit applications:

    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

    With minor changes to the example shown in the official tutorial , we get a code that intercepts calls to recv and send functions from WinSock and displays intercepted messages byte in hexadecimal format:

    Code of the program that performs DLL injection

    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);
                }
            }
        }
    }
    

    The code of the DLL itself, which will be injected into the target process

    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);
            }
        }
    }
    

    Of course, there is still something to work on:
    • Firstly, there is no interception of WSARecv , WSASend and some other functions that can be used in target applications.
    • Secondly, the lack of a GUI and a "string" representation of intercepted messages
    • etc

    However, the functionality demonstrated here was quite enough to solve the task set for me, and the rest is left to the discretion of the readers.

    Afterword


    Researching and modifying binary files does not always give instant results, as was observed in my previous articles (if interested, read about it here and here ), so sometimes you have to go back to the very beginning and change your approach to solving the problem. But this is absolutely nothing abnormal - as you know, any experience is useful, and reverse engineering is no exception.

    Thank you for your attention, and again I hope that the article was useful to someone.

    Also popular now: