Hacking the Blue Team Village Badge (DC27)

Hello! This year at Def Con 27 (2019), a friend of mine reserved me a Blue Team Village badge. Commas go where your heart desires. It is, in my opinion, the coolest of all the badges I saw at Def Con 27 because of all the cool, useful things it can do. It is a RasPi Zero W, with an attached face board that features a screen and a bunch of buttons (game boy style), and a built-in rechargeable battery pack.

It is a WiFi honeypot (HoneyDB), it records ssh sessions of people attached to it (I got a very cute ssh message from another con-goer), and it is readily hackable to do other things as well. It also had an ad-hoc badge-to-badge thing going on, and if you were near BTV the organizers could push updates to it (properly signed and authenticated). It is also set up very well; it's root password randomizes on start-up and you can find it using the badge's built-in screen and buttons. You can plug a USB cable into the data port on the RasPi Zero W and enable USB Ethernet so you can ssh into it on a wired connection.

One of the help files in the badge's menu said to drop down to a shell. When I did, nothing seemed to happen, it was just a shell and I had no keyboard. After a couple of moments it switched back to the badge menu. So, I decided to do what any gamer with a controller would do: I entered the Konami code using the buttons. Something unlocked!

this is the konami code unlockable add-on.  now that you know how the unlock process works, know that there are a few other items to unlock.

So, after putting it off for quite a while I decided to get down to it and try to hack the badge like I was told to. So let's explore what that was like!

Finding the challenge

Following the procedure from the help file, I connected to the USB Ethernet on the device and SSH'd into the badge.

malachite@xps:~$ ssh -p 22336 root@192.168.7.4
root@192.168.7.4's password: 
Linux MalachiteOS 4.14.52+ #1123 Wed Jun 27 17:05:32 BST 2018 armv6l
root@MalachiteOS:~# ls -alh
total 52K
drwx------  5 root root 4.0K Aug  1 04:28 .
drwxr-xr-x 23 root root 4.0K Jul 31 08:22 ..
-rw-------  1 root root 1.5K Aug  1 04:28 .bash_history
-rw-r--r--  1 root root  570 Mar 12  2018 .bashrc
drwx------  3 root root 4.0K Jul 22 21:43 .cache
-rw-------  1 root root 1.6K Aug 23  2018 .fbtermrc
drwx------  2 root root 4.0K Jul 23 14:18 .gnupg
-r--------  1 root root   32 Aug  1 04:30 passwd.txt
-rw-r--r--  1 root root  148 Mar 12  2018 .profile
-rw-------  1 root root 1.0K Jul 23 13:41 .rnd
-rw-r--r--  1 root root   75 Jan  5  2019 .selected_editor
dr-x------  2 root root 4.0K Jul 31 23:58 .ssh
-rw-r--r--  1 root root   37 Dec 11  2018 .vimrc
root@MalachiteOS:~# cd .gnupg/
root@MalachiteOS:~/.gnupg# ls -alh
total 12K
drwx------ 2 root root 4.0K Jul 23 14:18 .
drwx------ 5 root root 4.0K Aug  1 04:28 ..
-rw------- 1 root root   32 Jul 23 14:18 pubring.kbx
root@MalachiteOS:~/.gnupg# 

The only thing that stood out to me was the .gnupg folder, which contained a keyring. This might get brought up later.

