Firmware Diffing

Introduction: Netgear RAX30

symbols after porting

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 ?