Hubert Hackin''
  • All posts
  • About
  • Our CTF

NSEC25 Conference Badge - Wed, May 21, 2025 - Jean Privat

Fun is better that flags | Badge | Nsec25

Context: This part was done during the slow hours of the NorthSec conference while seated on a comfy blue sandbag, drinking community room coffee.

oveRview

Léodagan : Ils sont fiers de vous vos hommes vous croyez ?

Arthur : Faudra leur demander. J’crois ouais.

Léodagan : Les miens… chais pas. Pour la plupart ils ont les jetons ça c’est sûr mais alors de là à dire qu’ils sont fiers …

The badge of the year features:

  • An IR reader and emitter that is used for social interaction.
  • A small screen
  • Some buttons and electronics
  • A physical PC permanent denial of service tool (for PCs with a PCI port).
  • A ESP32-C3 chip

The big new feature is the usage of a real CPU with a real ISA. According to Wikipedia, ESP32-C3 is designed by Espressif Systems with

  • Single-core 32-bit RISC-V CPU, up to 160 MHz
  • 400 KiB SRAM, 384 KiB ROM, and 8 KiB RTC SRAM
  • Wi-Fi 2.4 GHz (IEEE 802.11b/g/n)
  • Bluetooth 5 (LE)
  • 2 × 12-bit SAR ADC — I have no idea what that is
  • Some GPIOs

RISC-V is a new-ish royalty-free ISA mainly used by the best universities for their assembly, microarchitecture, and compilation courses. Seeing it in the wild is fun.

The Espressif company offers ESP-IDF, a complete Espressif IoT Development Framework that is open-source, command-line oriented, and documented. Especially, esptool reads and writes firmware (and other stuff). It’s even packaged in Debian.

So, as a decent full computer with a decent architecture, decent documentation, and decent tools, and for the first year ever, I might be able to play willingly with the NorthSec badge (thank you NorthSec!).

Note: The chip of the badge also features Wi-Fi and Bluetooth. The badge track will be insane this year…

start hackIng

Arthur : Vous arrêtez pas de regarder de tous les côtés ! Vous êtes paumé !

Léodagan : J’suis paumé, j’suis paumé ? J’suis momentanément égaré !

Arthur : Ben vous qui vouliez du prestige, avouez qu’on commence fort !

dump the firmware

  1. Get a USB-C cable.
    Note: Very important. Write your name on the cable, or something, if you value your cable.
  2. Plug the badge into your computer with the USB-C cable.
    Note: for better performance, I wanted to plug it directly into the PCI port, but my laptop is missing one — am I that old? First they removed the ISA port, then the DE9 com port, now it’s PCI?
  3. Install Debian on your computer.
    Note: testing is freezing, therefore, it is the best time to install a rock-hard Linux distribution
  4. Apt-Install picocom and esptool.
    Note: beware of counterfeits. picom is a X11 compositor and epstool deals with Encapsulated PostScript.
  5. Power up the badge with the small switch near the USB-C port.
    Note: the small switch near the USB-C port on the laptop might also work. Note: In doubt, use both switches.
  6. Dump the firmware with esptool.
    Note: the baud value is random because, for some reason, there is no max-speed or any speedtest function. I don’t know either if there is any error detection or if the firmware file could be corrupted. The size also wants an exact number, but I cannot do math — since bash can, I never bothered to learn.
$ esptool -p /dev/ttyACM0 -b ${RANDOM}00 read_flash 0 $((2**32)) fw.bin
esptool.py v4.7.0
Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (QFN32) (revision v0.4)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e4:b0:63:c3:dc:c4
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 1337000
Changed.
4194304 (100 %)
Read 4194304 bytes at 0x00000000 in 46.9 seconds (714.9 kbit/s)...
Hard resetting via RTS pin...
$ ls -lh fw.bin
-rw-rw-r-- 1 privat privat 4,0M 19 mai 14:56 fw.bin
$ grep -a 

Note: For peace of mind, I used the IA-enhanced grep tool to find flags in the firmware, but it didn’t find anything — IA is so overrated.