root@MalachiteOS:/# ls -alh
total 91K
drwxr-xr-x  23 root root  4.0K Jul 31 08:22 .
drwxr-xr-x  23 root root  4.0K Jul 31 08:22 ..
-rw-r--r--   1 root root     0 Jul 31 10:10 0
# entries cut for space
-rw-r--r--   1 root root     0 Jul 31 08:22 30
drwxr-xr-x   9  501 staff 4.0K Jul 23 08:27 badge
drwxr-xr-x   2 root root  4.0K Aug 17  2018 bin
drwxr-xr-x   3 root root  3.0K Jan  1  1970 boot
drwxr-xr-x   2 root root  4.0K Jun 27  2018 debootstrap
drwxr-xr-x  14 root root  3.4K Aug  1 04:29 dev
lrwxrwxrwx   1 root root    13 May 18 11:12 dialogrc -> /etc/dialogrc
lrwxrwxrwx   1 root root    19 May 18 10:52 dialogrc-green -> /etc/dialogrc-green
lrwxrwxrwx   1 root root    17 May 18 10:59 dialogrc-red -> /etc/dialogrc-red
drwxr-xr-x 100 root root  4.0K Aug  1 04:30 etc
drwxr-xr-x   8 root root  4.0K Jul 14 05:27 home
drwxr-xr-x  16 root root  4.0K May 25 15:12 lib
drwx------   2 root root   16K Jun 27  2018 lost+found
drwxr-xr-x   2 root root  4.0K Jun 27  2018 media
drwxr-xr-x   3 root root  4.0K Jan  5  2019 mnt
drwxr-xr-x   3 root root  4.0K Jun 27  2018 opt
dr-xr-xr-x  72 root root     0 Jan  1  1970 proc
drwx------   5 root root  4.0K Aug  1 04:28 root
drwxr-xr-x  17 root root   520 Aug  1 04:33 run
drwxr-xr-x   2 root root  4.0K Aug 17  2018 sbin
drwxr-xr-x   2 root root  4.0K Jun 27  2018 srv
-rw-r--r--   1 root root     1 Feb 14 12:23 status_tail.txt
dr-xr-xr-x  12 root root     0 Jan  1  1970 sys
drwxrwxrwt   7 root root  4.0K Aug  1 04:38 tmp
drwxr-xr-x  10 root root  4.0K Jun 27  2018 usr
drwxr-xr-x  12 root root  4.0K Jul 23 03:23 var
root@MalachiteOS:/# 

I didn't know what the 0...30 files were until later: They are the buttons on the face of the BTV badge. Other than that, there is a folder named badge. That looks interesting, we'll look at that after we check status_tail.txt.

root@MalachiteOS:/# cat status_tail.txt 

root@MalachiteOS:/# ls -alh badge
total 36K
drwxr-xr-x  9  501 staff 4.0K Jul 23 08:27 .
drwxr-xr-x 23 root root  4.0K Jul 31 08:22 ..
drwxr-xr-x  2  501 staff 4.0K Jul 23 07:03 addons
drwxr-xr-x  3  501 staff 4.0K Jul 30 15:47 admin
drwxr-xr-x  4  501 staff 4.0K Aug  2  2019 art
drwxr-xr-x  3  501 staff 4.0K Jul 31 07:49 bin
drwxr-xr-x  7 root root  4.0K Jul 31 09:07 data
drwxr-xr-x  2  501 staff 4.0K Jul 22 19:25 deploy
drwxr-xr-x  2  501 staff 4.0K Jul 23 09:01 help
root@MalachiteOS:/#

status_tail.txt was empty, and there's a bunch of folders in badge. data is the only one here owned by root, so I want to see what that's about before I go rooting through other people's stuff.

root@MalachiteOS:/badge# ls -alh data
total 76K
drwxr-xr-x 7 root    root    4.0K Jul 31 09:07 .
drwxr-xr-x 9     501 staff   4.0K Jul 23 08:27 ..
-rw-r--r-- 1 root    root       2 Jul 31 07:54 blingrepeat.txt
-rw-r--r-- 1 root    root      42 Jul 31 10:10 friends.txt
-rw-r--r-- 1 root    root      12 Jul 31 08:11 handle.txt
drwxr-xr-x 2 honeydb honeydb 4.0K Jul 31 08:27 honeydb
-rw-r--r-- 1 root    root      14 Jul 31 19:40 honeyssid.txt
-rw-r--r-- 1 root    root    1.1K Aug  1 00:52 log-all-simple.txt
-rw-r--r-- 1 root    root     387 Jul 31 19:41 log-honeydb-simple.txt
-rw-r--r-- 1 root    root       0 Jul 31 07:54 log-honeydb.txt
-rw-r--r-- 1 root    root       0 Jul 31 08:27 log-honeydhcp.txt
-rw-r--r-- 1 root    root      42 Jul 31 09:07 log-honeyssh-simple.txt
-rw-r--r-- 1 root    root       0 Jul 31 07:54 log-honeyssh.txt
-rw-r--r-- 1 root    root     690 Aug  1 00:52 log-honeywap-simple.txt
-rw-r--r-- 1 root    root    1.4K Aug  1 02:01 log-honeywap.txt
drwx------ 2 root    root     16K Jul 31 07:54 lost+found
drwxr-xr-x 2 root    root    4.0K Jul 31 08:11 messages
drwxr-xr-x 2 cowrie  cowrie  4.0K Jul 31 09:08 ssh
drwxr-xr-x 2 root    root    4.0K Aug  1 00:56 unlocks
root@MalachiteOS:/badge# ls -alh data/unlocks/
total 16K
drwxr-xr-x 2 root root 4.0K Aug  1 00:56 .
drwxr-xr-x 7 root root 4.0K Jul 31 09:07 ..
-rwxr-xr-x 1 root root  337 Aug  1 00:57 easy.sh
-rwxr-xr-x 1 root root  258 Jul 31 15:24 konami.sh
root@MalachiteOS:/badge# cat data/unlocks/konami.sh 
#!/bin/bash

