Hidden Password

Using symbolic execution to solve an interesting reverse engineering challenge

I recently was introduced to crackmes.one by a coworker. This is a writeup of pjenik's hidden_password challenge. As you will see, it is a great problem to tackle with angr.

The challenge provides us with a hidden_password binary. To start off I performed some basic checks.

[michael@pwn ~/Pwn/CrackMes]$ file hidden_password 
hidden_password: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8064a21b129bd27bbcc843f635aae01b7b154a2a, for GNU/Linux 3.2.0, stripped
[michael@pwn ~/Pwn/CrackMes]$ checksec --file=hidden_password 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX disabled   PIE enabled     No RPATH   No RUNPATH   No S

So we have a 64-bit ELF executable with an executable stack. This will come into play later. Next I loaded the binary into Ghidra. In Ghidra we can see some interesting data being loaded on the stack, and some sort of checksum being calculated.

Next, the binary stores our "checksum" and uses it to seed a number generator to XOR against that stack data from the beginning. After that it calculates another checksum before executing the decoded shellcode on the stack. Ouch!

My first instinct was to do some math, but then I decided to try out the angr library. To avoid complications, I used the pre-built docker image. Getting up and running is as simple as running the following two commands!

$ docker pull angr/angr
$ docker run -it -v $(pwd):/pwn --rm angr/angr

After a while of reading the (very helpful) angr docs and testing things in ipython, it was apparent that angr was not going to have an easy time understanding the call into the shellcode. But it would still be great at crunching out the checksum values! After a few hours I came up with the following.

#!/usr/bin/env python3


ANGR Solver Script for hidden_password crackme.

Michael Hoefler 2022


import logging
import claripy
import string
import angr

l = logging.getLogger('angr')
# get some more logging output

TARGET = "hidden_password"

# the main angr project
p = angr.Project(TARGET,

# 14-char password
# fixed this after sucessfully decoding and disassembling
# the shellcode
pw = claripy.BVS("password", 8 * 14)

# entry state with password in argv
s = p.factory.entry_state(args=[TARGET, pw])

# ascii printable password
 for byte in pw.chop(8):
     s.solver.add(byte < 0x7f)
     s.solver.add(byte >= 0x20)

# simulation manager with our entry state
# using veritesting to trim down the number of branches
sm = p.factory.simulation_manager(s, veritesting=True)

sm.explore(find=0x1511, # right before the shellcode execution
           avoid=[0x1451, 0x14fe, 0x155b, 0x1569]) # invalid password

# print our results
if sm.found:
    l.info(f'Found {len(sm.found)} Potential Solutions...')
    for sol in sm.found:
        l.info(sol.solver.eval(pw, cast_to=bytes))
    l.info('No Solutions Found...')

The script is pretty simple but there is a lot going on under the hood. Initially I had specified a 30 character password and had veritesting toggled off. Without the veritesting, the script runs indefinitely.

This is confusing at first, but it is caused by a nearly infinite number of branches being created by the initial loop over the strlen of the input. Since I specified 30 bytes for the password, the length could range from [0-30], and those bytes could range [0-255]

All of these cases lead to different outcomes, but many of the outcomes are very very similar. Veritesting allows angr to merge branches which are in very similar states. This speeds up the script tremendously.

$ ipython -i crackme.py
IPython 8.2.0 -- An enhanced Interactive Python. Type '?' for help.                                                                                                           
INFO    | 2022-08-04 06:31:10,075 | angr.project | Loading binary hidden_password                                                                                             
INFO    | 2022-08-04 06:31:10,089 | angr.project | Using builtin SimProcedure for unresolved __libc_start_main from libc.so.0                                                 
INFO    | 2022-08-04 06:31:10,089 | angr.project | Using builtin SimProcedure for unresolved puts from libc.so.0   
[... snip ...]
INFO    | 2022-08-04 06:31:26,843 | angr | Found 1 Potential Solutions...
INFO    | 2022-08-04 06:31:26,843 | angr | b" ,'nnx~!zlr2}1"

If you needed a space heater, you've got one now! So clearly " ,'nnx~!zlr2}1" isn't the solution to the crackme, but it does pass the checksums. So this means that we can load the binary into GDB and read the decoded shellcode!