$ grep -ia fw.bin
...halucinated.contents...

Steganography (concealing information in images)

Arthur : ça me dit quelque chose ce coin…

Léodagan : Oh bah vous commenterez la visite une autre fois hein !

Arthur : Ah non non mais sans blague j’ai l’impression que j’suis déjà venu.

We were given some hardware, now we have just extracted the firmware, but the future is software; we need software.

There is a tool that claims to be able to extract an ELF from an ESP32 firmware — last commit was five years ago.

$ ./esp32_image_parser.py show_partitions fw.bin 
reading partition table...
entry 0:
  label      : otadata
  offset     : 0xd000
  length     : 8192
  type       : 1 [DATA]
  sub type   : 0 [OTA]

entry 1:
  label      : phy_init
  offset     : 0xf000
  length     : 4096
  type       : 1 [DATA]
  sub type   : 1 [RF]

entry 2:
  label      : factory
  offset     : 0x10000
  length     : 1572864
  type       : 0 [APP]
  sub type   : 0 [FACTORY]

entry 3:
  label      : ota_0
  offset     : 0x190000
  length     : 1572864
  type       : 0 [APP]
  sub type   : 16 [ota_0]

entry 4:
  label      : nvs
  offset     : 0x370000
  length     : 204800
  type       : 1 [DATA]
  sub type   : 2 [WIFI]

MD5sum: 
b1f35c38108ac442f3745e4ed8b6160f
Done
  • otadata and ota_0. OTA is used to update the firmware remotely by wifi. Maybe the CTF will do a flash update live… Or better, we will have to hack some OTA firmware update… I’m so hyped!
  • phy_init. It is the bootloader partition (the 2nd stage).
  • factory. Is the real NothSec application that drives the LEDs, the buttons, the IR, the social credits…
  • nvs: It is Non-Volatile Storage. Maybe it stores the social credits?

Extracting ELF

Let’s go!

$ ./esp32_image_parser.py create_elf -partition factory -output factory.elf ../conf/fw-orig.bin 
Dumping partition 'factory' to factory_out.bin
Traceback (most recent call last):
  File "/home/privat/work/ctf/nsec25/badge/esp32_image_parser/./esp32_image_parser.py", line 281, in <module>
    main()
    ~~~~^^
  File "/home/privat/work/ctf/nsec25/badge/esp32_image_parser/./esp32_image_parser.py", line 264, in main
    image2elf(dump_file, output_file, verbose)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/privat/work/ctf/nsec25/badge/esp32_image_parser/./esp32_image_parser.py", line 41, in image2elf
    image = LoadFirmwareImage('esp32', filename)
            ^^^^^^^^^^^^^^^^^
NameError: name 'LoadFirmwareImage' is not defined

That was quite unexpected. I was not in the mood to fix some random old Python code. So, it was time to update my findings on the teammates.

But nothing to update: Fob did everything first, extracted the firmware, uploaded it to out Discord, extracted the partition, uploaded them, saw the bug the the tool, fixed the tool, send a patch on our discord, uploaded the ELF, reverse-engineered the NVS data, updated is social and sponsor score to the max, and uploaded a patched firmware for the team. It was not even 15pm the first day…

nights at C

Education and training are the key to success. Therefore, instead of sleeping and being at 100% physical, mental, and emotional capacity for the CTF, why not spend the night doing useless reverse engineering of the machine code of the badge for no points? It seems to be classical C (with some C++) on a nice modern architecture. There is nothing to be afraid of!

$ readelf -S factory.elf 
There are 9 section headers, starting at offset 0xb4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 00021c 000000 00      0   0  0
  [ 1] .shstrtab         STRTAB          00000000 00021c 00005c 00      0   0  1
  [ 2] .flash.rodata     PROGBITS        3c080020 000278 02b1ac 00  WA  0   0 16
  [ 3] .dram0.data       PROGBITS        3fc8fe00 02b424 001eb4 00  WA  0   0 16
  [ 4] .iram0.vectors    PROGBITS        40380000 02d2d8 002f88 00  AX  0   0  4
  [ 5] .iram0.text       PROGBITS        40382f88 030260 00ccd0 00  AX  0   0  4
  [ 6] .flash.text       PROGBITS        42000020 03cf30 074850 00  AX  0   0  4
  [ 7] .strtab           STRTAB          00000000 0b1780 0027c3 00      0   0  1
  [ 8] .symtab           SYMTAB          00000000 0b3f43 003790 10      7 889  4
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

