This article will go over what I did to gain root access on the Meraki MS220-8P switch. TLDR: I modified both stage1 and stage2 flash so that I could eventually set a root password in /etc/passwd as well as change the default shell to /bin/sh.

For information on the switch itself, see Meraki MS220-8P.

Rooting the Switch[edit | edit source]

Without the ability to start a shell on the first-stage bootloader, I would need to modify the initramfs to change bootsh's behavior. Getting a shell at the first-stage bootloader would allow me to dump and inspect the rest of the NAND flash.

Examining the Boot Image[edit | edit source]

The initial bootloader, the VCore-III ROM Loader that is stored in the first 256Kb of the NOR flash, loads the first-stage bootloader into memory and verifies its integrity using a CRC32 check against the checksum stored as a 32bit word 16bytes into the boot image. If the CRC check fails, it will attempt to load the next partition (by jumping to the next hard-coded memory block containing the SPIM header, see below) and try again.

You can view the VCore-III ROM Loader source at meraki-firmware/linux-2.6.32/arch/mips/vcoreiii/loader/head.S.

When the CRC check fails, the loader will jump execution to the second loader. Your boot logs will show the double initialization as well:

LinuxLoader built Nov 12 2014 18:01:50
init_pll ok
init_spi ok
init_memctl ok
wait_memctl ok
Training DRAM ok
init_irq ok
init_dram_uncached ok
init_icache ok
init_dcache ok
enable_caches ok
init_board ok
Low level initialization complete, exiting boot mode
LinuxLoader built Nov 12 2014 18:01:50
init_pll ok
init_spi ok
init_irq ok
init_dram_uncached ok
init_icache ok
init_dcache ok
enable_caches ok
init_board ok
Low level initialization complete, exiting boot mode

The boot images must be of size 3932160 bytes (ie. MTD partition size of 0x3C0000) and containing the following header.

Boot Image Header

LOADER_MAGIC 32bit Kernel Address 32bit Payload Length 32bit Entrypoint Address 32bit
CRC32 CHKSUM 32bit Reserved 1 32bit Reserved 2 32bit Reserved 3 32bit

Boot Data. This is of length Payload Length

Example Boot Header

53 50 49 4D 00 00 10 80 E0 F1 38 00 B0 16 39 80
XX XX XX XX 00 00 00 00 00 00 00 00 00 00 00 00

00 00 ... boot data ... 00 00.

Note: Values are in little endian so a 32bit value of 0xAABBCCDD is stored as 0xDDCCBBAA.

The LOADER_MAGIC is 0x4d495053 in little endian looks like 53 50 49 4d or ASCII for SPIM.

The CRC32 CHKSUM value is calculated against the entire boot image of the given Payload Length with the CRC32 value zeroed out.

Modifying The Boot Image[edit | edit source]

The data after the boot image headers mentioned in the previous section contains the kernel and whatever else that's embedded into the kernel including a ramdisk. The ramdisk from the stock kernel is located after byte 0x339604.

To help facilitate modifying this ramdisk to do my bidding, I will split the data portion into 3 parts: The 'pre' ramdisk portion, the ramdisk portion, and the 'post' ramdisk portion. Pre-portion can be generated with dd if=boot1 of=boot1-patched-pre bs=1 count=$((0x339604)) and the post-portion with dd if=boot1 of=boot1-patched-post bs=1 skip=$((0x38e9c8)).

To make changes to the boot file:

  1. Extract the boot data using cat ../boot-data.xz | xz -d | cpio -i
  2. Make your changes to the ramdisk
  3. Recreate the cpio find . | cpio -o -c > ../modified.cpio
  4. Recompress cat modified.cpio | xz -c9 --check=crc32 > modified.xz
  5. If image is smaller, pad with 0's for the difference cat modified.xz zeros.bin > modified.xz
  6. If image is larger, append post data, then adjust data lengths after payload and at the start of the boot file
  7. Reassemble boot file cat boot1-patched-pre modified.xz boot1-patched-post > boot1-patched
  8. Zero out CRC code
  9. Calculate the CRC code php ../crc32.php boot1-patched
  10. Write new CRC code
  11. Reassemble the entire image cat loader1 boot1-patched loader2 boot2 rsvd bootubi conf stackconf syslog > dump-patched.dat
  12. Copy the dump-patched.dat file to raspberry pi
  13. Flash it flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=600 -c "MX25L12835F/MX25L12845E/MX25L12865E" -w dump-patched.dat.

