sandbox-caas

Setup

The server receives a 0x800 shellcode from the user, and runs it in a forked process.

The process configurations is as follows:

  • All namespaces are NEW namespaces
  • chroot into ./tmp/.challenge
  • rlimit of one second of CPU time with no core files
  • The process sent a kill signal after 2 seconds
  • uid and gid are mapped to the original (unknown) uid and gid of the server
  • Process only contains our shellcode (R/O) and a small stack (R/W)
  • The process is being ptrace()d
  • SECCOMP that only allows:
    • read
    • write
    • close
    • munmap
    • sched_yield
    • dup
    • dup2
    • nanosleep
    • connect
    • recvmsg
    • bind
    • exit
    • exit_group
    • clone (CLONE_THREAD | CLONE_VM | CLONE_SIGHAND, ...)
    • socket (AF_INET, AF_DGRAM, 0)
    • mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)
  • Process has the following open file descriptors:
    • fd 0 -> The original socket that was used to send the shellcode to the server
    • fd 1 -> ditto
    • fd 2 -> ditto
    • fd 100 -> UNIX socket. The other end is connected to a “connect-server” see below

The connect-server runs outside of the sandbox, and accepts 2 kinds of messages:

  1. GetEnvData(N) – Sends back a one of the following numbers 1,3,3,7
  2. Connect(AddressOfIP, port) – connects to IP and Port, and sends the connected socket, back to the sandboxed process
    • The only allowed IP and port are 127.0.0.1:8080

Two additional servers exist:

  1. Metadata server on 127.0.0.1:8080
    • Accepts a connections
    • Sends “Not implementeded”
    • Closes the connection
  2. Flag server on 127.0.0.1:6666
    • Accepts a connection
    • Sends the contents of the flag file

Observations

The Connect() function is the interesting function and contains 2 potential bugs:

  1. There seems to be a TOCTOU bug between the VERIFY stage and the CONNECT stage.
  2. If the connect() fails, the socket is still passed to our sandboxed process.

Main idea

The namespace of a socket is determined at the time of creation of the socket. Thus, if we receive a socket from the outside that has not been connect()ed, we can use it to connect to 127.0.0.1:6666 from within the sandbox.

Looking in to the TOCTOU issue:

The processing of the Connect() message is done as follows:

  1. Obtain port and address of IP from our message
  2. ip = SafeRead(sandboxPid, AddressOfIP)
  3. Fail if ip:port != 127.0.0.1:8080
  4. ip = SafeRead(sandboxPid, AddressOfIP)
  5. fd = connect(ip:port)
  6. SendFDToSandbox(fd)

The SafeRead(sandboxPid, AddressOfIP) function does as follows:

  1. Try 3 times until success or return error:
    1. Verify the sandboxPid is in read() or recvmsg() syscall
    2. Sleep(100 microseconds) <– Note: Always happens at least once!
  2. Check that GetNumberOfThreads(sandboxPid) in sandboxed process <= 1
  3. return process_vm_readv(sandboxPid, addressOfIP)

It seems that our process must be blocked on read() or recvmsg() and no other threads may be running.

After (many) failed attempts to pass the above checks, we realized that the GetNumberOfThreads() function had a bug in it, that caused it to always return -1 threads.

The bug was that GetNumberOfThreads() was reading the /proc/sandboxPid/status file, and using ftell() to determine the size of the file.

However, ftell() returns 0 for /proc/ files, thus GetNumberOfThreads() was not finding the string “Threads: ” in the empty buffer that was read from the file, and in-turn returned -1 as the number of threads.

Solving

Shellcode did the following:

  1. sys_clone(thread_t)
  2. sys_write(fd=100, “Connect(stack address containing 127.0.0.1, 8080)”, …)
  3. socket_fd = sys_recvmsg()
  4. sys_connect(socket_fd, “127.0.0.1”, 6666)
  5. sys_read(socket_fd, &flag)
  6. sys_write(1, &flag)

thread_t:

  1. sys_nanosleep(X microseconds) X ~= 600,000 (Actually this value worked first time)
  2. modify stack address IP address to a non-routable IP, we used 127.127.255.255
  3. sys_exit()

The flag was: CTF{W3irD_qu1rKs}

A few afterthoughts

We had to select an IP that will make connect() fail immediately, otherwise, connect() takes too long to timeout. Therefore, a non-routable address was chosen.

Wondering if the GetNumberOfThreads() bug was the only way to pass this riddle. The secomp filter included the option to create a UDP/IP socket, however, there is no interface to bind to. Binding to 0.0.0.0 succeeds, but subsequent read()s fail, due to the fact that there are no interfaces.