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.
Ghidra should now recognize the format and language.
Static analysis
Now for the classic static analysis. We found the string “FCSC” during basic static analysis, so let’s locate it in Ghidra.
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;
}
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}