Samsung NX mini (M7MU) Firmware File Format

Georg Lukas
Wed, 30 Apr 2025 17:51:47 +0200
hardware, net, reverse-engineering, samsung-nx

In 2014 and 2015, Samsung released the NX mini and NX3000/NX3300 cameras as part of their mirrorless camera line-up. My 2023 archaeological expedition showed that they use the Fujitsu M7MU SoC, which also powers the camera in the dual-SoC Exynos+M7MU Galaxy K-Zoom. This blog post performs a detailed step-by-step reverse engineering of the firmware file format. It is followed by reverse-engineering the LZSS compression, in order to obtain the raw firmware image for actual code analysis.

The TL;DR results of this research can be found in the project wiki: M7MU Firmware Format / SF_RESOURCE.

Prelude

Two years ago I did a heritage analysis of all NX models and found some details about the history of the Milbeaut MB86S22A SoC powering the above models. The few known details can be read up in that post.

Copyright (c) 2<80>^@^@5-2011, Jouni Ma^@^@linen <*@**.**>
^@^@and contributors^@^B^@This program ^@^Kf^@^@ree software. Yo!
u ^@q dis^C4e it^AF/^@<9c>m^D^@odify^@^Q
under theA^@ P+ms of^B^MGNU Gene^A^@ral Pub^@<bc> License^D^E versPy 2.

The firmware files are using some sort of compression that neither I nor binwalk knew about, so the further analysis was stalled. Until April 2025. Nina wrote a fascinating thread about the TRON operating system, I chimed in with a shameless plug of my own niche knowledge of µITRON on Samsung cameras, and got Igor Skochinsky nerd-sniped. Igor quickly realized it is a variant of LZSS, similar to a reverse-engineered HP firmware.

Together, we went on a three-week journey of puzzles within puzzles. This post is the cleaned up documentation of the first part of that treasure hunt, hoping to inspire and guide other reverse engineers.

Collecting .bin files

To analyze the format it's helpful to obtain as many diverse specimens as possible. Samsung still offers the latest camera firmware versions: NX mini 1.10, NX3000 1.11, NX3300 1.01. Older versions can be obtained from the NX Files archive. The Galaxy K Zoom firmware can be downloaded from portals like SamFw. The interesting part is stored in the sparse ext4 root filesystem as /vendor/firmware/RS_M7MU.bin. With only 6.2MB it's the smallest specimen, the dedicated camera files are over 100MB each.

The details of the header format were "discovered" back in 2023 by doing a github search for "M7MU", and finding an Exynos interface driver. The driver documents the header format that matches all known specimens.

The header has three interesting parts for further analysis (the values and hex-dumps in this blog post are all taken from DATANXmini.bin version 1.10; the header values are little-endian):

the "writer" (writer_load_size = 0x4fc00 and write_code_entry 0x40000400)

the "code" (code_size = 0xafee12 and offset_code = 0x50000)

the "sections"

The section_info field is an array of 25 integers, the first one looking like a count, and the following ones like tuples of [number, size] (we can rule out [number, offset] because the second column is not growing linearly):

section_info = 00000007
          00000001 0050e66c
          00000002 001a5985
          00000003 00000010
          00000004 00061d14
          00000005 003e89d6
          00000006 00000010
          00000007 00000010
           10x 00000000

Adding up the sizes of all sections gives us 0x958d86 or roughly 9.3MB.

The writer

The writer is an uncompressed 320KB ARM binary module. The load address of 0x40000400 and the header size of 1024 = 0x400 imply that the loader starts right after the header. A brief analysis indicates code to access a exFAT, FAT and SDIO. This seems to be the module that does a full copy of the firmware image from an SD card to internal flash, but without actually uncompressing it.

The writer also seems to end before 0x50000 = 0x400 + 0x4fc00, padded with 47KB of zero bytes:

00044270: 04f0 1fe5 280a 0040 0000 0000 0000 0000  ....(..@........
00044280: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00050000: 5a7d 0000 f801 9fe5 0010 90e5 c010 81e3  Z}..............
00050010: 0010 80e5 5004 ec04 1040 0410 100f 11ee  ....P....@......

The code

The above hex-dump also shows that something new begins at 0x50000, matching the offset_code header value. Assuming that it's the code block and that it's ~11MB (code_size = 0xafee12) we can check for its end as well, at 0xb4ee12:

