I've been attending Defcon for over a decade now, with a group of friends I participate in contests with. This is the first year where I used LLMs heavily to help solve challenges. I thought I'd share a few of my favorite "vibe coded" solutions from a contest we placed second in this year (SpyVSpy). The challenges contain codewords or "flags" that need to be found, which are then submitted for points.

This particular contest had a variety of challenges, many involving steganography and cryptography.

GitHub Copilot on VS Code was used heavily during the competition. About 34 different throwaway Python scripts were generated to analyze and solve challenges. This post contains a write-up on a few of my favorites.

vscode screenshot

Strange QR Code

Challenge text:

During a routine sweep of the casino floor, Agent Vega spotted something odd: a QR code sticker slapped onto the back of a restroom door, right above the handle. At first, it seemed like a typical ad — “Scan for VIP rewards!” — but on closer inspection, the QR pattern was odd and did not scan properly. Our analysts believe it’s an intentionally corrupted QR code, designed to embed a hidden message or codeword that only someone trained can extract. We need you to examine the data and isolate the embedded string.

StrangeQRCode.png

From the image, we can see that in addition to black and white, there are six other colors. I first asked copilot to extract a list of colors from the image and it wrote this function:

def get_image_colors(image_path, max_colors=10):
    """Get the most common colors in an image."""
    try:
        img = Image.open(image_path)
        if img.mode != 'RGB':
            img = img.convert('RGB')

        # Get all unique colors and their counts
        colors = img.getcolors(maxcolors=256*256*256)

        if colors is None:
            print("Too many colors to analyze")
            return []

        # Sort by frequency (most common first)
        colors.sort(key=lambda x: x[0], reverse=True)

        print(f"\nMost common colors in {os.path.basename(image_path)}:")
        for i, (count, color) in enumerate(colors[:max_colors]):
            hex_color = rgb_to_hex(color)
            print(f"  {i+1}. {hex_color} (RGB: {color}) - {count} pixels")

        return colors[:max_colors]

    except Exception as e:
        print(f"Error analyzing image: {e}")
        return []

Resulting output:

Analyzing original image colors...

Most common colors in StrangeQRCode.png:
  1. #000000 (RGB: (0, 0, 0)) - 323712 pixels
  2. #ffffff (RGB: (255, 255, 255)) - 273132 pixels
  3. #0000ff (RGB: (0, 0, 255)) - 75648 pixels
  4. #ff00ff (RGB: (255, 0, 255)) - 72000 pixels
  5. #ff0000 (RGB: (255, 0, 0)) - 66240 pixels
  6. #00ff00 (RGB: (0, 255, 0)) - 64320 pixels
  7. #ffff00 (RGB: (255, 255, 0)) - 61072 pixels
  8. #00ffff (RGB: (0, 255, 255)) - 61056 pixels

After my initial analysis, I used the following prompt to update my script and solve the problem:

Update the script to generate every possible combination of 6 bits (64 total). A 1 corresponds to color #FFFFFF, 0 corresponds to color #000000.

Apply substitutions to the following colors (the first bit affects the first color, the second bit affects the second color, and so on for all 6 colors):

#0000ff 
#ff00ff
#ff0000
#00ff00
#ffff00
#00ffff

For example, for 00111100 change:
  #0000ff to black, 
    #ff00ff to black, 
    #ff0000 to white, 
    #00ff00 to white, 
    #00ff00 to white, 
    #00ff00 to white, 
    #ffff00 to black, 
    #00ffff to black. 

This process should result in 64 different images, which are QR codes.  Attempt to decode them and dump the value.

Here is a snippet of the script it generated:

Output:

================================================================================
SUMMARY
================================================================================
Total combinations processed: 64
Successfully generated images: 64
Successfully decoded QR codes: 3
Success rate: 4.7%

--------------------------------------------------------------------------------
SUCCESSFULLY DECODED QR CODES:
--------------------------------------------------------------------------------
Pattern 000111 (dec:  7): 'Gr3y'
Pattern 011010 (dec: 26): 'd1th'
Pattern 110001 (dec: 49): 'Mer3'