The Journey to Rooting the Switch[edit | edit source]

This section will run on as a semi-journal on what I did in order to gain access to the switch.

Stage 1[edit | edit source]

Since I don't have a 32bit MIPS crosscompiler and being the lazy guy I am, I tried the simplest approach which was to patch the bootsh binary so that it calls a shell instead of kexec which is on the ramdisk. I am assuming that the bootsh file mounts the filesystems on the empty directories in the ramdisk.

To extract the embedded initramfs from the boot MTD partition, locate the start and end xz file header and footers. In my case, the initramfs data resides at 0x339604 with length 349124. Using dd, it can be extracted with:

dd if=bin/boot1 of=bin/boot1-patched-pre  bs=1 count=$((0x339604))
dd if=bin/boot1 of=bin/boot-data.xz       bs=1 skip=$((0x339604))  count=349124
dd if=bin/boot1 of=bin/boot1-patched-post bs=1 skip=$((0x38e9c8))

The boot1-patched-pre contains the boot image header and the Linux Kernel. boot1-patched-post contains mystery data. The exact structure of the file must remain intact which means the payload data has to be the same as the original. The extracted ramfs data is a cpio compressed with xz and can be extracted with zcat boot-data.xz | cpio -i on an empty directory.

After extracting the initramfs contents, I hex-edited kexec -f %s --reuse-cmdline to /bin/sh with a bunch of nulls after and tried booting. This did not work as the boot process continued on to the second kernel, implying that the kexec call still got made.

I hex-edited all reference to kexec to /sh with /sh symlinked to /bin/sh but that too did not work. It just tries over and over with this error:

[    3.633000] execl failed: 2
[    3.638000] kexec died
[    3.640000] Calling kexec for /dev/mtdblock/part1 failed!
[    3.647000] execl failed: 2
[    3.651000] kexec died
[    3.654000] Calling kexec for /dev/mtdblock/part2 failed!

Since I removed the kexec binary and replaced all kexec strings to /sh that is symlinked to /bin/sh, having the boot process fail in this manner implies that either the /kexec or the /sh binary is being called. Both of which are symlinked to /bin/sh which suggests that /bin isn't mounted.

I got a MIPS cross compiler and compiled busybox without a bunch of applets so that the final binary can fit inside the size of the existing xz archive. I wasn't totally sure if busybox would like being called directly by the kernel (because it might not know which applet to run?) so I created a /init script that invoked /sh that is symlinked to busybox and then called /sh.

The first few times failed. The first time, the CPU architecture wasn't correct (the MIPS compiler targetted rel6, not rel2). The next few times also failed because I compiled it with the wrong endianness. All these failed attempts resulted in

Starting init: /bin/sh exists but couldn't execute it (error -8) 
Kernel Panic - not syncing: No working init found. Try passing init= option to Kernel.


I eventually got busybox compiled for MIPS32 rel2 in little endian with these gcc flags:

root@fc:/tmp/busybox-1.30.0# make CROSS_COMPILE=mips-mti-linux-gnu- CFLAGS='-Wa,-mips32r2,-march=24kec,-mtune=24kec -mel -EL' LDFLAGS='-Wa,-mips32r2,-march=24kec,-mtune=24kec -mel -EL' ARCH=mips KBUILD_VERBOSE=1 -j 8

## The resulting binary should be like this
root@fc:/tmp/busybox-1.30.0# file busybox
busybox: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, stripped

Using this busybox executable, I rebuilt the ramdisk, repackaged the files, and reflashed it onto the switch and hoped for the best.

[    3.057000] devtmpfs: mounted

