Firmware Diffing¶
Introduction: Netgear RAX30¶
For this tutorial we are going to work on the Netgear RAX30 Wireless router, that was part of the targets for Pwn2Own Toronto 2022. A day before the submission deadline, Netgear released a firmware update that was also patching some vulnerabilities (which led multiple Pwn2Own competitors to withdraw theirs submission). The two versions are:
The goal of this tutorial is to show how performing a full-filesystem diff and to navigate the results programatically.
I. Unpacking firmwares¶
To extract the firmwares one can use Unblob which is now undeniably the best firmware extraction tool to date. We can extract the firmware with:
docker run \
--rm \
--pull always \
-v $PWD/extract/RAX30-V1.0.7.78_1.img_extract:/data/output \
-v $PWD:/data/input \
ghcr.io/onekey-sec/unblob:latest /data/input/RAX30-V1.0.7.78_1.img
(Considering the firmware is in your current directory)
Exercise: Unpack both firmwares
II. Exporting files¶
Before diffing executables between the two firmwares, we have to call BinExport on all of them. For that we can use the program binexporter
that can iterate a whole tree of files and to export all files encountered.
binexporter -t 10 extract/RAX30-V1.0.7.78_1.img_extract
The option -t
launch the export with 10 processes. As exporting implies disassembling all binaries it can take a while to perform (~1h for this firmware).
To continue the tutorial here are the binexports files for both firmwares.
III. Performing the diff¶
Now that all executable files are exported in a single directory for each firmwares we can perfom the diff of each BinExport files based on theirs name. (For simplicity we assume there are now duplicates. Also symlinks also led to duplicating binexport files).
Exercise: Write a script to diff all binexports files.
[ ]:
from pathlib import Path
from bindiff import BinDiff, BindiffFile
binexports1 = Path("binexports/RAX30-V1.0.7.78_1.img_extract")
binexports2 = Path("binexports/RAX30-V1.0.9.90_3.img_extract")
diff_dir = Path("diffs") # create output dir
diff_dir.mkdir()
for file1 in binexports1.iterdir():
file2 = binexports2 / file1.name
if file2.exists():
diff_file = diff_dir / f"{file1.with_suffix('').name}_vs_{file2.with_suffix('').name}.BinDiff"
if not diff_file.exists():
print(f"diff {file1} | {file2}: ", end="")
print(BinDiff.raw_diffing(file1, file2, diff_file)) # Perform the diff
IV. Analyzing diffs¶
Now that all diff files are created we can open them to analyze changes.
Exercise: Write a script that take the 15 most different binaries (similarity the lowest), and which shows the 10 most different functions (dissimilar ones).
[8]:
from ipywidgets import IntProgress
from IPython.display import display
from collections import Counter
from bindiff import BinDiff, BindiffFile
diff_dir = Path("diffs/")
progress = IntProgress(min=0, max=len(list(diff_dir.iterdir())), description="Load diffs")
display(progress)
diffs = Counter()
for diff_file in diff_dir.iterdir():
progress.value += 1
diff = BindiffFile(diff_file)
diffs[diff_file] = diff.similarity
del diff # useful to free the memory
# Reopen the most dissimilar binaries
for diff_file, similarity in diffs.most_common()[::-1][:15]:
diff = BindiffFile(diff_file)
binary_file = str(diff_file.name).split("_vs_")[0]
print(f"{binary_file}: {similarity:.0%} [matched:{len(diff.function_matches)}][unmatched:{diff.unmatched_primary_count}-{diff.unmatched_secondary_count}]:")
fun_sims = Counter({x.name1: x.similarity for x in diff.function_matches})
for fun, sim in fun_sims.most_common()[::-1][:10]:
print(f" - {fun} ({sim:.0%})")
xCloud_Debug_log.zip: 49% [matched:140][unmatched:28-28]:
- sub_000115D8 (10%)
- sub_00013DBC (59%)
- _ITM_registerTMCloneTable (100%)
- __imp___gmon_start__ (100%)
- _ITM_deregisterTMCloneTable (100%)
- __imp_fwrite (100%)
- __imp_json_object_object_add (100%)
- __imp_strstr (100%)
- __imp_snprintf (100%)
- __imp_cmsLog_cleanup (100%)
debug.cgi: 49% [matched:140][unmatched:28-28]:
- sub_000115D8 (10%)
- sub_00013DBC (59%)
- _ITM_registerTMCloneTable (100%)
- __imp___gmon_start__ (100%)
- _ITM_deregisterTMCloneTable (100%)
- __imp_fwrite (100%)
- __imp_json_object_object_add (100%)
- __imp_strstr (100%)
- __imp_snprintf (100%)
- __imp_cmsLog_cleanup (100%)
libmdm_db.so: 51% [matched:258][unmatched:418-418]:
- oalMdm_isParam64 (63%)
- strstr (66%)
- mdm_deleteObjectInstance (92%)
- j_mdm_getObject (99%)
- mdm_getNextObjPathDesc (99%)
- sub_00005F68 (99%)
- sub_0000B864 (99%)
- mdm_moveInstanceUsingNewOrderValue (99%)
- j_mdm_getOrderValue (99%)
- mdm_setOrderValue (99%)
wbd_master: 54% [matched:582][unmatched:13-13]:
- sub_00024890 (1%)
- sub_0002E244 (1%)
- sub_0001F640 (1%)
- sub_0002FA78 (2%)
- sub_0002F974 (3%)
- sub_0002F188 (3%)
- sub_0002ABA4 (3%)
- sub_000206D4 (5%)
- sub_00029A30 (6%)
- sub_0002A318 (6%)
wbd_slave: 55% [matched:867][unmatched:12-12]:
- sub_00040044 (0%)
- sub_00040520 (1%)
- sub_00040F88 (1%)
- sub_00041DC8 (1%)
- sub_00031930 (1%)
- sub_000277E4 (1%)
- sub_0001EEF4 (1%)
- sub_0001B518 (1%)
- sub_000277A0 (1%)
- sub_00040934 (1%)
xt_comment.ko: 63% [matched:3][unmatched:0-0]:
- comment_mt_exit (100%)
- init_module (100%)
- comment_mt (100%)
ssd: 63% [matched:88][unmatched:1-1]:
- sub_000117C4 (54%)
- sub_00010B94 (61%)
- sprintf (66%)
- nvram_set (66%)
- strcmp (66%)
- setsockopt (66%)
- strcspn (66%)
- strspn (66%)
- nvifname_to_osifname (66%)
- malloc (66%)
xt_SKIPLOG.ko: 64% [matched:4][unmatched:0-0]:
- blog_skip (100%)
- skiplog_tg_exit (100%)
- skiplog_tg_init (100%)
- skiplog_tg (100%)
wb_cli: 73% [matched:70][unmatched:0-0]:
- sub_00010AB8 (74%)
- sub_00011354 (77%)
- _ITM_registerTMCloneTable (100%)
- _ITM_deregisterTMCloneTable (100%)
- __imp___gmon_start__ (100%)
- __imp_wbd_get_cli_command_id (100%)
- __imp_printf (100%)
- __imp_fputs (100%)
- __imp_wbd_json_create_cli_cmd (100%)
- __imp_stub_send_hld_hlpr (100%)
tm_block.cgi: 75% [matched:170][unmatched:1-1]:
- sub_00012E78 (63%)
- sub_000126E4 (98%)
- sub_00012DB8 (99%)
- _ITM_registerTMCloneTable (100%)
- __imp___gmon_start__ (100%)
- _ITM_deregisterTMCloneTable (100%)
- __imp_sprintf (100%)
- __imp_cmsMsg_send (100%)
- __imp_strcpy (100%)
- __imp_cmsMem_free (100%)
libcurl.so.4: 75% [matched:1644][unmatched:29-29]:
- Curl_select (0%)
- strerror (1%)
- sub_0002952C (1%)
- sub_00041510 (1%)
- Curl_sendf (2%)
- sub_000415A4 (2%)
- j_Curl_md5it (3%)
- sub_00029238 (4%)
- sub_0001C1C0 (5%)
- sub_00041C10 (5%)
libcurl.so: 75% [matched:1644][unmatched:29-29]:
- Curl_select (0%)
- strerror (1%)
- sub_0002952C (1%)
- sub_00041510 (1%)
- Curl_sendf (2%)
- sub_000415A4 (2%)
- j_Curl_md5it (3%)
- sub_00029238 (4%)
- sub_0001C1C0 (5%)
- sub_00041C10 (5%)
wps_pbcd: 77% [matched:236][unmatched:3-3]:
- sub_00017AB0 (7%)
- sub_00014FD8 (10%)
- sub_00014500 (42%)
- sub_00015D00 (52%)
- sub_00014078 (79%)
- sub_000143BC (79%)
- sub_00013604 (85%)
- sub_00011B88 (91%)
- sub_00015864 (92%)
- sub_00014EA0 (94%)
reset_pwd.cgi: 77% [matched:213][unmatched:1-1]:
- sub_00013308 (63%)
- sub_00012B74 (96%)
- sub_000130C0 (98%)
- sub_000151FC (98%)
- sub_00013248 (99%)
- _ITM_registerTMCloneTable (100%)
- __imp___gmon_start__ (100%)
- _ITM_deregisterTMCloneTable (100%)
- __imp_sprintf (100%)
- __imp_cmsMsg_send (100%)
libwbdshared.so: 78% [matched:789][unmatched:0-0]:
- wbd_tlv_decode_weak_client_response (15%)
- wbd_parse_cli_args (20%)
- wbd_tlv_encode_fbt_config_response (22%)
- wbd_ds_is_interface_dedicated_backhaul (23%)
- wbd_get_command_id (24%)
- j_wbd_get_command_id (30%)
- wbd_tlv_encode_weak_client_response (34%)
- wbd_ds_get_i5_device (34%)
- wbd_ds_interface_init (35%)
- wbd_tlv_encode_fbt_config_request (35%)
Open Question
Try to identify interesting components security-wise. Then try to identify ones that have changed and into them what functions have changed.
Bonus¶
The latest version released is 1.0.11.96 which apparently provide a “hot fix”. Can you identify what has been patched and how ?