00b4ec10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00b4ee00: 800c 0100 0000 0200 0000 0300 0000 0000  ................
              ╭────────────────────────────────────────────────────
00b4ee10: ed08│0000 0000 0000 0000 0000 0000 0000  ................
──────────────╯
00b4ee20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00b4f000: 5346 5f52 4553 4f55 5243 4500 0000 0000  SF_RESOURCE.....

This is also a match, there is a bunch of zero-padding within the code block, and it ends with 0xed 0x08, followed by some more zero-padding after the code block.

Surprise SF_RESOURCE chunk

The just discovered block at 0xb4f000 looks like some sort of resource section. Again, it's not directly known to binwalk (but binwalk finds a number of known signatures within!). Let's investigate how it continues:

00b4f000: 5346 5f52 4553 4f55 5243 4500 0000 0000  SF_RESOURCE.....
00b4f010: 3031 2e30 a300 0000 0000 0000 0000 0000  01.0............
00b4f020: 4e58 4d49 4e49 2e48 4558 0000 0000 0000  NXMINI.HEX......
00b4f030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f050: 0000 0000 0000 0000 0000 0000 0c92 0000  ................
00b4f060: 6364 2e69 736f 0000 0000 0000 0000 0000  cd.iso..........
00b4f070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f090: 0000 0000 0000 0000 00c0 0000 00f8 0600  ................
00b4f0a0: 4951 5f43 4150 2e42 494e 0000 0000 0000  IQ_CAP.BIN......
00b4f0b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f0c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b4f0d0: 0000 0000 0000 0000 00c0 0700 8063 0000  .............c..
    <snipped a looong list of file headers>
00b518a0: 6c63 645f 6372 6f73 732e 6a70 6700 0000  lcd_cross.jpg...
00b518b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b518c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00b518d0: 0000 0000 0000 0000 00c0 5507 6d3d 0100  ..........U.m=..
    <end of the file headers? the following is not a filename>
00b518e0: 2001 0481 0000 005a 0000 7ff8 0000 014d   ......Z.......M
00b518f0: 0000 015b 0000 015d 0000 0000 0000 0000  ...[...]........

We have an obvious magic string (SF_RESOURCE), followed by a slightly weird version string ("01.0"), an 0xa3 byte and some zeroes to align to the next 32 bytes.

Then comes what looks like a file system with "NXMINI.HEX", "cd.iso", "IQ_CAP.BIN" etc...

Each file seems to have a 64-byte header, starting with the filename and ending with some numbers. The first filename is at 0xb4f020, the first non-filename is at 0xb518e0, giving us (0xb518e0 - 0xb4f020)/64 = 163 = 0xa3 files, and confirming that the header contains the number of files in the resource section. Given that the header numbers are little-endian, the number of files is probably not just one byte, but maybe two or four.

The numbers in each file header seem to be two little-endian integers, with the first one growing linearly (0x0, 0xc000, 0x7c000, ... 0x755c000), and the second one varying (0x920c, 0x6f800, 0x6380, ... 0x13d6d).

From that we can assume that the first number is the offset of the file, relative to the end of the file headers (first one is 0), and the second value is most probably the respective size. We can transfer this knowledge into a tool to print and dump the resouce section, sfresource.py:

Filename | Offset | Size | | Filename | Offset | Size
NXMINI.HEX | 0x00000000 | 37388 | | cd.iso | 0x0000c000 | 456704
IQ_CAP.BIN | 0x0007c000 | 25472 | | IQ_COMM.BIN | 0x00084000 | 60
IQ_M_FHD.BIN | 0x00088000 | 24784 | | IQ_M_HD.BIN | 0x00090000 | 24784
IQ_M_SD.BIN | 0x00098000 | 26096 | | IQ_V_FHD.BIN | 0x000a0000 | 24784
IQ_V_HD.BIN | 0x000a8000 | 24784 | | IQ_V_SD.BIN | 0x000b0000 | 26096
cac_par1.BIN | 0x000b8000 | 4896 | | cac_par2.BIN | 0x000bc000 | 4980
cac_par3.BIN | 0x000c0000 | 4980 | | cac_par4.BIN | 0x000c4000 | 4980
cac_par5.BIN | 0x000c8000 | 4980 | | COMMON.BIN | 0x000cc000 | 58
CAPTURE.BIN | 0x000d0000 | 57004 | | dt_bg.jpg | 0x000e0000 | 117219
file_ng.jpg | 0x00100000 | 16356 | | logo.bin | 0x00104000 | 307200
wifi_bg.yuv | 0x00150000 | 691200 | | mplay_bg.jpg | 0x001fc000 | 177465
PRD_CMD.XML | 0x00228000 | 15528 | | res.dat | 0x0022c000 | 85463552
Hdmi_res.dat | 0x053b0000 | 4492288 | | Hdmi_f_res.dat | 0x057fc000 | 3332608
pa_1.jpg | 0x05b2c000 | 548076 | | pa_1p.jpg | 0x05bb4000 | 113106
pa_2.jpg | 0x05bd0000 | 275490 | | pa_2p.jpg | 0x05c14000 | 51899
pa_3.jpg | 0x05c24000 | 283604 | | pa_3p.jpg | 0x05c6c000 | 73704
pa_4.jpg | 0x05c80000 | 308318 | | pa_4p.jpg | 0x05ccc000 | 85517
pa_5.jpg | 0x05ce4000 | 151367 | | pa_5p.jpg | 0x05d0c000 | 37452
pa_6.jpg | 0x05d18000 | 652185 | | pa_6p.jpg | 0x05db8000 | 101948
pa_7.jpg | 0x05dd4000 | 888479 | | pa_7p.jpg | 0x05eb0000 | 152815
Cross.raw | 0x05ed8000 | 617100 | | Fisheye2.jpg | 0x05f70000 | 114174
Fisheye1.raw | 0x05f8c000 | 460800 | | Fisheye3.bin | 0x06000000 | 1200
HTone3.raw | 0x06004000 | 76800 | | HTone5.raw | 0x06018000 | 76800
HTone10.raw | 0x0602c000 | 76800 | | Min320.raw | 0x06040000 | 19200
Min640.raw | 0x06048000 | 38400 | | Min460.raw | 0x06054000 | 63838
Movie_C1.jpg | 0x06064000 | 401749 | | Movie_C2.jpg | 0x060c8000 | 277949
Movie_C3.jpg | 0x0610c000 | 311879 | | Movie_V1.raw | 0x0615c000 | 307200
Movie_V2.raw | 0x061a8000 | 307200 | | Movie_V3.raw | 0x061f4000 | 307200
Movie_R0.raw | 0x06240000 | 1555200 | | Movie_R1.raw | 0x063bc000 | 388800
Sketch0.raw | 0x0641c000 | 1443840 | | Sketch1.raw | 0x06580000 | 322560
VignetC.jpg | 0x065d0000 | 191440 | | VignetV.raw | 0x06600000 | 460800
VignetV_PC.raw | 0x06674000 | 614400 | | FD_RSC1 | 0x0670c000 | 1781664
BD_RSC1 | 0x068c0000 | 28188 | | ED_RSC1 | 0x068c8000 | 323628
SD_RSC1 | 0x06918000 | 270508 | | OLDFILM1.JPG | 0x0695c000 | 154647
OLDFILM2.JPG | 0x06984000 | 158531 | | OLDFILM3.JPG | 0x069ac000 | 166034
OLDFILM4.JPG | 0x069d8000 | 170281 | | OLDFILM5.JPG | 0x06a04000 | 169271
BS_POW1.wav | 0x06a30000 | 104060 | | BS_POW2.wav | 0x06a4c000 | 109046
BS_POW3.wav | 0x06a68000 | 94412 | | BS_MOVE.wav | 0x06a80000 | 4678
BS_MOVE2.wav | 0x06a84000 | 5032 | | BS_MENU.wav | 0x06a88000 | 25340
BS_SEL.wav | 0x06a90000 | 3964 | | BS_OK.wav | 0x06a94000 | 3484
BS_TOUCH.wav | 0x06a98000 | 5340 | | BS_DEPTH.wav | 0x06a9c000 | 4904
BS_CANCL.wav | 0x06aa0000 | 17708 | | BS_NOBAT.wav | 0x06aa8000 | 194228
BS_NOKEY.wav | 0x06ad8000 | 13308 | | BS_INFO.wav | 0x06adc000 | 12168
BS_WARN.wav | 0x06ae0000 | 20768 | | BS_CONN.wav | 0x06ae8000 | 88888
BS_UNCON.wav | 0x06b00000 | 44328 | | BS_REC1.wav | 0x06b0c000 | 26632
BS_REC2.wav | 0x06b14000 | 47768 | | BS_AF_OK.wav | 0x06b20000 | 10612
BS_SHT_SHORT.wav | 0x06b24000 | 4362 | | BS_SHT_SHORT_5count.wav | 0x06b28000 | 35870
BS_SHT_SHORT_30ms.wav | 0x06b34000 | 1978 | | BS_SHT_Conti_Normal.wav | 0x06b38000 | 44236
BS_SHT_Conti_6fps.wav | 0x06b44000 | 22904 | | BS_SHT1.wav | 0x06b4c000 | 63532
BS_SHT_Burst_10fps.wav | 0x06b5c000 | 552344 | | BS_SHT_Burst_15fps.wav | 0x06be4000 | 375752
BS_SHT_Burst_30fps.wav | 0x06c40000 | 257624 | | BS_COUNT.wav | 0x06c80000 | 2480
BS_2SEC.wav | 0x06c84000 | 87500 | | BS_SHT_LONG_OPEN.wav | 0x06c9c000 | 16000
BS_SHT_LONG_CLOSE.wav | 0x06ca0000 | 15992 | | BS_MC1.wav | 0x06ca4000 | 13944
BS_FACE1.wav | 0x06ca8000 | 36428 | | BS_FACE2.wav | 0x06cb4000 | 36048
BS_FACE3.wav | 0x06cc0000 | 3428 | | BS_JINGLE.wav | 0x06cc4000 | 218468
BS_MEW.wav | 0x06cfc000 | 208920 | | BS_DRIPPING.wav | 0x06d30000 | 102244
BS_TIMER.wav | 0x06d4c000 | 30188 | | BS_TIMER_2SEC.wav | 0x06d54000 | 381484
BS_TIMER_3SEC.wav | 0x06db4000 | 278956 | | BS_ROTATION.wav | 0x06dfc000 | 5316
BS_NFC_START.wav | 0x06e00000 | 124714 | | BS_TEST.wav | 0x06e20000 | 24222
im_10_1m.bin | 0x06e28000 | 123154 | | im_13_3m.bin | 0x06e48000 | 134802
im_16_9m.bin | 0x06e6c000 | 191378 | | im_1_1m.bin | 0x06e9c000 | 10578
im_20m.bin | 0x06ea0000 | 238290 | | im_2m.bin | 0x06edc000 | 27218
im_2_1m.bin | 0x06ee4000 | 23570 | | im_4m.bin | 0x06eec000 | 40338
im_4_9m.bin | 0x06ef8000 | 57618 | | im_5m.bin | 0x06f08000 | 65682
im_5_9m.bin | 0x06f1c000 | 76114 | | im_7m.bin | 0x06f30000 | 69906
im_7_8m.bin | 0x06f44000 | 92178 | | im_vga.bin | 0x06f5c000 | 3474
set_bg.jpg | 0x06f60000 | 13308 | | DV_DSC.jpg | 0x06f64000 | 18605
DV_DSC.png | 0x06f6c000 | 2038 | | DV_DSC_S.jpg | 0x06f70000 | 12952
DV_DSC_S.png | 0x06f74000 | 740 | | DEV_NO.jpg | 0x06f78000 | 29151
wifi_00.bin | 0x06f80000 | 13583 | | wifi_01.bin | 0x06f84000 | 66469
wifi_02.bin | 0x06f98000 | 87936 | | wifi_03.bin | 0x06fb0000 | 63048
wifi_04.bin | 0x06fc0000 | 113645 | | wifi_05.bin | 0x06fdc000 | 172
wifi_06.bin | 0x06fe0000 | 12689 | | wifi_07.bin | 0x06fe4000 | 12750
wifi_08.bin | 0x06fe8000 | 3933 | | cNXMINI.bin | 0x06fec000 | 2048
net_bg0.jpg | 0x06ff0000 | 7408 | | net_bg2.jpg | 0x06ff4000 | 7409
net_bg3.jpg | 0x06ff8000 | 7409 | | qwty_bg.jpg | 0x06ffc000 | 10953
net_bg0.yuv | 0x07000000 | 691200 | | net_bg2.yuv | 0x070ac000 | 691250
net_bg3.yuv | 0x07158000 | 691208 | | qwty_bg.yuv | 0x07204000 | 691200
ChsSysDic.dic | 0x072b0000 | 1478464 | | ChsUserDic.dic | 0x0741c000 | 31744
ChtSysDic.dic | 0x07424000 | 1163484 | | ChtUserDic.dic | 0x07544000 | 31744
lcd_grad_cir.jpg | 0x0754c000 | 26484 | | lcd_grad_hori.jpg | 0x07554000 | 32586
lcd_cross.jpg | 0x0755c000 | 81261

