Description : x86_64 linux binary (tested on debian 9 and ubuntu 18.04, should run on any distro). Takes one command line argument and outputs “OK!” if it’s correct, “Wrong” if it’s not. Partially written in C, actual verification routine is assembly. Don’t patch the binary, of course - find the correct input.

Lets open the binary in radare2 and we’ll look at the disassembly of the main function

 110: int main (int argc, char **argv);
           ; arg int argc @ rdi
           ; arg char **argv @ rsi
           0x00000fc0      55             push rbp                    ; [14] -r-x section size 4002 named .text
           0x00000fc1      53             push rbx
           0x00000fc2      4889f5         mov rbp, rsi                ; argv
           0x00000fc5      89fb           mov ebx, edi                ; argc
           0x00000fc7      4883ec08       sub rsp, 8
           0x00000fcb      e8f6090000     call sigaltstack
           0x00000fd0      84c0           test al, al
           0x00000fd2      ba01000000     mov edx, 1
       ┌─< 0x00000fd7      744f           je 0x1028
          0x00000fd9      e887090000     call sigill_handler
          0x00000fde      84c0           test al, al
          0x00000fe0      ba02000000     mov edx, 2
      ┌──< 0x00000fe5      7441           je 0x1028
      ││   0x00000fe7      4863fb         movsxd rdi, ebx
      ││   0x00000fea      4889ee         mov rsi, rbp
      ││   0x00000fed      e89e010000     call flag_verification
      ││   0x00000ff2      4883f801       cmp rax, 1
      ││   0x00000ff6      488d3dda1000.  lea rdi, str.Wrong          ; 0x20d7 ; "Wrong"
     ┌───< 0x00000ffd      7422           je 0x1021
     │││   0x00000fff      4883f802       cmp rax, 2
     │││   0x00001003      488d3dd31000.  lea rdi, str.hell86_crackme__Please_pass_the_flag_as_a_command_line_argument. ; 0x20dd ; "[hell86 crackme] Please pass the flag as a command-line argument."
    ┌────< 0x0000100a      7415           je 0x1021
    ││││   0x0000100c      4885c0         test rax, rax
    ││││   0x0000100f      488d3dbd1000.  lea rdi, [0x000020d3]       ; "OK!"
    ││││   0x00001016      488d05021100.  lea rax, str.You_have_encountered_a_bug ; 0x211f ; "You have encountered a bug"
    ││││   0x0000101d      480f45f8       cmovne rdi, rax
    ││││   ; CODE XREFS from main @ 0xffd, 0x100a
    └└───> 0x00001021      e80affffff     call sym.imp.puts           ; int puts(const char *s)
      ││   0x00001026      31d2           xor edx, edx
      ││   ; CODE XREFS from main @ 0xfd7, 0xfe5
      └└─> 0x00001028      89d0           mov eax, edx
           0x0000102a      5a             pop rdx
           0x0000102b      5b             pop rbx
           0x0000102c      5d             pop rbp
           0x0000102d      c3             ret

The main function calls 3 functions respectively

sigaltstack function

The first function allocates 0x2000 bytes on the heap for an alternate stack for the signal handlers and the stack pointer points to the new stack which had been allocated also the flags have been set to 0 and later on it calls the sigaltstack function and if it encounters any errors it free’s the allocated memory.

This is an example for defining an alternate stack

 #include <signal.h>
           ...
           if ((sigstk.ss_sp = malloc(SIGSTKSZ)) == NULL)
               /* Error return. */
           sigstk.ss_size = SIGSTKSZ;
           sigstk.ss_flags = 0;
           if (sigaltstack(&sigstk,(stack_t *)0) < 0)
               perror("sigaltstack");

