NIT Breach 2 — Memory Forensics Writeup
category: forensics / memory Analysis
Tooling: Volatility 3
Image: nit_breach_2.raw (Ubuntu Linux memory dump)
Flag: nmctf{num1d14n_m3m0ry_s3cr3t_7c2b8a}
first instincts
I ran strings on the image, grepped for “nmctf{” but found nothing, afterwards i ran strings and grepped for “classified_secrets”
strings -t d nit_breach_2.raw | grep -iE "classified_secrets"
got:
321038056 ./nit_numidian_db/classified_secrets
next thing i did was regex scan for “classified_secrets”
python3 vol.py -f nit_breach_2.raw linux.vmaregexscan.VmaRegExScan --pattern "classified_secrets"
3105 mariadbd 0x791e544ec4ac classified_secrets 63 6c 61 73 73 69 66 69 65 64 5f 73 65 63 72 65 74 73
aha! the process is mariadbd, with PID 3105
now we know wassup, we start our digging.
Step 1 — Find the MariaDB process (PsList)
python3 vol.py -f nit_breach_2.raw linux.pslist.PsList | grep -iE "maria|mysql"
0x8b10029f5200 3105 3105 1 mariadbd ... 2026-06-18 17:50:00 UTC Disabled
made sure mariadb is running as: mariadbd, PID 3105.
Step 2 — file handles associated with mariadb
python3 vol.py -f nit_breach_2.raw linux.lsof.Lsof --pid 3105 \
| grep -iE "ibd"
3105 mariadbd 78 /var/lib/mysql/nit_numidian_db/classified_secrets.ibd (deleted) inode 266980
3105 mariadbd 79 /var/lib/mysql/nit_numidian_db/core_configuration.ibd (deleted) inode 266982
two InnoDB tablespace files are marked (deleted) but still held open by mariadbd:
| FD | File | Inode |
|---|---|---|
| 78 | classified_secrets.ibd | 266980 |
| 79 | core_configuration.ibd | 266982 |
the attacker rm’d the files, but with MariaDB still holding the descriptors the data remained in the kernel page cache / InnoDB buffer pool.
cheeky bugger.
Step 3 — Confirm the cached inodes (pagecache.Files)
python3 vol.py -f nit_breach_2.raw linux.pagecache.Files \
| grep -iE "classified_secrets|core_configuration"
266980 0x8b10066aca68 REG ... /var/lib/mysql/nit_numidian_db/classified_secrets.ibd 65536
266982 0x8b1006649848 REG ... /var/lib/mysql/nit_numidian_db/core_configuration.ibd 65536
both inodes are present in the page cache. but dumping them gives 0 bytes for these innodes, so the method isn’t dumping the inodepages.
the method is carving out the data pages from the raw image
Step 4 — Find InnoDB data pages
we anchor on something every InnoDB data page contains: the system records infimum and supremum, which bracket every page’s record list.
strings -t d nit_breach_2.raw | grep -E "infimum|supremum"
383642787 infimum
462212259 infimum
468249764 tinfimum
...
These offsets are the InnoDB data pages resident in memory.
Step 5 — Let the data dictionary name the tables and columns
carve the pages around those markers and pull out identifier-looking tokens. sht is guessy asf. InnoDB’s own data dictionary (SYS_FOREIGN, SYS_VIRTUAL, etc.) lives in these pages and lists every table:
import re
data = open('nit_breach_2.raw','rb').read()
for off in (m.start() for m in re.finditer(b'infimum\x00', data)):
page = data[off-100:off+16000]
txt = re.sub(rb'[^\x20-\x7e]', b' ', page).decode()
toks = re.findall(r'[A-Za-z_][A-Za-z0-9_]{4,}', txt)
if any(k in t for t in toks for k in ('token','secret','key','config')):
seen = list(dict.fromkeys(toks))
print(off, seen[:20]); break
477752485 ['infimum', 'supremum', 'SYS_FOREIGN', 'SYS_FOREIGN_COLS', 'SYS_VIRTUAL',
'mysql', 'innodb_table_stats', 'innodb_index_stats', 'transaction_registry',
'gtid_slave_pos', 'nit_numidian_db', 'classified_secrets', 'core_configuration']
we find the schema and the secrets file, next step is reading em rows to try and find smth meaningful
Step 6 — Read the classified_secrets rows
in InnoDB’s compact row format the column name is stored immediately before its value, so dumping the page literally prints column_name + value:
i = data.find(b'classified_secrets')
start = data.rfind(b'infimum', 0, data.find(b'telemetry_encryption_key'))
print(re.sub(rb'[^\x20-\x7e]', b'.', data[start:start+700]).decode())
infimum......supremum...
telemetry_encryption_key 6e6d6374667b6e756d316431346e5f6d336d3072795f7333637233745f3763326238617d
access_token_part_1 6e6d6374667b6e756d
access_token_part_2 316431346e5f6d33
access_token_part_3 6d3072795f733363
access_token_part_4 7233745f37633262
access_token_part_5 38617d
the column names (telemetry_encryption_key, access_token_part_1..5) are output of the carve, not search terms we brought in. The values are looking hexy vexy multiplexy [0-9a-f] — we assemble it all and decode it.s
Step 7 — Assemble and decode
Concatenate access_token_part_1..5 in order (or just take the single telemetry_encryption_key):
parts = ["6e6d6374667b6e756d", "316431346e5f6d33", "6d3072795f733363",
"7233745f37633262", "38617d"]
print(bytes.fromhex("".join(parts)).decode())
nmctf{num1d14n_m3m0ry_s3cr3t_7c2b8a}
Flag
nmctf{num1d14n_m3m0ry_s3cr3t_7c2b8a}
great challenge!