← Back to Writeups
CUSTOMMediumCrypto

1582

XESXOR9/10/20264 min read
#crypto#custom#medium#date_diff_key_derivation#gregorian_reform_correction#global_xor_offset_bruteforce#repeating_key_xor_decrypt

1582

Platform: bug-makers | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-29 | Status: Solved Techniques: date_diff_key_derivation, gregorian_reform_correction, global_xor_offset_bruteforce, repeating_key_xor_decrypt

Summary

Task: 39-byte XOR ciphertext plus a journal of 15 dates in 1582; key is the day-differences between adjacent dates. Solution: derive a repeating XOR key from date diffs, apply a global 0x40 offset, and correct the Oct 4->Oct 15 gap to 1 day per the Gregorian calendar reform (10 lost days).

1582 — bug-makers CTF

Description

Осень 1582 года. Архивариус в Риме нашёл повреждённый журнал дат и зашифрованный файл. Он утверждает, что файл зашифрован XOR, а ключ спрятан в разнице между соседними датами.

English summary: An archivist in Rome (Autumn 1582) found a damaged journal of dates and an encrypted file. The file is XOR-encrypted, and the key is hidden in the differences between adjacent dates.

Provided files:

  • journal.txt — a Russian-language journal listing 15 dates in 1582 (June through December). A header note says to use the dates exactly as written.
  • cipher.bin — 39 bytes: 133d25061f1e3d3d221e3e2137220e312724392b193c232d2b1175660e2c233c3807776675732f ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The 15 dates:

1582-06-21, 1582-07-08, 1582-07-16, 1582-07-18, 1582-07-23,
1582-08-03, 1582-08-27, 1582-09-02, 1582-09-21, 1582-10-04,
1582-10-15, 1582-11-02, 1582-11-16, 1582-11-20, 1582-12-12

Analysis

The description spells out the scheme directly: XOR cipher, key = differences between adjacent dates. With 15 dates there are 14 adjacent differences, giving a 14-byte repeating key for the 39-byte ciphertext.

Computing the naive day-differences (treating each date as an ordinary proleptic calendar date): ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

[17, 8, 2, 5, 11, 24, 6, 19, 13, 11, 18, 14, 4, 22]

These small integers are not in the printable ASCII range, so a plain cipher[i] ^ key[i%14] will not yield text. The diff values are concentrated around 1–24, while flag characters live around 0x30–0x7e. The standard fix is a global XOR offset: brute-force a single constant k over all 256 values and look for printable output containing {. The winning constant is 0x40, which lifts the small diff keystream into the letter/digit range. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Applying the naive key with the 0x40 offset:

B u g C T F { n o U l o s t _ y e a r s _ o n f y _ 1 0 _ d a y s _ 1 5 8 8 }
                  ^                       ^                             ^^

This is a near-flag — almost correct but with wrong bytes at exactly the positions that fall on key index 9 (and its repeat). That is the tell: the date differences are right everywhere except where the calendar itself is irregular.

The key insight — the Gregorian calendar reform of October 1582. When the Gregorian calendar was adopted, October 4, 1582 was directly followed by October 15, 1582 — the ten intermediate days (Oct 5–14) never existed. So the real elapsed time between 1582-10-04 and 1582-10-15 is 1 day, not 11. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The journal pair 1582-10-04 → 1582-10-15 is at index 9 of the key. Correcting that one value 11 → 1 fixes precisely the bytes that were corrupted in the near-flag. The flag text itself — REDACTED — confirms the theme: the 10 lost days of October 1582.

Corrected key:

[17, 8, 2, 5, 11, 24, 6, 19, 13, 1, 18, 14, 4, 22]

Solution

  1. Parse the 15 dates and compute the 14 adjacent day-differences → repeating XOR key.
  2. Brute-force the global XOR offset over 0–255; the printable result containing { appears at offset 0x40. (Or just recognize Bug from BugCTF{...} to recover the offset via known-plaintext.)
  3. The naive key produces a corrupted near-flag, with errors localized to key index 9 — pointing at the 1582-10-04 → 1582-10-15 pair.
  4. Apply the historical correction: that interval is 1 day (Gregorian reform), not 11.
  5. Re-decrypt with the corrected key to get the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
from datetime import date

ct = bytes.fromhex('133d25061f1e3d3d221e3e2137220e312724392b193c232d2b1175660e2c233c3807776675732f')
dates = ['1582-06-21','1582-07-08','1582-07-16','1582-07-18','1582-07-23',
         '1582-08-03','1582-08-27','1582-09-02','1582-09-21','1582-10-04',
         '1582-10-15','1582-11-02','1582-11-16','1582-11-20','1582-12-12']

ds  = [date(*map(int, d.split('-'))) for d in dates]
key = [(ds[i+1] - ds[i]).days for i in range(len(ds) - 1)]
# naive key: [17, 8, 2, 5, 11, 24, 6, 19, 13, 11, 18, 14, 4, 22]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Gregorian calendar reform of Oct 1582: Oct 4 -> Oct 15 is really 1 day,
# not 11 (the days Oct 5-14 never existed). Index 9 corresponds to that gap.
key[9] = 1
# corrected key: [17, 8, 2, 5, 11, 24, 6, 19, 13, 1, 18, 14, 4, 22]

# Global XOR offset 0x40 maps the small diff keystream into the ASCII range.
pt = bytes(ct[i] ^ key[i % len(key)] ^ 0x40 for i in range(len(ct)))
print(pt.decode())   # BugCTF{REDACTED}

Recovering the 0x40 offset without guessing (known-plaintext / brute):

python
for k in range(256):
    out = bytes(ct[i] ^ key[i % len(key)] ^ k for i in range(len(ct)))
    if all(32 <= b < 127 for b in out) and b'{' in out:
        print(hex(k), out.decode())   # 0x40 BugCTF{...}

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