Covert Timing Channels (CTC) in C++ for Interprocess Communication

2024-10-27

Introduction

Covert Timing Channels (CTC) are a method for transmitting information between processes by manipulating the time it takes to access certain shared resources, such as cache lines. This approach is often explored in security research to understand potential vulnerabilities and how data might be leaked between processes unintentionally.

In this write-up, we'll explore using a custom C++ implementation of CTC to facilitate covert communication between processes. While such channels are typically used to study security risks, they can also be creatively applied to build efficient interprocess communication mechanisms.

What is a Covert Timing Channel?

A Covert Timing Channel uses the time it takes for a process to access a shared resource (like a cache line) to encode and decode information. This means that the timing differences become the "signal" that one process can interpret to receive a message from another. This method is especially intriguing because it doesn't require explicit shared memory, making it challenging to detect.

In this article, we'll cover:

  • The CTC library files (CTC.h and CTC.cpp).
  • How to set up a communication channel between processes.
  • Example code to transmit and receive messages using timing.
  • A working demonstration of the setup.

The CTC Library

CTC.h

1#ifndef CTC_H 2#define CTC_H 3 4#include <intrin.h> 5#include <Windows.h> 6#include <winternl.h> 7#include <cstdint> 8 9namespace CTC { 10 11 constexpr int POSITIVE_THRESHOLD = 75; 12 constexpr int FLUSH_COUNT = 250000; 13 constexpr uint64_t START_MAGIC = 0xBEEFC0DE00000001; 14 constexpr uint64_t END_MAGIC = 0xBEEFC0DE00000002; 15 16#pragma pack(push, 1) 17 union TransmitBlock { 18 uint64_t AsUint64; 19 struct { 20 uint32_t Value; 21 uint16_t ArrayEntry; 22 uint16_t Checksum; 23 } Data; 24 }; 25#pragma pack(pop) 26 27 extern uint8_t* CommunicationLines; 28 extern uint64_t CacheLineSize; 29 30 void Initialize(LPVOID commLines = nullptr); 31 bool TransmitData(uint8_t* data, uint32_t length); 32 bool ReceiveData(uint8_t* data, uint32_t length); 33 34} // namespace CTC 35 36#endif // CTC_H

CTC.cpp