The actual function

 89: sigaltstack ();
           ; var void *var_8h @ rsp+0x8
           ; var int64_t var_18h @ rsp+0x18
           0x000019c6      53             push rbx
           0x000019c7      bf00200000     mov edi, 0x2000             ; "X\xbb\xff\xff\xff\xff\xff\xff\xb8\n" ; size_t size
           0x000019cc      4883ec20       sub rsp, 0x20
           0x000019d0      e87bf5ffff     call sym.imp.malloc         ;  void *malloc(size_t size)
           0x000019d5      31d2           xor edx, edx
           0x000019d7      4885c0         test rax, rax
       ┌─< 0x000019da      743b           je 0x1a17
          0x000019dc      488d7c2408     lea rdi, [var_8h]
          0x000019e1      4889c3         mov rbx, rax
          0x000019e4      b906000000     mov ecx, 6
          0x000019e9      31c0           xor eax, eax
          0x000019eb      31f6           xor esi, esi
          0x000019ed      f3ab           rep stosd dword [rdi], eax
          0x000019ef      488d7c2408     lea rdi, [var_8h]
          0x000019f4      48c744241800.  mov qword [var_18h], 0x2000 ; [0x2000:8]=0xffffffffffffbb58 ; "X\xbb\xff\xff\xff\xff\xff\xff\xb8\n"
          0x000019fd      48895c2408     mov qword [var_8h], rbx
          0x00001a02      e899f5ffff     call sym.imp.sigaltstack
          0x00001a07      85c0           test eax, eax
          0x00001a09      b201           mov dl, 1
      ┌──< 0x00001a0b      740a           je 0x1a17
      ││   0x00001a0d      4889df         mov rdi, rbx                ; void *ptr
      ││   0x00001a10      e86bf5ffff     call sym.imp.free           ; void free(void *ptr)
      ││   0x00001a15      31d2           xor edx, edx
      ││   ; CODE XREFS from sigaltstack @ 0x19da, 0x1a0b
      └└─> 0x00001a17      4883c420       add rsp, 0x20
           0x00001a1b      88d0           mov al, dl
           0x00001a1d      5b             pop rbx
           0x00001a1e      c3             ret

sigill handler

This function initializes the SIGILL handler it has two flags set SA_ONSTACK and SA_SIGINFO what does these flags do ????

The man page for sigaction clearly mentions that :

SA_ONSTACK If set and an alternate signal stack has been declared with sigaltstack(), the signal shall be delivered to the calling process on that stack. Otherwise, the signal shall be delivered on the current stack.

SA_SIGINFO If cleared and the signal is caught, the signal-catching function shall be entered as:

void func(int signo);

where signo is the only argument to the signal-catching function. In this case, the application shall use the sa_handler member to describe the signal-catching function and the application shall not modify the sa_sigaction member.

If SA_SIGINFO is set and the signal is caught, the signal-catching function shall be entered as:

void func(int signo, siginfo_t *info, void *context);

where two additional arguments are passed to the signal-catching function. The second argument shall point to an object of type siginfo_t explaining the reason why the signal was generated; the third argument can be cast to a pointer to an object of type ucontext_t to refer to the receiving thread’s context that was interrupted when the signal was delivered. In this case, the application shall use the sa_sigaction member to describe the signal-catching function and the application shall not modify the sa_handler member.

 97: sigill_handler ();
           ; var struct sigaction *act @ rsp+0x8
           ; var int64_t var_10h @ rsp+0x10
           ; var int64_t var_90h @ rsp+0x90
           0x00001965      53             push rbx
           0x00001966      31c0           xor eax, eax
           0x00001968      b926000000     mov ecx, 0x26               ; '&'
           0x0000196d      4881eca00000.  sub rsp, 0xa0
           0x00001974      488d7c2408     lea rdi, [act]
           0x00001979      f3ab           rep stosd dword [rdi], eax
           0x0000197b      488d05c4ffff.  lea rax, [0x00001946]
           0x00001982      488d7c2410     lea rdi, [var_10h]
           0x00001987      c78424900000.  mov dword [var_90h], 0x8000004 ; [0x8000004:4]=-1
           0x00001992      4889442408     mov qword [act], rax
           0x00001997      e8c4f5ffff     call sym.imp.sigfillset
           0x0000199c      31d2           xor edx, edx
           0x0000199e      85c0           test eax, eax
       ┌─< 0x000019a0      7519           jne 0x19bb
          0x000019a2      488d5c2408     lea rbx, [act]
          0x000019a7      31d2           xor edx, edx                ; struct sigaction *oldact
          0x000019a9      bf04000000     mov edi, 4                  ; int signum
          0x000019ae      4889de         mov rsi, rbx                ; const struct sigaction *act
          0x000019b1      e8daf5ffff     call sym.imp.sigaction      ; int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
          0x000019b6      85c0           test eax, eax
          0x000019b8      0f94c2         sete dl
          ; CODE XREF from sigill_handler @ 0x19a0
       └─> 0x000019bb      4881c4a00000.  add rsp, 0xa0
           0x000019c2      88d0           mov al, dl
           0x000019c4      5b             pop rbx
           0x000019c5      c3             ret

