1use crate::types::{AccessType, CpuType, ExploitabilityRating};
2
3#[derive(Debug, Clone)]
5pub struct ExploitabilityResult {
6 pub rating: ExploitabilityRating,
8 pub signal: u32,
10 pub real_exception: i32,
12 pub access_type: AccessType,
14 pub access_address: u64,
16 pub pc: u64,
18 pub disassembly: String,
20 pub messages: Vec<String>,
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct ClassifyConfig {
27 pub exploitable_reads: bool,
29 pub exploitable_jit: bool,
31 pub ignore_frame_pointer: bool,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum StackVerdict {
38 NoChange,
40 ChangeToExploitable,
42 ChangeToNotExploitable,
44}
45
46const SUSPICIOUS_FUNCTIONS: &[&str] = &[
49 "__stack_chk_fail",
50 "szone_error",
51 "CFRelease",
52 "CFRetain",
53 "malloc",
54 "calloc",
55 "realloc",
56 "objc_msgSend",
57 "objc_msgSend_stret",
58 "objc_msgSendSuper",
59 "objc_msgSendSuper_stret",
60 "objc_msgSend_fpret",
61 "szone_free",
62 "free_small",
63 "tiny_free_list_add_ptr",
64 "small_free_list_add_ptr",
65 "large_entries_free_no_lock",
66 "free",
67 "CSMemDisposeHandle",
68 "CSMemDisposePtr",
69 "WTF::fastFree",
70 "WTF::fastMalloc",
71 "WTFCrashWithSecurityImplication",
72 "__chk_fail_overflow",
73];
74
75const NON_EXPLOITABLE_FUNCTIONS: &[&str] = &["ABORTING_DUE_TO_OUT_OF_MEMORY"];
77
78const WTFCRASH_ADDR: u64 = 0xbbad_beef;
80
81fn page_size_for_cpu(cpu_type: CpuType) -> u64 {
84 if cpu_type == CpuType::ARM64 || cpu_type == CpuType::ARM {
85 0x4000
86 } else {
87 0x1000
88 }
89}
90
91fn is_x86(cpu_type: CpuType) -> bool {
93 cpu_type == CpuType::X86 || cpu_type == CpuType::X86_64
94}
95
96fn is_arm(cpu_type: CpuType) -> bool {
98 cpu_type == CpuType::ARM || cpu_type == CpuType::ARM64
99}
100
101pub fn classify_exception(
106 exception_type: i32,
107 exception_codes: &[i64],
108 disassembly: &str,
109 pc: u64,
110 cpu_type: CpuType,
111 config: &ClassifyConfig,
112) -> ExploitabilityResult {
113 let mut result = ExploitabilityResult {
114 rating: ExploitabilityRating::Unknown,
115 signal: 0,
116 real_exception: exception_type,
117 access_type: AccessType::Unknown,
118 access_address: 0,
119 pc,
120 disassembly: disassembly.to_string(),
121 messages: Vec::new(),
122 };
123
124 if exception_codes.len() > 1 {
126 result.access_address = exception_codes[1] as u64;
127 }
128
129 match exception_type {
131 1 => {
132 result.signal = 11; if !exception_codes.is_empty() {
135 match exception_codes[0] {
136 1 => {
137 result.signal = 11; }
140 2 => {
141 result.signal = 10; }
144 _ => {}
145 }
146 }
147
148 result.access_type = get_access_type(disassembly, cpu_type);
150
151 let page_size = page_size_for_cpu(cpu_type);
153 if result.access_address < page_size * 8 {
154 result.rating = ExploitabilityRating::NotExploitable;
155 result.messages.push(format!(
156 "Near-null dereference at 0x{:x} — not exploitable",
157 result.access_address
158 ));
159 return result;
160 }
161
162 if result.access_address == WTFCRASH_ADDR {
164 result.rating = ExploitabilityRating::NotExploitable;
165 result
166 .messages
167 .push("WTFCrash (0xbbadbeef) — not exploitable".into());
168 return result;
169 }
170
171 if result.access_type == AccessType::Write {
173 result.rating = ExploitabilityRating::Exploitable;
174 result.messages.push(format!(
175 "Write to 0x{:x} — exploitable",
176 result.access_address
177 ));
178 return result;
179 }
180
181 if result.access_type == AccessType::Exec {
183 result.rating = ExploitabilityRating::Exploitable;
184 result.messages.push(format!(
185 "Execution at 0x{:x} — exploitable",
186 result.access_address
187 ));
188 return result;
189 }
190
191 if result.access_type == AccessType::Read {
193 if config.exploitable_reads {
194 result.rating = ExploitabilityRating::Exploitable;
195 result.messages.push(format!(
196 "Read from 0x{:x} — exploitable (CR_EXPLOITABLE_READS)",
197 result.access_address
198 ));
199 } else {
200 result.rating = ExploitabilityRating::NotExploitable;
201 result.messages.push(format!(
202 "Read from 0x{:x} — not exploitable",
203 result.access_address
204 ));
205 }
206 return result;
207 }
208
209 result.rating = ExploitabilityRating::Unknown;
211 result
212 .messages
213 .push("Could not determine access type".into());
214 }
215 2 => {
216 result.signal = 4; result.rating = ExploitabilityRating::Exploitable;
219 result.messages.push("Bad instruction — exploitable".into());
220 }
221 3 => {
222 result.signal = 8; result.rating = ExploitabilityRating::NotExploitable;
225 result
226 .messages
227 .push("Arithmetic exception — not exploitable".into());
228 }
229 5 => {
230 result.signal = 6; result.rating = ExploitabilityRating::NotExploitable;
233 result
234 .messages
235 .push("Software exception (abort) — not exploitable".into());
236 }
237 6 => {
238 result.signal = 5; let is_trap = if !disassembly.is_empty() {
244 if is_arm(cpu_type) {
245 disassembly.contains("brk")
246 } else if is_x86(cpu_type) {
247 disassembly.contains("int3")
248 } else {
249 disassembly.contains("brk") || disassembly.contains("int3")
250 }
251 } else {
252 false
253 };
254
255 if is_trap {
256 result.rating = ExploitabilityRating::NotExploitable;
257 result
258 .messages
259 .push("Breakpoint trap — not exploitable".into());
260 } else {
261 result.rating = ExploitabilityRating::Exploitable;
262 result
263 .messages
264 .push("EXC_BREAKPOINT (non-trap) — exploitable".into());
265 }
266 }
267 10 => {
268 result.signal = 6; result.rating = ExploitabilityRating::NotExploitable;
271 result
272 .messages
273 .push("EXC_CRASH (undemuxed) — not exploitable".into());
274 }
275 _ => {
276 result.signal = 6; result.rating = ExploitabilityRating::Unknown;
279 result.messages.push(format!(
280 "Unknown exception type {} — unknown exploitability",
281 exception_type
282 ));
283 }
284 }
285
286 result
287}
288
289pub fn is_stack_suspicious(
296 crash_log: &str,
297 access_address: u64,
298 exception_type: i32,
299 cpu_type: CpuType,
300 config: &ClassifyConfig,
301) -> StackVerdict {
302 let frame_lines: Vec<&str> = crash_log
306 .lines()
307 .filter(|l| {
308 let trimmed = l.trim();
309 let mut chars = trimmed.chars();
310 if !matches!(chars.next(), Some(c) if c.is_ascii_digit()) {
312 return false;
313 }
314 for c in chars {
316 if c == ' ' || c == '\t' {
317 return true;
318 }
319 if !c.is_ascii_digit() {
320 return false;
321 }
322 }
323 false
324 })
325 .collect();
326
327 if frame_lines.len() > 300 {
329 return StackVerdict::ChangeToNotExploitable;
330 }
331
332 let frame_text: String = frame_lines.join("\n");
335
336 for func in NON_EXPLOITABLE_FUNCTIONS {
338 if frame_text.contains(func) {
339 return StackVerdict::ChangeToNotExploitable;
340 }
341 }
342
343 for line in &frame_lines {
345 let trimmed = line.trim();
346 if trimmed.starts_with("0 ") || trimmed.starts_with("0\t") {
347 if trimmed.contains("libdispatch") || trimmed.contains("libxpc") {
348 return StackVerdict::ChangeToNotExploitable;
349 }
350 break;
351 }
352 }
353
354 for func in SUSPICIOUS_FUNCTIONS {
356 if frame_text.contains(func) {
357 if *func == "__stack_chk_fail" || *func == "WTFCrashWithSecurityImplication" {
359 return StackVerdict::ChangeToExploitable;
360 }
361 if exception_type == 1 {
363 return StackVerdict::ChangeToExploitable;
364 }
365 }
366 }
367
368 if config.exploitable_jit {
370 for line in &frame_lines {
371 let trimmed = line.trim();
372 if (trimmed.starts_with("0 ") || trimmed.starts_with("0\t")) && trimmed.contains("???")
373 {
374 return StackVerdict::ChangeToExploitable;
375 }
376 }
377 }
378
379 if access_address != 0 {
382 let addr_bytes = access_address.to_be_bytes();
383
384 if addr_bytes[0] != 0 && addr_bytes.iter().all(|&b| b == addr_bytes[0]) {
387 return StackVerdict::ChangeToExploitable;
388 }
389
390 if access_address > 0xFFFF {
393 let lo = (access_address & 0xFFFF) as u16;
394 let hi = ((access_address >> 16) & 0xFFFF) as u16;
395 let hi2 = ((access_address >> 32) & 0xFFFF) as u16;
396 let hi3 = ((access_address >> 48) & 0xFFFF) as u16;
397 if lo != 0 && lo == hi && lo == hi2 && lo == hi3 {
398 return StackVerdict::ChangeToExploitable;
399 }
400 }
401
402 if cpu_type == CpuType::X86_64 {
406 let bit47 = (access_address >> 47) & 1;
407 let high_bits = access_address >> 48;
408 if high_bits != 0 && high_bits != 0xFFFF && bit47 == 0 {
409 return StackVerdict::ChangeToExploitable;
410 }
411 }
412
413 if cpu_type == CpuType::ARM64 {
420 let top_byte = (access_address >> 56) as u8;
421 let pac_bits = (access_address >> 48) & 0xFF;
422 let user_bits = access_address & 0x0000_FFFF_FFFF_FFFF;
423
424 if access_address > 0x0000_7FFF_FFFF_FFFF && access_address < 0xFFFF_FE00_0000_0000 {
428 return StackVerdict::ChangeToExploitable;
429 }
430
431 if pac_bits != 0 && user_bits < 0x0000_8000_0000_0000 && top_byte == 0 {
436 return StackVerdict::ChangeToExploitable;
437 }
438 }
439 }
440
441 StackVerdict::NoChange
442}
443
444fn get_access_type(disassembly: &str, cpu_type: CpuType) -> AccessType {
447 if disassembly.is_empty() {
448 return AccessType::Unknown;
449 }
450
451 if is_arm(cpu_type) {
452 get_access_type_arm64(disassembly)
453 } else if is_x86(cpu_type) {
454 get_access_type_x86(disassembly)
455 } else {
456 AccessType::Unknown
457 }
458}
459
460pub fn get_access_type_arm64(disassembly: &str) -> AccessType {
462 if disassembly.is_empty() {
463 return AccessType::Unknown;
464 }
465
466 let mnemonic = disassembly
468 .split_whitespace()
469 .next()
470 .unwrap_or("")
471 .to_lowercase();
472
473 if mnemonic.starts_with("str")
475 || mnemonic.starts_with("stp")
476 || mnemonic.starts_with("stur")
477 || mnemonic.starts_with("stlr")
478 || mnemonic.starts_with("stxr")
479 || mnemonic.starts_with("stlxr")
480 || mnemonic.starts_with("st1")
481 || mnemonic.starts_with("st2")
482 || mnemonic.starts_with("st3")
483 || mnemonic.starts_with("st4")
484 {
485 return AccessType::Write;
486 }
487
488 if mnemonic.starts_with("ldr")
490 || mnemonic.starts_with("ldp")
491 || mnemonic.starts_with("ldur")
492 || mnemonic.starts_with("ldar")
493 || mnemonic.starts_with("ldxr")
494 || mnemonic.starts_with("ldaxr")
495 || mnemonic.starts_with("ld1")
496 || mnemonic.starts_with("ld2")
497 || mnemonic.starts_with("ld3")
498 || mnemonic.starts_with("ld4")
499 {
500 return AccessType::Read;
501 }
502
503 if mnemonic == "bl"
505 || mnemonic == "blr"
506 || mnemonic == "br"
507 || mnemonic == "ret"
508 || mnemonic == "b"
509 {
510 return AccessType::Exec;
511 }
512
513 AccessType::Unknown
514}
515
516pub fn get_access_type_x86(disassembly: &str) -> AccessType {
518 if disassembly.is_empty() {
519 return AccessType::Unknown;
520 }
521
522 let lower = disassembly.to_lowercase();
523
524 if lower.starts_with("mov") && lower.contains("],") {
526 return AccessType::Write;
527 }
528 if lower.starts_with("mov") && lower.contains("[") {
530 return AccessType::Read;
531 }
532
533 if lower.starts_with("push") {
535 return AccessType::Write;
536 }
537 if lower.starts_with("pop") {
538 return AccessType::Read;
539 }
540
541 if lower.starts_with("call") || lower.starts_with("jmp") || lower.starts_with("ret") {
543 return AccessType::Exec;
544 }
545
546 AccessType::Unknown
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::types::{AccessType, ExploitabilityRating};
553
554 mod classify {
555 use super::*;
556
557 #[test]
558 fn null_deref_not_exploitable_arm64() {
559 let r = classify_exception(
560 1,
561 &[1, 0x10],
562 "",
563 0x1000,
564 CpuType::ARM64,
565 &ClassifyConfig::default(),
566 );
567 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
568 assert_eq!(r.signal, 11);
569 }
570
571 #[test]
572 fn near_null_deref_arm64_page_boundary() {
573 let r = classify_exception(
575 1,
576 &[1, 0x1_FFFF],
577 "",
578 0x1000,
579 CpuType::ARM64,
580 &ClassifyConfig::default(),
581 );
582 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
583 }
584
585 #[test]
586 fn near_null_deref_x86_page_boundary() {
587 let r = classify_exception(
590 1,
591 &[1, 0x7FFF],
592 "",
593 0x1000,
594 CpuType::X86_64,
595 &ClassifyConfig::default(),
596 );
597 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
598
599 let r2 = classify_exception(
602 1,
603 &[1, 0x8000],
604 "mov eax, [ecx]",
605 0x1000,
606 CpuType::X86_64,
607 &ClassifyConfig::default(),
608 );
609 assert_ne!(r2.rating, ExploitabilityRating::Unknown);
611 }
612
613 #[test]
614 fn x86_higher_null_deref_not_caught_as_arm64() {
615 let r = classify_exception(
619 1,
620 &[1, 0x9000],
621 "mov eax, [ecx]",
622 0x1000,
623 CpuType::X86_64,
624 &ClassifyConfig::default(),
625 );
626 assert_eq!(r.access_type, AccessType::Read);
628 }
629
630 #[test]
631 fn wtfcrash_not_exploitable() {
632 let r = classify_exception(
633 1,
634 &[1, 0xbbad_beef],
635 "",
636 0x1000,
637 CpuType::ARM64,
638 &ClassifyConfig::default(),
639 );
640 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
641 }
642
643 #[test]
644 fn write_access_exploitable_arm64() {
645 let r = classify_exception(
646 1,
647 &[2, 0x4141_4141],
648 "str x0, [x1]",
649 0x1000,
650 CpuType::ARM64,
651 &ClassifyConfig::default(),
652 );
653 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
654 assert_eq!(r.access_type, AccessType::Write);
655 }
656
657 #[test]
658 fn write_access_exploitable_x86() {
659 let r = classify_exception(
660 1,
661 &[2, 0x4141_4141],
662 "mov dword ptr [eax], ecx",
663 0x1000,
664 CpuType::X86_64,
665 &ClassifyConfig::default(),
666 );
667 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
668 assert_eq!(r.access_type, AccessType::Write);
669 }
670
671 #[test]
672 fn exec_access_exploitable_arm64() {
673 let r = classify_exception(
674 1,
675 &[2, 0x4141_4141],
676 "blr x8",
677 0x1000,
678 CpuType::ARM64,
679 &ClassifyConfig::default(),
680 );
681 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
682 assert_eq!(r.access_type, AccessType::Exec);
683 }
684
685 #[test]
686 fn exec_access_exploitable_x86() {
687 let r = classify_exception(
688 1,
689 &[2, 0x4141_4141],
690 "call rax",
691 0x1000,
692 CpuType::X86_64,
693 &ClassifyConfig::default(),
694 );
695 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
696 assert_eq!(r.access_type, AccessType::Exec);
697 }
698
699 #[test]
700 fn read_access_not_exploitable_by_default() {
701 let r = classify_exception(
702 1,
703 &[1, 0x4141_4141],
704 "ldr x0, [x1]",
705 0x1000,
706 CpuType::ARM64,
707 &ClassifyConfig::default(),
708 );
709 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
710 assert_eq!(r.access_type, AccessType::Read);
711 }
712
713 #[test]
714 fn read_access_exploitable_with_config() {
715 let config = ClassifyConfig {
716 exploitable_reads: true,
717 ..ClassifyConfig::default()
718 };
719 let r = classify_exception(
720 1,
721 &[1, 0x4141_4141],
722 "ldr x0, [x1]",
723 0x1000,
724 CpuType::ARM64,
725 &config,
726 );
727 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
728 }
729
730 #[test]
731 fn bad_instruction_exploitable() {
732 let r = classify_exception(
733 2,
734 &[1],
735 "",
736 0x1000,
737 CpuType::ARM64,
738 &ClassifyConfig::default(),
739 );
740 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
741 assert_eq!(r.signal, 4);
742 }
743
744 #[test]
745 fn arithmetic_not_exploitable() {
746 let r = classify_exception(
747 3,
748 &[1],
749 "",
750 0x1000,
751 CpuType::ARM64,
752 &ClassifyConfig::default(),
753 );
754 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
755 assert_eq!(r.signal, 8);
756 }
757
758 #[test]
759 fn software_exception_not_exploitable() {
760 let r = classify_exception(
761 5,
762 &[1],
763 "",
764 0x1000,
765 CpuType::ARM64,
766 &ClassifyConfig::default(),
767 );
768 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
769 assert_eq!(r.signal, 6);
770 }
771
772 #[test]
773 fn breakpoint_brk_not_exploitable_on_arm64() {
774 let r = classify_exception(
775 6,
776 &[1],
777 "brk #0x1",
778 0x1000,
779 CpuType::ARM64,
780 &ClassifyConfig::default(),
781 );
782 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
783 assert_eq!(r.signal, 5);
784 }
785
786 #[test]
787 fn breakpoint_int3_not_exploitable_on_x86() {
788 let r = classify_exception(
789 6,
790 &[1],
791 "int3",
792 0x1000,
793 CpuType::X86_64,
794 &ClassifyConfig::default(),
795 );
796 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
797 }
798
799 #[test]
800 fn breakpoint_brk_exploitable_on_x86() {
801 let r = classify_exception(
803 6,
804 &[1],
805 "brk #0x1",
806 0x1000,
807 CpuType::X86_64,
808 &ClassifyConfig::default(),
809 );
810 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
811 }
812
813 #[test]
814 fn breakpoint_int3_exploitable_on_arm64() {
815 let r = classify_exception(
817 6,
818 &[1],
819 "int3",
820 0x1000,
821 CpuType::ARM64,
822 &ClassifyConfig::default(),
823 );
824 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
825 }
826
827 #[test]
828 fn breakpoint_non_trap_exploitable() {
829 let r = classify_exception(
830 6,
831 &[1],
832 "",
833 0x1000,
834 CpuType::ARM64,
835 &ClassifyConfig::default(),
836 );
837 assert_eq!(r.rating, ExploitabilityRating::Exploitable);
838 }
839
840 #[test]
841 fn exc_crash_not_exploitable() {
842 let r = classify_exception(
843 10,
844 &[0],
845 "",
846 0x1000,
847 CpuType::ARM64,
848 &ClassifyConfig::default(),
849 );
850 assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
851 }
852
853 #[test]
854 fn unknown_exception_unknown_rating() {
855 let r = classify_exception(
856 99,
857 &[0],
858 "",
859 0x1000,
860 CpuType::ARM64,
861 &ClassifyConfig::default(),
862 );
863 assert_eq!(r.rating, ExploitabilityRating::Unknown);
864 }
865
866 #[test]
867 fn protection_failure_signal_is_sigbus() {
868 let r = classify_exception(
869 1,
870 &[2, 0x4141_4141],
871 "str x0, [x1]",
872 0x1000,
873 CpuType::ARM64,
874 &ClassifyConfig::default(),
875 );
876 assert_eq!(r.signal, 10); }
878
879 #[test]
880 fn arm64_disasm_not_recognized_on_x86() {
881 let r = classify_exception(
883 1,
884 &[2, 0x4141_4141],
885 "str x0, [x1]",
886 0x1000,
887 CpuType::X86_64,
888 &ClassifyConfig::default(),
889 );
890 assert_eq!(r.access_type, AccessType::Unknown);
891 }
892
893 #[test]
894 fn x86_disasm_not_recognized_on_arm64() {
895 let r = classify_exception(
897 1,
898 &[2, 0x4141_4141],
899 "push rbp",
900 0x1000,
901 CpuType::ARM64,
902 &ClassifyConfig::default(),
903 );
904 assert_eq!(r.access_type, AccessType::Unknown);
905 }
906 }
907
908 mod stack_suspicious {
909 use super::*;
910
911 #[test]
944 fn recursion_not_exploitable() {
945 let mut log = String::new();
946 for i in 0..301 {
947 log.push_str(&format!("{i} libfoo.dylib 0x1000 foo + 0\n"));
948 }
949 let v =
950 is_stack_suspicious(&log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
951 assert_eq!(v, StackVerdict::ChangeToNotExploitable);
952 }
953
954 #[test]
955 fn out_of_memory_not_exploitable() {
956 let log = "0 libfoo.dylib 0x1000 ABORTING_DUE_TO_OUT_OF_MEMORY + 0\n";
957 let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
958 assert_eq!(v, StackVerdict::ChangeToNotExploitable);
959 }
960
961 #[test]
962 fn libdispatch_frame0_not_exploitable() {
963 let log = "0 libdispatch.dylib 0x1000 _dispatch_main + 0\n1 libfoo.dylib 0x2000 main + 0\n";
964 let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
965 assert_eq!(v, StackVerdict::ChangeToNotExploitable);
966 }
967
968 #[test]
969 fn stack_chk_fail_exploitable() {
970 let log = "0 libSystem.B.dylib 0x1000 __stack_chk_fail + 0\n1 libfoo.dylib 0x2000 bar + 0\n";
971 let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
972 assert_eq!(v, StackVerdict::ChangeToExploitable);
973 }
974
975 #[test]
976 fn wtf_security_exploitable() {
977 let log = "0 WebCore 0x1000 WTFCrashWithSecurityImplication + 0\n";
978 let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
979 assert_eq!(v, StackVerdict::ChangeToExploitable);
980 }
981
982 #[test]
983 fn repeating_byte_address_exploitable() {
984 let v = is_stack_suspicious(
985 "0 libfoo.dylib 0x1000 main + 0\n",
986 0x4141_4141_4141_4141,
987 1,
988 CpuType::ARM64,
989 &ClassifyConfig::default(),
990 );
991 assert_eq!(v, StackVerdict::ChangeToExploitable);
992 }
993
994 #[test]
995 fn normal_stack_no_change() {
996 let log = "0 libfoo.dylib 0x1000 main + 0\n1 libbar.dylib 0x2000 start + 0\n";
997 let v = is_stack_suspicious(
998 log,
999 0x7fff_1234,
1000 1,
1001 CpuType::ARM64,
1002 &ClassifyConfig::default(),
1003 );
1004 assert_eq!(v, StackVerdict::NoChange);
1005 }
1006
1007 #[test]
1008 fn jit_frame0_exploitable_with_config() {
1009 let config = ClassifyConfig {
1010 exploitable_jit: true,
1011 ..ClassifyConfig::default()
1012 };
1013 let log = "0 ??? 0x1000 0x1000 + 0\n";
1014 let v = is_stack_suspicious(log, 0x7fff_1234, 1, CpuType::ARM64, &config);
1015 assert_eq!(v, StackVerdict::ChangeToExploitable);
1016 }
1017
1018 #[test]
1019 fn jit_frame0_not_flagged_without_config() {
1020 let log = "0 ??? 0x1000 0x1000 + 0\n";
1021 let v = is_stack_suspicious(
1022 log,
1023 0x7fff_1234,
1024 1,
1025 CpuType::ARM64,
1026 &ClassifyConfig::default(),
1027 );
1028 assert_eq!(v, StackVerdict::NoChange);
1029 }
1030
1031 #[test]
1032 fn non_canonical_x86_64_exploitable() {
1033 let v = is_stack_suspicious(
1035 "0 libfoo.dylib 0x1000 main + 0\n",
1036 0x0001_0000_0000_0000, 1,
1038 CpuType::X86_64,
1039 &ClassifyConfig::default(),
1040 );
1041 assert_eq!(v, StackVerdict::ChangeToExploitable);
1042 }
1043
1044 #[test]
1045 fn non_canonical_not_checked_on_arm64() {
1046 let v = is_stack_suspicious(
1049 "0 libfoo.dylib 0x1000 main + 0\n",
1050 0x0001_0000_0000_0000,
1051 1,
1052 CpuType::ARM64,
1053 &ClassifyConfig::default(),
1054 );
1055 assert_eq!(v, StackVerdict::ChangeToExploitable);
1057 }
1058
1059 #[test]
1064 fn two_byte_repeating_pattern_exploitable() {
1065 let v = is_stack_suspicious(
1067 "0 libfoo.dylib 0x1000 main + 0\n",
1068 0x4142_4142_4142_4142,
1069 1,
1070 CpuType::ARM64,
1071 &ClassifyConfig::default(),
1072 );
1073 assert_eq!(v, StackVerdict::ChangeToExploitable);
1074 }
1075
1076 #[test]
1077 fn two_byte_non_repeating_no_match() {
1078 let v = is_stack_suspicious(
1080 "0 libfoo.dylib 0x1000 main + 0\n",
1081 0x4142_4143_4142_4142,
1082 1,
1083 CpuType::X86_64,
1084 &ClassifyConfig::default(),
1085 );
1086 assert_eq!(v, StackVerdict::ChangeToExploitable);
1089 }
1090
1091 #[test]
1096 fn arm64_unmapped_gap_exploitable() {
1097 let v = is_stack_suspicious(
1100 "0 libfoo.dylib 0x1000 main + 0\n",
1101 0x0000_8000_0000_0000, 1,
1103 CpuType::ARM64,
1104 &ClassifyConfig::default(),
1105 );
1106 assert_eq!(v, StackVerdict::ChangeToExploitable);
1107 }
1108
1109 #[test]
1110 fn arm64_deep_gap_exploitable() {
1111 let v = is_stack_suspicious(
1113 "0 libfoo.dylib 0x1000 main + 0\n",
1114 0x4141_0000_0000_0000,
1115 1,
1116 CpuType::ARM64,
1117 &ClassifyConfig::default(),
1118 );
1119 assert_eq!(v, StackVerdict::ChangeToExploitable);
1120 }
1121
1122 #[test]
1123 fn arm64_valid_user_address_no_change() {
1124 let v = is_stack_suspicious(
1126 "0 libfoo.dylib 0x1000 main + 0\n",
1127 0x0000_0001_0000_0000,
1128 1,
1129 CpuType::ARM64,
1130 &ClassifyConfig::default(),
1131 );
1132 assert_eq!(v, StackVerdict::NoChange);
1133 }
1134
1135 #[test]
1136 fn arm64_kernel_address_no_false_positive() {
1137 let v = is_stack_suspicious(
1139 "0 libfoo.dylib 0x1000 main + 0\n",
1140 0xFFFF_FE00_0000_0000,
1141 1,
1142 CpuType::ARM64,
1143 &ClassifyConfig::default(),
1144 );
1145 assert_eq!(v, StackVerdict::NoChange);
1148 }
1149
1150 #[test]
1155 fn arm64_pac_bits_on_user_address_exploitable() {
1156 let v = is_stack_suspicious(
1159 "0 libfoo.dylib 0x1000 main + 0\n",
1160 0x0012_0000_1234_5678, 1,
1162 CpuType::ARM64,
1163 &ClassifyConfig::default(),
1164 );
1165 assert_eq!(v, StackVerdict::ChangeToExploitable);
1166 }
1167
1168 #[test]
1169 fn arm64_pac_bits_not_checked_on_x86() {
1170 let v = is_stack_suspicious(
1172 "0 libfoo.dylib 0x1000 main + 0\n",
1173 0x0012_0000_1234_5678,
1174 1,
1175 CpuType::X86_64,
1176 &ClassifyConfig::default(),
1177 );
1178 assert_eq!(v, StackVerdict::ChangeToExploitable);
1180 }
1181 }
1182
1183 mod access_type_detection {
1184 use super::*;
1185
1186 #[test]
1187 fn arm64_store_is_write() {
1188 assert_eq!(get_access_type_arm64("str x0, [x1]"), AccessType::Write);
1189 assert_eq!(get_access_type_arm64("stp x0, x1, [sp]"), AccessType::Write);
1190 assert_eq!(
1191 get_access_type_arm64("stur x0, [x1, #-8]"),
1192 AccessType::Write
1193 );
1194 }
1195
1196 #[test]
1197 fn arm64_load_is_read() {
1198 assert_eq!(get_access_type_arm64("ldr x0, [x1]"), AccessType::Read);
1199 assert_eq!(get_access_type_arm64("ldp x0, x1, [sp]"), AccessType::Read);
1200 assert_eq!(
1201 get_access_type_arm64("ldur x0, [x1, #-8]"),
1202 AccessType::Read
1203 );
1204 }
1205
1206 #[test]
1207 fn arm64_branch_is_exec() {
1208 assert_eq!(get_access_type_arm64("blr x8"), AccessType::Exec);
1209 assert_eq!(get_access_type_arm64("br x16"), AccessType::Exec);
1210 assert_eq!(get_access_type_arm64("ret"), AccessType::Exec);
1211 }
1212
1213 #[test]
1214 fn empty_is_unknown() {
1215 assert_eq!(get_access_type_arm64(""), AccessType::Unknown);
1216 assert_eq!(get_access_type_x86(""), AccessType::Unknown);
1217 }
1218
1219 #[test]
1220 fn x86_store_is_write() {
1221 assert_eq!(
1222 get_access_type_x86("mov dword ptr [eax], ecx"),
1223 AccessType::Write
1224 );
1225 assert_eq!(get_access_type_x86("push rbp"), AccessType::Write);
1226 }
1227
1228 #[test]
1229 fn x86_load_is_read() {
1230 assert_eq!(get_access_type_x86("mov eax, [ecx]"), AccessType::Read);
1231 assert_eq!(get_access_type_x86("pop rbp"), AccessType::Read);
1232 }
1233
1234 #[test]
1235 fn x86_branch_is_exec() {
1236 assert_eq!(get_access_type_x86("call 0x1234"), AccessType::Exec);
1237 assert_eq!(get_access_type_x86("jmp rax"), AccessType::Exec);
1238 assert_eq!(get_access_type_x86("ret"), AccessType::Exec);
1239 }
1240
1241 #[test]
1242 fn dispatch_uses_correct_arch() {
1243 assert_eq!(
1245 get_access_type("str x0, [x1]", CpuType::ARM64),
1246 AccessType::Write
1247 );
1248 assert_eq!(
1250 get_access_type("push rbp", CpuType::X86_64),
1251 AccessType::Write
1252 );
1253 assert_eq!(
1255 get_access_type("str x0, [x1]", CpuType::X86_64),
1256 AccessType::Unknown
1257 );
1258 assert_eq!(
1260 get_access_type("push rbp", CpuType::ARM64),
1261 AccessType::Unknown
1262 );
1263 }
1264 }
1265}