Windows Forensics Hell
So ehmm… I tried moving away from Windows to support the OSS movement, but somehow it went wrong and now I have gotten a virus? I though viruses were only for Windows??
Handouts:
Vi får udleveret challenge.E01, som er EWF/Expert Witness/EnCase image format, og som indeholder en Linux installation.
Step 1 - Mount image og grave rundt i filsystemet
Vi starter med at mounte E01 filen ved hjælp af ewfmount og så bruge mmls til at liste partitions og finde offset for ext4 partitionen. Efter det kan vi mounte den med mount og så kigge rundt i filsystemet.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
mkdir /tmp/ewf_mount
sudo ewfmount challenge.E01 /tmp/ewf_mount
sudo mmls /tmp/ewf_mount/ewf1
GUID Partition Table (EFI)
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Safety Table
001: ------- 0000000000 0000002047 0000002048 Unallocated
002: Meta 0000000001 0000000001 0000000001 GPT Header
003: Meta 0000000002 0000000033 0000000032 Partition Table
004: 000 0000002048 0000004095 0000002048
005: 001 0000004096 0001054719 0001050624 EFI System Partition
006: 002 0001054720 0041940991 0040886272
007: ------- 0041940992 0041943039 0000002048 Unallocated
|
Vi kan se at 006 er noget større end de andre, vi kan bruge fls til at liste filer i den partition og se at det er en ext4 partition, og så mounte den med mount og offset 1054720*512 bytes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
sudo fls -o 1054720 /tmp/ewf_mount/ewf1
d/d 786433: home
d/d 11: lost+found
d/d 262145: boot
r/r 13: swapfile
d/d 524289: etc
d/d 1048577: media
l/l 14: bin
d/d 393217: dev
d/d 655361: var
l/l 15: lib
l/l 16: lib64
d/d 917505: mnt
d/d 786434: opt
d/d 524291: proc
d/d 393218: root
d/d 917506: run
l/l 17: sbin
d/d 1048578: srv
d/d 524292: sys
d/d 262147: tmp
d/d 393219: usr
d/d 1048579: cdrom
V/V 1277953: $OrphanFiles
sudo mkdir /tmp/ext4
sudo mount -o ro,loop,offset=$((1054720*512)) /tmp/ewf_mount/ewf1 /tmp/ext4
❯ ls /tmp/ext4
bin cdrom etc lib lost+found mnt proc run srv sys usr
boot dev home lib64 media opt root sbin swapfile tmp var
❯ find /tmp/ext4 | grep "ddc" 2>/dev/null
...
/tmp/ext4/home/jens/Documents/flag.ddc.enc
...
❯ xxd /tmp/ext4/home/jens/Documents/flag.ddc.enc
00000000: ec6e 93bb 9d44 036e 11b4 e1e2 c180 247e .n...D.n......$~
00000010: 1324 42a7 1655 b996 2382 9988 e2df 29cf .$B..U..#.....).
00000020: 9b11 aad4 fb4e ce9f 75cd d8b3 ce11 b43f .....N..u......?
❯ cp /tmp/ext4/home/jens/Documents/flag.ddc.enc ~/windows_forensics_hell/
|
I jens’ Documents folder finder vi flag.ddc.enc, som er en AES-CBC krypteret fil, og som vi senere skal bruge til at få fat i flaget.
1
2
3
4
5
6
7
8
9
10
11
12
|
❯ file /tmp/ext4/home/jens/.mozilla/firefox/d1andqhl.default-release/places.sqlite
/tmp/ext4/home/jens/.mozilla/firefox/d1andqhl.default-release/places.sqlite: SQLite 3.x database, user version 82 (0x52), last written using SQLite version 3050001, page size 32768, writer version 2, read version 2, file counter 2, database pages 53, cookie 0x2d, schema 4, UTF-8, version-valid-for 2
❯ sqlite3 places.sqlite
SQLite version 3.51.2 2026-01-09 17:27:48
Enter ".help" for usage hints.
sqlite> SELECT * FROM 'moz_annos';
1|15|1|file:///home/jens/Downloads/iTunes64Setup.exe|0|4|3|1771530281252000|1771530281252000
2|15|2|{"state":1,"deleted":false,"endTime":1771530324229,"fileSize":210767328}|0|4|3|1771530324238000|1771530324238000
3|20|1|file:///home/jens/Downloads/update.exe|0|4|3|1771532948473000|1771532948473000
4|20|2|{"state":1,"deleted":false,"endTime":1771532948542,"fileSize":517972}|0|4|3|1771532948567000|1771532948567000
sqlite>
|
Firefox’s places.sqlite fortæller os at der er blevet downloaded en update.exe fra http://192.168.102.1:8000/update.exe og at den fyldte 517972 bytes.
Step 2 - Recover update.exe
Men da find /tmp/ext4 | grep "update.exe" 2>/dev/null ikke giver nogle resultater, kan vi konkludere at filen er slettet fra directory træet. Dette betyder dog ikke at dataen er væk, da ext4 kun fjerner referencer til filen og ikke selve blokkene med det samme.
Vi kan derfor forsøge at finde filens inode-metadata i ext4 journalen. Journalen indeholder tidligere filesystem-transaktioner og kan stadig indeholde extent information om slettede filer. Vi starter derfor med at dumpe journalen (inode 8):
1
2
3
4
5
|
❯ cd ~/windows_forensics_hell
❯ sudo icat -o 1054720 /tmp/ewf_mount/ewf1 8 > journal.bin
❯ strings -t d journal.bin | grep "update.exe"
20930616 update.exe
21069880 update.exe
|
Her får vi altså 2 hits på update.exe, scriptet scanner journalen for ext4 extent headers og rekonstruerer inode-strukturen for at finde den oprindelige filstørrelse samt de fysiske blocks filen lå i.
update.exe har en størrelse på 517972 bytes.
\[
extent\ length = \lceil \frac{517972}{4096} \rceil = 127 \text{ blocks}
\]
Med en ext4 block size på 4096 bytes giver det 517972 / 4096 ≈ 126.5, hvilket afrundes op til 127 blocks.
Dette matcher extent length fundet i journalen og bekræfter at vi har fundet den korrekte inode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
import struct
d=open('journal.bin',"rb").read()
EXP=517972 # kendt størrelse for update.exe
best=None
for i in range(len(d)):
# ext4 extent header magic → sandsynlig inode med block pointers
if d[i:i+2]!=b"\x0a\xf3":
continue
# extent tree ligger ved inode+0x28
ino=i-0x28
if ino<0:
continue
# basic inode info
mode=struct.unpack_from("<H",d,ino)[0]
size=struct.unpack_from("<I",d,ino+4)[0]
flags=struct.unpack_from("<I",d,ino+0x20)[0]
# kun regular files med extents
if not (mode & 0x8000):
continue
if not (flags & 0x80000):
continue
# extent header (depth 0 = direkte block pointers)
eh=struct.unpack_from("<HHHHI",d,ino+0x28)
if eh[3]!=0:
continue
# første extent = hvor filen fysisk starter
ee=struct.unpack_from("<IHHI",d,ino+0x34)
start=(ee[2]<<32)|ee[3]
length=ee[1] # antal blocks filen bruger
# update.exe bruger ~127 blocks
if length<120 or length>130:
continue
# simpel scoring for bedste match
score=0
if size==EXP:
score+=100
if length==127:
score+=50
if abs(size-EXP)<4096:
score+=20
if not best or score>best[0]:
best=(score,size,start,length)
print("start:",best[2])
print("blocks:",best[3])
print("size:",best[1])
|
Output:
1
2
3
|
start: 4560128
blocks: 127
size: 517972
|
Perfekt! Vi har fundet de fysiske blocks hvor update.exe lå. Vi kan nu bruge blkcat til at udtrække filen baseret på det startblock og antal blocks vi har fundet.
1
2
3
4
5
6
7
8
9
|
❯ sudo blkcat -o 1054720 /tmp/ewf_mount/ewf1 4560128 127 > update_recovered.exe
❯ file update_recovered.exe
update_recovered.exe: PE32+ executable for MS Windows 5.02 (console), x86-64, 19 sections
❯ stat update_recovered.exe
File: update_recovered.exe
Size: 520192 Blocks: 1016 IO Block: 4096 regular file
...
|
Vi kan se at den udtrukne fil er ca 520,192 bytes, hvilket er lidt større end de 517,972 bytes vi forventede. Dette skyldes at blkcat udtrækker hele blocks, og den sidste block er delvist fyldt, så der er nogle null bytes i slutningen.
Step 3 - Bad author edition
Grundet fejl i opgaven, endte author med at release en zip-fil med den ikke corrupted update.exe i. Men for at kunne åbne den skal vi bruge passwordet, som er SHA-256 hashen af den corrupted update.exe - altså den version vi lige har udtrukket.
For at finde det korrekte password, skal vi kun hashe de første 517,972 bytes af den udtrukne fil (uden de trailing null bytes):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
❯ head -c 517972 update_recovered.exe | sha256sum | awk '{print toupper($1)}'
4AC23960E0D1F9D5165F6E8D7029DA94199971A2686C1068BE4936FED397DBD6
❯ 7z x Windows.Forensics.Hell.appendix_-_bad.author.edition.7z -p4AC23960E0D1F9D5165F6E8D7029DA94199971A2686C1068BE4936FED397DBD6
7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
64-bit locale=C.UTF-8 Threads:16 OPEN_MAX:1024, ASM
Scanning the drive for archives:
1 file, 110738 bytes (109 KiB)
Extracting archive: Windows.Forensics.Hell.appendix_-_bad.author.edition.7z
--
Path = Windows.Forensics.Hell.appendix_-_bad.author.edition.7z
Type = 7z
Physical Size = 110738
Headers Size = 226
Method = LZMA2:19 BCJ 7zAES
Solid = -
Blocks = 1
Everything is Ok
Size: 517972
Compressed: 110738
❯ file update.exe
update.exe: PE32+ executable for MS Windows 5.02 (console), x86-64, 19 sections
❯ stat update.exe
File: update.exe
Size: 517972 Blocks: 1016 IO Block: 4096 regular file
...
|
Step 4 - Reverse-engineer update.exe
Efter at have fyret DiE (Detect It Easy) op på update.exe, kan vi se at det er en Nim binary.

