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}