[    3.069000] Freeing unused kernel memory: 484K
sh: can't execute 'echo': No such file or directory

BusyBox v1.30.0 (2019-01-16 20:37:46 MST) hush - the humble shell

/ # uname -a
Linux (none) 3.18.102-meraki-elemental #1 Fri Apr 13 11:18:08 PDT 2018 mips GNU/Linux

My echo "Hello world" inside failed failed since there was no echo in the PATH, but I got a shell!

After exploring around with my very limited busybox (which in my haste to minimize size, I apparently did not bundle cd into...), it turns out that /dev/mtdblock11 contains a 640K squashfs filesystem containing a better featured busybox. To make my life easier, I mounted and copied the entire contents to the ramdisk. No other devices seem to have a filesystem I can mount. I integrated this into init, so that the switch comes up with a more usable system.

/ # busybox mkdir  /proc /sys
/ # busybox mount -t proc none /proc
/ # busybox mount -t sysfs none /sys
/ # busybox mkdir /rootfs
/ # busybox rm /lib /sbin /usr /bin
/ # busybox mount -o ro /dev/mtdblock11 /rootfs 
/ # busybox cp -r /rootfs/* /
/ # /bin/sh

Unfortunately, there is no networking enabled with this first kernel. This will make copying data in and out a pain since it has to go through the serial connection.

## No networking is a bummer.
/ # ifconfig -a
lo        Link encap:Local Loopback
          LOOPBACK  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

CPU information:

/ # cat /proc/cpuinfo
system type             : Vitesse VCore-III VSC7425
machine                 : Unknown
processor               : 0
cpu model               : MIPS 24KEc V5.4
BogoMIPS                : 276.99
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 16
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0ffc, 0x0ffc, 0x0ffb, 0x0ffb]
isa                     : mips1 mips2 mips32r1 mips32r2
ASEs implemented        : mips16 dsp
shadow register sets    : 1
kscratch registers      : 0
package                 : 0
core                    : 0
VCED exceptions         : not available
VCEI exceptions         : not available

/ # cat /proc/meminfo
MemTotal:         125548 kB

Since the first 9 MTD are from the NOR flash device, everything after is from the larger NAND flash device. Since I have no ability to physically dump the contents of the NAND drive, being able to copy the data from the running kernel at this stage is extremely helpful.

The next task now is to get the contents of the flash memory and figure out how to gain root access on the second stage. Specifically, it would be nice to know if there is already a SSH account I can use, what the <MERAKI> console that gets brought up is (and what commands it actually takes), and if it is possible to add my own account in.

I tried to dd data out from the serial connection, but it seems like data randomly gets corrupted over the serial console. There are utilities that can be used to transfer files over serial such as lzrzr or kermit, but none of them exist. I figured it'd be easier to write my own shell script that chunks the data out using base64 and then sum these chunks for data integrity checks. It turns out, baud rates higher than 115200 yields a very unstable serial connection and it ends up garbling up the data that's sent across. After spending a few hours hacking a few scripts together, I am now able to send (albeit slowly, <8kb/s) files in and out of the switch.

I transferred mtd{9-21}ro out of the switch for analysis. Each MTD looks like this:

data9:  ~250k  Nulls, of length 0x0003dd8b
data10: 128k   Switch Data (Model, Serial Number)
data11: 540k   Squashfs filesystem, little endian, version 4.0, 548778 bytes, 166 inodes, blocksize: 131072 bytes, created: Fri Apr 13 18:19:18 2018
data12: 20MB   data
data13: 20MB   data, old copy/backup of the above?
data14: 8MB    UBIfs image, sequence number 1, length 4096, CRC 0x8a351c73
data15: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data16: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data17: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data18: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data19: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data20: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
data21: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped

The ELF binaries stored in mtdblock15 - mtdblock21 are all different versions of what seems to be the switch CLI management program. I later learn that these are different versions of SMBStax binaries for eCos. I'm unsure whether these actually get executed anywhere considering that they're supposed to run under eCos and not Linux.