But the binary code is a mess, and we do not know what we are really looking for. We do not know either what is part of the framework and what is the business logic of the badge.

IoT

I know next to nothing about IoT, except the common joke that the S in IoT stands for security.

The grand plan is to have a basic overview of the software architecture and possibly be able to run or debug the badge in a controlled environment or simulator.

I installed esp-idf, built examples/get-started/hello_world/, and uploaded it to the badge. Everything went mostly fine, and I got a running hello word on the NSec hardware.

But no screen. I found nothing in the examples about screens. And the internet is a mess of random acronyms, incompatible hardware parts, crappy python code, IDE screenshot, and non-standard protocols or connections.

So, let’s dig into the nsec firmware and get some information. The starting console log is not that helpful.

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xe (SPI_FAST_FLASH_BOOT)
Saved PC:0x40058eb6
--- 0x40058eb6: strlen in ROM

SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x1188
load:0x403cc710,len:0x8d4
load:0x403ce710,len:0x2c08
entry 0x403cc710

What about raw strings in the firmware? There were no flags, but some other strings could hint at useful information.

  • some user-interface string (error messages, labels, ASCII art of the boat)
  • some function or global data identifiers (esp_reset_reason_set_hint, pthread_once, etc.)
  • some source paths (debug information or logging information?), like IDF\components\esp_partition\partition.c

Those can give us an idea of what IDF components are used to build the badge software, thus hinting at what the badge hardware is. I assume that IDF files are the base features of the chip; the other ones should be specific components of the badge, especially the screen. Here they are:

components/cmd/console.cpp
components/utils/lock.hpp
components/utils/logging.hpp
components/badge-network/network_handler.cpp
components/badge-network/ir_interface.cpp
components/ssd1306/ssd1306_i2c_new.c
components\badge-persistence\config_store.cpp
components\badge-persistence\badge_store.cpp
components\badge-persistence\utils.cpp
components\badge-led-strip\strip_animator.cpp
components/cmd_sys/cmd_sys.cpp

Nothing related to any screen… but wait, what ssd1306 is? Solid-state drive? Bingo! it’s a component that drives small screens. With an API and examples.

The badge has four pins: GND (ground), VCC (power supply), SCL (clock), and SDA (data) for a small 4x1 ratio screen. So, according to the pictures on the GitHub project, it could be SSD1306 128x32 i2c.

To confirm the hypothesis, we can try to compile and run one of the examples. There are a bunch of small programs in the GitHub project, and the API is quite straightforward.

SSD1306_t dev;
i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO);
ssd1306_init(&dev, 128, 32);
ssd1306_clear_screen(&dev, false);
ssd1306_contrast(&dev, 0xff);
ssd1306_display_text(&dev, 1, "HELLO", 5, false);
ssd1306_bitmaps(&dev, 0, 0, image, 128, 32, false);

But unfortunately, I could not get a working example, because I didn’t know the configuration values *_GPIO. And because I was afraid of brute-forcing random hardware parameters and frying my badge — it could be unlikely, but because I do not know hardware stuff, better safe than sorry here.

Nevertheless, the framework exploration was useful. We now have a precise idea of how the badge displays text and images on the screen.

Ghidra

Léodagan : Ah là par contre j’me suis pas foutu d’vous! Des sacs d’or pleins une caisse ! Et pour garder le tout : une hydre à six têtes !

Ghidra loaded and decompiled the ELF. Unfortunately, the ELF extracted from the firmware might have lost some of its feathers. The debugging information is partial, and addresses might be fussy. Moreover, Ghidra had a hard time with the RISC-V ISA.

