Về Dirty Cow

Gần đây tôi có thấy nhiều thông tin về lỗ hổng này, được đánh giá là Serious, cho phép leo thang đặc quyền, ảnh hưởng đến nhiều distro Linux… Tôi cũng không hiểu sao một lỗ hổng sau khi được công bố thì có logo, website riêng, Twitter, Facebook Page thậm chí là có cả một cái shop bán đồ lưu niệm :| Vì tò mò nên cũng muốn tìm hiểu cơ chế hoạt động của vuln này, dù tôi không phải người chuyên research OS Kernel.

Dirty COW được gán mã lỗi CVE-2016-5195, là một cái bug Linux Kernel Race condition cho phép leo thang đặc quyền thông qua Local Exploit (nghĩa là kẻ tấn công phải vào được server nạn nhân trước với quyền Normal rồi dùng lỗi này để nâng lên quyền root). Dirty COW ảnh hưởng đến nhiều phiên bản Kernel(2.6.22–3.9) và hầu hết các distro Linux thông dụng, nhưng cách khai thác thì rất đơn giản và hiệu quả.

Người phát hiện ra lỗi này, Phil Oester, công bố sau khi một server anh này quản lý bị tấn công bằng chính Dirty COW. Có nghĩa là mã khai thác hiện có đã bị dùng để tấn công thực tế từ rất lâu rồi. Cha đẻ Linux, Linus Torvalds cũng đề cập trong bản vá lỗi rằng anh đã mường tượng lỗi này có thể xảy ra, và đã đưa ra bản vá vào năm 2005 nhưng nó đã không thực sự phát huy tác dụng.

Trong bài viết này, tôi sẽ cố gắng giải thích chi tiết cách lỗi này hoạt động. Và nếu có gì sai sót, hy vọng ai đó có thể góp ý thêm.

Chi tiết lỗi

Một cách phân tích bug hiệu quả là dựa vào mã khai thác đã có. Dưới đây là đoạn mã được cung cấp từ repository github “chính thức” của Dirty COW.

https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c

Mã khai thác này cho phép sửa nội dung một file đã được set permision Read Only, hoạt động được trên hầu hết các distro Linux trừ Red Hat Enterprise Linux 5 và 6 (lý do sẽ giải thích ở dưới). Cách dùng thì có thể thấy dễ hiểu thế này:

$ sudo -s
# echo this is not a test > foo

# chmod 0404 foo

$ ls -lah 
foo-r — — -r — 1 root root 19 Oct 20 15:23 foo

$ cat foo
this is not a test

$ ./dirtyc0w foo m00000000000000000

$ cat foo
m00000000000000000

Theo luồng xử lý của chương trình, chúng ta có thể chia nhỏ các công việc mà nó thực hiện như sau.

1. Load file và map vào Virtual Address Space

Tại dòng 87, chương trình mở file input và đọc với chế độ Read Only. Tiếp đến lấy thông tin file ghi vào biến con trỏ st và lưu lại tên file vào biến name. Mọi thứ khá rõ ràng.

87: f=open(argv[1],O_RDONLY); 
88: fstat(f,&st); 
89: name=argv[1];

Ở dòng 101, nội dung file được map vào Virtual Address Space bằng hàm mmap()

101: map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

mmap() không copy nội dung file được vào RAM, mà ánh xạ nội dung này vào vùng nhớ ảo của tiến trình đang chạy.

Hoạt động của mmap()

Hãy xem qua đặc tả hàm:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

Tham số addr trong trường hợp này là NULL, nghĩa là kernel tự quyết định địa chỉ lưu trữ, độ dài vùng nhớ length bằng độ lớn của file st.st_size, chế độ truy cập prot vẫn được để là Read Only PROT_READ vì thế vùng mapping này chỉ có thể được đọc, fdoffset tương ứng với file input và vị trí bắt đầu load file. Và tham sốquan trọng nhất ở đây, flags được gán bằng MAP_PRIVATE.

