Skip to main content

memf_linux/
kaslr.rs

1//! KASLR offset detection for Linux kernels.
2//!
3//! Scans physical memory for the `"Linux version "` banner string.
4//! The banner's physical address, combined with the known virtual address
5//! of `linux_banner` from the symbol table, yields the KASLR slide.
6
7use memf_format::PhysicalMemoryProvider;
8use memf_symbols::SymbolResolver;
9
10use crate::{Error, Result};
11
12/// The banner prefix to search for in physical memory.
13const BANNER_PREFIX: &[u8] = b"Linux version ";
14
15/// x86_64 kernel text mapping base (`__START_KERNEL_map`).
16const KERNEL_MAP_BASE: u64 = 0xFFFF_FFFF_8000_0000;
17
18/// Detect the KASLR offset by scanning for the Linux banner string.
19///
20/// Returns the KASLR slide (0 if KASLR is disabled).
21pub fn detect_kaslr_offset(
22    physical: &dyn PhysicalMemoryProvider,
23    symbols: &dyn SymbolResolver,
24) -> Result<u64> {
25    let banner_symbol_vaddr =
26        symbols
27            .symbol_address("linux_banner")
28            .ok_or_else(|| Error::MissingKernelSymbol {
29                name: "linux_banner".into(),
30            })?;
31
32    let banner_phys = scan_for_banner(physical)?;
33
34    let actual_virt = banner_phys.wrapping_add(KERNEL_MAP_BASE);
35    let offset = actual_virt.wrapping_sub(banner_symbol_vaddr);
36
37    Ok(offset)
38}
39
40/// Scan physical memory for the `"Linux version "` banner string.
41fn scan_for_banner(physical: &dyn PhysicalMemoryProvider) -> Result<u64> {
42    let mut buf = vec![0u8; 4096];
43
44    for range in physical.ranges() {
45        let mut addr = range.start;
46        while addr < range.end {
47            let to_read = ((range.end - addr) as usize).min(buf.len());
48            let n =
49                physical
50                    .read_phys(addr, &mut buf[..to_read])
51                    .map_err(|e| Error::WalkFailed {
52                        walker: "scan_for_banner",
53                        reason: format!("physical read error: {e}"),
54                    })?;
55            if n == 0 {
56                break;
57            }
58
59            if let Some(pos) = find_subsequence(&buf[..n], BANNER_PREFIX) {
60                return Ok(addr + pos as u64);
61            }
62
63            // Overlap by BANNER_PREFIX.len() to catch cross-boundary matches
64            if n > BANNER_PREFIX.len() {
65                addr += (n - BANNER_PREFIX.len()) as u64;
66            } else {
67                addr += n as u64;
68            }
69        }
70    }
71
72    Err(Error::WalkFailed {
73        walker: "scan_for_banner",
74        reason: "Linux banner string not found in physical memory".into(),
75    })
76}
77
78fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
79    haystack.windows(needle.len()).position(|w| w == needle)
80}
81
82/// Apply a KASLR offset to a symbol address.
83#[must_use]
84pub fn adjust_address(original: u64, kaslr_offset: u64) -> u64 {
85    original.wrapping_add(kaslr_offset)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use memf_format::PhysicalRange;
92    use memf_symbols::isf::IsfResolver;
93    use memf_symbols::test_builders::IsfBuilder;
94
95    struct BannerPhysMem {
96        data: Vec<u8>,
97        ranges: Vec<PhysicalRange>,
98    }
99
100    impl PhysicalMemoryProvider for BannerPhysMem {
101        fn read_phys(&self, addr: u64, buf: &mut [u8]) -> memf_format::Result<usize> {
102            let start = addr as usize;
103            if start >= self.data.len() {
104                return Ok(0);
105            }
106            let available = self.data.len() - start;
107            let to_read = buf.len().min(available);
108            buf[..to_read].copy_from_slice(&self.data[start..start + to_read]);
109            Ok(to_read)
110        }
111
112        fn ranges(&self) -> &[PhysicalRange] {
113            &self.ranges
114        }
115
116        fn format_name(&self) -> &str {
117            "Test"
118        }
119    }
120
121    #[test]
122    fn detect_no_kaslr() {
123        let banner_phys: u64 = 0x0200_0000;
124        let banner_vaddr: u64 = 0xFFFF_FFFF_8200_0000;
125
126        let mut data = vec![0u8; (banner_phys as usize) + 4096];
127        let banner = b"Linux version 5.15.0-generic";
128        data[banner_phys as usize..banner_phys as usize + banner.len()].copy_from_slice(banner);
129
130        let mem = BannerPhysMem {
131            ranges: vec![PhysicalRange {
132                start: 0,
133                end: data.len() as u64,
134            }],
135            data,
136        };
137
138        let isf = IsfBuilder::new()
139            .add_symbol("linux_banner", banner_vaddr)
140            .build_json();
141        let resolver = IsfResolver::from_value(&isf).unwrap();
142
143        let offset = detect_kaslr_offset(&mem, &resolver).unwrap();
144        assert_eq!(offset, 0);
145    }
146
147    #[test]
148    fn detect_with_kaslr() {
149        let kaslr_slide: u64 = 0x0060_0000;
150        let default_banner_vaddr: u64 = 0xFFFF_FFFF_8200_0000;
151        let banner_phys: u64 = 0x0260_0000;
152
153        let mut data = vec![0u8; (banner_phys as usize) + 4096];
154        let banner = b"Linux version 6.1.0-kaslr";
155        data[banner_phys as usize..banner_phys as usize + banner.len()].copy_from_slice(banner);
156
157        let mem = BannerPhysMem {
158            ranges: vec![PhysicalRange {
159                start: 0,
160                end: data.len() as u64,
161            }],
162            data,
163        };
164
165        let isf = IsfBuilder::new()
166            .add_symbol("linux_banner", default_banner_vaddr)
167            .build_json();
168        let resolver = IsfResolver::from_value(&isf).unwrap();
169
170        let offset = detect_kaslr_offset(&mem, &resolver).unwrap();
171        assert_eq!(offset, kaslr_slide);
172    }
173
174    #[test]
175    fn missing_linux_banner_symbol_returns_missing_kernel_symbol() {
176        let data = vec![0u8; 4096];
177        let mem = BannerPhysMem {
178            ranges: vec![PhysicalRange {
179                start: 0,
180                end: 4096,
181            }],
182            data,
183        };
184        // No linux_banner symbol
185        let isf = IsfBuilder::new().build_json();
186        let resolver = IsfResolver::from_value(&isf).unwrap();
187        let result = detect_kaslr_offset(&mem, &resolver);
188        assert!(
189            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "linux_banner"),
190            "expected MissingKernelSymbol {{name: \"linux_banner\"}}, got {result:?}"
191        );
192    }
193
194    #[test]
195    fn no_banner_found() {
196        let data = vec![0u8; 4096];
197        let mem = BannerPhysMem {
198            ranges: vec![PhysicalRange {
199                start: 0,
200                end: 4096,
201            }],
202            data,
203        };
204
205        let isf = IsfBuilder::new()
206            .add_symbol("linux_banner", 0xFFFF_FFFF_8200_0000)
207            .build_json();
208        let resolver = IsfResolver::from_value(&isf).unwrap();
209
210        let result = detect_kaslr_offset(&mem, &resolver);
211        assert!(result.is_err());
212    }
213
214    #[test]
215    fn adjust_address_with_offset() {
216        let original = 0xFFFF_FFFF_8260_0000u64;
217        let kaslr = 0x0060_0000u64;
218        let adjusted = adjust_address(original, kaslr);
219        assert_eq!(adjusted, 0xFFFF_FFFF_82C0_0000);
220    }
221
222    #[test]
223    fn find_subsequence_basic() {
224        let haystack = b"hello world Linux version 5.15";
225        let needle = b"Linux version ";
226        assert_eq!(find_subsequence(haystack, needle), Some(12));
227    }
228
229    #[test]
230    fn find_subsequence_not_found() {
231        let haystack = b"no banner here";
232        let needle = b"Linux version ";
233        assert_eq!(find_subsequence(haystack, needle), None);
234    }
235}