So, it is a mess, but we know what we are looking for: the parts of the code that display text and images. The black and while nsec̑ logo and the content of the strings should be in the RO data section. Text should be easier to spot because it’s ASCII, and we can grep it.

Note: I never had the time to learn to use Ghidra or read the book. Therefore, I only use features according to the pictures on buttons and the words on labels. First, search the text: Search>Memory>String>“Social Level”

What is interesting is the full content of the string: "Social Level %3u". It is a C format string that expects a single argument, an unsigned integer.

What is also interesting are the two functions that use this string:

  • FUN_42001602 at the instruction 4200163e
  • FUN_42001f1c at the instruction 420020c8

Here is the decompiled code of FUN_42001602 (comments are mine).

void FUN_42001602(int param_1)
{
  undefined1 auStack_24 [20];
  // maybe a char[20]
  
  *(undefined1 *)(param_1 + 0xc) = 1;
  FUN_420176f0();
  if ((*(char *)(param_1 + 0xc) == '\x01') && (*(char *)(param_1 + 0xd) == '\0')) {
    FUN_42062f60(auStack_24,s_Social_Level_%3u_3c080a50,*(undefined1 *)(param_1 + 4));
    // our string is used to initialize auStack_24
    FUN_42017768(0,auStack_24,0x10,0);
    // auStack_24 is then used here
    if ((*(char *)(param_1 + 0xc) == '\x01') && (*(char *)(param_1 + 0xd) == '\0')) {
      FUN_42062f60(auStack_24,s_Animation_%3u_3c080a64,*(undefined1 *)(param_1 + 9));
      // same thing for the 2nd displayed line
      FUN_42017768(1,auStack_24,0x10,0);
      if ((*(char *)(param_1 + 0xc) == '\x01') && (*(char *)(param_1 + 0xd) == '\0')) {
        FUN_42062f60(auStack_24,s_Sponsor_%3u_3c080a78,*(undefined1 *)(param_1 + 5));
        // same thing for the 3rd displayer line
        FUN_42017768(2,auStack_24,0x10,0);
        *(undefined1 *)(param_1 + 0xb) = 0;
        return;
      }
    }
  }
  *(undefined1 *)(param_1 + 0xb) = 0;
  return;
}

We see the usage of the “Social_Level” string (but also the 2 other labels of the status screen). We are at the right place: FUN_42001602 displays the status screen.

FUN_42062f60 takes 3 parameters: an address in the stack (a char[20]?), the “SocialLevel” label (with the format field %3u), and a value (a field in a structure?). What could it be? Possibly sprintf to write the value and have the full string to display.

    FUN_42017768(0,auStack_24,0x10,0);
    //...
      FUN_42017768(1,auStack_24,0x10,0);
      //...
        FUN_42017768(2,auStack_24,0x10,0);

FUN_42017768 is the only function that uses the sprinted string (auStack_24). So it should be the function to display the text on the screen. The first parameter could be the line number, the other parameters could be size or style information.

Let’s look at the decompiled code of this mysterirous function:

void FUN_42017768(undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4)
{
  if (_DAT_3fc93c84 == 0) {
    FUN_4201767c(param_4);
  }
  FUN_42017f08(_DAT_3fc93c84,param_1,param_2,param_3,param_4);
  return;
}

If _DAT_3fc93c84 (a global value) is not defined, do FUN_4201767c. In all cases, call FUN_42017f08 with the same parameters plus this global value. This seems to be a lazy initialization of _DAT_3fc93c84, then the call of the real display method.

FUN_42017f08 takes the global value, the line, the text, the length of the text, and a 0. This could be the function ssd1306_display_text of the display component. It means that _DAT_3fc93c84 is a SSD1306_t dev global handle on the screen.

We can try to glance at FUN_4201767c to confirm that it is the initialization of the handle.

