Hướng dẫn build aod vào kernel

Syzkaller là một fuzzer tìm lỗ hổng trên nhân Linux rất hiệu quả, và đã hỗ trợ tìm ra rất nhiều bug trong những năm gần đây. Các bạn có thể từng nghe đến những cái tên như Dirty Cow, Meltdown, Spectre, BlueBorne… Nhưng syzkaller tìm được hàng nghìn bug như thế trong vài năm qua. Đây là link những bug mà syzkaller tìm thấy.

Hướng dẫn build aod vào kernel
Hình 1. Một số kernel bugs nghiêm trọng đã được phát hiện [1]

Google xây dựng một hệ thống là syzbot, fuzz tự động và liên tục nhân Linux tại đây. Bug họ tìm thấy chủ yếu nhờ vào hệ thống này.

Hướng dẫn build aod vào kernel
Hình 2. Syzbot dashboard [2]

Trong danh sách này, các bug nghiêm trọng sẽ được ưu tiên fix trước. Có một điều đặc biệt tôi nhận thấy là tất cả các bug đều được public, kể cả các bug chưa được fix. Đồng thời, tôi nhận thấy rằng các hệ thống thật “in the wild” thường không được cập nhật nhân thường xuyên, đặc biệt là trên PC, server. Nếu như hacker target vào một mục tiêu nào đó, muốn thực hiện leo quyền hay RCE hệ thống, thì họ có thể đợi syzbot tìm ra một lỗi nghiêm trọng, rồi viết mã khai thác cho target đó.

Tôi đặt ra một bài toán là: Tìm lỗ hổng leo thang quyền trên một phiên bản nhân Linux và viết mã khai thác. Kiểm tra một số server, PC Linux thử nghiệm tôi thấy phiên bản nhân được biên dịch vào các thời điểm khá xa hiện tại (12/2020):

  • CentOS: 3/2020
  • 2 Ubuntu Desktop 18.04: 10/2020
  • Ubuntu server Ubuntu 18.04.3: 7/2020
  • Ubuntu server 1: 2/2020
  • Ubuntu server 2: 6/2020
  • Ubuntu server 3: 8/2019

Như vậy, tiềm năng exploit được nhân Linux kernel khá lớn, khi mà Linux kernel được update chậm khoảng vài tháng đến cả năm. Thông thường, khi muốn tìm N-day ta thường lên Internet rà soát các bản exploit có sẵn, hoặc kiếm CVE và tự viết mã khai thác. Với Linux kernel thì lại càng khó tìm CVE public, họa chăng được một vài cái trong một năm. Tuy nhiên, khi mà có syzbot liên tục public các lỗi mới, thì vấn đề trên có thể được giải quyết. Tôi chia sẻ cách thức tôi thực hiện như sau:

  1. Kiếm mã nguồn bản Linux kernel cần thử nghiệm khai thác.
  2. Biên dịch mã nguồn để fuzz bằng syzkaller.
  3. Dựng máy ảo qemu và chạy bản kernel biên dịch được trên qemu.
  4. Crawl toàn bộ bug đã tìm ra của syzbot về và tạo thành input corpus cho syzkaller.
  5. Fuzz target với syzkaller cho tới khi tìm được bug phù hợp, có thể viết mã khai thác.
  6. Phân tích log crash, minimize log để tìm ra testcase nhỏ nhất gây ra crash.
  7. Viết mã khai thác với bug tìm được.

Tôi chọn target là: kernel 4.15.0-60-generic trên Ubuntu 18.04 (biên dịch vào tháng 8/2019).

Hướng dẫn build aod vào kernel
Hình 3. Phiên bản kernel sẽ target

Đây là kết quả tôi làm được sau một thời gian dài tìm hiểu về exploit Linux kernel và fuzz syzkaller, tìm được bug leo quyền và viết mã khai thác thành công trên target:

Trong khuôn khổ bài viết này, tôi sẽ trình bày 6 bước đầu.

1. Tìm bản Linux kernel tương ứng với target

Tôi tìm hiểu được hai cách để tải về bản linux kernel tương ứng với target cần tấn công:

Cách 1: dựa vào kernel version map [3], tìm ra phiên bản Mainline Kernel Version tương ứng, từ đó tải source từ đường dẫn. Với target 4.15.0-60, ta có Mainline Version là 4.15.18.