lets dig deep down the handler routine

The handler holds a struct uncontext and in which uc_mcontext holds the GPR’s (general purpose registers) for hell86 and as we see it uses the same registers of the x64 architecture for hell86

gef  ptype ucontext_t
type = struct ucontext_t {
    unsigned long uc_flags;
    struct ucontext_t *uc_link;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    sigset_t uc_sigmask;
    struct _libc_fpstate __fpregs_mem;
    unsigned long long __ssp[4];
}
gef  ptype mcontext_t
type = struct {
    gregset_t gregs;
    fpregset_t fpregs;
    unsigned long long __reserved1[8];
}
gef  ptype gregset_t
type = long long [23]

;DATA XREF from fcn.00001965 @ 0x197b
            0x00001946      488b82a80000.  mov rax, qword [rdx + 0xa8]  ; rax = addr of ud2
            0x0000194d      488d7228       lea rsi, [rdx + 0x28]        ; rsi = register(ucontext_t)
            0x00001951      488d7802       lea rdi, [rax + 2]           ; rdi = the next bytes after the encountered ud2
            0x00001955      4883c00e       add rax, 0xe                 ; rax = points to the enocountered ud2 + 0xe  = next ud2
            0x00001959      488982a80000.  mov qword [rdx + 0xa8], rax
            0x00001960      e97b050000     jmp 0x1ee0
 ; CODE XREF from fcn.00001192 @ +0x7ce
            0x00001ee0      0fb65708       movzx edx, byte [rdi + 8]
            0x00001ee4      488d05951120.  lea rax, [0x00203080]
            0x00001eeb      ff24d0         jmp qword [rax + rdx*8]
            0x00001eee      6690           nop

The byte at rdi+8 indexes a jump table at 0x00203080 and the jump table which consists of 50 instructions for the vm to use and they are shown below

