citadel/scripts/extract-initramfs.sh

191 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
#
# extract-initramfs:
#
# Extracts and decompresses kernel binary (vmlinux) from a bzImage then searches
# the extracted kernel for an initramfs cpio archive.
#
# Assumes kernel is compressed with LZ4 and that embedded cpio archive is not compressed.
#
usage() {
cat <<-EOF
USAGE: extract-initramfs [options] [kernel bzImage]
OPTIONS
-d <directory-name> Choose a non-default directory for extracted initramfs (default ${PWD}/initramfs)
EOF
exit 0
}
info() {
printf "[+] ${1}\n"
}
error() {
>&2 echo "[!] ${1}"
exit 1
}
REQUIRED_UTILS="cpio lz4 readelf"
confirm_required_utils() {
local required=${1}
local missing=""
for util in ${required}; do
which ${util} > /dev/null || missing="${missing}${util} "
done
if [[ -n ${missing} ]]; then
error "Required utilities are not installed: ${missing}"
fi
}
confirm_required_utils "${REQUIRED_UTILS}"
CPIO_HEADER='070701'
LZ4_HEADER='\x02!L\x18'
img=""
workdir=$(mktemp -d /tmp/initramfs-extract.XXXX)
vmlinux=${workdir}/vmlinux
outdir=${PWD}/initramfs
trap "rm -rf $workdir" 0
#
# Search for $pattern in $infile and output a list of offsets where the pattern is found
#
binary_grep() {
local pattern=$1 infile=$2
grep --only-matching --byte-offset --text --perl-regexp ${pattern} ${infile} | cut -d: -f1
}
#
# Output $infile starting at byte position $offset
#
binary_tail() {
local offset=$1 infile=$2
tail -c +$(($offset + 1)) ${infile}
}
check_elf() {
readelf -h ${1} > /dev/null 2>&1
}
#
# Search for $pattern in $img file and for each offset found, attempt to decompress
# the file starting at that position by piping through $cmdline
#
try_decompress() {
local pattern=$1 cmdline=$2
for pos in $(binary_grep ${pattern} ${img}); do
printf "Testing offset %08x\n" $pos
binary_tail $pos $img | ${cmdline} > ${vmlinux} 2> /dev/null
check_elf ${vmlinux} && return 0
done
return 1
}
#
# Add more patterns and command lines from extract-vmlinux script in linux kernel source tree
# to support more compression formats
#
decompress_kernel() {
try_decompress $LZ4_HEADER 'lz4 -d' && return 0
error "Failed to decompress kernel"
}
#
# CPIO format is a list of file entries where each entry begins with a header that contains only
# ASCII hexadecimal digits. To avoid false positives matching only the magic value, check to make
# sure that some of the the bytes following the magic field are ASCII hex digits.
#
# From https://www.systutorials.com/docs/linux/man/5-cpio/
#
# New ASCII Format
#
# The "new" ASCII format uses 8-byte hexadecimal fields for all numbers and separates device numbers
# into separate fields for major and minor numbers.
#
# struct cpio_newc_header {
# char c_magic[6];
# char c_ino[8];
# char c_mode[8];
# char c_uid[8];
# char c_gid[8];
# char c_nlink[8];
# char c_mtime[8];
# char c_filesize[8];
# char c_devmajor[8];
# char c_devminor[8];
# char c_rdevmajor[8];
# char c_rdevminor[8];
# char c_namesize[8];
# char c_check[8];
# };
#
confirm_cpio() {
# Check that there are 100 ascii hex digit characters at offset
local offset=${1} checklen=100
dd if=${vmlinux} bs=1 count=${checklen} skip=${offset} 2> /dev/null | grep -q "[[:xdigit:]]\{${checklen}\}"
}
extract_cpio() {
cpio --make-directories --extract --no-absolute-filenames -D ${outdir} 2> /dev/null
}
extract_initramfs() {
for pos in $(binary_grep ${CPIO_HEADER} ${vmlinux}); do
printf "Found CPIO: %08x\n" $pos
if confirm_cpio ${pos}; then
mkdir ${outdir}
binary_tail $pos ${vmlinux} | extract_cpio
info "Initramfs files extracted to ${outdir}"
exit 0
fi
done
}
while [[ $# -gt 0 ]]; do
key=${1}
case $key in
-d)
outdir="$(realpath ${2})"
shift
shift
;;
-h|--help)
usage
;;
-*)
printf "Unknown option ${key}\n"
usage
;;
*)
img=${1}
shift
;;
esac
done
[[ -z ${img} ]] && usage
[[ -f ${img} ]] || error "kernel image file ${img} does not exist"
if [[ -d ${outdir} ]]; then
error "Output directory ${outdir} already exists. Remove it or choose a different directory with -d option."
fi
decompress_kernel
extract_initramfs