huge commit
2
2026/srdnlen_quals/forensic/chapter2/second_attempt/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
win98_extracted
|
||||
forge_attempts
|
||||
|
After Width: | Height: | Size: 576 KiB |
|
After Width: | Height: | Size: 576 KiB |
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
def brute_force_forge(bmp_path, forge_bin_path):
|
||||
with open(bmp_path, 'rb') as f:
|
||||
header = f.read(54)
|
||||
pixels = f.read()
|
||||
with open(forge_bin_path, 'rb') as f:
|
||||
forge = f.read()
|
||||
|
||||
os.makedirs("forge_attempts", exist_ok=True)
|
||||
|
||||
# We try shifting the starting point of the Forge key
|
||||
# Most common offsets are 0, 44 (WAV header), or small alignments
|
||||
for offset in range(0, 100):
|
||||
result = bytearray()
|
||||
for i in range(len(pixels)):
|
||||
# Cycle through the forge starting at the 'offset'
|
||||
result.append(pixels[i] ^ forge[(i + offset) % len(forge)])
|
||||
|
||||
with open(f"forge_attempts/offset_{offset}.bmp", "wb") as f:
|
||||
f.write(header + result)
|
||||
|
||||
print("[*] Generated 100 attempts. Look for the one that isn't static.")
|
||||
|
||||
brute_force_forge("./carved_bmps/bmp/00146504.bmp", "./carved_wavs/wav/wav_diff.raw")
|
||||
@@ -0,0 +1,59 @@
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
def strict_bmp_carve(dump_file):
|
||||
print(f"[*] Scanning {dump_file} for valid BMP headers...")
|
||||
|
||||
with open(dump_file, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
# Create an output directory
|
||||
os.makedirs("strict_bmps", exist_ok=True)
|
||||
|
||||
count = 0
|
||||
offset = 0
|
||||
|
||||
while True:
|
||||
# Find the next 'BM' signature
|
||||
offset = data.find(b'BM', offset)
|
||||
if offset == -1:
|
||||
break
|
||||
|
||||
# Ensure we have enough data for a header
|
||||
if offset + 14 > len(data):
|
||||
break
|
||||
|
||||
header = data[offset:offset+14]
|
||||
|
||||
# Unpack the header:
|
||||
# 2s (BM), I (Size), H (Res1), H (Res2), I (Data Offset)
|
||||
magic, size, res1, res2, data_offset = struct.unpack('<2sIHHI', header[:14])
|
||||
|
||||
# STRICT VALIDATION:
|
||||
# 1. Reserved bytes must be 0
|
||||
# 2. File size must be reasonable (e.g., between 1KB and 10MB)
|
||||
# 3. Data offset must be valid (usually 54 bytes for a standard BMP)
|
||||
if res1 == 0 and res2 == 0 and (1024 < size < 10000000) and (54 <= data_offset <= 1024):
|
||||
print(f"[+] Valid BMP found at offset {hex(offset)}")
|
||||
print(f" Size: {size} bytes | Data Offset: {data_offset}")
|
||||
|
||||
# Extract exactly 'size' bytes
|
||||
bmp_data = data[offset:offset+size]
|
||||
|
||||
out_name = f"strict_bmps/image_{hex(offset)}_{size}bytes.bmp"
|
||||
with open(out_name, 'wb') as out_f:
|
||||
out_f.write(bmp_data)
|
||||
|
||||
count += 1
|
||||
|
||||
# Move forward safely
|
||||
offset += 2
|
||||
|
||||
print(f"\n[*] Finished. Carved {count} strict BMP files into 'strict_bmps/' directory.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 carve_bmps_strict.py <dosbox_ram.raw>")
|
||||
else:
|
||||
strict_bmp_carve(sys.argv[1])
|
||||
@@ -0,0 +1,25 @@
|
||||
Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus
|
||||
Audit File
|
||||
|
||||
Foremost started at Sun Mar 1 16:07:24 2026
|
||||
Invocation: foremost -t bmp -i dosbox_ram.raw -o carved_bmps
|
||||
Output directory: /home/cato/ctf/2026/srdnlen_quals/forensic/chapter2/second_attempt/carved_bmps
|
||||
Configuration file: /etc/foremost.conf
|
||||
------------------------------------------------------------------
|
||||
File: dosbox_ram.raw
|
||||
Start: Sun Mar 1 16:07:24 2026
|
||||
Length: 128 MB (134221824 bytes)
|
||||
|
||||
Num Name (bs=512) Size File Offset Comment
|
||||
|
||||
0: 00103872.bmp 43 KB 53182464 (182 x 237)
|
||||
1: 00104770.bmp 5 KB 53642672 (310 x 35)
|
||||
2: 00146504.bmp 576 KB 75010048 (512 x 384)
|
||||
Finish: Sun Mar 1 16:07:24 2026
|
||||
|
||||
3 FILES EXTRACTED
|
||||
|
||||
bmp:= 3
|
||||
------------------------------------------------------------------
|
||||
|
||||
Foremost finished at Sun Mar 1 16:07:24 2026
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 576 KiB |
@@ -0,0 +1,27 @@
|
||||
Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus
|
||||
Audit File
|
||||
|
||||
Foremost started at Sun Mar 1 17:29:23 2026
|
||||
Invocation: foremost -t wav -i dosbox_ram.raw -T wav
|
||||
Output directory: /home/cato/ctf/2026/srdnlen_quals/forensic/chapter2/second_attempt/output_Sun_Mar__1_17_29_23_2026
|
||||
Configuration file: /etc/foremost.conf
|
||||
------------------------------------------------------------------
|
||||
File: dosbox_ram.raw
|
||||
Start: Sun Mar 1 17:29:23 2026
|
||||
Length: 128 MB (134221824 bytes)
|
||||
|
||||
Num Name (bs=512) Size File Offset Comment
|
||||
|
||||
0: 00014799.wav 36 B 7577580
|
||||
1: 00038872.wav 676 KB 19902464
|
||||
2: 00056278.wav 36 B 28814684
|
||||
3: 00072813.wav 676 KB 37280640
|
||||
4: 00099048.wav 1 KB 50712576
|
||||
Finish: Sun Mar 1 17:29:23 2026
|
||||
|
||||
5 FILES EXTRACTED
|
||||
|
||||
rif:= 5
|
||||
------------------------------------------------------------------
|
||||
|
||||
Foremost finished at Sun Mar 1 17:29:23 2026
|
||||
@@ -0,0 +1,13 @@
|
||||
def xor_wavs(wav1, wav2, out_file):
|
||||
with open(wav1, 'rb') as f1, open(wav2, 'rb') as f2:
|
||||
d1 = f1.read()[44:] # Skip headers
|
||||
d2 = f2.read()[44:]
|
||||
|
||||
# XOR the two audio streams
|
||||
res = bytes(a ^ b for a, b in zip(d1, d2))
|
||||
|
||||
with open(out_file, 'wb') as f:
|
||||
f.write(res)
|
||||
print(f"[+] XORed WAVs into {out_file}")
|
||||
|
||||
xor_wavs("00038872.wav", "00072813.wav", "wav_diff.raw")
|
||||
@@ -0,0 +1,21 @@
|
||||
def claim_the_armory(bmp_path, forge_bin_path, output_path):
|
||||
# Load the Armory (Relic 1)
|
||||
with open(bmp_path, 'rb') as f:
|
||||
bmp_header = f.read(54)
|
||||
bmp_pixels = f.read() # This is 589,824 bytes
|
||||
|
||||
# Load the Forge (The result of XORing the WAVs)
|
||||
with open(forge_bin_path, 'rb') as f:
|
||||
# We only need as much of the forge as we have pixels
|
||||
forge_key = f.read(len(bmp_pixels))
|
||||
|
||||
print(f"[*] Applying the Forge to the Armory...")
|
||||
# XOR the noise (fleeting air) with the system map (volatile stone)
|
||||
flag_pixels = bytes(a ^ b for a, b in zip(bmp_pixels, forge_key))
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(bmp_header + flag_pixels)
|
||||
|
||||
print(f"[+] The Armory is unlocked: {output_path}")
|
||||
|
||||
claim_the_armory("./carved_bmps/bmp/00146504.bmp", "./carved_wavs/wav/wav_diff.raw", "FLAG_REVEALED.bmp")
|
||||
@@ -0,0 +1,27 @@
|
||||
import math
|
||||
|
||||
def shannon_entropy(data):
|
||||
if not data: return 0
|
||||
entropy = 0
|
||||
for x in range(256):
|
||||
p_x = float(data.count(x))/len(data)
|
||||
if p_x > 0:
|
||||
entropy += - p_x * math.log(p_x, 2)
|
||||
return entropy
|
||||
|
||||
relic_1 = bytes.fromhex("8E97A917696BEB846B1937307F99E18F")
|
||||
file_path = "./dosbox_ram.raw"
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
data = f.read()
|
||||
# Scan in 16-byte chunks
|
||||
for i in range(0, len(data) - 16, 1):
|
||||
chunk = data[i:i+16]
|
||||
if chunk == relic_1:
|
||||
print(f"Relic 1 found at offset: {hex(i)}")
|
||||
continue
|
||||
|
||||
# We look for high entropy (typically > 3.5 for a 16-byte random-ish string)
|
||||
if shannon_entropy(chunk) > 3.5:
|
||||
# Check if it's surrounded by nulls or repetitive data (common for keys in RAM)
|
||||
print(f"Potential Relic 2 at {hex(i)}: {chunk.hex().upper()}")
|
||||
@@ -0,0 +1,24 @@
|
||||
import re
|
||||
|
||||
def find_win98_processes(ram_file):
|
||||
with open(ram_file, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
print(f"[*] Searching for active process signatures...")
|
||||
|
||||
# Windows 98 processes often have a characteristic 'magic' or
|
||||
# sit near their own filename in a specific K32 object structure.
|
||||
# We look for .EXE strings and check the surrounding 128 bytes.
|
||||
matches = re.finditer(b"[A-Za-z0-9_]{1,8}\.EXE\x00", data)
|
||||
|
||||
seen = set()
|
||||
for m in matches:
|
||||
name = m.group().decode('ascii', errors='ignore')
|
||||
if name not in seen:
|
||||
# Check if this looks like a process entry (usually preceded by pointers)
|
||||
# In Win9x, the 'Process Database' is often in the 0x80000000+ range
|
||||
# (which is mapped into our 128MB at different physical offsets).
|
||||
print(f"[+] Found potential process: {name} at {hex(m.start())}")
|
||||
seen.add(name)
|
||||
|
||||
find_win98_processes("dosbox_ram.raw")
|
||||
@@ -0,0 +1,34 @@
|
||||
def find_the_lock(bmp_path, fnd_path):
|
||||
with open(bmp_path, 'rb') as f:
|
||||
target_header = f.read(54)
|
||||
target_pixels = f.read()
|
||||
|
||||
with open(fnd_path, 'rb') as f:
|
||||
fnd_data = f.read()
|
||||
|
||||
print(f"[*] Attempting to align the Forge...")
|
||||
|
||||
# We search for a 16-byte known plaintext: the start of a second BMP
|
||||
# or the string 'srdnlen{'
|
||||
for offset in range(0, len(fnd_data) - len(target_pixels), 1):
|
||||
# We check the first 8 bytes of the XOR result at this offset
|
||||
# If it matches 'srdnlen{', we found it!
|
||||
sample = bytes(target_pixels[i] ^ fnd_data[offset + i] for i in range(8))
|
||||
if b"srdnlen{" in sample:
|
||||
print(f"[!!!] FLAG KEY FOUND AT OFFSET: {hex(offset)}")
|
||||
# Perform full XOR
|
||||
result = bytes(target_pixels[i] ^ fnd_data[offset + i] for i in range(len(target_pixels)))
|
||||
with open("REVEALED_WEAPON.txt", "wb") as out:
|
||||
out.write(result)
|
||||
return
|
||||
|
||||
print("[-] No direct string match. Trying visual alignment...")
|
||||
# If no string found, generate a few blocks to check for patterns
|
||||
import os
|
||||
os.makedirs("visual_align", exist_ok=True)
|
||||
for jump in range(0, 10000, 1000):
|
||||
res = bytes(target_pixels[i] ^ fnd_data[jump + i] for i in range(len(target_pixels)))
|
||||
with open(f"visual_align/jump_{jump}.bmp", "wb") as out:
|
||||
out.write(target_header + res)
|
||||
|
||||
find_the_lock("./carved_bmps/bmp/00146504.bmp", "./relic_fnd.bin")
|
||||
@@ -0,0 +1,31 @@
|
||||
# The "Relic" you found might be the start or a key within the buffer
|
||||
RELIC_1 = bytes.fromhex("8E97A917696BEB846B1937307F99E18F")
|
||||
PROCESS_DUMP = "./chall.dmp"
|
||||
|
||||
def find_armory():
|
||||
with open(PROCESS_DUMP, "rb") as f:
|
||||
# Map the file or read in chunks if it's huge
|
||||
data = f.read()
|
||||
|
||||
# Search for the first relic
|
||||
offset1 = data.find(RELIC_1)
|
||||
if offset1 == -1:
|
||||
print("Relic 1 not found in process dump.")
|
||||
return
|
||||
|
||||
print(f"Relic 1 found at process offset: {hex(offset1)}")
|
||||
|
||||
# Now search for a 'twin' relic at common VRAM offsets (2MB, 4MB, 8MB)
|
||||
for gap in [0x200000, 0x400000, 0x800000]:
|
||||
offset2 = offset1 + gap
|
||||
if offset2 + 16 < len(data):
|
||||
# Try XORing the first 2 bytes of the two relics
|
||||
# We are looking for "MZ" (0x4D 0x5A)
|
||||
header_check = (data[offset1] ^ data[offset2], data[offset1+1] ^ data[offset2+1])
|
||||
if header_check == (0x4D, 0x5A):
|
||||
print(f"ARMORY FOUND! XOR match at {hex(offset1)} and {hex(offset2)}")
|
||||
return offset1, offset2
|
||||
return None
|
||||
|
||||
# If found, you can then extract the weapon
|
||||
# weapon = bytes(a ^ b for a, b in zip(data[offset1:offset1+SIZE], data[offset2:offset2+SIZE]))
|
||||
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
def find_ghost_relics(ram_dump, original_bmp):
|
||||
target_pixel_size = 589824 # 512 * 384 * 3
|
||||
|
||||
with open(original_bmp, 'rb') as f:
|
||||
f.seek(54) # Skip header
|
||||
original_pixels = f.read()
|
||||
|
||||
with open(ram_dump, 'rb') as f:
|
||||
ram_data = f.read()
|
||||
|
||||
print(f"[*] Scanning RAM for blocks of size {target_pixel_size}...")
|
||||
|
||||
# We search in 4KB increments (standard memory page alignment)
|
||||
for offset in range(0, len(ram_data) - target_pixel_size, 4096):
|
||||
# Skip the offset where we already found the first BMP
|
||||
# (Assuming your first BMP was at 0x4789000 based on previous output)
|
||||
if offset == 0x4789000:
|
||||
continue
|
||||
|
||||
candidate = ram_data[offset : offset + target_pixel_size]
|
||||
|
||||
# Heuristic: Check if the block is "pixel-like"
|
||||
# Often, XORed or raw buffers have a high frequency of 0x00 or 0xFF
|
||||
# or share a similar byte distribution to the original.
|
||||
null_count = candidate.count(b'\x00')
|
||||
|
||||
# If the block is mostly empty or mostly full, it might be the 'Forge'
|
||||
# (the background layer or the XOR mask)
|
||||
if null_count > (target_pixel_size * 0.1): # If > 10% is null
|
||||
print(f"[!] Potential relic found at {hex(offset)} (Null-ratio: {null_count/target_pixel_size:.2%})")
|
||||
|
||||
# Save it as a raw bin and also a reconstructured BMP
|
||||
out_bmp = f"relics/relic_{hex(offset)}.bmp"
|
||||
|
||||
# Reconstruct with the header from the original
|
||||
with open(original_bmp, 'rb') as h:
|
||||
header = h.read(54)
|
||||
with open(out_bmp, 'wb') as b:
|
||||
b.write(header + candidate)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python3 relic_hunter.py <dosbox_ram.raw> <original.bmp>")
|
||||
else:
|
||||
find_ghost_relics(sys.argv[1], sys.argv[2])
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 576 KiB |
|
After Width: | Height: | Size: 576 KiB |
@@ -0,0 +1,58 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
def fine_tune_xor(target_path, key_path):
|
||||
with open(target_path, 'rb') as f:
|
||||
t_header, t_pixels = f.read(54), f.read()
|
||||
with open(key_path, 'rb') as f:
|
||||
k_pixels = f.read()[54:]
|
||||
|
||||
os.makedirs("tunes", exist_ok=True)
|
||||
# Try shifting the starting byte of the key
|
||||
for shift in range(0, 10):
|
||||
result = bytearray()
|
||||
for i in range(len(t_pixels)):
|
||||
result.append(t_pixels[i] ^ k_pixels[(i + shift) % len(k_pixels)])
|
||||
|
||||
with open(f"tunes/shift_{shift}.bmp", "wb") as f:
|
||||
f.write(t_header + result)
|
||||
print("[*] Generated 10 shifts in /tunes. Look for the one where lines are perfectly vertical.")
|
||||
|
||||
def xor_with_repeating_key(target_path, key_path, output_path):
|
||||
print(f"[*] Loading Target: {target_path}")
|
||||
print(f"[*] Loading Key: {key_path}")
|
||||
|
||||
with open(target_path, 'rb') as f:
|
||||
target_header = f.read(54)
|
||||
target_data = f.read()
|
||||
|
||||
with open(key_path, 'rb') as f:
|
||||
f.read(54) # Skip the 44KB BMP's header
|
||||
key_data = f.read()
|
||||
|
||||
print(f"[*] Target Data: {len(target_data)} bytes")
|
||||
print(f"[*] Key Data: {len(key_data)} bytes")
|
||||
|
||||
# Perform Repeating XOR
|
||||
# We use a generator to loop the key indefinitely
|
||||
def key_generator(k):
|
||||
while True:
|
||||
for byte in k:
|
||||
yield byte
|
||||
|
||||
gen = key_generator(key_data)
|
||||
|
||||
print("[*] Forging the weapon (XORing)...")
|
||||
result = bytearray()
|
||||
for b in target_data:
|
||||
result.append(b ^ next(gen))
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(target_header + result)
|
||||
|
||||
print(f"[+] SUCCESS! Relic forged: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# target = 589KB BMP, key = 44KB BMP
|
||||
xor_with_repeating_key("./carved_bmps/bmp/00146504.bmp", "./carved_bmps/bmp/00104770.bmp", "FINAL_ARMORY_FLAG.bmp")
|
||||
#fine_tune_xor("./carved_bmps/bmp/00146504.bmp", "./carved_bmps/bmp/00103872.bmp")
|
||||