0x00203080  0x0000000000001a1f  0x0000000000001a20   ........ .......
0x00203090  0x0000000000001a39  0x0000000000001a52   9.......R.......
0x002030a0  0x0000000000001a6c  0x0000000000001a87   l...............
0x002030b0  0x0000000000001aa2  0x0000000000001abe   ................
0x002030c0  0x0000000000001ae6  0x0000000000001ada   ................
0x002030d0  0x0000000000001afa  0x0000000000001b12   ................
0x002030e0  0x0000000000001b2b  0x0000000000001b43   +.......C.......
0x002030f0  0x0000000000001b5c  0x0000000000001b73   \.......s.......
0x00203100  0x0000000000001b8b  0x0000000000001ba3   ................
0x00203110  0x0000000000001bba  0x0000000000001bd2   ................
0x00203120  0x0000000000001be9  0x0000000000001c01   ................
0x00203130  0x0000000000001c1a  0x0000000000001c2e   ................
0x00203140  0x0000000000001c43  0x0000000000001c54   C.......T.......
0x00203150  0x0000000000001c6d  0x0000000000001c86   m...............
0x00203160  0x0000000000001c9f  0x0000000000001cb3   ................
0x00203170  0x0000000000001cd2  0x0000000000001cf1   ................
0x00203180  0x0000000000001d10  0x0000000000001d2f   ......../.......
0x00203190  0x0000000000001d4e  0x0000000000001d6d   N.......m.......
0x002031a0  0x0000000000001d87  0x0000000000001da1   ................
0x002031b0  0x0000000000001db9  0x0000000000001dcf   ................
0x002031c0  0x0000000000001de5  0x0000000000001e07   ................
0x002031d0  0x0000000000001e9c  0x0000000000001ebe   ................
0x002031e0  0x0000000000001e1e  0x0000000000001e32   ........2.......
0x002031f0  0x0000000000001e49  0x0000000000001e60   I.......`.......
0x00203200  0x0000000000001e74  0x0000000000001e88   t...............

FLag Function

This is the actual flag verification function, the first instruction is actually a ud2 instruction. so what does a ud2 instruction does? 🤔

opcodememonicdescription
0f 0bUD2raise invalid opcode exception
 2: flag_verification ();
           0x00001190      0f0b           ud2
            ; CALL XREF from main @ 0xfed
 28: fcn.00001192 (int64_t arg4);
           ; arg int64_t arg4 @ rcx
           0x00001192      0200           add al, byte [rax]
           0x00001194      0000           add byte [rax], al
           0x00001196      0000           add byte [rax], al
           0x00001198      0000           add byte [rax], al
           0x0000119a      090d00000f0b   or dword [0x0b0f11a0], ecx  ; [0xb0f11a0:4]=-1 ; arg4
           0x000011a0      0200           add al, byte [rax]
           0x000011a2      0000           add byte [rax], al
           0x000011a4      0000           add byte [rax], al
           0x000011a6      0000           add byte [rax], al
           0x000011a8      2400           and al, 0
           0x000011aa      0800           or byte [rax], al

The structure of the instructions for hell86 would look like this

struct ins {
    uint16_t ud2;
    uint64_t imm;
    uint8_t opcode;
    uint8_t dest_reg;
    uint8_t source_reg_1;
    uint8_t source_reg_2;
};

As we now know the routine of the handler its pretty easy to reverse the flag verification function so that being set we’ll need to make a disassembler and a script to reverse the flag function in order to get the flag

Disassembler

we can use objdump or gdb to dump the flag verification function from the binary

 reversing/vm/hell86 gef hell86
GEF for linux ready, type `gef' to start, `gef config' to configure
92 commands loaded for GDB 10.1 using Python engine 3.9
Reading symbols from hell86...
(No debugging symbols found in hell86)
gef  dump binary memory flag_func 0x1190 0x1946

As it was my first time creating a diassembler i got some help from a previously made disassembler.


