Skip to main content

lightswitch_object/
kernel.rs

1use anyhow::anyhow;
2use object::Endianness;
3use object::FileKind;
4use object::ReadCache;
5use object::elf::{ELF_NOTE_GNU, FileHeader32, FileHeader64, NT_GNU_BUILD_ID, PT_NOTE};
6use object::read::elf::FileHeader;
7use object::read::elf::NoteIterator;
8use object::read::elf::ProgramHeader;
9use std::fs::File;
10
11use crate::BuildId;
12
13const KCORE_PATH: &str = "/proc/kcore";
14const VMCORE_INFO_NAME: &[u8] = b"VMCOREINFO";
15const KERNEL_OFFSET: &[u8] = b"KERNELOFFSET";
16
17/// Parse the GNU build id from the ELF notes section.
18pub fn parse_gnu_build_id_from_notes(data: &[u8]) -> Result<BuildId, anyhow::Error> {
19    let notes: NoteIterator<'_, FileHeader32<Endianness>> =
20        NoteIterator::new(Endianness::Little, 4, data)?;
21
22    for note in notes {
23        let Ok(note) = note else {
24            continue;
25        };
26
27        let name = note.name();
28        let ntype = note.n_type(Endianness::Little);
29
30        if name != ELF_NOTE_GNU || ntype != NT_GNU_BUILD_ID {
31            continue;
32        }
33
34        return Ok(BuildId::gnu_from_bytes(note.desc())?);
35    }
36
37    Err(anyhow!("no GNU build id note found"))
38}
39
40/// Read KASLR information extracted off the notes of the vmlinux corefile.
41fn _parse_vm_core_info_line(data: &[u8]) -> impl Iterator<Item = (&[u8], &[u8])> {
42    data.split(|&e| e == b'\n').filter_map(|key_val| {
43        let mut split = key_val.split(|&e| e == b'=');
44        match (split.next(), split.next()) {
45            (Some(a), Some(b)) => Some((a, b)),
46            (_, _) => None,
47        }
48    })
49}
50
51/// Extract the KASLR offset from the running vmlinux.
52pub fn kaslr_offset() -> anyhow::Result<u64> {
53    let data = ReadCache::new(File::open(KCORE_PATH)?);
54
55    match FileKind::parse(&data) {
56        Ok(FileKind::Elf64) => {
57            let header: &FileHeader64<Endianness> = FileHeader64::<Endianness>::parse(&data)?;
58            let endian = header.endian()?;
59            let headers = header.program_headers(endian, &data)?;
60
61            for header in headers {
62                if header.p_type(endian) != PT_NOTE {
63                    continue;
64                }
65
66                let notes: NoteIterator<'_, FileHeader64<Endianness>> = NoteIterator::new(
67                    Endianness::Little,
68                    header.p_align(endian),
69                    header
70                        .data(endian, &data)
71                        .map_err(|_| anyhow!("invalid header data"))?,
72                )?;
73
74                for note in notes {
75                    let Ok(note) = note else {
76                        continue;
77                    };
78
79                    if note.name() == VMCORE_INFO_NAME {
80                        let found = _parse_vm_core_info_line(note.desc())
81                            .find(|(key, _val)| key == &KERNEL_OFFSET)
82                            .map(|(_key, val)| val);
83
84                        return Ok(
85                            // This entry is stored in hex-encoded ascii. It could be converted in
86                            // one go but this is not performance
87                            // sensitive as it runs once. It's ok to take 2 hops
88                            // to convert it rather than hand rolling it or bringing another
89                            // dependency.
90                            u64::from_str_radix(std::str::from_utf8(found.unwrap())?, 16)?,
91                        );
92                    }
93                }
94            }
95        }
96        Ok(_) => {
97            todo!("only 64 bit ELF kcore is supported")
98        }
99        Err(_) => {}
100    }
101
102    Err(anyhow!("could not find the kASLR offset"))
103}
104
105#[cfg(test)]
106mod tests {
107    use std::fs::File;
108    use std::io::Read;
109
110    use crate::kernel::*;
111    use crate::*;
112
113    #[test]
114    fn test_parse_gnu_build_id_from_notes() {
115        let mut file = File::open("src/testdata/fedora-kernel-notes").unwrap();
116        let mut data = Vec::new();
117        file.read_to_end(&mut data).unwrap();
118
119        assert_eq!(
120            parse_gnu_build_id_from_notes(&data).unwrap(),
121            BuildId {
122                flavour: buildid::BuildIdFlavour::Gnu,
123                data: vec![
124                    184, 215, 12, 245, 25, 250, 197, 165, 204, 205, 218, 26, 97, 195, 137, 149,
125                    189, 155, 48, 89
126                ],
127            }
128        );
129    }
130
131    #[test]
132    fn test_parse_vm_core_info_line() {
133        let data = b"OSRELEASE=6.12.8-100.fc40.x86_64
134BUILD-ID=0730dd9e6b959a79e0797de379bd078c3792ea98
135PAGESIZE=4096
136SYMBOL(init_uts_ns)=ffffffff9ebdefa0
137OFFSET(uts_namespace.name)=0
138SYMBOL(node_online_map)=ffffffff9ec48420
139SYMBOL(swapper_pg_dir)=ffffffff9e82a000
140SYMBOL(_stext)=ffffffff9c000000
141NUMBER(VMALLOC_START)=0xffffa5c140000000
142SYMBOL(vmemmap)=ffffd90a40000000
143SYMBOL(mem_section)=ffff8f3a6dfcd2c0
144LENGTH(mem_section)=4096
145SIZE(mem_section)=32
146OFFSET(mem_section.section_mem_map)=0
147NUMBER(SECTION_SIZE_BITS)=27
148NUMBER(MAX_PHYSMEM_BITS)=46
149SIZE(page)=64
150SIZE(pglist_data)=175424
151SIZE(zone)=1728
152SIZE(free_area)=104
153SIZE(list_head)=16
154SIZE(nodemask_t)=128
155OFFSET(page.flags)=0
156OFFSET(page._refcount)=52
157KERNELOFFSET=1b000000
158NUMBER(KERNEL_IMAGE_SIZE)=1073741824
159NUMBER(sme_mask)=0";
160        assert_eq!(
161            _parse_vm_core_info_line(data).find(|(k, _v)| k == b"KERNELOFFSET"),
162            Some(("KERNELOFFSET".as_bytes(), "1b000000".as_bytes()))
163        );
164    }
165
166    #[test]
167    fn test_aslr_offset() {
168        assert!(kaslr_offset().is_ok());
169    }
170}