VulnBank
Writeup for VulnBank (Pwn) - NahamCon Winter CTF (2025) π
Description
Bank security is serious business, and we get that.
We're so dedicated to security that we put "Vuln" right there in our name. It's a bold statement of our commitment to transparency, customer trust, and the occasional memory corruption incident.
Solution
I wasn't planning to make this challenge, but I heard there was a lack of pwn challenges and every CTF needs a buffer overflow πΈ Like Snorex CCTV, I wanted it to be reasonably easy due to the time constraints (dev/test time and competition duration) and the expected audience (web-leaning). If you did struggle with this one, why not check out my FREE Intro to Binary Exploitation video course!
Basic File Checks
Checking the binary protections, we'll see:
Stripped (harder to reverse)
Full RELRO (can't overwrite GOT)
No canary (stack overflow possible)
PIE enabled (might need leak)
file vulnbank; checksec --file vulnbank
vulnbank: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=97f05908fa4be2a289717d0e8860851af4556db1, for GNU/Linux 3.2.0, stripped
[*] '/home/crystal/Desktop/nahamcon-2025/Binary/vuln_bank/solution/vulnbank'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabledIf we run the binary, it asks for a pin number.
We get three attempts before the connection closes.
Static Analysis
Opening the binary in Ghidra, we'll find all the function names etc are stripped.
It's best to go through and rename them according to their behaviour. I'll also rename some variables along the way (for speed during the event, I recommend using an LLM for this).
We could search for "flag" to find the locations of the two flags and trace our way backwards.
We'll see that the the first flag is printed in the authenticate function, when we enter to correct PIN number.
This is where our first vulnerability comes in.
Format String Leak
Our input (the PIN) is passed to printf without a format specifier, allowing us to hijack it to leak values from the stack (or even perform write operations).
We can use this to leak:
PIE (well, calculate it)
The PIN number
We need to do it in that order, because the PIN isn't loaded onto the stack until we make our first attempt πΌ
Let's start by calculating PIE.
Leaking PIE
We want to find a reliable address, i.e. one that stays at the same offset each run, and always has static offset from the base of the binary.
While the process is still running, attach GDB.
Now just check the addresses, bearing in mind many indexes can be rules out, e.g.
Raw values (6-22 are ASCII)
0x7f range are outside the range (good for libc targets though)
So, we try index 25. In GDB, get the piebase and then subtract it from the leaked values.
Alright, so our leak is 0x24c9 bytes away from the base of the binary. If we subtract that offset, we can calculate the offset to any function we like. In fact, we can update the binary address in our pwntools script and reference it too. That's more useful if it wasn't stripped as we could access functions directly. Anyway, we just need to repeat this a few times to ensure it's stable, then note it down for later.
Leaking the PIN
We can repeat this process, but we are looking for a decimal so use the %d format specifier. We also need to enter an invalid entry to begin with, as the PIN has not yet been loaded onto the stack.
We are looking to a six digit value, so index 1 and 2 both fit the description. We could setup a breakpoint and check the comparison, or just test it out.
Authentication
Putting that all together, we should be able to login. Let's test it, we don't even need a debugger.
We've got the first flag, let's see where the second is (Ghidra string search again).
It's in the maintenance function, the problem? That function is never called.
How can we redirect the flow of execution?
Stack Buffer Overflow
We already saw the menu options after authenticating:
However, there is another function which doesn't show! We can see this option in the menu, I called it debug_panel and we can trigger it with the option "0".
Here we can enter some input.
Taking a look at that function, we'll see it has a 72-byte buffer, but reads in 128 bytes π±
If we overflow that variable, we can overwrite other things on the stack, like variable and the saved return address. Since there's no stack canaries, that would allow us to jump to any address of our choice with ease, e.g. the maintenance_mode function (let's call it "win" - we're going to "return2win").
Calculating the Offset
We can do this manually, with pwntools, with GDB etc. I normally use the cyclic pattern from pwntools (accessible with pwndbg), e.g.
Attach to the process again in pwndbg, then submit the payload in maintenance mode. We'll see the program crash:
The string in the RSP is not a valid memory address, hence it crashed before reaching the RIP. We can now look up the offset of that string in the cyclic pattern.
It's 88, so that's how many bytes we need to send before we will have control of the RIP. At that point, we'll write the address of the maintenance_mode function. For this, we need to use the address leak as we only know the offset of the function (0x1575). Let's chain it all together into a nice PoC πΌ
PoC
Flag 1: flag{firs7_y0u_s734l_7h3_pin} Flag 2: flag{7h3n_y0u_s734l_7h3_b4nk}
Last updated