#!/usr/bin/env python3
def disassemble(instructions):

        registers = ['r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', 'r15', 'rdi', 'rsi',
                 'rbp', 'rbx', 'rdx', 'rax', 'rcx', 'rsp', 'rip']

        asm_instr = [

        'nop', # no operation
        '{1:s} = {2:s} + {3:s}', # addition
        '{1:s} = {2:s} - {3:s}', # subtract
        '{1:s} = {2:s} * {3:s}', # multiply
        '{1:s} = {2:s} / {3:s}', # division (quotient)
        '{1:s} = {2:s} % {3:s}', # division (reminder)
        '{1:s} = {2:s} >> {3:s}', # right shift
        '{1:s} = {2:s} << {3:s}', # left shift
        '{1:s} = -{2:s}', # negation
        '{1:s} = 0x{0:x}', # mov a, b
        '{1:s} = byte [{2:s}+0x{0:x}]', # movzx
        '{1:s} = byte [{2:s}+0x{0:x}]', # movsx
        '{1:s} = word [{2:s}+0x{0:x}]', # movzx
        '{1:s} = word [{2:s}+0x{0:x}]', # movsx
        '{1:s} = dword [{2:s}+0x{0:x}]', # movzx
        '{1:s} = dword [{2:s}+0x{0:x}]', # movsx
        '{1:s} = qword [{2:s}+0x{0:x}]', # mov
        'mov byte [{2:s}+0x{0:x}], {3:s}',
        'mov word [{2:s}+0x{0:x}], {3:s}',
        'mov dword [{2:s}+0x{0:x}], {3:s}',
        'mov qword [{2:s}+0x{0:x}], {3:s}',
        'push {2:s}',
        'push 0x{0:x}',
        'pop {1:s}',
        '{1:s} = {2:s}',
        '{1:s} = {2:s} | {3:s}',
        '{1:s} = {2:s} & {3:s}',
        '{1:s} = {2:s} ^ {3:s}',
        '{1:s} = ~ {2:s}',
        'cmp.s {1:s}, {2:s}, {3:s}', # smaller
        'cmp.se {1:s}, {2:s}, {3:s}', # smaller or equal
        'cmp.g {1:s}, {2:s}, {3:s}', # greater
        'cmp.ge {1:s}, {2:s}, {3:s} ', # greater or equal
        '{1:s} == {3:s} ; {1:s} = 1', #equal
        'cmp.in {1:s}, {2:s}, {3:s}', #inequal
        'cmp.eq {1:s}, {2:s}, 0x{0:x}', # if equal to const
        '{2:s} != 0x{0:x} ; {1:s} = 1', # if inequal to const
        'cmp.z {1:s}, {2:s} ; {1:s} = {2:s} == 0 ',
        'if {2:s} = 0 jmp 0x{0:x}',
        'if {2:s} != 0 jmp 0x{0:x}',
        'call 0x{0:x}',
        'ret',
        'if {2:s} != 0 ret',
        'if {2:s} = 0 ret',
        'lea {1:s}, [{2:s} + 0x{0:x}]',
        '{1:s}, {2:s} >> 0x{0:x}', # rshift
        '{1:s}, {2:s} << 0x{0:x}', # lshift
        '{1:s}, {2:s} | 0x{0:x}',  # or
        '{1:s}, {2:s} & 0x{0:x}',  # and
        '{1:s}, {2:s} ^ 0x{0:x}'   # xor
        ]

        #print(len(assembly_mapping))
        addr = 0x1190
        for i in instructions:

            print('{:>10s}'.format('0x{:x}: '.format(addr)), end='')

            instr = int.from_bytes(i[2:10], byteorder='little')
            opcode = i[10]
            arg1 = registers[i[11]]
            arg2 = registers[i[12]]
            arg3 = registers[i[13]]


            print(asm_instr[opcode].format(instr, arg1, arg2, arg3))
            addr += 14


with open('flag_func', 'rb') as f:
   flag_func = f.read()

