Featured image of post Windows Forensics Hell : Writeup

Windows Forensics Hell : Writeup

A detailed writeup of the Windows Forensics Hell challenge from DDC FastTrack Junior 2026.

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:

File Description
challenge.E01 (6GiB!) Det udleverede E01 disk image
Windows.Forensics.Hell.appendix_-_bad.author.edition.7z En 7z arkiv med den ikke-corrupted update.exe udleveret af organisatorerne.

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

Step 3 - Extract update.exe med blkcat

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

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.

Strings fra BinaryNinja

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

aesEncrypt function

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

Buffer data

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

generateKey function

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}

Licensed under CC BY-NC-SA 4.0