Hướng dẫn build aod vào kernel
Hình 4. Kernel version map [3]

Tuy nhiên, map giữa Kernel Version và Mainline Version không phải map 1-1. Nếu sử dụng bản 4.15.18 để fuzz, sẽ có nhiều bug đã được vá. Cách này sẽ không chuẩn bằng cách số 2 dưới đây.

Cách 2: tải về mã nguồn kernel qua lệnh apt-getapt-get install linux-source

Tuy nhiên với cách này, trước hết bạn phải cài bản Ubuntu có version tương ứng với target trước, sau đó mới chạy được lệnh trên. Hơn nữa, cách này sẽ tải về source code mới nhất có thể tương thích với bản nhân đang cài, đã vá các lỗ hổng. Do đó lệnh này cũng không hoàn toàn phù hợp với mục đích ta cần. Bằng cách phân tích traffic khi chạy lệnh trên, tôi phát hiện ra package được tải về tại đường dẫn.

Hướng dẫn build aod vào kernel
Hình 5. Traffic được captured khi cài đặt từ linux source

Ta có thể lấy trực tiếp mã nguồn từ đường dẫn này, và nó sẽ hoàn toàn tương ứng với bản kernel của target.

Hướng dẫn build aod vào kernel
Hình 6. Target linux version [4]

Sử dụng các lệnh ar, tar để extract file .deb download được, thu được source code.

2. Biên dịch mã nguồn

Để biên dịch mã nguồn, ta làm theo hướng dẫn tại đây. Tuy nhiên, với cách sử dụng default config, có nhiều driver được build ở dạng loadable module (CONFIG_XXX=m), không build vào file vmlinux, mà theo cách setup syzkaller hiện tại thì chỉ fuzz được file vmlinux. Do đó, muốn fuzz module nào thì ta phải set CONFIG_XXX=y, và module sẽ được build sẵn vào vmlinux. Việc tự tìm hiểu xem loadable nào nên bật sẽ khá mất thời gian, nên tôi sử dụng một file .config sẵn của syzbot [5].

Chạy lệnh “make olddefconfig

Chú ý kiểm tra xem các tùy chọn sau có đang được enable không:

# Coverage collection.

CONFIG_KCOV=y

# Debug info for symbolization.

CONFIG_DEBUG_INFO=y

# Memory bug detector

CONFIG_KASAN=y

CONFIG_KASAN_INLINE=y

# Required for Debian Stretch

CONFIG_CONFIGFS_FS=y

CONFIG_SECURITYFS=y

Sau đó chạy lệnh: make bzImage

Chú ý: khi build tôi gặp một số bug, nên cần bỏ một số cấu hình sau:

  • CONFIG_MODVERSIONS=y
  • CONFIG_BATMAN_ADV=y

3. Tạo image để chạy được với qemu

Hướng dẫn có sẵn tại mục Image. Cơ bản, cách này sử dụng debootstrap để cài đặt một HĐH Linux vào một folder (có thể chroot). Sau đó ta tạo một file image và copy toàn bộ các file vào image này để làm ổ đĩa máy ảo. Chạy lệnh default để tạo một image 2G: ./create_image.sh

Chú ý:

  • Mật khẩu của root được set thành rỗng.
  • Image mặc định có kích thước 2G và là bản Stretch.
  • Network interface được set là eth0. Khi chạy lên bằng qemu, có thể không chạy được vì tên không trùng. Ta sẽ sửa khi boot lên thành công bằng qemu.
  • Có các key ssh stretch.id_rsastretch.id_rsa.pubđược tạo. Public key được add vào cấu hình ssh của image. Ta sẽ dùng key private này để cấu hình cho syzkaller fuzz được.

Khởi chạy image và kernel build được bằng lệnh sau:

qemu-system-x86_64 \

        -m 2G \

        -smp 2 \

        -kernel $KERNEL/arch/x86/boot/bzImage \

        -append "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0" \

        -drive file=$IMAGE/stretch.img,format=raw \

        -net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \

        -net nic,model=e1000 \

        -enable-kvm \

        -nographic

Hướng dẫn build aod vào kernel
Hình 7. target fuzzing machine