The JPEG files are backgrounds and artistic effects, the WAV files are shutter, timer and power-on/off effects. cd.iso is the i-Launcher install CD that the camera emulates over USB. PRD_CMD.XML is a structured list of "Production Mode System Functions":


<pm_system>

    <key cmd_id="0x1">
        <s1 index_id="0x1">s1</s1>
        <s2 index_id="0x2">s2</s2>
menu
        ...
        <ft_mode index_id="0x11">ft_mode</ft_mode>
        <ok_ng index_id="0x12">ok_ng</ok_ng>
    </key>

    <touch cmd_id="0x2">
        <mask index_id="0x1">mask</mask>
        <unmask index_id="0x2">unmask</unmask>
    </touch>
    ...
</pm_system>

The last file ends at 0xb518e0 + 0x755c000 + 81261 = 0x80c164d - can we find more surprise sections after that?

                                          ╭─────────────────────────
080c1640: 1450 0145 0014 5001 4500 7fff d9│00 0000  .P.E..P.E.......
──────────────────────────────────────────╯
080c1650: 0000 0000 0000 0000 0000 0000  0000 0000  ................
*
080c18e0: c075 12cf e018 0c08 fc00 0000  0000 0000  .u..............
080c18f0: 0000 0000                                 ....
<EOF>

