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

Screenshot of Ghidra with some Yahoo!  strings

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=%dn, headers=%srn
0139f5b8    DevHTTPResponseStart: This is YAHOO check !!!rn
0139f5f4    DevHTTPResponseStart: THIS IS GOOGLE/YAHOO/SAMSUNG PAGE!!!! 111nnn
0139f638    DevHTTPResponseStart: 301/302/307! cannot find yahoo!  safapi_is_browser_framebuffer_on : %d , safapi_is_browser_authed(): %d  rn

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:

Screenshot of Ghidra import options dialog

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 fn", $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 fn"  $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=%dn, headers=%srn",
      url,handle,status,headers);
  opd_printf(ctx,"DevHTTPResponseStart: This is YAHOO check !!!rn");
  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!!!! 3333nnn");
        *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  rn",
          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!! rn");
        safnotify_auth_ap(1);
      }
      result = false;
    } else {
      /* found "yahoo." in url */
      opd_printf(ctx, "DevHTTPResponseStart: THIS IS GOOGLE/YAHOO/SAMSUNG PAGE!!!! 111nnn");
      *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:

WB850F sending a photo

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.


Discuss on Mastodon