Friday, July 8, 2016

This Old Vulnerability #1: Plan 9 devenv Integer Overflow

It's Been a While

Around 2005, the infamous Matasano security team launched their blog, Chargen. Out of all the blogs and e-zines I've read over the years, their This Old Vulnerability (TOV) posts were my absolute favorite. Thomas Ptacek, Dave Goldsmith, Jeremy Rauch, Window Snyder, and their team (including the always brilliant Dino Dai Zovi) posted fun and short blogs describing classic and interesting programming flaws. They revisited everything from the old AIX FTP bug cache, the classic Sendmail injection attack, and, my personal favorite, OpenBSD and FreeBSD's failed attempt at securely integrating Plan 9's lightweight process model into their kernel via rfork.

Though the Matasano team has since joined El conglomerado de seguridad, their TOV posts will forever be, for me, one of those rare beacons of light in an otherwise dismal array of bland Full Disclosure XSS-or-similar advisories. While I probably should have asked permission to usurp their name, it was so good that I almost consider TOV infosec community property (sorry, pals). But, whatever, tqbf can always rant at me on Twitter about it later, right? ;-)

In this incarnation of This Old Vulnerability, we'll have guest posts, late night chats, and the smooth sounds of Pwnie Award qualifying summer jams, all discussing vulnerabilities that, at one point in time, were a lot of fun to poke at. We may even learn something along the way!

So, without further ado, prepare to be triggered by technological horrors from the past...

Plan 9 Kernel devenv.c

Around 2000, I decided that I wanted to focus my interests in computing on kernels and embedded systems. Though I spent a lot of my free time reading source code for various operating systems, Plan 9 was my stand out favorite. It was small, clean code, written by some of the best minds in computing. The C compilers were small enough to be read from start to finish, and the entire kernel was designed to be ported to various platforms without a single preprocessor macro. I loved Plan 9 so much, I used it as my primary OS for three years between 2002 and 2005 while studying kernel theory.

More importantly, Plan 9 introduced some extremely important concepts to computing. The proc file system (procfs) we're all familiar with on Linux? It originated on Plan 9. The rfork system call and the concept of lightweight processes (a large improvement over threads) came from Plan 9's resource sharing model. The entire concept of namespaces? Another idea that originated with Plan 9.

While there are many more innovations that Ken Thompson, Rob Pike, and the entire Bell Labs team are well known for, I could write an entire blog post (if not a book) solely on the virtues of this little known operating system and its tiny white rabbit mascot, Glenda. But, we're not here to learn about all the things they did correctly, are we? We're here to learn about the mistakes they made. Which, were few.

Relevant Abstractions

In order to understand why the devenv vulnerability mattered, it is important to understand the security model behind Plan 9. The highlights are:
  • There is no administrator or superuser account
  • There is no sudo, su, or setuid bit
  • Authentication and authorization is performed in kernel
  • Everything is a File
  • Standard system call based separation of kernel/user privilege
One of the core concepts behind Plan 9 was security. Their goal was to resolve the security problems inherent to the UNIX model by rearchitecting the OS security model. Where UNIX's login was an add-on component to the computing model, Plan 9's model ensures that every action can be authenticated by the kernel, allowing for much more elegant security checks.

Because Plan 9's security model is integrated into the kernel, and there is no way to elevate privileges from one user to another via the normal security constructs we're used to in Linux/etc, an attack must take on one of two forms:
  • We must know the cryptographic key(s) of our target user
  • We must attack the kernel to subvert its security model

Attacking Plan 9

Since each user authenticates to Plan 9 using a key that is stored in a protected object, and since public key cryptography can be used for authentication, it isn't feasible to guess an auth key like one would guess a login password. To attack the protected object where keys are stored, an adversary would need to subvert the kernel's security, anyway. So, it makes more sense to simply start out attacking the kernel.

Also, a successful kernel attack must have a clear objective. In Plan 9, since there is no concept of a superuser, the closest abstraction is the host owner. The host owner is important because their identity owns all critical objects related to security on the target host, such as the secstore and factotum that stores and cryptographically verifies user's security keys.

The best model for an attack is to manipulate the kernel's idea of which user is the actual host owner, so that your own user identity gains temporary access to these critical resources, in order to extract all of their juicy cryptographic goodness.

This is also because, like any self-respecting kernel, dynamic objects allocated in kernel memory that represent process credentials can be stored anywhere in kernel heap. So, knowing how to overwrite your kernel process credential structure requires reading through memory and extracting the kernel address where the credential is stored. This requires extra work that we don't want to bother doing if we don't have to. Instead, it is more effort-effective to overwrite static objects that relate to who a host owner is.

Thus, an elegant attack on Plan 9 is:
  • Identify static objects in the kernel that control the concept of host ownership
  • Manipulate the identity of host ownership such that the attacker's identity becomes the host owner's identity
  • Extract all relevant security tokens
  • Reset all manipulated objects

The Vulnerability

The security flaw I found was a vulnerability that could be exploited to gain precise kernel memory corruption through the manipulation of a single integer overflow. The exploit, published on the Daily Dave mailing list in early 2007, abused the vulnerability to perform the attack outlined above.

If you'd like to follow along, you can view the code for devenv.c here on the Plan 9 website. Plan 9 abstracts everything to the concept of a file, including environment variables. The devenv driver supports a virtual file system that simply allows the creation of environment variables that can be used (or passed) from process to process, but in a file format rather than the environment variable abstraction we know from UNIX. What's important to note about devenv is that every file in the env file system is simply a character array dynamically allocated in kernel heap. So, when a user performs the operation:

