Table Of Contents
Introduction
One of Linus TORVALDS famous quote is :
Debugging Linux kernel is different from debugging normal executable : normal applications (i.e process) always runs in security ring 3 ( user-mode layer), when the process invoke access to hardware resources, the request is sent to the kernel (security ring 0) via system calls and the kernel manage to do the task with low-level functions. To sum up, security rings exists to restrict privileges, and the kernel-mode plays the role of middleware between the hardware and the user-mode.
Usually when we use gdb
For user-mode : gdb
allow inspecting what is going inside an executable on run-time execution.
For kernel-mode : gdb
allow debugging the low-level system call functions and module loaded.
Set up Kernel Image
Typically, when we debug or exploit a kernel module we do that either on Virtual Machine or on an emulator like QEMU. (A simple bad handling can crash the host kernel and we don’t want to deal with PANIC errors). QEMU can do perfectly the job, so :
|
|
Buildroot is often used for building embedded systems that target processors other than the regular x86, like ARM and MIPS processors. In our use-case, we will focus only on the x86 architecture. All build process has been conducted on :
- Linux distribution release: Ubuntu 16.04 LTS
- Kernel version: 4.15.0–55
First, download and extract the source files from the downloaded tar archive :
|
|
1 ] Building Part
You’re free to make your choice :
Build for 64-bit
:
|
|
- In
Target options
>Target architecture
>x86_64
- In
Build options
> selectbuild packages with debugging symbols
- In
Build options
>build packages with debugging symbols
>gcc debug level and
select level 3
- In
Build options
>build packages with debugging symbols
>strip command for binaries on target
and selectnone
- In
Build options
>build packages with debugging symbols
>gcc optimization level
and selectoptimize for debugging
For building, run the command line:
|
|
Build for 32-bit
:
|
|
- In
Target options
>Target architecture
>i386
And the configuration of Build options is the same as 64-bit
2 ] Kernel Configuration Part
Run the command below for linux configuration :
|
|
- In
Kernel hacking
> selectKernel debugging
- In Kernel hacking >
Compile-time checks and compiler options
> selectCompile kernel with debug info
and selectProvide GDB scripts for kernel debugging
Or, we can just run the command line:
|
|
For building, run the command line:
|
|
3 ] Booting with QEMU
If all goes well, the compiled Linux kernel image should boot with QEMU. For booting the image with QEMU run:
|
|
Remember: the argument -s
refers to a shorthand for -gdb tcp::1234
.
Congratulations, the first step done!! 🎉 🎉
Basic Kernel Debugging
Now that we can boot the Linux kernel image in QEMU, let’s start gdb
and attach to gdb stub of QEMU gdbsever
running on the kernel image on default port :1234
An additional step left before begin debugging, by default Ubuntu restrict auto-loading of GDB scripts: remember in “Building Kernel Step”, we have configured the kernel to provide GDB scripts for kernel debugging, those scripts are useful helper when debugging kernel modules.
Those scripts are located in ./output/build/linux-<version>/scripts/gdb/
We have to allow loading scripts by adding the line below into the config file of gdb located in ~/.gdbinit
|
|
From now on, the kernel image is paused, we can resume the qemu kernel with gdb command c
, and pause it again with the shortcut Ctrl+c
.
In order to understand the basics core concepts of the Linux kernel, first and foremost we should cover the concept of process management.
Process Management
The two of the most important structures in the kernel are struct thread_info
and struct task_struct
.
From the kernel point of view about process descriptors:
- the user-space process is a task and the kernel allocates one
task_struct
object on memory for each task - With user-space and kernel-space threads, the kernel allocates one
task_struct
object for every thread running.
The structure objects are allocated with the memory management mechanism, either we can use SLAB or SLUB allocator. SLAB/SLUB use the model of object caching to reduce memory fragmentation caused by allocations and deallocations operations.
By default, the memory management used when building kernel is SLUB: the next generation of SLAB that improves performance.
To print the description of the structure thread_info
:
|
|
The thread_info
struct hold 2 important fields:
- task’s stack pointer
addr_limit
used to separate between user and kernel spaces
The main purpose of the global variable addr_limit
is to allow unprivileged functions to read or write from or to kernel-space memory.
Let’s take an example:
The executable ifconfig
use ioctl
to set and get the configuration of the network device.
The kernel-space use ./net/ipv4/ipconfig.c
module to configure the network device, then use the function devinet_ioctl()
to create an info request struct ifreq ifr
structure and copies data from user to kernel space with copy_from_user()
To transfer data from user to kernel space the function copy_from_user()
must be used in order to access the user-space pointer (in this case void __user *arg ;
the kernel uses __user
to identify pointers of user-space).
Before calling copy_from_user()
we should set the value of the variable addr_limit
.
The sequence code temporarily raises addr_limit
so that any function with copy_from_user()
, which is normally restricted reading data into user-space memory, can read write into a kernel-space pipe buffer:
|
|
GDB in Action
Get the value of addr_limit
using gdb,
When we tape ifconfig
for example as a command line, the shell fork()
, execve()
and wait()
the process. execve()
uses the system call sys_execve()
as an entry point which does reference to the function do_execve()
(execve() -> sys_execve() -> do_execve()
).
The gdb command info functions
will display all the loaded symbols of the kernel, for a matching regex like execve : info functions execve
Note
Kernel modules don’t execute sequentially as applications do, most actions performed by the kernel are related to a specific task. Kernel code can know the current task driving it by accessing the macro current, a pointer to the struct task_struct
. The current pointer refers to the user process (task) currently executing. During the execution of a system call, such as open or read, the current process is the one that invoked the call. Kernel code can use process-specific information by using current, if it needs to do so.
The thread_info
structure lives at the bottom of the kernel stack, it can be acquired by calculating the address of the RSP register:
|
|
The kernel stack … is fixed in size. The exact size of the kernel’s stack varies by architecture. On x86, the stack size is configurable at compile time and can be either 4KB or 8KB. Historically, the kernel stack is two pages, which generally means that it is 8KB on 32-bit architectures and 16KB on 64-bit architectures - this size is fixed and absolute. Each process receives its own stack…
In order to feel reassured, compile and execute the following C program:
|
|
The kernel stack output:
|
|
After confirming the thread size needed for accessing the thread_info
structure, with the gdb commands bellow we can access the structure fields :
Or, just using the loaded functions of the gdb scripts helper, we can recover the same result:
The field [addr_limit] between the Kernel and User Space
- - - -
task_struct
Structure
task_struct contains all the information about the current task,
|
|
The task pointer in thread_info
is actually the pointer to the task_struct
structure, and the stack pointer in task_struct
refers to the thread_info
structure. Thus, if we have one of the 2 pointers (cf. task_struct
or thread_info
), we can access the other.
For example, the structure cred
can be used as a part of security check performed by the kernel upon Linux objects (tasks, files, …).
The task credentials holds uid=0
gid=O
as the user is root :
thread_info
Clean up
For kernel version 4.8 or later, the structure thread_info
has been cleaned up, most of thread_info
fields were moved into other kernel structures: addr_limit
has been moved into thread_struct