- Samuel S., Senior Vulnerability Researcher
During an audit of NUUO’s NVRmini2, a stack overflow vulnerability was discovered in a request handling function in the ‘lite_mv’ custom SIP service binary. The NUUO NVRmini2 runs a custom SIP service on TCP ports 5160 and 5150 via a binary at /NUUO/bin/lite_mv. In order to examine this bug more closely, we analyze the function which handles SIP "NUSP" client requests.
When a client sends a request to the service on TCP port 5150, the ‘lite_mv’ binary calls a read-only relocated function from 0x0008fed8 from a thread.
The function creates 404 bytes of stack buffer for local variables on top of 28 bytes pushed from registers, including the return address from lr.
A call is made to sub_36d40 with arguments "argument_to_function + 188", "sscanf_source_buffer", "\r\n", "2048". It is assumed that these arguments are, respectively, an offset pointer passed to the calling function, a pointer to the buffer which will be later passed to sscanf, a string representing the bytes to receive up to, and a maximum number of bytes to receive.
This appears to be intended to receive the first line of a SIP request, up to \x0d\x0a, or 2048 bytes.
During dynamic analysis, sending a request totaling more than 2048 bytes did not proceed beyond this execution path. It was also noted that any METHOD field in the request would reach this execution path regardless of its value, as the value is checked later on in the function.
Next, another call is made to sub_36d40, this time replacing the pointer to the buffer that would be passed to sscanf with another offset of the pointer passed to the caller, and a different terminating string.
This appears to be intended to receive the next and final line of a SIP request, consisting of a CSeq header.
It was noted during dynamic analysis that this value could be arbitrary and still reach this execution path, as the value is not checked until later in the function, like the METHOD value above.
Next, the preparation of the arguments to sscanf are particularly interesting. The third argument to sscanf, r2, is a pointer to an offset inside var_1B0_sscanf_3rd_dest_pointer. The fourth argument to sscanf, r3, is set to stack_pointer+8.
This is interesting for two reasons, first, this location appears to be 8 bytes into the buffer used as the source buffer for the call to sscanf. Second, recalling that the function has only allocated 404 bytes on the stack for local variables, we can conclude that a URI value of 351 bytes will overflow the destination buffer, overwriting other local variables.
In pseudo code, this portion of the function might look like the following:
At this point, we can reasonably assume that we can overflow "var_1B0_sscanf_3rd_dest_pointer + 8" with 420 bytes of padding, and overwrite the functions return address with the 421st through 424th bytes and control execution flow. However, since this overflow occurs in beginning of a function with 115 basic blocks, there are some caveats to achieving code execution.
Aside from the ‘lite_mv’ target binary having NX enabled, we recall that the overflow also overwrites the local variables prepared at the beginning of the function.
If we follow the execution flow after the overflow occurs, we can see that, because we deliberately use a value other than "NUSP/1.0" or "NUSP/2.0" for the PROTOCOL value of the request, we follow a branch intended to handle a 500 internal server error response to the client.
Along this branch, we encounter the following instructions, which operate on one of the local variables that were overwritten by sscanf.
Here, the 337th through 340th bytes of our payload have overwritten the value of var_58_sscanf_source_buffer_pointer. This value is decreased by 12 and stored in r4, and compared to 0xffffffff. Since these values are not equal, the branch is taken.
This branch is not taken, and the following instructions are executed.
At this point, a function is called, sub_3366e4, which loads the value at the address stored in "var_58_sscanf_source_buffer_pointer - 4", wherein a kernel cmpxchg is performed on it, and if the address provided does not point to a string in a writeable memory region, the binary will SEGFAULT before execution flow reaches the terminating block of the function.
In order to bypass this caveat, bytes 337 through 340 must be a little endian, 32-bit integer representing the address of the original value of the local stack variable prior to the offending call to sscanf.
By crafting the payload to include this value, the following execution path is taken, resulting in our controlled return address being popped off the stack back into lr, and branched to.
From here, we can craft a GET request with a PROTOCOL value of anything but “NUSP”, a URI value which includes a leading “/”, 335 bytes of padding, a little endian, 32-bit integer represented as bytes which allows the program to continue execution, 80 bytes of padding, and a rop chain prepared without 0xff, 0x00, or 0x20.
Each portion of the request, as delimited by newline carriage returns, must be fewer than 2048 bytes in length. The second line of the request can consist of any non-bad characters, but must at least end with two sets of newline carriage returns.
Taking the path of least resistance, preparing a rop chain to directly achieve arbitrary command execution, as opposed to preparing a rop chain to bypass NX and directly execute code from the stack, we encounter a caveat in well charted waters; the executed command must not contain any spaces, or the space will be interpreted as the delimiter between the URI and PROTOCOL values. Though there are several ways to bypass this caveat, such as IFS characters or alternate delimiters like tabs, the most flexible solution which the target supported was encoding commands using bracket-expansion.
Note that exploiting this bug, successfully or not, will result in the service crashing and restarting. Continued execution was not explored while analyzing this bug.
Leveraging rop gadgets from shared libraries loaded by ‘lite_mv’, a proof-of-concept which executes arbitrary system commands was prepared, which was delivered to the vendor during the disclosure process.
The root cause of this stack overflow is an unsafe use of sscanf, using local stack variables as destination buffers. Because the lengths of strings are not checked against destination buffer sizes prior to the call to sscanf, it is possible to overflow local stack variables and gain control of the return address saved on the stack, and therefore, control of execution flow.
It should be noted that the affected service runs as root.