There is some more padding and an unknown 9-byte value. It might be a checksum, verification code or similar. We can probably ignore that for now.

The SF_RESOURCE chunk without this unknown "checksum" is 0x80c164d - 0xb4f000 bytes, or ~117MB.

The code sections

The section_info variable was outlining some sort of partitioning. So far we have found the writer (320KB), the code block (11MB) and the SF_RESOURCE chunk (117MB) in the .bin file. There is no space in the .bin to fit another 9.3MB, unless it is within one of the already-identified parts.

Given that the "code" part is 11MB and the sections are 9.3MB, they might actually fit into the code part. Let's see what is at offset_code + section[1].size = 0x50000 + 0x50e66c = 0x55e66c:

                                       ╭───────────────────────────
0055e660: 58b7 33e1 1f00 8000 0000 0000│0000 0000  X.3.............
───────────────────────────────────────╯
0055e670: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0055e800: 6ab3 0000 70b4 022b 08bf 5200 002a 4ff0  j...p..+..R..*O.

Okay, there is actual data and some zeroes, then 404 zero bytes until some more data comes. Apparently those 404 bytes are padding the first section to some alignment boundary - maybe it's block_size = 0x400 from the header?

At 0x55e800 + section[2].size = 0x704185 there is a similar picture of trailing zeroes within the expected section, followed by zero padding:

                      ╭─────────────────────────────────────────────
00704180: 0100 0000 00│00 0000 0000 0000 0000 0000  ................
──────────────────────╯
00704190: 0000 0000  0000 0000 0000 0000 0000 0000  ................
*
00704200: 800c 7047  0000 0300 0000 0000 0000 0000  ..pG............
────────────────────────────────────────────────────────────────────
00704210: 0000 0000  0000 0000 0000 0000 0000 0000  ................
*
00704400: 4efb 0001  10b5 7648 a1f6 20de 75a0 a1f6  N.....vH.. .u...

Hovever, 0x704200 is not divisible by 0x400, so we need to correct our assumptions on the section alignment. Section #3 at 0x704200 is only 0x10 = 16 bytes, and is followed by the next section at 0x704400, giving us an effective alignment of 0x200 bytes.

In total, we end up with seven sections as follows, and we can extend m7mu.py with the -x argument to extract all partitions (even including the writer and the resources):

Offset | Size | Section
0x050000 | 5301868 | chunk-01.bin
0x55e800 | 1726853 | chunk-02.bin
0x704200 | 16 | chunk-03.bin
0x704400 | 400660 | chunk-04.bin
0x766200 | 4098518 | chunk-05.bin
0xb4ec00 | 16 | chunk-06.bin
0xb4ee00 | 16 | chunk-07.bin

This is continued in part 2: reverse-engineering the LZSS compression, where we find out how the compression of the seven chunks works.

Discuss on Mastodon

Discuss on HN