Chú ý:

  • enable-kvm tùy từng host mới support, giúp fuzz nhanh hơn. Nếu test trên Virtualbox và enable-kvm thì chỉ được cấu hình 1 kernel -smp 1
  • Bằng cách forward cổng 22 -> 10021, ta có thể ssh vào máy bằng key ssh đã cấu hình.

4. Tạo corpus

Input cho syzkaller là một file corpus.db, chứa các syz program. Syz program có format do syzkaller định nghĩa, cơ bản là một danh sách có thứ tự của các syscall hoặc hàm do syzkaller tự định nghĩa. Ví dụ, một syz program như sau:

# https://syzkaller.appspot.com/bug?id=a839269752ca27e5a69938064ba906ac8cf3fb36

# See https://goo.gl/kgGztJ for information about syzkaller reproducers.

#{"procs":1,"sandbox":"","fault_call":-1,"close_fds":false}

r0 = syz_usb_connect$hid(0x0, 0x36, &(0x7f0000000080)=ANY=[@ANYBLOB="12010000000018105e04da070000000000010902240001000000000904000009030000000921000000012222000905810308"], 0x0)

syz_usb_control_io$hid(r0, 0x0, 0x0)

syz_usb_control_io$hid(r0, &(0x7f00000001c0)={0x24, 0x0, 0x0, &(0x7f00000000c0)=ANY=[@ANYBLOB="00222200000096031306e53f070c0000072a9000a7c900be0017cf6643a30b09007a1583"], 0x0}, 0x0)

syz_usb_ep_write(r0, 0x0, 0x1, &(0x7f0000000000)='B')

Trên trang syzbot, ta coi ví dụ một crash [6] thì sẽ thấy các thông tin như sau:

  • Commit: bản linux được sử dụng
  • Syzkaller: bản syzkaller tìm ra bug
  • Config: file .config dùng để build linux
  • Syz repro: file syz program gây ra crash. Đây là file ta cần crawl về.
  • C repro: file C gây ra crash
Hướng dẫn build aod vào kernel
Hình 8. Một bug được syzbot phát hiện [6]

Thông tin syz repro và C repro có thể không có, bởi vì không phải crash nào syzkaller cũng có thể reproduce lại được. Ta cần tải về toàn bộ các syz program. Đây là link mã nguồn script tôi dùng để tải corpus.

  • Script parse tất cả các trang có thông tin lỗ hổng, lấy ra syz prog nếu có.
  • Script cache lại các link đã get vào ổ cứng, tránh phải get lại.

Sau khi lấy hết các file syz program về, ta dùng chương trình syz-db để đóng gói chúng lại cho syzkaller dùng:

Hướng dẫn build aod vào kernel
Hình 9. Pack corpus bằng syz-db

5. Fuzz target với syzkaller

Làm tương tự theo hướng dẫn tại đây. Tuy nhiên, cần chú ý:

  • Đặt file corpus.db (đúng tên này) vào thư mục workdir để được nhận làm input corpus.
  • Thêm tham số reproduce = false vào file config vì khi tìm ra 1 crash, syzkaller sẽ dựa trên log crash (là danh sách các program đã thực thi), thực hiện bước reproduce để tìm ra program gây ra crash. Bước này rất mất thời gian nên ta bỏ qua.
  • Thêm tham số sandbox = setuid, để fuzz với quyền nobody chứ không phải quyền root. Crash gây ra bởi root sẽ không có ý nghĩa leo quyền.
  • Syzkaller gọi qemu với tham số snapshot, nên file image không bị sửa đổi sau khi fuzz.

File config của tôi:

{

      "target": "linux/amd64",

      "http": "127.0.0.1:56741",

      "reproduce": false,

      "sandbox": "setuid",

      "workdir": "/root/linux-data/qemu/blog/workdir",

      "kernel_obj": "/root/kernels/linux-source-4.15.0-60",

      "image": "/root/linux-data/qemu/blog/stretch.img",

      "sshkey": "/root/linux-data/qemu/blog/stretch.id_rsa",

      "syzkaller": "/root/gopath/src/github.com/google/syzkaller",

      "procs": 4,

      "type": "qemu",

      "vm": {

            "count": 1,

            "kernel": "/root/kernels/linux-source-4.15.0-60/arch/x86/boot/bzImage",

            "cpu": 1,

            "mem": 2048

      }

}

Vào link trình duyệt http://127.0.0.1:56741 ta sẽ thấy thống kê quá trình fuzz.

