rnicro 0.1.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
410
411
412
//! ELF core dump generation.
//!
//! Generates ELF core files from a stopped traced process,
//! capturing registers, memory, and mapped file information.

use crate::error::{Error, Result};

// ── ELF constants ──────────────────────────────────────────────────

const ELFMAG: &[u8] = b"\x7fELF";
const ELFCLASS64: u8 = 2;
const ELFDATA2LSB: u8 = 1;
const EV_CURRENT: u8 = 1;
const ELFOSABI_NONE: u8 = 0;
const ET_CORE: u16 = 4;
const EM_X86_64: u16 = 62;
const PT_NOTE: u32 = 4;
const PT_LOAD: u32 = 1;

const ELF64_EHDR_SIZE: u16 = 64;
const ELF64_PHDR_SIZE: u16 = 56;

// Note types
const NT_PRSTATUS: u32 = 1;
const NT_AUXV: u32 = 6;
const NT_FILE: u32 = 0x46494c45;

/// Permissions flags for PT_LOAD segments.
pub const PF_R: u32 = 0x4;
pub const PF_W: u32 = 0x2;
pub const PF_X: u32 = 0x1;

/// Information needed to generate a core dump.
pub struct CoreDumpInfo {
    /// Register values (x86_64 user_regs_struct order, 27 u64 values).
    pub registers: Vec<u64>,
    /// Signal number that caused the stop.
    pub signal: u32,
    /// PID of the process.
    pub pid: u32,
    /// Memory mappings to include.
    pub mappings: Vec<CoreMapping>,
    /// Auxiliary vector bytes.
    pub auxv: Vec<u8>,
}

/// A memory mapping for the core dump.
pub struct CoreMapping {
    /// Start virtual address.
    pub start: u64,
    /// End virtual address.
    pub end: u64,
    /// Permission flags (PF_R | PF_W | PF_X).
    pub flags: u32,
    /// Memory contents.
    pub data: Vec<u8>,
    /// Mapped file path (empty for anonymous).
    pub pathname: String,
    /// File offset.
    pub file_offset: u64,
}

/// Serialize an ELF64 header for a core dump.
pub fn serialize_elf_header(phnum: u16, phoff: u64) -> Vec<u8> {
    let mut hdr = vec![0u8; ELF64_EHDR_SIZE as usize];

    // e_ident
    hdr[0..4].copy_from_slice(ELFMAG);
    hdr[4] = ELFCLASS64;
    hdr[5] = ELFDATA2LSB;
    hdr[6] = EV_CURRENT;
    hdr[7] = ELFOSABI_NONE;

    // e_type = ET_CORE
    hdr[16..18].copy_from_slice(&ET_CORE.to_le_bytes());
    // e_machine = EM_X86_64
    hdr[18..20].copy_from_slice(&EM_X86_64.to_le_bytes());
    // e_version
    hdr[20..24].copy_from_slice(&1u32.to_le_bytes());
    // e_entry = 0
    // e_phoff
    hdr[32..40].copy_from_slice(&phoff.to_le_bytes());
    // e_shoff = 0
    // e_flags = 0
    // e_ehsize
    hdr[52..54].copy_from_slice(&ELF64_EHDR_SIZE.to_le_bytes());
    // e_phentsize
    hdr[54..56].copy_from_slice(&ELF64_PHDR_SIZE.to_le_bytes());
    // e_phnum
    hdr[56..58].copy_from_slice(&phnum.to_le_bytes());
    // e_shentsize
    hdr[58..60].copy_from_slice(&ELF64_PHDR_SIZE.to_le_bytes());
    // e_shnum = 0
    // e_shstrndx = 0

    hdr
}

/// Serialize an ELF64 program header.
pub fn serialize_phdr(
    p_type: u32,
    p_flags: u32,
    p_offset: u64,
    p_vaddr: u64,
    p_filesz: u64,
    p_memsz: u64,
) -> Vec<u8> {
    let mut phdr = vec![0u8; ELF64_PHDR_SIZE as usize];

    phdr[0..4].copy_from_slice(&p_type.to_le_bytes());
    phdr[4..8].copy_from_slice(&p_flags.to_le_bytes());
    phdr[8..16].copy_from_slice(&p_offset.to_le_bytes());
    phdr[16..24].copy_from_slice(&p_vaddr.to_le_bytes());
    // p_paddr = p_vaddr
    phdr[24..32].copy_from_slice(&p_vaddr.to_le_bytes());
    phdr[32..40].copy_from_slice(&p_filesz.to_le_bytes());
    phdr[40..48].copy_from_slice(&p_memsz.to_le_bytes());
    // p_align = 0 (for core dumps)

    phdr
}

