Perplexed

https://play.picoctf.org/practice/challenge/458?category=3&page=1

Challenge Overview

We were provided with a binary named perplexed. Our task was to reverse engineer it, understand its password validation logic, and extract the correct input that would pass the check.


Step 1: Initial Reconnaissance

We begin by inspecting the binary with the file command:

└─$ file perplexed 
perplexed: ELF 64-bit LSB executable, x86–64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86–64.so.2, 
BuildID[sha1]=..., for GNU/Linux 3.2.0, not stripped

The output confirms:

  • It's a 64-bit ELF binary

  • It is dynamically linked

  • It's not stripped (we may see symbol info)

We give it execute permissions and then run it

└─$ ./perplexed
Enter the password: password
Wrong :(

Step 2: Static Analysis using Binary Ninja

Opening the binary in Binary Ninja, we find two main functions:

  1. main() – handles input/output

  2. check() – performs validation of the input

Decompiled Code

int64_t check(char* arg1)
{
    if (strlen(arg1) != 0x1b) // Must be exactly 27 characters
        return 1;

    int64_t var_58;
    __builtin_memcpy(&var_58, 
        "\xe1\xa7\x1e\xf8\x75\x23\x7b\x61\xb9\x9d\xfc\x5a\x5b\xdf\x69\xd2\xfe\x1b\xed\xf4\xed\x67\xf4", 
        0x17); // 23-byte hardcoded binary pattern

    int var_1c_1 = 0; // Byte index in user input
    int var_20_1 = 0; // Bit index in current byte

    for (int i = 0; i <= 0x16; i++) {       // Loop through 23 bytes
        for (int j = 0; j <= 7; j++) {      // Loop through 8 bits in each byte
            if (!var_20_1)
                var_20_1 += 1;

            int rax_17 = (arg1[var_1c_1] & (1 << (7 - var_20_1))) > 0;

            if (rax_17 != ((*(char*)(&var_58 + i) & (1 << (7 - j))) > 0))
                return 1;

            var_20_1 += 1;

            if (var_20_1 == 8) {
                var_20_1 = 0;
                var_1c_1 += 1;
            }

            if (var_1c_1 == strlen(arg1))
                return 0;
        }
    }

    return 0;
}

Step 3:Understanding the check Function Logic

  • It validates the bitstream of the 27-byte password (0x1b = 27).

  • A hardcoded 23-byte array is used as a bitmask reference.

  • The user input is treated bit by bit (not byte-by-byte).

  • It compares 184 bits from the user input (23×8) against this hardcoded bitstream.

Example:

Let’s say the first character of input is 'p'.

  • 'p' → ASCII 112 → Binary: 01110000

  • The function compares certain bits of 'p' to the first bits of the hardcoded array.

Step 4: Map bits → reverse the check

For each bit in the stored 23-byte sequence:

  1. Decide which bit position in the result buffer it maps to.

  2. Remember:

    • You skip bit 0 (MSB)

    • You fill bits 1 through 7 of each byte

    • After reaching 7 bits, move to next byte

  3. So the filling order for result[0] will be bits 6 → 0 (because you're filling from MSB down, but skipping 7th bit)

Repeat this for 184 bits.

stored_bit = [ 0xe1,0xa7,0x1e,0xf8,0x75,0x23,0x7b
              ,0x61,0xb9,0x9d,0xfc,0x5a,0x5b,0xdf
              ,0x69,0xd2,0xfe,0x1b,0xed,0xf4,0xed
              ,0x67,0xf4]
              
result = [0]*28 # Result Buffer


# rev eng logic
var_20_1 = 0
index = 0

for  i in range(0,23):
    for j in range(0,8):
        if var_20_1 ==0:
           var_20_1=1
        temp = ( stored_bit[i] & 1 << ( 7 - j )) > 0;
        if temp:  #change bit when it is one 
            result[index] |= (1 << (7-var_20_1));
        var_20_1+=1
        if var_20_1==8:
            var_20_1=0
            index+=1
        if index==27:
            print(completed)
# print he rev eng flag
print("".join([chr(x) for x in result]))

After running the script we got out flag

Last updated