C/C++ Windows Networking

When debugging networking applications in Windows it is useful to be able to see what is actually going on. We can take advantage of the fact that you can place a DLL in the same directory as an executable and it will load that one instead of the official one.

So we make our own wsock32.dll, drop it next to the program we want to watch, and it loads ours. Ours loads the real one from the system folder, forwards every call straight through to it, and prints the name of each function as it goes past. The program talks to the network exactly as before; we just get to see every WinSock call it makes.

WinSock

WinSock is the Windows sockets library, the Berkeley sockets API wrapped up for Windows and shipped as wsock32.dll. It is how a 32-bit Windows program of this era opens a TCP or UDP connection and sends bytes over it. The names are the familiar ones: socket, connect, send, recv, closesocket, plus a WSAStartup and WSACleanup pair that Windows adds to start and stop the library, and the WSAAsync… functions that deliver results as window messages.

A small client that fetches something over HTTP looks like this:

WSADATA wsa;
WSAStartup(MAKEWORD(1, 1), &wsa);        //  start WinSock 1.1

SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr;
addr.sin_family      = AF_INET;
addr.sin_port        = htons(80);         //  port 80, in network byte order
addr.sin_addr.s_addr = inet_addr("193.82.0.1");
connect(s, (struct sockaddr *) &addr, sizeof(addr));

send(s, request, length, 0);              //  send the request
recv(s, buffer, sizeof(buffer), 0);       //  read the reply

closesocket(s);
WSACleanup();
A minimal WinSock client

Every one of those calls - WSAStartup, socket, htons, inet_addr, connect, send, recv, closesocket, WSACleanup - is an exported function in wsock32.dll. That is what we are going to stand in front of.

Windows DLLs

A DLL is a library of functions that a program links to at run time instead of at compile time. When the program is built, each DLL function it uses is recorded as an import: the name of the DLL and either the name or the ordinal number of the function inside it. When the program starts, Windows loads each DLL named in that table and fills in the real addresses.

The useful part here is the search order. When a program imports wsock32.dll by name, Windows looks for it in the application's own directory first, and only then in the system directory. So a file called wsock32.dll sitting next to the .exe is found before the real one in c:\windows\system. The program ends up loading ours without knowing any different.

A DLL has one special function, DllMain, which Windows calls when the DLL is attached to a process and again when it is detached. That is where we do our setup. On attach we load the real library and find all of its functions; on detach we tidy up:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD reason, LPVOID lpvReserved) {

  if(reason == DLL_PROCESS_ATTACH) {

    HINSTANCE ws_inst = LoadLibrary("c:\\windows\\system\\wsock32.dll");

    real_connect = RealImport(ws_inst, "connect");   //  4
    real_send    = RealImport(ws_inst, "send");      //  19
    real_recv    = RealImport(ws_inst, "recv");      //  16
    //  ...and so on for every function

  }
  return TRUE;
}
DllMain loads the real DLL and looks up each function (trimmed)

RealImport is a small wrapper around GetProcAddress that pops up a message box if a function is missing, so a broken build shows itself straight away. Each address goes into a real_… pointer, one per function, declared at the top of the file:

static int (WINAPI FAR *real_connect)();   //  the real connect, filled in by DllMain
One pointer to hold the real function's address

Writing the hook functions

Each function we export has to do two things: note that it was called, then behave exactly like the real one. The trick is to not get in the way of the arguments at all. We do that with a naked function and a jump:

__declspec(naked) __declspec(dllexport) WINAPI FAR connect () {
  abc("connect");             //  log the name
  _asm jmp [real_connect];    //  jump straight into the real one
}
One hook function: log the name, then jump to the real function

__declspec(naked) tells the compiler to add no prologue or epilogue, so the function does nothing to the stack on the way in. When the program calls connect, the arguments and the return address are sitting on the stack exactly as the program left them. We log the name, then jmp - not call - into the real connect. It runs with those same arguments, and when it returns it returns straight to the original caller, as if we were never there.

Because we only jump, the hook never touches the arguments and never needs to know how many there are or what they mean. The same shape works for every function in the library, which is what makes it worth generating them automatically.

Logging is deliberately crude. abc grabs the screen's device context and draws the text in the top-left corner with TextOut, wrapped in a critical section so two threads don't draw at once:

void abc(char * p) {
  HDC dc;
  int i;
  EnterCriticalSection(&abc_critical);
  dc = GetDC(NULL);                    //  the whole screen
  for(i = 0; p[i]; i++);               //  strlen
  TextOut(dc, 0, 0, p, i);             //  draw at the top-left corner
  ReleaseDC(NULL, dc);
  LeaveCriticalSection(&abc_critical);
}
The logger draws each name straight onto the screen

Generating the DLL

There are around eighty functions in wsock32.dll, and the pointer, the lookup and the hook for each one are identical bar the name. Writing them by hand would be dull and easy to get wrong, so they are generated. A Perl script, makehookvc.perl, reads a list of functions and writes the C file, along with the linker files needed to export them.

The list lives in exports.txt, one function per line, each with its ordinal number. It was taken from wsock32n.dll, because, as the file's own header notes, the main wsock32.dll does not seem to export any names to read:

//  Ordinal given here is one less than the one it should be declared with!
//  these are from wsock32n.dll because wsock32.dll doesn't seem to export
// any names...

