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.
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 ourPATH
. 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.
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.
- We cast the
h_addr_list
to a pointer to astruct in_addr
pointer. - We use iterator to iterate on the entries.
- We check whether it's a
NULL
if it's null we bail out. - If it's not null then we copy the content of that location to
addr_item
which will hold our address structure. - We convert
addr_item
to a string which we can read and then print them to screen. - We advance the pointer once to get next entry.
Until here, the result are like this:
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:
- Prepare
sockaddr_in
usingAF_INET
as the protocol family, and80
as the port - Open a socket, using
socket
call withPF_INET
as the protocol family. (http port). We copy the resolved address tosin_addr
. - Connect using
sockaddr_in
which is passed asstruct sockaddr *
. - 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:
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.
- First Line is the HTTP message type. This includes the method and path. Example:
GET /get HTTP/1.1
. - 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.
- 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:
- We'll receive a packet with maximum size of
MTU
tobuf
. - If the function fails or the received result exceeds the
BUF_MAX_SIZE
(8KB), we bail out. - Copy the response from
buf
to theresponse
on offseti
. - Advance
i
by the size of received packet. - Loop until
res
is zero which means that connection closed gracefully. - Go to error handling if we bails out before end of connection.
- 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.
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
- Copy
mingw-w64-x86_64.cmake
tomingw-w64-i686.cmake
- Edit and change the line who says
set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
toset(TOOLCHAIN_PREFIX i686-w64-mingw32)
. - 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
and then we run it on Windows 2000 SP 4.
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 definesDARWIN
on the compiler, and Windows definesWIN32
.