> For the complete documentation index, see [llms.txt](https://simon-6.gitbook.io/simoncyber/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://simon-6.gitbook.io/simoncyber/ctf-writeups/picoctf-2025/reverse-engineering/perplexed-medium.md).

# perplexed (Medium)

### Introduction

<figure><img src="/files/lWsiZ3qHvg88IHrMMaGf" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/mRFTaJo87ukoRBZyFv7i" alt=""><figcaption></figcaption></figure>

There's a variety of tools I could use to disassemble binary files, but I'll try using objdump. It seems like the more simpler option of the other options. I'll be using a writeup to help me, since it's also my first time using a disassembler.&#x20;

### objdump&#x20;

> The 'objdump' command in Linux is a utility that allows you to display information about object files. It is commonly used for debugging and reverse engineering purposes, providing insights into the structure and content of compiled files.
>
> It is used for the following listed purposes:
>
> * To retrieve archive header
> * To get the offset of the file
> * To get the bfdname
> * To get the demangle
> * To debug the file
> * To disassemble the file
> * To retrieve the file headers

#### **Syntax:**

```
objdump <option(s)> <file(s)>
```

There's a lot of parameters to go alongside objdump. I'll note the most common ones.

{% hint style="info" %}
The **file header** is the metadata at the start of an executable that describes the binary's structure. For ELF files (Linux executables), this includes:

**Key info in the ELF header:**

* Magic bytes (`7f 45 4c 46` for ELF)
* Architecture (32-bit vs 64-bit)
* Endianness (little/big endian)
* Entry point address (where execution starts)
* Program header and section header locations
  {% endhint %}

```bash
#### Credit to GeekforGeeks for supplying this infromation ####

objdump -f khushi
##Retrieve File Headers of an Object File#

objdump -p khushi
##Print Object-Specific File Header Content

objdump -h khushiKhushi
##Print Section Headers of the File 

objdump -x khushi
##Display All Headers of the Object File

objdump -d khushi
##Disassemble Executable Sections

objdump -D khushi
##Disassemble All Sections of the File

objdump -s khushi
##Print the Complete Content of All Sections
```

{% embed url="<https://www.man7.org/linux/man-pages/man1/objdump.1.html>" %}

***

As you can guess, since we need to disassemble this code, we need to use the -d parameter.

```
objdump -d perplexed > perplexed.txt 
```

And since this is my first time really analyzing assembly in a challenge, expect me to reference some writeups. The writeups told us to check the check() function.

```
0000000000401156 <check>:
  401156:       55                      push   %rbp
  401157:       48 89 e5                mov    %rsp,%rbp
  40115a:       53                      push   %rbx
  40115b:       48 83 ec 58             sub    $0x58,%rsp
  40115f:       48 89 7d a8             mov    %rdi,-0x58(%rbp)
  401163:       48 8b 45 a8             mov    -0x58(%rbp),%rax
  401167:       48 89 c7                mov    %rax,%rdi
  40116a:       e8 d1 fe ff ff          call   401040 <strlen@plt>
  40116f:       48 83 f8 1b             cmp    $0x1b,%rax
  401173:       74 0a                   je     40117f <check+0x29>
  401175:       b8 01 00 00 00          mov    $0x1,%eax
  40117a:       e9 20 01 00 00          jmp    40129f <check+0x149>
  40117f:       48 b8 e1 a7 1e f8 75    movabs $0x617b2375f81ea7e1,%rax
  401186:       23 7b 61 
  401189:       48 ba b9 9d fc 5a 5b    movabs $0xd269df5b5afc9db9,%rdx
  401190:       df 69 d2 
  401193:       48 89 45 b0             mov    %rax,-0x50(%rbp)
  401197:       48 89 55 b8             mov    %rdx,-0x48(%rbp)
  40119b:       48 b8 d2 fe 1b ed f4    movabs $0xf467edf4ed1bfed2,%rax
  4011a2:       ed 67 f4 
  4011a5:       48 89 45 bf             mov    %rax,-0x41(%rbp)
  4011a9:       c7 45 ec 00 00 00 00    movl   $0x0,-0x14(%rbp)
  4011b0:       c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)
  4011b7:       c7 45 dc 00 00 00 00    movl   $0x0,-0x24(%rbp)
  4011be:       c7 45 e4 00 00 00 00    movl   $0x0,-0x1c(%rbp)
  4011c5:       e9 c4 00 00 00          jmp    40128e <check+0x138>
  4011ca:       c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%rbp)
  4011d1:       e9 aa 00 00 00          jmp    401280 <check+0x12a>
  4011d6:       83 7d e8 00             cmpl   $0x0,-0x18(%rbp)
  4011da:       75 04                   jne    4011e0 <check+0x8a>
  4011dc:       83 45 e8 01             addl   $0x1,-0x18(%rbp)
  4011e0:       b8 07 00 00 00          mov    $0x7,%eax
  4011e5:       2b 45 e0                sub    -0x20(%rbp),%eax
  4011e8:       ba 01 00 00 00          mov    $0x1,%edx
  4011ed:       89 c1                   mov    %eax,%ecx
  4011ef:       d3 e2                   shl    %cl,%edx
  4011f1:       89 d0                   mov    %edx,%eax
  4011f3:       89 45 d8                mov    %eax,-0x28(%rbp)
  4011f6:       b8 07 00 00 00          mov    $0x7,%eax
  4011fb:       2b 45 e8                sub    -0x18(%rbp),%eax
  4011fe:       ba 01 00 00 00          mov    $0x1,%edx
  401203:       89 c1                   mov    %eax,%ecx
  401205:       d3 e2                   shl    %cl,%edx
  401207:       89 d0                   mov    %edx,%eax
  401209:       89 45 d4                mov    %eax,-0x2c(%rbp)
  40120c:       8b 45 e4                mov    -0x1c(%rbp),%eax
  40120f:       48 98                   cltq
  401211:       0f b6 44 05 b0          movzbl -0x50(%rbp,%rax,1),%eax
  401216:       0f be c0                movsbl %al,%eax
  401219:       23 45 d8                and    -0x28(%rbp),%eax
  40121c:       85 c0                   test   %eax,%eax
  40121e:       0f 9f c1                setg   %cl
  401221:       8b 45 ec                mov    -0x14(%rbp),%eax
  401224:       48 63 d0                movslq %eax,%rdx
  401227:       48 8b 45 a8             mov    -0x58(%rbp),%rax
  40122b:       48 01 d0                add    %rdx,%rax
  40122e:       0f b6 00                movzbl (%rax),%eax
  401231:       0f be c0                movsbl %al,%eax
  401234:       23 45 d4                and    -0x2c(%rbp),%eax
  401237:       85 c0                   test   %eax,%eax
  401239:       0f 9f c0                setg   %al
  40123c:       31 c8                   xor    %ecx,%eax
  40123e:       84 c0                   test   %al,%al
  401240:       74 07                   je     401249 <check+0xf3>
  401242:       b8 01 00 00 00          mov    $0x1,%eax
  401247:       eb 56                   jmp    40129f <check+0x149>
  401249:       83 45 e8 01             addl   $0x1,-0x18(%rbp)
  40124d:       83 7d e8 08             cmpl   $0x8,-0x18(%rbp)
  401251:       75 0b                   jne    40125e <check+0x108>
  401253:       c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)
  40125a:       83 45 ec 01             addl   $0x1,-0x14(%rbp)
  40125e:       8b 45 ec                mov    -0x14(%rbp),%eax
  401261:       48 63 d8                movslq %eax,%rbx
  401264:       48 8b 45 a8             mov    -0x58(%rbp),%rax
  401268:       48 89 c7                mov    %rax,%rdi
  40126b:       e8 d0 fd ff ff          call   401040 <strlen@plt>
  401270:       48 39 c3                cmp    %rax,%rbx
  401273:       75 07                   jne    40127c <check+0x126>
  401275:       b8 00 00 00 00          mov    $0x0,%eax
  40127a:       eb 23                   jmp    40129f <check+0x149>
  40127c:       83 45 e0 01             addl   $0x1,-0x20(%rbp)
  401280:       83 7d e0 07             cmpl   $0x7,-0x20(%rbp)
  401284:       0f 8e 4c ff ff ff       jle    4011d6 <check+0x80>
  40128a:       83 45 e4 01             addl   $0x1,-0x1c(%rbp)
  40128e:       8b 45 e4                mov    -0x1c(%rbp),%eax
  401291:       83 f8 16                cmp    $0x16,%eax
  401294:       0f 86 30 ff ff ff       jbe    4011ca <check+0x74>
  40129a:       b8 00 00 00 00          mov    $0x0,%eax
  40129f:       48 8b 5d f8             mov    -0x8(%rbp),%rbx
  4012a3:       c9                      leave
  4012a4:       c3                      ret
```

Let's analysis what the first line does just to get a feel:

```
401156:       55                      push   %rbp
```

401156 is the address of the instruction.  `0x55` is the x86-64 opcode that means "push %rbp", essentially the hexadecimal byte that is used to run the instruction. We are then adding the register, %rbp on top of the stack, which is called pushing. Since %rdp is 64 bits, we're putting 8 bytes on the stack.&#x20;

It's a little hard to really analyze this code in assembly, so I'll be using Ghidra to convert it.&#x20;

***

### Using Ghidra

<figure><img src="/files/78zSG3JI2TQ0cK7uzikt" alt=""><figcaption></figcaption></figure>

Once I loaded the file and clicked on the check function, I could finally see the de complied code.&#x20;

```c
undefined8 check(char *param_1)

{
  size_t sVar1;
  undefined8 uVar2;
  size_t sVar3;
  char local_58 [36];
  uint local_34;
  uint local_30;
  undefined4 local_2c;
  int local_28;
  uint local_24;
  int local_20;
  int local_1c;
  
  sVar1 = strlen(param_1);
  if (sVar1 == 0x1b) {
    local_58[0] = -0x1f;
    local_58[1] = -0x59;
    local_58[2] = '\x1e';
    local_58[3] = -8;
    local_58[4] = 'u';
    local_58[5] = '#';
    local_58[6] = '{';
    local_58[7] = 'a';
    local_58[8] = -0x47;
    local_58[9] = -99;
    local_58[10] = -4;
    local_58[0xb] = 'Z';
    local_58[0xc] = '[';
    local_58[0xd] = -0x21;
    local_58[0xe] = 'i';
    local_58[0xf] = 0xd2;
    local_58[0x10] = -2;
    local_58[0x11] = '\x1b';
    local_58[0x12] = -0x13;
    local_58[0x13] = -0xc;
    local_58[0x14] = -0x13;
    local_58[0x15] = 'g';
    local_58[0x16] = -0xc;
    local_1c = 0;
    local_20 = 0;
    local_2c = 0;
    for (local_24 = 0; local_24 < 23; local_24 = local_24 + 1) {
      for (local_28 = 0; local_28 < 8; local_28 = local_28 + 1) {
        if (local_20 == 0) {
          local_20 = 1;
        }
        local_30 = 1 << (7U - (char)local_28 & 0x1f);
        local_34 = 1 << (7U - (char)local_20 & 0x1f);
        if (0 < (int)((int)param_1[local_1c] & local_34) !=
            0 < (int)((int)local_58[(int)local_24] & local_30)) {
          return 1;
        }
        local_20 = local_20 + 1;
        if (local_20 == 8) {
          local_20 = 0;
          local_1c = local_1c + 1;
        }
        sVar3 = (size_t)local_1c;
        sVar1 = strlen(param_1);
        if (sVar3 == sVar1) {
          return 0;
        }
      }
    }
    uVar2 = 0;
  }
  else {
    uVar2 = 1;
  }
  return uVar2;
}

```

One thing that should be noted is that if sVar1 = 27 (0x1b), it returns false (since uVar2 becomes 0), and returns true if it isn't. Additionally, it assigns a secret array of only 23 characters in comparison of the conditional checking 27 characters.&#x20;

The for loops begins by checking the secret array by each byte, and then by each bit. Since there's 23 bytes, and 8 bits, making a total of 184 bits.&#x20;

```
if (local_20 == 0) {
    local_20 = 1;
```

This checks if the bit is equal to 0. This makes sure each character contributes 7 bits instead of 8 bits.

```
local_30 = 1 << (7U - (char)local_28 & 0x1f);
local_34 = 1 << (7U - (char)local_20 & 0x1f);
if (0 < (int)((int)param_1[local_1c] & local_34) !=
    0 < (int)((int)local_58[(int)local_24] & local_30)) {
  return 1;
```

This one is a complex one that I couldn't really figure out. However, this explanation does a good job explaining.&#x20;

***

#### 3. Bit Comparison Logic

The function uses nested loops to compare bits:

* **Outer Loop**: Iterates over the 23 reference bytes (controlled by a counter).
* **Inner Loop**: Iterates over the 8 bits of each byte (from MSB to LSB).
* For each reference bit (184 total), it:
  * Computes a position in the input string using two variables (`v1` for bit position, `v2` for byte index).
  * Extracts the corresponding bit from the input.
  * Compares it to the reference bit.
* If any bit mismatches, it returns 1. If all 184 bits match, it returns 0.

***

Basically, our input, param\_1, is being compared bit by bit with the secret array that is referenced in the code. If one bit is wrong, it returns false.&#x20;

Regarding the masks:

<figure><img src="/files/dlxBcvI7hOZGNO62Hdx9" alt=""><figcaption></figcaption></figure>

This creates a "mask" to isolate one specific bit. For example:

* `1 << 7` = `10000000` (checks the leftmost bit)
* `1 << 6` = `01000000` (checks the 2nd bit)
* `1 << 0` = `00000001` (checks the rightmost bit)

```
local_30 = 1 << (7 - bit)
```

***

Next is to write the script. I'm going to write the script in python with a help of a write up.

{% embed url="<https://picoctfsolutions.com/picoctf-2025-perplexed>" %}

The first part of the code that's unique is the "<<" operator. In python, it's the same as C.&#x20;

<figure><img src="/files/Y7nD3MFTtTRZGpPzgeoT" alt=""><figcaption></figcaption></figure>

What the << operator does is essentially moves left the amount with the given input.&#x20;

```
bit = 0  →  local_30 = 1 << 7  →  10000000  (128)
bit = 1  →  local_30 = 1 << 6  →  01000000  (64)
bit = 2  →  local_30 = 1 << 5  →  00100000  (32)
bit = 3  →  local_30 = 1 << 4  →  00010000  (16)
bit = 7  →  local_30 = 1 << 0  →  00000001  (1)
```

The "1" is the number getting moved. The 7,6,5, is the number that tells how much to shift left from right side. The opposite would apply for >>. Now that we know that, let's just analyze the code.&#x20;

<figure><img src="/files/3SM0j2ETlY5PkzCFj99R" alt=""><figcaption><p>Take note of this, we'll need it before analyzing it</p></figcaption></figure>

<figure><img src="/files/bYlaaGKIAXs9hRU7eWsu" alt=""><figcaption></figcaption></figure>

***

```python
#### Creates a list of secret values, all of these numbers can be converted to decimal.
#### ord('u") converts the letter to its ASCII number
local_58 = [-0x1f, -0x59, 0x1e, -8, ord('u'), ord('#'), ord('{'), ord('a'), -0x47, -99, -4, ord('Z'), ord('['), -0x21, ord('i'), 0xd2, -2, 0x1b, -0x13, -0xc, -0x13, ord('g'), -0xc]

flag = []
### tracks the bit position 0-7.
local_20 = 0
### accumulates the bits, which is then used to output the characters
local_2c = 0
for value in local_58:
    ### Examines each bit per value. (0-7, including the 0!)
    for bit in range(8):
        ### Skips the first bit posistion 
        if local_20 == 0:
            local_20 = 1
            
        #### Declare local varaibles, local_30 and local_34
        #### This does NOT compare bits directly. local_30 is used to read bits, and local_34 is used to write bits.
        #### It tests the leftest bit (0 out of 7), and then has a index to write for the next char
        local_30 = 1 << (7 - bit)  #### bit = 0
        local_34 = 1 << (7 - local_20) #### local_20 = 1 
        
        #### Remeber this isn't C or Java, in Python AND is and. The "&" is a Bitwise operator!
        #### It's not comparing booleans, or integers, it's comparing the results of the & operator
        #### For example, Let's compare "-0x1f" and 128
        
# value (-31)    = 11100001
# local_30 (128) = 10000000
# value & local_30 = 10000000  (result is 128, which is "True")

        #### If value & local_30 equals 0: the condition is False (don't execute)
        #### If value & local_30 equals any non-zero number: the condition is True (execute)
        
        if value & local_30:
        
        #### Once the condition is true, it does a |= operator (a OR operator)
        #### The default OR operator is just |, but the |= also means store this result
        #### Same thing as local_2c = local_2c | local_34
        #### local_2c is 0. local_34 is the next index which would be 1 from the start.
        #### local_2c is flipped on (1) or not flipped 
        
# local_2c     = 00000000
# local_34     = 01000000
# local_2c |=    01000000  (Now local_2c = 64)     

# local_2c     = 01000000  (from previous step)
# local_34     = 00100000
              ----------
# local_2c |=    01100000  (Now local_2c = 96)           
            local_2c |= local_34
            
        #### Increase the index by 1
        local_20 += 1
    `    
        #If all bits is finished (one byte)
        if local_20 == 8:
        #We convert local_2c that is filled with bits, into actual letters!
        # Add to our flag
            flag.append(chr(local_2c))
        
        # Reset Counters for next character 
            local_20 = 0
            local_2c = 0
print(''.join(flag))

```

Claude also gives us another good explanation:

````python
if local_20 == 8:
    flag.append(chr(local_2c))
    local_20 = 0
    local_2c = 0  # ← RESET BACK TO 0!
```

Every time we've built a complete byte (8 bits), we:
1. Save the character we built
2. **Reset `local_2c` back to 0** (`00000000`)
3. Start building the NEXT character from scratch

## Timeline Example:
```
Building character #1:
local_2c = 00000000  (start)
local_2c = 01000000  (after 1st bit set)
local_2c = 01100000  (after 2nd bit set)
local_2c = 01110000  (after 3rd bit set)
... (continue until 8 bits)
local_2c = 01110101  (complete! This is 'u')

Save 'u' to flag
local_2c = 00000000  (RESET!)

Building character #2:
local_2c = 00000000  (start fresh)
local_2c = 01000000  (after 1st bit set)
... (and so on)
````

The answer ends up being picoCTF{0n3\_bi7\_4t\_a\_7im3}. Definitely one of the toughest challenges I technically did.&#x20;