Theo như tài liệu của linux:

MAP_PRIVATE: Create a private copy-on-write mapping

Copy-on-write là một khái niệm quan trọng của hệ điều hành, tư tưởng của nó là nếu một tiến trình đọc một tài nguyên dạng copy-on-write thì sẽ vẫn như đọc tài nguyên thường, nhưng nếu thực hiện thao tác ghi/sửa thì hệ điều hành sẽ tạo một bản sao của tài nguyên đó và cập nhật các nội dung mới vào đây, bản gốc không bị thay đổi.

Hoạt động copy-on-write

Với cờ MAP_PRIVATE, bản mapping được xử lý riêng với tiến trình sở hữu nó. Các thay đổi về nội dung trên mapping không ảnh hưởng đến bản gốc và cũng không chịu sự tác động của bất kỳ tiến trình nào khác. Cụ thể hơn, nếu chúng ta sửa nội dung file input sau khi đã map vào Virtual Address Space thì file gốc không bị thay đổi.

Cụm Copy-on-write cũng chính là dạng đầy đủ của từ COW trong cái tên Dirty COW

2. Race Condition

Như đã nói ở phần đầu, Dirty COW là một lỗi Race Condition, và lỗi này được tạo ra bởi 2 dòng lệnh sau:

106: pthread_create(&pth1,NULL,madviseThread,argv[1]); 
107: pthread_create(&pth2,NULL,procselfmemThread,argv[2]);

Về cơ bản thì Race Condition xảy ra khi mà có nhiều hơn một luồng thực thi cùng đọc/ghi vào tài nguyên dùng chung dẫn đến xung đột và những tình huống không đoán được trước. Trong trường hợp của Dirty COW, các luồng thực thi này là 2 thread madviseThreadprocselfmemThread, còn tài nguyên dùng chung là bản mapping của file input. Ta sẽ đi cụ thể vào từng thread.

1. madviseThread()

Chú ý vào dòng 45, đây là lệnh quan trọng nhất của thread này. Còn vòng lặp for chỉ giúp cho Race được diễn ra.

45: c+=madvise(map,100,MADV_DONTNEED);

madvise là một system call, nó đưa ra chỉ thị cho kernel biết phải làm gì với không gian nhớ bắt đầu từ addr và có độ dài length

int madvise(void *addr, size_t length, int advice);

Ở đây, chỉ thị là MADV_DONTNEED, nó yêu cầu kernel không sử dụng không gian nhớ này nữa và có thể giải phóng tài nguyên liên kết đến file input. Lúc này vùng nhớ trên trở thành dirty, đây là từ đầu tiên trong tên Dirty COW. Với vùng nhớ dirty, lại có thêm chỉ thị MADV_DONTNEED (do thực hiện vòng lặp), dữ liệu ghi vào sẽ bị “ thrown away”, còn dữ liệu đọc ra vẫn sẽ được lấy từ file gốc.

Tóm lại, mục đích thread này là ngăn cản việc sử dụng vùng mapping.

2. procselfmemThread()

61:     int f=open("/proc/self/mem",O_RDWR); 
62:     int i,c=0; 
63:     for(i=0;i<100000000;i++) {
64: /*
65: You have to reset the file pointer to the memory position.
66: */ 
67:         lseek(f,(uintptr_t) map,SEEK_SET);      
68:         c+=write(f,str,strlen(str)); 
69: }

Chúng ta đã biết là không thể ghi vào file input được, do nó được set permission Read Only. Vì thế, người viết mã khai thác này thực hiện một trick là ghi vào vùng không gian nhớ đã được map, thay vì file gốc. Cách thực hiện là thông qua /proc/self/mem.

Trong các hệ điều hành nix thì Everything is a file và memory cũng không ngoại lệ, vùng nhớ của mỗi tiến trình có thể truy cập thông qua file /proc/<pid>/mem. Ở đây, POC process truy cập chính vùng nhớ của mình nên pid = self.

