Secure Oldies I : Introducing Windows Sockets and Simple HTTP Client

Secure Oldies I : Introducing Windows Sockets and Simple HTTP Client

When Windows Takes Good Ideas from Unix

Goal

TLS 1.3 has been defined in 2018 and it has been 4 years from the definition, and earlier version TLS 1.2 has been defined one decade earlier at 2008. TLS 1.2 supports on Windows started from Windows 8.1 and Windows 7 Service Pack 1. If you want to access modern website using browsers on Windows XP, for example, it won't work.

Jepretan Layar 2022-05-11 pukul 06.37.01.png

This series aim to create a command line tool to demonstrate ability to bring security to old platforms back to Windows 95 OSR 2 era. The primary goal is just for fun, the secondary goal is maybe for those who has these old operating systems around for mission-critical applications, can use this as a guide to write a secure applications.

Because we want to create a networked application, we'll start by TCP/IP and sockets.

About TCP/IP

Let's start series with short discussion about TCP/IP. The de-facto protocol of the internet. It was invented and built within the ARPA and by fate and momentum becomes the ubiquitous internet we know today. I won't explain this, I'd refer to a popular video directed to laymen video instead, because I'm lazy.

TCP/IP comes in two versions: IPV4 and IPV6. I also won't indulge much on this one. In short, they are different in addressing scheme and there are also different setup which will be relevant for our next discussion point: the sockets API.

Sockets API

In the beginning of the internet there's one API that prevails as the de-facto standard for interprocess communications: The BSD Sockets. Socket is just like a wall-socket: It's a place for two devices to connect and communicate. It abstracts the mechanism of networking between processes and machines. It's first implemented by BSD Unix. It provides an API standard that allows two processes or more to communicate. Sockets is pretty abstract and not limited to networked applications, there's also sockets between user space and kernel space. However, networked applications is the most common use case.

Socket API is pretty ubiquitous nowadays. Not only BSDs like FreeBSD, OpenBSD, or NetBSD. Unix clones like macOS and Linux also implement BSD sockets API. Non Unix operating systems, including Windows also joined the bandwagon since mid-90s because they don't want to be excluded in the internet era.

About Windows Sockets

Windows Sockets is Microsoft Windows implementation of BSD Sockets. Microsoft has released 2 versions of it. Before Windows Sockets there are vendor-specific socket libraries. The first WinSock 2 implementation was available for Windows 95 OSR 2 and Windows NT 4.0. So this will be our oldest target platforms.

Differences between IPV4 and IPV6

Before IPV6 the function to get address information is by using gethostbyname(). Now, it's getaddrinfo() as documented in RFC-2553and POSIX-2001 standard. As we want to support the ultimate ancient platforms who might not have getaddrinfo() on their platform library, we will use gethostbyname() in all of our programs.

Writing Simple HTTP Client

Before we delve into secure socket connection, we'll learn at least how to create a simple TCP/IP socket client by using WinSock. This client will get a simple HTTP payload from httpbin.org. We'll start by creating plaintext HTTP.

Setting Up Projects

Toolchains

There are two toolchains that we can use:

  • We'll be using GNU-based MinGW toolchains. In Windows, we can install them from here and for Linux and macOS, you can install MinGW using your install
  • We also need to install CMake. And add cmake to our PATH. Please refer to the installation guide how to do it.

Writing The Source Code

First we'll create a simple Win32 Console Application main.c.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#include <stdio.h>

int main(int argc, char **argv) {
  // Winsock data 
  WSADATA wsa;

  printf("Starting Up...\n");

  // Result
  int res = WSAStartup(MAKEWORD(2, 0), &wsa);


  printf("Cleaning Up...\n");

  WSACleanup();

  return res;
}

This code just startup and cleanup WinSock, nothing else. Just to test if the winsock works.

If you're cross compiling, you'd create a file called mingw32-w64-x86_64.cmake with this contents.

# Sample toolchain file for building for Windows from an Ubuntu Linux system.
#
# Typical usage:
#    *) install cross compiler: `sudo apt-get install mingw-w64`
#    *) cd build
#    *) cmake -DCMAKE_TOOLCHAIN_FILE=~/mingw-w64-x86_64.cmake ..

set(CMAKE_SYSTEM_NAME Windows)
set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)

