Initramfs on linux & Qemu setup
how to extract initramfs image on archlinux
About
I was following the book “Linux Kernel Programming 2nd Edition” to study the kernel. One of the chapters had a section about initramfs, which I was quite familiar with, having used it several times in kernel exploits. But for better understanding, I wanted to extract the current OS’s initramfs image and analyze it. But it seemed that there were some differences in the initramfs image compared to what I was used to in CTF challenges.
Also, I appended a section about setting up a QEMU environment on Arch Linux to run Ubuntu, which I used for building the kernel.
Usual way to extract initramfs
In a nicely configured “challenge”, the usual way to extract the initramfs image is like this:
1
2
3
mkdir initramfs
cd initramfs
cpio -idv < ../initramfs.img
This is the way I would have usually done it. Not a big deal, just extract it using cpio.
However, when I tried this on Arch Linux, the extracted image seemed a bit off.
1
2
$ ls
bin early_cpio lib lib64 sbin usr var
The extracted image only had a few directories like bin, lib, usr, var, etc. And init was missing as well.
Linux’s initramfs format
After some research, I found out that actual distros use a different format for the initramfs image. It uses layers or multiple cpio archives concatenated together and compressed using gzip or other compression algorithms. For example, to extract Arch Linux’s initramfs image, use
1
2
3
mkdir -p initramfs
cd initramfs
lsinitcpio -x /boot/initramfs-linux.img
For the case of Ubuntu,
1
2
3
mkdir -p initramfs
cd initramfs
unmkinitramfs /boot/initrd.img-$(uname -r) initramfs
From there, the expected files such as init and other scripts were present.
ASCII cpio archive (SVR4 with no CRC)" followed by garbage
Implies that the file is not a single cpio archive, but rather a concatenation of multiple cpio archives, possibly with compression applied.
Having a look
Ubuntu
1
2
$ ls
early early2 main
This is Ubuntu’s initramfs structure.
- early: CPU microcode update blobs. NOT an initramfs, but a patcher for the CPU microcode.
early/kernel/x86/microcode/AuthenticAMD.bin - early2: Ubuntu-specific initramfs scripts and binaries.
- main: The initramfs we want. Contains the actual initramfs files and scripts.
Arch Linux
1
2
3
$ ls
VERSION buildconfig dev etc init keymap.bin lib new_root run sys usr
bin config early_cpio hooks init_functions keymap.utf8 lib64 proc sbin tmp var
It gets extracted immediately without any subdirectories.
Extra notes
- Nowadays, sbin/init is replaced with systemd in most distros.
- Arch Linux uses busybox to initialize the system during boot.
- Ubuntu uses scripting to initialize the system during boot.
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
$ cat init # ubuntu's init script
#!/bin/sh
# Default PATH differs between shells, and is not automatically exported
# by klibc dash. Make it consistent.
# Furthermore, this PATH ends up being used by the init, set it to the
# Standard PATH, without /snap/bin as documented in
# https://wiki.ubuntu.com/PATH
# This also matches /etc/environment, but without games path
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ -d /dev ] || mkdir -m 0755 /dev
[ -d /root ] || mkdir -m 0700 /root
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
...
# Move virtual filesystems over to the real filesystem
mount -n -o move /sys ${rootmnt}/sys
mount -n -o move /proc ${rootmnt}/proc
# Chain to real filesystem
# shellcheck disable=SC2086,SC2094
exec run-init ${drop_caps} "${rootmnt}" "${init}" "$@" <"${rootmnt}/dev/console" >"${rootmnt}/dev/console" 2>&1
echo "Something went badly wrong in the initramfs."
panic "Please file a bug on initramfs-tools."
Appendix: dev environment setup on Arch Linux
Use them as needed.
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
#!/bin/bash
set -euo pipefail
# Variables
PROJECT_DIR="YOUR_PROJECT_PATH_HERE" # Set this to your project directory path
VM_DIR="${PROJECT_DIR}/vm/QEMU" # Directory to store VM images
IMG_NAME="ubuntu-desktop-22.04.qcow2" # Name of the VM image
CD_PATH="${PROJECT_DIR}/iso/ubuntu-22.04-desktop-amd64.iso" # Path to Ubuntu ISO
SSH_PORT=2222
# VM will be saved under VM_DIR
mkdir -p "${VM_DIR}"
# adjust image size as needed.
qemu-img create -f qcow2 "${VM_DIR}/${IMG_NAME}" 20G
qemu-system-x86_64 \
-m 4096 \
-smp 2 \
-drive file="${VM_DIR}/${IMG_NAME}",if=virtio,format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \
-device e1000,netdev=net0 \
-cdrom "${CD_PATH}" \
-enable-kvm \
-cpu host
# Be sure to create swap file or partition during installation!!
# It may run out of memory otherwise.
#
# If you want to mount a directory from host, add the following lines:
# -virtfs local,path="${PROJECT_DIR}/src",mount_tag=hostsrc,security_model=none \
# Note that kernel should NOT be built under the shared directory to avoid mmap related issues.
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
#!/bin/bash
set -euo pipefail
# Variables
PROJECT_DIR="YOUR_PROJECT_PATH_HERE" # Set this to your project directory path
VM_DIR="${PROJECT_DIR}/vm/QEMU" # Directory to store VM images
IMG_NAME="ubuntu-desktop-22.04.qcow2" # Name of the VM image
# Resources
CPU_COUNT=8
RAM_SIZE=4G
SSH_PORT=2222
echo "allocating CPU cores: ${CPU_COUNT}"
echo "allocating RAM size: ${RAM_SIZE}"
qemu-system-x86_64 \
-m ${RAM_SIZE} \
-smp ${CPU_COUNT} \
-drive file="${VM_DIR}/${IMG_NAME}",if=virtio,format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \
-device e1000,netdev=net0 \
-enable-kvm \
-cpu host \
-nographic
# You may have to disable nographic mode to troubleshoot VM related issues.
# Just remove the -nographic option above.
#
# Access via ssh:
# ssh -p 2222 user@localhost
How to build a kernel inside the VM
The flow is as follows:
- install required packages
- clone linux repo
- checkout desired version
- configure the kernel. I used localmodconfig for simplicity.
- build the kernel
- install the kernel modules (
make modules_install) - install the kernel (
make install) - configure bootloader (grub,
/etc/default/grubandsudo update-grub)