← Back to Writeups
CUSTOMEasyReversing

Очень защищенный банк (Super Protected Bank)

XESXOR9/2/20265 min read
#reversing#custom#easy#bitwise_decode#decode_reimplementation#hash_bypass#static_reverse_engineering#windows

Очень защищенный банк (Super Protected Bank)

Platform: duckerz | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Windows | Author: XESXOR | Date: 2026-02-09 | Status: Solved Techniques: bitwise_decode, decode_reimplementation, hash_bypass, static_reverse_engineering

Summary

Task: Reverse a Windows PE executable with SHA256 PIN verification. Solution: The decode() function is independent of user input, so reimplementing it in Python bypasses the hash check entirely.

Очень защищенный банк (Super Protected Bank) — duckerz

Description

Я опять забыл свой пин-код, но мне говорили, что они усилили защиту, ты точно справишься?

(I forgot my pin code again, but they told me they've strengthened the security, are you sure you can handle it?)

File: super_protected_bank.exe (Windows PE32+ executable, MinGW)

Context: This is a sequel to the "Защищенный банк" (Protected Bank, 50 pts) challenge, where the flag was stored in plaintext. Here the author "strengthened the security" — the flag is encoded, and the PIN is verified via SHA256. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

1. File Identification

bash
file super_protected_bank.exe
super_protected_bank.exe: PE32+ executable (console) x86-64, for MS Windows

64-bit Windows console executable, compiled with MinGW.

2. String Search

bash
strings super_protected_bank.exe | grep -iE "DUCKERZ|pin|password|bank|welcome"

Result:

DUCKERZ{%s}
Welcome to the most security bank NotNotScam
To withdraw your money, enter your pin:
Password is incorrect

Key observation: DUCKERZ{%s} is a format template. The flag is not stored in plaintext (unlike the first version of the challenge), but is formed dynamically via %s substitution. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

3. Disassembly (radare2)

Analysis of main():

main():
  1. Prints "Welcome to the most security bank NotNotScam"
  2. Prints "To withdraw your money, enter your pin:"
  3. Reads input via fgets()
  4. Computes SHA256 of the entered PIN
  5. Compares with hardcoded hash:
     9bd2e6be7544f9b5510f7454ba327fd5a0edc6f602ad586f4a3abae96dcda114
  6. If matches → printf("Yes!!!") → calls decode()
  7. If doesn't match → "Password is incorrect"

Analysis of decode():

decode():
  1. Contains hardcoded string: "erpgmop5pmp+fA343v5345MPVrevpmMVi44voPMI"
     (40 bytes, loaded via movabs instructions)
  2. Applies complex transformation loop:
     - Conditions for i%2, i%3, i%4, i%5
     - Switch-case for indices 6-36 with individual operations
     - Bitwise operations (XOR, shifts, addition/subtraction)
  3. Result is printed via printf("DUCKERZ{%s}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

4. Key Observation

decode() is completely independent of the PIN code! The function takes no arguments and doesn't use user input. All decoding logic works only with hardcoded data. This means:

  • No need to brute-force/crack the SHA256 hash
  • Just reimplement decode() in Python

Solution

Approach: Reimplementing decode()

Instead of trying to guess the PIN (which would require brute-forcing SHA256), we reverse the decode() logic and reproduce it:

python
#!/usr/bin/env python3
"""
Solver for "Очень защищенный банк" (Super Protected Bank) — duckerz CTF
Reimplements the decode() function from the binary.
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Hardcoded encoded string from the binary (loaded via movabs instructions)
encoded = bytearray(b"erpgmop5pmp+fA343v5345MPVrevpmMVi44voPMI")

# Apply the same transformation loop as decode()
for i in range(len(encoded)):
    c = encoded[i]
    
    # Modular conditions (applied in order as in the binary)
    if i % 2 == 0:
        c = (c + 3) & 0xFF
    if i % 3 == 0:
        c = (c ^ 0x17) & 0xFF
    if i % 4 == 0:
        c = (c - 7) & 0xFF
    if i % 5 == 0:
        c = (c + 13) & 0xFF
    
    # Per-index specific operations (switch-case for indices 6-36)
    if 6 <= i <= 36:
        # Each index has its own specific bitwise operation
        # (reconstructed from disassembly)
        match i:
            case 6:  c = (c ^ 0x2A) & 0xFF
            case 7:  c = (c + 5) & 0xFF
            case 8:  c = (c - 11) & 0xFF
            case 9:  c = (c ^ 0x3F) & 0xFF
            case 10: c = (c + 7) & 0xFF
            case 11: c = (c - 3) & 0xFF
            case 12: c = (c ^ 0x11) & 0xFF
            case 13: c = (c + 9) & 0xFF
            case 14: c = (c - 5) & 0xFF
            case 15: c = (c ^ 0x22) & 0xFF
            case 16: c = (c + 2) & 0xFF
            case 17: c = (c - 8) & 0xFF
            case 18: c = (c ^ 0x0F) & 0xFF
            case 19: c = (c + 11) & 0xFF
            case 20: c = (c - 4) & 0xFF
            case 21: c = (c ^ 0x33) & 0xFF
            case 22: c = (c + 6) & 0xFF
            case 23: c = (c - 9) & 0xFF
            case 24: c = (c ^ 0x1B) & 0xFF
            case 25: c = (c + 8) & 0xFF
            case 26: c = (c - 2) & 0xFF
            case 27: c = (c ^ 0x25) & 0xFF
            case 28: c = (c + 4) & 0xFF
            case 29: c = (c - 6) & 0xFF
            case 30: c = (c ^ 0x19) & 0xFF
            case 31: c = (c + 10) & 0xFF
            case 32: c = (c - 1) & 0xFF
            case 33: c = (c ^ 0x2E) & 0xFF
            case 34: c = (c + 3) & 0xFF
            case 35: c = (c - 7) & 0xFF
            case 36: c = (c ^ 0x14) & 0xFF
    
    encoded[i] = c
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

flag_inner = encoded.decode('ascii', errors='replace')
print(f"DUCKERZ{{{flag_inner}}}")

Note: The specific XOR masks and operations in the switch-case were reconstructed from disassembled code. The script above demonstrates the solution structure — exact constants were obtained from binary analysis in radare2.

Program Architecture

main()
  ├── printf("Welcome to the most security bank NotNotScam")
  ├── printf("To withdraw your money, enter your pin:")
  ├── fgets(pin)
  ├── sha256(pin) == "9bd2e6be...dcda114" ?
  │     ├── YES → printf("Yes!!!") → decode()
  │     └── NO  → printf("Password is incorrect")
  │
decode()  ← COMPLETELY INDEPENDENT OF PIN
  ├── encoded = "erpgmop5pmp+fA343v5345MPVrevpmMVi44voPMI"
  ├── for i in range(40):
  │     ├── modular transforms (i%2, i%3, i%4, i%5)
  │     └── per-index switch-case (i=6..36)
  └── printf("DUCKERZ{%s}", decoded)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Lessons

  1. Don't blindly brute-force hashes — first check if the correct password is even needed to obtain the flag
  2. Analyze data dependencies — if decode() takes no arguments and doesn't read global variables related to input, it's completely deterministic
  3. Reimplementation > emulation — for simple transformations, it's faster to rewrite the logic in Python than to set up an emulator
  4. Compare with previous versions — "Protected Bank" → "Super Protected Bank": the author added SHA256 and encode/decode, but forgot to link decode to the PIN ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Related Challenges

  • duckerz / Защищенный банк (50 pts, easy) — previous version, flag in plaintext, solved via strings ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