1 of 70

Fuzzing the Linux kernel

Andrey Konovalov, xairy.io

�May 20th 2021

2 of 70

  • Andrey Konovalov

​

  • Work on Linux kernel bug detectors, fuzzers, and exploit mitigations
    • KASAN, syzkaller, Memory Tagging

​

  • xairy.io

Who am I?

3 of 70

  • Network fuzzing via syscalls
    • 3 LPE exploits

​

  • External network fuzzing

​

  • External USB fuzzing
    • 300+ bugs

My experience with Linux kernel fuzzing

​

4 of 70

  • Fuzzing
  • Fuzzing the Linux kernel
    • Legacy
    • Foundation
    • Charged
  • Approaches
  • Tips
  • Final note

Agenda

Concepts, from simplest to most involved

5 of 70

ФСТЭК

6 of 70

Fuzzing

7 of 70

  • Fuzzing — feeding in random inputs until the program crashes

Fuzzing

Generate

input

Execute

program

Crash?

No

Yes

Great!

8 of 70

  • Fuzzing — feeding in random XML files until the parser crashes

Fuzzing an XML parser

Generate

random

XML file

Feed into

parser

Crash?

No

Yes

Great!

9 of 70

  • Fuzzing — feeding in random inputs until the program crashes
  • Programs:
    • Application
    • Library
    • Kernel
    • Firmware
    • ...

Programs

10 of 70

  • Fuzzing — feeding in random inputs until the program crashes
  • — How do we execute the program?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate the process?

Fuzzing

11 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate the process?

Kernel fuzzing

12 of 70

Fuzzing the Linux kernel:

Legacy

13 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate the process?

Running the kernel

14 of 70

Running the kernel

​

Physical device

VM (e.g. QEMU)

Fuzzing surface

Native

(includes device drivers)

Only what

the VM supports

Management

(restarting, debugging, getting kernel logs)

Hard;

hardware gets bricked

Easy

Scalability

Buy more devices

Spawn more VMs

15 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Kernel inputs

16 of 70

Kernel inputs

vmlinux

module.ko

Userspace

Kernel

Syscalls (open, write, ioctl, …)

17 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute a binary

Legacy approach

Works everywhere!

18 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute a binary

Generating inputs

19 of 70

  • In case of an XML file parser
  • How do we generate inputs for it when fuzzing?
  • Idea #1: just generate random data

Generating inputs for userspace apps

20 of 70

if (input[0] == '<')

if (input[1] == 'x')

if (input[2] == 'm')

if (input[3] == 'l')

// Need to reach at least here.

​

  • Parser expects the file to start with "<xml" header
  • Fuzzer needs ~2^32 guesses to get past the header check

Random inputs

21 of 70

  • Random binary data works poorly as inputs
  • So what should we do?
  • Generate better inputs, duh
  • How?
  • Structured inputs (a.k.a. structure-aware fuzzing)
  • [Discussed later]
  • [Discussed later]

Better inputs

22 of 70

XML_GRAMMAR = {

"<start>": ["<xml-tree>"],

"<xml-tree>": ["<text>", "<xml-open-tag><xml-tree><xml-close-tag>",

"<xml-openclose-tag>", "<xml-tree><xml-tree>"],

"<xml-open-tag>": ["<<id>>", "<<id> <xml-attribute>>"],

"<xml-openclose-tag>": ["<<id>/>", "<<id> <xml-attribute>/>"],

"<xml-close-tag>": ["</<id>>"],

"<xml-attribute>" : ["<id>=<id>", "<xml-attribute> <xml-attribute>"],

"<id>": ["<letter>", "<id><letter>"],

"<text>" : ["<text><letter_space>","<letter_space>"],

"<letter>": srange(string.ascii_letters + string.digits +"\""+"'"+"."),

"<letter_space>": srange(string.ascii_letters + string.digits +"\""+"'"+" "+"\t"),

}

Structured inputs

23 of 70

  • Can generate structured blobs
  • But the kernel does not accept blobs as inputs
    • (Except when limiting fuzzing surface to e.g. a single syscall)

Generating kernel inputs

24 of 70

int fd = open("/dev/something", …);

ioctl(fd, SOME_IOCTL, &{0x10, ...});

close(fd);

Example of a kernel input

25 of 70

int fd = open("/dev/something", …);

ioctl(fd, SOME_IOCTL, &{0x10, ...});

close(fd);

​

  • A sequence of calls

Example of a kernel input

26 of 70

int fd = open("/dev/something", …);

ioctl(fd, SOME_IOCTL, &{0x10, ...});

close(fd);

​

  • A sequence of calls
  • Arguments are structured

Example of a kernel input

27 of 70

int fd = open("/dev/something", …);

