Filesystem forensic image

Is it possible to create a bit-by-bit image of a Mikrotik device and mount it on another OS, like Linux?

I need to perform a forensic analysis on a Mikrotik device, but I was unable to extract the filesystem from the flash storage.

Any technical articles por general guides on what to do/learn will be helpful. Thanks.

I hope it isn’t possible… I know some thieves across the street who try to do just that!

See https://blog.redcrowlab.com/dumping-firmware/, https://ivanorsolic.github.io/post/hardwarehacking1/

Aside from updating, what can be done to prevent someone from making such forensic bit-by-bit images and make sure there is plenty of variance?

Generally by restricting physical access to device, using cabinets with locks, security guards on premises…

You can update as much as you want, if the hypothetical hacker interested in the secrets you store on your router has physical access to the device for enough time, It Is game over, physical extraction from a memory chip Is and will always be possible, as It uses exactly the same mechanisms that allow you to store and retrieve data on/from It.
Then, extracting from these data meaningful info may be easier or more difficult, but in theory It Is always possible.

I was able to dump the MTD devices contents, by PXE booting the device using a OpenWRT firmware using this guide: https://openwrt.org/toh/mikrotik/common. After running binwalk on each dump file, a SquashFS filesystem was found:


root@debian:/home/kauedg/Downloads/mtd# binwalk OpenWrt.mtd5.bin 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
20800         0x5140          Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5570552 bytes, 587 inodes, blocksize: 262144 bytes, created: 2025-01-16 09:19:35
5606140       0x558AFC        xz compressed data
5668086       0x567CF6        xz compressed data
5670160       0x568510        xz compressed data
5672458       0x568E0A        xz compressed data
5673264       0x569130        xz compressed data
5677042       0x569FF2        xz compressed data
5679304       0x56A8C8        xz compressed data
5679718       0x56AA66        xz compressed data
5772539       0x5814FB        xz compressed data
5772643       0x581563        xz compressed data
7187594       0x6DAC8A        xz compressed data
7381067       0x70A04B        Neighborly text, "neighbor discovery-settings set discover-interface-list=LAN /tool mac-server set allowed-interface-list=LAN"
7382574       0x70A62E        Neighborly text, "neighbor discovery-settings set discover-interface-list=!dynamicc"

Then I tried unsquashing the filesystem but got stuck at these errors:

root@debian:/home/kauedg/Downloads/mtd# unsquashfs _OpenWrt.mtd5.bin.extracted/5140.squashfs 
Lseek failed because Invalid argument
read_block: failed to read block @0x701000056100000
read_id_table: failed to read id table block
FATAL ERROR: File system corruption detected

I also unsquashing the filesystem from the RouterOS NPK image file used to flash the device and I was able to get a filesystem:

root@debian:/home/kauedg/Downloads/mtd# unsquashfs _routeros-7.17-smips.npk.extracted/1000.squashfs 
Parallel unsquashfs: Using 8 processors
425 inodes (416 blocks) to write

created 393 files
created 162 directories
created 32 symlinks
created 0 devices
created 0 fifos
created 0 sockets
created 0 hardlinks

root@debian:/home/kauedg/Downloads/mtd# ll squashfs-root/
total 64K
drwxr-xr-x 16 root root 4.0K Jan 16 06:19 .
drwxr-xr-x  5 root root 4.0K Jan 21 19:09 ..
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 bin
drwxr-xr-x  8 root root 4.0K Jan 16 06:19 bndl
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 boot
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 dev
lrwxrwxrwx  1 root root   11 Jan 16 06:19 dude -> /flash/dude
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 etc
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 flash
drwxr-xr-x  3 root root 4.0K Jan 16 06:19 home
drwxr-xr-x  3 root root 4.0K Jan 16 06:19 lib
drwxr-xr-x  5 root root 4.0K Jan 16 06:19 nova
lrwxrwxrwx  1 root root    9 Jan 16 06:19 pckg -> /ram/pckg
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 proc
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 ram
lrwxrwxrwx  1 root root    9 Jan 16 06:19 rw -> /flash/rw
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 sbin
drwxr-xr-x  2 root root 4.0K Jan 16 06:19 sys
lrwxrwxrwx  1 root root    7 Jan 16 06:19 tmp -> /rw/tmp
drwxr-xr-x  5 root root 4.0K Jan 16 06:19 var