# keep this here to read badge variables
source /badge/bin/badge_vars.sh

# put add-on code here
echo "this is the konami code unlockable add-on.  now that you know how the unlock process works, know that there are a few other items to unlock."
root@MalachiteOS:/badge# cat data/unlocks/easy.sh 
#!/bin/bash

clear

echo "congrats, you've unlocked the easy challenge.  there are 3 other unlock challenges that will require skill, luck, hashing or cracking.

prizes await for the first to unlock the hard, medium and expert challenges.

separate from this contest, there are also several games and utilities to unlock on the badge.
"
root@MalachiteOS:/badge# 

Okay. So those files were what I unlocked when I was pushing buttons. easy.sh was just the 'A' button, and konami.sh was... well, the konami code (Up Down Up Down Left Right Left Right A B Start). There was a contest but I have to assume someone else managed to unlock them during Def Con 27 and claimed their prize at the venue. Anyways, there are other things that I need to unlock, but at this point I am not sure they are all button inputs. Maybe there are some CTF things I need to do around the badge, so let's keep looking around.

Data has all of the user-facing variables, and of course the unlocks. The text tiles contain things like the handle, your HoneyDB scores, friends list, etc.. Basically anything that the badge displays to users. These are all probably handled by other scripts, so messing around with them won't do anything here.

So, typically bin is where all the executables and the like are. I reckon that the button scripts are there, and there might be a clue as to what buttons will unlock other challenges.

root@MalachiteOS:/badge# ls -alh bin
total 308K
drwxr-xr-x 3  501 staff 4.0K Jul 31 07:49 .
drwxr-xr-x 9  501 staff 4.0K Jul 23 08:27 ..          
# many, many entries removed for space
-rwxr-xr-x 1  501 staff 2.5K Jul 31 07:43 button_debug.py
-rwxr-xr-x 1  501 staff 4.1K Jul 31 07:43 button_handler_konami.py
-rwxr-xr-x 1  501 staff 3.3K Jul 31 07:43 button_handler_main_menu.py
-rwxr-xr-x 1  501 staff 3.4K Jul 31 07:43 button_handler_moon-buggy.py
-rwxr-xr-x 1  501 staff 3.4K Jul 31 07:43 button_handler_netris.py
-rwxr-xr-x 1  501 staff 2.9K Jul 31 07:43 button_handler_test.py
# many, many more entries removed for space
root@MalachiteOS:/badge#

There was a load of stuff in the bin folder. More than I expected, honestly. I ended up having to go back and pipe it through less to make it easier to look through them. Most of them were python scripts. I truncated the results above so that I could point out the scripts that caught my eye: button_*.py. I looked at all of them, and button_handler_konami.py is the one that has the booty. I am going to go through that right here.

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import signal
import sys
import subprocess
import hashlib

'''
Bunch of stuff truncated here, setting up the GPIO signaling for the face buttons
'''

global press
press=""

def interrupt_handler(channel):
    global press

    if channel == 19:
        #print("19 - A")
        press+="A"
        time.sleep(.01)
    elif channel == 26:
        #print("26 - START")
        press+="S"
        subprocess.call(['systemctl', 'restart', 'getty@tty1.service'], shell=False)
        time.sleep(.01)
    elif channel == 20:
        #print("20 - SELECT")
        subprocess.call(['/badge/bin/badge_display_pwm.sh'], shell=False)
        time.sleep(.01)
# A bunch more elif commands, setting up the buttons. Followed by the button initialization that reference these inturrupts.

