Georg Lukas, 2024-05-24 17:30
Samsung's WB850F compact camera
was the first model to combine the DRIMeIII SoC with WiFi. Together with the
EX2F it features an uncompressed firmware binary where Samsung helpfully added
a partialImage.o.map
file with a full linker dump and all symbol names into
the firmware ZIP. We are using this gift to reverse-engineer the main SoC
firmware, so that we can make it pass the WiFi hotspot detection and use
samsung-nx-emailservice.
This is a follow-up to the Samsung WiFi cameras article and part of the Samsung NX series.
WB850F_FW_210086.zip
- the outer container
The WB850F is one of the few models where Samsung still publishes firmware and support files after discontinuing the iLauncher application.
The WB850F_FW_210086.zip
archive we can get there contains quite a few files
(as identified by file
):
GPS_FW/BASEBAND_FW_Flash.mbin: data
GPS_FW/BASEBAND_FW_Ram.mbin: data
GPS_FW/Config.BIN: data
GPS_FW/flashBurner.mbin: data
FWUP: ASCII text, with CRLF line terminators
partialImage.o.map: ASCII text
WB850-FW-SR-210086.bin: data
wb850f_adj.txt: ASCII text, with CRLF line terminators
The FWUP
file just contains the string upgrade all
which is a script for
the firmware testing/automation module. The wb850f_adj.txt
file is a similar
but more complex script to upgrade the GPS firmware and delete the respective
files. Let's skip the GPS-related script and GPS_FW
folder for now.
partialImage.o.map
- the linker dump
The partialImage.o.map
is a text file with >300k lines, containing the
linker output for partialImage.o
, including a full memory map of the linked
file:
output input virtual
section section address size file
.text 00000000 01301444
.text 00000000 000001a4 sysALib.o
$a 00000000 00000000
sysInit 00000000 00000000
L$_Good_Boot 00000090 00000000
archPwrDown 00000094 00000000
...
DevHTTPResponseStart 00321a84 000002a4
DevHTTPResponseData 00321d28 00000100
DevHTTPResponseEnd 00321e28 00000170
...
.data 00000000 004ed40c
.data 00000000 00000874 sysLib.o
sysBus 00000000 00000004
sysCpu 00000004 00000004
sysBootLine 00000008 00000004
This goes on and on and on, and it's a real treasure map! Now we just need to find the island that it belongs to.
WB850-FW-SR-210086.bin
- header analysis
Looking into WB850-FW-SR-210086.bin
with binwalk
yields a long list of
file headers (HTML, PNG, JPEG, ...), a VxWorks header, quite a number of Unix
paths, but nothing that looks like partitions or filesystems.
Let's hex-dump the first kilobyte instead:
00000000: 3231 3030 3836 0006 4657 5f55 502f 4f4e 210086..FW_UP/ON
00000010: 424c 312e 6269 6e00 0000 0000 0000 0000 BL1.bin.........
00000020: 0000 0000 0000 0000 c400 0000 0008 0000 ................
00000030: 4f4e 424c 3100 0000 0000 0000 0000 0000 ONBL1...........
00000040: 0000 0000 4657 5f55 502f 4f4e 424c 322e ....FW_UP/ONBL2.
00000050: 6269 6e00 0000 0000 0000 0000 0000 0000 bin.............
00000060: 0000 0000 30b6 0000 c408 0000 4f4e 424c ....0.......ONBL
00000070: 3200 0000 0000 0000 0000 0000 0000 0000 2...............
00000080: 5b57 4238 3530 5d44 5343 5f35 4b45 595f [WB850]DSC_5KEY_
00000090: 5742 3835 3000 0000 0000 0000 0000 0000 WB850...........
000000a0: 38f4 d101 f4be 0000 4d61 696e 5f49 6d61 8.......Main_Ima
000000b0: 6765 0000 0000 0000 0000 0000 526f 6d46 ge..........RomF
000000c0: 532f 5350 4944 2e52 6f6d 0000 0000 0000 S/SPID.Rom......
000000d0: 0000 0000 0000 0000 0000 0000 00ac f402 ................
000000e0: 2cb3 d201 5265 736f 7572 6365 0000 0000 ,...Resource....
000000f0: 0000 0000 0000 0000 4657 5f55 502f 5742 ........FW_UP/WB
00000100: 3835 302e 4845 5800 0000 0000 0000 0000 850.HEX.........
00000110: 0000 0000 0000 0000 864d 0000 2c5f c704 .........M..,_..
00000120: 4f49 5300 0000 0000 0000 0000 0000 0000 OIS.............
00000130: 0000 0000 4657 5f55 502f 736b 696e 2e62 ....FW_UP/skin.b
00000140: 696e 0000 0000 0000 0000 0000 0000 0000 in..............
00000150: 0000 0000 48d0 2f02 b2ac c704 534b 494e ....H./.....SKIN
00000160: 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
000003f0: 0000 0000 0000 0000 0000 0000 5041 5254 ............PART
This looks very interesting. It starts with the firmware version, 210086
,
then 0x00 0x06
, directly followed by FW_UP/ONBL1.bin
at the offset
0x008
, which very much looks like a file name. The next file name,
FW_UP/ONBL2.bin
comes at 0x044
, so this is probably a 60-byte "partition"
record:
00000008: 4657 5f55 502f 4f4e 424c 312e 6269 6e00 FW_UP/ONBL1.bin.
00000018: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000028: c400 0000 0008 0000 4f4e 424c 3100 0000 ........ONBL1...
00000038: 0000 0000 0000 0000 0000 0000 ............
After the file name, there is quite a bunch of zeroes (making up a 32-byte
zero-padded string), followed by two little-endian integers 0xc4
and
0x800
, followed by a 20-byte zero-padded string ONBL1
, which is
probably the respective partition name. After that, the next records of the
same structure follow. The integers in the second record (ONBL2
) are
0xb630
and 0x8c4
, so we can assume the first number is the length, and the
second one is the offset in the file (the offset of one record is always
offset+length of the previous one).
In total, there are six records, so the 0x00 0x06
between the version string
and the first record is probably a termination or pading byte for the firmware
version and a one-byte number of partitions.
With this knowledge, we can reconstruct the partition table as follows:
File name | size | offset | partition name |
---|---|---|---|
FW_UP/ONBL1.bin | 196 (0xc4) | 0x0000800 |
ONBL1 |
FW_UP/ONBL2.bin | 46 KB (0xb630) | 0x00008c4 |
ONBL2 |
[WB850]DSC_5KEY_WB850 | 30 MB (0x1d1f438) | 0x000bef4 |
Main_Image |
RomFS/SPID.Rom | 48 MB (0x2f4ac00) | 0x1d2b32c |
Resource |
FW_UP/WB850.HEX | 19 KB (0x4d86) | 0x4c75f2c |
OIS |
FW_UP/skin.bin | 36 MB (0x22fd048) | 0x4c7acb2 |
SKIN |
Let's write a tool to extract DRIMeIII firmware partitions, and use it!
WB850-FW-SR-210086.bin
- code and data partitions
The tool is extracting partitions based on their partition names, appending
".bin"
respectively. Running file
on the output is not very helpful:
ONBL1.bin: data
ONBL2.bin: data
Main_Image.bin: OpenPGP Secret Key
Resource.bin: MIPSEB-LE MIPS-III ECOFF executable stripped - version 0.0
OIS.bin: data
SKIN.bin: data
ONBL1
andONBL2
are probably the stages 1 and 2 of the bootloader (as confirmed by a string inMain_Image
:"BootLoader(ONBL1, ONBL2) Update Done"
).Main_Image
is the actual firmware: the OpenPGP Secret Key is a false positive,binwalk -A
reports quite a number of ARM function prologues in this file.Resource
andSKIN
are pretty large containers, maybe provided by the SoC manufacturer to "skin" the camera UI?OIS
is not really hex as claimed by its file name, but it might be the firmware for a dedicated optical image stabilizer.
Of all these, Main_Image
is the most interesting one.
Loading the code in Ghidra
The three partitions ONBL1
, ONBL2
and Main_Image
contain actual ARM code.
A typical ARM firmware will contain the
reset vector table
at address 0x0000000
(usually the beginning of flash / ROM), which is a
series of jump instructions. All three binaries however contain actual linear
code at their respective beginning, so most probably they need to be
re-mapped to some yet unknown address.
To find out how and why the camera is mis-detecting a hotspot, we need to:
- Find the right memory address to map
Main_Image
to - Load the symbol names from
partialImage.o.map
into Ghidra - Find and analyze the function that is mis-firing the hotspot login
Loading and mapping Main_Image
By default, Ghidra will assume that the binary loads to address 0x0000000
and try to analyze it this way. To get the correct memory address, we need to
find a function that accesses some known value from the binary using an
absolute address. Given that there are 77k functions, we can start with
something that's close to task #3, and search in the "Defined Strings" tab of
Ghidra for "yahoo"
:
Excellent! Ghidra identified a few strings that look like an annoyed
developer's printf debugging, probably from a function called
DevHTTPResponseStart()
, and it seems to be the function that checks whether
the camera can properly access Yahoo, Google or Samsung:
0139f574 DevHTTPResponseStart: url=%s, handle=%x, status=%d\n, headers=%s\r\n
0139f5b8 DevHTTPResponseStart: This is YAHOO check !!!\r\n
0139f5f4 DevHTTPResponseStart: THIS IS GOOGLE/YAHOO/SAMSUNG PAGE!!!! 111\n\n\n
0139f638 DevHTTPResponseStart: 301/302/307! cannot find yahoo! safapi_is_browser_framebuffer_on : %d , safapi_is_browser_authed(): %d \r\n
According to partialImage.o.map
, a function with that name actually exists
at address 0x321a84
, and Ghidra also found a function at 0x321a84
. There
are some more matching function offsets between the map and the binary, so we
can assume that the .text
addresses from the map file actually correspond
1:1 to Main_Image
! We found the right island for our map!
Here's the beginning of that function:
bool FUN_00321a84(undefined4 param_1,ushort param_2,int param_3,int param_4) {
/* snip variable declarations */
FUN_0031daec(*(DAT_00321fd4 + 0x2c),DAT_00322034,param_3,param_1,param_2,param_4);
FUN_0031daec(*(DAT_00321fd4 + 0x2c),DAT_00322038);
FUN_00326f84(0x68);
It starts with two calls to FUN_0031daec()
with different
numbers of parameters - this smells very much of printf
debugging again.
According to the memory map, it's called opd_printf()
! The first parameter
is some sort of context / destination, and the second one must be a reference
to the format string. The two DAT_
values are detected by Ghidra as 32-bit
undefined values:
DAT_00322034:
74 35 3a c1 undefined4 C13A3574h
DAT_00322038:
b8 35 3a c1 undefined4 C13A35B8h
However, the respective last three digits match the "DevHTTPResponseStart: "
debug strings encountered earlier:
0xc13a3574 - 0x0139f574 = 0xc0004000
(first format string with four parameters)0xc13a35b8 - 0x0139f5b8 = 0xc0004000
(second format strings without parameters)
From that we can reasonably conclude that Main_Image
needs to be loaded to
the memory address 0xc0004000
. This cannot be changed after the fact in
Ghidra, so we need to remove the binary from the project, re-import it, and
set the base address accordingly:
Loading function names from partialImage.o.map
Ghidra has a script to bulk-import data labels and function names from a text
table,
ImportSymbolScript.py.
It expects each line to contain three variables, separated by arbitrary
amounts of whitespace (as determined by python's string.split()
):
- symbol name
- (hexadecimal) address
- "f" for "function" or "l" for "label"
Our symbol map contains multiple sections, but we are only interested in the
functions defined in .text
(for now), which are mapped 1:1 to addresses in
Main_Image
. Besides of function names, it also contains empty lines, object
file offsets (with .text
as the label), labels (prefixed with "L$_"
) and
local symbols (prefixed with "$"
).
We need to limit our symbols to the .text
section (everything after .text
and before .debug_frame
), get rid of the empty lines and non-functions, then
add 0xc0004000
to each address so that we match up with the base address in
Ghidra. We can do this very obscurely with an awk one-liner:
awk '/^\.text /{t=1;next}/^\.debug_frame /{t=0} ; !/[$.]/ { if (t && $1) { printf "%s %x f\n", $1, (strtonum("0x"$2)+0xc0004000) } }'
Or slightly less obscurely with a much slower shell loop:
sed '1,/^\.text /d;/^\.debug_frame /,$d' | grep -v '^$' | grep -v '[.$]' | \
while read sym addr f ; do
printf "%s %x f\n" $sym $((0xc0004000 + 0x$addr))
done
Both will generate the same output that can be loaded into Ghidra via "Window" / "Script Manager" / "ImportSymbolsScript.py":
sysInit c0004000 f
archPwrDown c0004094 f
MMU_WriteControlReg c00040a4 f
MMU_WritePageTableBaseReg c00040b8 f
MMU_WriteDomainAccessReg c00040d0 f
...
Reverse engineering DevHTTPResponseStart
Now that we have the function names in place, we need to manually set the type
of quite a few DAT_
fields to "pointer", rename the parameters according to
the debug string, and we get a reasonably usable decompiler output.
The following is a commented version, edited for better readability (inlined the string references, rewrote some conditionals):
bool DevHTTPResponseStart(undefined4 handle,ushort status,char *url,char *headers) {
bool result;
opd_printf(ctx,"DevHTTPResponseStart: url=%s, handle=%x, status=%d\n, headers=%s\r\n",
url,handle,status,headers);
opd_printf(ctx,"DevHTTPResponseStart: This is YAHOO check !!!\r\n");
safnotify_page_load_status(0x68);
if ((url == NULL) || (status != 301 && status != 302 && status != 307)) {
/* this is not a HTTP redirect */
if (status == 200) {
/* HTTP 200 means OK */
if (headers == NULL ||
(strstr(headers,"domain=.yahoo") == NULL &&
strstr(headers,"Domain=.yahoo") == NULL &&
strstr(headers,"domain=kr.yahoo") == NULL &&
strstr(headers,"Domain=kr.yahoo") == NULL)) {
/* no response headers or no yahoo cookie --> check fails! */
result = true;
} else {
/* we found a yahoo cookie bit in the headers */
opd_printf(ctx,"DevHTTPResponseData: THIS IS GOOGLE/YAHOO PAGE!!!! 3333\n\n\n");
*p_request_ongoing = 0;
if (!safapi_is_browser_authed())
safnotify_auth_ap(0);
result = false;
}
} else if (status < 0) {
/* negative status = aborted? */
result = false;
} else {
/* positive status, not a redirect, not "OK" */
result = !safapi_is_browser_framebuffer_on();
}
} else {
/* this is a HTTP redirect */
char *match = strstr(url,"yahoo.");
if (match == NULL || match > (url+11)) {
opd_printf(ctx, "DevHTTPResponseStart: 301/302/307! cannot find yahoo! safapi_is_browser_framebuffer_on : %d , safapi_is_browser_authed(): %d \r\n",
safapi_is_browser_framebuffer_on(), safapi_is_browser_authed());
if (!safapi_is_browser_framebuffer_on() && !safapi_is_browser_authed()) {
opd_printf(ctx,"DevHTTPResponseStart: 302 auth failed!!! kSAFAPIAuthErrNotAuth!! \r\n");
safnotify_auth_ap(1);
}
result = false;
} else {
/* found "yahoo." in url */
opd_printf(ctx, "DevHTTPResponseStart: THIS IS GOOGLE/YAHOO/SAMSUNG PAGE!!!! 111\n\n\n");
*p_request_ongoing = 0;
if (!safapi_is_browser_authed())
safnotify_auth_ap(0);
result = false;
}
}
return result;
}
Interpreting the hotspot detection
So to summarize, the code in DevHTTPResponseStart
will check for one of two
conditions and call safnotify_auth_ap(0)
to mark the WiFi access point as
authenticated:
on a HTTP 200 OK response, the server must set a cookie on the domain
".yahoo.something"
or"kr.yahoo.something"
on a HTTP 301/302/307 redirect, the URL (presumably the redirect location?) must contain
"yahoo."
close to its beginning.
If we manually contact the queried URL, http://www.yahoo.co.kr/
, it will
redirect us to https://www.yahoo.com/
, so everything is fine?
GET / HTTP/1.1
Host: www.yahoo.co.kr
HTTP/1.1 301 Moved Permanently
Location: https://www.yahoo.com/
Well, the substring "yahoo."
is on position 12 in the url
"https://www.yahoo.com/"
, but the code is requiring it to be in one of the
first 11 positions. This check has been killed by TLS!
To pass the hotspot check, we must unwind ten years of HTTPS-everywhere, or point the DNS record to a different server that will either HTTP-redirect to a different, more yahooey name, or set a cookie on the yahoo domain.
After patching samsung-nx-emailservice accordingly, the camera will actually connect and upload photos:
Summary: the real treasure
This deep-dive allowed to understand and circumvent the hotspot detection in Samsung's WB850F WiFi camera based on one reverse-engineered function. The resulting patch was tiny, but guessing the workaround just from the packet traces was impossible due to the "detection method" implemented by Samsung's engineers. Once knowing what to look for, the same workaround was applied to cameras asking for MSN.com, thus also adding EX2F, ST200F, WB3xF and WB1100F to the supported cameras list.
However, the real treasure is still waiting! Main_Image
contains over 77k
functions, so there is more than enough for a curious treasure hunter to
explore in order to better understand how digital cameras work.