← Back to Writeups
CUSTOMMediumForensics

128 кассир (128 Cashier)

XESXOR9/2/20264 min read
#forensics#custom#medium#barcode_decoding#bmp_header_analysis#color_inversion#dimension_calculation#rle8_decoding#windows

128 кассир (128 Cashier)

Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-01-06 | Status: Solved Techniques: barcode_decoding, bmp_header_analysis, color_inversion, dimension_calculation, rle8_decoding

Summary

Task: BMP file with corrupted header and wrong dimensions in metadata. Solution: Repair BMP signature, decode RLE8 data, calculate real dimensions from pixel count, invert colors and decode Code 128 barcode.

128 кассир (128 Cashier) - Hackerlab

Description

Черно-белый или бело-чёрный? Может быть всё наоборот? Хотя какая разница, Вы ведь этого не видите... Позовите кассира!

Files: task.zip (contains task.bmp)

Analysis

Step 1: Initial Inspection

bash
unzip task.zip
file task.bmp
# task.bmp: data

The file is identified as "data" instead of BMP - the header is corrupted.

Step 2: Hex Dump Analysis

bash
xxd task.bmp | head -5
# 00000000: 004d 3e00 0000 0000 0000 3604 0000 2800  .M>...........(.

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The first byte is 0x00 instead of 0x42 ('B'). BMP files must start with "BM" (0x42 0x4D).

Step 3: Header Repair

bash
printf '\x42' | dd of=task.bmp bs=1 count=1 conv=notrunc
file task.bmp
# task.bmp: PC bitmap, Windows 3.x format, 100 x 20 x 8, image size 15878, resolution 2835 x 2835 px/m, 256 important colors, RLE 8-bit

Header restored:

  • Size: 100 x 20 pixels
  • Depth: 8 bit (256 colors)
  • Compression: RLE8 (compression=1)

Step 4: Dimension Problem

When opening the image, we only see a black rectangle 100x20. But the RLE8 data decodes to a much larger volume! ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5: RLE8 Decoding and Real Dimension Calculation

python
# RLE8 decoding yields 247040 pixels
# 100 x 20 = 2000 pixels - doesn't match!

# Factorization of 247040:
# 247040 = 1280 × 193

Real image dimensions: 1280 x 193 pixels.

Solution

python
#!/usr/bin/env python3
"""
128 кассир (128 Cashier) - BMP Header Repair + RLE8 Decode + Code128 Barcode
"""

from PIL import Image
from pyzbar.pyzbar import decode
import struct

def decode_rle8(data):
    """Decode RLE8 compressed data"""
    pixels = []
    i = 0
    while i < len(data):
        count = data[i]
        value = data[i + 1]
        i += 2
        
        if count == 0:
            if value == 0:  # End of line
                continue
            elif value == 1:  # End of bitmap
                break
            elif value == 2:  # Delta
                i += 2  # Skip delta bytes
            else:  # Absolute mode
                pixels.extend(data[i:i + value])
                i += value
                if value % 2:  # Padding
                    i += 1
        else:
            pixels.extend([value] * count)
    
    return pixels
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Read BMP file
with open('task.bmp', 'rb') as f:
    bmp_data = f.read()

# Fix header if needed (first byte should be 0x42 'B')
if bmp_data[0] == 0x00:
    bmp_data = b'\x42' + bmp_data[1:]
    with open('task_fixed.bmp', 'wb') as f:
        f.write(bmp_data)

# Parse BMP header
data_offset = struct.unpack('<I', bmp_data[10:14])[0]
width = struct.unpack('<i', bmp_data[18:22])[0]
height = struct.unpack('<i', bmp_data[22:26])[0]
compression = struct.unpack('<I', bmp_data[30:34])[0]

print(f"Header dimensions: {width}x{height}")
print(f"Compression: {compression} (1=RLE8)")
print(f"Data offset: {data_offset}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Extract palette (256 colors, 4 bytes each)
palette = []
for i in range(256):
    offset = 54 + i * 4
    b, g, r, _ = bmp_data[offset:offset + 4]
    palette.append((r, g, b))

# Decode RLE8 data
rle_data = bmp_data[data_offset:]
pixels = decode_rle8(rle_data)
print(f"Decoded pixels: {len(pixels)}")

# Calculate real dimensions
# 247040 = 1280 * 193
real_width = 1280
real_height = len(pixels) // real_width
print(f"Real dimensions: {real_width}x{real_height}")

# Create image with correct dimensions
img = Image.new('RGB', (real_width, real_height))
for y in range(real_height):
    for x in range(real_width):
        idx = y * real_width + x
        if idx < len(pixels):
            color_idx = pixels[idx]
            img.putpixel((x, real_height - 1 - y), palette[color_idx])  # BMP is bottom-up
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

img.save('barcode.png')

# Invert colors (as hinted in description)
from PIL import ImageOps
img_inverted = ImageOps.invert(img.convert('RGB'))
img_inverted.save('barcode_inverted.png')

# Decode barcode
barcodes = decode(img_inverted)
for barcode in barcodes:
    print(f"Type: {barcode.type}")
    print(f"Data: {barcode.data.decode()}")

Result

Header dimensions: 100x20
Compression: 1 (RLE8)
Decoded pixels: 247040
Real dimensions: 1280x193
Type: CODE128
Data: CODEBY{REDACTED}

Techniques

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

TechniqueDescription
BMP Header RepairRestoring the "BM" signature (0x42 0x4D)
RLE8 DecodingManual decoding of Run-Length Encoding
Dimension CalculationCalculating real dimensions from pixel count
Color InversionInverting colors for barcode readability
Barcode DecodingDecoding Code 128 using pyzbar

Useful Commands

bash
# Check BMP signature
xxd file.bmp | head -1

# Fix first byte
printf '\x42' | dd of=file.bmp bs=1 count=1 conv=notrunc
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# BMP information
identify -verbose file.bmp

# Decode barcode
python3 -c "from pyzbar.pyzbar import decode; from PIL import Image; print(decode(Image.open('barcode.png')))"

References