/// Serialize an ELF note entry.
pub fn serialize_note(name: &[u8], note_type: u32, desc: &[u8]) -> Vec<u8> {
    let namesz = name.len() as u32;
    let descsz = desc.len() as u32;
    let name_aligned = align4(name.len());
    let desc_aligned = align4(desc.len());

    let total = 12 + name_aligned + desc_aligned;
    let mut note = vec![0u8; total];

    note[0..4].copy_from_slice(&namesz.to_le_bytes());
    note[4..8].copy_from_slice(&descsz.to_le_bytes());
    note[8..12].copy_from_slice(&note_type.to_le_bytes());
    note[12..12 + name.len()].copy_from_slice(name);
    let desc_off = 12 + name_aligned;
    note[desc_off..desc_off + desc.len()].copy_from_slice(desc);

    note
}

/// Build the NT_PRSTATUS note descriptor.
///
/// Simplified prstatus: signal info + register set.
/// The register set is the user_regs_struct (27 * 8 = 216 bytes).
pub fn build_prstatus(signal: u32, pid: u32, registers: &[u64]) -> Vec<u8> {
    // Simplified elf_prstatus layout (total ~336 bytes):
    // Offset 0: si_signo (4), si_code (4), si_errno (4) = 12 bytes
    // Offset 12: pr_cursig (2), padding (2) = 4 bytes
    // Offset 16: pr_sigpend (8), pr_sighold (8) = 16 bytes
    // Offset 32: pr_pid (4), pr_ppid (4), pr_pgrp (4), pr_sid (4) = 16 bytes
    // Offset 48: pr_utime (16), pr_stime (16), pr_cutime (16), pr_cstime (16) = 64 bytes
    // Offset 112: pr_reg (27 * 8 = 216 bytes)
    // Offset 328: pr_fpvalid (4) = 4 bytes
    // Total: 332 bytes
    let mut desc = vec![0u8; 332];

    // si_signo
    desc[0..4].copy_from_slice(&signal.to_le_bytes());
    // pr_cursig
    desc[12..14].copy_from_slice(&(signal as u16).to_le_bytes());
    // pr_pid
    desc[32..36].copy_from_slice(&pid.to_le_bytes());

    // pr_reg: copy register values
    let reg_offset = 112;
    for (i, &val) in registers.iter().enumerate().take(27) {
        let off = reg_offset + i * 8;
        desc[off..off + 8].copy_from_slice(&val.to_le_bytes());
    }

    desc
}

/// Build the NT_FILE note descriptor from mapped file information.
pub fn build_file_note(mappings: &[(u64, u64, u64, &str)]) -> Vec<u8> {
    // Filter to only file-backed mappings
    let file_mappings: Vec<_> = mappings.iter().filter(|(_, _, _, p)| !p.is_empty()).collect();

    let count = file_mappings.len() as u64;
    let page_size: u64 = 4096;

    // Header: count (8) + page_size (8)
    let mut desc = Vec::new();
    desc.extend_from_slice(&count.to_le_bytes());
    desc.extend_from_slice(&page_size.to_le_bytes());

    // Per-file entries: start (8) + end (8) + offset_in_pages (8)
    for (start, end, offset, _) in &file_mappings {
        desc.extend_from_slice(&start.to_le_bytes());
        desc.extend_from_slice(&end.to_le_bytes());
        desc.extend_from_slice(&(offset / page_size).to_le_bytes());
    }

    // File names (NUL-terminated)
    for (_, _, _, pathname) in &file_mappings {
        desc.extend_from_slice(pathname.as_bytes());
        desc.push(0);
    }

    desc
}

/// Generate a complete ELF core dump.
///
/// Returns the serialized bytes of the core file.
pub fn generate(info: &CoreDumpInfo) -> Result<Vec<u8>> {
    let num_load = info.mappings.len();
    let phnum = 1 + num_load; // PT_NOTE + PT_LOAD per mapping

    if phnum > u16::MAX as usize {
        return Err(Error::Other("too many segments for core dump".into()));
    }

    // Build notes
    let prstatus_desc = build_prstatus(info.signal, info.pid, &info.registers);
    let prstatus_note = serialize_note(b"CORE\0", NT_PRSTATUS, &prstatus_desc);

    let file_mappings: Vec<(u64, u64, u64, &str)> = info
        .mappings
        .iter()
        .map(|m| (m.start, m.end, m.file_offset, m.pathname.as_str()))
        .collect();
    let file_note = serialize_note(b"CORE\0", NT_FILE, &build_file_note(&file_mappings));

    let auxv_note = if !info.auxv.is_empty() {
        serialize_note(b"CORE\0", NT_AUXV, &info.auxv)
    } else {
        Vec::new()
    };

    let notes_data: Vec<u8> = [&prstatus_note[..], &file_note[..], &auxv_note[..]].concat();

    // Calculate layout
    let ehdr_size = ELF64_EHDR_SIZE as u64;
    let phdr_total = phnum as u64 * ELF64_PHDR_SIZE as u64;
    let notes_offset = ehdr_size + phdr_total;
    let notes_size = notes_data.len() as u64;

    // Data offset starts after notes (aligned to 4096 for PT_LOAD)
    let mut data_offset = align_page(notes_offset + notes_size);

    // Build program headers
    let mut phdrs = Vec::new();

    // PT_NOTE
    phdrs.push(serialize_phdr(
        PT_NOTE,
        0,
        notes_offset,
        0,
        notes_size,
        0,
    ));

    // PT_LOAD for each mapping
    let mut load_offsets = Vec::new();
    for mapping in &info.mappings {
        let size = mapping.data.len() as u64;
        let memsz = mapping.end - mapping.start;
        phdrs.push(serialize_phdr(
            PT_LOAD,
            mapping.flags,
            data_offset,
            mapping.start,
            size,
            memsz,
        ));
        load_offsets.push(data_offset);
        data_offset = align_page(data_offset + size);
    }

    // Assemble the core dump
    let mut core = Vec::new();

    // ELF header
    core.extend_from_slice(&serialize_elf_header(phnum as u16, ehdr_size));

    // Program headers
    for phdr in &phdrs {
        core.extend_from_slice(phdr);
    }

    // Notes (pad to offset)
    while core.len() < notes_offset as usize {
        core.push(0);
    }
    core.extend_from_slice(&notes_data);

    // Memory data
    for (i, mapping) in info.mappings.iter().enumerate() {
        let target_offset = load_offsets[i] as usize;
        while core.len() < target_offset {
            core.push(0);
        }
        core.extend_from_slice(&mapping.data);
    }

    Ok(core)
}