Và đoạn mã phía trên thực hiện việc ghi vào vùng mapping một cách liên tục (bằng vòng lặp) dữ liệu mà user truyền vào.

Phương pháp ghi vào mem này hoạt động tốt trên phần lớn distro Linux trừ Red Hat Enterprise Linux 5 và 6, vì mấy distro này không cho phép ghi vào /proc/self/mem. Đấy là lý do tại sao mã khai thác này không hoạt động được trên những distro đấy. Ở những trường hợp này thì có một cách khác thay thế là dùng ptrace() system call.
Đến đây thì chúng ta có thể hình dung ra được là có 1 thread liên tục free vùng mapping, còn thread còn lại thì liên tục ghi vào đấy.

Nếu mọi chuyện tốt đẹp thì sẽ như thế này:
Intention

Tuy nhiên thực tế lại xảy ra không như mong đợi (cũng là vì Race):

Fact

Chú ý một chút, chúng ta thấy là nếu 2 thread trên hoạt động độc lập hoặc có thứ tự thì nó vẫn có thể coi là bình thường hoặc chỉ sai một chút về logic. Tuy nhiên cái vấn đề sai ở đây là procselfmemThread() đang cố tình ghi vào một vùng mapping được định nghĩa là Read Only (dòng 101) cộng thêm việc vùng này đã bị clear bởi madvise() trước đó. Điều này dẫn đến 1 cái fault mà Linus Torvalds đã nhận ra từ 11 năm trước, nó cho phép break COW để ghi trực tiếp nội dung vào file gốc. Anh này cũng đã fix lỗi này tại thời điểm ấy, tuy nhiên không thật sự thành công.

Vá lỗi

Trên Linux repo, chúng ta có thể thấy cách Linus Torvalds vá lỗi này. Một cờ mới là FOLL_COW được đưa vào để đảm bảo quá trình COW đã hoàn tất trước khi 1 thread khác xen vào.

+/*
+ * FOLL_FORCE can write to even unwritable pte’s, but only
+ * after we’ve gone through a COW cycle and they are dirty.
+ */
+static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
+{
+    return pte_write(pte) ||
+    ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
+}
+

Kết

Dirty Cow là một lỗi nghiêm trọng, dù nó chỉ thuộc loại Local Exploit nhưng thời gian tồn tại trước khi công bố cũng đủ lâu để ai đó làm gì biến mọi thứ trở nên nguy hiểm hơn. Nhiều thiết bị android cũng đã được xác nhận có ảnh hưởng. Vì thế, hãy cập nhật hệ điều hành của các bạn sớm nhất có thể và đặc biệt là với các Honey pot hay Vuln Server của các cuộc thi CTF. Thông tin thêm có thể tìm thấy ở trang chủ Dirty Cow.

Tài liệu tham khảo

[1] https://dirtycow.ninja/
[2] https://github.com/dirtycow/dirtycow.github.io
[3] http://man7.org/linux/man-pages/man2/mmap.2.html
[4] http://man7.org/linux/man-pages/man2/madvise.2.html
[5] https://github.com/torvalds/linux/blob/5924bbecd0267d87c24110cbe2041b5075173a25/mm/madvise.c#L452
[6] https://en.wikipedia.org/wiki/Copy-on-write
[7] https://en.wikipedia.org/wiki/Dirtybit
[8] https://sandstorm.io/news/2016-10-25-cve-2016-5195-dirtycow-mitigated
[9] https://github.com/torvalds/linux/commit/4ceb5db9757aaeadcf8fbbf97d76bd42aa4df0d6
[10] http://www.v3.co.uk/v3-uk/news/2474845/linux-users-urged-to-protect-against-dirty-cow-security-flaw
[11] https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619
[12] https://www.youtube.com/watch?v=kEsshExn7aE