1#![allow(dead_code)]
2use crate::binary::BinaryAnalysis;
3use crate::enterprise::secure::control_flow::ControlFlowGraph;
4use crate::enterprise::types::{CodeLocation, ConfidenceLevel, SeverityLevel};
5use chrono::Utc;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BehavioralAnalysisResult {
11 pub analysis_id: Uuid,
12 pub file_path: String,
13 pub control_flow_anomalies: Vec<ControlFlowAnomaly>,
14 pub network_patterns: Vec<NetworkPattern>,
15 pub data_flow_issues: Vec<DataFlowIssue>,
16 pub analysis_duration_ms: u64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ControlFlowAnomaly {
21 pub anomaly_type: ControlFlowAnomalyType,
22 pub location: CodeLocation,
23 pub confidence: ConfidenceLevel,
24 pub description: String,
25 pub call_graph_fragment: Option<Vec<String>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum ControlFlowAnomalyType {
30 UnexpectedJump,
31 SuspiciousLoop,
32 DeadCode,
33 HiddenBranch,
34 AntiDebugPattern,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct NetworkPattern {
39 pub pattern_type: NetworkPatternType,
40 pub endpoints: Vec<String>,
41 pub frequency: Option<u32>,
42 pub suspicious_indicators: Vec<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum NetworkPatternType {
47 Beaconing,
48 DataExfiltration,
49 CommandAndControl,
50 DNSTunneling,
51 SuspiciousPort,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DataFlowIssue {
56 pub issue_type: DataFlowIssueType,
57 pub source: CodeLocation,
58 pub sink: CodeLocation,
59 pub severity: SeverityLevel,
60 pub data_path: Vec<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum DataFlowIssueType {
65 UncontrolledInput,
66 DataLeakage,
67 PrivilegeEscalation,
68 UnsanitizedOutput,
69}
70
71pub fn analyze_behavioral_security(analysis: &BinaryAnalysis) -> BehavioralAnalysisResult {
72 let start_time = Utc::now();
73
74 let mut result = BehavioralAnalysisResult {
75 analysis_id: Uuid::new_v4(),
76 file_path: analysis.file_name.clone(),
77 control_flow_anomalies: Vec::new(),
78 network_patterns: Vec::new(),
79 data_flow_issues: Vec::new(),
80 analysis_duration_ms: 0,
81 };
82
83 let cfg = match ControlFlowGraph::build_cfg(analysis) {
85 Ok(cfg) => cfg,
86 Err(e) => {
87 tracing::warn!("CFG construction failed: {}, using simplified analysis", e);
89 return BehavioralAnalysisResult {
90 analysis_id: Uuid::new_v4(),
91 file_path: analysis.file_name.clone(),
92 control_flow_anomalies: analyze_control_flow_anomalies_simple(analysis),
93 network_patterns: analyze_network_patterns(analysis),
94 data_flow_issues: analyze_data_flow_issues(analysis),
95 analysis_duration_ms: (Utc::now() - start_time).num_milliseconds() as u64,
96 };
97 }
98 };
99
100 result.control_flow_anomalies = analyze_control_flow_anomalies(analysis, &cfg);
102
103 result.network_patterns = analyze_network_patterns(analysis);
105
106 result.data_flow_issues = analyze_data_flow_issues(analysis);
108
109 let end_time = Utc::now();
110 result.analysis_duration_ms = (end_time - start_time).num_milliseconds() as u64;
111
112 result
113}
114
115fn analyze_control_flow_anomalies(
116 analysis: &BinaryAnalysis,
117 cfg: &ControlFlowGraph,
118) -> Vec<ControlFlowAnomaly> {
119 let mut anomalies = Vec::new();
120
121 for loop_info in &cfg.loops {
123 match loop_info.loop_type {
124 crate::enterprise::secure::control_flow::LoopType::Infinite => {
125 anomalies.push(ControlFlowAnomaly {
126 anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
127 location: CodeLocation {
128 file_path: analysis.file_name.clone(),
129 line_number: None,
130 column_number: None,
131 function_name: None,
132 binary_offset: None,
133 },
134 confidence: ConfidenceLevel::High,
135 description: format!(
136 "Infinite loop detected at address 0x{:x}",
137 loop_info.header
138 ),
139 call_graph_fragment: Some(vec![format!("0x{:x}", loop_info.header)]),
140 });
141 }
142 crate::enterprise::secure::control_flow::LoopType::Irreducible => {
143 anomalies.push(ControlFlowAnomaly {
144 anomaly_type: ControlFlowAnomalyType::HiddenBranch,
145 location: CodeLocation {
146 file_path: analysis.file_name.clone(),
147 line_number: None,
148 column_number: None,
149 function_name: None,
150 binary_offset: None,
151 },
152 confidence: ConfidenceLevel::Medium,
153 description: "Irreducible control flow detected - possible obfuscation"
154 .to_string(),
155 call_graph_fragment: None,
156 });
157 }
158 _ => {}
159 }
160 }
161
162 let call_graph = cfg.analyze_call_graph(analysis);
164
165 for recursive_func in &call_graph.recursive_functions {
167 anomalies.push(ControlFlowAnomaly {
168 anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
169 location: CodeLocation {
170 file_path: analysis.file_name.clone(),
171 line_number: None,
172 column_number: None,
173 function_name: Some(recursive_func.clone()),
174 binary_offset: None,
175 },
176 confidence: ConfidenceLevel::Medium,
177 description: format!("Recursive function detected: {}", recursive_func),
178 call_graph_fragment: Some(vec![recursive_func.clone()]),
179 });
180 }
181
182 for (func_name, summary) in &call_graph.function_summaries {
184 if summary.cyclomatic_complexity > 20 {
185 anomalies.push(ControlFlowAnomaly {
186 anomaly_type: ControlFlowAnomalyType::HiddenBranch,
187 location: CodeLocation {
188 file_path: analysis.file_name.clone(),
189 line_number: None,
190 column_number: None,
191 function_name: Some(func_name.clone()),
192 binary_offset: None,
193 },
194 confidence: ConfidenceLevel::Medium,
195 description: format!(
196 "High cyclomatic complexity ({}) in function {}",
197 summary.cyclomatic_complexity, func_name
198 ),
199 call_graph_fragment: Some(vec![func_name.clone()]),
200 });
201 }
202 }
203
204 anomalies.extend(analyze_control_flow_anomalies_simple(analysis));
206
207 anomalies
208}
209
210fn analyze_control_flow_anomalies_simple(analysis: &BinaryAnalysis) -> Vec<ControlFlowAnomaly> {
211 let mut anomalies = Vec::new();
212
213 let anti_debug_functions = [
215 "ptrace",
216 "IsDebuggerPresent",
217 "CheckRemoteDebuggerPresent",
218 "NtQueryInformationProcess",
219 "OutputDebugString",
220 "GetTickCount",
221 "timeGetTime",
222 "rdtsc",
223 "cpuid",
224 ];
225
226 for func in &anti_debug_functions {
227 if analysis.imports.contains(&func.to_string())
228 || analysis.detected_symbols.contains(&func.to_string())
229 {
230 anomalies.push(ControlFlowAnomaly {
231 anomaly_type: ControlFlowAnomalyType::AntiDebugPattern,
232 location: CodeLocation {
233 file_path: analysis.file_name.clone(),
234 line_number: None,
235 column_number: None,
236 function_name: Some(func.to_string()),
237 binary_offset: None,
238 },
239 confidence: ConfidenceLevel::High,
240 description: format!("Anti-debugging function {} detected", func),
241 call_graph_fragment: Some(vec![func.to_string()]),
242 });
243 }
244 }
245
246 for string in &analysis.embedded_strings {
248 let lower = string.to_lowercase();
249
250 if lower.contains("upx") || lower.contains("packer") || lower.contains("packed") {
252 anomalies.push(ControlFlowAnomaly {
253 anomaly_type: ControlFlowAnomalyType::HiddenBranch,
254 location: CodeLocation {
255 file_path: analysis.file_name.clone(),
256 line_number: None,
257 column_number: None,
258 function_name: None,
259 binary_offset: None,
260 },
261 confidence: ConfidenceLevel::Medium,
262 description: "Potential code packing or obfuscation detected".to_string(),
263 call_graph_fragment: None,
264 });
265 }
266
267 if lower.contains("vmware")
269 || lower.contains("virtualbox")
270 || lower.contains("sandboxie")
271 || lower.contains("wine")
272 {
273 anomalies.push(ControlFlowAnomaly {
274 anomaly_type: ControlFlowAnomalyType::AntiDebugPattern,
275 location: CodeLocation {
276 file_path: analysis.file_name.clone(),
277 line_number: None,
278 column_number: None,
279 function_name: None,
280 binary_offset: None,
281 },
282 confidence: ConfidenceLevel::Medium,
283 description: "Virtual machine detection strings found".to_string(),
284 call_graph_fragment: None,
285 });
286 }
287 }
288
289 let suspicious_patterns = detect_suspicious_call_patterns(analysis);
291 anomalies.extend(suspicious_patterns);
292
293 if analysis.detected_symbols.len() > analysis.imports.len() * 5 {
295 anomalies.push(ControlFlowAnomaly {
296 anomaly_type: ControlFlowAnomalyType::DeadCode,
297 location: CodeLocation {
298 file_path: analysis.file_name.clone(),
299 line_number: None,
300 column_number: None,
301 function_name: None,
302 binary_offset: None,
303 },
304 confidence: ConfidenceLevel::Low,
305 description: "Large number of symbols relative to imports - potential dead code"
306 .to_string(),
307 call_graph_fragment: None,
308 });
309 }
310
311 anomalies
312}
313
314fn detect_suspicious_call_patterns(analysis: &BinaryAnalysis) -> Vec<ControlFlowAnomaly> {
315 let mut anomalies = Vec::new();
316
317 let has_dlopen = analysis.imports.contains(&"dlopen".to_string());
319 let has_dlsym = analysis.imports.contains(&"dlsym".to_string());
320 let has_exec = analysis.imports.iter().any(|s| s.starts_with("exec"));
321
322 if has_dlopen && has_dlsym && has_exec {
323 anomalies.push(ControlFlowAnomaly {
324 anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
325 location: CodeLocation {
326 file_path: analysis.file_name.clone(),
327 line_number: None,
328 column_number: None,
329 function_name: None,
330 binary_offset: None,
331 },
332 confidence: ConfidenceLevel::Medium,
333 description: "Dynamic loading combined with execution - potential code injection"
334 .to_string(),
335 call_graph_fragment: Some(vec![
336 "dlopen".to_string(),
337 "dlsym".to_string(),
338 "exec*".to_string(),
339 ]),
340 });
341 }
342
343 let has_mmap = analysis.imports.contains(&"mmap".to_string());
345 let has_mprotect = analysis.imports.contains(&"mprotect".to_string());
346 let has_network = analysis.imports.iter().any(|s| {
347 s.contains("socket") || s.contains("connect") || s.contains("recv") || s.contains("send")
348 });
349
350 if (has_mmap || has_mprotect) && has_network {
351 anomalies.push(ControlFlowAnomaly {
352 anomaly_type: ControlFlowAnomalyType::UnexpectedJump,
353 location: CodeLocation {
354 file_path: analysis.file_name.clone(),
355 line_number: None,
356 column_number: None,
357 function_name: None,
358 binary_offset: None,
359 },
360 confidence: ConfidenceLevel::Medium,
361 description: "Memory manipulation combined with network functions - potential remote code execution".to_string(),
362 call_graph_fragment: Some(vec!["mmap/mprotect".to_string(), "network_functions".to_string()]),
363 });
364 }
365
366 anomalies
367}
368
369fn analyze_network_patterns(analysis: &BinaryAnalysis) -> Vec<NetworkPattern> {
370 let mut patterns = Vec::new();
371
372 let network_functions = [
374 "socket",
375 "connect",
376 "bind",
377 "listen",
378 "accept",
379 "send",
380 "recv",
381 "sendto",
382 "recvfrom",
383 "getsockopt",
384 "setsockopt",
385 "select",
386 "poll",
387 ];
388
389 let has_network = network_functions.iter().any(|&func| {
390 analysis.imports.contains(&func.to_string())
391 || analysis.detected_symbols.contains(&func.to_string())
392 });
393
394 if has_network {
395 for string in &analysis.embedded_strings {
397 let mut endpoints = Vec::new();
398 let mut suspicious_indicators = Vec::new();
399
400 if is_ip_address(string) || is_domain_name(string) {
402 endpoints.push(string.clone());
403 }
404
405 if let Some(port) = extract_port_number(string) {
407 if is_suspicious_port(port) {
408 patterns.push(NetworkPattern {
409 pattern_type: NetworkPatternType::SuspiciousPort,
410 endpoints: vec![string.clone()],
411 frequency: None,
412 suspicious_indicators: vec![format!("Suspicious port: {}", port)],
413 });
414 }
415 }
416
417 if string.len() > 50
419 && string.contains('.')
420 && string.chars().filter(|&c| c == '.').count() > 5
421 {
422 patterns.push(NetworkPattern {
423 pattern_type: NetworkPatternType::DNSTunneling,
424 endpoints: vec![string.clone()],
425 frequency: None,
426 suspicious_indicators: vec!["Unusually long domain name".to_string()],
427 });
428 }
429
430 if is_likely_base64(string) && string.len() > 20 {
432 suspicious_indicators.push("Base64-encoded data".to_string());
433 patterns.push(NetworkPattern {
434 pattern_type: NetworkPatternType::CommandAndControl,
435 endpoints: vec![string.clone()],
436 frequency: None,
437 suspicious_indicators,
438 });
439 }
440 }
441
442 if !endpoints_found(&patterns) {
444 patterns.push(NetworkPattern {
445 pattern_type: NetworkPatternType::CommandAndControl,
446 endpoints: vec!["Network functions detected".to_string()],
447 frequency: None,
448 suspicious_indicators: vec!["Generic network capability".to_string()],
449 });
450 }
451 }
452
453 let has_timer = analysis
455 .imports
456 .iter()
457 .any(|s| s.contains("sleep") || s.contains("timer") || s.contains("delay"));
458
459 if has_network && has_timer {
460 patterns.push(NetworkPattern {
461 pattern_type: NetworkPatternType::Beaconing,
462 endpoints: vec!["Timer + Network functions".to_string()],
463 frequency: None,
464 suspicious_indicators: vec!["Periodic network communication pattern".to_string()],
465 });
466 }
467
468 patterns
469}
470
471fn analyze_data_flow_issues(analysis: &BinaryAnalysis) -> Vec<DataFlowIssue> {
472 let mut issues = Vec::new();
473
474 let input_functions = ["scanf", "gets", "fgets", "getchar", "recv", "recvfrom"];
476 let output_functions = ["printf", "fprintf", "sprintf", "send", "sendto"];
477
478 let has_input = input_functions.iter().any(|&func| {
479 analysis.imports.contains(&func.to_string())
480 || analysis.detected_symbols.contains(&func.to_string())
481 });
482
483 let has_output = output_functions.iter().any(|&func| {
484 analysis.imports.contains(&func.to_string())
485 || analysis.detected_symbols.contains(&func.to_string())
486 });
487
488 if has_input {
490 for func in &input_functions {
491 if analysis.imports.contains(&func.to_string())
492 || analysis.detected_symbols.contains(&func.to_string())
493 {
494 let severity = match *func {
495 "gets" => SeverityLevel::Critical,
496 "scanf" => SeverityLevel::High,
497 _ => SeverityLevel::Medium,
498 };
499
500 issues.push(DataFlowIssue {
501 issue_type: DataFlowIssueType::UncontrolledInput,
502 source: CodeLocation {
503 file_path: analysis.file_name.clone(),
504 line_number: None,
505 column_number: None,
506 function_name: Some(func.to_string()),
507 binary_offset: None,
508 },
509 sink: CodeLocation {
510 file_path: analysis.file_name.clone(),
511 line_number: None,
512 column_number: None,
513 function_name: Some("unknown".to_string()),
514 binary_offset: None,
515 },
516 severity,
517 data_path: vec![func.to_string()],
518 });
519 }
520 }
521 }
522
523 if has_input && has_output {
525 issues.push(DataFlowIssue {
526 issue_type: DataFlowIssueType::DataLeakage,
527 source: CodeLocation {
528 file_path: analysis.file_name.clone(),
529 line_number: None,
530 column_number: None,
531 function_name: Some("input_functions".to_string()),
532 binary_offset: None,
533 },
534 sink: CodeLocation {
535 file_path: analysis.file_name.clone(),
536 line_number: None,
537 column_number: None,
538 function_name: Some("output_functions".to_string()),
539 binary_offset: None,
540 },
541 severity: SeverityLevel::Medium,
542 data_path: vec![
543 "input".to_string(),
544 "processing".to_string(),
545 "output".to_string(),
546 ],
547 });
548 }
549
550 let privilege_functions = ["setuid", "setgid", "seteuid", "setegid", "sudo", "su"];
552 for func in &privilege_functions {
553 if analysis.imports.contains(&func.to_string())
554 || analysis.detected_symbols.contains(&func.to_string())
555 {
556 issues.push(DataFlowIssue {
557 issue_type: DataFlowIssueType::PrivilegeEscalation,
558 source: CodeLocation {
559 file_path: analysis.file_name.clone(),
560 line_number: None,
561 column_number: None,
562 function_name: Some("user_input".to_string()),
563 binary_offset: None,
564 },
565 sink: CodeLocation {
566 file_path: analysis.file_name.clone(),
567 line_number: None,
568 column_number: None,
569 function_name: Some(func.to_string()),
570 binary_offset: None,
571 },
572 severity: SeverityLevel::High,
573 data_path: vec!["user_input".to_string(), func.to_string()],
574 });
575 }
576 }
577
578 let system_functions = ["system", "exec", "popen"];
580 if has_input {
581 for func in &system_functions {
582 if analysis.imports.contains(&func.to_string())
583 || analysis.detected_symbols.contains(&func.to_string())
584 {
585 issues.push(DataFlowIssue {
586 issue_type: DataFlowIssueType::UnsanitizedOutput,
587 source: CodeLocation {
588 file_path: analysis.file_name.clone(),
589 line_number: None,
590 column_number: None,
591 function_name: Some("user_input".to_string()),
592 binary_offset: None,
593 },
594 sink: CodeLocation {
595 file_path: analysis.file_name.clone(),
596 line_number: None,
597 column_number: None,
598 function_name: Some(func.to_string()),
599 binary_offset: None,
600 },
601 severity: SeverityLevel::Critical,
602 data_path: vec!["user_input".to_string(), func.to_string()],
603 });
604 }
605 }
606 }
607
608 issues
609}
610
611fn is_ip_address(s: &str) -> bool {
614 let parts: Vec<&str> = s.split('.').collect();
615 if parts.len() != 4 {
616 return false;
617 }
618
619 for part in parts {
620 if part.parse::<u8>().is_err() {
621 return false;
622 }
623 }
624 true
625}
626
627fn is_domain_name(s: &str) -> bool {
628 s.contains('.')
629 && s.len() > 4
630 && s.len() < 255
631 && !s.contains(' ')
632 && s.chars()
633 .all(|c| c.is_alphanumeric() || c == '.' || c == '-')
634}
635
636fn extract_port_number(s: &str) -> Option<u16> {
637 if let Some(colon_pos) = s.rfind(':') {
639 if let Ok(port) = s[colon_pos + 1..].parse::<u16>() {
640 return Some(port);
641 }
642 }
643
644 if let Ok(port) = s.parse::<u16>() {
646 if port > 0 {
647 return Some(port);
648 }
649 }
650
651 None
652}
653
654fn is_suspicious_port(port: u16) -> bool {
655 matches!(
657 port,
658 31337 | 12345 | 54321 | 9999 | 4444 | 5555 | 7777 | 8888 | 6666 | 666 | 1234 | 4321 | 31338 | 31339 )
664}
665
666fn is_likely_base64(s: &str) -> bool {
667 if s.len() < 4 || s.len() % 4 != 0 {
668 return false;
669 }
670
671 s.chars()
672 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
673 && s.chars().filter(|&c| c == '=').count() <= 2
674}
675
676fn endpoints_found(patterns: &[NetworkPattern]) -> bool {
677 patterns
678 .iter()
679 .any(|p| !p.endpoints.is_empty() && !p.endpoints.iter().all(|e| e.contains("functions")))
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use chrono::Utc;
686
687 fn create_test_analysis() -> BinaryAnalysis {
688 BinaryAnalysis {
689 id: Uuid::new_v4(),
690 file_name: "test.bin".to_string(),
691 format: "elf".to_string(),
692 architecture: "x86_64".to_string(),
693 languages: vec!["C".to_string()],
694 detected_symbols: vec!["socket".to_string(), "ptrace".to_string()],
695 embedded_strings: vec!["192.168.1.1".to_string(), "vmware".to_string()],
696 suspected_secrets: vec![],
697 imports: vec![
698 "connect".to_string(),
699 "recv".to_string(),
700 "gets".to_string(),
701 ],
702 exports: vec![],
703 hash_sha256: "test".to_string(),
704 hash_blake3: None,
705 size_bytes: 1024,
706 linked_libraries: vec!["libc.so.6".to_string()],
707 static_linked: false,
708 version_info: None,
709 license_info: None,
710 metadata: serde_json::json!({}),
711 created_at: Utc::now(),
712 sbom: None,
713 binary_data: Some(vec![0x7f, 0x45, 0x4c, 0x46]),
714 entry_point: Some("0x401000".to_string()),
715 code_sections: vec![],
716 }
717 }
718
719 #[test]
720 fn test_analyze_behavioral_security() {
721 let analysis = create_test_analysis();
722 let result = analyze_behavioral_security(&analysis);
723
724 assert_eq!(result.file_path, "test.bin");
725 assert!(!result.control_flow_anomalies.is_empty());
726 assert!(!result.network_patterns.is_empty());
727 assert!(!result.data_flow_issues.is_empty());
728 }
729
730 #[test]
731 fn test_is_ip_address() {
732 assert!(is_ip_address("192.168.1.1"));
733 assert!(is_ip_address("127.0.0.1"));
734 assert!(!is_ip_address("256.1.1.1"));
735 assert!(!is_ip_address("not.an.ip"));
736 }
737
738 #[test]
739 fn test_is_suspicious_port() {
740 assert!(is_suspicious_port(31337));
741 assert!(is_suspicious_port(12345));
742 assert!(!is_suspicious_port(80));
743 assert!(!is_suspicious_port(443));
744 }
745}