rnicro 0.3.0

A Linux x86_64 debugger and exploit development toolkit written in Rust
Documentation
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! ASLR/PIE leak calculator and libc offset database.
//!
//! Helpers for computing base addresses from information leaks,
//! partial overwrite analysis, and libc version identification.

use crate::error::{Error, Result};
use std::collections::HashMap;

/// ASLR/PIE base address calculator.
///
/// Tracks computed base addresses for different memory regions
/// (e.g., "binary", "libc", "heap") and resolves symbol addresses.
#[derive(Debug, Clone)]
pub struct AslrCalculator {
    /// Known symbol offsets: name -> file offset.
    known_offsets: HashMap<String, u64>,
    /// Computed base addresses: region -> base.
    bases: HashMap<String, u64>,
}

impl Default for AslrCalculator {
    fn default() -> Self {
        Self::new()
    }
}

impl AslrCalculator {
    pub fn new() -> Self {
        Self {
            known_offsets: HashMap::new(),
            bases: HashMap::new(),
        }
    }

    /// Calculate base address from a leaked runtime address and known offset.
    ///
    /// `region` is a label like "libc" or "binary".
    /// `leaked_addr` is the runtime address observed.
    /// `offset` is the known file offset of the leaked symbol.
    pub fn calc_base(&mut self, region: &str, leaked_addr: u64, offset: u64) -> u64 {
        let base = leaked_addr.wrapping_sub(offset);
        self.bases.insert(region.to_string(), base);
        base
    }

    /// Get the stored base address for a region.
    pub fn get_base(&self, region: &str) -> Option<u64> {
        self.bases.get(region).copied()
    }

    /// Calculate a runtime address given a stored base and offset.
    pub fn calc_addr(&self, region: &str, offset: u64) -> Option<u64> {
        self.bases.get(region).map(|base| base.wrapping_add(offset))
    }

    /// Register a known symbol offset for later resolution.
    pub fn add_offset(&mut self, name: &str, offset: u64) {
        self.known_offsets.insert(name.to_string(), offset);
    }

    /// Resolve a symbol's runtime address using stored base and offset.
    pub fn resolve(&self, region: &str, name: &str) -> Option<u64> {
        let base = self.bases.get(region)?;
        let offset = self.known_offsets.get(name)?;
        Some(base.wrapping_add(*offset))
    }

    /// List all computed bases.
    pub fn all_bases(&self) -> &HashMap<String, u64> {
        &self.bases
    }

    /// Validate that a leaked address looks like a valid x86_64 userspace pointer.
    pub fn validate_leak(addr: u64) -> bool {
        addr > 0x1000 && addr < 0x0000_8000_0000_0000
    }

    /// Check if an address is page-aligned (4096-byte boundary).
    pub fn is_page_aligned(addr: u64) -> bool {
        addr & 0xFFF == 0
    }

    /// Extract the page offset (low 12 bits) from an address.
    pub fn page_offset(addr: u64) -> u64 {
        addr & 0xFFF
    }
}

/// Result of partial overwrite analysis.
#[derive(Debug, Clone)]
pub struct PartialOverwrite {
    /// Number of bytes being overwritten.
    pub overwrite_bytes: usize,
    /// The byte values to write (little-endian).
    pub payload: Vec<u8>,
    /// Number of random ASLR bits within the overwrite region.
    pub random_bits: u32,
    /// Probability of success per attempt (1.0 = deterministic).
    pub success_probability: f64,
    /// Expected number of attempts needed.
    pub expected_attempts: u64,
}

/// Analyze a partial overwrite scenario.
///
/// Given a target address and how many bytes we can overwrite,
/// compute the payload and probability of success under ASLR.
pub fn partial_overwrite(target_addr: u64, overwrite_bytes: usize) -> PartialOverwrite {
    assert!((1..=8).contains(&overwrite_bytes));

    let mask = if overwrite_bytes >= 8 {
        u64::MAX
    } else {
        (1u64 << (overwrite_bytes * 8)) - 1
    };

    let payload_value = target_addr & mask;
    let payload = payload_value.to_le_bytes()[..overwrite_bytes].to_vec();

    // ASLR randomizes page-aligned bases, so low 12 bits are fixed.
    // For an N-byte overwrite controlling bits 0..(N*8-1):
    // - Bits 0..11 are deterministic (page offset)
    // - Bits 12..(N*8-1) are random
    let controlled_bits = (overwrite_bytes * 8) as u32;
    let random_bits = controlled_bits.saturating_sub(12);

    let success_probability = if random_bits == 0 {
        1.0
    } else {
        1.0 / (1u64 << random_bits) as f64
    };
    let expected_attempts = if random_bits == 0 {
        1
    } else {
        1u64 << random_bits
    };

    PartialOverwrite {
        overwrite_bytes,
        payload,
        random_bits,
        success_probability,
        expected_attempts,
    }
}