tick = 0
while (tick < 20):
    time.sleep(.25)
    tick = tick + 1

# The rest of the script, continued below.

Alright. So I can tell from opening it that it uses /usr/bin/python and not python3. Then I can see that there is hashlib in there, which tells me there's going to be some authentication or hashing involved. Looking at press and the interrupt_handler(), I can see that whenever you press a button it appends a character to the string press. It's global, so it'll get used everywhere (I don't know if this gives it any other properties).

Now, I truncated the interrupt_handler(), but here's what to take away from it:

  • Each button press adds a letter to string press
  • The list of possible letters that gets added to press are A,S,U,B,L,D,R
  • Start (S) and Select run subprocesses, that may or may not interrupt the rest of the script

From what I can tell about the tick part is that this determines how long you get to start pushing buttons, and if it gets interrupted by a button press, the interrupt sleeps for .01s. Without the button interrupts this gives you about five seconds to input buttons, which is a pretty reasonable amount of time to enter any codes.

What's next?

# welcome.  this should get you started :)
salt = 'XstblibaQNaAWO8dYo:'
codes = {
        '50f9d9597642a0d090d4a613bf81f9d8bc4c7b4ec1db48c9f4abf88225c3cfad' : 'konami',
        'b3a218481d173dcd066a42eb74702714c5bf7f0c77982666c703f0d2b78b4a35' : 'admin',
        'fda91114369fdd643f5ab0c12a808dfbc0d360a1eb2fd3d9a56e66220d000313' : 'debug',
        'df722c019cb312748828b3b547d685448e43092e65d78eae2999d39bd96052b3' : 'moonbuggy',
        '3a868834d0e8d1690020d9fd01793df75496a127d7e82dab8dece423ee0bad5f' : 'netris',
        '90974350053c8ea1fc721da375242d888bde1d5b7c818ab1aa2f5265bcc2eebc' : 'da',
        '1d5420fdff3a2529e89edc307eba788c9b177d8482d49e4f05a3bd71fadf2444' : 'easy', 
        'b62d9205a693b3601ade197944cd39f305c6388dd1b97cff1190b5b7e6801077' : 'medium', 
        '8c30f3cdcf02ec6e0fc8e97f12738be7e2b2f710f5888932da5163a8891f75ea' : 'hard', 
        'f2664042ea3d51d5b94f49c36d5d9892eb9b913fd306b9648e1fd5dcd848932f' : 'expert', 
    }

hashed = hashlib.sha256(salt + press.encode()).hexdigest()

for item in codes:
    if hashed in codes:
        passphrase = salt + press
        addone = '/badge/addons/' + codes[hashed] + '.sh.asc'
        addond = '/badge/data/unlocks/' + codes[hashed] + '.sh'
        
        cmd = '/usr/bin/gpg -q --batch --passphrase ' + passphrase + ' ' + '-d ' + addone + ' > ' + addond
        
        subprocess.Popen([cmd], shell=True)
        
        cmd = 'chmod +x ' + addond
        subprocess.Popen([cmd], shell=True)


        print codes[hashed] + ' unlocked!'
        time.sleep(1)
        break

Aha, here's the booty. Here is where I get to show some of my college education doing authentication and encryption and the like (and also how long it's been). Forgive me any incorrect terms. Let's break down the first half, before the for item in codes bit:

  • The salt is XstblibaQNaAWO8dYo:, and this should be added to any hashing we do. This is like a nonce, which helps prevent dictionary attacks by adding additional randomness to the hashes.
  • codes is a dictionary of salted hashes, next to what appear to be challenge names. Close analogy: usernames and password hashes.
  • The hashes are sha256, and are generated by adding the salt to press and then running it through hashlib.sha256. The hexdigest moves it from a <byte object> in python to a string, which we can actually use later.

So, we happen to know what the value of press will be for two entries, konami and easy. If we add XstblibaQNaAWO8dYo: to A, and then run it through the hashing algorithm, we'll get a hash code that will match easy. For konami, it's XstblibaQNaAWO8dYo: + UUDDLRLRBAS (Up Up Down Down Left Right Left Right B A Start). I am getting a little ahead of myself though, and should probably explain what we'll do with this.