1#include "CTC.h" 2 3namespace CTC { 4 5#define PREFETCH_LINE(n, hint) _mm_prefetch(reinterpret_cast<const char*>(CommunicationLines + (n * CacheLineSize)), hint) 6#define FLUSH_LINE(n) _mm_clflushopt(CommunicationLines + (n * CacheLineSize)) 7 8 uint8_t* CommunicationLines = nullptr; 9 uint64_t CacheLineSize = 0; 10 11 void Initialize(LPVOID commLines) { 12 uint32_t cpuInfo[4]{}; 13 14 __cpuidex(reinterpret_cast<int*>(cpuInfo), 1, 0); 15 CacheLineSize = ((cpuInfo[1] >> 8) & 0xFF) * 8; 16 17 if(!commLines) { 18 CommunicationLines = reinterpret_cast<uint8_t*>(GetModuleHandleA("kernelbase.dll")); 19 } 20 else { 21 CommunicationLines = reinterpret_cast<uint8_t*>(reinterpret_cast<uint64_t>(commLines) & ~(CacheLineSize - 1ull)); 22 } 23 } 24 25 uint64_t MeasureLine_Internal(uint32_t line) { 26 uint32_t junk; 27 uint64_t startTSC = __rdtscp(&junk); 28 junk = *(CommunicationLines + (line * CacheLineSize)); 29 return __rdtscp(&junk) - startTSC; 30 } 31 32 uint64_t MeasureLine(uint32_t lineNumber) { 33 uint64_t total = 0; 34 PREFETCH_LINE(lineNumber, _MM_HINT_T0); 35 36 for(uint32_t i = 0; i < 10; ++i) { 37 total += MeasureLine_Internal(lineNumber); 38 } 39 40 return total / 10; 41 } 42 43 bool IsLinePositive(uint32_t lineNumber) { 44 uint16_t measurements = 0; 45 uint16_t positiveCount = 0; 46 47 while(measurements < 16) { 48 if(MeasureLine(lineNumber) > POSITIVE_THRESHOLD) { 49 ++positiveCount; 50 } 51 ++measurements; 52 } 53 54 return (positiveCount > (measurements / 2)); 55 } 56 57 void SetLinesToUINT64(uint64_t value) { 58 for(uint32_t n = FLUSH_COUNT; n--; ) { 59 for(uint64_t i = 0; i < 64; ++i) { 60 if(value & (1ull << i)) { 61 FLUSH_LINE(i); 62 } 63 } 64 } 65 } 66 67 uint64_t MostFrequentUINT64(uint64_t* values, uint16_t count) { 68 uint64_t result = 0; 69 uint16_t maxOccurrences = 0; 70 71 for(uint16_t i = 0; i < count; ++i) { 72 uint16_t occurrences = 0; 73 for(uint16_t j = 0; j < count; ++j) { 74 if(values[i] == values[j]) { 75 ++occurrences; 76 } 77 } 78 if(occurrences > maxOccurrences) { 79 maxOccurrences = occurrences; 80 result = values[i]; 81 } 82 } 83 84 return result; 85 } 86 87 void GenerateChecksum(TransmitBlock* block, uint16_t* checksum) { 88 constexpr uint32_t CRC_SEED = 0x5596A0B1; 89 uint32_t crc = _mm_crc32_u16(CRC_SEED, block->Data.ArrayEntry ^ ((block->Data.Value & 0xFFFF) + ((block->Data.Value >> 16) & 0xFFFF))); 90 *checksum = ((crc >> 16) & 0xFFFF) ^ (crc & 0xFFFF); 91 } 92 93 uint64_t ConvertLinesToUINT64() { 94 uint64_t samples[16]{}; 95 uint16_t sampleCount = 0; 96 97 while(sampleCount < sizeof(samples)) { 98 for(uint64_t i = 0; i < 64; ++i) { 99 samples[sampleCount] |= (static_cast<uint64_t>(IsLinePositive(i)) << i); 100 } 101 ++sampleCount; 102 } 103 104 return MostFrequentUINT64(samples, sampleCount); 105 } 106 107 void TransmitData_Internal(uint32_t* array, uint32_t size) { 108 SetLinesToUINT64(START_MAGIC); 109 110 for(uint32_t i = 0; i < size; ++i) { 111 TransmitBlock block; 112 block.Data.Value = array[i]; 113 block.Data.ArrayEntry = i; 114 GenerateChecksum(&block, &block.Data.Checksum); 115 SetLinesToUINT64(block.AsUint64); 116 } 117 118 SetLinesToUINT64(END_MAGIC); 119 } 120 121 void ReceiveData_Internal(uint32_t* array, uint32_t size) { 122 while(ConvertLinesToUINT64() != START_MAGIC) {} 123 124 while(true) { 125 uint64_t value = ConvertLinesToUINT64(); 126 if(value == END_MAGIC) break; 127 128 auto* block = reinterpret_cast<TransmitBlock*>(&value); 129 uint16_t checksum; 130 GenerateChecksum(block, &checksum); 131 132 if(checksum == block->Data.Checksum && block->Data.ArrayEntry < size) { 133 array[block->Data.ArrayEntry] = block->Data.Value; 134 } 135 } 136 } 137 138 bool TransmitData(uint8_t* data, uint32_t length) { 139 uint32_t alignedLength = (length + 3) & ~3; 140 auto* buffer = static_cast<uint32_t*>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, alignedLength)); 141 if(!buffer) return false; 142 143 RtlCopyMemory(buffer, data, length); 144 TransmitData_Internal(buffer, alignedLength / 4); 145 HeapFree(GetProcessHeap(), 0, buffer); 146 147 return true; 148 } 149 150 bool ReceiveData(uint8_t* data, uint32_t length) { 151 uint32_t alignedLength = (length + 3) & ~3; 152 auto* buffer = static_cast<uint32_t*>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, alignedLength)); 153 if(!buffer) return false; 154 155 ReceiveData_Internal(buffer, alignedLength / 4); 156 RtlCopyMemory(data, buffer, length); 157 HeapFree(GetProcessHeap(), 0, buffer); 158 159 return true; 160 } 161 162} // namespace CTC

Example: Communication Between Clients

To demonstrate how to use the Covert Timing Channel (CTC) library, let's create a simple example where two clients communicate using shared memory and cache lines.

Overview

In this example, we will have:

  • Client A (Sender): This client allows you to type messages and sends them over the shared memory channel.
  • Client B (Receiver): This client waits for a message and displays it once received.

Prerequisites

Before running the sender and receiver, make sure that:

  • Both clients are using the same shared memory name.
  • The receiver is started before the sender, so it is ready to listen for incoming messages.

Sender (Client A)

This client reads user input, transmits the message over shared memory using the CTC library, and allows you to send multiple messages interactively:

1#include "CTC.h" 2#include <iostream> 3#include <Windows.h> 4#include <string> 5 6constexpr size_t SHARED_MEMORY_SIZE = 4096; 7 8void* CreateOrOpenSharedMemory(const char* name) { 9 HANDLE hMapFile = CreateFileMappingA( 10 INVALID_HANDLE_VALUE, // Use the pagefile 11 NULL, // Default security 12 PAGE_READWRITE, // Read/write access 13 0, // Max size (high-order DWORD) 14 SHARED_MEMORY_SIZE, // Max size (low-order DWORD) 15 name // Name of the mapping object 16 ); 17 18 if (!hMapFile) { 19 std::cerr << "Error creating file mapping: " << GetLastError() << std::endl; 20 return nullptr; 21 } 22 23 void* pBuf = MapViewOfFile( 24 hMapFile, // Handle to the map object 25 FILE_MAP_ALL_ACCESS, // Read/write permission 26 0, // Offset (high-order DWORD) 27 0, // Offset (low-order DWORD) 28 SHARED_MEMORY_SIZE // Number of bytes to map 29 ); 30 31 if (!pBuf) { 32 std::cerr << "Error mapping view of file: " << GetLastError() << std::endl; 33 CloseHandle(hMapFile); 34 } 35 36 return pBuf; 37} 38 39int main() { 40 void* sharedMemory = CreateOrOpenSharedMemory("Global\\CTCSharedMemory"); 41 if (!sharedMemory) { 42 return -1; 43 } 44 45 CTC::Initialize(sharedMemory); 46 47 std::cout << "Enter messages to send (type 'exit' to quit):" << std::endl; 48 std::string message; 49 50 while (true) { 51 std::getline(std::cin, message); 52 53 if (message == "exit") { 54 break; 55 } 56 57 uint32_t length = static_cast<uint32_t>(message.size() + 1); 58 59 if (CTC::TransmitData(reinterpret_cast<uint8_t*>(const_cast<char*>(message.c_str())), length)) { 60 std::cout << "Message sent: " << message << std::endl; 61 } else { 62 std::cerr << "Failed to send message." << std::endl; 63 } 64 } 65 66 std::cout << "Exiting..." << std::endl; 67 return 0; 68}

Receiver (Client B)

This client waits for incoming messages, displays them when received, and remains active until the message is received successfully:

1#include "CTC.h" 2#include <iostream> 3#include <Windows.h> 4#include <thread> 5#include <chrono> 6 7constexpr size_t SHARED_MEMORY_SIZE = 4096; 8 9void* CreateOrOpenSharedMemory(const char* name) { 10 HANDLE hMapFile = CreateFileMappingA( 11 INVALID_HANDLE_VALUE, // Use the pagefile 12 NULL, // Default security 13 PAGE_READWRITE, // Read/write access 14 0, // Max size (high-order DWORD) 15 SHARED_MEMORY_SIZE, // Max size (low-order DWORD) 16 name // Name of the mapping object 17 ); 18 19 if (!hMapFile) { 20 std::cerr << "Error creating file mapping: " << GetLastError() << std::endl; 21 return nullptr; 22 } 23 24 void* pBuf = MapViewOfFile( 25 hMapFile, // Handle to the map object 26 FILE_MAP_ALL_ACCESS, // Read/write permission 27 0, // Offset (high-order DWORD) 28 0, // Offset (low-order DWORD) 29 SHARED_MEMORY_SIZE // Number of bytes to map 30 ); 31 32 if (!pBuf) { 33 std::cerr << "Error mapping view of file: " << GetLastError() << std::endl; 34 CloseHandle(hMapFile); 35 } 36 37 return pBuf; 38} 39 40int main() { 41 void* sharedMemory = CreateOrOpenSharedMemory("Global\\CTCSharedMemory"); 42 if (!sharedMemory) { 43 return -1; 44 } 45 46 CTC::Initialize(sharedMemory); 47 48 uint8_t receivedData[256]; 49 uint32_t length = sizeof(receivedData); 50 51 std::cout << "Waiting for data..." << std::endl; 52 53 while (true) { 54 if (CTC::ReceiveData(receivedData, length)) { 55 std::cout << "Message received: " << reinterpret_cast<char*>(receivedData) << std::endl; 56 } else { 57 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 58 } 59 } 60 61 return 0; 62}

Example Output

Here is an example of what you might see when running the sender and receiver:

  • Sender Console:

    Enter messages to send (type 'exit' to quit):
    Hello, Client B!
    Message sent: Hello, Client B!
    
  • Receiver Console:

    Waiting for data...
    Message received: Hello, Client B!
    

Visualization

Below is a visualization of the data flow between the sender and receiver:

Data Flow Between Sender and Receiver

Summary

This example demonstrates how you can use the Covert Timing Channel (CTC) library to send and receive data between processes using shared memory. The sender reads user input and transmits it, while the receiver waits for the data and displays it once received. By using shared memory and cache lines, this approach provides a low-level mechanism for interprocess communication that can be both efficient and stealthy.