# cross compilers to use for C, C++ and Fortran
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)

# target environment on the build host system
set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})


# modify default behavior of FIND_XXX() commands
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Then you'd create a file called CMakeLists.txt with this contents:

cmake_minimum_required(VERSION 3.20.0)
project(binfetch)

set (SOURCES main.c) 

add_executable(binfetch ${SOURCES})
target_link_libraries (binfetch ws2_32)

Building

Create a directory called build inside the current directory. Use this command to generate the makefile and executable.

On Windows

> mkdir build 

> cd build 

> cmake -G "MinGW Makefiles" ..

> migw32-make

On Linux/Mac or if you're using MingW for Windows

$ mkdir build

$ cd build 

$ cmake --toolchain ../mingw-w64-x86_64.cmake ..

$ make

The result will be an executable named binfetch.exe, in which we can run. The result will be like this.

Jepretan Layar 2022-05-11 pukul 09.45.42.png

Preparing structure for hostname resolution.

Hostname resolution in IPV4 is done by using gethostbyname. This will result in struct hostent structure. This structure is defined as:

typedef struct hostent {
  char  *h_name;
  char  **h_aliases;
  short h_addrtype;
  short h_length;
  char  **h_addr_list;
} HOSTENT, *PHOSTENT, *LPHOSTENT;

What we're interested in is the h_addr_list member. This will contains a pointer to a buffer which each of the entry is a struct in_addr*. Honestly, I think the type of this should be void** as it's confusing and by char** it implies that it's a character buffer or array of string which is actually not.

Alright, let's declare an hostent variable on top and get the entries.

struct hostent *entry;
struct in_addr **addr_list, **addr_iter, addr_item;
struct in_addr addr;

static const char httpbin[] = "httpbin.org";

And then show time, we'll get the host entry for the address.

entry = gethostbyname(httpbin);

  if ( entry == NULL ) {
    res = WSAGetLastError();
    goto error;
  }

We'll use goto on this case because we want to bail out when error happens and we don't want to type multiple types. In the bottom of the function, we can put an error label just after return

  WSACleanup();
  return res;

// error handling, printing something before exiting
error:
  printf("Get Winsock error 0x%X!", res);
  WSACleanup();

  return res;

If the call succeeded then we iterate on each of the host entries.

  addr_list = (struct in_addr **) entry->h_addr_list; // (1)
  addr_iter = addr_list; // (2) 

  while (*addr_iter != NULL) { // (3)
    memcpy(&addr_item, *addr_iter, entry->h_length); // (4)
    printf("%s\n", inet_ntoa(addr_item)); // (5) 
    ++addr_iter;  // (6)
  }

So let me explain line by line, because this can be mouthful.

  1. We cast the h_addr_list to a pointer to a struct in_addr pointer.
  2. We use iterator to iterate on the entries.
  3. We check whether it's a NULL if it's null we bail out.
  4. If it's not null then we copy the content of that location to addr_item which will hold our address structure.
  5. We convert addr_item to a string which we can read and then print them to screen.
  6. We advance the pointer once to get next entry.

Until here, the result are like this:

Jepretan Layar 2022-05-11 pukul 10.53.54.png

These are the IP addresses of httpbin.org. Which one we want to choose? We can just pick the first entry for our next activity: connecting and disconnecting.

Prepare the socket and connect to the server.

To connect to a server we'd need to do these steps:

  1. Prepare sockaddr_in using AF_INET as the protocol family, and 80 as the port
  2. Open a socket, using socket call with PF_INET as the protocol family. (http port). We copy the resolved address to sin_addr.
  3. Connect using sockaddr_in which is passed as struct sockaddr *.
  4. Close the socket when done.

On each step, we'll check for error.

  struct sockaddr_in addr;
  //...

  memset(&addr, 0, sizeof(struct sockaddr_in)); // (1)

  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);

  memcpy(&addr.sin_addr, addr_list[0], entry->h_length);

  client = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // (2)

  if (INVALID_SOCKET == client) {
    goto error;
  }

  printf("Socket opened 0x%X\n", client);

  res = connect(client, (struct sockaddr *) &addr, 
    sizeof(struct sockaddr_in)); // (3)

  if (SOCKET_ERROR == res) {
    goto error;
  }

  printf("Connected to %s (%s)\n", inet_ntoa(addr.sin_addr), httpbin);

  res = closesocket(client); // (4)

  if ( SOCKET_ERROR == res ) {
    goto error;
  }

  printf("Socket 0x%X closed\n", client);