We are going to want to crack the hashes in the table, that is, attempt to find a matching set of words that when put into their hashing function will get us matches. We want to build what's called a rainbow table. We want to do this in an offline attack, so that our brute-forcing attempts are not being run on the device we're trying to break into. So what do we have that I have determined is useful to this goal?

  • We've got a seven character alphabet: A,S,U,B,L,D,R
  • We have the salt: XstblibaQNaAWO8dYo:
  • We have the function used to generate the hashes: hashlib.sha256(salt + press.encode()).hexdigest()
  • We have the list of all correct hashes.

A quick rundown of some cryptography and attacker concepts

Alright, quick run-down of terminology from above before we continue (how I remember/use them, and not what they may actually mean):

  • A salt is like a nonce, which is a number that is used once in encryption. These exist to add randomness.
  • A word is well, a word, that you want to encrypt. This stands in for plaintext which is unencrypted information, or passwords in our case.
  • A rainbow table is a pre-computed table used for cracking password hashes. It's a table filled with words and the resulting hashes in different protocols, like sha256. We will be generating this table by brute-forcing all possible button combinations and finding matches. (For those about to be pedantic, brute-forcing and rainbow tables are different things. Actual rainbow tables are much more complex.)
  • An offline attack is an attack that is not run on/against the active system; often cracking passwords/hashes, or decrypting files
  • An alphabet in encryption is all the possible characters that could be part of a word or plaintext. If we are cracking actual passwords, the alphabet would be a-zA-Z0-9 and maybe a list of special characters.

I guess now that I have explained an alphabet I should take a moment to explain why a seven character is important for us. If our alphabet only contains seven characters, it means each character in a password can only be one of those seven characters. This decreases the amount of time it will take to compute all reasonable possibilities.

An important part about encryption (and hashing is a form of encryption, fight me Scott, pedantic teacher of my security class) is that encryption is meant to be timely, which is to say that it cannot be unbreakable. You want an attacker to spend as much time as possible decrypting/discovering your encrypted/hashed data, so much time that by the time the attacker succeeds the data is irrelevant. Encryption protocols/algorithms must advance as computational power advances to ensure attacks against encrypted data are unreasonable for most attackers.

Below is some math that shows how the size of the alphabet and the width increase computational time by adding an ideally unreasonable amount of possibilities to try.

4 [alphabet] ^ 4 [width] = 256 possible results
4 [alphabet] ^ 7 [width] = 16384 possible results
7 [alphabet] ^ 4 [width] = 2401 possible results
7 [alphabet] ^ 7 [width] = 823543 possible results

Anyways, we need to do the thing. So let's get back to this script, why don't we?

Back to business

The script way above includes how when it finds a matching has it locates the file, decrypts the file, moves it to the proper place on the badge, and then change the permissions so it belongs to root. I could take time to really parse it all out and make it into a script, but I want to push the buttons myself (unless one of the combinations is too long). We're going to be doing a proper attack, but first we should test the functions to make sure our understanding of the inputs is correct.

Let's test something first in REPL. (This fails in python 3 by the way, but succeeds in python 2.7.15+, as noted earlier)

Python 2.7.15+ (default, Nov 27 2018, 23:36:35) 
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> 
>>> salt = 'XstblibaQNaAWO8dYo:'
>>> press=""
>>> press+="A"
>>> codes = {
...         '50f9d9597642a0d090d4a613bf81f9d8bc4c7b4ec1db48c9f4abf88225c3cfad' : 'konami',
...         'b3a218481d173dcd066a42eb74702714c5bf7f0c77982666c703f0d2b78b4a35' : 'admin',
...         'fda91114369fdd643f5ab0c12a808dfbc0d360a1eb2fd3d9a56e66220d000313' : 'debug',
...         'df722c019cb312748828b3b547d685448e43092e65d78eae2999d39bd96052b3' : 'moonbuggy',
...         '3a868834d0e8d1690020d9fd01793df75496a127d7e82dab8dece423ee0bad5f' : 'netris',
...         '90974350053c8ea1fc721da375242d888bde1d5b7c818ab1aa2f5265bcc2eebc' : 'da',
...         '1d5420fdff3a2529e89edc307eba788c9b177d8482d49e4f05a3bd71fadf2444' : 'easy', 
...         'b62d9205a693b3601ade197944cd39f305c6388dd1b97cff1190b5b7e6801077' : 'medium', 
...         '8c30f3cdcf02ec6e0fc8e97f12738be7e2b2f710f5888932da5163a8891f75ea' : 'hard', 
...         'f2664042ea3d51d5b94f49c36d5d9892eb9b913fd306b9648e1fd5dcd848932f' : 'expert', 
...     }
>>> 
>>> hashed = hashlib.sha256(salt + press.encode()).hexdigest()
>>> 
>>> if hashed in codes:
...     print(codes[hashed])
... 
easy
>>> press="UUDDLRLRBAS"
>>> hashed = hashlib.sha256(salt + press.encode()).hexdigest()
>>> if hashed in codes:
...     print(codes[hashed])
...
>>> press="UUDDLRLRBA"
>>> hashed = hashlib.sha256(salt + press.encode()).hexdigest()
>>> if hashed in codes:
...     print(codes[hashed])
... 
konami
>>> 

