balrog~ ELF file analysis tool to be used after firmware extraction |
| git clone https://github.com/djnnvx/balrog.git |
| log | files | refs | archive | README |
rules.rs (38864B)
1 use crate::graph::{CallSite, Graph, Val}; 2 use crate::load::Binary; 3 use serde::Serialize; 4 use std::collections::{HashMap, HashSet}; 5 6 #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Serialize, clap::ValueEnum)] 7 #[serde(rename_all = "lowercase")] 8 pub enum Class { 9 Exec, 10 Ipc, 11 Tls, 12 Fmt, 13 Mem, 14 Auth, 15 Priv, 16 Path, 17 } 18 19 pub const ORDER: &[Class] = &[ 20 Class::Exec, 21 Class::Ipc, 22 Class::Tls, 23 Class::Fmt, 24 Class::Mem, 25 Class::Auth, 26 Class::Priv, 27 Class::Path, 28 ]; 29 30 impl Class { 31 fn base(self) -> i32 { 32 match self { 33 Class::Exec => 50, 34 Class::Ipc => 45, 35 Class::Tls => 45, 36 Class::Fmt => 45, 37 Class::Mem => 35, 38 Class::Auth => 30, 39 Class::Priv => 30, 40 Class::Path => 20, 41 } 42 } 43 pub fn tag(self) -> &'static str { 44 match self { 45 Class::Exec => "exec", 46 Class::Ipc => "ipc", 47 Class::Tls => "tls", 48 Class::Fmt => "fmt", 49 Class::Mem => "mem", 50 Class::Auth => "auth", 51 Class::Priv => "priv", 52 Class::Path => "path", 53 } 54 } 55 } 56 57 pub const SHELL: &[&str] = &[ 58 "system", 59 "popen", 60 "wordexp", 61 "twsystem", 62 "CsteSystem", 63 "bstar_system", 64 "___system", 65 "doShell", 66 "cgi_deal_popen", 67 "ExeCmd", 68 "ExecShell", 69 "exec_shell_popen", 70 "exec_shell_popen_str", 71 "exec_shell_async", 72 "exec_shell_sync", 73 "exec_shell_sync2", 74 "execFormatCmd", 75 "exec_cmd", 76 "tp_systemEx", 77 "pegaSystem", 78 "SLIBCSystem", 79 "SLIBCExec", 80 "SLIBCExecv", 81 "SLIBCPopen", 82 "smbrun", 83 "smbrun_no_sanitize", 84 "smbrunsecret", 85 "afprun", 86 "popen_utf8", 87 "do_system", 88 "safe_system", 89 "_backexec", 90 "execRoot_bg2", 91 "ugs_execRoot_shell", 92 "prctl_runCommandInShellWithTimeout", 93 "safe_popen", 94 ]; 95 pub const SHELL_ARG1: &[&str] = &["SLIBCExecl"]; 96 pub const EVAL: &[&str] = &[ 97 "zend_eval_string", 98 "zend_eval_stringl", 99 "zend_eval_string_ex", 100 "PyRun_SimpleString", 101 "PyRun_SimpleStringFlags", 102 "PyRun_String", 103 "PyRun_StringFlags", 104 "Py_CompileString", 105 "PyEval_EvalCode", 106 ]; 107 pub const PRINTF_EXEC: &[&str] = &["doSystem", "doSystemCmd", "sysprintf", "lxmldbc_system"]; 108 pub const ARGV_EXEC: &[&str] = &[ 109 "execl", 110 "execlp", 111 "execle", 112 "execv", 113 "execvp", 114 "execvpe", 115 "execve", 116 "posix_spawn", 117 "posix_spawnp", 118 "_eval", 119 "_cpu_eval", 120 "_cpu_mask_eval", 121 "fm_exec", 122 ]; 123 pub const UNBOUNDED: &[(&str, usize, Option<usize>)] = &[ 124 ("strcpy", 0, None), 125 ("strcat", 0, None), 126 ("stpcpy", 0, None), 127 ("sprintf", 0, Some(1)), 128 ("vsprintf", 0, Some(1)), 129 ("gets", 0, None), 130 ("wcscpy", 0, None), 131 ("wcscat", 0, None), 132 ("getwd", 0, None), 133 ("__sprintf_chk", 0, Some(3)), 134 ("__vsprintf_chk", 0, Some(3)), 135 ("__strcpy_chk", 0, None), 136 ("__strcat_chk", 0, None), 137 ]; 138 pub const SIZED: &[(&str, usize)] = &[ 139 ("memcpy", 2), 140 ("memmove", 2), 141 ("strncpy", 2), 142 ("strncat", 2), 143 ("bcopy", 2), 144 ("mempcpy", 2), 145 ("stpncpy", 2), 146 ("wcsncpy", 2), 147 ("wmemcpy", 2), 148 ("strlcpy", 2), 149 ("strlcat", 2), 150 ("__memcpy_chk", 2), 151 ("__memmove_chk", 2), 152 ]; 153 pub const FORTIFY_BOUND: &[(&str, usize)] = &[ 154 ("__sprintf_chk", 2), 155 ("__vsprintf_chk", 2), 156 ("__strcpy_chk", 2), 157 ("__strcat_chk", 2), 158 ("__memcpy_chk", 3), 159 ("__memmove_chk", 3), 160 ("__snprintf_chk", 3), 161 ("__vsnprintf_chk", 3), 162 ]; 163 pub const FORMAT: &[(&str, usize)] = &[ 164 ("printf", 0), 165 ("syslog", 1), 166 ("fprintf", 1), 167 ("vsyslog", 1), 168 ("asprintf", 1), 169 ("vasprintf", 1), 170 ("vprintf", 0), 171 ("vfprintf", 1), 172 ("snprintf", 2), 173 ("vsnprintf", 2), 174 ("dprintf", 1), 175 ("vdprintf", 1), 176 ("g_strdup_printf", 0), 177 ("setproctitle", 0), 178 ("err", 1), 179 ("errx", 1), 180 ("verr", 1), 181 ("verrx", 1), 182 ("warn", 0), 183 ("warnx", 0), 184 ("vwarn", 0), 185 ("vwarnx", 0), 186 ("__printf_chk", 1), 187 ("__fprintf_chk", 2), 188 ("__syslog_chk", 2), 189 ("__snprintf_chk", 4), 190 ("__vsnprintf_chk", 4), 191 ]; 192 pub const SCANF: &[(&str, usize)] = &[ 193 ("scanf", 0), 194 ("fscanf", 1), 195 ("sscanf", 1), 196 ("__isoc99_scanf", 0), 197 ("__isoc99_fscanf", 1), 198 ("__isoc99_sscanf", 1), 199 ("vscanf", 0), 200 ("vfscanf", 1), 201 ("vsscanf", 1), 202 ]; 203 pub const PRIVOPS: &[&str] = &[ 204 "setuid", 205 "setgid", 206 "seteuid", 207 "setegid", 208 "setresuid", 209 "setresgid", 210 "setreuid", 211 "setregid", 212 "setfsuid", 213 "setfsgid", 214 "setgroups", 215 "initgroups", 216 "umask", 217 ]; 218 pub const TEMPFILE: &[&str] = &["mktemp", "tmpnam", "tmpnam_r", "tempnam"]; 219 pub const WEAK_RANDOM: &[&str] = &[ 220 "rand", 221 "random", 222 "rand_r", 223 "drand48", 224 "lrand48", 225 "mrand48", 226 "RAND_pseudo_bytes", 227 ]; 228 pub const SEED: &[&str] = &["srand", "srandom", "srand48", "seed48"]; 229 pub const WEAK_CRYPTO: &[&str] = &[ 230 "MD5", 231 "MD5_Init", 232 "MD4", 233 "SHA1_Init", 234 "EVP_md5", 235 "EVP_md4", 236 "EVP_sha1", 237 "mbedtls_md5", 238 "mbedtls_sha1", 239 "RC4", 240 "RC4_set_key", 241 "EVP_rc4", 242 "EVP_des_cbc", 243 "EVP_des_ecb", 244 "DES_set_key", 245 "DES_ecb_encrypt", 246 "EVP_rc2_cbc", 247 "EVP_bf_cbc", 248 "mbedtls_des_setkey_enc", 249 "EVP_aes_128_ecb", 250 "EVP_aes_192_ecb", 251 "EVP_aes_256_ecb", 252 ]; 253 pub const OLD_TLS: &[&str] = &[ 254 "SSLv2_method", 255 "SSLv3_method", 256 "SSLv3_client_method", 257 "SSLv3_server_method", 258 "TLSv1_method", 259 "TLSv1_client_method", 260 "TLSv1_1_method", 261 ]; 262 pub const VERIFY_MODE: &[&str] = &[ 263 "SSL_CTX_set_verify", 264 "SSL_set_verify", 265 "wolfSSL_CTX_set_verify", 266 "wolfSSL_set_verify", 267 ]; 268 pub const DESER: &[&str] = &[ 269 "soap_get", 270 "soap_serve", 271 "soap_begin_recv", 272 "xmlReadMemory", 273 "xmlParseMemory", 274 "xmlReadDoc", 275 "XML_Parse", 276 "d2i_X509", 277 "PEM_read_bio_X509", 278 ]; 279 pub const UNTRUSTED_INPUTS: &[&str] = &[ 280 "recv", 281 "recvfrom", 282 "recvmsg", 283 "recvmmsg", 284 "read", 285 "accept", 286 "accept4", 287 "getenv", 288 "fgets", 289 "getline", 290 "__getdelim", 291 "fread", 292 "SSL_read", 293 "BIO_read", 294 "json_tokener_parse", 295 "json_tokener_parse_ex", 296 "json_object_from_file", 297 "cJSON_Parse", 298 "cJSON_ParseWithLength", 299 "blobmsg_data", 300 "blobmsg_parse", 301 "soap_get", 302 "websGetVar", 303 "websDecodeUrl", 304 "FCGX_GetParam", 305 "hdf_get_value", 306 "config_get", 307 "find_val", 308 "get_cgi", 309 "httpGetEnv", 310 "nvram_get", 311 "nvram_safe_get", 312 "_nvram_get", 313 "nvram_get_int", 314 "nvram_get_buf", 315 "nvram_bufget", 316 "nvram_getf", 317 "nvram_get_adv", 318 "envram_get", 319 "envram_getf", 320 "apmib_get", 321 "artblock_get", 322 "artblock_safe_get", 323 "acosNvramConfig_get", 324 "acos_nvram_get", 325 "WAN_ith_CONFIG_GET", 326 "uci_lookup_option_string", 327 "uci_lookup_option", 328 "Uci_Get_Str", 329 "GetValue", 330 "acosNvramConfig_read", 331 "bcm_nvram_get", 332 "wlcsm_nvram_get", 333 "dni_nvram_get", 334 "PTI_nvram_get", 335 "CGI_Find_Parameter", 336 "getcgivars", 337 "getcgi", 338 "mg_get_var", 339 "webGetVar", 340 "websGetVarString", 341 "cmsObj_get", 342 "cJSON_GetObjectItem", 343 "cJSON_GetObjectItemCaseSensitive", 344 ]; 345 pub const PEERCRED: &[&str] = &["getsockopt", "getpeereid", "getpeername", "SO_PEERCRED"]; 346 347 pub const SANITIZERS: &[&str] = &[ 348 "canonicalize_file_name", 349 "strtol", 350 "strtoul", 351 "strtoll", 352 "strtoull", 353 "strtoimax", 354 "strtoumax", 355 "inet_pton", 356 "inet_aton", 357 "inet_ntop", 358 "fnmatch", 359 "g_shell_quote", 360 "quote_for_shell", 361 "secure_getenv", 362 ]; 363 pub const BOUNDED: &[&str] = &[ 364 "snprintf", 365 "vsnprintf", 366 "strlcpy", 367 "strlcat", 368 "strncpy", 369 "strncat", 370 "strnlen", 371 "stpecpy", 372 "strscpy", 373 "__snprintf_chk", 374 "__strcpy_chk", 375 "__memcpy_chk", 376 ]; 377 pub const CT_COMPARE: &[&str] = &[ 378 "CRYPTO_memcmp", 379 "timingsafe_bcmp", 380 "timingsafe_memcmp", 381 "sodium_memcmp", 382 "mbedtls_ct_memcmp", 383 ]; 384 pub const CSPRNG: &[&str] = &[ 385 "getrandom", 386 "arc4random", 387 "arc4random_buf", 388 "arc4random_uniform", 389 "RAND_bytes", 390 "mbedtls_ctr_drbg_random", 391 ]; 392 pub const HOSTNAME_CHECK: &[&str] = &[ 393 "X509_check_host", 394 "X509_check_ip", 395 "X509_check_ip_asc", 396 "SSL_set1_host", 397 "SSL_add1_host", 398 "X509_VERIFY_PARAM_set1_host", 399 ]; 400 pub const SIGVERIFY: &[&str] = &[ 401 "RSA_verify", 402 "EVP_VerifyFinal", 403 "EVP_DigestVerifyFinal", 404 "ECDSA_verify", 405 "mbedtls_pk_verify", 406 ]; 407 408 const CURL_VERIFY_OPTS: &[(u64, &str)] = &[ 409 (64, "CURLOPT_SSL_VERIFYPEER"), 410 (81, "CURLOPT_SSL_VERIFYHOST"), 411 (248, "CURLOPT_PROXY_SSL_VERIFYPEER"), 412 (249, "CURLOPT_PROXY_SSL_VERIFYHOST"), 413 ]; 414 const WRDE_NOCMD: u64 = 4; 415 const MBEDTLS_VERIFY_NONE: u64 = 0; 416 const MBEDTLS_VERIFY_OPTIONAL: u64 = 1; 417 const PROT_WRITE_EXEC: u64 = 0o6; 418 const VERIFY_OFF_BONUS: i32 = 30; 419 const FORTIFY_OFF_BONUS: i32 = 40; 420 const FORTIFY_OFF_NOTE: &str = "FORTIFY bound is -1: the size check is disabled"; 421 const SHELL_TEMPLATE_BONUS: i32 = 25; 422 const DIGEST_LENGTHS: &[u64] = &[16, 20, 32, 48, 64]; 423 const MAX_TEMPLATES: usize = 3; 424 const MAX_SOCKET_PATHS: usize = 2; 425 const MAX_EXEC_NOTES: usize = 3; 426 const MAX_CONTEXT_STRINGS: usize = 2; 427 const MIN_CONTEXT_STRING: usize = 8; 428 const CGI_VARS: &[&str] = &[ 429 "QUERY_STRING", 430 "REQUEST_URI", 431 "CONTENT_LENGTH", 432 "HTTP_COOKIE", 433 ]; 434 const SEARCH_PATH_VARS: &[&str] = &[ 435 "LD_PRELOAD", 436 "LD_LIBRARY_PATH", 437 "PATH", 438 "IFS", 439 "SHELL", 440 "PHPRC", 441 "PERL5LIB", 442 "PYTHONSTARTUP", 443 ]; 444 const PRIV_DROP: &[&str] = &[ 445 "setuid", 446 "setgid", 447 "seteuid", 448 "setegid", 449 "setresuid", 450 "setresgid", 451 "setreuid", 452 "setregid", 453 "setfsuid", 454 "setfsgid", 455 ]; 456 457 const CMD_HEADS: &[&str] = &[ 458 "cp ", 459 "mv ", 460 "rm ", 461 "cat ", 462 "tar ", 463 "chmod ", 464 "chown ", 465 "wget ", 466 "curl ", 467 "tftp ", 468 "ping ", 469 "traceroute ", 470 "iwpriv ", 471 "ifconfig ", 472 "route ", 473 "killall ", 474 "iptables ", 475 "nvram ", 476 "sed ", 477 "mount ", 478 "umount ", 479 "dd ", 480 "mtd ", 481 "echo ", 482 "touch ", 483 "mkdir ", 484 "insmod ", 485 "modprobe ", 486 "reboot", 487 "sh ", 488 ]; 489 490 #[derive(Serialize)] 491 pub struct Finding { 492 pub bin: String, 493 pub class: Class, 494 pub risky_call: String, 495 #[serde(serialize_with = "hex")] 496 pub site: u64, 497 pub func: String, 498 pub reach: Option<u32>, 499 pub evidence: Vec<String>, 500 pub score: i32, 501 } 502 503 fn hex<S: serde::Serializer>(v: &u64, s: S) -> Result<S::Ok, S::Error> { 504 s.serialize_str(&format!("{v:#x}")) 505 } 506 507 fn stack_off(k: i64) -> String { 508 if k < 0 { 509 format!("sp-{:#x}", -k) 510 } else { 511 format!("sp+{k:#x}") 512 } 513 } 514 515 fn looks_shell(s: &str) -> bool { 516 if !(s.contains("%s") || s.contains("%d")) { 517 return false; 518 } 519 let meta = [ 520 " -", 521 ";", 522 "|", 523 "&&", 524 "`", 525 "$(", 526 ">", 527 "${IFS}", 528 "2>&1", 529 "/bin/", 530 "/sbin/", 531 "/usr/bin/", 532 ]; 533 if meta.iter().any(|m| s.contains(m)) { 534 return true; 535 } 536 let trimmed = s.trim_start(); 537 CMD_HEADS.iter().any(|head| trimmed.starts_with(head)) 538 } 539 540 fn is_neutered(v: u64) -> bool { 541 v == u64::MAX || v == 0xffff_ffff 542 } 543 544 fn unbounded_scan(fmt: &str) -> bool { 545 let chars: Vec<char> = fmt.chars().collect(); 546 let mut i = 0; 547 while i < chars.len() { 548 if chars[i] != '%' { 549 i += 1; 550 continue; 551 } 552 let mut j = i + 1; 553 if j < chars.len() && chars[j] == '%' { 554 i = j + 1; 555 continue; 556 } 557 if j < chars.len() && chars[j] == '*' { 558 j += 1; 559 } 560 let had_width = j < chars.len() && chars[j].is_ascii_digit(); 561 while j < chars.len() && chars[j].is_ascii_digit() { 562 j += 1; 563 } 564 if j < chars.len() && (chars[j] == 's' || chars[j] == '[') && !had_width { 565 return true; 566 } 567 i = j + 1; 568 } 569 false 570 } 571 572 fn mode_note(m: u64) -> Option<String> { 573 if m & 0o4000 != 0 { 574 Some(format!("setuid bit in mode {m:#o}")) 575 } else if m & 0o2000 != 0 { 576 Some(format!("setgid bit in mode {m:#o}")) 577 } else if m & 0o2 != 0 { 578 Some(format!("world-writable mode {m:#o}")) 579 } else { 580 None 581 } 582 } 583 584 struct Hit { 585 class: Class, 586 score: i32, 587 evidence: Vec<String>, 588 } 589 590 impl Hit { 591 fn new(class: Class, adjust: i32) -> Hit { 592 Hit { 593 class, 594 score: class.base() + adjust, 595 evidence: Vec::new(), 596 } 597 } 598 fn note(&mut self, ev: impl Into<String>) { 599 self.evidence.push(ev.into()); 600 } 601 fn bump(&mut self, delta: i32, ev: impl Into<String>) { 602 self.score += delta; 603 self.evidence.push(ev.into()); 604 } 605 } 606 607 struct Scan<'a> { 608 bin: &'a Binary, 609 graph: &'a Graph, 610 name: String, 611 reach: HashMap<u64, u32>, 612 wrappers: HashSet<u64>, 613 calls: HashMap<u64, Vec<&'a str>>, 614 unix_paths: Vec<String>, 615 exec_imports: Vec<&'a str>, 616 tokenish: bool, 617 no_peercred: bool, 618 drops_privilege: bool, 619 } 620 621 fn call_index(g: &Graph) -> HashMap<u64, Vec<&str>> { 622 let mut m: HashMap<u64, Vec<&str>> = HashMap::new(); 623 for s in &g.sites { 624 if let Some(n) = s.import.as_deref() { 625 m.entry(s.func).or_default().push(n); 626 } 627 } 628 for v in m.values_mut() { 629 v.sort_unstable(); 630 v.dedup(); 631 } 632 m 633 } 634 635 fn is_unix_socket_path(text: &str) -> bool { 636 text.contains(".sock") && text.starts_with('/') 637 } 638 639 fn socket_paths(g: &Graph) -> Vec<String> { 640 let mut paths: Vec<String> = g 641 .strings 642 .values() 643 .flatten() 644 .filter(|text| is_unix_socket_path(text)) 645 .cloned() 646 .collect(); 647 paths.sort_unstable(); 648 paths.dedup(); 649 paths 650 } 651 652 fn is_exec_primitive(import: &str) -> bool { 653 SHELL.contains(&import) 654 || PRINTF_EXEC.contains(&import) 655 || ARGV_EXEC.contains(&import) 656 || import.contains("exec") 657 || import.contains("system_cmd") 658 } 659 660 fn exec_imports(bin: &Binary) -> Vec<&str> { 661 let mut exec_like: Vec<&str> = bin 662 .imports 663 .iter() 664 .map(String::as_str) 665 .filter(|import| is_exec_primitive(import)) 666 .collect(); 667 exec_like.sort_unstable(); 668 exec_like 669 } 670 671 fn names_credential(text: &str) -> bool { 672 let lower = text.to_ascii_lowercase(); 673 lower.contains("token") || lower.contains("session") || lower.contains("cookie") 674 } 675 676 fn mentions_session_token(g: &Graph) -> bool { 677 g.strings 678 .values() 679 .flatten() 680 .any(|text| names_credential(text)) 681 } 682 683 impl<'a> Scan<'a> { 684 fn new(bin: &'a Binary, graph: &'a Graph) -> Scan<'a> { 685 let mut seeds: Vec<&str> = SHELL.to_vec(); 686 seeds.extend_from_slice(PRINTF_EXEC); 687 Scan { 688 bin, 689 graph, 690 name: bin.name(), 691 reach: graph.reach_from(UNTRUSTED_INPUTS), 692 wrappers: graph.shell_equivalent(&seeds), 693 calls: call_index(graph), 694 unix_paths: socket_paths(graph), 695 exec_imports: exec_imports(bin), 696 tokenish: mentions_session_token(graph), 697 no_peercred: !PEERCRED.iter().any(|n| bin.imports.contains(*n)), 698 drops_privilege: PRIV_DROP.iter().any(|n| bin.imports.contains(*n)), 699 } 700 } 701 fn has(&self, n: &str) -> bool { 702 self.bin.imports.contains(n) 703 } 704 fn any(&self, v: &[&str]) -> bool { 705 v.iter().any(|n| self.has(n)) 706 } 707 } 708 709 struct Site<'a> { 710 scan: &'a Scan<'a>, 711 call: &'a CallSite, 712 imp: &'a str, 713 reach: Option<u32>, 714 } 715 716 impl Site<'_> { 717 fn text(&self, n: usize) -> Option<String> { 718 match self.call.args.get(n) { 719 Some(Some(Val::Num(a))) => self.scan.bin.cstr_at(*a), 720 _ => None, 721 } 722 } 723 fn num(&self, n: usize) -> Option<u64> { 724 match self.call.args.get(n) { 725 Some(Some(Val::Num(v))) => Some(*v), 726 _ => None, 727 } 728 } 729 fn stack(&self, n: usize) -> Option<i64> { 730 match self.call.args.get(n) { 731 Some(Some(Val::Stack(k))) => Some(*k), 732 _ => None, 733 } 734 } 735 fn unknown(&self, n: usize) -> bool { 736 self.call.args.get(n).map(Option::is_none).unwrap_or(false) 737 } 738 fn visible(&self, n: usize) -> bool { 739 n < self.call.args.len() 740 } 741 fn calls(&self) -> &[&str] { 742 self.scan 743 .calls 744 .get(&self.call.func) 745 .map_or(&[], Vec::as_slice) 746 } 747 fn strings(&self) -> &[String] { 748 self.scan 749 .graph 750 .strings 751 .get(&self.call.func) 752 .map_or(&[], Vec::as_slice) 753 } 754 fn has(&self, n: &str) -> bool { 755 self.scan.has(n) 756 } 757 fn any(&self, v: &[&str]) -> bool { 758 self.scan.any(v) 759 } 760 } 761 762 fn shell(s: &Site) -> Option<Hit> { 763 let mut h = Hit::new(Class::Exec, 0); 764 let cmd_arg = if SHELL_ARG1.contains(&s.imp) { 1 } else { 0 }; 765 if PRINTF_EXEC.contains(&s.imp) { 766 h.note(format!( 767 "{} takes a printf format, not a finished command", 768 s.imp 769 )); 770 } 771 match s.text(cmd_arg) { 772 Some(t) if t.contains('%') => h.bump(25, format!("command template {t:?}")), 773 Some(t) => h.bump(-30, format!("constant command {t:?}")), 774 None if s.num(cmd_arg).is_some() => { 775 h.bump(-10, "argument is a constant that is not readable text") 776 } 777 None => h.bump(15, "argument not a compile-time constant"), 778 } 779 if s.imp == "wordexp" { 780 match s.num(2) { 781 Some(f) if f & WRDE_NOCMD != 0 => { 782 h.bump(-25, "WRDE_NOCMD set, command substitution refused") 783 } 784 _ => h.bump(25, "no WRDE_NOCMD: `$(...)` in the input is executed"), 785 } 786 } 787 let templates: Vec<&String> = s.strings().iter().filter(|t| looks_shell(t)).collect(); 788 for t in templates.iter().take(MAX_TEMPLATES) { 789 h.note(format!("command template {t:?}")); 790 } 791 if !templates.is_empty() { 792 h.score += SHELL_TEMPLATE_BONUS; 793 } 794 Some(h) 795 } 796 797 fn script_eval(s: &Site) -> Option<Hit> { 798 let mut h = Hit::new(Class::Exec, -5); 799 h.note(format!("{} evaluates a script at runtime", s.imp)); 800 if s.text(0).is_none() { 801 h.bump(20, "the evaluated string is not a compile-time constant"); 802 } 803 Some(h) 804 } 805 806 fn argv_exec(s: &Site) -> Option<Hit> { 807 let mut h = Hit::new(Class::Exec, -25); 808 h.note("argv exec, metacharacters are inert"); 809 let path_relative = !matches!(s.text(0), Some(t) if t.starts_with('/')); 810 if (s.imp == "execvp" || s.imp == "execlp") && path_relative { 811 h.bump( 812 25, 813 "execvp/execlp resolves through $PATH, not an absolute path", 814 ); 815 } 816 Some(h) 817 } 818 819 fn dynamic_load(s: &Site) -> Option<Hit> { 820 let mut h = Hit::new(Class::Exec, -20); 821 match s.text(0) { 822 Some(t) if t.starts_with("/lib") || t.starts_with("/usr/lib") => return None, 823 Some(t) => h.note(format!("loads library from {t:?}")), 824 None => { 825 h.note("library path is not a compile-time constant"); 826 if !s.calls().contains(&"strchr") { 827 h.bump(20, "no strchr guard on the name: no '/' rejection"); 828 } 829 } 830 } 831 Some(h) 832 } 833 834 fn env_hijack(s: &Site) -> Option<Hit> { 835 let name = s.text(0)?; 836 if !SEARCH_PATH_VARS.iter().any(|v| name.starts_with(v)) { 837 return None; 838 } 839 let mut h = Hit::new(Class::Exec, -15); 840 h.note(format!( 841 "sets {name:?}, a search-path primitive for any child" 842 )); 843 Some(h) 844 } 845 846 fn curl_verify(s: &Site) -> Option<Hit> { 847 let (opt, val) = (s.num(1)?, s.num(2)?); 848 let (_, name) = CURL_VERIFY_OPTS.iter().find(|(o, _)| *o == opt)?; 849 if val != 0 { 850 return None; 851 } 852 let mut h = Hit::new(Class::Tls, VERIFY_OFF_BONUS); 853 h.note(format!("{name} set to 0: certificate check disabled")); 854 h.note("DNS-hijack MITM works without a valid certificate"); 855 Some(h) 856 } 857 858 fn verify_mode(s: &Site) -> Option<Hit> { 859 if s.num(1) != Some(0) { 860 return None; 861 } 862 let mut h = Hit::new(Class::Tls, VERIFY_OFF_BONUS); 863 h.note("verify mode 0 (VERIFY_NONE): peer certificate not checked"); 864 if s.has("SSL_get_verify_result") { 865 h.bump( 866 -20, 867 "SSL_get_verify_result present, the result may still be inspected", 868 ); 869 } 870 Some(h) 871 } 872 873 fn mbedtls_authmode(s: &Site) -> Option<Hit> { 874 let (adjust, note) = match s.num(1)? { 875 MBEDTLS_VERIFY_NONE => ( 876 VERIFY_OFF_BONUS, 877 "MBEDTLS_SSL_VERIFY_NONE: peer certificate not checked", 878 ), 879 MBEDTLS_VERIFY_OPTIONAL => ( 880 0, 881 "MBEDTLS_SSL_VERIFY_OPTIONAL: handshake continues on failure", 882 ), 883 _ => return None, 884 }; 885 let mut h = Hit::new(Class::Tls, adjust); 886 h.note(note); 887 Some(h) 888 } 889 890 fn unbounded_write(s: &Site, dest: usize, fmt: Option<usize>) -> Option<Hit> { 891 let mut h = Hit::new(Class::Mem, 0); 892 let stack_dest = s.stack(dest); 893 if let Some(k) = stack_dest { 894 h.bump( 895 20, 896 format!("destination is a stack buffer at {}", stack_off(k)), 897 ); 898 } 899 let mut conversions = 0; 900 if let Some(fmt) = fmt { 901 match s.text(fmt) { 902 Some(t) => { 903 conversions = t.matches("%s").count(); 904 h.bump(10 * conversions as i32, format!("format {t:?}")); 905 } 906 None if s.num(fmt).is_some() => h.note("format not a readable literal"), 907 None => h.bump(25, "format argument is not a literal (format string bug)"), 908 } 909 } 910 if stack_dest.is_some() && (conversions > 0 || fmt.is_none()) { 911 h.bump( 912 35, 913 "unbounded write of a variable-length value into a fixed stack buffer", 914 ); 915 } 916 fortify_note(s, &mut h); 917 Some(h) 918 } 919 920 fn fortify_bound(s: &Site) -> Option<u64> { 921 let (_, bound) = FORTIFY_BOUND.iter().find(|(n, _)| *n == s.imp)?; 922 s.num(*bound) 923 } 924 925 fn fortify_note(s: &Site, h: &mut Hit) { 926 match fortify_bound(s) { 927 Some(v) if is_neutered(v) => h.bump(FORTIFY_OFF_BONUS, FORTIFY_OFF_NOTE), 928 Some(_) => h.bump(-30, "FORTIFY bound is a real constant, overflow aborts"), 929 None => {} 930 } 931 } 932 933 fn measures_input(s: &Site) -> bool { 934 s.calls().contains(&"strlen") || s.calls().contains(&"strspn") 935 } 936 937 fn sized_write(s: &Site, len: usize) -> Option<Hit> { 938 if s.num(len) == Some(0) { 939 let mut h = Hit::new(Class::Mem, -10); 940 h.note("length 0 turns this into a no-op, usually an underflow artefact"); 941 return Some(h); 942 } 943 let fortify_disabled = fortify_bound(s).is_some_and(is_neutered); 944 let stack_dest = s.stack(0); 945 let measures_reachable_input = measures_input(s) && s.reach == Some(0); 946 let suspicious = stack_dest.is_some() || fortify_disabled || measures_reachable_input; 947 if !s.unknown(len) || !suspicious { 948 return None; 949 } 950 let mut h = Hit::new(Class::Mem, 0); 951 if let Some(k) = stack_dest { 952 h.note(format!( 953 "non-constant length into a stack buffer at {}", 954 stack_off(k) 955 )); 956 } 957 if fortify_disabled { 958 h.bump(FORTIFY_OFF_BONUS, FORTIFY_OFF_NOTE); 959 } 960 if measures_input(s) { 961 h.bump( 962 20, 963 "strlen/strspn in the same function: size may be derived from the \ 964 input length, not the destination size", 965 ); 966 } 967 Some(h) 968 } 969 970 fn stack_alloc(s: &Site) -> Option<Hit> { 971 let mut h = Hit::new(Class::Mem, -5); 972 match s.num(0) { 973 Some(v) => h.bump(-20, format!("alloca of a constant {v:#x}")), 974 None => h.note("alloca with a non-constant size: stack clash"), 975 } 976 Some(h) 977 } 978 979 fn unbounded_read(s: &Site, fmt: usize) -> Option<Hit> { 980 if !s.visible(fmt) { 981 return None; 982 } 983 let f = s.text(fmt)?; 984 if !unbounded_scan(&f) { 985 return None; 986 } 987 let mut h = Hit::new(Class::Mem, 10); 988 h.note(format!("format {f:?} has a %s or %[ with no field width")); 989 Some(h) 990 } 991 992 fn format_string(s: &Site, fmt: usize) -> Option<Hit> { 993 if !s.visible(fmt) || s.text(fmt).is_some() { 994 return None; 995 } 996 let mut h = Hit::new(Class::Fmt, 0); 997 h.note("format argument is not a readable literal"); 998 Some(h) 999 } 1000 1001 fn writable_executable(s: &Site) -> Option<Hit> { 1002 let prot = s.num(2)?; 1003 if prot & PROT_WRITE_EXEC != PROT_WRITE_EXEC { 1004 return None; 1005 } 1006 let mut h = Hit::new(Class::Mem, 5); 1007 h.note(format!( 1008 "prot {prot:#x} is PROT_WRITE|PROT_EXEC: writable and executable at once" 1009 )); 1010 Some(h) 1011 } 1012 1013 fn des_crypt(s: &Site) -> Option<Hit> { 1014 let salt = s.text(1)?; 1015 if salt.starts_with('$') { 1016 return None; 1017 } 1018 let mut h = Hit::new(Class::Auth, 15); 1019 h.note(format!( 1020 "salt {salt:?} selects traditional DES crypt, key truncated to 8 characters" 1021 )); 1022 Some(h) 1023 } 1024 1025 fn weak_random(s: &Site) -> Option<Hit> { 1026 let mut h = Hit::new(Class::Auth, -15); 1027 h.note(format!("{} is not a CSPRNG", s.imp)); 1028 if !s.any(CSPRNG) { 1029 h.bump( 1030 15, 1031 "no getrandom/arc4random/RAND_bytes anywhere in this binary", 1032 ); 1033 } 1034 if s.scan.tokenish { 1035 h.bump(20, "binary references token/session/cookie strings"); 1036 } 1037 Some(h) 1038 } 1039 1040 fn predictable_seed(s: &Site) -> Option<Hit> { 1041 let mut h = Hit::new(Class::Auth, -10); 1042 match s.num(0) { 1043 Some(v) => h.note(format!("seeded with the constant {v:#x}")), 1044 None if s.calls().iter().any(|c| *c == "time" || *c == "getpid") => { 1045 h.note("seeded from time()/getpid() in the same function") 1046 } 1047 None => return None, 1048 } 1049 Some(h) 1050 } 1051 1052 fn mac_compare(s: &Site) -> Option<Hit> { 1053 let len = s.num(2)?; 1054 if !DIGEST_LENGTHS.contains(&len) { 1055 return None; 1056 } 1057 let beside_mac = s.calls().iter().any(|c| { 1058 c.contains("HMAC") || c.contains("MD5") || c.contains("SHA") || c.contains("CMAC") 1059 }); 1060 if !beside_mac { 1061 return None; 1062 } 1063 let mut h = Hit::new(Class::Auth, 0); 1064 h.note(format!( 1065 "memcmp of {len} bytes beside a MAC/hash call: not constant time" 1066 )); 1067 if s.any(CT_COMPARE) { 1068 h.bump(-20, "a constant-time compare exists in this binary"); 1069 } 1070 Some(h) 1071 } 1072 1073 fn privilege_op(s: &Site) -> Option<Hit> { 1074 let mut h = Hit::new(Class::Priv, -10); 1075 match (s.imp, s.num(0)) { 1076 ("umask", Some(mode)) => { 1077 let masks_world_write = mode != 0 && mode & 0o2 != 0; 1078 if masks_world_write { 1079 return None; 1080 } 1081 h.bump( 1082 25, 1083 format!("umask({mode:#o}) leaves later creations group- or world-writable"), 1084 ); 1085 } 1086 (_, Some(v)) => h.note(format!("argument {v:#x}")), 1087 _ => {} 1088 } 1089 if s.imp == "setuid" && !s.has("setgroups") && !s.has("initgroups") { 1090 h.bump( 1091 20, 1092 "no setgroups/initgroups anywhere: setuid keeps the supplementary groups", 1093 ); 1094 } 1095 Some(h) 1096 } 1097 1098 fn chroot_escape(s: &Site) -> Option<Hit> { 1099 if s.calls().contains(&"chdir") { 1100 return None; 1101 } 1102 let mut h = Hit::new(Class::Priv, 20); 1103 h.note("no chdir in the same function: `../` walks out of the jail"); 1104 Some(h) 1105 } 1106 1107 fn permissive_mode(s: &Site) -> Option<Hit> { 1108 let note = mode_note(s.num(1)?)?; 1109 let mut h = Hit::new(Class::Path, 20); 1110 h.note(note); 1111 if let Some(p) = s.text(0) { 1112 h.note(format!("path {p:?}")); 1113 } 1114 Some(h) 1115 } 1116 1117 fn temp_file(s: &Site) -> Option<Hit> { 1118 let mut h = Hit::new(Class::Path, 10); 1119 h.note(format!( 1120 "{} names a temp file without creating it atomically", 1121 s.imp 1122 )); 1123 if let Some(p) = s.text(0) { 1124 h.note(format!("template {p:?}")); 1125 } 1126 Some(h) 1127 } 1128 1129 fn unix_listener(s: &Site) -> Option<Hit> { 1130 let mut h = Hit::new(Class::Ipc, 0); 1131 for p in s.scan.unix_paths.iter().take(MAX_SOCKET_PATHS) { 1132 h.note(format!("socket path {p:?}")); 1133 } 1134 if s.scan.no_peercred { 1135 h.bump(30, "no SO_PEERCRED / getpeereid anywhere in this binary"); 1136 } 1137 let execs = &s.scan.exec_imports; 1138 for e in execs.iter().take(MAX_EXEC_NOTES) { 1139 h.note(format!("exec reachable via {e}")); 1140 } 1141 if !execs.is_empty() { 1142 h.score += 20; 1143 } 1144 if !s.scan.drops_privilege && !execs.is_empty() { 1145 h.bump( 1146 15, 1147 "no setuid/setgid anywhere: this listener never drops privilege", 1148 ); 1149 } 1150 Some(h) 1151 } 1152 1153 fn classify(s: &Site) -> Option<Hit> { 1154 let imp = s.imp; 1155 if SHELL.contains(&imp) || PRINTF_EXEC.contains(&imp) || SHELL_ARG1.contains(&imp) { 1156 shell(s) 1157 } else if EVAL.contains(&imp) { 1158 script_eval(s) 1159 } else if ARGV_EXEC.contains(&imp) { 1160 argv_exec(s) 1161 } else if imp == "dlopen" { 1162 dynamic_load(s) 1163 } else if imp == "setenv" || imp == "putenv" { 1164 env_hijack(s) 1165 } else if imp == "curl_easy_setopt" { 1166 curl_verify(s) 1167 } else if VERIFY_MODE.contains(&imp) { 1168 verify_mode(s) 1169 } else if imp == "mbedtls_ssl_conf_authmode" { 1170 mbedtls_authmode(s) 1171 } else if OLD_TLS.contains(&imp) { 1172 obsolete_tls(s) 1173 } else if let Some((_, dest, fmt)) = UNBOUNDED.iter().find(|(n, _, _)| *n == imp) { 1174 unbounded_write(s, *dest, *fmt) 1175 } else if let Some((_, len)) = SIZED.iter().find(|(n, _)| *n == imp) { 1176 sized_write(s, *len) 1177 } else if imp == "alloca" { 1178 stack_alloc(s) 1179 } else if let Some((_, fmt)) = SCANF.iter().find(|(n, _)| *n == imp) { 1180 unbounded_read(s, *fmt) 1181 } else if let Some((_, fmt)) = FORMAT.iter().find(|(n, _)| *n == imp) { 1182 format_string(s, *fmt) 1183 } else if imp == "mmap" || imp == "mprotect" { 1184 writable_executable(s) 1185 } else if imp == "crypt" || imp == "crypt_r" { 1186 des_crypt(s) 1187 } else if WEAK_CRYPTO.contains(&imp) { 1188 weak_crypto(s) 1189 } else if WEAK_RANDOM.contains(&imp) { 1190 weak_random(s) 1191 } else if SEED.contains(&imp) { 1192 predictable_seed(s) 1193 } else if imp == "memcmp" { 1194 mac_compare(s) 1195 } else if DESER.contains(&imp) { 1196 parses_document(s) 1197 } else if PRIVOPS.contains(&imp) { 1198 privilege_op(s) 1199 } else if imp == "chroot" { 1200 chroot_escape(s) 1201 } else if imp == "chmod" || imp == "fchmod" || imp == "mkdir" { 1202 permissive_mode(s) 1203 } else if TEMPFILE.contains(&imp) { 1204 temp_file(s) 1205 } else if imp == "bind" { 1206 unix_listener(s) 1207 } else { 1208 None 1209 } 1210 } 1211 1212 fn note_sanitizers(s: &Site, h: &mut Hit) { 1213 if matches!(h.class, Class::Ipc | Class::Tls) { 1214 return; 1215 } 1216 let sanitizers_in_function: Vec<&str> = s 1217 .calls() 1218 .iter() 1219 .copied() 1220 .filter(|name| SANITIZERS.contains(name)) 1221 .collect(); 1222 if sanitizers_in_function.is_empty() { 1223 return; 1224 } 1225 h.bump( 1226 -20, 1227 format!( 1228 "value may be normalized first: {} in the same function", 1229 sanitizers_in_function.join(", ") 1230 ), 1231 ); 1232 } 1233 1234 fn note_function_strings(s: &Site, h: &mut Hit) { 1235 if !matches!(h.class, Class::Mem | Class::Fmt) { 1236 return; 1237 } 1238 let context_strings = s 1239 .strings() 1240 .iter() 1241 .filter(|text| text.len() >= MIN_CONTEXT_STRING) 1242 .take(MAX_CONTEXT_STRINGS); 1243 for text in context_strings { 1244 h.note(format!("function references {text:?}")); 1245 } 1246 } 1247 1248 fn obsolete_tls(s: &Site) -> Option<Hit> { 1249 let mut h = Hit::new(Class::Tls, -15); 1250 h.note(format!("{} pins an obsolete protocol version", s.imp)); 1251 Some(h) 1252 } 1253 1254 fn weak_crypto(s: &Site) -> Option<Hit> { 1255 let mut h = Hit::new(Class::Auth, -10); 1256 h.note(format!("{}: broken or ECB-mode primitive", s.imp)); 1257 Some(h) 1258 } 1259 1260 fn parses_document(s: &Site) -> Option<Hit> { 1261 let mut h = Hit::new(Class::Mem, -10); 1262 h.note(format!("{} parses an attacker-supplied document", s.imp)); 1263 Some(h) 1264 } 1265 1266 fn is_cgi_variable(text: &str) -> bool { 1267 text.starts_with("HTTP_") || CGI_VARS.contains(&text) 1268 } 1269 1270 fn note_cgi_variable(s: &Site, h: &mut Hit) { 1271 let Some(var) = s.strings().iter().find(|text| is_cgi_variable(text)) else { 1272 return; 1273 }; 1274 h.bump( 1275 20, 1276 format!("reads the CGI variable {var:?}, a raw client header"), 1277 ); 1278 } 1279 1280 fn note_reachability(s: &Site, h: &mut Hit) { 1281 match s.reach { 1282 Some(0) => h.bump(40, "same function as a network/env input"), 1283 Some(d) if d <= 2 => h.bump(25, format!("{d} hop(s) from a network/env input")), 1284 Some(d) if d <= 5 => h.bump(10, format!("{d} hops from a network/env input")), 1285 Some(_) => {} 1286 None if s.scan.graph.addr_taken.contains(&s.call.func) => { 1287 h.note("address-taken: reached by indirect dispatch, not a direct call") 1288 } 1289 None => h.bump(-20, "no path from any network or environment input"), 1290 } 1291 } 1292 1293 fn adjust(s: &Site, h: &mut Hit) { 1294 note_sanitizers(s, h); 1295 note_function_strings(s, h); 1296 note_cgi_variable(s, h); 1297 if s.scan.wrappers.contains(&s.call.func) && h.class != Class::Exec { 1298 h.bump(10, "inside a shell-equivalent wrapper"); 1299 } 1300 note_reachability(s, h); 1301 } 1302 1303 pub fn analyze(bin: &Binary, graph: &Graph) -> Vec<Finding> { 1304 let scan = Scan::new(bin, graph); 1305 let mut out = Vec::new(); 1306 for call in &graph.sites { 1307 let Some(imp) = call.import.as_deref() else { 1308 continue; 1309 }; 1310 let s = Site { 1311 scan: &scan, 1312 call, 1313 imp, 1314 reach: scan.reach.get(&call.func).copied(), 1315 }; 1316 let Some(mut h) = classify(&s) else { continue }; 1317 adjust(&s, &mut h); 1318 out.push(Finding { 1319 bin: scan.name.clone(), 1320 class: h.class, 1321 risky_call: imp.to_string(), 1322 site: call.site, 1323 func: graph.func_name(call.func), 1324 reach: s.reach, 1325 evidence: h.evidence, 1326 score: h.score, 1327 }); 1328 } 1329 out.extend(absences(&scan)); 1330 out.sort_by(|a, b| b.score.cmp(&a.score).then(a.site.cmp(&b.site))); 1331 out 1332 } 1333 1334 struct Absence { 1335 present: &'static [&'static str], 1336 absent: &'static [&'static str], 1337 class: Class, 1338 risky_call: &'static str, 1339 score: i32, 1340 evidence: &'static [&'static str], 1341 } 1342 1343 const SSL_CLIENT: &[&str] = &["SSL_connect", "SSL_CTX_new"]; 1344 1345 const ABSENT: &[Absence] = &[ 1346 Absence { 1347 present: SSL_CLIENT, 1348 absent: &["SSL_CTX_set_verify", "SSL_set_verify"], 1349 class: Class::Tls, 1350 risky_call: "(no verify mode set)", 1351 score: 75, 1352 evidence: &[ 1353 "SSL_CTX_new/SSL_connect present but no verify mode is ever configured", 1354 "OpenSSL's documented default is SSL_VERIFY_NONE, so the handshake completes regardless of the certificate", 1355 ], 1356 }, 1357 Absence { 1358 present: SSL_CLIENT, 1359 absent: HOSTNAME_CHECK, 1360 class: Class::Tls, 1361 risky_call: "(no hostname check)", 1362 score: 65, 1363 evidence: &[ 1364 "no X509_check_host / SSL_set1_host / X509_VERIFY_PARAM_set1_host", 1365 "OpenSSL does not bind a chain to a hostname on its own, so any CA-signed certificate for any name is accepted", 1366 ], 1367 }, 1368 Absence { 1369 present: SSL_CLIENT, 1370 absent: &["SSL_CTX_load_verify_locations", "SSL_CTX_set_default_verify_paths"], 1371 class: Class::Tls, 1372 risky_call: "(no trust store)", 1373 score: 55, 1374 evidence: &["no CA store is ever loaded, so chain verification cannot succeed"], 1375 }, 1376 Absence { 1377 present: &["mbedtls_ssl_handshake"], 1378 absent: &["mbedtls_ssl_set_hostname"], 1379 class: Class::Tls, 1380 risky_call: "(no mbedtls hostname)", 1381 score: 65, 1382 evidence: &["mbedtls_ssl_set_hostname is never called, so CN/SAN matching is skipped"], 1383 }, 1384 Absence { 1385 present: &["chroot"], 1386 absent: &["chdir"], 1387 class: Class::Priv, 1388 risky_call: "(chroot without chdir)", 1389 score: 55, 1390 evidence: &["chroot imported but chdir is not, so the jail is trivially escaped"], 1391 }, 1392 Absence { 1393 present: TEMPFILE, 1394 absent: &["mkstemp", "mkostemp", "mkdtemp"], 1395 class: Class::Path, 1396 risky_call: "(no atomic temp file)", 1397 score: 45, 1398 evidence: &["mktemp/tmpnam used and no mkstemp/mkdtemp is linked at all"], 1399 }, 1400 ]; 1401 1402 fn is_unbounded_write(import: &str) -> bool { 1403 UNBOUNDED.iter().any(|(name, _, _)| *name == import) 1404 } 1405 1406 fn uses_unbounded_write(g: &Graph) -> bool { 1407 let mut imports = g.sites.iter().filter_map(|site| site.import.as_deref()); 1408 imports.any(is_unbounded_write) 1409 } 1410 1411 fn names_flash_target(text: &str) -> bool { 1412 text.contains("sysupgrade") || text.starts_with("mtd ") || text.contains("/dev/mtd") 1413 } 1414 1415 fn flashes_firmware(g: &Graph) -> bool { 1416 g.strings 1417 .values() 1418 .flatten() 1419 .any(|text| names_flash_target(text)) 1420 } 1421 1422 fn absences(sc: &Scan) -> Vec<Finding> { 1423 let mut out = Vec::new(); 1424 let mut push = |class: Class, risky_call: &str, score: i32, ev: &[&str]| { 1425 out.push(Finding { 1426 bin: sc.name.clone(), 1427 class, 1428 risky_call: risky_call.to_string(), 1429 site: 0, 1430 func: "(whole binary)".into(), 1431 reach: None, 1432 evidence: ev.iter().map(|e| e.to_string()).collect(), 1433 score, 1434 }) 1435 }; 1436 1437 let unbounded = uses_unbounded_write(sc.graph); 1438 if unbounded && !sc.has("__stack_chk_fail") { 1439 push( 1440 Class::Mem, 1441 "(no stack canary)", 1442 60, 1443 &[ 1444 "no __stack_chk_fail import: built without -fstack-protector", 1445 "every stack overflow here reaches the saved return address directly", 1446 ], 1447 ); 1448 } 1449 if unbounded && !sc.any(BOUNDED) { 1450 push( 1451 Class::Mem, 1452 "(no bounded copy)", 1453 55, 1454 &[ 1455 "no snprintf/strlcpy/strncpy/strncat anywhere in this binary", 1456 "every string operation it performs is unbounded", 1457 ], 1458 ); 1459 } 1460 for a in ABSENT { 1461 if sc.any(a.present) && !sc.any(a.absent) { 1462 push(a.class, a.risky_call, a.score, a.evidence); 1463 } 1464 } 1465 if flashes_firmware(sc.graph) && (sc.has("system") || sc.has("popen")) && !sc.any(SIGVERIFY) { 1466 push( 1467 Class::Exec, 1468 "(unsigned update path)", 1469 60, 1470 &[ 1471 "references mtd/sysupgrade and shells out, but links no RSA/ECDSA verify", 1472 "check whether the update image is authenticated at all", 1473 ], 1474 ); 1475 } 1476 out 1477 }