gef$  b *0x555555555542               # call rdx
gef$  r " ,'nnx~!zlr2}1"
gef$  si
gef$  x/50i $rip

This gives us the following assembly. Note that the first parameter RDI is our password and the second parameter RSI is the length of the password. In order to win, we need to return with RAX = 1.

=> 0x7fffffffe450:      push   rbp
   0x7fffffffe451:      mov    rbp,rsp
   0x7fffffffe454:      mov    QWORD PTR [rbp-0x38],rdi
   0x7fffffffe458:      mov    QWORD PTR [rbp-0x40],rsi
   0x7fffffffe45c:      movabs rax,0xfbe0bce158ca53e2
   0x7fffffffe466:      mov    QWORD PTR [rbp-0x12],rax
   0x7fffffffe46a:      mov    DWORD PTR [rbp-0xa],0x95d8d4a7
   0x7fffffffe471:      mov    WORD PTR [rbp-0x6],0xb283
   0x7fffffffe477:      movabs rax,0x9497e38e34a6368a
   0x7fffffffe481:      mov    QWORD PTR [rbp-0x20],rax
   0x7fffffffe485:      mov    DWORD PTR [rbp-0x18],0xcabcb8d5
   0x7fffffffe48c:      mov    WORD PTR [rbp-0x14],0x80b7
   0x7fffffffe492:      mov    QWORD PTR [rbp-0x2e],0x0
   0x7fffffffe49a:      mov    DWORD PTR [rbp-0x26],0x0
   0x7fffffffe4a1:      mov    WORD PTR [rbp-0x22],0x0
   0x7fffffffe4a7:      cmp    QWORD PTR [rbp-0x40],0xe
   0x7fffffffe4ac:      je     0x7fffffffe4b5
   0x7fffffffe4ae:      mov    eax,0x0
   0x7fffffffe4b3:      jmp    0x7fffffffe513
   0x7fffffffe4b5:      mov    DWORD PTR [rbp-0x4],0x0
   0x7fffffffe4bc:      jmp    0x7fffffffe506
   0x7fffffffe4be:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe4c1:      movsxd rdx,eax
   0x7fffffffe4c4:      mov    rax,QWORD PTR [rbp-0x38]
   0x7fffffffe4c8:      add    rax,rdx
   0x7fffffffe4cb:      movzx  edx,BYTE PTR [rax]
   0x7fffffffe4ce:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe4d1:      cdqe   
   0x7fffffffe4d3:      movzx  eax,BYTE PTR [rbp+rax*1-0x12]
   0x7fffffffe4d8:      xor    edx,eax
   0x7fffffffe4da:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe4dd:      cdqe   
   0x7fffffffe4df:      mov    BYTE PTR [rbp+rax*1-0x2e],dl
   0x7fffffffe4e3:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe4e6:      cdqe   
   0x7fffffffe4e8:      movzx  edx,BYTE PTR [rbp+rax*1-0x2e]
   0x7fffffffe4ed:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe4f0:      cdqe   
   0x7fffffffe4f2:      movzx  eax,BYTE PTR [rbp+rax*1-0x20]
   0x7fffffffe4f7:      cmp    dl,al
   0x7fffffffe4f9:      je     0x7fffffffe502
   0x7fffffffe4fb:      mov    eax,0x0
   0x7fffffffe500:      jmp    0x7fffffffe513
   0x7fffffffe502:      add    DWORD PTR [rbp-0x4],0x1
   0x7fffffffe506:      mov    eax,DWORD PTR [rbp-0x4]
   0x7fffffffe509:      cmp    eax,0xd
   0x7fffffffe50c:      jbe    0x7fffffffe4be
   0x7fffffffe50e:      mov    eax,0x1
   0x7fffffffe513:      pop    rbp
   0x7fffffffe514:      ret 

Reading through this assembly it becomes clear that it is just a simple XOR with the values loaded onto the stack from 0x7fffffffe45c-0x7fffffffe48c. Our password is XORed against one of the size 14 keys and compared to the other one. To reverse this process we can just XOR the two keys together (and fix the endianness).

This yields the password hello_world_42 which is successful against the hidden_password crackme. Thanks to pjenik for the awesome cracking challenge!

[michael@pwn ~/Pwn/CrackMes]$ ./hidden_password hello_world_42
Good password!