A Guide to Kernel Exploitation Attacking the Core [Paperback]
465 pág.

A Guide to Kernel Exploitation Attacking the Core [Paperback]


DisciplinaLinux714 materiais1.845 seguidores
Pré-visualização50 páginas
setting a boot flag or
modifying a value via a kernel debugger) or at compile time (via compile options). We
can take advantage of them to see how our heap exploit is behaving (is it overwriting a
chunk?) or employ them along with fuzzing to have a better understanding of the kinds
of bugs we hit.
INTEGER ISSUES
Integer issues affect the way integers are manipulated and used. The two most
common classes for integer-related bugs are (arithmetic) integer overflows and
sign conversion issues.
In our earlier discussion about data models, we mentioned that integers, like
other variables, have a specific size which determines the range of values that can
be expressed by and stored in them. Integers can also be signed, representing both
positive and negative numbers, or unsigned, representing only positive numbers.
With n representing the size of an integer in bits, logically up to 2n values can
be represented. An unsigned integer can store all the values from 0 to 2n \u2013 1,
whereas a signed integer, using the common two\u2019s complement approach, can
represent ranges from \u2013(2n \u2013 1) to (2n \u2013 1 \u2013 1).
Before we move on to a more detailed description of various integer issues,
we want to stress a point. This kind of vulnerability is usually not exploitable
per se, but it does lead to other vulnerabilities\u2014in most cases, memory over-
flows. A lot of integer issues have been detected in basically all the modern
kernels, and that makes them a pretty interesting (and, indeed, rewarding) bug
class.
(Arithmetic) Integer Overflows
An integer overflow occurs when you attempt to store inside an integer vari-
able a value that is larger than the maximum value the variable can hold. The
C standard defines this situation as undefined behavior (meaning that anything
might happen). In practice, this usually translates to a wrap of the value if an
unsigned integer was used and a change of the sign and value if a signed inte-
ger was used.
Integer overflows are the consequence of \u201cwild\u201d increments/multiplications,
generally due to a lack of validation of the variables involved. As an example,
Integer Issues 29
take a look at the following code (taken from a vulnerable path that affected the
OpenSolaris kernel;6 the code is condensed here to improve readability):
static int64_t
kaioc(long a0, long a1, long a2, long a3, long a4, long a5)
{
[\u2026]
switch ((int)a0 & ~AIO_POLL_BIT) {
[\u2026]
case AIOSUSPEND:
error = aiosuspend((void *)a1, (int)a2, (timespec_t *)a3, [1]
(int)a4, &rval, AIO_64);
break;
[\u2026]
/*ARGSUSED*/
static int
aiosuspend(void *aiocb, int nent, struct timespec *timout, int flag,
long *rval, int run_mode)
{
[\u2026]
size_t ssize;
[\u2026]
aiop = curproc->p_aio;
if (aiop == NULL || nent <= 0) [2]
return (EINVAL);
if (model == DATAMODEL_NATIVE)
ssize = (sizeof (aiocb_t *) * nent);
else
ssize = (sizeof (caddr32_t) * nent); [3]
[\u2026]
cbplist = kmem_alloc(ssize, KM_NOSLEEP) [4]
if (cbplist == NULL)
return (ENOMEM);
if (copyin(aiocb, cbplist, ssize)) {
error = EFAULT;
goto done;
}
[\u2026]
if (aiop->aio_doneq) {
if (model == DATAMODEL_NATIVE)
ucbp = (aiocb_t **)cbplist;
else
ucbp32 = (caddr32_t *)cbplist;
[\u2026]
for (i = 0; i < nent; i++) { [5]
if (model == DATAMODEL_NATIVE) {
if ((cbp = *ucbp++) == NULL)
30 CHAPTER 2 A Taxonomy of Kernel Vulnerabilities
In the preceding code, kaioc() is a system call of the OpenSolaris kernel that
a user can call without any specific privileges to manage asynchronous I/O. If the
command passed to the system call (as the first parameter, a0) is AIOSUSPEND [1],
the aiosuspend() function is called, passing as parameters the other parameters
passed to kaioc(). At [2] the nent variable is not sanitized enough; in fact, any
value above 0x3FFFFFFF (which is still a positive value that passes the check at
[2]), once used in the multiplication at [3], will make ssize (declared as a
size_t, so either 32 bits or 64 bits wide, depending on the model) overflow and,
therefore, wrap. Note that this will happen only on 32-bit systems since nent is
explicitly a 32-bit value (it is obviously impossible to overflow a 64-bit positive
integer by multiplying a small number, as, for example, at [3], by the highest
positive 32-bit integer). Seeing this in code form might be helpful; the following
is a 32-bit scenario:
0x3FFFFFFF \ufffd 4 = 0xFFFFFFFC ½fits in size\ufffdt\ufffd
0x400000000 \ufffd 4 = 0x100000000 ½does not fit in size\ufffdt andwill result to 0\ufffd
In the preceding code, the integer value is cropped, which translates to a loss
of information (the discarded bits). ssize is then used at [4] as a parameter to
kmem_alloc(). As a result, much less space is allocated than what the nent
variable initially dictated.
This is a typical scenario in integer overflow issues and it usually leads to other
vulnerabilities, such as heap overflows, if later in the code the original value is used
as a loop guard to populate the (now too small) allocated space. An example of this
can be seen at [5], even if in this snippet of code nothing is written to the buffer and
\u201conly\u201d memory outside it is referenced. Notwithstanding this, this is a very good
example of the type of code path you should hunt for in case of an integer overflow.
Sign Conversion Issues
Sign conversion issues occur when the same value is erroneously evaluated first as
an unsigned integer and then as a signed one (or vice versa). In fact, the same value
at the bit level can mean different things depending on whether it is of a signed or
unsigned type. For example, take the value 0xFFFFFFFF. If you consider this value
to be unsigned, it actually represents the number 232 \u2013 1 (4,294,967,295), whereas
if you consider it to be signed, it represents the number \u20131.
The typical scenario for a sign conversion issue is a signed integer variable that
is evaluated against some maximum legal value and then is used as a parameter
of a function that expects an unsigned value. The following code is an example
of this, taken from a vulnerable path in the FreeBSD kernel7 up to the 6.0 release:
int fw_ioctl (struct cdev *dev, u_long cmd, caddr_t data, int flag,
fw_proc *td)
{
[\u2026]
int s, i, len, err = 0; [1]
Integer Issues 31
[\u2026]
struct fw_crom_buf *crom_buf = (struct fw_crom_buf *)data; [2]
[\u2026]
if (fwdev == NULL) {
[\u2026]
len = CROMSIZE;
[\u2026]
} else {
[\u2026]
if (fwdev->rommax < CSRROMOFF)
len = 0;
else
len = fwdev->rommax - CSRROMOFF + 4;
}
if (crom_buf->len < len) [3]
len = crom_buf->len;
else
crom_buf->len = len;
err = copyout(ptr, crom_buf->ptr, len); [4]
Both len [1] and crom_buf->len are of the signed integer type, and we can
control the value of crom_buf->len since it is taken directly from the para-
meter passed through the ioctl call [2]. Regardless of what specific value
len is initialized to, either 0 or some small positive value, the condition
check at [3] can be satisfied by setting crom_buf->len to a negative value.
At [4] copyout() is called with len as one of its parameters. The copyout()
prototype is as follows:
int copyout(const void * __restrict kaddr, void * __restrict
udaddr, size_t len) __nonnull(1) __nonnull(2);
As you can see, the third parameter is of type size_t, which is a typedef (a
\u201csynonymous of\u201d in C) to an unsigned integer; this means the negative value will
be interpreted as a large positive value. Since crom_buf->ptr is a destination in
user land, this issue translates to an arbitrary read of kernel memory.
With the release in 2009 of Mac OS X Snow Leopard, all the operating sys-
tems we will cover in this book now support a 64-bit kernel on x86 64-bit-capable
machines. This is a direct indication of wider adoption of the x86 64-bit architec-
ture (introduced by AMD in 2003), in both the server and user/consumer markets.
We will discuss the x86-64 architecture in more detail in Chapter 3.
Of course, change is never easy, especially