--------------------------------------------------------------------------------
FAILED TO DECODE (61 patterns):
--------------------------------------------------------------------------------
  000000  000001  000010  000011  000100  000101  000110  001000
  001001  001010  001011  001100  001101  001110  001111  010000
  010001  010010  010011  010100  010101  010110  010111  011000
  011001  011011  011100  011101  011110  011111  100000  100001
  100010  100011  100100  100101  100110  100111  101000  101001
  101010  101011  101100  101101  101110  101111  110000  110010
  110011  110100  110101  110110  110111  111000  111001  111010
  111011  111100  111101  111110  111111

Actual flag: Mer3d1thGr3y. In past years, this is something that would probably have taken me about two hours to research all the libraries I'd need and code a solution in python. It took about 20 minutes to vibe code a solution.

Ransom Note

Challenge text:

Security at Casino Royale intercepted a message delivered to the VIP manager’s office — a crudely assembled ransom note, cut from casino brochures, playing cards, and betting slips. While the staff panicked over the missing high roller, Agent Ledger suspected the note itself was the real message. Gray Faction operatives often embed ciphers inside ransom notes. We’ve transcribed the ransom note text and marked the key elements below. Crack it, and we’ll know what password to give at the private room.

Screenshot of PDF file containing ransom note:

Ransomnote PDF

I looked at this for about 30 minutes, and even had GPT 5 take a look:

Here’s what I checked so far:

Pulled the PDF’s raw bytes and structure, scanned for embedded files, JavaScript, open actions, optional content layers, object streams, and XObjects (images).

Parsed each page’s content streams looking for “invisible text” tricks like white text (rg 1 1 1), invisible rendering (Tr 3), or hidden strings in Tj/TJ.

Extracted metadata and searched for extra data after the final %%EOF (a common hiding spot).

What I found:

No embedded files, JavaScript, open actions, or optional content groups.

No images/XObjects to extract.

No white-colored text, no invisible text rendering, and no suspicious strings tucked in content streams.

Nothing appended after the final %%EOF.

The only visible message is the one you saw.

I saved a concise scan report you can open: Download the JSON report.

If you want, I can go one level deeper and brute-decode any compressed object streams, then diff visible text vs. raw text to catch obfuscated characters or zero-width spaces. I can also sweep for acrostics or index-based patterns (e.g., “every nth letter”, line/word indices). Want me to run that next?

No immediate help there, but I eventually had the idea to extract the text properties for the various characters in the note (which I had copilot do).

Output:

============================================================
RANSOM NOTE PDF ANALYSIS
============================================================
=== PyMuPDF Analysis ===

Page 1:
----------------------------------------
Text: 'T'
  Font: Tahoma
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (72.0999984741211, 184.67501831054688, 87.44459533691406, 216.38893127441406)

Text: 'H'
  Font: HGSGothicE
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (87.0999984741211, 188.37979125976562, 102.70735168457031, 214.65478515625)

Text: 'I'
  Font: Elephant Pro
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (102.87000274658203, 186.69818115234375, 114.16825103759766, 218.22817993164062)

Text: 'S'
  Font: Calibri
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (114.1500015258789, 191.24375915527344, 126.2102279663086, 217.51876831054688)

Text: 'I'
  Font: OCRAExtended
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (126.1500015258789, 188.40606689453125, 173.57009887695312, 215.5744171142578)

Text: 'S'
  Font: Daytona Condensed
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (173.47000122070312, 184.2808837890625, 192.34097290039062, 216.94070434570312)

Text: 'G'
  Font: Ebrima
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (202.75, 182.5992889404297, 220.774658203125, 216.46775817871094)

Text: 'R'
  Font: Walbaum Display
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (220.77000427246094, 183.96559143066406, 238.4268035888672, 218.412109375)

Text: 'A'
  Font: OldEnglishTextMT
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (238.8000030517578, 188.32723999023438, 257.9544677734375, 214.60223388671875)

Text: 'Y'
  Font: Rockwell Nova Extra Bold
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: True
  Italic: False
  Position: (258.3299865722656, 184.0181427001953, 288.84051513671875, 217.3348388671875)

Text: 'S'
  Font: Daytona Condensed Light
  Size: 26.274999618530273
  Color: #000000 (0)
  Bold: False
  Italic: False
  Position: (299.6300048828125, 184.2808837890625, 311.7427673339844, 216.94070434570312)

Looking through the output, it looks like the first character of the font name used for each character forms a secret message (we can see "THE CODEWORD" in the snippet above). I asked copilot to concatenate the first character of each font name together to get the solution:

THECODEWORDISSHARKATTACKTHECODEWORDISSHARKATTACKTHECODEWORDISSHARKATTACKTHECODEWORDISSHARKATTACKTHECODELORDISSHARKATTACKTHECODEWORDISSHARKATTACKTHECODEWORDISSHARKATTACKTHECODEWORDISSHARKATTACK

The flag for the challenge was "SHARKATTACK". Once I had the idea to extract font properties, this challenge was very easy to solve and have copilot code a solution.

Square Words

Challenge text:

Agent Ledger recovered a tiny scrap of paper near the cashier cage — just a crumpled slip, almost thrown away. On it: a long string of random letters and numbers, with no context, no labels, no obvious pattern.

Most would ignore it as garbage data or misprinted figures — but Whack Faction insiders know better.

We’ve cleaned up the sequence and transcribed it in the attached file. Break the number code and reveal the message hidden within.

Text file contents:

37    F    68    39    31    I    62    A    N
G    1    27    21    T    74    4    98    I
F    55    H    57    S    22    T    19    3
29    T    99    91    77    5    16    42    H
2    49    Y    E    T    51    R    95    35
74    L    82    46    D    25    69    H    E
A    27    65    I    85    12    W    71    53
18    N    T    84    O    37    33    N    39
Y    54    D    88    E   96    X    S    T

This one I ran through GPT5 which actually got close to a solution:

Alright, this looks like a 9×9 “pointer grid” (a square of mixed numbers and letters). The numbers are 2-digit coordinates: **rc** → “row r, column c”. When you land on a number you jump again; when you land on a letter you keep that letter. Doing that for each cell gives a fully lettered 9×9.

Using that rule (and treating single-digit numbers as (n,n) so they still point inside the square), the grid resolves to this letter square:

TFHHFILAN
GTYGTIYSI
FTHRSTTNH
ITTYWTITH
THYETTRES
ILNTDTEHE
AYDIOFWAY
ANT?OTHNH
YEDNE?XST

The "pointer grid" idea does look to be correct, it just didn't extract the text properly. So I asked copilot.

Prompt:

Analyze square_words.txt - A 9x9 grid containing letters and 2-digit numbers with whitespace separating columns and newlines separating rows. 
For each 2 digit number in the grid, do a lookup (row, col) using 1 based indexing which should resolve to a single character.  Output all of these characters.```

Output:

COORDINATE POINTER ANALYSIS (1-based indexing):
   Each two-digit number interpreted as (row, col) coordinates using 1-based indexing

   Grid with 1-based coordinates:
       1  2  3  4  5  6  7  8  9
   1:  37  F 68 39 31  I 62  A  N
   2:   G  1 27 21  T 74  4 98  I
   3:   F 55  H 57  S 22  T 19  3
   4:  29  T 99 91 77  5 16 42  H
   5:   2 49  Y  E  T 51  R 95 35
   6:  74  L 82 46  D 25 69  H  E
   7:   A 27 65  I 85 12  W 71 53
   8:  18  N  T 84  O 37 33  N 39
   9:   Y 54  D 88  E 96  X  S  T

   Following the pointers:
     Step 1: 37 -> row 3, col 7 = 'T'
     Step 2: 68 -> row 6, col 8 = 'H'
     Step 3: 39 -> row 3, col 9 = 3 = digit '3'
     Step 4: 31 -> row 3, col 1 = 'F'
     Step 5: 62 -> row 6, col 2 = 'L'
     Skipping 1 (not a two-digit number)
     Step 7: 27 -> row 2, col 7 = 4 = digit '4'
     Step 8: 21 -> row 2, col 1 = 'G'
     Step 9: 74 -> row 7, col 4 = 'I'
     Skipping 4 (not a two-digit number)
     ...
🎯 POINTER_MESSAGE: 'TH3FL4GISTR1NITYWITH2ESIN5TE4DOFAYATTH3END'

Conclusion

LLMs are certainly a challenge for CTF contest designers. The challenges have to be designed in such a way to make them difficult for an LLM to solve without a lot of user direction. This probably makes the challenges a lot more difficult than they would be without the existence of LLMs.

The contests this year seemed to have a lot more challenges than in previous years. So not only are the contest participants using LLMs, but the designers are too. :)