In the previous post, some brief information on analyzing memory corruption issues was given. In the following post, a few prevalent issues in this category that can lead to security impacts will be addressed.
It all starts with a runtime error leading to a crash, but in the end, it all boils down to one question: whether a memory corruption issue can be exploited or not. The exploitability of a memory corruption vulnerability depends on lot of factors and exploit developers generally face a lot of challenges (and frustrations due to exploit mitigations and countermeasures) while developing a `reliable` exploit. Sometimes a vulnerability that is not exploitable at the moment may become exploitable in the upcoming releases; sometimes a reliable exploit can be developed by chaining multiple low level vulnerabilities. The price of a `reliable` exploit normally ranges from $10,000 to $1 Million or more; depending on the target and respective `market’s` business model. Hence, such vulnerabilities are usually treated as exploitable by default.
The memory corruption issues in a software are identified if a crash has occurred during its `runtime` when accessing the contents at an arbitrary memory location that was not programmatically intended. Such vulnerabilities in the open source software we’ve analyzed (in C, C++, Ruby, Golang packages) were identified primarily through fuzzing (mostly AFL) the application binary – basically, a new issue in its source code repositories was created with the respective crash reports as attachments.
Manually identifying them is quite challenging due to the fact that a range of typical scenarios or boundary conditions have to addressed just by reading the source code. Some vulnerabilities are easy to find at the assembly level but not at the source code level. For example, integer errors like sign extensions can be identified easily using disassemblers by observing `movsx` instructions (in Intel platforms); however, it is usually hard to interpret the value changing type conversions causing sign-extensions (eg: signed char to unsigned int) that are performed by the compiler, just by reading the source code.
A few scenarios addressing a software’s operations that leads to memory corruptions are mentioned below.
- A read/write operation from/to a memory location that is outside of the intended boundary of the buffer.
- A write operation exceeding the destination buffer’s length, thereby overwriting other variables and stack pointers present on the stack.
- A free() is called using the same pointer which has already been free()’d.
- An arithmetic operation or a condition, resulting the integer value to be incremented to a huge value and wraparound.
There are number of factors that are considered while developing a fully functional and reliable exploit. However, we’ll address a few while tackling the vulnerable scenarios mentioned in the previous section that are essential for a developer to contextualize the impact of a memory corruption in her/his code.
- Whether the buffer is present in stack, heap or BSS (sometimes, unprotected buffers in BSS can allow important pointers to be overwritten)
- Whether the data used to overwrite memory can be controlled directly by the attacker i.e whether the pointers that are overwritten can be replaced with a pointer to attacker controlled data.
- What data is corrupted – whether the variable in adjacent data structure that is not used in the current execution context. (unexploitable)
- Whether over-write is possible to multiple memory locations at once. (eg: leveraging format string vulnerabilities). Which usually increases the likelihood of exploit.
There are complex cases as well, for example:
- If the overflow is of only one byte and the attacker don’t have enough liberty to choose what data in the memory can be overwritten with.
- If the data supplied by the attacker to a memory location is being freed in the subsequent instructions.
- Controlling the overwritten data in the multithreaded applications assuming concurrency and synchronization.
A motivated attacker can compromise the software with a multitude of techniques. For example, if the buffer’s size is not enough to hold the required shell code or if the stack segments are non-executable (NX), or even if the program is dropping the privileges upon executing an argument (`/bin/sh`) to `system()` function call; the attacker still find a way by chaining up the required trampolines and conduct a ret2libc style attacks, even restoring the privileges by invoking `setreuid()`.
Hence, memory corruption issues shouldn’t be regarded as low priority issues even if they cannot be exploited at that moment. The developers must treat all memory corruption issues as exploitable by default.
Credit: ACE Team – Loginsoft