Pico Bank

Published: April 2, 2026

Description

A banking APK hides credentials in its source code and encodes transaction amounts as ASCII values. Decompile, find the credentials and token, then craft a server request to retrieve the flag.

Download the APK file.

Install jadx: `sudo apt install jadx`

Solution

  1. Step 1Decompile with jadx for readable Java
    jadx converts Android DEX bytecode to near-readable Java source. This is easier to analyze than raw smali.
    jadx -d pico_bank_src/ picobank.apk
    Learn more

    jadx (Java Decompiler for Android) converts Android's DEX bytecode directly into Java source code, bypassing the smali intermediate representation. The output is not identical to the original source (variable names are often generic, and some constructs decompile differently), but it is close enough to understand the application's logic at a high level.

    jadx includes both a command-line tool (jadx) and a GUI (jadx-gui) with a class browser, search functionality, and cross-reference navigation. The GUI is particularly useful for larger apps where you need to trace method calls across multiple classes. For this challenge, the command-line tool dumps all classes to a directory for easy searching with grep.

    The decompiled Java source reveals class names, method signatures, string constants, API endpoints, and hardcoded values exactly as they appeared in the original code. This makes jadx the standard first step in Android security research and penetration testing - before running an app on a device, reviewing its decompiled source reveals the intended behavior, API structure, and any obvious security flaws.

  2. Step 2Find hardcoded credentials
    Search the decompiled source for hardcoded usernames, passwords, API keys, or tokens. Check LoginActivity and NetworkActivity classes.
    grep -r 'password\|secret\|token\|api_key' pico_bank_src/ --include='*.java'
    Learn more

    Hardcoded credentials in mobile apps are a widespread vulnerability. Developers embed credentials directly in source code for convenience (testing, demo accounts, API integration) and forget to remove them before shipping. Because anyone can decompile an APK, these credentials are effectively public.

    Common locations to check in Android apps include: string constants in Activity classes (especially LoginActivity, NetworkActivity, ApiClient), BuildConfig fields (populated from Gradle properties), res/values/strings.xml, raw assets, and native libraries (searchable with strings libname.so). grep with -r (recursive) and -i (case-insensitive) covers the decompiled source directory quickly.

    The proper fix is to never ship credentials in app code. API keys should be fetched from the server after user authentication, stored in secure enclaves (Android Keystore), or replaced with short-lived tokens generated server-side. The OWASP Mobile Top 10 lists hardcoded credentials as M8: Security Misconfiguration.

  3. Step 3Decode transaction amounts as ASCII
    Transaction amounts stored as integer arrays represent ASCII character codes. Convert each integer to its character to reveal a token or secret.
    python3 -c "amounts=[112,105,99,111,...]; print(''.join(chr(x) for x in amounts))"
    Learn more

    Storing strings as arrays of ASCII integer values is a simple obfuscation technique - it avoids having the string appear directly in a string constant (which tools like grep and strings would find immediately) by instead storing the numeric character codes. ASCII assigns integers 0-127 to characters: 97='a', 65='A', 48='0', and so on. Applying chr() to each integer recovers the original character.

    This technique appears in obfuscated JavaScript, Android malware, and CTF challenges. It is easily defeated because the integers are still present in the code and their conversion to characters is visible in the decompiled source. More sophisticated obfuscation uses encryption (XOR with a key, AES) to protect string constants, but even those can be defeated by setting breakpoints at the decryption routine.

    Python's chr(x) converts an integer to its Unicode character. For pure ASCII (0-127), this is equivalent to the ASCII table. ''.join(chr(x) for x in amounts) maps each integer through chr() and concatenates the results into a string - a clean one-liner to decode any integer array encoding.

  4. Step 4POST a crafted request to the API
    Use the discovered credentials and decoded token to make an authenticated API call that returns the flag.
    curl -X POST https://<host>/api/flag -H 'Content-Type: application/json' -d '{"username":"<user>","password":"<pass>","token":"<token>"}'
    Learn more

    REST APIs typically accept JSON payloads in POST request bodies. The Content-Type: application/json header tells the server to parse the body as JSON rather than form data. curl -d sends the body; combined with the Content-Type header, this replicates exactly what the Android app's HTTP client sends when making the same API call.

    After obtaining credentials from static analysis (decompiled source), the next step is to call the backend API directly - without needing to run the app on a device. This "headless" API interaction skips the UI layer entirely and interacts with the server as the app would. Tools like Postman, Insomnia, or HTTPie provide more ergonomic interfaces than raw curl for API testing.

    In real penetration tests, hardcoded credentials in a mobile app typically allow direct access to backend APIs that may expose other users' data, administrative functions, or internal infrastructure. This is why mobile app security reviews are considered part of the backend API security review - the app's code reveals the API structure and any authentication shortcuts.

Flag

picoCTF{...}

Hardcoded API credentials in mobile apps are a top mobile security finding - jadx makes Android bytecode nearly as readable as the original Java source.

Want more picoMini by CMU-Africa writeups?

Useful tools for Reverse Engineering

More Reverse Engineering