% echo Bar > /env/Foo
% cat /env/Foo        
Bar                   
%                     

...the user knows that the "file" named Foo is actually just a dynamically allocated character buffer defined in the Evalue struct in 9/port/portdat.h

struct Evalue              
{                          
char *name;               
char *value;              
int len;                  
Evalue *link;             
Qid qid;                  
};                       

When a file in the env file system is read, devenv.c finds the file's dynamic buffer and reads from the specified offset within value. Like any virtual file system, the support functions are abstracted into a file system structure, which is referenced whenever a file operation is executed.

The actual vulnerability occurs in the envwrite function, which, as one would expect, allows a user to write new contents to a file in the virtual env file system. When the write operation is called the vulnerable version of envwrite would add the requested write count to the requested file offset. The result of this operation was checked to ensure it did not exceed a ceiling of Maxenvsize.

envwrite(Chan *c, void *a, long n, vlong off)
{                                             
        char *s;                             
        int vend;                            
        Egrp *eg;                            
        Evalue *e;                           
        ulong offset = off;                  
                                             
        if(n <= 0)                           
                return 0;                    
                                             
        vend = offset+n;                     
        if(vend > Maxenvsize)                
                error(Etoobig);             
       ...                                 
       memmove(e->value+offset, a, n);     

The above code has an obvious flaw, however. Integer overflow can occur during the addition operation, and can result in a vend value that is either small enough to not exceed Maxenvsize or can be negative. Either way, the check can be bypassed by providing a large enough offset.
At this point, the savvy reader will note that since the address of e->value cannot be known ahead of time, and since offset or n must be a fairly large value, there is little chance in accurately guessing offsets to even static structures in kernel memory without first being able to expose kernel memory addresses.

Ahh, savvy reader. That is correct. However, there is one very useful part of files and file systems that is supported by devenv: file truncation. When an existing file is reopened, the devenv code will support the OTRUNC flag, allowing for truncation of the existing file's contents. In this case, the envopen function will do something magical and wonderful: it will deallocate the existing Evalue buffer and set the pointer to NULL.

static Chan*                                          
envopen(Chan *c, int omode)                           
{                                                     
Egrp *eg;                                            
Evalue *e;                                           
int trunc;                                           
                                                     
eg = envgrp(c);                                      
if(c->qid.type & QTDIR) {                            
 if(omode != OREAD)                                  
  error(Eperm);                                      
}                                                    
else {                                               
 trunc = omode & OTRUNC;                             
 if(omode != OREAD && !envwriteable(c))              
  error(Eperm);                                      
 if(trunc)                                           
  wlock(eg);                                         
 else                                                
  rlock(eg);                                         
 e = envlookup(eg, nil, c->qid.path);                
 if(e == 0) {                                        
  if(trunc)                                          
   wunlock(eg);                                      
  else                                               
   runlock(eg);                                      
  error(Enonexist);                                  
 }                                                   
 if(trunc && e->value) {                             
  e->qid.vers++;                                     
  free(e->value);                                    
  e->value = 0;                                      
  e->len = 0;                                        
 }                                                  

And this functionality completes our attack. With the e->value offset truncated to NULL, we can write to any offset in memory starting at a predictable address: zero! What's even better is that the envwrite function will check to see if the write request exceeds the current size of the buffer in Evalue. However, because of the integer overflow, the expression will never evaluate to true, allowing the write operation to be performed at an offset from NULL.


The Resolution


The easy resolution to this issue is simply parsing the offset and n argument passed to envwrite, ensuring that integer overflow is not possible. However, this isn't as simple as it sounds. Even if an integer overflow can be detected in the raw arguments, the check would also need to be performed against the base address of e->value. Because, remember, the address stored in e->value may have a high address, depending on where the kernel heap is stored in memory, depending on the MMU configuration, and depending on the underlying architecture.

Linus Torvalds made some excellent comments about this problem when we were resolving the LZO and LZ4 vulnerabilities in 2014, which I agree with. It is exceptionally difficult to properly program checks of this nature into kernel functions because of the way memory is organized across multiple architectures. Performing this check in a portable manner requires a significant amount of work, and is almost unreasonable.

Thus, it is imperative to point out that a holistic solution to this type of problem can be quite impractical. Rather, creating a policy and procedure set that enforces limits on the size values of the parameters, and ensures the distance between any kernel heap and the next segment of memory, must be defined to ensure that object boundaries are not crossed. This can be done without inspecting the kernel allocator itself, rather than engaging extra locks and computational expense to extract metadata from a universal an critical object. But, the resultant mitigation is quite clear: policies and code must be defined and enhanced to ensure parameters adhere to a specification within the context of the underlying architecture.

The Exploit

The exploit, written by me in 2006, abuses this vulnerability by overwriting the function iseve() in kernel memory. The exploit patches this function to return True by default. The iseve function in the Plan 9 kernel determines whether the user identity bound to a calling process is the same identity as the host owner. If these two identities match, iseve returns True. If not? False. By forcing iseve to return True, we trick the kernel into granting us access to any resource owned by the host owner.

As depicted in the attack model above, this allows the exploit to manipulate the kernel into believing the calling process is owned by the host owner, then extracts critical resources from the kernel.

The exploit can be found in full here.

It's notable that the CVE score for this vulnerability was, somehow, a 7.2. What does that even mean, guys? How is the CVE a usable metric?

Until next time!

Don A. Bailey
Founder / CEO
Lab Mouse Security
@DonAndrewBailey

No comments:

Post a Comment