Georg Lukas, 2024-05-24 17:30
三星的WB850F紧凑型相机是首个结合了DRIMeIII SoC和WiFi的型号。与EX2F一起,它具备一个未压缩的固件二进制文件,三星在固件ZIP中贴心地添加了一个partialImage.o.map
文件,其中包含了完整的链接器转储和所有符号名称。我们正在利用这份礼物来逆向工程主SoC固件,以便我们可以让相机通过WiFi热点检测并使用samsung-nx-emailservice。
这是对三星WiFi相机文章的后续,也是三星NX系列的一部分。
-
[WB850F_FW_210086.zip – 外层容器] -
[partialImage.o.map – 链接器转储] -
[WB850-FW-SR-210086.bin – 头文件分析] -
[WB850-FW-SR-210086.bin – 代码和数据分区] -
[在Ghidra中加载代码] -
[加载和映射Main_Image] -
[从partialImage.o.map加载函数名称] -
[逆向工程DevHTTPResponseStart]( -
[解释热点检测] -
[总结:真正的宝藏]
WB850F_FW_210086.zip
– 外层容器
WB850F是三星在停止iLauncher应用程序后仍然发布firmware and support files的少数型号之一。
我们可以从那里获取的WB850F_FW_210086.zip
存档包含了相当多的文件(由file
识别):
GPS_FW/BASEBAND_FW_Flash.mbin: 数据
GPS_FW/BASEBAND_FW_Ram.mbin: 数据
GPS_FW/Config.BIN: 数据
GPS_FW/flashBurner.mbin: 数据
FWUP: ASCII 文本,使用 CRLF 行终止符
partialImage.o.map: ASCII 文本
WB850-FW-SR-210086.bin: 数据
wb850f_adj.txt: ASCII 文本,使用 CRLF 行终止符
FWUP
文件只包含字符串upgrade all
,这是一个固件测试/自动化模块的脚本。wb850f_adj.txt
文件是一个类似但更复杂的脚本,用于升级GPS固件并删除相应的文件。现在让我们暂时跳过与GPS相关的脚本和GPS_FW
文件夹。
partialImage.o.map
– 链接器转储
partialImage.o.map
是一个文本文件,包含超过300k行,包含partialImage.o
的链接器输出,包括链接文件的完整内存映射:
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
这持续不断,真是一张宝藏图!现在我们只需要找到它所属的岛屿。
WB850-FW-SR-210086.bin
– 头文件分析
使用binwalk
查看WB850-FW-SR-210086.bin
会得到一个长长的文件头列表(HTML, PNG, JPEG, …),一个VxWorks头,相当多的Unix路径,但没有看起来像分区或文件系统的任何东西。
让我们改为转储前一千字节:
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
这看起来非常有趣。它以固件版本号开始,210086
,然后是0x00 0x06
,紧接着在偏移量0x008
处是FW_UP/ONBL1.bin
,这非常像一个文件名。下一个文件名FW_UP/ONBL2.bin
出现在0x044
处,所以这可能是一个60字节的“分区”记录:
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 ............
在文件名之后,有相当多的零(构成一个32字节的零填充字符串),接着是两个小端整数0xc4
和0x800
,然后是一个20字节的零填充字符串ONBL1
,这可能是相应的分区名称。之后,相同结构的下一条记录跟随其后。第二条记录中的整数(ONBL2
)是0xb630
和0x8c4
,因此我们可以假设第一个数字是长度,第二个数字是文件中的偏移量(一条记录的偏移量总是前一条记录的偏移量加长度)。
总共有六条记录,所以版本字符串和第一条记录之间的0x00 0x06
可能是固件版本号的终止或填充字节,以及分区数量的一字节数字。
有了这些知识,我们可以如下重建分区表:
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 |
让我们编写一个提取DRIMeIII固件分区的工具,并使用它!
WB850-FW-SR-210086.bin
– 代码和数据分区
该工具根据分区名称提取分区,并分别添加".bin"
后缀。在输出上运行file
并没有太大帮助:
ONBL1.bin: 数据 ONBL2.bin: 数据 Main_Image.bin: OpenPGP 秘密密钥 Resource.bin: MIPSEB-LE MIPS-III ECOFF 可执行文件剥离 – 版本 0.0 OIS.bin: 数据 SKIN.bin: 数据
-
ONBL1
和ONBL2
可能是引导程序的第一阶段和第二阶段(由Main_Image
中的字符串确认:"BootLoader(ONBL1, ONBL2) Update Done"
)。 -
Main_Image
是实际的固件:OpenPGP 秘密密钥是错误的阳性,binwalk -A
报告此文件中相当多的ARM函数序言。 -
Resource
和SKIN
是相当大的容器,可能是由SoC制造商提供,用于“换肤”相机用户界面? -
OIS
并不真的是其文件名所声称的十六进制,但它可能是专用光学图像稳定器的固件。
在所有这些中,Main_Image
是最有趣的一个。
在Ghidra中加载代码
ONBL1
、ONBL2
和Main_Image
三个分区包含实际的ARM代码。典型的ARM固件将在地址0x0000000
(通常是闪存/只读存储器的开头)包含重置向量表,这是一系列跳转指令。然而,所有三个二进制文件在其各自的开头都包含实际的线性代码,所以它们很可能需要重新映射到某个未知的地址。
为了弄清楚相机如何错误地检测热点,我们需要:
-
找到正确的内存地址来映射 Main_Image
-
将 partialImage.o.map
中的符号名称加载到Ghidra中 -
找到并分析错误触发热点登录的函数
加载和映射Main_Image
默认情况下,Ghidra会假设二进制文件加载到地址0x0000000
并尝试这样分析它。要获得正确的内存地址,我们需要找到一个使用绝对地址访问二进制文件中已知值的函数。鉴于有77k个函数,我们可以从接近任务#3的东西开始,在Ghidra的“已定义字符串”标签中搜索"yahoo"
:
太好了!Ghidra识别了一些看起来像一个恼怒的开发者的printf调试的字符串,可能来自一个名为DevHTTPResponseStart()
的函数,它似乎是检查相机是否能够正确访问Yahoo、Google或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!!!! 111nn
0139f638 DevHTTPResponseStart: 301/302/307! cannot find yahoo! safapi_is_browser_framebuffer_on : %d , safapi_is_browser_authed(): %d rn
根据partialImage.o.map
,实际存在一个名为该地址0x321a84
的函数,Ghidra也在0x321a84
找到了一个函数。在映射和二进制文件之间还有一些其他匹配的函数偏移量,所以我们可以假设映射文件中的.text
地址实际上与Main_Image
一一对应!我们找到了我们地图的正确岛屿!
这是该函数的开始:
bool FUN_00321a84(undefined4 param_1,ushort param_2,int param_3,int param_4) {
/* 省略变量声明 */
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);
它以两次调用FUN_0031daec()
开始,参数数量不同 – 这非常像是printf
调试。根据内存映射,它被调用为opd_printf()
!第一个参数是某种上下文/目标,第二个参数必须是对格式字符串的引用。两个DAT_
值被Ghidra检测为32位未定义值:
DAT_00322034:
74 35 3a c1 undefined4 C13A3574h
DAT_00322038:
b8 35 3a c1 undefined4 C13A35B8h
然而,相应的后三位数字与前面遇到的"DevHTTPResponseStart: "
调试字符串相匹配:
-
0xc13a3574 - 0x0139f574 = 0xc0004000
(带有四个参数的第一个格式字符串) -
0xc13a35b8 - 0x0139f5b8 = 0xc0004000
(第二个不带参数的格式字符串)
从这一点我们可以合理地得出结论,Main_Image
需要加载到内存地址0xc0004000
。这在Ghidra中无法事后更改,因此我们需要从项目中移除二进制文件,重新导入它,并相应地设置基地址:
从 partialImage.o.map
加载函数名称
Ghidra 有一个脚本可以批量导入文本表中的数据标签和函数名称,ImportSymbolScript.py。它期望每行包含三个变量,由任意数量的空格分隔(由 Python 的 string.split()
确定):
-
符号名称 -
(十六进制)地址 -
函数为 “f” 或标签为 “l”
我们的符号映射包含多个部分,但我们目前只对 .text
中定义的函数感兴趣,它们与 Main_Image
中的地址一一对应。除了函数名称外,它还包含空行、对象文件偏移量(标签为 .text
)、标签(以 "L$_"
开头)和局部符号(以 "$"
开头)。
我们需要将符号限制在 .text
节(.text
之后和 .debug_frame
之前的所有内容),去掉空行和非函数,然后将每个地址加上 0xc0004000
,以便我们与 Ghidra 中的基地址匹配。我们可以用一个 awk
一行命令非常隐秘地做到这一点:
awk '/^.text /{t=1;next}/^.debug_frame /{t=0} ; !/[$.]/ { if (t && $1) { printf "%s %x fn", $1, (strtonum("0x"$2)+0xc0004000) } }'
或者用一个稍微不那么隐秘但速度慢得多的 shell 循环:
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
这两者都会生成相同的输出,可以通过 Ghidra 的 “Window” / “Script Manager” / “ImportSymbolsScript.py” 导入:
sysInit c0004000 f
archPwrDown c0004094 f
MMU_WriteControlReg c00040a4 f
MMU_WritePageTableBaseReg c00040b8 f
MMU_WriteDomainAccessReg c00040d0 f
...
逆向工程 DevHTTPResponseStart
现在我们已经放置了函数名称,我们需要手动设置许多 DAT_
字段的类型为 “pointer”,根据调试字符串重命名参数,然后我们得到了一个相当可用的反编译输出。
以下是一个为了更好的可读性而编辑的注释版本(内联了字符串引用,重写了一些条件):
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)) {
/* 这不是一个 HTTP 重定向 */
if (status == 200) {
/* HTTP 200 表示 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)) {
/* 没有响应头或没有 yahoo cookie --> 检查失败! */
result = true;
} else {
/* 我们在 headers 中找到了 yahoo cookie 位 */
opd_printf(ctx, "DevHTTPResponseData: THIS IS GOOGLE/YAHOO PAGE!!!! 3333rnrnrn");
*p_request_ongoing = 0;
if (!safapi_is_browser_authed())
safnotify_auth_ap(0);
result = false;
}
} else if (status < 0) {
/* 负数状态 = 中止? */
result = false;
} else {
/* 正数状态,不是重定向,不是 "OK" */
result = !safapi_is_browser_framebuffer_on();
}
} else {
/* 这是 HTTP 重定向 */
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 {
/* 在 url 中找到了 "yahoo." */
opd_printf(ctx, "DevHTTPResponseStart: THIS IS GOOGLE/YAHOO/SAMSUNG PAGE!!!! 111rnrnrn");
*p_request_ongoing = 0;
if (!safapi_is_browser_authed())
safnotify_auth_ap(0);
result = false;
}
}
return result;
}
解释热点检测
总之,DevHTTPResponseStart
中的代码将检查两个条件之一,并调用 safnotify_auth_ap(0)
将 WiFi 接入点标记为已认证:
-
在 HTTP 200 OK 响应中,服务器必须在域
".yahoo*.something*"
或"kr.yahoo*.something*"
上设置 cookie。 -
在 HTTP 301/302/307 重定向中,URL(可能是重定向位置?)必须在其开头附近包含
"yahoo."
。
如果我们手动联系查询的 URL http://www.yahoo.co.kr/
,它将重定向我们到 https://www.yahoo.com/
,所以一切都好吗?
GET / HTTP/1.1
Host: www.yahoo.co.kr
HTTP/1.1 301 Moved Permanently
Location: https://www.yahoo.com/
嗯,子字符串 "yahoo."
在 URL "https://www.yahoo.com/"
的位置 12,但代码要求它在前 11 个位置之一。这个检查已经被 TLS 杀死了!
要通过热点检查,我们必须倒退十年的 HTTPS-everywhere,或者将 DNS 记录指向一个不同的服务器,该服务器要么 HTTP-redirect 到一个不同的、更像 yahoo 的名称,要么在 yahoo 域上设置 cookie。
在相应地修补 samsung-nx-emailservice之后,相机实际上会连接并上传照片:
总结:真正的宝藏
这次深入研究使我们能够理解并绕过基于逆向工程函数的三星 WB850F WiFi 相机的热点检测。结果的补丁很小,但由于三星工程师实施的“检测方法”,仅凭数据包跟踪猜测解决方法是不可能的。一旦知道要寻找什么,同样的解决方法也应用于请求 MSN.com 的相机,因此在支持相机列表中也增加了 EX2F, ST200F, WB3xF 和 WB1100F。
然而,真正的宝藏仍在等待!Main_Image
包含超过 77k 个函数,所以对于一个好奇的寻宝者来说,有足够的东西可以探索,以便更好地了解数码相机的工作原理。
– END –
原文始发于微信公众号(3072):三星WB850F 相机固件逆向(译)