ECW 2022 - Minifilter

Minifilter is a reverse-engineering challenge I designed for the 2022 edition of the European Cyber Week. Here is the source code for this challenge.

For this challenge, you’re given a truc.sys driver and a file.txt.lock file and the following scenario: “some user tried to save his file using her notepad but the saved file looks funny. Find out what’s going on there, find the file.txt cleartext”.

The truc.sys implements a Windows minifilter that is xor-encoding the file when saved to disk. The key is randomly generated but not saved anywhere. Or is it? The file is UTF-16 encoded (with BOM). We are able to recover the xor key using some null bytes in the flag, due to xor properties.

Here is my annotated resolution script:

 1import math
 2import codecs
 3
 4CHUNK_SIZE = 7
 5KEY_SIZE = 4
 6
 7# Step 0: read the data
 8with open(r"./secret/file.txt.lock", "rb") as fh:
 9    data = fh.read()
10
11# Step 1: read the key using the xored null bytes of UTF16-LE encoding while skipping BOM
12key = [0xff] * KEY_SIZE
13found = 0
14for i in range(
15    KEY_SIZE - 1, # Skip BOM
16    len(data)
17):
18    if found == KEY_SIZE:
19        break
20
21    if i % 2: # Even bytes reveal one byte of the key
22        chunk_idx = (int)(i / CHUNK_SIZE) + 1
23        if found == 0:
24            key[0] = data[i]  ^ chunk_idx
25        elif found == 1:
26            key[2] = data[i]  ^ chunk_idx
27        elif found == 2:
28            key[3] = data[i]  ^ chunk_idx
29        elif found == 3:
30            key[1] = data[i]  ^ chunk_idx
31
32        found += 1
33
34# Little endian repr
35key = key[::-1]
36
37print("Key: ", end="")
38for k in key:
39    print(f"{k:#x}, ", end="")
40print("")
41
42# Step 2: decode data
43def chunks(xs, n):
44    for i in range(0, len(xs), n):
45        yield xs[i:i + n]
46
47print("Chunks:")
48clear = []
49chunks = list(chunks(data, CHUNK_SIZE))
50for chunk_idx, chunk in enumerate(chunks):
51    for chunk_offset, byte in enumerate(chunk):
52        # We can use chunk_offset because CHUNK_SIZE > KEY_SIZE, we don't need an offset from the beginning here
53        byte = byte ^ key[chunk_offset % KEY_SIZE] ^ (chunk_idx + 1)
54        print(f"0x{byte:02x}, ", end="")
55        clear.append(byte)
56        #print(f"{byte:c}", end="")
57    print("")
58
59# Step 3: reconstruct the text file
60with open("clear.txt", "wb") as fh:
61    decoded = bytes(clear).decode("utf-16-le")
62    fh.write(decoded.encode('utf-16-le'))
63    print(f"Contents: {decoded}")
 1$ xxd file.txt.lock
 200000000: fc32 be2f 40cc ac00 b4f8 6a00 a3f8 36ce  .2./@.....j...6.
 300000010: a62d 71ce b606 fcfe 7e06 f9fe 57c8 a02b  .-q.....~...W..+
 400000020: 61c8 b604 85fc 6104 b8fc 4dca ce29 41ca  a.....a...M..)A.
 500000030: a20a 95f2 740a f5f2 48c4 c027 58c4 c608  ....t...H..'X...
 600000040: aef0 4a08 80f0 74c6 fc25 03c6            ..J...t..%..
 7$ xxd original.txt
 800000000: fffe 4500 4300 5700 7b00 4600 6c00 3700  ..E.C.W.{.F.l.7.
 900000010: 5f00 7000 4f00 3500 5400 3000 5000 5f00  _.p.O.5.T.0.P._.
1000000020: 6600 4900 4e00 4900 7300 4800 3300 4400  f.I.N.I.s.H.3.D.
1100000030: 5f00 5000 5200 3000 4300 3300 5300 3500  _.P.R.0.C.3.S.5.
1200000040: 6900 6e00 4700 7d00 0d00 0a00            i.n.G.}.....

flag: ECW{Fl7_pO5T0P_fINIsH3D_PR0C3S5inG}