Detect It Easy fortæller os at binaryen er skrevet i Nim og efter compilet til en Windows PE fil.
Vi er nu ramt det klassiske punkt hvor alle forensics challenges ender med at blive til reverse engineering challenge… Nå men vi fyrer BinaryNinja op og loader update.exe ind for at se hvad vi har at arbejde med.

Her finder vi allerede nogle interessante strings i .rdata sektionen, som peger på at der sker noget kryptering i update.exe, og at det er nimcrypto biblioteket der bliver brugt.
Find AES-CBC IV’en

Her i aesEncrypt funktionen kan vi se at den laver et nimCopyMem call til at kopiere noget data til en buffer.

Hvis vi tager længden af den data der bliver kopieret, så er det præcis 16 bytes, hvilket matcher AES block størrelsen! Det er derfor rimeligt at antage at det er IV’en der bliver kopieret her, og at den er hardcoded i binaryen.
1
2
3
4
5
6
|
Python 3.14.2 (main, Jan 2 2026, 14:27:39) [GCC 15.2.1 20251112] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> len(b"\x02\xb5\x16\x8c\xfd\x01Bc\xac\r\xbf\x0f\x98I*[")
16 # AES block size for AES-CBC
>>> b"\x02\xb5\x16\x8c\xfd\x01Bc\xac\r\xbf\x0f\x98I*[".hex()
'02b5168cfd014263ac0dbf0f98492a5b' # IV'en i hex
|
Find nøglen til AES krypteringen