/// Calculate all possible base addresses from a partial address leak.
///
/// `partial_leak`: the known low bytes of a runtime address.
/// `n_bytes`: how many bytes of the address are known (1-6).
/// `offset`: the known file offset of the leaked symbol.
pub fn brute_force_bases(partial_leak: u64, n_bytes: usize, offset: u64) -> Vec<u64> {
    assert!((1..=6).contains(&n_bytes));

    let known_mask = (1u64 << (n_bytes * 8)) - 1;
    let known_low = partial_leak & known_mask;
    let step = 1u64 << (n_bytes * 8);

    let mut bases = Vec::new();
    let mut addr = known_low;
    while addr < 0x0000_8000_0000_0000 {
        let base = addr.wrapping_sub(offset);
        if AslrCalculator::is_page_aligned(base) && base < 0x0000_8000_0000_0000 {
            bases.push(base);
        }
        addr = match addr.checked_add(step) {
            Some(a) => a,
            None => break,
        };
    }
    bases
}

/// Convenience: calculate base from a leaked function address.
pub fn base_from_leak(leaked_func: u64, func_offset: u64) -> u64 {
    leaked_func.wrapping_sub(func_offset)
}

/// A libc version entry with known symbol offsets.
#[derive(Debug, Clone)]
pub struct LibcVersion {
    /// Human-readable identifier (e.g., "libc-2.35-ubuntu22.04").
    pub id: String,
    /// BuildID hex string (from ELF .note.gnu.build-id).
    pub build_id: String,
    /// Symbol name -> file offset.
    pub symbols: HashMap<String, u64>,
}

/// In-memory libc offset database.
#[derive(Default)]
pub struct LibcDb {
    versions: Vec<LibcVersion>,
}

impl LibcDb {
    /// Create an empty database.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a libc version entry.
    pub fn add_version(&mut self, version: LibcVersion) {
        self.versions.push(version);
    }

    /// Look up a libc version by BuildID.
    pub fn lookup_by_build_id(&self, build_id: &str) -> Option<&LibcVersion> {
        self.versions.iter().find(|v| v.build_id == build_id)
    }

    /// Identify libc versions by matching the low 12 bits of a leaked symbol address.
    ///
    /// ASLR randomizes page-aligned bases, so the low 12 bits of any
    /// symbol's runtime address equal the low 12 bits of its file offset.
    pub fn identify_by_leak(&self, symbol_name: &str, leaked_addr: u64) -> Vec<&LibcVersion> {
        let low12 = leaked_addr & 0xFFF;
        self.versions
            .iter()
            .filter(|v| {
                v.symbols
                    .get(symbol_name)
                    .map(|off| off & 0xFFF == low12)
                    .unwrap_or(false)
            })
            .collect()
    }

    /// Number of entries in the database.
    pub fn len(&self) -> usize {
        self.versions.len()
    }

    /// Whether the database is empty.
    pub fn is_empty(&self) -> bool {
        self.versions.is_empty()
    }