void FUN_4201767c(void)
{
  undefined4 uVar1;
  
  // guard if _DAT_3fc93c84 is already initalized
  if (_DAT_3fc93c84 != 0) {
    FUN_42017f14(0);
    return;
  }
  FUN_420175cc();
  // allocate _DAT_3fc93c84
  uVar1 = FUN_4201a7ce(0x478);
  (*(code *)&SUB_40000354)(0,0x478);
  _DAT_3fc93c84 = uVar1;
  // setup _DAT_3fc93c84
  FUN_42017800(uVar1,4,5,0xffffffff);
  FUN_42017e48(_DAT_3fc93c84,0x80,0x20);
  FUN_42018038(_DAT_3fc93c84,0xff);
  FUN_42017f14(_DAT_3fc93c84,0);  return;
}

Yes, it is. And we also got this line:

FUN_42017800(uVar1,4,5,0xffffffff);

This should be

i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO);

so we have found the values SDA=4, SCL=5 and RESET=-1 of the display.

Deception

At NorthSec, the badge is a social device. But social hacking is essentially deception. Therefore, to hack the badge, we must use deception!

We know the configuration values of the screen. We can compile and execute the examples of the esp-idf-sdd1306 project.

And it did work, we control the badge; if we change all the software, we can display text and images.

Text is easy, it’s ASCII. Images are another matter: they need a black and white encoding. Old computer scientists know some of them, the XBM format is one of the simplest: it’s just a bunch of bits (white or black) stored as C arrays of bytes. The XBM format is also standard and most image tools (convert, gimp, etc) can read and write them. The fun thing about that format is that the content is valid C code you can simply #include a .xbm image in your C programs. Except that, for some reason, the badge does not use this standard and universal format, but a custom one. So, I used a converter found on the web to convert images of chickens to some custom black and white encoding.

Here is a snippet of the example program I developed. Get the full code here.

uint8_t poulet_bits[] = {
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xfa, 0x4f, 0xff, 0xff, 0xe3, 0x0f, 0xff, 
	0xff, 0xc3, 0xe7, 0xff, 0xff, 0xdf, 0xe1, 0xff, 0xff, 0xcf, 0xf1, 0xff, 0xff, 0x8f, 0xf9, 0xff, 
	0xfe, 0x04, 0x38, 0x7f, 0xfe, 0x31, 0x00, 0x7f, 0xfe, 0x0c, 0xc1, 0xff, 0xff, 0x9e, 0x4c, 0x7f, 
	0xff, 0x9e, 0x66, 0x3f, 0xff, 0x31, 0x36, 0x3f, 0xfe, 0x31, 0xb3, 0x3f, 0xfe, 0x31, 0x98, 0x7f, 
	0xfc, 0x11, 0x99, 0xff, 0xf8, 0x0f, 0xd9, 0xff, 0xf0, 0x07, 0xdc, 0xff, 0xfc, 0x03, 0xcd, 0xbf, 
	0xfc, 0x0b, 0xcd, 0xbf, 0xfc, 0x19, 0xcd, 0xbf, 0xfc, 0x09, 0xcf, 0x3f, 0xf8, 0x01, 0xce, 0x3f, 
	0xfc, 0x23, 0xcc, 0x7f, 0xfc, 0x7f, 0xd9, 0x9f, 0xfc, 0x3f, 0x93, 0x9f, 0xfc, 0x0f, 0x26, 0x3f, 
	0xfe, 0x66, 0x0c, 0x7f, 0xff, 0x30, 0x99, 0xff, 0xff, 0xf3, 0xa3, 0xff, 0xff, 0xff, 0xaf, 0xff
};
void app_main(void)
{
	SSD1306_t dev;
	i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO);
	ssd1306_init(&dev, 128, 32);
	ssd1306_contrast(&dev, 0xff);
	while(1) {
		ssd1306_clear_screen(&dev, false);
		ssd1306_display_text(&dev, 1, "Hubert", 6, false);
		ssd1306_display_text(&dev, 2, "Hackin''", 8, false);
		ssd1306_bitmaps(&dev, 127-32, 0, poulet_bits, 32, 32, true);
		vTaskDelay(5000 / portTICK_PERIOD_MS);

		ssd1306_clear_screen(&dev, false);
		ssd1306_display_text(&dev, 0, "Social Level xFF", 16, false);
		ssd1306_display_text(&dev, 1, "Animation     -1", 16, false);
		ssd1306_display_text(&dev, 2, "Sponsor      NaN", 16, false);
		vTaskDelay(5000 / portTICK_PERIOD_MS);
	}
}
  • Build.
  • Flash.
  • Run.
  • Publish.

