← Back to blog

Building a C2 on Raw Sockets: Why Krait Doesn't Use WinHTTP

· Krait Team · 6 min read

Almost every Windows C2 framework uses WinHTTP or WinINet for HTTPS communication. These APIs are convenient — a few function calls get you a TLS connection with certificate validation, proxy support, and HTTP/1.1 framing. The OS handles the complexity.

Krait doesn’t use either of them. Every HTTPS connection goes through raw Winsock sockets with Schannel for TLS. This isn’t a preference for difficulty — it’s a requirement imposed by our sleep encryption architecture. WinHTTP is fundamentally incompatible with the goal of zero cleartext in memory during sleep.

The WinHTTP Problem

WinHTTP maintains internal state that the caller cannot access or control. When you call WinHttpSendRequest and WinHttpReceiveResponse, the library allocates buffers, caches headers, stores the response body, maintains connection pools, and keeps TLS session tickets — all in memory regions that belong to WinHTTP, not to your code.

This matters because Krait encrypts all of its memory during sleep. The sleep encryption triad covers code (.text), data (global config), heap blocks, and the stack. But WinHTTP’s internal buffers are none of these — they’re opaque allocations managed by a system DLL, invisible to the implant’s encryption logic.

After a poll cycle, Krait zeroes and frees every buffer it owns. But WinHTTP has already copied the C2 response into its own connection cache. The session handle holds a reference to the connection pool, which holds cached TLS state, which holds decrypted response fragments. The implant calls WinHttpCloseHandle, but the library’s cleanup is lazy — memory is recycled, not zeroed, and the connection pool may survive handle closure if keep-alive is active.

The result: a memory forensics examiner (or an EDR with memory scanning) can find cleartext C2 URLs, HTTP headers, and response fragments in WinHTTP’s internal buffers even after the implant has encrypted its own memory and entered sleep. The sleep encryption triad encrypts everything the implant controls, but it can’t encrypt what WinHTTP owns.

This isn’t theoretical. Early versions of Krait used WinHTTP, and memory analysis of a sleeping implant consistently recovered the poll URL from WinHTTP’s cached connection state. Encrypting the implant’s copy of the URL is meaningless when WinHTTP keeps its own copy in cleartext.

The Raw Socket Approach

Krait builds HTTP requests manually on raw Winsock sockets. The implant owns every buffer at every stage:

1. Allocate a buffer for the HTTP request
2. Write headers + body into the buffer
3. Encrypt with Schannel (SChannel EncryptMessage)
4. Send encrypted bytes over the socket (Winsock send)
5. Receive encrypted bytes (Winsock recv)
6. Decrypt with Schannel (DecryptMessage)
7. Parse the HTTP response from the decrypted buffer
8. Copy the payload out of the response buffer
9. SecureZeroMemory the request buffer, response buffer, decrypt buffer
10. HeapFree all buffers

Every buffer that ever contained cleartext data is explicitly zeroed before being freed. No system library holds a secondary copy. No connection pool caches decrypted fragments. When the implant enters sleep, there is genuinely nothing left in process memory containing C2 communication artifacts.

The socket itself is closed before sleep — no persistent connection. Each poll cycle opens a fresh TCP connection, performs the TLS handshake, sends one request, receives one response, and tears everything down. This eliminates connection-pool state and TLS session caching entirely.

Schannel TLS

TLS is handled through Windows’ built-in Schannel SSPI provider, the same TLS implementation that Internet Explorer, Edge, and WinHTTP use internally. The implant calls AcquireCredentialsHandle with UNISP_NAME to get a Schannel credential, then InitializeSecurityContext to perform the TLS handshake over the raw socket.

Once the handshake completes, EncryptMessage and DecryptMessage handle the record-layer encryption. The implant sends and receives TLS records directly — there’s no HTTP library between the application and the TLS layer.

This means Krait’s TLS traffic is indistinguishable from any other Schannel-based HTTPS client at the network level. The TLS fingerprint (cipher suites, extensions, curve preferences) matches the host OS’s Schannel configuration, not a custom TLS implementation or a hardcoded cipher list. JA3/JA4 fingerprinting sees a legitimate Windows TLS client, because that’s exactly what it is.

Self-signed certificates are handled by passing ISC_REQ_MANUAL_CRED_VALIDATION during the handshake and implementing custom validation in the implant — accept self-signed certs for the configured C2 domain, reject everything else. This avoids the certificate-store manipulation that some tools require (adding a root CA, which is both noisy and requires admin privileges).

What This Costs

Raw sockets are harder to work with than WinHTTP. The implant needs to:

  • Build HTTP/1.1 requests byte by byte (request line, headers, content-length, body)
  • Parse HTTP responses (status line, chunked transfer encoding, content-length)
  • Handle TLS handshake state machine (potentially multiple round trips)
  • Manage buffer sizing (Schannel’s EncryptMessage has specific size requirements for the header, trailer, and padding)
  • Handle connection errors, partial reads, and TLS renegotiation

This is roughly 800 lines of C that WinHTTP handles in a single function call. It’s error-prone code that had to be carefully tested against edge cases — chunked responses, large payloads, servers that send TCP segments at odd boundaries, TLS record fragmentation.

The alternative was accepting that sleep encryption would always leak network IOCs through WinHTTP’s internal state. For a tool that markets itself on memory evasion, that tradeoff was unacceptable. If the sleep encryption triad claims zero cleartext in memory, the network layer can’t be the exception.

The Design Principle

The raw socket decision reflects a broader architectural principle in Krait: own every buffer that touches sensitive data. Wherever possible, avoid passing C2-related data through system APIs that maintain opaque internal state.

This principle extends beyond networking. Krait’s API hash resolution walks the PEB export tables directly instead of calling GetProcAddress (which touches loader internal structures). BOF output uses implant-managed buffers instead of stdout handles. DNS resolution in the DNS transport uses raw UDP sockets instead of DnsQuery_A (which loads dnsapi.dll and touches the system resolver cache).

Each of these choices adds implementation complexity but removes an opaque state store from the process memory. The cumulative effect is that the implant’s memory footprint is fully enumerable — every buffer that contains sensitive data is tracked and encrypted during sleep, because the implant allocated every one of them.

This is the kind of architectural decision that doesn’t show up in a feature comparison table. “HTTPS transport” looks identical whether it’s built on WinHTTP or raw sockets. The difference only matters when you examine what happens to process memory between polls — and that’s exactly where modern detection lives.