I’m looking for a way to unsquash the dumped filesystem without errors.

Try with extract binwalk command line option:

binwalk --run-as=root -e OpenWrt.mtd5.bin

This should extract any supported extractable data found in image including Squashfs.

I think unsquash Is very “strict” and expects a “sound” filesystem and throws a fit even if minor issues are found.
I cannot remember if a tool for recovery/fix exists, something like dmde which Is excellent for other filesystems.
7-zip should be capable of reading a squashfs and in some cases It may be more tolerant.
There may be ways to decompress the single zlib blocks and then reassemble the stuff, but provided that It works, It can usually work for text contents and similar - given the nature of squashfs - fragmentation should not be and issue, but beginning and end of files, even using trid or file to detect signatures will required a lot of manuale work with a hex viewer/editor.

Yes, that’s exactly how I’m extracting the mtd’s dump content. It creates a file named [XXXX].squashfs and tries to unsquash it automatically but it only gives an empty directory. When I try unsquashing it manually, I get the “Lseek failed because Invalid argument” error stated above.

Using 7z was a great idea but no cigar

root@debian:/home/kauedg/Downloads/mtd# 7z l _OpenWrt.mtd5.bin.extracted/5140.squashfs 

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz (806EB),ASM,AES-NI)

Scanning the drive for archives:
1 file, 5570552 bytes (5440 KiB)

Listing archive: _OpenWrt.mtd5.bin.extracted/5140.squashfs


ERROR: _OpenWrt.mtd5.bin.extracted/5140.squashfs : opening : E_FAIL

Errors: 1


System ERROR:
E_FAIL