After compilation the result is like this:

Jepretan Layar 2022-05-11 pukul 11.40.54.png

This means that our connection is succeeded.

Send request and receive response.

HTTP is request-response text-based protocol. To fetch from httpbin.org, we'll execute GET to /get path. To send this request, according to RFC2616 § 4 is separated line-by line.

  1. First Line is the HTTP message type. This includes the method and path. Example: GET /get HTTP/1.1.
  2. The next line is the headers. As we're using HTTP 1.1, we'd need to put at least two headers.
    • Host: httpbin.org telling the host name of the server. Just in case there are multiple 'virtual hosts' within one single web server, the web server will decide which traffic to serve based on this header.
    • Connection: close we tell the web server to use HTTP 1.0 connection. We close connection on each request-response.
  3. We'll add more headers
    • User-Agent: Retrocoder/1.0. We're telling our software name and version.
    • Content-Length: 0. We don't have content within our body.
    • Accept: application/json. We're asking JSON response.

We'll hard code this request for now.

static const char request [] = 
    "GET /get HTTP/1.1\r\n"
    "Host: httpbin.org\r\n"
    "Connection: close\r\n"
    "User-Agent: Retrocoder/1.0\r\n"
    "Accept: application/json\r\n"
    "Content-Length: 0\r\n"
    "\r\n";

Then we'll send the request to the server using send() function.

res = send(client, request, sizeof(request) - 1, 0);

if ( SOCKET_ERROR == res ) {
    goto error;
}

This will send our request to httpbin.org web server. We substract 1 from request size because we only want the string without the terminating zero.

To receive response we'd need to define receive buffer and total buffer we have. We'll set and MTU (minimum transfer unit) of 1500 and buffer size of 8KB.

#define MTU 1500 
#define BUF_MAX_SIZE (8 * 2014)
  uint8_t buf[MTU];
  char response[BUF_MAX_SIZE]; // 8K buffer

This we define two local variables. We'll use them as the buffer for the data we receive later using recv(). We'll receive the packet using buf and then append the result to response. Before we continue, I'd like to give you the prototype of recv from WinSock API:

int recv(
  [in]  SOCKET s,
  [out] char   *buf,
  [in]  int    len,
  [in]  int    flags
);

The return of this function is bytes receive, so we'd need to keep track of that. We'll use i to track the total bytes we receive.

size_t i = 0;

And then we begin to receive

  do {
    res = recv(client, (char *) buf, MTU, 0); // (1)

    if ( SOCKET_ERROR == res || i + res > BUF_MAX_SIZE ) { // (2)
      break;
    }

    memcpy(response + i, buf, res); // (3)

    i += res; // (4)
  } while (res != 0);  // (5)

  if (res != 0) { // (6)
    goto error;
  }

  printf("Response:\n\n");

  fwrite(response, sizeof(char), i, stdout); // (7)

The explanation is as follows:

  1. We'll receive a packet with maximum size of MTU to buf.
  2. If the function fails or the received result exceeds the BUF_MAX_SIZE (8KB), we bail out.
  3. Copy the response from buf to the response on offset i.
  4. Advance i by the size of received packet.
  5. Loop until res is zero which means that connection closed gracefully.
  6. Go to error handling if we bails out before end of connection.
  7. Write the response to standard out.

Now that our program is complete, here's the complete source code:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#include <stdio.h>
#include <stdint.h>

