Intigriti 1337UP CTF 2024: Secure Bank
TL;DR
- Binary gates access behind a hardcoded superadmin PIN check.
- A custom 2FA generator derives the expected code from the PIN using rotations, XOR, and constants.
- Static reversing reproduces the algorithm to compute the correct 2FA value.
- Alternatively, dynamic analysis breaks after generation to read the code from EAX.
Video Walkthrough
Challenge Description
Can you crack the bank?
Solution
Players try to login to the secure bank but don't have the correct pin.
./secure_bank
****************************************
* Welcome to SecureBank *
* Your trusted partner in security *
****************************************
========================================
= SecureBank Superadmin Login System =
========================================
Enter superadmin PIN: 1234
Access Denied! Incorrect PIN.
Checking the strings will uncover the plaintext flag (different for remote) but no pin.
strings -n 10 secure_bank
Access Granted! Welcome, Superadmin!
INTIGRITI{fake_flag}
Here is your flag: %s
Access Denied! Incorrect 2FA code.
Enter superadmin PIN:
Access Denied! Incorrect PIN.
Enter your 2FA code:
GCC: (Debian 12.2.0-14) 12.2.0
We could try a tool like ltrace to see if the pin comparison is displayed.
printf("Enter superadmin PIN: ") = 22
__isoc99_scanf(0x55d5a141b1ea, 0x7ffe6418d938, 0, 0Enter superadmin PIN: 1234
) = 1
puts("Access Denied! Incorrect PIN."Access Denied! Incorrect PIN.
) = 30
+++ exited (status 1) +++
Unfortunately, it is not. Let's check the decompiled code with ghidra.
Static Analysis
banner();
login_message();
printf("Enter superadmin PIN: ");
__isoc99_scanf(&%u,&pin);
if (pin == 1337) {
valid_code = generate_2fa_code(1337);
printf("Enter your 2FA code: ");
__isoc99_scanf(&%u,&user_code);
validate_2fa_code(user_code,valid_code);
}
else {
puts("Access Denied! Incorrect PIN.");
}
return pin != 1337;
Classic 1337, so predictable is almost unpredictable 🤔ðŸ§
./secure_bank
****************************************
* Welcome to SecureBank *
* Your trusted partner in security *
****************************************
========================================
= SecureBank Superadmin Login System =
========================================
Enter superadmin PIN: 1337
Enter your 2FA code: 1234
Access Denied! Incorrect 2FA code.
Checking the generate_2fa_code function, it looks a little complicated.
local_10 = param_1 * 0xbeef;
local_c = local_10;
for (local_14 = 0; local_14 < 10; local_14 = local_14 + 1) {
local_c = obscure_key(local_c);
local_10 = ((local_10 ^ local_c) << 5 | (local_10 ^ local_c) >> 0x1b) +
(local_c << ((char)local_14 + (char)(local_14 / 7) * -7 & 0x1fU) ^
local_c >> ((char)local_14 + (char)(local_14 / 5) * -5 & 0x1fU));
}
return local_10 & 0xffffff;
Nothing that a little variable renaming can't fix!
key = pin * 0xbeef;
code = key;
for (i = 0; i < 10; i = i + 1) {
code = obscure_key(code);
key = ((key ^ code) << 5 | (key ^ code) >> 27) +
(code << ((char)i + (char)(i / 7) * -7 & 31U) ^
code >> ((char)i + (char)(i / 5) * -5 & 31U));
}
return key & 0xffffff;
Much better! The obscure_code function is quite simple.
return ((code ^ 0xa5a5a5a5) << 3 | (code ^ 0xa5a5a5a5) >> 29) * 0x1337 ^ 0x5a5a5a5a;
There's a lot of different ways to solve the challenge from here. One easy way might be to run the binary in a debugger like gdb (I like pwndbg) and set a breakpoint around the generate_2f_code function (or validate_2fa_code).
Dynamic Analysis

Actually, we want to break right after the function (offset 0x1386).
pwndbg> breakrva 0x1386
Breakpoint 1 at 0x555555555386
pwndbg> run
****************************************
* Welcome to SecureBank *
* Your trusted partner in security *
****************************************
========================================
= SecureBank Superadmin Login System =
========================================
Enter superadmin PIN: 1337
Breakpoint 1, 0x0000555555555386 in main ()
─────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────
0x555555555381 <main+112> call generate_2fa_code <generate_2fa_code>
â–º 0x555555555386 <main+117> mov dword ptr [rbp - 4], eax [0x7fffffffda8c] <= 0x568720
0x555555555389 <main+120> lea rax, [rip + 0xe7b] RAX => 0x55555555620b ◂— 'Enter your 2FA code: '
0x555555555390 <main+127> mov rdi, rax RDI => 0x55555555620b ◂— 'Enter your 2FA code: '
0x555555555393 <main+130> mov eax, 0 EAX => 0
0x555555555398 <main+135> call printf@plt <printf@plt>
See the value being moved from the EAX register onto the stack? 0x568720 in decimal is 5670688, let's try it!
./secure_bank
****************************************
* Welcome to SecureBank *
* Your trusted partner in security *
****************************************
========================================
= SecureBank Superadmin Login System =
========================================
Enter superadmin PIN: 1337
Enter your 2FA code: 5670688
Access Granted! Welcome, Superadmin!
Here is your flag: INTIGRITI{pffft_what_2fa?!}
Solve.py
Another option is to make a solve script according to the decompiled code. I like to copy/paste from ghidra to ChatGPT and get a python script to run.
def obscure_key(key):
key ^= 0xA5A5A5A5
# Make sure it stays within 32 bits
key = (key << 3) & 0xFFFFFFFF | (key >> 29)
key *= 0x1337
key &= 0xFFFFFFFF # Keep it within 32-bit unsigned integer bounds
key ^= 0x5A5A5A5A
return key
def generate_2fa_code(pin):
key = pin * 0xBEEF
key &= 0xFFFFFFFF # Ensure it's 32-bit
code = key
for i in range(10):
key = obscure_key(key)
code ^= key
code = (code << 5) & 0xFFFFFFFF | (
code >> 27) # Rotate and ensure 32 bits
code += (key >> (i % 5)) ^ (key << (i % 7))
code &= 0xFFFFFFFF # Keep it within 32-bit unsigned integer bounds
code &= 0xFFFFFF # Ensure the 2FA code is 24 bits (6 digits)
return code
if __name__ == "__main__":
pin = 1337 # The superadmin PIN
expected_code = generate_2fa_code(pin)
print(f"Expected 2FA Code: {expected_code}")
python solve.py
Expected 2FA Code: 5670688
Flag: INTIGRITI{pfff7_wh47_2f4?!}