0000 accept
0001 bind
0002 closesocket
0003 connect
...
03e8 WsControl
03e9 closesockinfo
03ea Arecv
03eb Asend
exports.txt: the ordinal in hex, then the name (trimmed)

The script writes the hook functions in a single loop over that list:

while(@exports) {
  $exp = pop(@exports);
  print CFILE "__declspec(naked) __declspec(dllexport) WINAPI FAR ".&getname($exp)." () {\n";
  print CFILE "  abc(\"".&getname($exp)."\");\n";
  print CFILE "  _asm jmp[real_".&getname($exp)."];\n";
  print CFILE "}\n\n";
}
code_c writes one naked forwarding function per export

The same list drives three more passes: the real_… pointer declarations, the RealImport calls inside DllMain, and the export tables. There is a wrinkle in the ordinals. The numbers in exports.txt are one short, so the script adds one back when it needs the real value:

sub getordinal {
  local($exp) = @_;
  local($ord, $name);
  ($ord, $name) = split(/ /, $exp);
  $ord =~ s/([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])/hex($1)/eg;
  return $ord + 1;
}
Reading the hex ordinal and correcting the off-by-one

The ordinals matter because wsock32.dll has no names to import by, so programs import its functions by number. For our stand-in to work, each function has to sit at exactly the same ordinal as it does in the real DLL. That is what the generated .def file (for Visual C) pins down:

EXPORTS
accept @1
bind @2
closesocket @3
connect @4
...
WSAStartup @115
WsControl @1001
wshookvc.def: each function exported at its own ordinal

The Watcom build gets the same information in its own format in the .lbc file, mapping each name and ordinal onto the generated function.

The list is not just the documented WinSock calls. It also carries the undocumented ones a program might reach for by ordinal - WsControl, closesockinfo, Arecv, Asend, WSHEnumProtocols, WSApSetPostRoutine, dn_expand and the rest. We don't need to know what they do; the point is only to forward them, so they are generated exactly like the others. If one of them were missing from our DLL, a program that imported it by ordinal would fail to start.

What the generated hooks actually do, and how you would make them do more, is in Uses below.

Building

The DLL is a single C file, wshookvc.c, built with its matching export file and named wsock32.dll. With Visual C you compile wshookvc.c and link it with wshookvc.def so the exports land on the right ordinals; the Watcom build uses wshookvc.lbc instead. It links against the normal Windows libraries for GetProcAddress, TextOut and MessageBox. Because the hooks use __declspec(naked) and inline _asm, this is x86-only and needs a compiler that supports both.

Deployment

Deployment is just copying the file. Put the built wsock32.dll in the same directory as the program you want to watch and run the program normally. Windows finds our DLL before the real one, loads it, and from then on every WinSock call the program makes passes through our hooks first. To stop watching, delete the file; the program goes back to loading the real library from the system directory.

Nothing about the target program changes, and it needs no cooperation - it doesn't know it has been hooked. This only redirects that one program, because only that directory has the extra file; everything else on the machine still loads the real wsock32.dll.

What it looks like

The output is live rather than a log file. As the program runs, each WinSock call draws its name in the very top-left corner of the screen, each one over the last, so you see the names flicker past as the program works. Running the little HTTP client above you would see WSAStartup, then socket, htons, inet_addr, connect, send, recv and closesocket flash up in turn, and WSACleanup at the end. If a function fails to import when the DLL loads, a message box appears first, titled LKS c:\windows\system\wsock32.dll hook, naming the function.

So a screenshot would be a Windows 98 desktop with a few pixels of small black text tucked into the extreme top-left corner - a single WinSock function name like connect - with the hooked application's own window sitting below it.

Uses

By itself the DLL does two things: it forwards every call to the real library so the program keeps working, and it logs the name of each call. The forwarding is the part you leave alone. The logging is one example of putting code in the path of a call, and the hook function is where any such code goes. Two things are worth doing there.

Logging

This is what it does now. Each hook calls abc with its own name, and abc draws that name on the screen. It is enough to see the sequence of calls, but it throws away the arguments and it is gone as soon as the next call overwrites it.

To keep the log, change abc alone; the hooks don't need touching. Open a file once and append a line per call:

void abc(char * p) {
  FILE *fp = fopen("c:\\wshook.log", "a");
  fprintf(fp, "%s\n", p);
  fclose(fp);
}
Logging to a file instead of the screen

To log what a call was given as well as its name, that particular hook has to read its arguments, which means it can no longer be the one-size-fits-all naked jump - see below.

Rewriting URLs

Watching a call is easy because the hook never has to understand it. Changing one is more work, because now the hook has to read the arguments, alter them, call the real function itself and return what it returns. It can't be naked and it can't just jmp; it needs the real function's prototype so the compiler lays the arguments out properly.

Rewriting a URL means catching the call that carries it - the HTTP request goes out through send, so that is the one to replace - finding the address in the buffer and swapping it before passing it on:

int WINAPI FAR send (SOCKET s, char *buf, int len, int flags) {
  //  look for the old URL in buf and write the new one in its place,
  //  adjusting len if the length changed
  return real_send(s, buf, len, flags);   //  forward the edited request
}
A hook that edits the request before forwarding it

This is how the earlier version pointed http://www.roland.co.uk/cgi-bin/ at a copy on another server: the same stand-in DLL, but with the relevant hook rewriting the address on its way through instead of only noting that it went past. Everything else - loading the real DLL, the ordinals, the deployment - stays exactly the same.

Source 5 files