I found this OpenWRT forum post (https://forum.archive.openwrt.org/viewtopic.php?id=67564) about using a tool named nand-dump-tool.py (NandTool/Nand-dump-tool.py at master · Hitsxx/NandTool · GitHub)to separate the OOB area but I’m still searching how to get the “ID code” parameter for a Mikrotik hAP lite (RB941-2nD)

Could be that MT uses non-standard Squashfs for devices or it was not correctly dumped or fs is corrupted on flash. I know for sure that from CHR image Squashfs can be extracted with binwalk, done it many times.

I dumped using both OpenWRT’s MTD backup function and dd tool. Also dumped with dd the /etc/mtdXro and /etc/mtdblockX devices. All files with same sequential number are the same, so unless there’s some gotcha on the acquisition itself, I believe their integrity is ok. Filesystem can’t be corrupt on flash because I’ve freshly reflashed the device with Mikrotik’s released RouterOS for the device, and, as I stated before, I dumped the filesystem from the npk file succesfully.

I’m following this https://fastcall.medium.com/dumping-firmware-from-a-router-5d7e819199fd article regarding OOB, because I think it’s causing the errors.

[double post]

Nice find. :slight_smile:
The related blog post:
https://www.j-michel.org/blog/2014/05/27/from-nand-chip-to-files
explains in detail how the oob/spare data works :slight_smile: , and on the main page:
https://github.com/Hitsxx/NandTool/tree/master
it is clear that you can use oob and page size instead of chip ID (as it seems that there is no Winbond entry in the small program database of chips).
The “normal” mtd should have a way to show (detected/guessed) sizes. :confused:
The flash should be however a Winbond W25Q128JVSM, though finding the right values in the datasheet;
https://www.winbond.com/hq/product/code-storage-flash-memory/serial-nor-flash/?__locale=en&partNo=W25Q128JV
seems not easy/immediate.
But with a hex viewer it should be identifiable.

That’s exactly the flash chip model. I got to the same datasheet as you did, but I’m inexperienced with flash chips. Could you help me identify the page and OOB size for this one?

I found this in the datasheet:

The W25Q128JV array is organized into 65,536 programmable pages of 256-bytes each

But I can’t find a reference to OOB anywhere in it.

Neither can I, but again, with a hex viewer and a little patience it should be easy to identify the oob, see here another example:
https://redballoonsecurity.com/flash-dump/

Some older topic https://forum.archive.openwrt.org/viewtopic.php?id=70636, but here is stated 16-byte OOB but for different MTD model of same brand with same size.
Also as I see here https://openwrt.org/docs/techref/flash OpenWrt has nanddump which prints mtd info, like “Block size 131072, page size 2048, OOB size 64”, also Nand-dump-tool.py should print OOB size according to this line from source code.
If you missed it from dump tool, try to dump again and see.

Re-thinking about it, I believe you can also attempt “brute-forcing” the values.
It is not like there are tens or hundreds of possibilities, the single oob record cannot logically be smaller than 8 bytes, and more likely is either 16 or 32 bytes (i don’t think that odd values or non multiples of 8 or 16 are used), grouped maybe in sets of 4 or 8.
The size of your dump in bytes, minus 65536*256= 16,777,216 is the total of oob space.
Let’s say that your dump is 17,301,504.
The difference is 524,288.
That can be:
65,536x8 → page size 256
32,768x16-> page size 512
16,384x32-> page size 1024
8,192x64-> page size 2048
4,096x128-> page size 4096
2,048x256-> page size 8192 ← improbable

Running that python script five or six times with these values (and with the parameter “guess” or double the times, once with “adjacent” and once with “separate”) shouldn’t take much time.

I was writing a long write-up on how I was troubleshooting the issue on the OOB but I found out something else was the problem. I downloaded the newest version available for squashfs-tools (https://github.com/plougher/squashfs-tools), compiled it with the tracing function enabled and fiddled around with the two squashfs files I dumped: the one from the RouterOS firmware npk file and the one I extracted from the device using nanddump, after flashing it.

I checked if the information about the files could be read

# ./unsquashfs -s ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs 
squashfs: read_bytes: reading from position 0x0, bytes 96
Found a valid SQUASHFS 4:0 superblock on ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs.
Creation or last append time Thu Jan 30 08:26:56 2025
Filesystem size 5743084 bytes (5608.48 Kbytes / 5.48 Mbytes)
Compression xz
Block size 262144
Filesystem is exportable via NFS
Inodes are compressed
Data is compressed
Uids/Gids (Id table) are compressed
Fragments are compressed
Tailends are not packed into fragments
Xattrs are not stored
Duplicates are removed
Number of fragments 51
Number of inodes 593
Number of ids 1
squashfs: sBlk.s.inode_table_start 0x57706c
squashfs: sBlk.s.directory_table_start 0x57848e
squashfs: sBlk.s.fragment_table_start 0x579dac
squashfs: sBlk.s.lookup_table_start 0x57a1d6
squashfs: sBlk.s.id_table_start 0x57a1e4
squashfs: sBlk.s.xattr_id_table_start 0xffffffffffffffff

# ./unsquashfs -s ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs 
squashfs: read_bytes: reading from position 0x0, bytes 96
Found a valid SQUASHFS 4:0 superblock on ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs.
Creation or last append time Thu Jan 30 08:26:56 2025
Filesystem size 5743084 bytes (5608.48 Kbytes / 5.48 Mbytes)
Compression xz
Block size 262144
Filesystem is exportable via NFS
Inodes are compressed
Data is compressed
Uids/Gids (Id table) are compressed
Fragments are compressed
Tailends are not packed into fragments
Xattrs are not stored
Duplicates are removed
Number of fragments 51
Number of inodes 593
Number of ids 1
squashfs: sBlk.s.inode_table_start 0x57706c
squashfs: sBlk.s.directory_table_start 0x57848e
squashfs: sBlk.s.fragment_table_start 0x579dac
squashfs: sBlk.s.lookup_table_start 0x57a1d6
squashfs: sBlk.s.id_table_start 0x57a1e4
squashfs: sBlk.s.xattr_id_table_start 0xffffffffffffffff

The only difference was the filename

# diff <(./unsquashfs -s ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs) <(./unsquashfs -s ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs)
2c2
< Found a valid SQUASHFS 4:0 superblock on ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs.
---
> Found a valid SQUASHFS 4:0 superblock on ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs.

This is the initial output from the npk file listing:

# ./unsquashfs -l ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs | head
squashfs: read_bytes: reading from position 0x0, bytes 96
squashfs: read_id_table: no_ids 1
squashfs: read_bytes: reading from position 0x57a1e4, bytes 8
squashfs: read_bytes: reading from position 0x57a1de, bytes 2
squashfs: read_block: block @0x57a1de, 4 uncompressed bytes
squashfs: read_bytes: reading from position 0x57a1e0, bytes 4
squashfs: read_bytes: reading from position 0x57a1d6, bytes 8
squashfs: read_fragment_table: 51 fragments, reading 1 fragment indexes from 0x579dac
squashfs: read_bytes: reading from position 0x579dac, bytes 8
squashfs: read_bytes: reading from position 0x579c1a, bytes 2
[...]

This is the full output for the mtd5 dumped file:

# ./unsquashfs -l ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs 
squashfs: read_bytes: reading from position 0x0, bytes 96
squashfs: read_id_table: no_ids 1
squashfs: read_bytes: reading from position 0x57a1e4, bytes 8
squashfs: read_bytes: reading from position 0xffffffffffffffff, bytes 2
Lseek failed because Invalid argument
read_block: failed to read block @0xffffffffffffffff
read_id_table: failed to read id table block
FATAL ERROR: File system corruption detected

I manually checked the superblock section (96 bytes) and found out that the value 0xffffffffffffffff is in the “Xattr table” field, which the documentation (https://dr-emann.github.io/squashfs/squashfs.html) says

“The Xattr table, fragment table and export table are optional. If they are omitted from the archive, the respective fields indicating their position must be set to 0xFFFFFFFFFFFFFFFF (i.e. all bits set).”

Now let’s compare both files’ headers

# xxd -l 96 ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs 
00000000: 6873 7173 5102 0000 0062 9b67 0000 0400  hsqsQ....b.g....
00000010: 3300 0000 0400 1200 c002 0100 0400 0000  3...............
00000020: 5c0b d810 0000 0000 eca1 5700 0000 0000  \.........W.....
00000030: e4a1 5700 0000 0000 ffff ffff ffff ffff  ..W.............
00000040: 6c70 5700 0000 0000 8e84 5700 0000 0000  lpW.......W.....
00000050: ac9d 5700 0000 0000 d6a1 5700 0000 0000  ..W.......W.....

# diff <(xxd -l 96 ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs) <(xxd -l 96 ../../_routeros-7.17.1-smips.npk.extracted/1000.squashfs)
#

They are the same, right? When I try to unsquash the firmware’s file, it runs without problems, but the absolutely same header, even with the -no-xattrs option, can’t be read from the extracted file. I event checked the “flags” field and the bit for the option “Xattrs are stored uncompressed” was disabled and the bit for “There are no Xattrs in the archive was enabled”

# ./unsquashfs -l -no-xattrs ../../_mtd5.nanddump.bin.extracted/7534D0.squashfs 
squashfs: read_bytes: reading from position 0x0, bytes 96
squashfs: read_id_table: no_ids 1
squashfs: read_bytes: reading from position 0x57a1e4, bytes 8
squashfs: read_bytes: reading from position 0xffffffffffffffff, bytes 2
Lseek failed because Invalid argument
read_block: failed to read block @0xffffffffffffffff
read_id_table: failed to read id table block
FATAL ERROR: File system corruption detected

Is this a unsquashfs command bug or what?