ioctl(fd, SOME_IOCTL, &{0x10, ...});

close(fd);

​

  • A sequence of calls
  • Arguments are structured
  • Return values (output fields of structures) used in subsequent calls

Example of a kernel input

28 of 70

int fd = open("/dev/something", …);

ioctl(fd, SOME_IOCTL, &{0x10, ...});

close(fd);

​

  • A sequence of calls
  • Arguments are structured
  • Return values (output fields of structures) used in subsequent calls

​

  • Syscalls are used as an API

Example of a kernel input

29 of 70

  • Fuzzer knows about API calls and their arguments
    • Need to describe APIs manually for the kernel
      • (No way to generate them automatically)
  • Fuzzer remembers and then uses return/output values
    • Example: keep a list of opened file descriptors of each type

API-aware fuzzing

30 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute a binary

​

​

​

API-awareness

Kernel panics

while (true) syscall(…)

Legacy approach

This is Trinity!

31 of 70

Fuzzing the Linux kernel:

Foundation

32 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute binary

Foundational approach

33 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute binary

Generating inputs

34 of 70

  • Random binary data works poorly as inputs
  • So what should we do?
  • Generate better inputs, duh
  • How?
  • Structured inputs (a.k.a. structure-aware fuzzing)
  • [Discussed later]
  • [Discussed later]

Better inputs

35 of 70

  • Random binary data works poorly as inputs
  • So what should we do?
  • Generate better inputs, duh
  • How?
  • Structured inputs (a.k.a. structure-aware fuzzing)
  • Coverage-guided generation (a.k.a coverage-guided fuzzing)
  • [Discussed later]

Better inputs

36 of 70

Mutate according to the structure (e.g. insert/remove XML tags)

Coverage-guided generation

Corpus of inputs

Choose a random input

Mutate

New cover?

Add to corpus

Execute

No

Yes

37 of 70

  • Need a notion of a test case
    • Unlike infinite stream of calls the legacy approach had
    • => Generate (and mutate) finite API-call sequences
  • Need a way to collect relevant code coverage
    • Relevant — only from code that handles syscalls
    • => Use KCOV
      • Based on compiler instrumentation
      • Collects coverage from the current task context

Applying to the kernel

38 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute binary�API-awareness + KCOV

Running the kernel

39 of 70

  • Kernel panic is not a good indicator
    • Some bugs are not panics (e.g. info-leaks)
    • Other bugs do not panic immediately (e.g. memory corruptions)

​

  • Use dynamic bug detectors
    • Dynamic — finds bugs that happen during execution

​

Detecting kernel bugs

40 of 70

  • Most notable: KASAN — detects memory corruptions
    • Slab/stack/global out-of-bounds, use-after-frees, etc.

​

​

  • Note: detectors not tied to fuzzer, can use with Trinity as well

​

Dynamic bug detectors for the kernel

41 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute binary�API-awareness + KCOV�KASAN and others

Running the kernel

42 of 70

  • Monitoring kernel log for crashes
  • Restarting crashed VMs
  • Deduplicating crashes
  • Generating reproducers
  • Reporting bugs / tracking fixes

​

  • How? Write code!

​

Automation

43 of 70

  • Fuzzing — feeding in random inputs until the kernel crashes
  • — How do we run the kernel?
  • — What are inputs?
  • — How do we inject inputs?
  • — How do we generate inputs?
  • — How do we detect bugs?
  • — How do we automate?

QEMU or physical device

Syscalls

Execute binary

​

​

​

API-awareness + KCOV

KASAN and others

All that mentioned fancy stuff

Foundational approach

This is syzkaller! (in its base)

44 of 70

Fuzzing the Linux kernel:

Charged

45 of 70

  • Works for code that is separable from the rest of the kernel
  • No need to bother with emulators/hypervisors
  • Downside: hard to maintain and scale

Running kernel code in userspace

46 of 70

Kernel inputs: syscalls

vmlinux

module.ko

Userspace

Kernel

Syscalls (open, write, ioctl, …)

47 of 70

Kernel inputs: external

vmlinux

module.ko

Hardware / Firmware

Userspace

Kernel

Network packets, USB devices, ...

48 of 70

  • Unlike syscalls, can not simply execute a binary
  • Inject either from userspace or through hypervisor
  • Userspace
    • Network: /dev/tun
    • USB: /dev/raw-gadget + Dummy UDC/HCD
  • Hypervisor/emulator
    • USB: QEMU + usbredir (vUSBf)

Injecting external inputs

49 of 70

  • Not all syscalls work as straightforward API
  • Or accept simple structures as arguments
  • clone, sigaction
    • API with callbacks?
  • eBPF, KVM (also netfilter?)
    • Need to generate valid code
    • Script-aware fuzzing? (Something like fuzzilli?)