We understand the inputs. We have also learned that the S in my konami entry earlier was incorrect. That's good to know. This also means we have two known good results that we can use to test against, and one of them is the first possible thing we'll find. Now we just have to build a brute-forcer in python using these bits. We need to make a script that will check every possible combination of letters from our alphabet with an arbitrary width.

I had a lot of difficulty getting it to loop past two characters, but luckily I had help from a friend, @kardonice who helped me out hugely by introducing itertools (which I had tried but didn't understand, and I also tried the wrong thing from itertools), and later multiprocessing to speed this up. Let's see the results of the first pass with the working (single-threaded) version.

Python 2.7.15+ (default, Nov 27 2018, 23:36:35)
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> import itertools as it
>>>
>>> salt = 'XstblibaQNaAWO8dYo:'
>>> codes = {
...         '50f9d9597642a0d090d4a613bf81f9d8bc4c7b4ec1db48c9f4abf88225c3cfad' : 'konami',
...         'b3a218481d173dcd066a42eb74702714c5bf7f0c77982666c703f0d2b78b4a35' : 'admin',
...         'fda91114369fdd643f5ab0c12a808dfbc0d360a1eb2fd3d9a56e66220d000313' : 'debug',
...         'df722c019cb312748828b3b547d685448e43092e65d78eae2999d39bd96052b3' : 'moonbuggy',
...         '3a868834d0e8d1690020d9fd01793df75496a127d7e82dab8dece423ee0bad5f' : 'netris',
...         '90974350053c8ea1fc721da375242d888bde1d5b7c818ab1aa2f5265bcc2eebc' : 'da',
...         '1d5420fdff3a2529e89edc307eba788c9b177d8482d49e4f05a3bd71fadf2444' : 'easy',
...         'b62d9205a693b3601ade197944cd39f305c6388dd1b97cff1190b5b7e6801077' : 'medium',
...         '8c30f3cdcf02ec6e0fc8e97f12738be7e2b2f710f5888932da5163a8891f75ea' : 'hard',
...         'f2664042ea3d51d5b94f49c36d5d9892eb9b913fd306b9648e1fd5dcd848932f' : 'expert',
...     }
>>> alphabet = "ABSURDL"
>>>
>>> def check_match(password):
...     hashed = hashlib.sha256((salt + password).encode()).hexdigest()
...     if hashed in codes:
...         print(password, codes[hashed])
...
>>> for i in range(1, 11):
...    for comb in it.product(alphabet, repeat=i):
...        check_match(''.join(comb))
...
('A', 'easy')
# REDACTED
# REDACTED
# REDACTED
# REDACTED
# REDACTED
('UUDDLRLRBA', 'konami')
>>> exit()

Conclusion

The both the single and multithreaded versions of this script work perfectly. I managed to unlock one additional challenge after setting it to try combinations up to twenty and running it for nine hours on my Dell XPS 13, and only on three cores. I am going to start re-running the script on a more powerful machine, and hopefully I'll unlock the extreme one sometime soon! Special thanks to Munin, who got me my BTV badge and to @Kardonice, who helped me write the repeater.

Support the Author

Devon Taylor (They/Them) is a Canadian python developer, network architect, and consultant. Their blog covers technical problems they encounter and design and media analysis. Their twitter You can support their independent work via Patreon (USD), or directly through Ko-Fi (CAD).