Victory?

Léodagan : Je ne passerai pas pour un con auprès d’mes hommes ! J’ai dit « j’ramène un trésor », j’ramène un trésor.

Ok, that was fun. But can we hack the real NSec original firmware instead of developing our own from scratch? The benefits will be to keep intact all the other features like the leds and pairing.

The real stuff

We have the binary, we can simply change some bits to achieve what we want. Let’s go back to Ghidra.

We got the function that displays strings, and the initialization of the display handle. We can ask Ghidra what are the other usages of the handle. One of these must be the display of the NSec logo.

  • contextual menu>References>Find references to _DAT_3fc93c84

One of the function that use the handle is FUN_42017720. That function is only used by FUN_420177ea.

void FUN_42017720(undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
                 undefined4 param_5,undefined4 param_6)
{
  if (_DAT_3fc93c84 == 0) {
    FUN_4201767c(param_6);
  }

Where FUN_42018202 could be ssd1306_bitmaps(&dev, x, y, image, width, height, inverted);

void FUN_420177ea(void)
{
  FUN_42017720(gp + -0x5ac,0,0,0x80,0x20,0);
  return;
}

Victory, we found the function that displays the NSec logo, and the address of the logo… except that the value gp is unknown. Usually, the register gp is used for data access: gp is set to the middle of the data segment, then all access to global variables can use a simple address mode, relative to gp (positive or negative).

Here, I was bored and inefficient. Instead of finding the value of gp, I simply searched in the data segment a blob of binary with a lot of 00 (a horizontal line of 8 white pixels) and FF (8 black pixels). The address was 0x2B428.

Then I used an hexadecimal editor, to change the bits of the image with a nice picture and the labels with hard-coded values, uploaded the firmware… and that failed. The badge went into a reboot loop.

SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x1188
load:0x403cc710,len:0x8d4
load:0x403ce710,len:0x2c08
entry 0x403cc710
E (140) esp_image: Image hash failed - image is corrupt
E (140) boot: Factory app partition is not bootable
E (140) esp_image: image at 0x190000 has invalid magic byte (nothing flashed here?)
E (140) boot: OTA app partition slot 0 is not bootable
E (141) boot: No bootable app partitions in the partition table
ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x3 (RTC_SW_SYS_RST),boot:0xe (SPI_FAST_FLASH_BOOT)
Saved PC:0x40048b82
--- 0x40048b82: software_reset in ROM

Damn, esp_image: Image hash failed - image is corrupt. Checksums are so annoying.

To understand what exactly is happening, we must be able to reproduce the boot loop in a controlled environment.

  • Get the helloword example.
  • Compile.
  • Change a byte in the image. e.g. Hello → HeXlo.
  • Flash.
  • Run.
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x1574
load:0x403cc710,len:0xc30
load:0x403ce710,len:0x2f64
entry 0x403cc71a
I (24) boot: ESP-IDF v5.4.1-dirty 2nd stage bootloader
I (24) boot: compile time May 16 2025 19:21:36
I (24) boot: chip revision: v0.4
I (24) boot: efuse block revision: v1.3
I (28) boot.esp32c3: SPI Speed      : 80MHz
I (32) boot.esp32c3: SPI Mode       : DIO
I (36) boot.esp32c3: SPI Flash Size : 4MB
I (39) boot: Enabling RNG early entropy source...
I (44) boot: Partition Table:
I (46) boot: ## Label            Usage          Type ST Offset   Length
I (53) boot:  0 phy_init         RF data          01 01 0000f000 00001000
I (59) boot:  1 factory          factory app      00 00 00010000 00100000
I (66) boot:  2 nvs              WiFi data        01 02 00370000 00032000
I (72) boot: End of partition table
I (76) esp_image: segment 0: paddr=00010020 vaddr=3c020020 size=08c0ch ( 35852) map
I (89) esp_image: segment 1: paddr=00018c34 vaddr=3fc8ba00 size=0128ch (  4748) load
I (91) esp_image: segment 2: paddr=00019ec8 vaddr=40380000 size=06150h ( 24912) load
I (102) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=16770h ( 92016) map
I (120) esp_image: segment 4: paddr=00036798 vaddr=40386150 size=056ech ( 22252) load
I (124) esp_image: segment 5: paddr=0003be8c vaddr=50000200 size=0001ch (    28) load
E (125) esp_image: Checksum failed. Calculated 0x8a read 0xbe
E (129) boot: Factory app partition is not bootable
E (134) boot: No bootable app partitions in the partition table
ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x3 (RTC_SW_SYS_RST),boot:0xe (SPI_FAST_FLASH_BOOT)
Saved PC:0x40048b82
--- 0x40048b82: software_reset in ROM

Same issue, except that the log is far more verbose — maybe it is the default build option of IDF as I changed nothing.

esp_image: Checksum failed. Calculated 0x8a read 0xbe

We know what we are failing… So, we can try to fix the image to match the checksum, or fix the checksum to match the image. We should also RTFM and RTFM.

Note: At this point, I was confused. My confusion was the understanding of what ESP-IDF calls an “image”. Initially, I thought the image was the full firmware; it is the meaning with virtualisation and emulation, for instance: the whole memory dump or disk dump (with or without some metadata). Whereas ESP-IDF uses “image” for an executable partition, so only a part of the firmware, but a whole application with segments that can be loaded in RAM. An ESP-IDF image corresponds more or less to a ELF file: this explains the commands image_info and elf2image of esptool.

What is important is found in the documentation of the image format.

The image has a single checksum byte after the last segment. This byte is written on a sixteen byte padded boundary, so the application image might need padding. If the hash_appended field from esp_image_header_t is set then a SHA256 checksum will be appended. The value of the SHA256 hash is calculated on the range from the first byte and up to this field. The length of this field is 32 bytes.

So, there is a byte at the end of the image that we can change. But where exactly is this byte? Maybe we can do math and sum the lengths of the sections. But maybe we did not read the damn manual as thoroughly as we should, so why not ignore the easy path and improve the error message to give more information?

So, which part of the firmware complained?

$ grep -r 'Checksum failed. Calculated'
...
components/bootloader_support/src/esp_image_format.c:        FAIL_LOAD("Checksum failed. Calculated 0x%x read 0x%x", calc_checksum, read_checksum);
...

The bootloader job is to load the application. Naturally, it is the one chat checks and complains about checksums. But the bootloader is a normal piece of software, so let’s just hack it and increase the verbosity of messages to display the address of the stored checksum.

diff --git a/components/bootloader_support/src/esp_image_format.c b/components/bootloader_support/src/esp_image_format.c
index 386cafdc87..9ce224d18c 100644
--- a/components/bootloader_support/src/esp_image_format.c
+++ b/components/bootloader_support/src/esp_image_format.c
@@ -944,10 +944,14 @@ static esp_err_t process_checksum(bootloader_sha256_handle_t sha_handle, uint32_
     if (!skip_check_checksum || sha_handle != NULL) {
         CHECK_ERR(bootloader_flash_read(data->start_addr + unpadded_length, buf, length, true));
     }
+    ESP_LOGI(TAG, "XXXX process_checksum length=%ld start_addr=0x%08lx checksum_word=0x%08lx chkaddr?=0x%08lx", length, data->start_addr, checksum_word, data->start_addr + unpadded_length + length - 1);
     uint8_t read_checksum = buf[length - 1];
     uint8_t calc_checksum = (checksum_word >> 24) ^ (checksum_word >> 16) ^ (checksum_word >> 8) ^ (checksum_word >> 0);
     if (!skip_check_checksum && calc_checksum != read_checksum) {

Et voilà! We can now

  • Rebuild firmware hello world; that will rebuild the bootloader.
  • Change a Byte in the firmware Hello → HeXlo.
  • Flash.
  • Run.

Same boot loop, but with improved information.

...
I (125) esp_image: XXXX process_checksum length=8 start_addr=0x00010000 checksum_word=0x6a756eae chkaddr?=0x0003beaf
I (134) esp_image: Checksum failed. Calculated 0xdf read 0xeb
  • Change the checksum byte at 0x0003beaf to 0xdf.
  • Flash.
  • Run.
...
I (125) esp_image: XXXX process_checksum length=8 start_addr=0x00010000 checksum_word=0x6a756eae chkaddr?=0x0003beaf
I (134) esp_image: Checksum success. Calculated 0xdf read 0xdf
E (140) esp_image: Image hash failed - image is corrupt.
E (129) boot: Factory app partition is not bootable
E (134) boot: No bootable app partitions in the partition table
ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x3 (RTC_SW_SYS_RST),boot:0xe (SPI_FAST_FLASH_BOOT)
Saved PC:0x40048b82
--- 0x40048b82: software_reset in ROM

Waaat?

What about the second checksum?

Damn, this is annoying. There is a second MD5 checksum.

At this point, I was really bored. Then I remember what we learned during the whole journey: the badge is about deception.

So I simply hacked the bootloader to ignore the checksums.

diff --git a/components/bootloader_support/src/esp_image_format.c b/components/bootloader_support/src/esp_image_format.c
index 386cafdc87..9ce224d18c 100644
--- a/components/bootloader_support/src/esp_image_format.c
+++ b/components/bootloader_support/src/esp_image_format.c
@@ -944,10 +944,14 @@ static esp_err_t process_checksum(bootloader_sha256_handle_t sha_handle, uint32_
     if (!skip_check_checksum || sha_handle != NULL) {
         CHECK_ERR(bootloader_flash_read(data->start_addr + unpadded_length, buf, length, true));
     }
+    ESP_LOGI(TAG, "XXXX process_checksum length=%ld start_addr=0x%08lx checksum_word=0x%08lx chkaddr?=0x%08lx", length, data->start_addr, checksum_word, data->start_addr + unpadded_length + length - 1);
     uint8_t read_checksum = buf[length - 1];
     uint8_t calc_checksum = (checksum_word >> 24) ^ (checksum_word >> 16) ^ (checksum_word >> 8) ^ (checksum_word >> 0);
     if (!skip_check_checksum && calc_checksum != read_checksum) {
-        FAIL_LOAD("Checksum failed. Calculated 0x%x read 0x%x", calc_checksum, read_checksum);
+        ESP_LOGI(TAG, "Checksum failed. Calculated 0x%x read 0x%x, but ignore it", calc_checksum, read_checksum);
     }
     if (sha_handle != NULL) {
         bootloader_sha256_data(sha_handle, buf, length);
@@ -1051,13 +1055,14 @@ static esp_err_t verify_simple_hash(bootloader_sha256_handle_t sha_handle, esp_i
 
     // Simple hash for verification only
     if (memcmp(data->image_digest, image_hash, HASH_LEN) != 0) {
-        ESP_LOGE(TAG, "Image hash failed - image is corrupt");
+        ESP_LOGE(TAG, "Image hash failed - image is corrupt. but ignore it");
         bootloader_debug_buffer(data->image_digest, HASH_LEN, "Expected hash");
 #if CONFIG_IDF_ENV_FPGA || CONFIG_IDF_ENV_BRINGUP
         ESP_LOGW(TAG, "Ignoring invalid SHA-256 as running on FPGA / doing bringup");
         return ESP_OK;
 #endif
-        return ESP_ERR_IMAGE_INVALID;
+        return ESP_OK;
     }

Et voila… I can upload and run the corrupted hello word program! Isn’t that wonderful?

Oh, the original NSec firmware? No problem! Just replace the original bootloader in the firmware file by the lenient one.

hacked badge

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2025-05-21 | theme hugo.386