Hướng dẫn build aod vào kernel
Hình 10. Fuzzing dashboard

Sau khoảng nửa ngày fuzz, tôi phát hiện thấy khá nhiều bug:

Hướng dẫn build aod vào kernel
Hình 11 Kết quả fuzzing

Trong đó tôi thấy bug KASAN: use-after-free Read in bcsp_close là tiềm năng khai thác được, bởi vì vào đọc log report ta thấy skb được phân phát và free tại hàmbcsp_recv, sau đó lại được free tại bcsp_close. Đây rõ ràng là một bug UAF điển hình.

BUG: KASAN: use-after-free in kfree_skb+0x2b6/0x340 net/core/skbuff.c:659

Read of size 4 at addr ffff88802b4fa2a4 by task syz-executor.0/8945

CPU: 0 PID: 8945 Comm: syz-executor.0 Not tainted 4.15.18 #1

Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014

Call Trace:

 …

 skb_unref include/linux/skbuff.h:960 [inline]

 kfree_skb+0x2b6/0x340 net/core/skbuff.c:659

 bcsp_close+0xce/0x160 drivers/bluetooth/hci_bcsp.c:763

 hci_uart_tty_close+0x1af/0x240 drivers/bluetooth/hci_ldisc.c:551

 tty_ldisc_close.isra.2+0x91/0xd0 drivers/tty/tty_ldisc.c:506

 tty_ldisc_kill+0x46/0xc0 drivers/tty/tty_ldisc.c:652

 tty_ldisc_release+0xfa/0x210 drivers/tty/tty_ldisc.c:819

 tty_release_struct+0x14/0x50 drivers/tty/tty_io.c:1611

 …

RIP: 0033:0x417e77

RSP: 002b:00007ffca8899c00 EFLAGS: 00000293 ORIG_RAX: 0000000000000003

RAX: 0000000000000000 RBX: 0000000000000003 RCX: 0000000000417e77

RDX: 0000000000000000 RSI: 00000000a31766cd RDI: 0000000000000003

RBP: 0000000000000001 R08: 00000000a31766d1 R09: 0000000000000000

R10: 00007ffca8899d40 R11: 0000000000000293 R12: 000000000076c980

R13: 000000000076bf00 R14: 000000000076bf21 R15: 000000000000dc8d

Allocated by task 102:

 kmem_cache_alloc_node+0x69/0x400 mm/slab.c:3640

 __alloc_skb+0xa4/0x500 net/core/skbuff.c:193

 alloc_skb include/linux/skbuff.h:988 [inline]

 bt_skb_alloc include/net/bluetooth/bluetooth.h:339 [inline]

 bcsp_recv+0x82c/0x13f0 drivers/bluetooth/hci_bcsp.c:685

 hci_uart_tty_receive+0x1eb/0x4b0 drivers/bluetooth/hci_ldisc.c:616

 tty_ldisc_receive_buf+0x12f/0x160 drivers/tty/tty_buffer.c:460

 …

Freed by task 102:

 …

 __kfree_skb net/core/skbuff.c:646 [inline]

 kfree_skb+0xb6/0x340 net/core/skbuff.c:663

 bcsp_recv+0x531/0x13f0 drivers/bluetooth/hci_bcsp.c:623

 hci_uart_tty_receive+0x1eb/0x4b0 drivers/bluetooth/hci_ldisc.c:616

 tty_ldisc_receive_buf+0x12f/0x160 drivers/tty/tty_buffer.c:460

 …

Phân tích code ta thấy skb được phân phát tại drivers/bluetooth/hci_bcsp.c:685 hàm bcsp_close:

Hướng dẫn build aod vào kernel
Hình 12. skb allocation

Skb được free tại drivers/bluetooth/hci_bcsp.c:623 bcsp_recv:

Hướng dẫn build aod vào kernel
Hình 13. free skb

Sau đó dược free lần nữa tại drivers/bluetooth/hci_bcsp.c:763 bcsp_close:

Hướng dẫn build aod vào kernel
Hình 14. double-free skb

Tại bcsp_close, bcsp->rx_skb được kiểm tra xem có = NULL không, rồi mới free. Tuy nhiên trước đó, tại dòng 623, bcsp->rx_skbđược free nhưng không set về NULL, nên mới gây ra bug.

