Skip to content
wargamewriteuppwnexploit-me

exploit.me: Vulnerable Service Exploitation

4 min read

Overview

exploit.me is a deliberately vulnerable binary covering 13 classic vulnerability classes across x86, x64, ARM, and AArch64 targets. I worked through it as a structured way to practice binary exploitation before moving on to ROP on non-x86 architectures.

Vulnerability index:

Level 1:  Integer overflow
Level 2:  Stack overflow
Level 3:  Array overflow
Level 4:  Off by one
Level 5:  Stack cookie
Level 6:  Format string
Level 7:  Heap overflow
Level 8:  Structure redirection / Type confusion
Level 9:  Zero pointers
Level 10: Command injection
Level 11: Path Traversal
Level 12: Basic ROP
Level 13: Use-after-free

Each level is unlocked by the previous level's password. The binary runs as follows:

./exploit64 <level_password> [args...]

Level 1: Integer Overflow

The first password is hardcoded in the usage message: hello.

int int_overflow(char* value)
{
    unsigned short number;
    int i = atoi(value);
    unsigned int h = i;
 
    if (h <= 0) {
        printf("Value less or equal 0 is not allowed.\n");
        exit(0);
    }
    number = i;
 
    if (number != 0) {
        printf("Value %d defined.\n", number);
        exit(0);
    }
 
    if (i < 0 || number == 0)  // Two ways of overflow possible ... int AND short :)
    {
        printf("Level 2 Password: \"%s\"\n", passwds[1]);
    }
    return 0;
}

This function takes a string, converts it with atoi, and assigns the result to both an unsigned int h and an unsigned short number. The constraints are:

  • h > 0 — excludes negatives and zero
  • number != 0 — must fail this check to avoid an early exit
  • i < 0 || number == 0 — must be satisfied to print the password

i and h are 32-bit, while number is 16-bit. Assigning a 32-bit value to a 16-bit variable truncates the upper two bytes. If the lower 16 bits are all zero, number becomes 0 while h remains positive.

The target value in decimal is 0x7FFF0000: 2147418112.

./exploit64 hello 2147418112
Level 2 Password: "help"

The upper 16 bits 0x7FFF satisfy h > 0, and the lower 16 bits 0x0000 make number == 0, so the password is printed.

Level 2: Stack Overflow

Password: help. The binary now requires a username and password argument.

./exploit64 help
usage: ./exploit64 help <username> <password>

Source:

void level3password()
{
    printf("Level 3 Password: \"%s\"\n", passwds[2]);
    return;
}
 
int stack_overflow(char* username, char* password)
{
    char buffer[8] = {"\0"};
    char admin_user[] = "admin";
    char admin_pass[] = "funny";
 
    int i;
    strcpy(buffer, username);
    buffer[strlen(admin_user)] = '\0';
    if (strcmp(buffer, admin_user)) {
        printf("Login failed\n");
        return 0;
    }
 
    strcpy(buffer, password);
    buffer[strlen(admin_pass)] = '\0';
    if (strcmp(buffer, admin_pass)) {
        printf("Login failed\n");
        return 0;
    }
    printf("Login succeeded, but still you failed :P\n");
    return 1;
}

The stack layout places buffer[8] adjacent to admin_user and admin_pass on the stack. strcpy copies the username into the 8-byte buffer without bounds checking, so a long enough username can overflow into admin_user and admin_pass.

Attack strategy:

  1. Overflow buffer with a username crafted precisely to write "admin" into the adjacent admin_user storage, passing the first strcmp.
  2. Overflow again via the password argument to write "funny" into admin_pass.

Since the buffer sits right before the credential variables on the stack, the exploit only needs the exact byte offsets. The end goal is to overwrite the return address to redirect execution to level3password().

Stack visualization:

[buffer  8B][admin_user 6B][admin_pass 6B][padding][SFP][RET]

Overflowing buffer with a username longer than 8 bytes overwrites the bytes strcmp reads as admin_user. Writing exactly "admin\0" at the correct offset passes the first check. The same technique applies to the password.

Key takeaways

strcpy is never safe. It ignores the destination buffer size and copies until it finds a null byte. Use strncpy or strlcpy instead.

Adjacent stack variables are not isolated. The C standard doesn't define the ordering of local variables on the stack. In practice, the compiler lays them out in declaration order (barring optimization), so overflowing one variable can silently corrupt the next.

Integer truncation is exploitable. Assigning a wide integer to a narrower type is defined behavior in C (modular reduction), but an attacker can craft an input so the truncated result satisfies a condition the original value would not.

Type confusion between signed and unsigned types is subtle. The split here between int i and unsigned short number is a textbook example: the sign check on i and the range check on number interact in a non-obvious way to create an exploitable window.