Samsung WB850F Firmware Reverse-Engineering

Georg Lukas
Fri, 24 May 2024 17:30:20 +0200
hardware, net, reverse-engineering, samsung-nx

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 and ONBL2 are probably the stages 1 and 2 of the bootloader (as confirmed by a string in Main_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 and SKIN 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:

1. Find the right memory address to map Main_Image to

2. Load the symbol names from partialImage.o.map into Ghidra

3. 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()):

1. symbol name

2. (hexadecimal) address

3. "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:

1. on a HTTP 200 OK response, the server must set a cookie on the domain ".yahoo.something" or "kr.yahoo.something"

2. 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.