/// Align a value up to 4-byte boundary.
fn align4(n: usize) -> usize {
    (n + 3) & !3
}

/// Align a value up to page boundary (4096).
fn align_page(n: u64) -> u64 {
    (n + 4095) & !4095
}

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

    #[test]
    fn elf_header_format() {
        let hdr = serialize_elf_header(3, 64);
        assert_eq!(&hdr[0..4], ELFMAG);
        assert_eq!(hdr[4], ELFCLASS64);
        assert_eq!(hdr[5], ELFDATA2LSB);
        assert_eq!(u16::from_le_bytes([hdr[16], hdr[17]]), ET_CORE);
        assert_eq!(u16::from_le_bytes([hdr[18], hdr[19]]), EM_X86_64);
        assert_eq!(u16::from_le_bytes([hdr[56], hdr[57]]), 3); // phnum
    }

    #[test]
    fn phdr_format() {
        let phdr = serialize_phdr(PT_LOAD, PF_R | PF_W, 0x1000, 0x400000, 0x2000, 0x3000);
        assert_eq!(
            u32::from_le_bytes(phdr[0..4].try_into().unwrap()),
            PT_LOAD
        );
        assert_eq!(
            u32::from_le_bytes(phdr[4..8].try_into().unwrap()),
            PF_R | PF_W
        );
        assert_eq!(
            u64::from_le_bytes(phdr[8..16].try_into().unwrap()),
            0x1000
        );
        assert_eq!(
            u64::from_le_bytes(phdr[16..24].try_into().unwrap()),
            0x400000
        );
    }

    #[test]
    fn note_serialization() {
        let note = serialize_note(b"CORE\0", NT_PRSTATUS, &[1, 2, 3, 4]);
        let namesz = u32::from_le_bytes(note[0..4].try_into().unwrap());
        let descsz = u32::from_le_bytes(note[4..8].try_into().unwrap());
        let ntype = u32::from_le_bytes(note[8..12].try_into().unwrap());
        assert_eq!(namesz, 5); // "CORE\0"
        assert_eq!(descsz, 4);
        assert_eq!(ntype, NT_PRSTATUS);
    }

    #[test]
    fn prstatus_has_registers() {
        let regs = vec![0x1234u64; 27];
        let desc = build_prstatus(11, 1000, &regs);
        // Check signal
        assert_eq!(u32::from_le_bytes(desc[0..4].try_into().unwrap()), 11);
        // Check pid
        assert_eq!(u32::from_le_bytes(desc[32..36].try_into().unwrap()), 1000);
        // Check first register
        assert_eq!(
            u64::from_le_bytes(desc[112..120].try_into().unwrap()),
            0x1234
        );
    }

    #[test]
    fn file_note_empty() {
        let note = build_file_note(&[]);
        let count = u64::from_le_bytes(note[0..8].try_into().unwrap());
        assert_eq!(count, 0);
    }

    #[test]
    fn align4_works() {
        assert_eq!(align4(0), 0);
        assert_eq!(align4(1), 4);
        assert_eq!(align4(4), 4);
        assert_eq!(align4(5), 8);
    }

    #[test]
    fn generate_minimal_core() {
        let info = CoreDumpInfo {
            registers: vec![0; 27],
            signal: 11,
            pid: 1234,
            mappings: vec![CoreMapping {
                start: 0x400000,
                end: 0x401000,
                flags: PF_R | PF_X,
                data: vec![0x90; 64], // Some NOP bytes
                pathname: String::new(),
                file_offset: 0,
            }],
            auxv: Vec::new(),
        };
        let core = generate(&info).unwrap();
        // Verify ELF magic
        assert_eq!(&core[0..4], ELFMAG);
        // Verify it's a core file
        assert_eq!(u16::from_le_bytes([core[16], core[17]]), ET_CORE);
    }
}