Tags: linux sandbox seccomp elf
Rating:
# execve-sandbox
This task was part of the 'PWN' category at the 2018 Google CTF Quals round (during 23-24 June 2018).
It was solved by [or523](https://github.com/or523) and [RonXD](https://github.com/RonXD), in [The Maccabees](https://ctftime.org/team/60231) team, and we are pretty sure it is not the intended solution!
## The challenge
*(If you're already familiar with the challenge, just skip down to our solution).*
The hint available in the website was:
```
What a kewl sandbox! Seccomp makes it impossible to execute ./flag
```
And, of-course, we got a domain and port for ```nc```. Inside the attached zip, there was a single file named ```execve-sandbox.c```.
This file is the source for the executable running on the remote server, and its main function is to receive x86-64 ELF executable files and run them in a "sandbox". This sandbox is constructed in a way that should (in theory, at least) prevent from the executable ELF from executing any file with the ```execve``` system call.
When the binary starts running, it runs ```system("/bin/ls -l .");```, and we can see the files in the directory. There, we can see the ```flag``` file in our directory - which is executable-only for everyone. From the hint and this directory listing, it is obvious that our goal is to run the ```./flag``` file in order to get the flag itself.
Let's talk briefly about the interesting parts of the sandbox: the program receives a file which is executed under a seccomp-bpf filter, which is set up like as follows:
```c
static int install_syscall_filter(unsigned long mmap_min_addr)
{
int allowed_syscall[] = {
SCMP_SYS(rt_sigreturn),
SCMP_SYS(rt_sigaction),
SCMP_SYS(rt_sigprocmask),
SCMP_SYS(sigreturn),
SCMP_SYS(exit_group),
SCMP_SYS(exit),
SCMP_SYS(brk),
SCMP_SYS(access),
SCMP_SYS(fstat),
SCMP_SYS(write),
SCMP_SYS(close),
SCMP_SYS(mprotect),
SCMP_SYS(arch_prctl),
SCMP_SYS(munmap),
SCMP_SYS(fstat),
SCMP_SYS(readlink),
SCMP_SYS(uname),
};
scmp_filter_ctx ctx;
unsigned int i;
int ret;
ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
warn("seccomp_init");
return -1;
}
for (i = 0; i < sizeof(allowed_syscall) / sizeof(int); i++) {
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, allowed_syscall[i], 0) != 0) {
warn("seccomp_rule_add");
ret = -1;
goto out;
}
}
/* prevent mmap to map mmap_min_addr */
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 1,
SCMP_A0(SCMP_CMP_GE, mmap_min_addr + PAGE_SIZE)) != 0) {
warn("seccomp_rule_add");
ret = -1;
goto out;
}
/* first execve argument (filename) must be mapped at mmap_min_addr */
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(execve), 1,
SCMP_A0(SCMP_CMP_EQ, mmap_min_addr)) != 0) {
warn("seccomp_rule_add");
ret = -1;
goto out;
}
puts("[*] seccomp-bpf filters installed");
ret = seccomp_load(ctx);
if (ret != 0)
warn("seccomp_load");
out:
seccomp_release(ctx);
return ret;
}
```
## The mmap_min_addr kernel parameter
The Linux kernel holds a list of runtime parameters that may be changed during runtime by user programs. The parameters can be accessed using the ``/proc/sys`` directory, where they appear as files, and reading/writing to those virtual files will cause the parameter to be read into user-space/written from user-space. The ``vm.mmap_min_addr`` donates the minimal virtual address that may be allocated - a lower virtual address will **never** be allocated with ``mmap`` (or any other system call), even with a non-NULL hint. In ``install_syscall_filter``, the ``mmap_min_addr`` argument is the ``vm.mmap_min_addr`` parameter read from the kernel. So, if we wish to allocated the ``mmap_min_addr`` virtual address it should be the very first page in our allocation, making it much harder to allocate.
## The sandbox
The set-up filter allows only the short list of system calls (listed with SCMP_SYS) to be executed without any restriction; in addition, the ```mmap``` system call is allowed only if its first parameter (the address hint) is not the ```mmap_min_addr```; and the ```execve``` system call is allowed only if its first parameter (the string that contains the path to the executable) is **exactly** ```mmap_min_addr```. This is the 'execve-sandbox' the task is named after - we can execute only paths that are written in a specific address, which we can't allocate (at least easily) using ```mmap```.
Another sandbox validation is validation of the ELF - the function ```elf_check``` validates that the ELF is a valid x86-64 ELF, and that there isn't any section or program header with a non-zero virtual address in the "prohibited range" (the single page after ```mmap_min_addr```) - so easy ELF editing is also not an option.
## Our solution
Our solution doesn't require any ```mmap``` feng-shui that will try to map the ```mmap_min_addr``` page - in fact, we do not map this page at all.
Our solution is based on an edited program header in the ELF we're sending called ```PT_INTERP```. This program header contains a path, which is the path of the interpreter (executable file) our ELF will run under. The main usage of this field is in dynamically-linked files, that use the dynamic linker of the machine as an interpreter, in order to link dynamically with libraries. For example, on a standard Ubuntu machine, you will get:
```
$ readelf -Wl `which cat`
...
Program Headers:
Type Offset VirtAddr PhysAddr
...
INTERP 0x000238 0x0000000000000238 0x0000000000000238 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
```
Which tells the kernel that when running this file, load ```ld.so``` ELF file as the interpreter for our ELF, and branch to it when starting the process. If you are interested in how the interpreter knows the information about the original mapped ELF when the kernel runs it, I recommend reading [about auxiliary vectors](http://articles.manugarg.com/aboutelfauxiliaryvectors.html) and have a look at ```man getauxval```.
So, the solution is easy! Because the kernel maps and runs the executable written in the ```PT_INTERP``` program header of our ELF, we can just change it to be ```"./flag"``` in the following way:
```c
const char interp_section[] __attribute__((section(".interp"))) = "./flag";
```
Note that this is the entire source-code - there are no other lines besides this one. We compile it in the following way (to make sure it is small enough to reach the one page size limitation - with these flags, it is only 512 bytes ELF!):
```bash
gcc -s -fno-ident -Wl,--build-id=none -Wl,-e,0 -static -nostdlib solution.c -o solution
```
The resulting binary will contain only one section, ``.interp``, no pieces of code, and no entrypoint - so this binary will do nothing more than running ``./flag`` as its interpreter. Usually, executable binaries request an interpreter, and an interpreter may not request its own interpreter, so this solution should not work. However, we notice the ``open`` system call is not allowed - so dynamic linking will simply not work since the dynamic linker has to ``open`` the libraries it wants to link. Since we assume the flag binary should be able to run under the sandbox, we assume it must be statically-linked, meaning it will not request an interpreter and may be run as our interpreter.
## Success!
When we send the binary to the server, we get the flag!
```CTF{Time_to_read_that_underrated_Large_Memory_Management_Vulnerabilities_paper}```
Which makes us think we found an unintended solution, because no memory management tricks were required!
See you next CTF!
\~ RonXD && or523
> Written with [StackEdit](https://stackedit.io/).