int main(int argc, char **argv) {
  // Winsock data 
  WSADATA wsa;
  struct hostent *entry;
  struct in_addr **addr_list, **addr_iter, addr_item;
  struct sockaddr_in addr;
  static const char httpbin[] = "httpbin.org";
  static const short port = 80;
  static const char request [] = 
    "GET /get HTTP/1.1\r\n" /* Request Line */
    "Host: httpbin.org\r\n"
    "Connection: close\r\n"
    "User-Agent: Retrocoder/1.0\r\n"
    "Accept: application/json\r\n"
    "Content-Length: 0\r\n"
    "\r\n";
  SOCKET client = INVALID_SOCKET;
  size_t i = 0;

#define MTU 1500 
#define BUF_MAX_SIZE (8 * 2014)

  uint8_t buf[MTU];
  char response[BUF_MAX_SIZE]; // 4K buffer

  printf("Starting Up...\n");

  // Result
  int res = WSAStartup(MAKEWORD(2, 0), &wsa);

  // host entries (this is allocated by gethostbyname) 
  entry = gethostbyname(httpbin);

  if ( entry == NULL ) {
    res = WSAGetLastError();
    goto error;
  }

  printf("Address resolved for %s\n", httpbin);

  addr_list = (struct in_addr **) entry->h_addr_list; // (1)
  addr_iter = addr_list; // (2) 

  i = 0;
  while (*addr_iter != NULL) { 
    memcpy(&addr_item, *addr_iter, entry->h_length); // (3)
    printf("%s\n", inet_ntoa(addr_item)); // (4) 
    ++addr_iter;  // (5)
    ++i;
  }

  printf("%d addresses resolved.\n\n", i);

  memset(&addr, 0, sizeof(struct sockaddr_in));

  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);

  memcpy(&addr.sin_addr, addr_list[0], entry->h_length);

  client = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

  if (INVALID_SOCKET == client) {
    goto error;
  }

  printf("Socket opened 0x%X\n", client);

  res = connect(client, (struct sockaddr *) &addr, 
    sizeof(struct sockaddr_in));

  if (SOCKET_ERROR == res) {
    goto error;
  }

  printf("Connected to %s (%s)\n", inet_ntoa(addr.sin_addr), httpbin);

  res = send(client, request, sizeof(request) - 1, 0);

  if ( SOCKET_ERROR == res ) {
    goto error;
  }

  i = 0;
  do {
    res = recv(client, (char *) buf, MTU, 0);

    if ( SOCKET_ERROR == res || i + res > BUF_MAX_SIZE ) {
      break;
    }

    memcpy(response + i, buf, res);

    i += res;
  } while (res != 0); 

  if (res != 0) {
    goto error;
  }

  printf("Response:\n\n");

  fwrite(response, sizeof(char), i, stdout);

  res = closesocket(client); 

  if ( SOCKET_ERROR == res ) {
    goto error;
  }

  printf("Socket 0x%X closed\n", client);

out:
  printf("Cleaning Up...\n");
  WSACleanup();
  return res;

error:
  printf("Get Winsock error 0x%X!", res);
  WSACleanup();

  return res;
}

And here's the result running on 64-bits Windows 10.

httpclient.gif

Testing on Older Windows

Adjusting the Toolchain to 32-bit.

First we try to run the application for Windows XP, an operating system from 2001. To compile for Windows XP we'd need to change our toolchain file.

As ironic as it is, MinGW doesn't really work for 32-bit Windows nowadays. Especially if your host is Windows 10. So, the solution is just us docker on Windows and use it as a cross compiler.

On macOS and Linux

  1. Copy mingw-w64-x86_64.cmake to mingw-w64-i686.cmake
  2. Edit and change the line who says set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) to set(TOOLCHAIN_PREFIX i686-w64-mingw32).
  3. Run the cmake like follow ``` cmake --toolchain ../mingw-w64-i686.cmake ..

make ```

We'll fix this issue by using even older toolchain called OpenWatcom. But for now, we'll stick on using MinGW first.

Here's the test using Windows XP Service Pack 3

xptest.gif

and then we run it on Windows 2000 SP 4.

win2ktest.gif

So, this software is able to run on top of 23 years old operating system. There's one problem tho: because Windows 7 SP 1 was the earliest to support TLS 1.2 which is mandatory to access secure HTTP, we cannot use the TLS layer from Windows' built-in SChannels. We'll be using mbedTLS as the TLS library on top of Winsock.

Trivia and Exercise

The code on main.c is actually pretty portable. The only platform specific code is the Windows Header and Winsock startup and shutdown. Can you make it portable so it can be compiled on Linux and macOS?

Here's the hint

  • Look at the definition of sockets structures from the manpages.
  • Linux define __linux__ on their headers and macos defines DARWIN on the compiler, and Windows defines WIN32.