Kiểm tra mã nguồn nhân Linux trên github [7], tôi phát hiện lỗ hổng xuất hiện vào ngày 6/7/2019, khi vá một lỗ hổng memory leak do không free rx_skb [8]:

Hướng dẫn build aod vào kernel
Hình 15. commit vá lỗ hổng memory leak trong bcsp_close, nhưng lại gây ra lỗi double free [8]


Tới ngày 4/11/2019 thì lỗ hổng được vá bằng cách gán bcsp->rx_skb = NULL ngay sau khi free [9]:

Hướng dẫn build aod vào kernel
Hình 16. vá lỗ hổng double free trong bcsp_recv [9]

6. Reproduce crash ra file syz/C proc

Hướng dẫn thực hiện tại đường dẫn. Reproduce là bước tìm ra program gây ra crash từ log gọi các program.

Hướng dẫn build aod vào kernel
Hình 17. log crash

Trong quá trình fuzz, các program được fuzz đa luồng (tham số procs trong file config), nên không thể dựa vào log mà nhìn ra được program nào gây ra crash. Ta sẽ phải sử dụng phương pháp sau:

  1. Khởi chạy máy ảo qemu như bước 3.
  2. Dùng ssh đẩy các file syz-execprog, syz-executor lên máy ảo target.
  3. Dùng chương trình syz-execprog chạy log crash trên target. Ta thử remove dần dần các program khỏi log, cho đến khi chỉ còn một program. Sau đó thử remove từng syscall khỏi program để minimize nó.
    ./syz-execprog -executor=./syz-executor -repeat=1 -sandbox=setuid -enable=none -collide=false ./log0

Program tìm được:

03:47:02 executing program 0:

r0 = openat$ptmx(0xffffffffffffff9c, &(0x7f0000000280)='/dev/ptmx\x00', 0x0, 0x0)

ioctl$TIOCSETD(r0, 0x5423, &(0x7f0000000040)=0xf)

ioctl$KDADDIO(r0, 0x400455c8, 0x1)

r1 = socket$inet6(0xa, 0x400000000001, 0x0)

bind$inet6(r1, &(0x7f0000000600)={0xa, 0x4e20, 0x0, @loopback}, 0x1c)

sendto$inet6(r1, 0x0, 0x0, 0x20000008, &(0x7f00008d4fe4)={0xa, 0x4e20, 0x0, @loopback}, 0x1c)

r2 = open(&(0x7f0000000240)='./bus\x00', 0x100000141042, 0x0)

ftruncate(r2, 0x10099b7)

sendfile(r1, r2, 0x0, 0x8000fffffffe)

Dùng syz-prog2c để convert chương trình thu được sang code C: syz-prog2c -prog crash.syz -repeat=1 -enable=none > crash.c

Để chạy thử nghiệm chương trình crash.c trên, ta tạo một user test trên target, login vào và chạy file crash:

Kết luận

Vậy là việc thử nghiệm với target kernel 4.15.0-60-generic thành công, nếu không có syzbot thì công việc tìm N-day trên nhân Linux rất khó khăn. Tuy nhiên, còn cần phải thực hiện thêm với nhiều target nữa mới có thể khẳng định phương pháp này khả thi khi thử nghiệm tấn công thực tế hay không. Nếu khả thi thì tôi nghĩ vấn đề security của nhân Linux cần được quan tâm hơn và khi sử dụng, các cá nhân và tổ chức cần thường xuyên cập nhật bản mới nhất ngay khi có thể.

Tham khảo

[1]

https://events19.linuxfoundation.org/wp-content/uploads/2017/11/Syzbot-and-the-Tale-of-Thousand-Kernel-Bugs-Dmitry-Vyukov-Google.pdf.

[2]

https://syzkaller.appspot.com/upstream.

[3]

https://people.canonical.com/~kernel/info/kernel-version-map.html.

[4]

http://us.archive.ubuntu.com/ubuntu/pool/main/l/linux/.

[5]

https://syzkaller.appspot.com/text?tag=KernelConfig&x=fac7c3360835a4e0.

[6]

https://syzkaller.appspot.com/bug?id=a839269752ca27e5a69938064ba906ac8cf3fb36.

ManhND

Trung tâm Dịch vụ, VinCSS (a member of Vingroup)