Skip to main content

nabla_decompiler/
patcher.rs

1//! Binary patching with safe state management and rollback
2
3use anyhow::{anyhow, Result};
4use nabla_scanner::binary::analysis::BinaryAnalysis;
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, RwLock};
10use tempfile::TempDir;
11use uuid::Uuid;
12use chrono::{DateTime, Utc};
13
14use crate::types::Address;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PatchSnapshot {
18    pub original_hash: String,
19    pub patches: Vec<AppliedPatch>,
20    pub created_at: chrono::DateTime<chrono::Utc>,
21    pub description: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct AppliedPatch {
26    pub address: Address,
27    pub original_bytes: Vec<u8>,
28    pub patched_bytes: Vec<u8>,
29    pub description: String,
30    pub pseudocode: String,
31}
32
33#[derive(Debug, Clone)]
34pub struct PatchRequest {
35    pub address: Address,
36    pub hex_data: String,
37    pub description: String,
38    pub dry_run: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionPatch {
43    pub id: Uuid,
44    pub address: Address,
45    pub original_bytes: Vec<u8>,
46    pub patched_bytes: Vec<u8>,
47    pub description: String,
48    pub applied_at: DateTime<Utc>,
49}
50
51pub struct BinaryPatcher {
52    binary_path: PathBuf,
53    temp_dir: TempDir,
54    snapshots: Vec<PatchSnapshot>,
55    current_hash: String,
56}
57
58impl BinaryPatcher {
59    pub fn new<P: AsRef<Path>>(binary_path: P) -> Result<Self> {
60        let binary_path = binary_path.as_ref().to_path_buf();
61        let temp_dir = TempDir::new()?;
62        
63        // Calculate initial hash
64        let binary_data = fs::read(&binary_path)?;
65        let current_hash = format!("{:x}", Sha256::digest(&binary_data));
66        
67        Ok(Self {
68            binary_path,
69            temp_dir,
70            snapshots: Vec::new(),
71            current_hash,
72        })
73    }
74    
75    /// Apply hex patch to binary
76    pub fn apply_patch(&mut self, request: PatchRequest, analysis: &BinaryAnalysis) -> Result<String> {
77        // Step 1: Validate the patch request
78        self.validate_patch_request(&request, analysis)?;
79        
80        // Step 2: Parse hex data directly into bytes
81        let machine_code = self.parse_hex_data(&request.hex_data)?;
82        
83        if request.dry_run {
84            return Ok(format!(
85                "DRY RUN - Would patch {} bytes at 0x{:x}:\nHex data: {}\nBytes: {:02x?}",
86                machine_code.len(),
87                request.address,
88                request.hex_data,
89                machine_code
90            ));
91        }
92        
93        // Step 3: Create backup snapshot
94        let _snapshot = self.create_snapshot(&request.description)?;
95        
96        // Step 4: Apply the patch
97        let original_bytes = self.patch_binary_at_address(request.address, &machine_code)?;
98        
99        // Step 5: Update snapshot with patch details
100        let applied_patch = AppliedPatch {
101            address: request.address,
102            original_bytes,
103            patched_bytes: machine_code.clone(),
104            description: request.description.clone(),
105            pseudocode: format!("hex: {}", request.hex_data),
106        };
107        
108        if let Some(last_snapshot) = self.snapshots.last_mut() {
109            last_snapshot.patches.push(applied_patch);
110        }
111        
112        // Step 6: Update current hash
113        let binary_data = fs::read(&self.binary_path)?;
114        self.current_hash = format!("{:x}", Sha256::digest(&binary_data));
115        
116        Ok(format!(
117            "āœ… Successfully patched {} bytes at 0x{:x}\nšŸ“ Description: {}\nšŸ”§ Hex data: {}\nšŸ” Bytes applied: {:02x?}",
118            machine_code.len(),
119            request.address,
120            request.description,
121            request.hex_data,
122            machine_code
123        ))
124    }
125    
126    /// Rollback to a previous snapshot
127    pub fn rollback(&mut self, snapshot_index: Option<usize>) -> Result<String> {
128        let target_index = snapshot_index.unwrap_or(0);
129        
130        if target_index >= self.snapshots.len() {
131            return Err(anyhow!("Invalid snapshot index: {}", target_index));
132        }
133        
134        // Clone the data we need from the target snapshot before mutating
135        let original_hash = self.snapshots[target_index].original_hash.clone();
136        let created_at = self.snapshots[target_index].created_at;
137        let description = self.snapshots[target_index].description.clone();
138        
139        // Restore from backup
140        let backup_path = self.temp_dir.path().join(format!("backup_{}.bin", target_index));
141        if backup_path.exists() {
142            fs::copy(&backup_path, &self.binary_path)?;
143            self.current_hash = original_hash;
144            
145            // Remove snapshots after the target
146            self.snapshots.truncate(target_index + 1);
147            
148            Ok(format!(
149                "āœ… Rolled back to snapshot {}\nšŸ“… Created: {}\nšŸ“ Description: {}",
150                target_index,
151                created_at.format("%Y-%m-%d %H:%M:%S UTC"),
152                description
153            ))
154        } else {
155            Err(anyhow!("Backup file not found for snapshot {}", target_index))
156        }
157    }
158    
159    /// List all snapshots
160    pub fn list_snapshots(&self) -> String {
161        if self.snapshots.is_empty() {
162            return "No snapshots available".to_string();
163        }
164        
165        let mut result = String::from("šŸ“ø Binary Patch Snapshots:\n\n");
166        
167        for (i, snapshot) in self.snapshots.iter().enumerate() {
168            result.push_str(&format!(
169                "#{}: {} ({})\n  šŸ“… {}\n  šŸ”§ {} patches\n  šŸ” Hash: {}...\n\n",
170                i,
171                snapshot.description,
172                if i == self.snapshots.len() - 1 { "current" } else { "historical" },
173                snapshot.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
174                snapshot.patches.len(),
175                &snapshot.original_hash[..16]
176            ));
177        }
178        
179        result
180    }
181    
182    /// Validate patch request
183    fn validate_patch_request(&self, request: &PatchRequest, analysis: &BinaryAnalysis) -> Result<()> {
184        // Check if address is in a code section first
185        let target_section = analysis.code_sections.iter().find(|section| {
186            request.address >= section.start_address && request.address < section.end_address
187        });
188        
189        let Some(_section) = target_section else {
190            return Err(anyhow!(
191                "Address 0x{:x} is not in a known code section. This could corrupt data.",
192                request.address
193            ));
194        };
195        
196        // Convert virtual address to file offset
197        let binary_data = fs::read(&self.binary_path)?;
198        let file_offset = self.virtual_address_to_file_offset(request.address, &binary_data)?;
199        
200        // Check if file offset is within binary bounds
201        if file_offset >= binary_data.len() {
202            return Err(anyhow!(
203                "Address 0x{:x} maps to file offset 0x{:x}, which is beyond binary bounds (size: 0x{:x})",
204                request.address,
205                file_offset,
206                binary_data.len()
207            ));
208        }
209        
210        // Validate hex data format
211        if request.hex_data.trim().is_empty() {
212            return Err(anyhow!("Hex data cannot be empty"));
213        }
214        
215        Ok(())
216    }
217    
218    /// Parse hex data directly into bytes with error handling
219    fn parse_hex_data(&self, hex_data: &str) -> Result<Vec<u8>> {
220        let cleaned = hex_data.trim().replace("0x", "").replace(" ", "");
221        
222        // Validate hex format
223        if cleaned.is_empty() {
224            return Err(anyhow!("Hex data cannot be empty"));
225        }
226        
227        if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
228            return Err(anyhow!("Invalid hex data '{}': contains non-hex characters", hex_data));
229        }
230        
231        if cleaned.len() % 2 != 0 {
232            return Err(anyhow!("Invalid hex data '{}': must have even number of characters", hex_data));
233        }
234        
235        // Convert hex string to bytes
236        let mut bytes = Vec::new();
237        for chunk in cleaned.as_bytes().chunks(2) {
238            let hex_str = std::str::from_utf8(chunk)
239                .map_err(|e| anyhow!("Invalid UTF-8 in hex data: {}", e))?;
240            let byte = u8::from_str_radix(hex_str, 16)
241                .map_err(|e| anyhow!("Invalid hex byte '{}': {}", hex_str, e))?;
242            bytes.push(byte);
243        }
244        
245        if bytes.is_empty() {
246            return Err(anyhow!("Parsed hex data resulted in empty byte array"));
247        }
248        
249        println!("Successfully parsed {} hex bytes: {:02x?}", bytes.len(), bytes);
250        Ok(bytes)
251    }
252    
253    
254    
255    /// Create a backup snapshot
256    fn create_snapshot(&mut self, description: &str) -> Result<PatchSnapshot> {
257        let binary_data = fs::read(&self.binary_path)?;
258        let hash = format!("{:x}", Sha256::digest(&binary_data));
259        
260        // Save backup to temp directory
261        let backup_path = self.temp_dir.path().join(format!("backup_{}.bin", self.snapshots.len()));
262        fs::write(&backup_path, &binary_data)?;
263        
264        let snapshot = PatchSnapshot {
265            original_hash: hash,
266            patches: Vec::new(),
267            created_at: chrono::Utc::now(),
268            description: description.to_string(),
269        };
270        
271        self.snapshots.push(snapshot.clone());
272        Ok(snapshot)
273    }
274    
275    /// Apply patch to binary at specific address
276    fn patch_binary_at_address(&self, address: Address, new_bytes: &[u8]) -> Result<Vec<u8>> {
277        let mut binary_data = fs::read(&self.binary_path)?;
278        
279        // Convert virtual address to file offset
280        let file_offset = self.virtual_address_to_file_offset(address, &binary_data)?;
281        let start_addr = file_offset;
282        let end_addr = start_addr + new_bytes.len();
283        
284        if end_addr > binary_data.len() {
285            return Err(anyhow!(
286                "Patch would extend beyond binary bounds (file offset: 0x{:x}, patch size: {}, binary size: 0x{:x})",
287                start_addr,
288                new_bytes.len(),
289                binary_data.len()
290            ));
291        }
292        
293        // Backup original bytes
294        let original_bytes = binary_data[start_addr..end_addr].to_vec();
295        
296        // Apply patch
297        binary_data.splice(start_addr..end_addr, new_bytes.iter().cloned());
298        
299        // Write patched binary
300        fs::write(&self.binary_path, &binary_data)?;
301        
302        Ok(original_bytes)
303    }
304    
305    /// Read bytes from binary at specified address
306    pub fn read_bytes(&self, address: Address, length: usize) -> Result<Vec<u8>> {
307        let binary_data = fs::read(&self.binary_path)?;
308        let file_offset = self.virtual_address_to_file_offset(address, &binary_data)?;
309        
310        if file_offset + length > binary_data.len() {
311            return Err(anyhow!(
312                "Read would extend beyond binary bounds (file offset: 0x{:x}, read size: {}, binary size: 0x{:x})",
313                file_offset,
314                length,
315                binary_data.len()
316            ));
317        }
318        
319        Ok(binary_data[file_offset..file_offset + length].to_vec())
320    }
321    
322    /// Write bytes to binary at specified address
323    pub fn write_bytes(&mut self, address: Address, bytes: &[u8]) -> Result<()> {
324        let mut binary_data = fs::read(&self.binary_path)?;
325        let file_offset = self.virtual_address_to_file_offset(address, &binary_data)?;
326        let end_offset = file_offset + bytes.len();
327        
328        if end_offset > binary_data.len() {
329            return Err(anyhow!(
330                "Write would extend beyond binary bounds (file offset: 0x{:x}, write size: {}, binary size: 0x{:x})",
331                file_offset,
332                bytes.len(),
333                binary_data.len()
334            ));
335        }
336        
337        // Replace bytes in binary data
338        binary_data.splice(file_offset..end_offset, bytes.iter().cloned());
339        
340        // Write updated binary back to file
341        fs::write(&self.binary_path, &binary_data)?;
342        
343        // Update current hash
344        self.current_hash = format!("{:x}", Sha256::digest(&binary_data));
345        
346        Ok(())
347    }
348    
349    /// Convert virtual address to file offset using heuristics
350    fn virtual_address_to_file_offset(&self, virtual_address: u64, binary_data: &[u8]) -> Result<usize> {
351        // This is a heuristic approach similar to the one used in disasm.rs
352        // For more accurate mapping, we'd need to parse the binary format (ELF, PE, etc.)
353        
354        let potential_offset = if virtual_address > 0x400000 {
355            // Typical Linux x86_64 binary base address
356            (virtual_address - 0x400000) as usize
357        } else if virtual_address > 0x8000000 {
358            // Typical ARM binary base address  
359            (virtual_address - 0x8000000) as usize
360        } else if virtual_address > 0x10000000 {
361            // Windows PE base address
362            (virtual_address - 0x10000000) as usize
363        } else if virtual_address > 0x1000 {
364            // Small offset from base
365            (virtual_address - 0x1000) as usize
366        } else {
367            // Assume it's already a file offset
368            virtual_address as usize
369        };
370        
371        // Ensure the calculated offset is within bounds
372        if potential_offset < binary_data.len() {
373            Ok(potential_offset)
374        } else {
375            // Fallback: try the address as-is (maybe it's already a file offset)
376            let direct_offset = virtual_address as usize;
377            if direct_offset < binary_data.len() {
378                Ok(direct_offset)
379            } else {
380                Err(anyhow!(
381                    "Cannot map virtual address 0x{:x} to valid file offset (tried 0x{:x}, binary size: 0x{:x})",
382                    virtual_address,
383                    potential_offset,
384                    binary_data.len()
385                ))
386            }
387        }
388    }
389}
390
391pub struct MemoryPatcher {
392    original_binary: Vec<u8>,
393    working_binary: Arc<RwLock<Vec<u8>>>,
394    applied_patches: Vec<SessionPatch>,
395}
396
397impl MemoryPatcher {
398    pub fn new(binary_data: Vec<u8>) -> Self {
399        Self {
400            original_binary: binary_data.clone(),
401            working_binary: Arc::new(RwLock::new(binary_data)),
402            applied_patches: Vec::new(),
403        }
404    }
405
406    pub fn apply_patch(&mut self, request: PatchRequest) -> Result<String> {
407        let address = request.address;
408        
409        // Parse hex data into bytes
410        let machine_code = self.parse_hex_data(&request.hex_data)?;
411        
412        if request.dry_run {
413            return Ok(format!(
414                "DRY RUN - Would patch {} bytes at 0x{:x}:\nHex data: {}\nBytes: {:02x?}",
415                machine_code.len(),
416                address,
417                request.hex_data,
418                machine_code
419            ));
420        }
421
422        // Apply patch to in-memory binary
423        let mut binary = self.working_binary.write().unwrap();
424        
425        // Convert virtual address to file offset
426        let file_offset = self.virtual_address_to_file_offset(address, &binary)?;
427        let start_idx = file_offset;
428        let end_idx = start_idx + machine_code.len();
429        
430        if end_idx > binary.len() {
431            return Err(anyhow!("Patch would extend beyond binary bounds"));
432        }
433
434        // Read original bytes before patching
435        let original_bytes = binary[start_idx..end_idx].to_vec();
436
437        // Apply the patch
438        binary[start_idx..end_idx].copy_from_slice(&machine_code);
439
440        // Record the patch
441        let patch = SessionPatch {
442            id: Uuid::new_v4(),
443            address,
444            original_bytes,
445            patched_bytes: machine_code.clone(),
446            description: request.description.clone(),
447            applied_at: Utc::now(),
448        };
449        
450        self.applied_patches.push(patch);
451
452        Ok(format!(
453            "āœ… Successfully patched {} bytes at 0x{:x}\nšŸ“ Description: {}\nšŸ”§ Hex data: {}\nšŸ” Bytes applied: {:02x?}",
454            machine_code.len(),
455            address,
456            request.description,
457            request.hex_data,
458            machine_code
459        ))
460    }
461
462    pub fn read_bytes(&self, address: Address, length: usize) -> Result<Vec<u8>> {
463        let binary = self.working_binary.read().unwrap();
464        let file_offset = self.virtual_address_to_file_offset(address, &binary)?;
465        let start_idx = file_offset;
466        let end_idx = start_idx + length;
467
468        if end_idx > binary.len() {
469            return Err(anyhow!("Read would extend beyond binary bounds"));
470        }
471
472        Ok(binary[start_idx..end_idx].to_vec())
473    }
474
475    pub fn get_working_binary(&self) -> Vec<u8> {
476        self.working_binary.read().unwrap().clone()
477    }
478
479    pub fn get_original_binary(&self) -> &[u8] {
480        &self.original_binary
481    }
482
483    pub fn get_applied_patches(&self) -> &[SessionPatch] {
484        &self.applied_patches
485    }
486
487    pub fn get_applied_patches_mut(&mut self) -> &mut Vec<SessionPatch> {
488        &mut self.applied_patches
489    }
490
491    pub fn get_working_binary_arc(&self) -> Arc<RwLock<Vec<u8>>> {
492        Arc::clone(&self.working_binary)
493    }
494
495    pub fn get_original_binary_ref(&self) -> &[u8] {
496        &self.original_binary
497    }
498
499    pub fn virtual_address_to_file_offset_public(&self, virtual_address: u64, binary_data: &[u8]) -> Result<usize> {
500        self.virtual_address_to_file_offset(virtual_address, binary_data)
501    }
502
503    pub fn rollback_patch(&mut self, patch_id: Uuid) -> Result<String> {
504        // Find the patch to rollback
505        let patch_idx = self.applied_patches
506            .iter()
507            .position(|p| p.id == patch_id)
508            .ok_or_else(|| anyhow!("Patch not found: {}", patch_id))?;
509
510        // Rollback all patches from this point forward in reverse order
511        let patches_to_rollback = self.applied_patches.split_off(patch_idx);
512        
513        for patch in patches_to_rollback.iter().rev() {
514            let mut binary = self.working_binary.write().unwrap();
515            let file_offset = self.virtual_address_to_file_offset(patch.address, &binary)?;
516            let start_idx = file_offset;
517            let end_idx = start_idx + patch.original_bytes.len();
518            binary[start_idx..end_idx].copy_from_slice(&patch.original_bytes);
519        }
520
521        Ok(format!("āœ… Rolled back patch and {} subsequent patches", patches_to_rollback.len() - 1))
522    }
523
524    pub fn rollback_all(&mut self) -> Result<String> {
525        // Reset to original binary
526        let mut binary = self.working_binary.write().unwrap();
527        *binary = self.original_binary.clone();
528        
529        let patch_count = self.applied_patches.len();
530        self.applied_patches.clear();
531
532        Ok(format!("āœ… Rolled back all {} patches", patch_count))
533    }
534
535    fn parse_hex_data(&self, hex_data: &str) -> Result<Vec<u8>> {
536        let cleaned = hex_data.trim().replace("0x", "").replace(" ", "");
537        
538        if cleaned.is_empty() {
539            return Err(anyhow!("Hex data cannot be empty"));
540        }
541        
542        if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
543            return Err(anyhow!("Invalid hex data '{}': contains non-hex characters", hex_data));
544        }
545        
546        if cleaned.len() % 2 != 0 {
547            return Err(anyhow!("Invalid hex data '{}': must have even number of characters", hex_data));
548        }
549        
550        let mut bytes = Vec::new();
551        for chunk in cleaned.as_bytes().chunks(2) {
552            let hex_str = std::str::from_utf8(chunk)?;
553            let byte = u8::from_str_radix(hex_str, 16)
554                .map_err(|e| anyhow!("Invalid hex byte '{}': {}", hex_str, e))?;
555            bytes.push(byte);
556        }
557        
558        Ok(bytes)
559    }
560
561    fn virtual_address_to_file_offset(&self, virtual_address: u64, binary_data: &[u8]) -> Result<usize> {
562        let potential_offset = if virtual_address > 0x400000 {
563            (virtual_address - 0x400000) as usize
564        } else if virtual_address > 0x8000000 {
565            (virtual_address - 0x8000000) as usize
566        } else if virtual_address > 0x10000000 {
567            (virtual_address - 0x10000000) as usize
568        } else if virtual_address > 0x1000 {
569            (virtual_address - 0x1000) as usize
570        } else {
571            virtual_address as usize
572        };
573        
574        if potential_offset < binary_data.len() {
575            Ok(potential_offset)
576        } else {
577            let direct_offset = virtual_address as usize;
578            if direct_offset < binary_data.len() {
579                Ok(direct_offset)
580            } else {
581                Err(anyhow!(
582                    "Cannot map virtual address 0x{:x} to valid file offset",
583                    virtual_address
584                ))
585            }
586        }
587    }
588}
589
590/// Utility functions for parsing addresses from strings
591pub fn parse_address(addr_str: &str) -> Result<Address> {
592    let cleaned = addr_str.trim().to_lowercase();
593    
594    if cleaned.starts_with("0x") {
595        u64::from_str_radix(&cleaned[2..], 16)
596            .map_err(|e| anyhow!("Invalid hex address '{}': {}", addr_str, e))
597    } else {
598        cleaned.parse::<u64>()
599            .map_err(|e| anyhow!("Invalid address '{}': {}", addr_str, e))
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606    use tempfile::NamedTempFile;
607    
608    #[test]
609    fn test_parse_address() {
610        assert_eq!(parse_address("0x1000").unwrap(), 0x1000);
611        assert_eq!(parse_address("4096").unwrap(), 4096);
612        assert_eq!(parse_address("0X2000").unwrap(), 0x2000);
613        assert!(parse_address("invalid").is_err());
614    }
615    
616    #[test]
617    fn test_parse_hex_data() {
618        let patcher = create_test_patcher().unwrap();
619        
620        let machine_code = patcher.parse_hex_data("48c7c000000000").unwrap();
621        assert_eq!(machine_code, vec![0x48, 0xc7, 0xc0, 0x00, 0x00, 0x00, 0x00]);
622        
623        let machine_code = patcher.parse_hex_data("0x90").unwrap();
624        assert_eq!(machine_code, vec![0x90]);
625        
626        let machine_code = patcher.parse_hex_data("48 c7 c0 00").unwrap();
627        assert_eq!(machine_code, vec![0x48, 0xc7, 0xc0, 0x00]);
628        
629        assert!(patcher.parse_hex_data("invalid").is_err());
630        assert!(patcher.parse_hex_data("4").is_err()); // odd length
631    }
632    
633    fn create_test_patcher() -> Result<BinaryPatcher> {
634        let temp_file = NamedTempFile::new()?;
635        let test_data = vec![0x48, 0xc7, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xc3]; // mov rax, 0; ret
636        fs::write(temp_file.path(), &test_data)?;
637        BinaryPatcher::new(temp_file.path())
638    }
639}