Georg Lukas, 2025-04-30 17:51
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 will be followed by a similar post about the LZSS-based compression mechanism, 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 specimen 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 specimen.
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
andwrite_code_entry 0x40000400
) - the "code" (
code_size = 0xafee12
andoffset_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":
<!--Production Mode System Functions-->
<pm_system>
<!------------Key Command-------------->
<key cmd_id="0x1">
<s1 index_id="0x1">s1</s1>
<s2 index_id="0x2">s2</s2>
<menu index_id="0x3">menu</menu>
...
<ft_mode index_id="0x11">ft_mode</ft_mode>
<ok_ng index_id="0x12">ok_ng</ok_ng>
</key>
<!------------Touch Command-------------->
<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 section 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 |
Stay tuned for the next part of the series, where we will find out how the compression of the seven chunks works.