instructions = [flag_func[i*14:(i+1)*14] for i in range(len(flag_func)//14)]
disassemble(instructions)

Now lets take a look at the actual disassembly of the vm


0x1190: rax = 0x2
0x119e: rdi != 0x2 ; r8 = 1
0x11ac: if r8 != 0 ret
0x11ba: lea rsi, [rsi + 0x8]
0x11c8: rdi = qword [rsi+0x0]
0x11d6: rip = 0x11e4
0x11e4: push rbp
0x11f2: rbp = rsp
0x1200: lea rsp, [rsp + 0xfffffffffffffff0]
0x120e: mov qword [rbp+0xfffffffffffffff0], rdi
0x121c: call 0x17da
0x122a: rax != 0x24 ; rax = 1
0x1238: if rax != 0 jmp 0x13ce
0x1246: rdi = 0x20cd
0x1254: call 0x17da
0x1262: mov qword [rbp+0xfffffffffffffff8], rax
0x1270: rdi = qword [rbp+0xfffffffffffffff0]
0x127e: rsi = 0x20cd
0x128c: rdx = rax
0x129a: call 0x182e
0x12a8: if rax != 0 jmp 0x13ce
0x12b6: rdi = qword [rbp+0xfffffffffffffff0]
0x12c4: rsi = byte [rdi+0x23]
0x12d2: rsi != 0x7d ; rsi = 1
0x12e0: if rsi != 0 jmp 0x13ce
0x12ee: rsi = qword [rbp+0xfffffffffffffff8]
0x12fc: rdi = rdi + rsi
0x130a: rsi = -rsi
0x1318: lea rsi, [rsi + 0x23]
0x1326: push rsi
0x1334: call 0x1406
0x1342: pop rsi
0x1350: if rax = 0 jmp 0x13ce
0x135e: rdi = rax
0x136c: push rdi
0x137a: call 0x15fe
0x1388: pop rdi
0x1396: push rax
0x13a4: call 0x0
0x13b2: pop rax
0x13c0: rip = 0x13dc
0x13ce: rax = 0x1
0x13dc: rsp = rbp
0x13ea: pop rbp
0x13f8: ret
0x1406: rax = 0x0
0x1414: if rsi = 0 ret
0x1422: push rdi
0x1430: push rsi
0x143e: rdi, rsi << 0x3
0x144c: call 0x0
0x145a: pop rsi
0x1468: pop rdi
0x1476: if rax = 0 ret
0x1484: r8 = rax
0x1492: r9 = rax
0x14a0: push r9
0x14ae: push r8
0x14bc: push rdi
0x14ca: push rsi
0x14d8: rsi = byte [rdi+0x0]
0x14e6: rdi = 0x20a0
0x14f4: call 0x18c8
0x1502: pop rsi
0x1510: pop rdi
0x151e: pop r8
0x152c: pop r9
0x153a: if rax = 0 jmp 0x15c6
0x1548: r10 = 0x20a0
0x1556: rax = rax - r10
0x1564: mov qword [r9+0x0], rax
0x1572: lea r9, [r9 + 0x8]
0x1580: lea rdi, [rdi + 0x1]
0x158e: lea rsi, [rsi + 0xffffffffffffffff]
0x159c: if rsi != 0 jmp 0x14a0
0x15aa: rax = r8
0x15b8: ret
0x15c6: rdi = r8
0x15d4: call 0x0
0x15e2: rax = 0x0
0x15f0: ret
0x15fe: rax = 0x1
0x160c: if rsi = 0 ret
0x161a: r8 = qword [rdi+0x0]
0x1628: r8 != 0x16 ; r8 = 1
0x1636: if r8 != 0 ret
0x1644: push rdi
0x1652: push rsi
0x1660: call 0x1724
0x166e: pop rsi
0x167c: pop rdi
0x168a: lea rsi, [rsi + 0xffffffffffffffff]
0x1698: push rdi
0x16a6: rdx, rsi << 0x3
0x16b4: rsi = 0x1fa0
0x16c2: call 0x182e
0x16d0: pop rdi
0x16de: r8 = rax
0x16ec: rax = 0x1
0x16fa: if r8 != 0 ret
0x1708: rax = 0x0
0x1716: ret
0x1724: if rsi = 0 ret
0x1732: lea rsi, [rsi + 0xffffffffffffffff]
0x1740: if rsi = 0 ret
0x174e: r8 = qword [rdi+0x0]
0x175c: r9 = qword [rdi+0x8]
0x176a: r8 = r9 - r8
0x1778: r8 = r8 ^ rsi
0x1786: r9 = r8 * r8
0x1794: r8 = r9 * r8
0x17a2: mov qword [rdi+0x0], r8
0x17b0: lea rdi, [rdi + 0x8]
0x17be: lea rsi, [rsi + 0xffffffffffffffff]
0x17cc: rip = 0x1740
0x17da: rax = 0x0
0x17e8: r10 = byte [rdi+0x0]
0x17f6: if r10 = 0 ret
0x1804: lea rdi, [rdi + 0x1]
0x1812: lea rax, [rax + 0x1]
0x1820: rip = 0x17e8
0x182e: rax = 0x0
0x183c: if rdx = 0 ret
0x184a: r8 = byte [rdi+0x0]
0x1858: r9 = byte [rsi+0x0]
0x1866: r8 = r8 ^ r9
0x1874: rax = rax | r8
0x1882: lea rdx, [rdx + 0xffffffffffffffff]
0x1890: lea rdi, [rdi + 0x1]
0x189e: lea rsi, [rsi + 0x1]
0x18ac: if rdx != 0 jmp 0x184a
0x18ba: ret
0x18c8: rax = rdi
0x18d6: r8 = byte [rax+0x0]
0x18e4: if r8 = 0 jmp 0x192a
0x18f2: r8 == rsi ; r8 = 1
0x1900: if r8 != 0 ret
0x190e: lea rax, [rax + 0x1]
0x191c: rip = 0x18d6
0x192a: rax = 0x0
0x1938: ret

Flag verification

The actual verification goes like this

  • if argc != 2 ; returns
  • if input != len(flag) ; returns
  • if input doesn’t start with FLAG{ ; returns
  • if input doesn’t end with } ; returns
  • if flag[0] != x ; returns
  • now it verifies our flag with a global array of and checks if the indices of flag are correct
  • now it compares it with a array at 0x1fa0 and if they are equal returns 0 and prints OK! if its correct or else it prints Wrong

To verify the indices of the flag of which is between 30 characters with the global array it uses this algorithm

iterates over 30 - 1  times cause the len of the flag will be reduced as we already know the first character which is x 
r8 = qword [rdi+0x0]
r9 = qword [rdi+0x8]
r8 = r9 - r8
r8 = r8 ^ rsi 
r9 = r8 * r8
r8 = r9 * r8
mov qword [rdi+0x0], r8

The solve script using z3

#!/usr/bin/env python3
from z3 import *

characters = 'abdfgehikmanoqrstucvwlxyz-01h23p456u78j9-_.+'

hash = [
    5832, -29791, -8000, 13824, -6859, 5832, -29791, 24389, -10648, -8, 24389, -13824, -17576, 2744,
    -17576, 19683, -32768, 729, 19683, -1, 729, 1000, 125, -5832, 512, 512, -6859, 8000, -8000
]

x = [BitVec(f'x[{i}]', 16) for i in range(30)] # 30 char of input
s = Solver()
s.add(x[0] == 22) # first char = x
indices = []

for i in range(29): # len of flag - 1
    r8 = x[i]
    r9 = x[i+1]
    r8 = r9 - r8
    r8 = r8 ^ (29 - i) # rsi = 29 - i
    r9 = r8 * r8
    r8 = r9 * r8
    indices.append(r8)
    i+=1

for i in range(29):
    s.add(indices[i] == hash[i])
    s.add(And(x[i] >= 0, x[i] < 44))


flag = ''
if s.check() == sat:
    m = s.model()
    for i in range(30):
        obj = x[i]
        flag += characters[m[obj].as_long()]
    print(s.assertions())
    print(f"FLAG{{{flag}}}")

And voila when we run the script we get the flag. We’ll check it actual binary which should print OK! if its a valid flag

 vm/hell86/solve ./solve
FLAG{x86-1s-s0-fund4m3nt4lly-br0k3n}
 vm/hell86/solve ./hell86 FLAG{x86-1s-s0-fund4m3nt4lly-br0k3n}
OK!

That was an awesome crackme