    /// Extract symbol offsets from an ELF's dynamic symbol table.
    pub fn extract_offsets(data: &[u8]) -> Result<HashMap<String, u64>> {
        let elf =
            goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;
        let mut offsets = HashMap::new();
        for sym in &elf.dynsyms {
            if sym.st_value != 0 {
                if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
                    if !name.is_empty() {
                        offsets.insert(name.to_string(), sym.st_value);
                    }
                }
            }
        }
        Ok(offsets)
    }

    /// Extract BuildID from ELF .note.gnu.build-id section.
    pub fn extract_build_id(data: &[u8]) -> Result<String> {
        let elf =
            goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;

        for sh in &elf.section_headers {
            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
            if name == ".note.gnu.build-id" {
                let offset = sh.sh_offset as usize;
                let size = sh.sh_size as usize;
                if offset + size > data.len() || size < 16 {
                    continue;
                }
                let note_data = &data[offset..offset + size];
                let namesz = u32::from_le_bytes(note_data[0..4].try_into().unwrap()) as usize;
                let descsz = u32::from_le_bytes(note_data[4..8].try_into().unwrap()) as usize;
                let name_aligned = (namesz + 3) & !3;
                let desc_start = 12 + name_aligned;
                if desc_start + descsz <= note_data.len() {
                    let build_id = &note_data[desc_start..desc_start + descsz];
                    return Ok(build_id.iter().map(|b| format!("{:02x}", b)).collect());
                }
            }
        }
        Err(Error::Other("BuildID not found".into()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calc_base_simple() {
        let mut calc = AslrCalculator::new();
        let base = calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
        assert_eq!(base, 0x7f0000100000);
        assert!(AslrCalculator::is_page_aligned(base));
    }

    #[test]
    fn calc_addr_roundtrip() {
        let mut calc = AslrCalculator::new();
        calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
        let addr = calc.calc_addr("libc", 0x50d70).unwrap();
        assert_eq!(addr, 0x7f0000100000 + 0x50d70);
    }

    #[test]
    fn resolve_symbol() {
        let mut calc = AslrCalculator::new();
        calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
        calc.add_offset("system", 0x50d70);
        let addr = calc.resolve("libc", "system").unwrap();
        assert_eq!(addr, 0x7f0000150d70);
    }

    #[test]
    fn resolve_missing_returns_none() {
        let calc = AslrCalculator::new();
        assert!(calc.resolve("libc", "system").is_none());
    }

    #[test]
    fn validate_leak_values() {
        assert!(!AslrCalculator::validate_leak(0));
        assert!(!AslrCalculator::validate_leak(0x100));
        assert!(AslrCalculator::validate_leak(0x400000));
        assert!(AslrCalculator::validate_leak(0x7f0000000000));
        assert!(!AslrCalculator::validate_leak(0xffff800000000000)); // kernel
    }

    #[test]
    fn page_alignment_check() {
        assert!(AslrCalculator::is_page_aligned(0x400000));
        assert!(AslrCalculator::is_page_aligned(0x7f0000100000));
        assert!(!AslrCalculator::is_page_aligned(0x400001));
        assert!(!AslrCalculator::is_page_aligned(0x400aa0));
    }

    #[test]
    fn page_offset_extraction() {
        assert_eq!(AslrCalculator::page_offset(0x7f00001050d0), 0x0d0);
        assert_eq!(AslrCalculator::page_offset(0x400000), 0);
    }

    #[test]
    fn partial_overwrite_1byte() {
        let po = partial_overwrite(0x7f0000100060, 1);
        assert_eq!(po.payload, vec![0x60]);
        assert_eq!(po.random_bits, 0); // 8 bits, all within page offset
        assert_eq!(po.success_probability, 1.0);
        assert_eq!(po.expected_attempts, 1);
    }

    #[test]
    fn partial_overwrite_2byte() {
        let po = partial_overwrite(0x7f0000101060, 2);
        assert_eq!(po.payload, vec![0x60, 0x10]); // low 16 bits of 0x1060
        assert_eq!(po.random_bits, 4); // bits 12-15 are random
        assert_eq!(po.expected_attempts, 16);
    }

    #[test]
    fn brute_force_bases_small_leak() {
        // 2-byte leak of puts address (low 16 bits = 0x0aa0, offset = 0x80aa0)
        let bases = brute_force_bases(0x0aa0, 2, 0x80aa0);
        // All bases should be page-aligned
        for base in &bases {
            assert!(AslrCalculator::is_page_aligned(*base));
        }
        assert!(!bases.is_empty());
    }

    #[test]
    fn base_from_leak_convenience() {
        let base = base_from_leak(0x7f00001050d0, 0x50d0);
        assert_eq!(base, 0x7f0000100000);
    }

    #[test]
    fn libc_db_identify_by_leak() {
        let mut db = LibcDb::new();
        let mut syms = HashMap::new();
        syms.insert("puts".to_string(), 0x80aa0u64);
        syms.insert("system".to_string(), 0x50d70u64);
        db.add_version(LibcVersion {
            id: "test-libc".into(),
            build_id: "abc123".into(),
            symbols: syms,
        });

        // Leaked puts address with matching low 12 bits
        let matches = db.identify_by_leak("puts", 0x7f00001800aa0);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].id, "test-libc");

        // Non-matching low 12 bits
        let matches = db.identify_by_leak("puts", 0x7f0000180bbb);
        assert!(matches.is_empty());
    }

    #[test]
    fn libc_db_build_id_lookup() {
        let mut db = LibcDb::new();
        db.add_version(LibcVersion {
            id: "test".into(),
            build_id: "deadbeef".into(),
            symbols: HashMap::new(),
        });
        assert!(db.lookup_by_build_id("deadbeef").is_some());
        assert!(db.lookup_by_build_id("00000000").is_none());
    }
}