Writeup by etan.1444 for Extensif

reverse

July 15, 2026

Introduction

In this write-up, we will install the esp32-flash-image-loader extension in Ghidra. Since the target is an ESP32, we will solve this challenge statically.

Basic static analysis

Running file and strings reveals that the binary is an ESP32 firmware containing useful text.

$ file extensif.bin
extensif.bin: ESP-IDF application image for ESP32, project name: "extensif", version 1, compiled on Mar 24 2026 15:29:16, IDF version: v5.5.1, entry address: 0x400811E8
$ strings extensif.bin | grep -i "FCSC"  
preuve avec FCSC{%s}

Installing esp32 flash image loader

We install an extension to import ESP32 flash images, as Ghidra cannot recognize the binary by default.

Install gradle and clone the repository github.com/dynacylabs/ghidra-esp32-flash-loader.git

git clone https://github.com/espressif/xtensa-isa-doc.git
cd ghidra-esp32-flash-loader
export GHIDRA_INSTALL_DIR=`YOUR_GHIDRA_PATH_HERE`
gradle

The ZIP extension is located in the dist folder. Move it to ghidra/Extensions/Ghidra. Next, launch Ghidra, go to File -> Install Extensions, and enable it. If it does not appear, add it manually using the + button. image

Ghidra should now recognize the format and language.

image

Static analysis

Now for the classic static analysis. We found the string “FCSC” during basic static analysis, so let’s locate it in Ghidra.

image

We found the main algorithm. It first initializes a flag to "FCSCFCSCFCSCFCSC", then calls a function to modify it. Finally, it XORs the modified flag with some data.

void main_algo(void)
{
    int i*4;
    int i;
    char flag_xored [33];
    byte expected [48];
    code *ret_fun;
    undefined *str;
    undefined *xor keys;
    
    str = PTR_s_Bienvenue_400d0698;
    puts(str);

    // Init flag to FCSCFCSCFCSCFCSC
    for (i = 0; i < 4; i = i + 1) (
        1*4 = j * 4;
        str = flags;
        str[i * 4] = 'F';
        str[i*4 + 1] = 'C';
        str[i*4 + 2] = 'S';
        str[i*4 + 3] = 'C';
    }

    modif_expected_flag();

    // XOR flag with keys
    for (i = 0; i < 16; i = i + 1) {
        xor_keys = xor_keys_ptr;
        str = flags;
        expected[i] = xor_keys[i] ^ str[i];
    }

    // [...]

Here is the modif_expected_flag function and the trick of the challenge: the function uses an uninitialized a0 register, which Ghidra renamed to unaff_retaddr.

void modif_expected_flag(void)
{
    char *flag_ptr;
    flag_ptr = flags;
    algo_rec(74, flag_ptr);
    return;    
}

image

In Ghidra, the unaff_ prefix indicates that the variable is unaffected, and retaddr means the register used holds the return address, which is a0 in Xtensa architecture. The return address is utilized when a ret instruction is called.

The information about unaff_ can be found in Ghidra: Fix unaff_ via Set Register Values… by 0x6d696368 The calling convention details are available in the xtensa documentation pdf.

The first unaff_retaddr is the address of the modif_expected_flag return instruction: 0x400d60ef. The other unaff_retaddr values are the address of the algo_rec return instruction: 0x400d6108.

The rest of the main algorithm is just a simple scanf and memcmp.

Full solve script

def modif_expected_flag(n, flag, ind, unaff_retaddr):
    if (n & 8 != 0):
        flag[ind] = n
        flag[ind+1] = unaff_retaddr
        modif_expected_flag(n+1, flag, ind+2, 0x08)

flags = list(map(ord, list("FCSCFCSCFCSCFCSC")))
modif_expected_flag(74, flags, 0, 0xef)
xor_keys = [0x79, 0x8d, 0x2d, 0x3e, 0x2e, 0x6e, 0x79, 0x6d, 0x28, 0x38, 0x2d, 0x38, 0x77, 0x75, 0x60, 0x21]

xored_flag = [xor_keys[i] ^ flags[i] for i in range(16)]

res = "".join(list(map(chr, xored_flag)))
print(f"FCSC{{{res}}}")

# FCSC{REDACTED}