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:

FDFileInode
78classified_secrets.ibd266980
79core_configuration.ibd266982

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!