mtdblock12 and mtdblock13 contains the second stage kernel as well as an embedded ramdisk image similar to the first stage boot1/boot2 partitions. The kernel that is there is slightly older at version 3.18.57-meraki-elemental. The ramdisk image extracted out contains all the Meraki OS binaries and scripts which we'll get into soon.

mtdblock14 is a UBIfs image that gets mapped to /dev/ubi1_4 and gets mounted as /storage when the switch boots into the second stage. This partition contains some logs, core dumps, last known good configs, etc.

# mount -t ubifs /dev/ubi1_4 /mnt
# find /mnt

To boot the next stage, you can copy the kexec binary from the original boot1/boot2 partition over and then run:

# ./kexec -f /dev/mtdblock12

However, this will just bring you back to the locked down console.

Stage 2[edit | edit source]

With the second stage ramfs image obtained, I can now dig through how the Meraki OS actually works. The file listing of the ramdisk can be found at After extracting the image and digging around the filesystem, a few interesting points to note:

  • /etc/passwd contains some accounts:
    leo@fc:/home/leo/extracted/stage2/extracted% cat etc/passwd
    support:x:1:1:Meraki Support:/support:/bin/ash
  • The 'mf' account runs /usr/bin/serial_logincheck, which is the same getty that gets launched on ttyS0 in /etc/inittab:
    leo@fc:/home/leo/extracted/stage2/extracted% cat etc/inittab
    ttyS0::respawn:/usr/bin/serial_logincheck --login
  • There is an empty /etc/dropbear/authorized_keys file
  • lighttpd runs with 'authorized_ips' set to 6.0.0.{2,10,11,12} and
  • Speed test script in /www downloads from
  • The Vitesse kernel module is at /lib/modules/jaguar/vtss_core.ko. Perhaps I can copy this and load it on the first stage kernel?
  • usr/bin/config_updater writes to the NAND flash using nanddump, nandwrite, flash_erase, etc. This is probably the thing that phones home and interfaces with the switch_brain.
  • /usr/bin/switch_brain seems to manage the switch hardware and services? Deals a lot with /click partition.
  • The only mention of OpenWRT is the /etc/ipkg.conf file.
  • /usr/bin/board_data_config gets or sets board config values. (mfg_done if set sets it to manufacturing mode).
  • /usr/bin/odm is a script that does lots of diagnostic things including writing a new firmware.
  • /usr/bin/serial_logincheck appears to lock the console down except if the switch is in manufacturing mode. Despite its threatening warning, it does not appear to actually log anything to the cloud.

Getting networking working in the stage1 kernel would be nice as it would allow me to easily copy files in and out of the switch. My shell scripts to copy data in/out reliably can only copy files at around 6-7kb/s. Since the kernel module for networking is probably part of the vtss_core.ko, I'll copy and then load the module.

Modules are loaded by the /etc/init.d/S10boot startup script. The type of CPU or board determines which module gets loaded. The MS220-8P board has the VSC7425 CPU which will load the luton26/vtss_core.ko module. The module is loaded with insmod provided by busybox inside a wrapper function called modload.

## Load vtss_core and unload the module without board_desc
insmod/lib/modules/$vtss_board/vtss_core.ko board_desc=$switch_board && modrm /lib/modules/$vtss_board/vtss_core.ko

Of course, the kernel versions mismatch and there is no force option. Ugh.

[28928.328000] vtss_core: version magic '3.18.57-meraki-elemental mod_unload MIPS32_R2 32BIT ' should be '3.18.102-meraki-elemental mod_unload MIPS32_R2 32BIT '

The next approach is just to modify the second stage initramfs so that the root account has a password set and the serial console isn't locked down.

Modifying the second stage boot image.

The image has another header. It looks like this time, the 32bit words are in big endian.

Boot Image Header

LOADER_MAGIC Payload Address Payload Length SHA1

Empty Header Data until Payload Address

Example Boot Header

8E 73 ED 8A 00 00 40 00 01 11 29 9C 6A 33 34 92
DA 33 DC 66 36 1E C9 3C FE DD D2 8C EE 1E 33 ED

