Vulnerabilities
- 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 subsystemV4L2 - (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ứngvideo4linux
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ụngRW 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ứavb2_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ệnhread
thực hiện, nó gọivb_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ừ buffervb2_buffer
được thêm vàovb2_queue (vb2_queue->queued_list)
- Điều chú ý ở đây, khóa
dev->mutex
củavb2_fop_read
và khóa tạivb2_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ọivb2_start_streaming
để đưa dữ liệu vàovb_buffer
. Quá trình này nó sẽ gọi__enqueue_in_driver
để thêm buffer vàovivid_dev
, hàng đợivid_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
|
|
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
|
|
- Hàm
vivid_thread_vid_cap
này có nhiệm vụ đưa dữ liệu vàovb2_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óalock(mutex_lock(&dev->mutex))
, khi nhận được khóa, dữ liệu trênvb2_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ênvivid_thread_vid_cap
là một, điều này có nghĩa, khi thread đang thực hiệnvivid_thread_vid_cap
mở khóa, một thread nào đó thực hiệnvb2_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ạyvivid_thread_vid_cap
vàvb2_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ồivb2_fop_read
không kết thúc ngay, nó lại gọivb2_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 đợidev->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 khivb2_fop_read
kết thúc, thì địavb2_buffer
lại được lưu trongdev->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_streaming
vàvivid_stop_generating_vid_cap
.vivid_thread_vid_cap
: lấy tất cả dữ liệu trong hàng đợivid_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àmclose(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ọikthread_stop
để dừng kernel thread đang thực hiện hàmvivid_thread_vid_cap
.
Nếu như kết thúc hàmread()
nó chỉ tạo ra một nguy cơ xảy ra lỗiuaf
, thì tại đây chính là nguyên nhân gây ra lỗiuaf
.
|
|
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->bufs
và vb2_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ỗiuse-after-free
trong các thao tác danh sách liên kết trongvid_cap_buf_queue()
. Nguyên nhân là do sự sai sót trong quá trình sử dụng khóa (mutex lock) trongvivid_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àmvivid_stop_generating_vid_cap()
:
|
|
- 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 rause-after-free
sau này khi streaming hoạt động trở lại.
chưa biết cách sử dụngskyzkaller fuzzer
.
Vì sao dẫn tới khả năng xảy ra(đã trả lời trong cơ chế hàmuse-after-free
?read, close
trong vivid) - Để giải quyết tình trạng trên, tác giả đề xuất như sau:
- 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:
|
|
- Sử dụng
mutex_trylock()
vớischedule_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:
|
|
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ỗiUAF
, 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
- Đã có
- 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.
|
|
- Chúng ta tiến hành race
|
|
- Khai bắt đầu streaming hàm
vid_cap_start_streaming()
sẽ , được gọi bởiV4L2
trong khivb2_core_streamon()
đọc từ file descriptor. - Khi dừng streaming, hàm
vivid_stop_generating_vid_cap()
sẽ đượcV4L2
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êmvb2_buffer
vàovb2_queue.queued_list
. - Khi chạy đoạn code trên, chúng ta có thể sẽ được kết quả như sau:
- 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ọivb2_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óngvb2_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 đã đượcKASAN
phát hiện.
Heap spraying
Heap spraying
là kĩ thuật, mục đích đặt nhữngcontrolled 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ớicontrolled 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ácuse-after-free
trong Linux Kernel sẽ thường dùngkmalloc()
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ớicontrolled contents
sẽ cho phép chúng ta ghi đè vùng nhớ có thể khai thác.
Ả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ằnguserfaultfd()
sẽ cho phép chúng ta kiểm soát đượclifetime
của dữ liệu được cấp phát bởisetxattr()
trong kernelspace. - Quay trở về với lỗ hổng này,
vb2_buffer
sẽ đượcfree
khi streaming dừng và sẽ đượcuse
ở lần streaming tiếp theo. Do đó, nếu chúng ta sử dụng kĩ thuậtheap 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ùngkmalloc()
để cấp phát vùng nhớ vừa free ở cuối vòng lặp trước vớicontrolled 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ọikmalloc)
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 dosetxattr()
cấp phát chỉ tồn tại cho đến khi trình xử lí lỗi tranguserfaultfd()
gọi hàmioctl
với flagUFFDIO_COPY
. Hay nói cách khác, muốn dữ liệu được cấp phát bởisetxattr()
không bị mất thìuserfaultfd()
không gọiioctl
. Tác giả giải quyết vấn đề này bằng cách tạo ra mộtpool of threads
: mỗi thread này được gọi làspraying thread
, gọi hàmsetxattr()
được xử lí bởiuserfaultfd()
và lưu dữ liệu được cấp phát.
userfaultfd()
hay mỗi spraying thread sẽ có 1 userfaultfd()
tương ứngBâ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
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.
- Tắt
SMAP (Supervisor Mode Access Prevention), SMEP (Supervisor Mode Execution Prevention), KPTI (Kernel Page-Table Isolation)
- Làm
vb2_buffer.vb2_queue
trỏ tới một vùng nhớ được ánh xạ trên userspace. - 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 đặtvb2_queue
vàvb2_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ủakernel 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àokernel stack
và giữ nó bằnguserfaultfd()
, giống như kĩ thuật heap spraying sử dụnguserfaultfd()
+setxattr()
. Hàmcopy_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
- Tạo ra
pool of threads
và đồng bộ hóa chúng bằng hàmpthread_barriers
. Dưới đây là codepthread_barriers
được đặt tại các con trỏ tham khảo chính trong suốt quá trình khai thác
|
|
- 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ởiuserfaultfd()
- 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
.
- 2
- 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ủapthread_barrier_init()
chỉ định số threadPHẢI
gọipthread_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! |
- 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:
- Trong
kernel heap
bằngsprayer threads
bằng cách sử dụng hàmsetxattr()
được xử lí bởiuserfaultfd()
- Trong
kernel stack
bằngracer threads
bằng cách sử dụngadjtimex()
được xử lí bởiuserfaultfd()
. System call này được chọn vì nó có gọi hàmcopy_from_user()
tới kernel stack.
- Payload gồm 3 phần:
vb2_buffer
trong kernel heap.vb2_queue
trong kernel stack.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.
|
|
adjtimex() syscall
ghi dữ liệu vào kernel stack
|
|
- 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ủaKASLR
.
- Giá trị
|
|
- Sau đó, pthread phân tích cú pháp của
kmsg
, điều chỉnh payload trên heap và stack.
|
|
Lược đồ thể hiện mối quan hệ giữa các thành phần bên trong kernel memory.
ROP’n’JOP
- Tiếp theo sẽ tạo ra ROP chain để khai thác lỗi.
Từ lược đồ, có thể thấyvoid *(*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ệnhpush rdi; pop rsp
sẽ đưa stack pointer tới nơi mà chúng ta muốnRDI
. - Tiêp theo
ROP chain
cho mục đích leo thang đặc quyền:
|
|
Cách thức hoạt động của ROP chain
ROP chain
đưa địa chỉ của hàmrun_cmd()
từkernel/reboot.c
vào thanh ghiR15
.- 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àmrun_cmd()
. - 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.
|
|
- ROP chain nhảy tới
__noreturn_do_task_dead()
trongkernel/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:
- Thiết lập
/proc/sys/vm/unprivileged_userfaultfd
vê0
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ởiprivileged users
(vớiSYS_CAP_PTRACE
capability). - Thiết lập
kernel.dmesg_restrict
sysctl về1
sẽ chặn rò rỉ thông tin quakernel log
. VIệc này hạn chế người dùng đọc thông tin từkerbel syslog
quadmesg
. Tuy nhiên, dù chokernel.dmesg_restrict = 1
. Người dùng Ubuntu từ groupadm
vẫn có thể đọc kernel log từ/var/log/syslog
. - 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ủavb2_qeue
. PAX_RAP
từ bản vá củagrsecurity/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
- qemu
- ubuntu server 18.04.3
- How To Install “linux-tools-virtual-lts-vivid” Package on Ubuntu
- Trong quá trình cài đặt và kiểm tra với lệnh
getfacl /dev/video0
có thể bạn sẽ bị lỗi vì thiêu thư mụcdev/video0
. Bài viết này giúp tôi giải quyết được vấn đề này. - Cài đặt KASAN để debug
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
|
|
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õ:
- Hàm
sysconf(_SC_PAGE_SIZE)
: