28
Apr
2014

PlaidCTF 2014 – tenement [100]

For PlaidCTF2014, Eindbazen and fail0verflow joined forces as 0xffa, the Final Fail Alliance. Don’t miss out on other write-ups at fail0verflow’s site!

The Plague has tried to make things easy for you in this service, but not too easy. He’s called The Plague, not The Nice Guy. The service should be running at 54.235.7.236:9999.

Tenement is a remote pwnable – it’s a normal x86 binary.

Upon initialization it loads a json file, using libjansson, which contains the flag and an array with memory addresses. The flow goes like this:

  1. The flag is first copied to a malloc’d buffer, prefixed with “PPPP:”
  2. a random memory address is picked from the json’s array mentioned earlier
  3. mmap() is called using this picked address as starting address
  4. the “PPPP:<flag>” buffer is copied over there, and the memory protection is set to PROT_READ
  5. finally, the malloc’d buffer and the stack is “cleaned” (memset) and the json objects “deleted”

At this point, right before reading arbitrary shellcode from the socket and executing it, the application installs a SECCOMP filter.
SECCOMP enables one to, besides various other operations, whitelist system calls, and therefore blacklist others. An example usage of SECCOMP is to limit exploitation vectors (by taking away critical system calls) when a vulnerability has been identified in an application or service. It can therefore be used to implement a sandbox (as far as system calls go.)

The SECCOMP filter installed by this Tenement is a whitelist on the following system calls (it thus blacklists every other system call):

3 __NR_read
4 __NR_write
6 __NR_close
0x21 __NR_access
0x14a __NR_dup3
0x6c __NR_fstat
0xfc __NR_exit_group

From here on we wrongly started by feeding the service shellcode to read the flag file and write it to the socket.
SECCOMP would make the application crash as the open() system call was not allowed.

After some fiddling around we got to the conclusion that the stack/registers didn’t contain any pointers we could abuse. However, as the application only free() the json objects without deleting their contents first, surely we’d be able to read back all of these heap objects’ content.
Reading up on libjansson (the JSON library being used) we find the following declaration of JSON integer objects:

typedef struct {
    json_type_t type;
    unsigned int refcount;
} json_t;
typedef struct {
    json_t object;
    json_integer_t value;
} json_int_t;

With a 64-bit integer value this makes the size of the json_int_t structure 16 bytes.

Following these thoughts we also figured that calling malloc(16) should give us back buffers from the malloc-free list, and hopefully gives us at some point one of those earlier interesting json objects. The rational was that malloc() in this case shouldn’t have to call any syscalls, hence having no issue with the SECCOMP filters.

So libjansson keeps a reference count in order to determine whether an object should be free()’d when json_decref() is called on it. Earlier we stated that the objects of the json array and the json picked address that is used as starting address for mmap() are free’d – this is partially true as the application in fact calls json_decref() on them.
For the json array this means the reference counts of all elements go from 1 to 0 and now they’re 0 the objects are free()’d. However, the json picked address that is being used as destination address has an extra json_decref(), making its reference count -1.

From here on the logical step forward is to malloc(16) objects until we find an object with reference count -1, then align the pointer to the page boundary, and to read the first couple of bytes back to our socket. (We write 128 bytes, but any number equal to or higher than the length of the flag will do – so usually 40, or even 20, should be enough.)

NB: We later found out that a nice trick was to use write() on memory page to detect whether or not the page is mapped: indeed, write() returns EFAULT when a bad address is given, but does NOT crash. This enables for an easy memory dumper, walking through the pages, using write() to dump them through the socket.

Following is our shellcode doing exactly this. It can be assembled using nasm.

[bits 32]
_start:
    ; jmp to malloc
    mov eax, 0x8048810 
    push 16
    call eax
    ; reference count
    cmp dword [eax+4], 0xffffffff
    jnz _start
    ; align the pointer
    mov ecx, [eax+8]
    and ecx, 0xfffff000
    ; write(1, ecx, 128)
    xor eax, eax
    mov al, 4
    xor ebx, ebx
    inc ebx
    xor edx, edx
    mov dl, 128
    int 0x80

This challenge took us longer than expected for 100 points, but it was fun nonetheless.

Comments are closed.