Input structure: unusual syscalls

50 of 70

  • Network packets
    • Might seem like blobs
    • More like API due to TCP SYN/ACK numbers, SCTP cookies, ...
  • USB (also FUSE?) is weird
    • Host-driven communication
    • The fuzzer is responding to API calls
    • Not knowing which call will be next

Input structure: external

51 of 70

  • Compiler instrumentation
    • KCOV
    • Other hacks piggy-backing on top of GCC/Clang
  • Emulators
    • TriforceAFL via QEMU
    • Unicorefuzz via Unicorn
  • Hardware tracing features
    • kAFL via Intel PT

Collecting code coverage

52 of 70

  • Collecting coverage for the current task works in many cases

​

  • But relevant code might be executed in a different context
    • Example: syscall uses workers to process input
    • Example: USB control packets are processed in global threads

​

  • KCOV supports collecting coverage from background threads and interrupts via custom annotations

Relevant code coverage

53 of 70

  • Code coverage is not the only relevant guidance signal
    • Memory state
    • Object state
    • ...

Beyond code coverage

54 of 70

  • Random binary data works poorly as inputs
  • So what should we do?
  • Generate better inputs, duh
  • How?
  • Structured inputs (a.k.a. structure-aware fuzzing)
  • Coverage-guided generation (a.k.a coverage-guided fuzzing)
  • [Discussed later]

Better inputs

55 of 70

  • Random binary data works poorly as inputs
  • So what should we do?
  • Generate better inputs, duh
  • How?
  • Structured inputs (a.k.a. structure-aware fuzzing)
  • Coverage-guided generation (a.k.a coverage-guided fuzzing)
  • Collect a corpus of sample inputs and mutate them
  • Moonshine uses strace

Collecting a corpus of samples

56 of 70

  • Modify existing bug detectors
    • KASAN annotations for custom allocators (mempool)
    • Add info-leak checks for KMSAN for external buses (for USB)
  • Write your own bug-detectors
    • Simple BUG_ON() assertions
    • More intricate checks for logical bugs

Detecting more bugs

57 of 70

Fuzzing approaches

58 of 70

  • Reusing a userspace fuzzer
  • Using syzkaller
  • Writing a fuzzer from scratch

Fuzzing approaches

59 of 70

  • Take a userspace fuzzer (AFL, libFuzzer, …)
  • Interact with the kernel instead of calling into a userspace library
  • Or run kernel code in userspace

​

  • Works fine for fuzzing blob-like inputs: filesystem images, netlink, etc.
  • Other kinds of inputs => Need custom generators/mutators
  • Need to plug kernel coverage into the fuzzer for coverage-guidance

​

Reusing a userspace fuzzer

60 of 70

  • See syzkaller talks for usage
  • Good for fuzzing API-based interfaces out-of-the-box
  • Custom language to describe API/structures (syzlang)
  • Tip #1: Do not just fuzz mainline with the default config
    • Add new descriptions
    • Tighten attack surface: fuzz a small number of related syscalls
    • Fuzz distro kernels

Using syzkaller

61 of 70

  • Tip #3: Use syzkaller as a framework
    • Only use crash parsing code
    • Only use VM management code
    • ...

syzkaller is extensible

62 of 70

  • Great way to learn
  • Might be beneficial for targeted fuzzing
  • Or if the interface is not API-based

Writing a fuzzer from scratch

63 of 70

Fuzzing tips

64 of 70

  • Check code coverage, make sure you cover the targeted layer

​

  • Inject bugs (WARN_ON()/BUG_ON()) and check that fuzzer finds them

​

  • Revert fixes for bugs/CVEs and check that fuzzer finds them

Is my fuzzer good?

65 of 70

  • Understand the code you are fuzzing
    • What kind of inputs it expects
    • Which part you are trying to target
  • Write a fuzzer based on that
    • Writing fuzzer based on specs/docs does not work well

Read the code

66 of 70

  • Fast fuzzer
    • More execs/sec
  • Smart fuzzer
    • Better input generation, more relevant guidance signal, etc.
  • Focus on smart first
    • Formal investigation would be interesting; related paper and discussion

Fast vs smart

67 of 70

Final note

68 of 70

  • Based on engineering skills
    • Designing systems
    • Writing code
    • Testing and debugging
    • Benchmarking
  • => You need basic programming skills to get started
  • => You need decent engineering skills to excel

Writing fuzzers is engineering

69 of 70

​

  • Telegram channel with links on Linux kernel security: t.me/linkersec

Linux kernel fuzzing materials

70 of 70

Thank you for your attention!

Andrey Konovalov, xairy.io

�May 20th 2021