FF FF .. FF FF until Payload Address

The SHA1 checksum is done against the payload only. If the hash is incorrect, kexec will tell you:

/ # kexec -ld /dev/mtdblock12
Try gzip decompression.
kernel: 0x76aff010 kernel_size: 0x140e800
meraki_part_mips: SHA1 doesn't match

Erase and then flash the image on the NAND flash:

# flash_erase /dev/mtdchar/part1 0 0
# nandwrite -pam /dev/mtdchar/part1  firmware.bin

The kernel can be loaded and executed once the SHA1 hash was fixed. My change to make the console run /bin/sh instead of the locked down console worked like a treat too.

[   12.586000] UBIFS: reserved for root: 347364 bytes (339 KiB)
[   12.592000] UBIFS: media format: w4/r0 (latest is w4/r0), UUID 2EA2ACA1-07FA-472B-BC2D-F0F2DB4314D3, small LPT model
In manufacturing: FALSE
In rma [   12.615000] /initmode: FALSE
: reading file /storage/config: No such file or directory
init started: BusyBox v1.25.1 (2018-08-24 12:51:28 PDT)

BusyBox v1.25.1 (2018-08-24 12:51:28 PDT) built-in shell (ash)

/ # [   13.515000] sysctl: error: 'kernel.softlockup_panic' is an unknown key
[   13.523000] sysctl: error: 'kernel.watchdog_thresh' is an unknown key
[   13.768000] sh: write error: Device or resource busy
[   13.882000] vtss_core: module license '(c) Vitesse Semiconductor Inc.' taints kernel.
[   13.890000] Disabling lock debugging due to kernel taint
[   14.490000] switch: 'Meraki MS220-8' board detected
[   14.837000] grep: /storage/config: No such file or directory
[   15.446000] sysctl -w vm.panic_on_oom=2
[   15.473000] vm.panic_on_oom = 2
[   16.087000] click: starting router thread pid 745 (87bfca00)
[   16.943000] Single synchronous check for reset
[   17.252000]
[   17.289000] boot 47 build switch-10-201808241214-G0a4ba17b-rel-owner board elemental mac 0C:8D:DB:7E:D7:76
[   17.324000] Module: vtss_core  .text=0xc1411000 .data=0xc14a90b0 .bss=0xc14a9320
[   17.324000] Module: proclikefs  .text=0xc007c000 .data= .bss=0xc007d040
[   17.324000] Module: merakiclick  .text=0xc182c000 .data=0xc197c800 .bss=0xc197ca80
[   17.324000] Module: elts_meraki  .text=0xc1f59000 .data=0xc224faa0 .bss=0xc22513d0
[   17.324000] Module: vc_click  .text=0xc23ba000 .data=0xc23ebfa0 .bss=0xc23ec130
[   17.488000] ls -1 /sys/fs/pstore/dmesg-ramoops-* 2>/dev/null
[   17.520000] /usr/bin/check_bootreason: reading file : No such file or directory
[   20.336000] !!!!! {/usr/bin/switch_brain} opening /click/switch_port_table/dump_stack_info_and_reset_stack_change failed: No such file or directory
[   22.423000] chatter: from_sw0 :: FromVitesse: initializing fdma
[   23.566000] chatter: dhcp_tracker :: DHCPTracker: skipping undersized restore buffer (buf size: 0)
[   24.918000] !!!!! {/usr/bin/switch_brain} failed writing /click/switch_port_table/set_port_storm_control  errno 2 len 211 data: "PORT 1, ENABLED true\nPORT 2, ENABLED ..."
[   25.884000] chatter: big_acl :: BigACL: skipping undersized restore buffer (buf size: 0)

/ #
/ #

Other Failures[edit | edit source]

Here are a collection of failures when I tried various things.

Bad XZ Encoding[edit | edit source]

When building the ramdisk, you need to compress with -C crc32 or else you will get this:

[    0.106000] mkp_lg: Input was encoded with settings that are not supported by this XZ decoder
[    0.106000] Kernel panic - not syncing: Input was encoded with settings that are not supported by this XZ decoder
[    0.106000] Rebooting in 5 seconds..LinuxLoader built Nov 12 2014 18:01:50

The first search result returned this post which suggested compressing the archive using xz -C crc32 -z -c init > init.xz.

Misaligning Ramdisk in Boot Image[edit | edit source]

If you misalign the embedded ramdisk, the kernel won't be able to mount it but the kernel will print out all the block devices it sees. Fix this by making sure the ramdisk is exactly the same size as the original.

[    2.544000] devtmpfs: error mounting -2
[    2.548000] Warning: unable to open an initial console.
[    2.555000] VFS: Cannot open root device "(null)" or unknown-block(0,0): error -2
[    2.563000] Please append a correct "root=" boot option; here are the available partitions:
[    2.571000] 1f00          131072 mtdblock0  (driver?)
[    2.576000] 1f01             256 mtdblock1  (driver?)
[    2.582000] 1f02            3840 mtdblock2  (driver?)
[    2.587000] 1f03             256 mtdblock3  (driver?)
[    2.592000] 1f04            3840 mtdblock4  (driver?)
[    2.597000] 1f05             512 mtdblock5  (driver?)
[    2.602000] 1f06            6144 mtdblock6  (driver?)
[    2.607000] 1f07             256 mtdblock7  (driver?)
[    2.613000] 1f08            1024 mtdblock8  (driver?)
[    2.618000] 1f09             256 mtdblock9  (driver?)
[    2.623000] 1f0a             126 mtdblock10  (driver?)
[    2.628000] 1f0b             536 mtdblock11  (driver?)
[    2.633000] 1f0c           20538 mtdblock12  (driver?)
[    2.639000] 1f0d           20538 mtdblock13  (driver?)
[    2.644000] 1f0e            8316 mtdblock14  (driver?)
[    2.649000] 1f0f            2095 mtdblock15  (driver?)
[    2.654000] 1f10            2632 mtdblock16  (driver?)
[    2.660000] 1f11            2242 mtdblock17  (driver?)
[    2.665000] 1f12            2223 mtdblock18  (driver?)
[    2.670000] 1f13            2671 mtdblock19  (driver?)
[    2.675000] 1f14            2681 mtdblock20  (driver?)
[    2.681000] 1f15            2674 mtdblock21  (driver?)
[    2.686000] mkp_lg: VFS: Unable to mount root fs on unknown-block(0,0)
[    2.686000] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[    2.686000] Rebooting in 5 seconds..

Failed Attempt at Expanding the Ramdisk[edit | edit source]

The smallest shell I can find is the busybox 1.20 version at 1.5MB. This will cause the archive to be larger than the original which will screw with how the archive gets loaded if I just dump the data in. The data immediately after the archive is also a concern since I have no clue what it is or what it does. My obervation is that immediately after the end of the archive data, there is a 32bit word containing the length of the archive data, followed by some data, padded by 00's to the next 16byte. So, I swapped the archive data out with the larger version with busybox, rewrote the 32bit word to the new length, updated the data length at the start of the boot image as well as the CRC32 code and tried booting it. No go, with the following error:

[    0.106000] mkp_lg: compression method <<▒i=▒▒R;▒,▒C▒8▒C▒\▒C▒x▒C▒▒▒C▒ not configu
[    0.106000] Kernel panic - not syncing: compression method <<▒i=▒▒R;▒,▒C▒8▒C▒\▒C▒x▒C▒▒▒C▒ not configu
[    0.106000] Rebooting in 5 seconds..LinuxLoader built Nov 12 2014 18:01:50

Serial Speed[edit | edit source]

You can raise the serial speed, but anything higher than the default 115200 baud seems to be unstable for me and it ends up corrupting data.

/ # stty -F /dev/ttyS0 921600
/ # dmesg -n 1

See Also[edit | edit source]

Source[edit | edit source]

Open source code can be found at:

OpenWRT[edit | edit source]

Flash Memory[edit | edit source]