Ved analyse af generateKey funktionen i Binary Ninja kan vi se at funktionen bruger Nim runtime til at hente navnet på den nuværende procedure via getFrame().
1
|
cstrToNimstr(&var_38, *(uint64_t*)(getFrame() + 8));
|
Fra Nim runtime strukturen ved vi at offset +8 i en TFrame indeholder procedure-navnet (procname). Da funktionen hedder generateKey, vil værdien derfor være: "generateKey".
Derefter kan vi se at funktionen arbejder med en hardcoded Nim string fra .rdata sektionen. Denne kan identificeres via referencen til: TM__HSZE2vatNNSRvcJy9cWwVYA_36. Hvis vi dumper memory på adressen ser vi:
1
2
3
4
5
6
7
8
|
❯ radare2 -q -c "px 80 @ 0x140053f40" update.exe
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
- offset - 4041 4243 4445 4647 4849 4A4B 4C4D 4E4F 0123456789ABCDEF
0x140053f40 4700 0000 0000 0040 443a 5c43 5446 5c44 G......@D:\CTF\D
0x140053f50 4443 5c44 4443 3230 3236 5c66 6f72 656e DC\DDC2026\foren
0x140053f60 7369 6373 5c57 696e 646f 7773 2046 6f72 sics\Windows For
0x140053f70 656e 7369 6320 4865 6c6c 5c65 6e63 7279 ensic Hell\encry
0x140053f80 7074 6f72 5c75 7064 6174 652e 6e69 6d00 ptor\update.nim.
|
Her kan vi se Nim string layout: 47 00 00 00 00 00 00 40, hvor den første værdi: 0x47 = 71 er længden af stringen, og derefter er selve stringen: D:\CTF\DDC\DDC2026\forensics\Windows Forensic Hell\encryptor\update.nim.
Key material bliver derfor: D:\CTF\DDC\DDC2026\forensics\Windows Forensic Hell\encryptor\update.nimgenerateKey.
Decrypt flag.ddc.enc med AES-CBC
Vi har nu både IV’en og nøglen til AES-CBC krypteringen, så det er bare at smække det sammen i Python og få fat i flaget.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from Crypto.Cipher import AES
import hashlib
ciphertext = bytes.fromhex(
"ec6e93bb9d44036e11b4e1e2c180247e"
"132442a71655b99623829988e2df29cf"
"9b11aad4fb4ece9f75cdd8b3ce11b43f"
)
key_material = (
r"D:\CTF\DDC\DDC2026\forensics\Windows Forensic Hell\encryptor\update.nim"
"generateKey"
)
key = hashlib.sha256(key_material.encode()).digest()
iv = bytes.fromhex("02b5168cfd014263ac0dbf0f98492a5b")
plaintext = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
print(plaintext.decode())
|
Flag: DDC{w0uld_y0u_l1k3_4_f0rk_f0r_th4t_c4rv1ng}