diff --git a/Cargo.lock b/Cargo.lock index f9401a8a9d..785c634434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.8.26", ] [[package]] @@ -260,6 +260,43 @@ dependencies = [ "objc2", ] +[[package]] +name = "bntl_cli" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "bntl_utils", + "clap", + "rayon", + "serde_json", + "thiserror 2.0.12", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bntl_utils" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "dashmap", + "minijinja", + "minijinja-embed", + "nt-apiset", + "serde", + "serde_json", + "similar", + "tempdir", + "thiserror 2.0.12", + "tracing", + "url", + "uuid", + "walkdir", + "windows-metadata", +] + [[package]] name = "bon" version = "3.6.4" @@ -407,9 +444,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -417,9 +454,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -429,9 +466,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -441,9 +478,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -652,6 +689,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", +] + [[package]] name = "debugid" version = "0.8.0" @@ -681,6 +727,12 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "directories" version = "6.0.0" @@ -714,6 +766,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dwarf_export" version = "0.1.0" @@ -885,6 +948,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1073,6 +1145,87 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "idb-rs" version = "0.1.12" @@ -1105,6 +1258,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.25.6" @@ -1269,6 +1443,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.13" @@ -1285,6 +1465,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.5" @@ -1394,6 +1583,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -1404,6 +1599,29 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nt-apiset" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ab316ee07638762db759975a633e8971c17a346ff2bed93321c5cb2600f024" +dependencies = [ + "bitflags 2.9.1", + "displaydoc", + "nt-string", + "pelite", + "zerocopy 0.6.6", +] + +[[package]] +name = "nt-string" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64f73b19d9405e886b53b9dee286e7fbb622a5276a7fd143c2d8e4dac3a0c6c" +dependencies = [ + "displaydoc", + "widestring", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1642,6 +1860,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1713,6 +1950,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1811,9 +2057,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1821,9 +2067,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2088,18 +2334,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2286,6 +2542,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -2400,6 +2667,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2489,11 +2766,15 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", "parking_lot", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2526,6 +2807,23 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2534,13 +2832,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", + "serde_core", "sha1_smol", "wasm-bindgen", ] @@ -2785,6 +3083,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -2822,6 +3126,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-metadata" +version = "0.59.0" +source = "git+https://github.com/microsoft/windows-rs?tag=72#bcc24b5c5fb3fe0a7d00559ceee824abc66e030b" + [[package]] name = "windows-sys" version = "0.59.0" @@ -3078,6 +3387,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "x11rb" version = "0.13.1" @@ -3095,13 +3410,57 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.26", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3115,6 +3474,60 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 7061c201be..ca260b9dba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ members = [ "plugins/warp/examples/headless", "plugins/workflow_objc", "plugins/workflow_objc/demo", + "plugins/bntl_utils", + "plugins/bntl_utils/cli", ] [workspace.dependencies] diff --git a/about.toml b/about.toml index 29f0772fdc..a0954a66a3 100644 --- a/about.toml +++ b/about.toml @@ -9,4 +9,5 @@ accepted = [ "LicenseRef-scancode-google-patent-license-fuchsia", "MPL-2.0", "LicenseRef-scancode-unknown-license-reference", + "BSD-2-Clause" ] \ No newline at end of file diff --git a/arch/msp430/Cargo.toml b/arch/msp430/Cargo.toml index d5bcc31ee8..9931ff330f 100644 --- a/arch/msp430/Cargo.toml +++ b/arch/msp430/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["jrozner"] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/arch/riscv/Cargo.toml b/arch/riscv/Cargo.toml index d5ab661e5e..ff9643df8a 100644 --- a/arch/riscv/Cargo.toml +++ b/arch/riscv/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Ryan Snyder "] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/plugins/bntl_utils/CMakeLists.txt b/plugins/bntl_utils/CMakeLists.txt new file mode 100644 index 0000000000..b1573e3748 --- /dev/null +++ b/plugins/bntl_utils/CMakeLists.txt @@ -0,0 +1,168 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(bntl_utils) + +if(NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD) + if(NOT BN_API_PATH) + # If we have not already defined the API source directory try and find it. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + # List of paths to search for the clone of the api + HINTS ../../.. ../../binaryninja/api/ binaryninjaapi binaryninja-api $ENV{BN_API_PATH} + REQUIRED + ) + endif() + set(CARGO_STABLE_VERSION 1.91.1) + add_subdirectory(${BN_API_PATH} binaryninjaapi) +endif() + +file(GLOB_RECURSE PLUGIN_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/*.rs) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/dev-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=dev-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/debug) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target) + endif() +else() + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=release-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --release) + endif() +endif() + +if(FORCE_COLORED_OUTPUT) + set(CARGO_OPTS ${CARGO_OPTS} --color always) +endif() + +if(DEMO) + set(CARGO_FEATURES --features demo --manifest-path ${PROJECT_SOURCE_DIR}/demo/Cargo.toml) + + set(OUTPUT_FILE_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}_static${CMAKE_STATIC_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR $) +else() + # NOTE: --no-default-features is set to disable building artifacts used for testing + # NOTE: the linker is looking in the target dir and linking on it apparently. + set(CARGO_FEATURES "--no-default-features") + + set(OUTPUT_FILE_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR ${BN_INSTALL_BIN_DIR}) +endif() + + +add_custom_target(${PROJECT_NAME} ALL DEPENDS ${OUTPUT_FILE_PATH}) +add_dependencies(${PROJECT_NAME} binaryninjaapi) +get_target_property(BN_API_SOURCE_DIR binaryninjaapi SOURCE_DIR) +list(APPEND CMAKE_MODULE_PATH "${BN_API_SOURCE_DIR}/cmake") +find_package(BinaryNinjaCore REQUIRED) + +set_property(TARGET ${PROJECT_NAME} PROPERTY OUTPUT_FILE_PATH ${OUTPUT_FILE_PATH}) + +# Add the whole api to the depends too +file(GLOB API_SOURCES CONFIGURE_DEPENDS + ${BN_API_SOURCE_DIR}/binaryninjacore.h + ${BN_API_SOURCE_DIR}/rust/src/*.rs + ${BN_API_SOURCE_DIR}/rust/binaryninjacore-sys/src/*.rs) + +find_program(RUSTUP_PATH rustup REQUIRED HINTS ~/.cargo/bin) +set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_STABLE_VERSION} cargo) + +if(APPLE) + if(UNIVERSAL) + if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + endif() + else() + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release/${OUTPUT_FILE_NAME}) + endif() + endif() + + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=aarch64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=x86_64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=aarch64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=x86_64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND lipo -create ${AARCH64_LIB_PATH} ${X86_64_LIB_PATH} -output ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +elseif(WIN32) + if(DEMO) + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_PDB_NAME} ${OUTPUT_PDB_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) +endif() diff --git a/plugins/bntl_utils/Cargo.toml b/plugins/bntl_utils/Cargo.toml new file mode 100644 index 0000000000..a3f0780fb1 --- /dev/null +++ b/plugins/bntl_utils/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "bntl_utils" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +binaryninja.workspace = true +binaryninjacore-sys.workspace = true +tracing = "0.1" +thiserror = "2.0" +similar = "2.7.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tempdir = "0.3" +nt-apiset = "0.1.0" +url = "2.5" +uuid = "1.20" +walkdir = "2.5" +dashmap = "6.1" + +# For reports +minijinja = "2.10.2" +minijinja-embed = "2.10.2" + +[build-dependencies] +minijinja-embed = "2.10.2" + +# TODO: We need to depend on latest because the windows-metadata crate has not yet been bumped, but depending on the crate +# TODO: with git will mean we pull in all of the data of the crate instead of just the necessary bits, we likely need to +# TODO: wait until the windows-metadata crate is bumped before merging this PR. +# TODO: Relevant PR: https://github.com/microsoft/windows-rs/pull/3799 +# TODO: Relevant issue: https://github.com/microsoft/windows-rs/issues/3887 +[dependencies.windows-metadata] +git = "https://github.com/microsoft/windows-rs" +tag = "72" \ No newline at end of file diff --git a/plugins/bntl_utils/README.md b/plugins/bntl_utils/README.md new file mode 100644 index 0000000000..2ad2ab4bc2 --- /dev/null +++ b/plugins/bntl_utils/README.md @@ -0,0 +1,5 @@ +# BNTL Utilities + +A plugin and CLI tool for processing Binary Ninja type libraries (BNTL). + +For CLI build instructions and usage see [here](./cli/README.md). \ No newline at end of file diff --git a/plugins/bntl_utils/build.rs b/plugins/bntl_utils/build.rs new file mode 100644 index 0000000000..648b03715a --- /dev/null +++ b/plugins/bntl_utils/build.rs @@ -0,0 +1,48 @@ +use std::path::PathBuf; + +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(target_os = "linux")] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + #[cfg(target_os = "macos")] + { + let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set"); + let lib_name = crate_name.replace('-', "_"); + println!( + "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib", + lib_name + ); + } + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR specified"); + let out_dir_path = PathBuf::from(out_dir); + + // Copy all binaries to OUT_DIR for unit tests. + let bin_dir: PathBuf = "fixtures/".into(); + if let Ok(entries) = std::fs::read_dir(bin_dir) { + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let file_name = path.file_name().unwrap(); + let dest_path = out_dir_path.join(file_name); + std::fs::copy(&path, &dest_path).expect("failed to copy binary to OUT_DIR"); + } + } + } + + println!("cargo::rerun-if-changed=src/templates"); + // Templates used for rendering reports. + minijinja_embed::embed_templates!("src/templates"); +} diff --git a/plugins/bntl_utils/cli/Cargo.toml b/plugins/bntl_utils/cli/Cargo.toml new file mode 100644 index 0000000000..863b950d36 --- /dev/null +++ b/plugins/bntl_utils/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bntl_cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +binaryninja.workspace = true +binaryninjacore-sys.workspace = true +bntl_utils = { path = "../" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4.5.58", features = ["derive"] } +rayon = "1.11" +serde_json = "1.0" +thiserror = "2.0" \ No newline at end of file diff --git a/plugins/bntl_utils/cli/README.md b/plugins/bntl_utils/cli/README.md new file mode 100644 index 0000000000..5c4ddcb78f --- /dev/null +++ b/plugins/bntl_utils/cli/README.md @@ -0,0 +1,61 @@ +# Headless BNTL Processor + +Provides headless support for generating, inspecting, and validating Binary Ninja type libraries (BNTL). + +### Building + +> Assuming you have the following: +> - A compatible Binary Ninja with headless usage (see [this documentation](https://docs.binary.ninja/dev/batch.html#batch-processing-and-other-automation-tips) for more information) +> - Clang +> - Rust (currently tested for 1.91.1) +> - Set `BINARYNINJADIR` env variable to your installation directory (see [here](https://docs.binary.ninja/guide/#binary-path) for more details) + > - If this is not set, the -sys crate will try and locate using the default installation path and last run location. + +1. Clone this repository (`git clone https://github.com/Vector35/binaryninja-api/tree/dev`) +2. Build in release (`cargo build --release`) + +If compilation fails because it could not link against binaryninjacore than you should double-check you set `BINARYNINJADIR` correctly. + +Once it finishes you now will have a `bntl_cli` binary in `target/release` for use. + +### Usage + +> Assuming you already have the `bntl_cli` binary and a valid headless compatible Binary Ninja license. + +#### Create + +Generate a new type library from local files or remote projects. + +Examples: + +- `./bntl_cli create sqlite3.dll "windows-x86_64" ./headers/ ./output/` + - Places a single `sqlite.dll.bntl` file in the `output` directory, as headers have no dependency names associated they will be named `sqlite.dll`. +- `./bntl_cli create myproject "windows-x86_64" binaryninja://enterprise/https://enterprise.com/23ce5eaa-f532-4a93-80f2-a7d7f0aed040/ ./output/` + - Downloads and processes all files in the project, placing potentially multiple `.bntl` files in the `output` directory. +- `./bntl_cli create sqlite3.dll "windows-x86_64" ./winmd/ ./output/` + - `winmd` files are also supported as input, they will be processed together. You also probably want to provide some apiset schema files as well. + +#### Dump + +Export a type library back into a C header file for inspection. + +Examples: + +- `./bntl_cli dump sqlite3.dll.bntl ./output/sqlite.h` + +#### Diff + +Compare two type libraries and generate a .diff file containing a similarity ratio. + +Examples: + +- `./bntl_cli diff sqlite3.dll.bntl sqlite3.dll.bntl ./output/sqlite.diff` + +#### Validate + +Check type libraries for common errors, ensuring all referenced types exist across specified platforms. + +Examples: + +- `./bntl_cli validate ./typelibs/ ./output/` + - Pass in a directory containing `.bntl` files to validate, outputting a JSON file for each type library containing any errors. diff --git a/plugins/bntl_utils/cli/build.rs b/plugins/bntl_utils/cli/build.rs new file mode 100644 index 0000000000..ed6cec7d27 --- /dev/null +++ b/plugins/bntl_utils/cli/build.rs @@ -0,0 +1,15 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(not(target_os = "windows"))] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } +} diff --git a/plugins/bntl_utils/cli/src/create.rs b/plugins/bntl_utils/cli/src/create.rs new file mode 100644 index 0000000000..1a997b5f61 --- /dev/null +++ b/plugins/bntl_utils/cli/src/create.rs @@ -0,0 +1,74 @@ +use crate::input::{Input, ResolvedInput}; +use binaryninja::platform::Platform; +use bntl_utils::process::TypeLibProcessor; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct CreateArgs { + /// The name of the type library to create. + /// + /// TODO: Note that this wont be used for inputs which provide a name + pub name: String, + /// TODO: Note that this wont be used for inputs which provide a platform + pub platform: String, + pub input: Input, + pub output_directory: Option, + #[clap(long)] + pub dry_run: bool, +} + +impl CreateArgs { + pub fn execute(&self) { + let Some(_platform) = Platform::by_name(&self.platform) else { + tracing::error!("Failed to find platform: {}", self.platform); + let platforms: Vec<_> = Platform::list_all().iter().map(|p| p.name()).collect(); + tracing::error!("Available platforms: {}", platforms.join(", ")); + panic!("Platform not found"); + }; + + let output_path = self + .output_directory + .clone() + .unwrap_or(PathBuf::from("./output/")); + if output_path.exists() && !output_path.is_dir() { + tracing::error!("Output path {} is not a directory", output_path.display()); + return; + } + std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); + + let processor = TypeLibProcessor::new(&self.name, &self.platform); + // TODO: Need progress indicator here, when downloading files. + let resolved_input = self.input.resolve().expect("Failed to resolve input"); + + let data = match resolved_input { + ResolvedInput::Path(path) => processor.process(&path), + ResolvedInput::Project(project) => processor.process_project(&project), + ResolvedInput::ProjectFolder(project_folder) => { + processor.process_project_folder(&project_folder) + } + ResolvedInput::ProjectFile(project_file) => { + processor.process_project_file(&project_file) + } + } + .expect("Failed to process input"); + + if self.dry_run { + tracing::info!("Dry run enabled, skipping actual type library creation"); + return; + } + + for type_library in data.type_libraries { + let output_path = output_path.join(format!("{}.bntl", type_library.name())); + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } +} diff --git a/plugins/bntl_utils/cli/src/diff.rs b/plugins/bntl_utils/cli/src/diff.rs new file mode 100644 index 0000000000..1eedb97013 --- /dev/null +++ b/plugins/bntl_utils/cli/src/diff.rs @@ -0,0 +1,37 @@ +use binaryninja::types::TypeLibrary; +use bntl_utils::diff::TILDiff; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct DiffArgs { + pub file_a: PathBuf, + pub file_b: PathBuf, + /// Path to write the `.diff` file to. + pub output_path: PathBuf, + /// Timeout in seconds for the diff operation to complete, if provided the diffing will begin + /// to approximate after the deadline has passed. + #[clap(long)] + pub timeout: Option, +} + +impl DiffArgs { + pub fn execute(&self) { + let type_lib_a = + TypeLibrary::load_from_file(&self.file_a).expect("Failed to load type library"); + let type_lib_b = + TypeLibrary::load_from_file(&self.file_b).expect("Failed to load type library"); + + let diff_result = + match TILDiff::new().diff((&self.file_a, &type_lib_a), (&self.file_b, &type_lib_b)) { + Ok(diff_result) => diff_result, + Err(err) => { + tracing::error!("Failed to diff type libraries: {}", err); + return; + } + }; + tracing::info!("Similarity Ratio: {}", diff_result.ratio); + std::fs::write(&self.output_path, diff_result.diff).unwrap(); + tracing::info!("Diff written to: {}", self.output_path.display()); + } +} diff --git a/plugins/bntl_utils/cli/src/dump.rs b/plugins/bntl_utils/cli/src/dump.rs new file mode 100644 index 0000000000..fb25446b01 --- /dev/null +++ b/plugins/bntl_utils/cli/src/dump.rs @@ -0,0 +1,26 @@ +use binaryninja::types::TypeLibrary; +use bntl_utils::dump::TILDump; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct DumpArgs { + pub input: PathBuf, + pub output_path: Option, +} + +impl DumpArgs { + pub fn execute(&self) { + let type_lib = + TypeLibrary::load_from_file(&self.input).expect("Failed to load type library"); + let default_output_path = self.input.with_extension("h"); + let output_path = self.output_path.as_ref().unwrap_or(&default_output_path); + let dependencies = + bntl_utils::helper::path_to_type_libraries(&self.input.parent().unwrap()); + let printed_types = TILDump::new() + .with_type_libs(dependencies) + .dump(&type_lib) + .expect("Failed to dump type library"); + std::fs::write(output_path, printed_types).expect("Failed to write type library header"); + } +} diff --git a/plugins/bntl_utils/cli/src/input.rs b/plugins/bntl_utils/cli/src/input.rs new file mode 100644 index 0000000000..1e1dbb6eac --- /dev/null +++ b/plugins/bntl_utils/cli/src/input.rs @@ -0,0 +1,167 @@ +use binaryninja::collaboration::RemoteFile; +use binaryninja::project::Project; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::folder::ProjectFolder; +use binaryninja::rc::Ref; +use bntl_utils::url::{BnParsedUrl, BnResource}; +use std::fmt::Display; +use std::path::PathBuf; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug)] +pub enum ResolvedInput { + Path(PathBuf), + Project(Ref), + ProjectFolder(Ref), + ProjectFile(Ref), +} + +#[derive(Error, Debug)] +pub enum InputResolveError { + #[error("Resource resolution failed: {0}")] + ResourceError(#[from] bntl_utils::url::BnResourceError), + + #[error("Collaboration API error: {0}")] + CollaborationError(String), + + #[error("Download failed for {url}: status {status}")] + DownloadFailed { url: String, status: u16 }, + + #[error("Download provider error: {0}")] + DownloadProviderError(String), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Environment error: {0}")] + EnvError(String), +} + +/// An input to the CLI to locate a "resource", such as a file or directory. +#[derive(Debug, Clone)] +pub enum Input { + /// A URL which references a Binary Ninja resource, such as a remote project or file. + ParsedUrl(BnParsedUrl), + /// A local filesystem path pointing to a file or directory. + LocalPath(PathBuf), +} + +impl Input { + /// Attempt to acquire a path from this input, this can download files over the network and + /// is meant to be called when the file contents are desired. + pub fn resolve(&self) -> Result { + let try_download_file = |file: &RemoteFile| -> Result<(), InputResolveError> { + if !file.core_file().unwrap().exists_on_disk() { + let _span = + tracing::info_span!("Downloading project file", file = %file.name()).entered(); + file.download().map_err(|_| { + InputResolveError::CollaborationError("Failed to download project file".into()) + })?; + } + Ok(()) + }; + + match self { + Input::ParsedUrl(url) => match url.to_resource()? { + BnResource::RemoteProject(project) => { + let files = project.files().map_err(|_| { + InputResolveError::CollaborationError("Failed to get files".into()) + })?; + + for file in &files { + try_download_file(&file)?; + } + + let core = project.core_project().map_err(|_| { + InputResolveError::CollaborationError("Missing core project".into()) + })?; + Ok(ResolvedInput::Project(core)) + } + + BnResource::RemoteProjectFile(file) => { + try_download_file(&file)?; + let core = file.core_file().expect("Missing core file"); + Ok(ResolvedInput::ProjectFile(core)) + } + + BnResource::RemoteProjectFolder(folder) => { + let project = folder.project().map_err(|_| { + InputResolveError::CollaborationError("Failed to get project".into()) + })?; + let files = project.files().map_err(|_| { + InputResolveError::CollaborationError("Failed to get files".into()) + })?; + + for file in &files { + if let Some(file_folder) = file.folder().ok().flatten() { + if file_folder == folder { + try_download_file(&file)?; + } + } + } + + let core = folder.core_folder().map_err(|_| { + InputResolveError::CollaborationError("Missing core folder".into()) + })?; + Ok(ResolvedInput::ProjectFolder(core)) + } + + BnResource::RemoteFile(url) => { + let safe_name = url.to_string().replace(['/', ':', '?'], "_"); + let cached_file_path = std::env::temp_dir().join(safe_name); + if cached_file_path.exists() { + return Ok(ResolvedInput::Path(cached_file_path)); + } + + let download_provider = binaryninja::download::DownloadProvider::try_default() + .expect("Failed to get default download provider"); + let mut instance = download_provider + .create_instance() + .expect("Failed to create download provider instance"); + let _span = + tracing::info_span!("Downloading remote file", url = %url).entered(); + let response = instance + .get(&url.to_string(), Vec::new()) + .map_err(|e| InputResolveError::DownloadProviderError(e.to_string()))?; + if response.is_success() { + std::fs::write(&cached_file_path, response.data)?; + Ok(ResolvedInput::Path(cached_file_path)) + } else { + Err(InputResolveError::DownloadFailed { + url: url.to_string(), + status: response.status_code, + }) + } + } + + BnResource::LocalFile(path) => Ok(ResolvedInput::Path(path.clone())), + }, + Input::LocalPath(path) => Ok(ResolvedInput::Path(path.clone())), + } + } +} + +impl FromStr for Input { + type Err = String; + + fn from_str(s: &str) -> Result { + // Try to parse as a Binary Ninja URL + if s.starts_with("binaryninja:") { + let url = BnParsedUrl::parse(s).map_err(|e| format!("URL Parse Error: {}", e))?; + return Ok(Input::ParsedUrl(url)); + } + + let path = PathBuf::from(s); + Ok(Input::LocalPath(path)) + } +} + +impl Display for Input { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Input::ParsedUrl(url) => write!(f, "{}", url), + Input::LocalPath(path) => write!(f, "{}", path.display()), + } + } +} diff --git a/plugins/bntl_utils/cli/src/main.rs b/plugins/bntl_utils/cli/src/main.rs new file mode 100644 index 0000000000..1466da1fdc --- /dev/null +++ b/plugins/bntl_utils/cli/src/main.rs @@ -0,0 +1,71 @@ +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +mod create; +mod diff; +mod dump; +mod input; +mod validate; + +/// Generate, inspect, and validate Binary Ninja type libraries (BNTL) +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Parser, Debug)] +pub enum Command { + /// Create a new type library from a set of files. + Create(create::CreateArgs), + /// Dump the type library to a C header file. + Dump(dump::DumpArgs), + /// Generate a diff between two type libraries. + Diff(diff::DiffArgs), + /// Validate the type libraries for common errors. + Validate(validate::ValidateArgs), +} + +impl Command { + pub fn execute(&self) { + match self { + Command::Create(args) => { + args.execute(); + } + Command::Dump(args) => { + args.execute(); + } + Command::Diff(args) => { + args.execute(); + } + Command::Validate(args) => { + args.execute(); + } + } + } +} + +fn main() { + let cli = Cli::parse(); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + + // Capture logs from Binary Ninja + let _listener = binaryninja::tracing::TracingLogListener::new().register(); + + // Initialize Binary Ninja, requires a headless compatible license like commercial or ultimate. + let _session = binaryninja::headless::Session::new() + .expect("Failed to create headless binary ninja session"); + + cli.command.execute(); +} diff --git a/plugins/bntl_utils/cli/src/validate.rs b/plugins/bntl_utils/cli/src/validate.rs new file mode 100644 index 0000000000..743ca927af --- /dev/null +++ b/plugins/bntl_utils/cli/src/validate.rs @@ -0,0 +1,66 @@ +use binaryninja::platform::Platform; +use bntl_utils::validate::{TypeLibValidater, ValidateIssue}; +use clap::Args; +use rayon::prelude::*; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct ValidateArgs { + /// Path to the directory containing the type libraries to validate. + /// + /// This must contain all the type libraries referencable. + pub input: PathBuf, + /// Dump validation results to the directory specified. + #[clap(short, long)] + pub output: Option, +} + +impl ValidateArgs { + pub fn execute(&self) { + if let Some(output_dir) = &self.output { + std::fs::create_dir_all(output_dir).expect("Failed to create output directory"); + } + + // TODO: For now we just pass all the type libraries in the containing input directory. + let type_libs = bntl_utils::helper::path_to_type_libraries(&self.input); + type_libs.par_iter().for_each(|type_lib| { + // We run validation per platform. This is to make sure that if we depend on platform + // types that they exist in each one of the specified platforms, not just one of them. + let mut platform_mapped_issues: HashMap> = HashMap::new(); + let available_platforms = type_lib.platform_names(); + + for platform in &available_platforms { + let platform = Platform::by_name(platform).expect("Failed to load platform"); + let mut ctx = TypeLibValidater::new() + .with_type_libraries(type_libs.clone()) + .with_platform(&platform); + let result = ctx.validate(&type_lib); + for issue in &result.issues { + platform_mapped_issues + .entry(issue.clone()) + .or_default() + .push(platform.name().to_string()); + } + + if let Some(output_dir) = &self.output + && !result.issues.is_empty() + { + let dump_path = output_dir + .join(type_lib.name()) + .with_extension(format!("{}.problems.json", platform.name())); + let result = serde_json::to_string_pretty(&result.issues) + .expect("Failed to serialize result"); + std::fs::write(dump_path, result).expect("Failed to write validation result"); + } + } + + for (issue, platforms) in platform_mapped_issues { + match (available_platforms.len(), platforms.len()) { + (1, _) => tracing::error!("{}", issue), + _ => tracing::error!("{}: {}", platforms.join(", "), issue), + } + } + }); + } +} diff --git a/plugins/bntl_utils/src/command.rs b/plugins/bntl_utils/src/command.rs new file mode 100644 index 0000000000..39f25b9945 --- /dev/null +++ b/plugins/bntl_utils/src/command.rs @@ -0,0 +1,66 @@ +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::user_directory; +use std::path::PathBuf; + +pub mod create; +pub mod diff; +pub mod dump; +pub mod validate; +// TODO: Load? + +pub struct InputFileField; + +impl InputFileField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "File Path".to_string(), + // TODO: This is called extension but is really a filter. + extension: None, + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("File Path")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct OutputDirectoryField; + +impl OutputDirectoryField { + pub fn field() -> FormInputField { + let type_lib_dir = user_directory().join("typelib"); + FormInputField::DirectoryName { + prompt: "Output Directory".to_string(), + default: Some(type_lib_dir.to_string_lossy().to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Output Directory")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct InputDirectoryField; + +impl InputDirectoryField { + pub fn field() -> FormInputField { + FormInputField::DirectoryName { + prompt: "Input Directory".to_string(), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Input Directory")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} diff --git a/plugins/bntl_utils/src/command/create.rs b/plugins/bntl_utils/src/command/create.rs new file mode 100644 index 0000000000..0dcf6c3b35 --- /dev/null +++ b/plugins/bntl_utils/src/command/create.rs @@ -0,0 +1,196 @@ +use crate::command::{InputDirectoryField, OutputDirectoryField}; +use crate::process::{new_processing_state_background_thread, TypeLibProcessor}; +use crate::validate::TypeLibValidater; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use binaryninja::interaction::{Form, FormInputField, MessageBoxButtonSet, MessageBoxIcon}; +use binaryninja::platform::Platform; +use std::thread; + +pub struct CreateFromCurrentView; + +impl Command for CreateFromCurrentView { + fn action(&self, view: &BinaryView) { + let mut form = Form::new("Create From View"); + // TODO: The choice to select what types to include + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + let Some(default_platform) = view.default_platform() else { + tracing::error!("No default platform set for view"); + return; + }; + + let file_path = view.file().file_path(); + let file_name = file_path.file_name().unwrap_or_default().to_string_lossy(); + let processor = TypeLibProcessor::new(&file_name, &default_platform.name()); + let data = match processor.process_view(file_path, view) { + Ok(data) => data, + Err(err) => { + tracing::error!("Failed to process view: {}", err); + return; + } + } + .prune(); + + let attached_libraries = view + .type_libraries() + .iter() + .map(|t| t.to_owned()) + .chain(data.type_libraries.iter().map(|t| t.to_owned())) + .collect::>(); + let mut validator = TypeLibValidater::new() + .with_platform(&default_platform) + .with_type_libraries(attached_libraries); + + for type_library in data.type_libraries { + let output_path = output_dir.join(format!("{}.bntl", type_library.name())); + + let validation_result = validator.validate(&type_library); + if !validation_result.issues.is_empty() { + tracing::error!( + "Found {} issues in type library '{}'", + validation_result.issues.len(), + type_library.name() + ); + match validation_result.render_report() { + Ok(rendered) => { + view.show_html_report(&type_library.name(), &rendered, ""); + if let Err(e) = std::fs::write(output_path.with_extension("html"), rendered) + { + tracing::error!( + "Failed to write validation report to {}: {}", + output_path.display(), + e + ); + } + } + Err(err) => tracing::error!("Failed to render validation report: {}", err), + } + } + + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} + +pub struct NameField; + +impl NameField { + pub fn field() -> FormInputField { + FormInputField::TextLine { + prompt: "Dependency Name".to_string(), + default: Some("foo.dll".to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Dependency Name")?; + field.try_value_string() + } +} + +pub struct PlatformField; + +impl PlatformField { + pub fn field() -> FormInputField { + FormInputField::TextLine { + prompt: "Platform Name".to_string(), + default: Some("windows-x86_64".to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Platform Name")?; + field.try_value_string() + } +} + +pub struct CreateFromDirectory; + +impl CreateFromDirectory { + pub fn execute() { + let mut form = Form::new("Create From Directory"); + // TODO: The choice to select what types to include + form.add_field(InputDirectoryField::field()); + form.add_field(PlatformField::field()); + form.add_field(NameField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let input_dir = InputDirectoryField::from_form(&form).unwrap(); + let platform_name = PlatformField::from_form(&form).unwrap(); + let default_name = NameField::from_form(&form).unwrap(); + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + + let Some(default_platform) = Platform::by_name(&platform_name) else { + tracing::error!("Invalid platform name: {}", platform_name); + return; + }; + + let processor = TypeLibProcessor::new(&default_name, &default_platform.name()); + + let background_task = BackgroundTask::new("Processing started...", true); + new_processing_state_background_thread(background_task.clone(), processor.state()); + let data = processor.process_directory(&input_dir); + background_task.finish(); + + let pruned_data = match data { + // Prune off empty type libraries, no need to save them. + Ok(data) => data.prune(), + Err(err) => { + binaryninja::interaction::show_message_box( + "Failed to process directory", + &err.to_string(), + MessageBoxButtonSet::OKButtonSet, + MessageBoxIcon::ErrorIcon, + ); + tracing::error!("Failed to create signature file: {}", err); + return; + } + }; + + for type_library in pruned_data.type_libraries { + let output_path = output_dir.join(format!("{}.bntl", type_library.name())); + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } +} + +impl Command for CreateFromDirectory { + fn action(&self, _view: &BinaryView) { + thread::spawn(move || { + CreateFromDirectory::execute(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/diff.rs b/plugins/bntl_utils/src/command/diff.rs new file mode 100644 index 0000000000..ae93dafe39 --- /dev/null +++ b/plugins/bntl_utils/src/command/diff.rs @@ -0,0 +1,103 @@ +use crate::command::OutputDirectoryField; +use crate::diff::TILDiff; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::Command; +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::types::TypeLibrary; +use std::path::PathBuf; +use std::thread; + +pub struct InputFileAField; + +impl InputFileAField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "Library A".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.bntl".to_string()), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Library A")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct InputFileBField; + +impl InputFileBField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "Library B".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.bntl".to_string()), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Library B")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct Diff; + +impl Diff { + pub fn execute() { + let mut form = Form::new("Diff type libraries"); + form.add_field(InputFileAField::field()); + form.add_field(InputFileBField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let a_path = InputFileAField::from_form(&form).unwrap(); + let b_path = InputFileBField::from_form(&form).unwrap(); + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + + let _bg_task = BackgroundTask::new("Diffing type libraries...", false).enter(); + let Some(type_lib_a) = TypeLibrary::load_from_file(&a_path) else { + tracing::error!("Failed to load type library: {}", a_path.display()); + return; + }; + let Some(type_lib_b) = TypeLibrary::load_from_file(&b_path) else { + tracing::error!("Failed to load type library: {}", b_path.display()); + return; + }; + + let diff_result = match TILDiff::new().diff((&a_path, &type_lib_a), (&b_path, &type_lib_b)) + { + Ok(diff_result) => diff_result, + Err(err) => { + tracing::error!("Failed to diff type libraries: {}", err); + return; + } + }; + tracing::info!("Similarity Ratio: {}", diff_result.ratio); + let output_path = output_dir + .join(type_lib_a.dependency_name()) + .with_extension("diff"); + std::fs::write(&output_path, diff_result.diff).unwrap(); + tracing::info!("Diff written to: {}", output_path.display()); + } +} + +impl Command for Diff { + fn action(&self, _view: &BinaryView) { + thread::spawn(move || { + Diff::execute(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/dump.rs b/plugins/bntl_utils/src/command/dump.rs new file mode 100644 index 0000000000..4b68cd799d --- /dev/null +++ b/plugins/bntl_utils/src/command/dump.rs @@ -0,0 +1,52 @@ +use crate::command::{InputFileField, OutputDirectoryField}; +use crate::dump::TILDump; +use crate::helper::path_to_type_libraries; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::Command; +use binaryninja::interaction::Form; +use binaryninja::types::TypeLibrary; + +pub struct Dump; + +impl Command for Dump { + // TODO: We need a command type that does not require a binary view. + fn action(&self, _view: &BinaryView) { + let mut form = Form::new("Dump to C Header"); + // TODO: The choice to select what to include? + form.add_field(InputFileField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + let input_path = InputFileField::from_form(&form).unwrap(); + + let type_lib = match TypeLibrary::load_from_file(&input_path) { + Some(type_lib) => type_lib, + None => { + tracing::error!("Failed to load type library from {}", input_path.display()); + return; + } + }; + + // TODO: Currently we collect input path dependencies from the platform and the parent directory. + let dependencies = path_to_type_libraries(input_path.parent().unwrap()); + let dump = match TILDump::new().with_type_libs(dependencies).dump(&type_lib) { + Ok(dump) => dump, + Err(err) => { + tracing::error!("Failed to dump type library: {}", err); + return; + } + }; + + let output_path = output_dir.join(format!("{}.h", type_lib.name())); + if let Err(e) = std::fs::write(&output_path, dump) { + tracing::error!("Failed to write dump to {}: {}", output_path.display(), e); + } + tracing::info!("Dump written to {}", output_path.display()); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/validate.rs b/plugins/bntl_utils/src/command/validate.rs new file mode 100644 index 0000000000..8f0095ae2c --- /dev/null +++ b/plugins/bntl_utils/src/command/validate.rs @@ -0,0 +1,78 @@ +use crate::helper::path_to_type_libraries; +use crate::validate::TypeLibValidater; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use binaryninja::interaction::get_open_filename_input; +use binaryninja::platform::Platform; +use binaryninja::types::TypeLibrary; + +pub struct Validate; + +impl Command for Validate { + fn action(&self, _view: &BinaryView) { + let Some(input_path) = + get_open_filename_input("Select a type library to validate", "*.bntl") + else { + return; + }; + + let type_lib = match TypeLibrary::load_from_file(&input_path) { + Some(type_lib) => type_lib, + None => { + tracing::error!("Failed to load type library from {}", input_path.display()); + return; + } + }; + + // Type libraries should always have at least one platform associated with them. + if type_lib.platform_names().is_empty() { + tracing::error!("Type library {} has no platforms!", input_path.display()); + return; + } + + // TODO: Currently we collect input path dependencies from the platform and the parent directory. + let dependencies = path_to_type_libraries(input_path.parent().unwrap()); + + let validator = TypeLibValidater::new().with_type_libraries(dependencies); + // Validate for every platform so that we can find issues in lesser used platforms. + for platform_name in &type_lib.platform_names() { + let Some(platform) = Platform::by_name(platform_name) else { + tracing::error!("Failed to find platform with name {}", platform_name); + continue; + }; + let results = validator + .clone() + .with_platform(&platform) + .validate(&type_lib); + if results.issues.is_empty() { + tracing::info!( + "No issues found for type library {} on platform {}", + type_lib.name(), + platform_name + ); + continue; + } + let rendered = match results.render_report() { + Ok(rendered) => rendered, + Err(err) => { + tracing::error!("Failed to render validation report: {}", err); + continue; + } + }; + let out_path = input_path.with_extension(format!("{}.html", platform_name)); + let out_name = format!("{} ({})", type_lib.name(), platform_name); + _view.show_html_report(&out_name, &rendered, ""); + if let Err(e) = std::fs::write(out_path, rendered) { + tracing::error!( + "Failed to write validation report to {}: {}", + input_path.display(), + e + ); + } + } + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/diff.rs b/plugins/bntl_utils/src/diff.rs new file mode 100644 index 0000000000..18d4dce896 --- /dev/null +++ b/plugins/bntl_utils/src/diff.rs @@ -0,0 +1,83 @@ +use crate::dump::TILDump; +use crate::helper::path_to_type_libraries; +use binaryninja::types::TypeLibrary; +use similar::{Algorithm, TextDiff}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TILDiffError { + #[error("Could not determine parent directory for path: {0}")] + InvalidPath(PathBuf), + + #[error("Failed to dump type library: {0}")] + DumpError(String), +} + +pub struct DiffResult { + pub ratio: f32, + pub diff: String, +} + +pub struct TILDiff { + timeout: Duration, +} + +impl TILDiff { + pub fn new() -> Self { + Self { + timeout: Duration::from_secs(180), + } + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + pub fn diff( + &self, + (a_path, a_type_lib): (&Path, &TypeLibrary), + (b_path, b_type_lib): (&Path, &TypeLibrary), + ) -> Result { + let a_parent = a_path + .parent() + .ok_or_else(|| TILDiffError::InvalidPath(a_path.to_path_buf()))?; + let b_parent = b_path + .parent() + .ok_or_else(|| TILDiffError::InvalidPath(b_path.to_path_buf()))?; + + let a_dependencies = path_to_type_libraries(a_parent); + let b_dependencies = path_to_type_libraries(b_parent); + + let dumped_a = TILDump::new() + .with_type_libs(a_dependencies) + .dump(a_type_lib) + .map_err(|e| TILDiffError::DumpError(e.to_string()))?; + + let dumped_b = TILDump::new() + .with_type_libs(b_dependencies) + .dump(b_type_lib) + .map_err(|e| TILDiffError::DumpError(e.to_string()))?; + + let diff = TextDiff::configure() + .algorithm(Algorithm::Patience) + .timeout(self.timeout) + .diff_lines(&dumped_a, &dumped_b); + + let diff_content = diff + .unified_diff() + .context_radius(3) + .header( + a_path.to_string_lossy().as_ref(), + b_path.to_string_lossy().as_ref(), + ) + .to_string(); + + Ok(DiffResult { + ratio: diff.ratio(), + diff: diff_content, + }) + } +} diff --git a/plugins/bntl_utils/src/dump.rs b/plugins/bntl_utils/src/dump.rs new file mode 100644 index 0000000000..bfcb03fa61 --- /dev/null +++ b/plugins/bntl_utils/src/dump.rs @@ -0,0 +1,146 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::file_metadata::FileMetadata; +use binaryninja::metadata::{Metadata, MetadataType}; +use binaryninja::platform::Platform; +use binaryninja::rc::Ref; +use binaryninja::types::printer::TokenEscapingType; +use binaryninja::types::{CoreTypePrinter, TypeLibrary}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TILDumpError { + #[error("Failed to create empty BinaryView")] + ViewCreationFailed, + + #[error("Type library has no associated platforms")] + NoPlatformFound, + + #[error("Platform '{0}' not found in Binary Ninja")] + PlatformNotFound(String), + + #[error("Failed to print types from library")] + PrinterError, + + #[error("Metadata error: {0}")] + MetadataError(String), + + #[error("Unexpected metadata type for 'ordinals': {0:?}")] + UnexpectedMetadataType(MetadataType), +} + +pub struct TILDump { + /// The type libraries that are accessible to the type printer. + available_type_libs: Vec>, +} + +impl TILDump { + pub fn new() -> Self { + Self { + available_type_libs: Vec::new(), + } + } + + pub fn with_type_libs(mut self, type_libs: Vec>) -> Self { + self.available_type_libs = type_libs; + self + } + + pub fn dump(&self, type_lib: &TypeLibrary) -> Result { + let empty_file = FileMetadata::new(); + let empty_bv = BinaryView::from_data(&empty_file, &[]) + .map_err(|_| TILDumpError::ViewCreationFailed)?; + + let type_lib_plats = type_lib.platform_names(); + let platform_name = type_lib_plats + .iter() + .next() + .ok_or(TILDumpError::NoPlatformFound)?; + + let platform_name_str = platform_name.to_string(); + let platform = Platform::by_name(&platform_name_str) + .ok_or_else(|| TILDumpError::PlatformNotFound(platform_name_str))?; + + empty_bv.set_default_platform(&platform); + + for dependency in &self.available_type_libs { + empty_bv.add_type_library(dependency); + } + empty_bv.add_type_library(type_lib); + + for ty in &type_lib.named_types() { + empty_bv.import_type_library(ty.name, None); + } + for obj in &type_lib.named_objects() { + empty_bv.import_type_object(obj.name, None); + } + + let dep_sorted_types = empty_bv.dependency_sorted_types(); + let unsorted_functions = type_lib.named_objects(); + let mut all_types: Vec<_> = dep_sorted_types + .iter() + .chain(unsorted_functions.iter()) + .collect(); + all_types.sort_by_key(|t| t.name.clone()); + + let type_printer = CoreTypePrinter::default(); + let printed_types = type_printer + .print_all_types( + all_types, + &empty_bv, + 4, + TokenEscapingType::NoTokenEscapingType, + ) + .ok_or(TILDumpError::PrinterError)?; + + let mut printed_types_str = printed_types.to_string_lossy().to_string(); + printed_types_str.push_str("\n// TYPE LIBRARY INFORMATION\n"); + + let metadata_lines = type_library_metadata_to_string(type_lib)?; + printed_types_str.push_str(&metadata_lines.join("\n")); + + empty_file.close(); + Ok(printed_types_str) + } +} + +fn type_library_metadata_to_string(type_lib: &TypeLibrary) -> Result, TILDumpError> { + let mut result = Vec::new(); + for alt_name in &type_lib.alternate_names() { + result.push(format!("// ALTERNATE NAME: {}", alt_name)); + } + + let mut add_ordinals = |metadata: Ref| -> Result<(), TILDumpError> { + if let Some(map) = metadata.get_value_store() { + let mut list = map.iter().collect::>(); + list.sort_by_key(|&(key, _)| key.parse::().unwrap_or_default()); + for (key, value) in list { + result.push(format!("// ORDINAL {}: {}", key, value)); + } + } + Ok(()) + }; + + if let Some(ordinal_key) = type_lib.query_metadata("ordinals") { + match ordinal_key.get_type() { + MetadataType::StringDataType => { + let queried_key = ordinal_key.get_string().ok_or_else(|| { + TILDumpError::MetadataError("Failed to get ordinal key string".into()) + })?; + + let queried_key_str = queried_key.to_string_lossy(); + let queried_md = type_lib.query_metadata(&queried_key_str).ok_or_else(|| { + TILDumpError::MetadataError(format!( + "Failed to query metadata for key: {}", + queried_key_str + )) + })?; + + add_ordinals(queried_md)?; + } + MetadataType::KeyValueDataType => add_ordinals(ordinal_key)?, + ty => return Err(TILDumpError::UnexpectedMetadataType(ty)), + } + } + + Ok(result) +} diff --git a/plugins/bntl_utils/src/helper.rs b/plugins/bntl_utils/src/helper.rs new file mode 100644 index 0000000000..3fc1a0470b --- /dev/null +++ b/plugins/bntl_utils/src/helper.rs @@ -0,0 +1,45 @@ +use binaryninja::rc::Ref; +use binaryninja::types::{NamedTypeReference, Type, TypeClass, TypeLibrary}; +use std::path::Path; +use walkdir::WalkDir; + +pub fn path_to_type_libraries(path: &Path) -> Vec> { + WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "bntl")) + .filter_map(|e| TypeLibrary::load_from_file(e.path())) + .collect::>() +} + +pub fn visit_type_reference(ty: &Type, visit: &mut impl FnMut(&NamedTypeReference)) { + if let Some(ntr) = ty.get_named_type_reference() { + visit(&ntr); + } + match ty.type_class() { + TypeClass::StructureTypeClass => { + let structure = ty.get_structure().unwrap(); + for field in structure.members() { + visit_type_reference(&field.ty.contents, visit); + } + for base in structure.base_structures() { + visit(&base.ty); + } + } + TypeClass::PointerTypeClass => { + visit_type_reference(&ty.child_type().unwrap().contents, visit); + } + TypeClass::ArrayTypeClass => { + visit_type_reference(&ty.child_type().unwrap().contents, visit); + } + TypeClass::FunctionTypeClass => { + let params = ty.parameters().unwrap(); + for param in params { + visit_type_reference(¶m.ty.contents, visit); + } + visit_type_reference(&ty.return_value().unwrap().contents, visit); + } + _ => {} + } +} diff --git a/plugins/bntl_utils/src/lib.rs b/plugins/bntl_utils/src/lib.rs new file mode 100644 index 0000000000..3c6dbe1957 --- /dev/null +++ b/plugins/bntl_utils/src/lib.rs @@ -0,0 +1,61 @@ +mod command; +pub mod diff; +pub mod dump; +pub mod helper; +pub mod process; +pub mod schema; +pub mod url; +pub mod validate; +mod winmd; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginInit() -> bool { + if plugin_init().is_err() { + tracing::error!("Failed to initialize BNTL Utils plug-in"); + return false; + } + true +} + +fn plugin_init() -> Result<(), ()> { + binaryninja::tracing_init!("BNTL Utils"); + + binaryninja::command::register_command( + "BNTL\\Create\\From Current View", + "Create .bntl files from the current view", + command::create::CreateFromCurrentView {}, + ); + + binaryninja::command::register_command( + "BNTL\\Create\\From Project", + "Create .bntl files from the given project", + command::create::CreateFromCurrentView {}, + ); + + binaryninja::command::register_command( + "BNTL\\Create\\From Directory", + "Create .bntl files from the given directory", + command::create::CreateFromDirectory {}, + ); + + binaryninja::command::register_command( + "BNTL\\Diff", + "Diff two .bntl files and output the difference to a file", + command::diff::Diff {}, + ); + + binaryninja::command::register_command( + "BNTL\\Dump To Header", + "Dump a .bntl file to a header file", + command::dump::Dump {}, + ); + + binaryninja::command::register_command( + "BNTL\\Validate", + "Validate a .bntl file and report the issues", + command::validate::Validate {}, + ); + + Ok(()) +} diff --git a/plugins/bntl_utils/src/process.rs b/plugins/bntl_utils/src/process.rs new file mode 100644 index 0000000000..f2adf58e2f --- /dev/null +++ b/plugins/bntl_utils/src/process.rs @@ -0,0 +1,932 @@ +//! Process different types of files into Binary Ninja type libraries. + +use binaryninja::architecture::CoreArchitecture; +use dashmap::DashMap; +use std::collections::{HashMap, HashSet}; +use std::env::temp_dir; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use thiserror::Error; +use walkdir::WalkDir; + +use crate::schema::BntlSchema; +use crate::winmd::WindowsMetadataImporter; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::custom_binary_view::BinaryViewType; +use binaryninja::file_metadata::FileMetadata; +use binaryninja::metadata::Metadata; +use binaryninja::platform::Platform; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::folder::ProjectFolder; +use binaryninja::project::Project; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::section::Section; +use binaryninja::types::{CoreTypeParser, Type, TypeLibrary, TypeParser, TypeParserError}; +use nt_apiset::{ApiSetMap, NtApiSetError}; + +#[derive(Error, Debug)] +pub enum ProcessingError { + #[error("Binary view load error: {0}")] + BinaryViewLoad(PathBuf), + + #[error("Failed to read binary view at offset {0:?} with length {1:?}")] + BinaryViewRead(u64, usize), + + #[error("Failed to read .apiset section: {0}")] + FailedToReadApiSet(#[from] NtApiSetError), + + #[error("Failed to read file: {0}")] + FileRead(std::io::Error), + + #[error("Failed to retrieve path to project file: {0:?}")] + NoPathToProjectFile(Ref), + + #[error("Processing state has been poisoned")] + StatePoisoned, + + #[error("Processing has been cancelled")] + Cancelled, + + #[error("Skipping file: {0}")] + SkippedFile(PathBuf), + + #[error("Failed to find platform: {0}")] + PlatformNotFound(String), + + #[error("Failed to parse types: {0:?}")] + TypeParsingFailed(Vec), + + #[error("Failed to import winmd: {0}")] + WinMdFailedImport(crate::winmd::ImportError), + + #[error("Failed to parse type library: {0}")] + InvalidTypeLibrary(PathBuf), +} + +#[derive(Default, Debug)] +pub struct ProcessingState { + pub cancelled: AtomicBool, + pub files: DashMap, +} + +impl ProcessingState { + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Relaxed) + } + + pub fn cancel(&self) { + self.cancelled.store(true, Relaxed) + } + + pub fn files_with_state(&self, state: bool) -> usize { + self.files.iter().filter(|f| *f.value() == state).count() + } + + pub fn set_file_state(&self, path: PathBuf, state: bool) { + self.files.insert(path, state); + } + + pub fn total_files(&self) -> usize { + self.files.len() + } +} + +pub fn new_processing_state_background_thread( + task: Ref, + state: Arc, +) { + std::thread::spawn(move || { + let start = Instant::now(); + while !task.is_finished() { + std::thread::sleep(Duration::from_millis(100)); + // Check if the user wants to cancel the processing. + if task.is_cancelled() { + state.cancel(); + } + + let total = state.total_files(); + let processed = state.files_with_state(true); + let unprocessed = state.files_with_state(false); + let completion = (processed as f64 / total as f64) * 100.0; + let elapsed = start.elapsed().as_secs_f32(); + let text = format!( + "Processing {} files... {{{}|{}}} ({:.2}%) [{:.2}s]", + total, unprocessed, processed, completion, elapsed + ); + task.set_progress_text(&text); + } + }); +} + +/// The result of running [`TypeLibProcessor`]. +#[derive(Debug, Clone)] +pub struct ProcessedData { + // TODO: Maybe we instead have an intermediate format that is easier to work with? + pub type_libraries: Vec>, +} + +impl ProcessedData { + pub fn new(type_libraries: Vec>) -> Self { + Self { type_libraries } + } + + /// Prune empty type libraries from the processed data. + /// + /// This is useful if you intend to save the type libraries to disk in a finalized form. + pub fn prune(self) -> Self { + let is_empty = + |tl: &TypeLibrary| tl.named_types().is_empty() && tl.named_objects().is_empty(); + let pruned_type_libraries = self + .type_libraries + .into_iter() + .filter(|tl| !is_empty(tl)) + .collect::>(); + Self::new(pruned_type_libraries) + } + + /// Merges multiple [`ProcessedData`] into one, deduplicating type libraries. + /// + /// This is necessary to allow the [`TypeLibProcessor`] to operate on a wide range of formats whilst + /// also guaranteeing no collisions and valid external references. Without merging libraries with + /// identical dependency names would be separate, which is not a supported scenario when loading + /// type libraries into Binary Ninja. + pub fn merge(list: &[ProcessedData]) -> Self { + let mut type_libraries = Vec::new(); + for data in list { + type_libraries.extend(data.type_libraries.iter().cloned()); + } + + // We merge type libraries with the same dependency name, as that is what needs to be unique + // when we go to load them into Binary Ninja. + let mut mapped_type_libraries: HashMap<(String, CoreArchitecture), Vec>> = + HashMap::new(); + for tl in type_libraries.iter() { + mapped_type_libraries + .entry((tl.dependency_name(), tl.arch())) + .or_default() + .push(tl.clone()); + } + + let mut merged_type_libraries = Vec::new(); + for ((dependency_name, arch), type_libraries) in mapped_type_libraries { + let merged_type_library = TypeLibrary::new(arch, &dependency_name); + merged_type_library.set_dependency_name(&dependency_name); + for tl in type_libraries { + // TODO: Cheap type overrides (if one type is set as void* and the other as Foo* we take Foo*) + for named_type in &tl.named_types() { + merged_type_library.add_named_type(named_type.name.clone(), &named_type.ty); + } + for named_object in &tl.named_objects() { + merged_type_library + .add_named_object(named_object.name.clone(), &named_object.ty); + } + for alt_name in &tl.alternate_names() { + merged_type_library.add_alternate_name(alt_name); + } + for platform_name in &tl.platform_names() { + if let Some(platform) = Platform::by_name(&platform_name) { + merged_type_library.add_platform(&platform); + } else { + // TODO: Upgrade this to an error? + tracing::warn!( + "Unknown platform name when merging '{}': '{}'", + dependency_name, + platform_name + ); + } + } + // TODO: Stealing the type sources is literally impossible there is no getter, incredible... + // TODO: Replace this with a getter to type sources :/ + let tmp_file = temp_dir().join(format!("{}_{}.bntl", dependency_name, tl.guid())); + if tl.write_to_file(&tmp_file) { + let schema = BntlSchema::from_path(&tmp_file); + for type_source in schema.type_sources { + merged_type_library + .add_type_source(type_source.name.into(), &type_source.source); + } + } + // TODO: Enumerate metadata and merge it, most importantly, we need to merge ordinals. + } + merged_type_libraries.push(merged_type_library); + } + + Self::new(merged_type_libraries) + } +} + +pub struct TypeLibProcessor { + state: Arc, + /// The Binary Ninja settings to use when analyzing the binaries. + analysis_settings: serde_json::Value, + /// The default name to use for the type library dependency name (e.g. "sqlite.dll"). + /// + /// When processing information that does not contain the dependency name, this will be used, + /// such as processing header files. We need to set a dependency name, otherwise the library + /// will not be able to be referenced by other libraries and/or the binary view. + /// + /// This dependency name will NOT be used when it can otherwise be inferred by the processing + /// data, if you wish to override the resulting dependency name, you can do so by calling + /// [`TypeLibrary::set_dependency_name`] on the libraries returned via [`ProcessedData::type_libraries`]. + default_dependency_name: String, + /// The default platform name to use when processing (e.g. "windows-x86_64"). + /// + /// When processing information that does not have an associated platform, this will be used, + /// such as processing header files or processing winmd files. When processing binary files, + /// the platform will be derived from the binary view default platform. + /// + /// For WINMD files you typically want to run the processor for each of the following platforms: + /// + /// - "windows-x86_64" + /// - "windows-x86" + /// - "windows-aarch64" + default_platform_name: String, + /// Set the include directories to use when processing header files. These will be passed to the + /// Clang type parser, which will use them to resolve header file includes. + include_directories: Vec, +} + +impl TypeLibProcessor { + pub fn new(default_dependency_name: &str, default_platform_name: &str) -> Self { + Self { + state: Arc::new(ProcessingState::default()), + analysis_settings: serde_json::json!({ + "analysis.linearSweep.autorun": false, + "analysis.mode": "full", + }), + default_dependency_name: default_dependency_name.to_owned(), + default_platform_name: default_platform_name.to_owned(), + include_directories: Vec::new(), + } + } + + /// Retrieve a thread-safe shared reference to the [`ProcessingState`]. + pub fn state(&self) -> Arc { + self.state.clone() + } + + pub fn with_include_directories(mut self, include_directories: Vec) -> Self { + self.include_directories = include_directories; + self + } + + /// Place a call to this in places to interrupt when canceled. + fn check_cancelled(&self) -> Result<(), ProcessingError> { + match self.state.is_cancelled() { + true => Err(ProcessingError::Cancelled), + false => Ok(()), + } + } + + pub fn process(&self, path: &Path) -> Result { + match path.extension() { + Some(ext) if ext == "bntl" => self.process_type_library(&path), + Some(ext) if ext == "h" || ext == "hpp" => self.process_source(path), + // NOTE: A typical processor will not go down this path where we only provide a single + // winmd file to be processed. You almost always want to process multiple winmd files, + // which can be done by passing a directory with the relevant winmd files. + Some(ext) if ext == "winmd" => self.process_winmd(&[path.to_owned()]), + _ if path.is_dir() => self.process_directory(path), + _ => self.process_file(path), + } + } + + pub fn process_directory(&self, path: &Path) -> Result { + // Collect all files in the directory + let files = WalkDir::new(path) + .into_iter() + .filter_map(|e| { + let path = e.ok()?.into_path(); + if path.is_file() { + Some(path) + } else { + None + } + }) + .collect::>(); + + // TODO: Parallel processing of files? + let unmerged_data: Result, _> = files + .iter() + .map(|file| { + self.check_cancelled()?; + self.process(file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping directory file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Directory file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&unmerged_data?)) + } + + pub fn process_project(&self, project: &Project) -> Result { + // Inform the state of the new unprocessed project files. + for project_file in &project.files() { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state.set_file_state(path, false); + } + } + + let data: Result, _> = project + .files() + .iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(&file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping project root file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Project root file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&data?)) + } + + pub fn process_project_folder( + &self, + project_folder: &ProjectFolder, + ) -> Result { + for project_file in &project_folder.files() { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state.set_file_state(path, false); + } + } + + let unmerged_data: Result, _> = project_folder + .files() + .iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(&file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping project directory file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Project folder file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&unmerged_data?)) + } + + pub fn process_project_file( + &self, + project_file: &ProjectFile, + ) -> Result { + let file_name = project_file.name(); + let extension = file_name.split('.').last(); + let path = project_file + .path_on_disk() + .ok_or_else(|| ProcessingError::NoPathToProjectFile(project_file.to_owned()))?; + match extension { + Some(ext) if ext == "bntl" => self.process_type_library(&path), + Some(ext) if ext == "h" || ext == "hpp" => self.process_source(&path), + // NOTE: A typical processor will not go down this path where we only provide a single + // winmd file to be processed. You almost always want to process multiple winmd files, + // which can be done by passing a directory with the relevant winmd files. + Some(ext) if ext == "winmd" => self.process_winmd(&[path]), + _ => self.process_file(&path), + } + } + + // TODO: Process mapping file + // TODO: A json file that maps type names to their type dlls + // TODO: Apples format + + pub fn process_file(&self, path: &Path) -> Result { + // If the file cannot be parsed, it should be skipped to avoid a load error. + if !is_parsable(path) { + return Err(ProcessingError::SkippedFile(path.to_owned())); + } + + let file = binaryninja::load_with_options_and_progress( + &path, + false, + self.analysis_settings.as_str(), + |_pos, _total| { + // TODO: Report progress + true + }, + ) + .ok_or_else(|| ProcessingError::BinaryViewLoad(path.to_owned()))?; + let data = self.process_view(path.to_owned(), &file); + file.file().close(); + data + } + + pub fn process_view( + &self, + path: PathBuf, + view: &BinaryView, + ) -> Result { + self.state.set_file_state(path.to_owned(), false); + let view_platform = view.default_platform().unwrap_or(self.default_platform()?); + let type_library = TypeLibrary::new(view_platform.arch(), &self.default_dependency_name); + type_library.add_platform(&view_platform); + + // TODO: This has to be extremely slow + let platform_types = view_platform + .types() + .iter() + .map(|t| t.name.clone()) + .collect::>(); + let mut type_name_to_library = HashMap::new(); + for tl in view.type_libraries().iter() { + let lib_name = tl.name().to_string(); + for t in tl.named_types().iter() { + type_name_to_library.insert(t.name.clone(), lib_name.clone()); + } + } + + let add_referenced_types = |type_library: &TypeLibrary, ty: &Type| { + crate::helper::visit_type_reference(ty, &mut |ntr| { + let referenced_name = ntr.name(); + if platform_types.contains(&referenced_name) { + // The type referenced comes from the platform, so we do not need to do anything. + } else if let Some(source) = type_name_to_library.get(&referenced_name) { + type_library.add_type_source(referenced_name, source); + } else { + // Type does not belong to another type library, so we add it to the current one. + type_library.add_named_type(referenced_name, ty); + } + }); + }; + + let mut ordinals: HashMap = HashMap::new(); + let functions = view.functions(); + tracing::info!("Adding {} functions", functions.len()); + for func in &functions { + if !func.is_exported() { + continue; + } + let Some(defined_symbol) = func.defined_symbol() else { + tracing::debug!( + "Function '{}' has no defined symbol, skipping...", + func.symbol() + ); + continue; + }; + let qualified_name = QualifiedName::from(defined_symbol.to_string()); + type_library.add_named_object(qualified_name, &func.function_type()); + add_referenced_types(&type_library, &func.function_type()); + + if let Some(ordinal) = defined_symbol.ordinal() { + ordinals.insert(ordinal.to_string(), defined_symbol.to_string()); + } + } + + if !ordinals.is_empty() { + tracing::warn!( + "Found {} ordinals in '{}', adding metadata...", + ordinals.len(), + view.file(), + ); + // TODO: The ordinal version is OSMAJOR_OSMINOR, pull from pe metadata (use object crate) + let key_md: Ref = String::from("ordinals_10_0").into(); + type_library.store_metadata("ordinals", &key_md); + let map_md: Ref = ordinals.into(); + type_library.store_metadata("ordinals_10_0", &map_md); + } + + let mut processed_data = self.process_external_libraries(&view)?; + processed_data.type_libraries.push(type_library); + if let Some(api_set_section) = view.section_by_name(".apiset") { + let processed_api_set = self.process_api_set(&view, &api_set_section)?; + processed_data = ProcessedData::merge(&[processed_data, processed_api_set]); + } + self.state.set_file_state(path.to_owned(), true); + Ok(processed_data) + } + + pub fn process_external_libraries( + &self, + view: &BinaryView, + ) -> Result { + let view_platform = view.default_platform().unwrap_or(self.default_platform()?); + let mut extern_type_libraries = HashMap::new(); + for extern_lib in &view.external_libraries() { + let extern_type_library = TypeLibrary::new(view_platform.arch(), &extern_lib.name()); + extern_type_library.add_platform(&view_platform); + extern_type_library.set_dependency_name(&extern_lib.name()); + extern_type_libraries.insert(extern_lib.name(), extern_type_library); + } + + // Pull import types and add them to respective type libraries. + for extern_loc in &view.external_locations() { + // The source symbol represents the symbol represented in the binary, while the target + // symbol represents the symbol that we intend to map the information to. + let src_sym = extern_loc.source_symbol(); + let Some(extern_lib) = extern_loc.library() else { + tracing::warn!( + "External location '{}' has no library, skipping...", + src_sym + ); + continue; + }; + let Some(extern_type_library) = extern_type_libraries.get_mut(&extern_lib.name()) + else { + tracing::warn!( + "External location '{}' is referencing a detached external library, skipping...", + src_sym + ); + continue; + }; + let Some(src_data_var) = view.data_variable_at_address(src_sym.address()) else { + tracing::debug!( + "External location '{}' has no data variable, skipping...", + src_sym + ); + continue; + }; + if src_data_var.auto_discovered { + // We do not want to record objects which are not modified by the user, otherwise + // we are recording the object each time we visit a binary view, possibly retrieving + // the old definition of the object. + tracing::debug!( + "External location '{}' is auto discovered, skipping...", + src_sym + ); + continue; + } + let target_sym_name = extern_loc + .target_symbol() + .unwrap_or_else(|| src_sym.raw_name()); + // TODO: Need to visit all types referenced and add it to the type library. + extern_type_library.add_named_object(target_sym_name.into(), &src_data_var.ty.contents); + } + + Ok(ProcessedData::new( + extern_type_libraries.values().cloned().collect(), + )) + } + + /// Process API sets on Windows binaries, so we can fill in the alternative names for type libraries + /// we are processing. + /// + /// Creates an empty type library for the host and adds the alternative names to it. This should then + /// be passed to the [`ProcessedData::merge`] set to be merged with the type library of the host name. + /// + /// For more information see: https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets + pub fn process_api_set( + &self, + view: &BinaryView, + section: &Section, + ) -> Result { + let section_bytes = view + .read_buffer(section.start(), section.len()) + .ok_or_else(|| ProcessingError::BinaryViewRead(section.start(), section.len()))?; + let api_set_map = ApiSetMap::try_from_apiset_section_bytes(§ion_bytes.get_data())?; + + let mut target_map: HashMap> = HashMap::new(); + for entry in api_set_map.namespace_entries()? { + let alternative_name = entry.name()?.to_string_lossy(); + for value_entry in entry.value_entries()? { + // TODO: In cases where alt -> kernel32.dll -> kernelbase.dll we currently associate + // TODO: with kernel32.dll as its assumed there is a wrapper function that calls into + // TODO: kernelbase.dll. This keeps us from having to validate against both, in the case + // TODO: of kernelbase.dll being before the function was moved there. + let _forwarder_name = value_entry.name()?.to_string_lossy(); + let target_name = value_entry.value()?.to_string_lossy(); + target_map + .entry(target_name) + .or_default() + .insert(alternative_name.clone()); + } + } + + // Instead of using the view, we use the user-provided platform, the reason is because the + // 'apisetschema.dll' is shared across multiple archs, and we need to be able to merge its data + // with other platforms so that they get the correct alternative names. + let platform = self.default_platform()?; + let mut mapping_type_libraries = Vec::new(); + for (target_name, alternative_names) in target_map { + let type_library = TypeLibrary::new(platform.arch(), &target_name); + for alt_name in alternative_names { + type_library.add_alternate_name(&alt_name); + } + mapping_type_libraries.push(type_library); + } + + Ok(ProcessedData::new(mapping_type_libraries)) + } + + /// We want to be able to process already created type libraries so that they can be consulted + /// during the [`ProcessedData::merge`] step. This lets us add overrides like extra platforms. + pub fn process_type_library(&self, path: &Path) -> Result { + self.state.set_file_state(path.to_owned(), false); + let finalized_type_library = TypeLibrary::load_from_file(&path) + .ok_or_else(|| ProcessingError::InvalidTypeLibrary(path.to_owned()))?; + self.state.set_file_state(path.to_owned(), true); + Ok(ProcessedData::new(vec![finalized_type_library])) + } + + pub fn process_source(&self, path: &Path) -> Result { + self.state.set_file_state(path.to_owned(), false); + let platform = self.default_platform()?; + let parser = + CoreTypeParser::parser_by_name("ClangTypeParser").expect("Failed to get clang parser"); + let platform_type_container = platform.type_container(); + + let header_contents = + std::fs::read_to_string(path).map_err(|e| ProcessingError::FileRead(e))?; + + let file_name = path + .file_name() + .unwrap_or(OsStr::new("source.hpp")) + .to_string_lossy(); + // TODO: Allow specifying options? + let mut include_dirs = self.include_directories.clone(); + if let Some(p) = path.parent() { + include_dirs.push(p.to_owned()); + } + let parsed_types = parser + .parse_types_from_source( + &header_contents, + &file_name, + &platform, + &platform_type_container, + &[], + &include_dirs, + "", + ) + .map_err(|e| ProcessingError::TypeParsingFailed(e))?; + + let type_library = TypeLibrary::new(platform.arch(), &self.default_dependency_name); + type_library.add_platform(&platform); + for ty in parsed_types.types { + type_library.add_named_type(ty.name, &ty.ty); + } + for func in parsed_types.functions { + type_library.add_named_object(func.name, &func.ty); + } + self.state.set_file_state(path.to_owned(), true); + Ok(ProcessedData::new(vec![type_library])) + } + + /// Unlike [`TypeLibProcessor::process_source`] which can pass include directories, this processing + /// requires us to actually load multiple files to parse the correct information. + /// + /// A specific example of this is the "Windows.Wdk.winmd" references types in "Windows.Win32.winmd". + /// If we did not process them together, we would have unresolved references when loading kernel + /// type libraries. + pub fn process_winmd(&self, paths: &[PathBuf]) -> Result { + for path in paths { + self.state.set_file_state(path.to_owned(), false); + } + let platform = self.default_platform()?; + let type_libraries = WindowsMetadataImporter::new() + .with_files(&paths) + .map_err(ProcessingError::WinMdFailedImport)? + .import(&platform) + .map_err(ProcessingError::WinMdFailedImport)?; + for path in paths { + self.state.set_file_state(path.to_owned(), true); + } + Ok(ProcessedData::new(type_libraries)) + } + + pub fn default_platform(&self) -> Result, ProcessingError> { + Platform::by_name(&self.default_platform_name) + .ok_or_else(|| ProcessingError::PlatformNotFound(self.default_platform_name.clone())) + } +} + +pub fn is_parsable(path: &Path) -> bool { + if binaryninja::is_database(path) { + return true; + } + let mut metadata = FileMetadata::with_file_path(&path); + let Ok(view) = BinaryView::from_path(&mut metadata, path) else { + return false; + }; + // If any view type parses this file, consider it for this source. + // All files will have a "Raw" file type, so we account for that. + BinaryViewType::list_valid_types_for(&view).len() > 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_parsable() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let x86_file_path = data_dir.join("x86").join("mfc42.dll.bndb"); + assert!(x86_file_path.exists()); + assert!(is_parsable(&x86_file_path)); + let header_file_path = data_dir.join("headers").join("test.h"); + assert!(header_file_path.exists()); + assert!(!is_parsable(&header_file_path)); + } + + #[test] + fn test_process_winmd() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let win32_winmd_path = data_dir.join("winmd").join("Windows.Win32.winmd"); + assert!(win32_winmd_path.exists()); + let wdk_winmd_path = data_dir.join("winmd").join("Windows.Wdk.winmd"); + assert!(wdk_winmd_path.exists()); + + let processor = TypeLibProcessor::new("foo", "windows-x86_64"); + let processed_data = processor + .process_winmd(&[win32_winmd_path, wdk_winmd_path]) + .expect("Failed to process winmd"); + assert_eq!(processed_data.type_libraries.len(), 591); + + // Make sure processing a directory will correctly group winmd files. + let processed_folder_data = processor + .process_directory(&data_dir.join("winmd")) + .expect("Failed to process directory"); + assert_eq!(processed_folder_data.type_libraries.len(), 591); + } + + #[test] + fn test_process_source() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let header_file_path = data_dir.join("headers").join("test.h"); + assert!(header_file_path.exists()); + + let processor = TypeLibProcessor::new("test.dll", "windows-x86_64"); + let processed_data = processor + .process_source(&header_file_path) + .expect("Failed to process source"); + assert_eq!(processed_data.type_libraries.len(), 1); + let processed_library = &processed_data.type_libraries[0]; + assert_eq!(processed_library.name(), "test.dll"); + assert_eq!(processed_library.dependency_name(), "test.dll"); + assert_eq!( + processed_library.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + + processed_library + .get_named_type("MyStruct".into()) + .expect("Failed to get type"); + + // Make sure includes are pulled into the type library. + let header2_file_path = data_dir.join("headers").join("test2.hpp"); + let processed_data_2 = processor + .process_source(&header2_file_path) + .expect("Failed to process source"); + assert_eq!(processed_data_2.type_libraries.len(), 1); + let processed_library_2 = &processed_data_2.type_libraries[0]; + assert_eq!(processed_library_2.name(), "test.dll"); + assert_eq!(processed_library_2.dependency_name(), "test.dll"); + assert_eq!( + processed_library_2.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + processed_library_2 + .get_named_type("MyStruct2".into()) + .expect("Failed to get type"); + processed_library_2 + .get_named_type("MyStruct".into()) + .expect("Failed to get included type"); + } + + #[test] + fn test_process_file() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let x86_file_path = data_dir.join("x86_64").join("mfc42.dll.bndb"); + assert!(x86_file_path.exists()); + let processor = TypeLibProcessor::new("mfc42.dll", "windows-x86_64"); + let processed_data = processor + .process_file(&x86_file_path) + .expect("Failed to process file"); + assert_eq!(processed_data.type_libraries.len(), 27); + let processed_library = processed_data + .type_libraries + .iter() + .find(|lib| lib.name() == "mfc42.dll") + .expect("Failed to find mfc42.dll library"); + assert_eq!(processed_library.name(), "mfc42.dll"); + assert_eq!(processed_library.dependency_name(), "mfc42.dll"); + assert_eq!( + processed_library.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + } + + #[test] + fn test_process_api_set() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let apiset_file_path = data_dir.join("apiset").join("apisetschema.dll"); + assert!(apiset_file_path.exists()); + let processor = TypeLibProcessor::new("foo", "windows-x86_64"); + let processed_data = processor + .process_file(&apiset_file_path) + .expect("Failed to process file"); + + assert_eq!(processed_data.type_libraries.len(), 287); + let combase_library = processed_data + .type_libraries + .iter() + .find(|tl| tl.name() == "combase.dll") + .expect("Failed to find combase.dll type library"); + assert_eq!( + combase_library.alternate_names().to_vec(), + vec![ + "api-ms-win-core-com-l1-1-3", + "api-ms-win-core-com-midlproxystub-l1-1-0", + "api-ms-win-core-com-private-l1-1-1", + "api-ms-win-core-com-private-l1-2-0", + "api-ms-win-core-com-private-l1-3-1", + "api-ms-win-core-marshal-l1-1-0", + "api-ms-win-core-winrt-error-l1-1-1", + "api-ms-win-core-winrt-errorprivate-l1-1-1", + "api-ms-win-core-winrt-l1-1-0", + "api-ms-win-core-winrt-registration-l1-1-0", + "api-ms-win-core-winrt-roparameterizediid-l1-1-0", + "api-ms-win-core-winrt-string-l1-1-1", + "api-ms-win-downlevel-ole32-l1-1-0" + ] + ); + } + + #[test] + fn test_data_merging() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let x86_platform = Platform::by_name("x86").expect("Failed to get x86 platform"); + let x86_windows_platform = + Platform::by_name("windows-x86").expect("Failed to get windows x86 platform"); + // Make two type libraries with the same name, but different dependencies. + let tl1 = TypeLibrary::new(x86_platform.arch(), "foo"); + tl1.set_dependency_name("foo"); + tl1.add_platform(&x86_platform); + tl1.add_named_type("bar".into(), &Type::named_float(3, "bla")); + let tl1_data = ProcessedData::new(vec![tl1]); + + let tl2 = TypeLibrary::new(x86_platform.arch(), "bar"); + tl2.set_dependency_name("foo"); + tl2.add_platform(&x86_windows_platform); + tl2.add_named_type("baz".into(), &Type::named_int(64, false, "fre")); + let tl2_data = ProcessedData::new(vec![tl2]); + + let merged_data = ProcessedData::merge(&[tl1_data, tl2_data]); + assert_eq!(merged_data.type_libraries.len(), 1); + let merged_tl = &merged_data.type_libraries[0]; + assert_eq!(merged_tl.name(), "foo"); + assert_eq!(merged_tl.dependency_name(), "foo"); + assert_eq!(merged_tl.platform_names().len(), 2); + assert_eq!(merged_tl.named_types().len(), 2); + } +} diff --git a/plugins/bntl_utils/src/schema.rs b/plugins/bntl_utils/src/schema.rs new file mode 100644 index 0000000000..ff6d408920 --- /dev/null +++ b/plugins/bntl_utils/src/schema.rs @@ -0,0 +1,58 @@ +use binaryninja::types::TypeLibrary; +use serde::Deserialize; +use std::collections::{HashMap, HashSet}; +use std::fs::File; + +#[derive(Deserialize, Debug)] +pub struct BntlSchema { + // The list of library names this library depends on + pub dependencies: Vec, + // Maps internal type IDs or names to their external sources + pub type_sources: Vec, +} + +impl BntlSchema { + pub fn from_file(file: &File) -> Self { + serde_json::from_reader(file).expect("JSON schema mismatch") + } + + pub fn from_path(path: &std::path::Path) -> Self { + match path.extension() { + Some(ext) if ext == "json" => { + Self::from_file(&File::open(path).expect("Failed to open schema file")) + } + Some(ext) if ext == "bntl" => { + // Need to decompress first + let out_path = path.with_extension("json"); + // TODO: Need a way to decompress without writing to disk? e.g. write uncompressed. + if !TypeLibrary::decompress_to_file(path, &out_path) { + panic!("Failed to decompress type library"); + } + Self::from_file( + &File::open(out_path).expect("Failed to open decompressed schema file"), + ) + } + _ => panic!("Invalid schema file extension"), + } + } + + pub fn to_source_map(&self) -> HashMap> { + let mut dependencies_map: HashMap> = HashMap::new(); + for ts in &self.type_sources { + let full_name = ts.name.join("::"); + dependencies_map + .entry(ts.source.clone()) + .or_default() + .insert(full_name); + } + dependencies_map + } +} + +#[derive(Deserialize, Debug)] +pub struct TypeSource { + // The components of the name, e.g., ["std", "string"] + pub name: Vec, + // The name of the dependency library it comes from + pub source: String, +} diff --git a/plugins/bntl_utils/src/templates/validate.html b/plugins/bntl_utils/src/templates/validate.html new file mode 100644 index 0000000000..e37af45baa --- /dev/null +++ b/plugins/bntl_utils/src/templates/validate.html @@ -0,0 +1,101 @@ + + + + {# palette() is reading from the QT style sheet FYI #} + + + + +
+

Type Library Validation Report

+
+ +{% for issue in issues %} +
+ + {% if issue.DuplicateGUID %} + Duplicate GUID + The GUID {{ issue.DuplicateGUID.guid }} is already used by {{ issue.DuplicateGUID.existing_library }}. + + {% elif issue.DuplicateDependencyName %} + Dependency Name Collision + The name {{ issue.DuplicateDependencyName.name }} is already provided by {{ issue.DuplicateDependencyName.existing_library }}. + + {% elif issue.InvalidMetadata %} + Invalid Metadata + Key: {{ issue.InvalidMetadata.key }} | Issue: {{ issue.InvalidMetadata.issue }} + + {% elif issue.DuplicateOrdinal %} + Duplicate Ordinal + Ordinal #{{ issue.DuplicateOrdinal.ordinal }} is assigned to {{ issue.DuplicateOrdinal.existing_name }} and {{ issue.DuplicateOrdinal.duplicate_name }}. + + {% elif issue.NoPlatform %} + Missing Platform + The type library has no target platform associated with it. + + {% elif issue.UnresolvedExternalReference %} + Unresolved External Reference + Type {{ issue.UnresolvedExternalReference.name }} (in {{ issue.UnresolvedExternalReference.container }}) has no source. + + {% elif issue.UnresolvedSourceReference %} + Unresolved Source Reference + Type {{ issue.UnresolvedSourceReference.name }} was not found in expected source {{ issue.UnresolvedSourceReference.source }}. + + {% elif issue.UnresolvedTypeLibrary %} + Unresolved Type Library + Could not find dependency library file for {{ issue.UnresolvedTypeLibrary.name }}. + {% endif %} + +
+{% endfor %} + + + \ No newline at end of file diff --git a/plugins/bntl_utils/src/url.rs b/plugins/bntl_utils/src/url.rs new file mode 100644 index 0000000000..16fe75d208 --- /dev/null +++ b/plugins/bntl_utils/src/url.rs @@ -0,0 +1,345 @@ +use binaryninja::collaboration::{RemoteFile, RemoteFolder, RemoteProject}; +use binaryninja::rc::Ref; +use std::fmt::Display; +use std::path::PathBuf; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +#[derive(Error, Debug, PartialEq)] +pub enum BnUrlParsingError { + #[error("Invalid URL format: {0}")] + UrlParseError(#[from] url::ParseError), + + #[error("Invalid scheme: expected 'binaryninja', found '{0}'")] + InvalidScheme(String), + + #[error("Invalid Enterprise path: missing server or project GUID")] + InvalidEnterprisePath, + + #[error("Invalid server URL in enterprise path")] + InvalidServerUrl, + + #[error("Invalid UUID: {0}")] + InvalidUuid(#[from] uuid::Error), + + #[error("Unknown or unsupported URL format")] + UnknownFormat, +} + +#[derive(Error, Debug)] +pub enum BnResourceError { + #[error("Enterprise server not found for address: {0}")] + RemoteNotFound(String), + + #[error("Remote connection error: {0}")] + RemoteConnectionError(String), + + #[error("Project not found with GUID: {0}")] + ProjectNotFound(String), + + #[error("Project resource not found with GUID: {0}")] + ItemNotFound(String), + + #[error("Local filesystem error: {0}")] + IoError(#[from] std::io::Error), +} + +#[derive(Debug, Clone)] +pub enum BnResource { + RemoteProject(Ref), + RemoteProjectFile(Ref), + RemoteProjectFolder(Ref), + /// A remote file. + RemoteFile(Url), + /// A regular file on the local filesystem. + LocalFile(PathBuf), +} + +// TODO: Make the BnUrl from this. +impl Display for BnResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BnResource::RemoteProject(project) => write!(f, "RemoteProject({})", project.id()), + BnResource::RemoteProjectFile(file) => write!(f, "RemoteFile({})", file.id()), + BnResource::RemoteProjectFolder(folder) => write!(f, "RemoteFolder({})", folder.id()), + BnResource::RemoteFile(url) => write!(f, "RemoteFile({})", url), + BnResource::LocalFile(path) => write!(f, "LocalFile({})", path.display()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum BnParsedUrlKind { + Enterprise { + server: Url, + project_guid: Uuid, + /// Optional GUID of the project item, currently can be a folder or a file. + item_guid: Option, + }, + // TODO: Local projects? + RemoteFile(Url), + LocalFile(PathBuf), +} + +#[derive(Debug, Clone)] +pub struct BnParsedUrl { + pub kind: BnParsedUrlKind, + pub expression: Option, +} + +impl BnParsedUrl { + pub fn parse(input: &str) -> Result { + let parsed = Url::parse(input)?; + if parsed.scheme() != "binaryninja" { + return Err(BnUrlParsingError::InvalidScheme( + parsed.scheme().to_string(), + )); + } + + let expression = parsed + .query_pairs() + .find(|(k, _)| k == "expr") + .map(|(_, v)| v.into_owned()); + + let kind = match parsed.host_str() { + // TODO: This should really go down the same path as the remote file parsing, if it + // TODO: matches the host of an enterprise server... But that requires us to change how + // TODO: the core outputs these enterprise URLs... + // Case: binaryninja://enterprise/... + Some("enterprise") => { + let segments: Vec<&str> = + parsed.path().split('/').filter(|s| !s.is_empty()).collect(); + + if segments.len() < 3 { + return Err(BnUrlParsingError::InvalidEnterprisePath); + } + + let (server_parts, resource_parts) = if segments.len() >= 4 { + ( + &segments[..segments.len() - 2], + &segments[segments.len() - 2..], + ) + } else { + ( + &segments[..segments.len() - 1], + &segments[segments.len() - 1..], + ) + }; + + BnParsedUrlKind::Enterprise { + server: Url::parse(&server_parts.join("/")) + .map_err(|_| BnUrlParsingError::InvalidServerUrl)?, + project_guid: Uuid::parse_str(resource_parts[0])?, + item_guid: resource_parts + .get(1) + .map(|s| Uuid::parse_str(s)) + .transpose()?, + } + } + // Case: binaryninja:///bin/ls + None | Some("") + if parsed.path().starts_with('/') && !parsed.path().starts_with("/https") => + { + BnParsedUrlKind::LocalFile(PathBuf::from(parsed.path())) + } + // Case: binaryninja:https://... + _ => { + let path = parsed.path(); + if path.starts_with("https:/") || path.starts_with("http:/") { + let nested_url = path.replacen(":/", "://", 1); + BnParsedUrlKind::RemoteFile( + Url::parse(&nested_url).map_err(BnUrlParsingError::UrlParseError)?, + ) + } else { + return Err(BnUrlParsingError::UnknownFormat); + } + } + }; + + Ok(BnParsedUrl { kind, expression }) + } + + pub fn to_resource(&self) -> Result { + match &self.kind { + BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid, + } => { + // NOTE: We must strip the trailing slash from the server URL, because the core will + // not accept it otherwise, we should probably have a fuzzy get_remote_by_address here, + // so we can accept either with or without the trailing slash, but for now we'll just + // strip it. + let server_addr = server.as_str().strip_suffix('/').unwrap_or(server.as_str()); + let remote = binaryninja::collaboration::get_remote_by_address(server_addr) + .ok_or_else(|| BnResourceError::RemoteNotFound(server_addr.to_string()))?; + if !remote.is_connected() { + remote.connect().map_err(|_| { + BnResourceError::RemoteConnectionError(server_addr.to_string()) + })?; + } + + let project = remote + .get_project_by_id(&project_guid.to_string()) + .ok() + .flatten() + .ok_or_else(|| BnResourceError::ProjectNotFound(project_guid.to_string()))?; + + match item_guid { + Some(item_guid) => { + let item_guid_str = item_guid.to_string(); + + // Check if it's a folder first + if let Some(folder) = + project.get_folder_by_id(&item_guid_str).ok().flatten() + { + return Ok(BnResource::RemoteProjectFolder(folder)); + } + + // Then check if it's a file + let file = project + .get_file_by_id(&item_guid_str) + .ok() + .flatten() + .ok_or_else(|| BnResourceError::ItemNotFound(item_guid_str))?; + + Ok(BnResource::RemoteProjectFile(file)) + } + None => Ok(BnResource::RemoteProject(project)), + } + } + BnParsedUrlKind::RemoteFile(remote_url) => { + Ok(BnResource::RemoteFile(remote_url.clone())) + } + BnParsedUrlKind::LocalFile(local_path) => Ok(BnResource::LocalFile(local_path.clone())), + } + } +} + +impl Display for BnParsedUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.kind { + BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid, + } => write!( + f, + "binaryninja://enterprise/{}/{}{}", + server.as_str().strip_suffix('/').unwrap_or(server.as_str()), + project_guid, + item_guid + .map(|guid| format!("/{}", guid)) + .unwrap_or_default() + ), + BnParsedUrlKind::RemoteFile(remote_url) => write!(f, "{}", remote_url), + BnParsedUrlKind::LocalFile(local_path) => { + write!(f, "binaryninja:///{}", local_path.display()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_enterprise_full() { + let input = "binaryninja://enterprise/https://enterprise.test.com/0268b954-0d7b-41c3-a603-960a59fdd0f7/0268b954-0d7b-41c3-a603-960a59fdd0f6?expr=sub_1234"; + let action = BnParsedUrl::parse(input).unwrap(); + + if let BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid: project_file, + } = action.kind + { + assert_eq!(server.as_str(), "https://enterprise.test.com/"); + assert_eq!( + project_guid, + Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f7").unwrap() + ); + assert_eq!( + project_file, + Some(Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f6").unwrap()) + ); + } else { + panic!("Wrong target type"); + } + assert_eq!(action.expression, Some("sub_1234".to_string())); + } + + #[test] + fn test_parse_enterprise_no_file() { + let input = "binaryninja://enterprise/https://enterprise.test.com/0268b954-0d7b-41c3-a603-960a59fdd0f7/"; + let action = BnParsedUrl::parse(input).unwrap(); + + if let BnParsedUrlKind::Enterprise { + project_guid, + item_guid: project_file, + .. + } = action.kind + { + assert_eq!( + project_guid, + Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f7").unwrap() + ); + assert_eq!(project_file, None); + } else { + panic!("Wrong target type"); + } + } + + #[test] + fn test_parse_remote_file() { + let input = "binaryninja:https://captf.com/2015/plaidctf/pwnable/datastore.elf?expr=main"; + let action = BnParsedUrl::parse(input).unwrap(); + + match action.kind { + BnParsedUrlKind::RemoteFile(url) => { + assert_eq!(url.host_str(), Some("captf.com")); + assert!(url.path().ends_with("datastore.elf")); + } + _ => panic!("Expected RemoteFile"), + } + assert_eq!(action.expression, Some("main".to_string())); + } + + #[test] + fn test_parse_local_file() { + let input = "binaryninja:///bin/ls?expr=sub_2830"; + let action = BnParsedUrl::parse(input).unwrap(); + + match action.kind { + BnParsedUrlKind::LocalFile(path) => assert_eq!(path.to_string_lossy(), "/bin/ls"), + _ => panic!("Expected LocalFile"), + } + assert_eq!(action.expression, Some("sub_2830".to_string())); + } + + #[test] + fn test_invalid_scheme() { + let input = "https://google.com"; + let result = BnParsedUrl::parse(input); + assert!(matches!(result, Err(BnUrlParsingError::InvalidScheme(_)))); + } + + #[test] + fn test_missing_enterprise_guid() { + let input = "binaryninja://enterprise/https://internal.us/"; + let result = BnParsedUrl::parse(input); + assert_eq!( + result.unwrap_err(), + BnUrlParsingError::InvalidEnterprisePath + ); + } + + #[test] + fn test_invalid_uuid_format() { + let input = "binaryninja://enterprise/https://internal.us/not-a-uuid/"; + let result = BnParsedUrl::parse(input); + assert!(matches!(result, Err(BnUrlParsingError::InvalidUuid(_)))); + } +} diff --git a/plugins/bntl_utils/src/validate.rs b/plugins/bntl_utils/src/validate.rs new file mode 100644 index 0000000000..3c863c6410 --- /dev/null +++ b/plugins/bntl_utils/src/validate.rs @@ -0,0 +1,341 @@ +use crate::schema::BntlSchema; +use binaryninja::platform::Platform; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::types::TypeLibrary; +use minijinja::{context, Environment}; +use serde::Serialize; +use std::collections::{HashMap, HashSet}; +use std::env::temp_dir; +use std::fmt::Display; + +#[derive(Debug, PartialEq, PartialOrd, Clone, Eq, Hash, Serialize)] +pub enum ValidateIssue { + DuplicateGUID { + guid: String, + existing_library: String, + }, + DuplicateDependencyName { + name: String, + existing_library: String, + }, + InvalidMetadata { + key: String, + issue: String, + }, + DuplicateOrdinal { + ordinal: u64, + existing_name: String, + duplicate_name: String, + }, + NoPlatform, + UnresolvedExternalReference { + name: String, + container: String, + }, + UnresolvedSourceReference { + name: String, + source: String, + }, + UnresolvedTypeLibrary { + name: String, + }, // TODO: Overlapping type name of platform? + // TODO: E.g. a type is found in the type library, and also in the platform. +} + +impl Display for ValidateIssue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidateIssue::DuplicateGUID { + guid, + existing_library, + } => { + write!( + f, + "Duplicate GUID: '{}' is already used by library '{}'", + guid, existing_library + ) + } + ValidateIssue::DuplicateDependencyName { + name, + existing_library, + } => { + write!( + f, + "Duplicate Dependency Name: '{}' is already provided by '{}'", + name, existing_library + ) + } + ValidateIssue::InvalidMetadata { key, issue } => { + write!(f, "Invalid Metadata: Key '{}' - {}", key, issue) + } + ValidateIssue::DuplicateOrdinal { + ordinal, + existing_name, + duplicate_name, + } => { + write!( + f, + "Duplicate Ordinal: #{} is assigned to both '{}' and '{}'", + ordinal, existing_name, duplicate_name + ) + } + ValidateIssue::NoPlatform => { + write!( + f, + "Missing Platform: The type library has no target platform associated with it" + ) + } + ValidateIssue::UnresolvedExternalReference { name, container } => { + write!( + f, + "Unresolved External Reference: Type '{}' referenced inside '{}' is marked as external but has no source", + name, container + ) + } + ValidateIssue::UnresolvedSourceReference { name, source } => { + write!( + f, + "Unresolved Source Reference: Type '{}' expects source '{}', but it wasn't found there", + name, source + ) + } + ValidateIssue::UnresolvedTypeLibrary { name } => { + write!( + f, + "Unresolved Type Library: Could not find dependency library file for '{}'", + name + ) + } + } + } +} + +#[derive(Debug, Default)] +pub struct ValidateResult { + pub issues: Vec, +} + +impl ValidateResult { + /// Render the validation report as HTML. + pub fn render_report(&self) -> Result { + let mut environment = Environment::new(); + // Remove trailing lines for blocks, this is required for Markdown tables. + environment.set_trim_blocks(true); + minijinja_embed::load_templates!(&mut environment); + let tmpl = environment.get_template("validate.html")?; + tmpl.render(context!(issues => self.issues)) + } +} + +#[derive(Debug, Default, Clone)] +pub struct TypeLibValidater { + pub seen_guids: HashMap, + // TODO: This needs to be by platform as well. + pub seen_dependency_names: HashMap, + /// These are the type libraries that are accessible to the type library under validation. + /// + /// Used to validate external references. + pub type_libraries: Vec>, + /// Built from the available type libraries. + pub valid_external_references: HashSet, +} + +impl TypeLibValidater { + pub fn new() -> Self { + Self { + seen_guids: HashMap::new(), + seen_dependency_names: HashMap::new(), + type_libraries: Vec::new(), + valid_external_references: HashSet::new(), + } + } + + /// These are the type libraries that are accessible to the type library under validation. + /// + /// Used to validate external references. + pub fn with_type_libraries(mut self, type_libraries: Vec>) -> Self { + self.type_libraries = type_libraries; + for type_lib in &self.type_libraries { + for ty in &type_lib.named_types() { + self.valid_external_references.insert(ty.name); + } + for obj in &type_lib.named_objects() { + self.valid_external_references.insert(obj.name); + } + } + self + } + + /// The platform that is accessible to the type library under validation. + /// + /// Used to validate external references. + pub fn with_platform(mut self, platform: &Platform) -> Self { + for ty in &platform.types() { + self.valid_external_references.insert(ty.name); + } + self + } + + pub fn validate(&mut self, type_lib: &TypeLibrary) -> ValidateResult { + let mut result = ValidateResult::default(); + + if type_lib.platform_names().is_empty() { + result.issues.push(ValidateIssue::NoPlatform); + } + + if let Some(issue) = self.validate_guid(type_lib) { + result.issues.push(issue); + } + + if let Some(issue) = self.validate_dependency_name(type_lib) { + result.issues.push(issue); + } + + result.issues.extend(self.validate_ordinals(type_lib)); + result + .issues + .extend(self.validate_external_references(type_lib)); + + // TODO: This is currently disabled because it's too slow. + // result.issues.extend(self.validate_source_files(type_lib)); + + result + } + + pub fn validate_guid(&mut self, type_lib: &TypeLibrary) -> Option { + match self.seen_guids.insert(type_lib.guid(), type_lib.name()) { + None => None, + Some(existing_library) => Some(ValidateIssue::DuplicateGUID { + guid: type_lib.guid(), + existing_library, + }), + } + } + + pub fn validate_dependency_name(&mut self, type_lib: &TypeLibrary) -> Option { + match self + .seen_dependency_names + .insert(type_lib.dependency_name(), type_lib.name()) + { + None => None, + Some(existing_library) => Some(ValidateIssue::DuplicateDependencyName { + name: type_lib.dependency_name(), + existing_library, + }), + } + } + + pub fn validate_source_files(&self, type_lib: &TypeLibrary) -> Vec { + let mut issues = Vec::new(); + let tmp_type_lib_path = temp_dir().join(type_lib.name()); + type_lib.write_to_file(&tmp_type_lib_path); + let schema = BntlSchema::from_path(&tmp_type_lib_path); + for (src, types) in schema.to_source_map() { + let Some(dep_type_lib) = self.type_libraries.iter().find(|tl| tl.name() == src) else { + issues.push(ValidateIssue::UnresolvedTypeLibrary { + name: src.to_string(), + }); + continue; + }; + + for ty in &types { + let qualified_name = QualifiedName::from(ty); + let is_named_ty = dep_type_lib + .get_named_type(qualified_name.clone()) + .is_none(); + let is_named_obj = dep_type_lib.get_named_object(qualified_name).is_none(); + if !is_named_ty && !is_named_obj { + issues.push(ValidateIssue::UnresolvedSourceReference { + name: ty.to_string(), + source: src.to_string(), + }); + } + } + } + issues + } + + pub fn validate_external_references(&self, type_lib: &TypeLibrary) -> Vec { + let mut issues = Vec::new(); + for ty in &type_lib.named_types() { + crate::helper::visit_type_reference(&ty.ty, &mut |ntr| { + if !self.valid_external_references.contains(&ntr.name()) { + issues.push(ValidateIssue::UnresolvedExternalReference { + name: ntr.name().to_string(), + container: ty.name.to_string(), + }); + } + }) + } + for obj in &type_lib.named_objects() { + crate::helper::visit_type_reference(&obj.ty, &mut |ntr| { + if !self.valid_external_references.contains(&ntr.name()) { + issues.push(ValidateIssue::UnresolvedExternalReference { + name: ntr.name().to_string(), + container: obj.name.to_string(), + }); + } + }) + } + issues + } + + pub fn validate_ordinals(&self, type_lib: &TypeLibrary) -> Vec { + let Some(metadata_key_md) = type_lib.query_metadata("metadata") else { + return vec![]; + }; + let Some(metadata_key_str) = metadata_key_md.get_string() else { + return vec![ValidateIssue::InvalidMetadata { + key: "metadata".to_owned(), + issue: "Expected string".to_owned(), + }]; + }; + + let Some(metadata_map_md) = type_lib.query_metadata(&metadata_key_str.to_string_lossy()) + else { + return vec![ValidateIssue::InvalidMetadata { + key: metadata_key_str.to_string_lossy().to_string(), + issue: "Missing metadata map key".to_owned(), + }]; + }; + + let Some(metadata_map) = metadata_map_md.get_value_store() else { + return vec![ValidateIssue::InvalidMetadata { + key: metadata_key_str.to_string_lossy().to_string(), + issue: "Expected value store".to_owned(), + }]; + }; + + let mut discovered_ordinals = HashMap::new(); + let mut issues = Vec::new(); + for (key, value) in metadata_map.iter() { + let Ok(ordinal_num) = key.parse::() else { + issues.push(ValidateIssue::InvalidMetadata { + key: key.to_string(), + issue: "Expected ordinal number".to_owned(), + }); + continue; + }; + + let Some(value_bn_str) = value.get_string() else { + issues.push(ValidateIssue::InvalidMetadata { + key: key.to_string(), + issue: "Expected string".to_owned(), + }); + continue; + }; + let value_str = value_bn_str.to_string_lossy().to_string(); + + match discovered_ordinals.insert(ordinal_num, value_str.clone()) { + None => (), + Some(existing_ordinal) => issues.push(ValidateIssue::DuplicateOrdinal { + ordinal: ordinal_num, + existing_name: existing_ordinal, + duplicate_name: value_str, + }), + } + } + issues + } +} diff --git a/plugins/bntl_utils/src/winmd.rs b/plugins/bntl_utils/src/winmd.rs new file mode 100644 index 0000000000..7622fe891e --- /dev/null +++ b/plugins/bntl_utils/src/winmd.rs @@ -0,0 +1,594 @@ +//! Import windows metadata types into a Binary Ninja type library. + +use std::collections::HashMap; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use thiserror::Error; + +use binaryninja::architecture::Architecture; +use binaryninja::platform::Platform; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::types::{ + EnumerationBuilder, FunctionParameter, MemberAccess, MemberScope, NamedTypeReference, + NamedTypeReferenceClass, StructureBuilder, StructureType, Type, TypeBuilder, TypeLibrary, +}; + +use info::{LibraryName, MetadataFunctionInfo, MetadataInfo, MetadataTypeInfo, MetadataTypeKind}; + +pub mod info; +pub mod translate; + +#[derive(Error, Debug)] +pub enum ImportError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("failed to translate windows metadata")] + TransactionError(#[from] translate::TranslationError), + #[error("the type '{0}' has an unhandled size")] + UnhandledTypeSize(&'static str), +} + +#[derive(Debug)] +pub struct WindowsMetadataImporter { + info: MetadataInfo, + // TODO: If we can replace / add this with type libraries we can make multi-pass importer. + type_lookup: HashMap<(String, String), MetadataTypeInfo>, + address_size: usize, + integer_size: usize, +} + +impl WindowsMetadataImporter { + pub fn new() -> Self { + Self { + info: MetadataInfo::default(), + type_lookup: HashMap::new(), + address_size: 8, + integer_size: 8, + } + } + + #[allow(dead_code)] + pub fn new_with_info(info: MetadataInfo) -> Self { + let mut res = Self::new(); + res.info = info; + res.build_type_lookup(); + res + } + + pub fn with_files(mut self, paths: &[PathBuf]) -> Result { + let mut files = Vec::new(); + for path in paths { + let file = windows_metadata::reader::File::read(path).expect("Failed to read file"); + files.push(file); + } + self.info = translate::WindowsMetadataTranslator::new().translate(files)?; + // We updated info, so we must rebuild the lookup table. + self.build_type_lookup(); + Ok(self) + } + + pub fn with_platform(mut self, platform: &Platform) -> Self { + // TODO: platform.address_size() + self.address_size = platform.arch().address_size(); + self.integer_size = platform.arch().default_integer_size(); + self + } + + /// Build the lookup table for us to use when referencing types. + /// + /// Should be called anytime we update `self.info`. + fn build_type_lookup(&mut self) { + for ty in &self.info.types { + if let Some(_existing) = self + .type_lookup + .insert((ty.namespace.clone(), ty.name.clone()), ty.clone()) + { + tracing::warn!( + "Duplicate type name '{}' found when building type lookup", + ty.name + ); + } + } + } + + pub fn import(&self, platform: &Platform) -> Result>, ImportError> { + // TODO: We need to take all of these enums and figure out where to put them. + // let mut test = self.info.clone(); + // let constant_enums = test.create_constant_enums(); + // // TODO: Creating zero width enums + // // test.types.extend(constant_enums); + // // let blah: Vec<_> = constant_enums.iter().take(10).collect(); + // // for enum_kind in blah { + // // println!("{:?}", enum_kind); + // // } + let partitioned_info = self.info.partitioned(); + + let mut type_libs = Vec::new(); + for (name, info) in partitioned_info.libraries { + let type_lib_name = match name { + LibraryName::Module(module_name) => module_name.clone(), + LibraryName::Namespace(ns_name) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + ns_name.clone() + } + }; + let til = TypeLibrary::new(platform.arch(), &type_lib_name); + til.add_platform(platform); + til.set_dependency_name(&type_lib_name); + for ty in &info.metadata.types { + self.import_type(&til, &ty)?; + } + for func in &info.metadata.functions { + self.import_function(&til, &func)?; + } + for (name, library_name) in &info.external_references { + let qualified_name = QualifiedName::from(name.clone()); + match library_name { + LibraryName::Namespace(source) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + til.add_type_source(qualified_name, source); + } + LibraryName::Module(source) => { + til.add_type_source(qualified_name, source); + } + } + } + + type_libs.push(til); + } + + Ok(type_libs) + } + + pub fn import_function( + &self, + til: &TypeLibrary, + func: &MetadataFunctionInfo, + ) -> Result<(), ImportError> { + // TODO: Handle ordinals? Ordinals exist in binaries that need to be parsed, maybe we + // TODO: make another handler for that + let qualified_name = QualifiedName::from(func.name.clone()); + let ty = self.convert_type_kind(&func.ty)?; + til.add_named_object(qualified_name, &ty); + Ok(()) + } + + pub fn import_type( + &self, + til: &TypeLibrary, + type_info: &MetadataTypeInfo, + ) -> Result<(), ImportError> { + let qualified_name = QualifiedName::from(type_info.name.clone()); + let ty = self.convert_type_kind(&type_info.kind)?; + til.add_named_type(qualified_name, &ty); + Ok(()) + } + + pub fn convert_type_kind(&self, kind: &MetadataTypeKind) -> Result, ImportError> { + match kind { + MetadataTypeKind::Void => Ok(Type::void()), + MetadataTypeKind::Bool { size: None } => Ok(Type::bool()), + MetadataTypeKind::Bool { size: Some(size) } => { + Ok(TypeBuilder::bool().set_width(*size).finalize()) + } + MetadataTypeKind::Integer { size, is_signed } => { + Ok(Type::int(size.unwrap_or(self.integer_size), *is_signed)) + } + MetadataTypeKind::Character { size: 1 } => Ok(Type::int(1, true)), + MetadataTypeKind::Character { size } => Ok(Type::wide_char(*size)), + MetadataTypeKind::Float { size } => Ok(Type::float(*size)), + MetadataTypeKind::Pointer { + is_const, + is_pointee_const: _is_pointee_const, + target, + } => { + let target_ty = self.convert_type_kind(target)?; + Ok(Type::pointer_of_width( + &target_ty, + self.address_size, + *is_const, + false, + None, + )) + } + MetadataTypeKind::Array { element, count } => { + let element_ty = self.convert_type_kind(element)?; + Ok(Type::array(&element_ty, *count as u64)) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut structure = StructureBuilder::new(); + // Current offset in bytes + let mut current_byte_offset = 0usize; + + // TODO: Change how this operates now that we have an is_packed flag. + // Used to add tail padding to satisfy alignment requirements. + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + if let Some((bit_pos, bit_width)) = field.bitfield { + let current_bit_offset = current_byte_offset * 8; + let field_bit_offset = current_bit_offset + bit_pos as usize; + // TODO: member access and member scope have definitions inside winmd we can use. + structure.insert_bitwise( + &field_ty, + &field.name, + field_bit_offset as u64, + Some(bit_width), + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + + if let Some(next_field) = field_iter.peek() { + if next_field.bitfield.is_some() { + // Continue as if we are in the same storage unit (no alignment) + current_byte_offset = (current_bit_offset + bit_width as usize) / 8; + } else { + // Find the start of the storage unit. + // if we are at byte 1 of u32 (align 4), storage starts at 0. + // if we are at byte 5 of u32 (align 4), storage starts at 4. + let storage_start = + (current_byte_offset / field_alignment) * field_alignment; + // Jump to the end of that storage unit. + current_byte_offset = storage_start + field_size; + } + } + } else { + // Align the field placement based on the current field alignment. + let aligned_current_offset = + align_up(current_byte_offset as u64, field_alignment as u64); + structure.insert( + &field_ty, + &field.name, + aligned_current_offset, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + current_byte_offset = aligned_current_offset as usize + field_size; + } + } + structure.alignment(max_alignment); + + // TODO: Only add tail padding if we are not packed? I think we still need to do more. + if *is_packed { + structure.packed(true); + } else { + let total_size = align_up(current_byte_offset as u64, max_alignment as u64); + structure.width(total_size); + } + + Ok(Type::structure(&structure.finalize())) + } + MetadataTypeKind::Enum { ty, variants } => { + let enum_ty = self.convert_type_kind(ty)?; + let mut builder = EnumerationBuilder::new(); + for (name, value) in variants { + builder.insert(name, *value); + } + Ok(Type::enumeration( + &builder.finalize(), + NonZeroUsize::new(enum_ty.width() as usize) + .ok_or_else(|| ImportError::UnhandledTypeSize("Enum with zero width"))?, + enum_ty.is_signed().contents, + )) + } + MetadataTypeKind::Function { + params, + return_type, + is_vararg, + } => { + let return_ty = self.convert_type_kind(return_type)?; + let mut bn_params = Vec::new(); + for param in params { + let param_ty = self.convert_type_kind(¶m.ty)?; + bn_params.push(FunctionParameter::new(param_ty, param.name.clone(), None)); + } + Ok(Type::function(&return_ty, bn_params, *is_vararg)) + } + MetadataTypeKind::Reference { name, namespace } => { + // We are required to set the ID here since type libraries seem to only look up through + // the ID, and never fall back to name lookup. This is strange considering you must also + // set the types source to the given library, which seems counterintuitive. + // TODO: Add kind to ntr. + let ntr = NamedTypeReference::new_with_id( + NamedTypeReferenceClass::TypedefNamedTypeClass, + &format!("{}::{}", namespace, name), + name, + ); + // TODO: Type alignment? + let type_size = self.type_kind_size(kind)?; + Ok(TypeBuilder::named_type(&ntr) + .set_width(type_size) + .set_alignment(type_size) + .finalize()) + } + MetadataTypeKind::Union { fields } => { + let mut union = StructureBuilder::new(); + union.structure_type(StructureType::UnionStructureType); + + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + union.insert( + &field_ty, + &field.name, + 0, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + } + + union.alignment(max_alignment); + Ok(Type::structure(&union.finalize())) + } + } + } + + /// Retrieve the size of a type kind in bytes, references to types will be looked up + /// such that we can determine the size of structures with references as fields. + pub fn type_kind_size(&self, kind: &MetadataTypeKind) -> Result { + match kind { + MetadataTypeKind::Void => Ok(0), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Integer { size, .. } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Float { size } => Ok(*size), + MetadataTypeKind::Pointer { .. } => Ok(self.address_size), + MetadataTypeKind::Array { element, count } => { + let elem_size = self.type_kind_size(element)?; + Ok(elem_size * *count) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut current_offset = 0usize; + let mut max_struct_alignment = 1usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = if *is_packed { + 1 + } else { + self.type_kind_alignment(&field.ty)? + }; + max_struct_alignment = max_struct_alignment.max(field_alignment); + current_offset = + align_up(current_offset as u64, field_alignment as u64) as usize; + current_offset += field_size; + } + // Tail padding is only needed if not packed. + let final_alignment = if *is_packed { 1 } else { max_struct_alignment }; + let total_size = align_up(current_offset as u64, final_alignment as u64) as usize; + Ok(total_size) + } + MetadataTypeKind::Union { fields } => { + let mut largest_field_size = 0usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + largest_field_size = largest_field_size.max(field_size); + } + Ok(largest_field_size) + } + MetadataTypeKind::Enum { ty, .. } => self.type_kind_size(ty), + MetadataTypeKind::Function { .. } => Err(ImportError::UnhandledTypeSize( + "Function types are not sized", + )), + MetadataTypeKind::Reference { name, namespace } => { + // Look up the type and return its size. + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // This should really only happen if we did not specify all the required winmd files. + tracing::error!( + "Failed to find type '{}' when looking up type size for reference", + name + ); + return Ok(1); + }; + self.type_kind_size(&ty_info.kind) + } + } + } + + pub fn type_kind_alignment(&self, kind: &MetadataTypeKind) -> Result { + match kind { + MetadataTypeKind::Bool { size: None } => Ok(1), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + // TODO: Clean this stuff up. + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Integer { size: Some(1), .. } => Ok(1), + MetadataTypeKind::Integer { size: Some(2), .. } => Ok(2), + MetadataTypeKind::Integer { size: Some(4), .. } => Ok(4), + MetadataTypeKind::Integer { size: Some(8), .. } + | MetadataTypeKind::Float { size: 8 } + | MetadataTypeKind::Pointer { .. } => Ok(self.address_size), // 8 on x64 + MetadataTypeKind::Array { element, .. } => self.type_kind_alignment(element), + MetadataTypeKind::Struct { fields, is_packed } => { + if *is_packed { + return Ok(1); + } + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Union { fields } => { + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Reference { name, namespace } => { + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // TODO: Failed to find it in local type lookup, try type libraries? + tracing::error!( + "Failed to find type '{}' when looking up type alignment for reference", + name + ); + return Ok(4); + }; + self.type_kind_alignment(&ty_info.kind) + } + _ => Ok(4), + } + } +} + +// Aligns an offset up to the nearest multiple of `align`. +fn align_up(offset: u64, align: u64) -> u64 { + if align == 0 { + return offset; + } + let mask = align - 1; + (offset + mask) & !mask +} + +#[cfg(test)] +mod tests { + use super::info::{ + MetadataFieldInfo, MetadataImportInfo, MetadataImportMethod, MetadataModuleInfo, + }; + use super::*; + use binaryninja::architecture::CoreArchitecture; + use binaryninja::types::TypeClass; + + #[test] + fn test_import_type() { + // We must initialize binary ninja to access architectures. + let _session = binaryninja::headless::Session::new().expect("Failed to create session"); + + let mut info = MetadataInfo::default(); + info.functions = vec![MetadataFunctionInfo { + name: "MyFunction".to_string(), + ty: MetadataTypeKind::Function { + params: vec![], + return_type: Box::new(MetadataTypeKind::Void), + is_vararg: false, + }, + namespace: "Win32.Test".to_string(), + import_info: Some(MetadataImportInfo { + method: MetadataImportMethod::ByName("MyFunction".to_string()), + module: MetadataModuleInfo { + name: "TestModule.dll".to_string(), + }, + }), + }]; + info.types = vec![ + MetadataTypeInfo { + name: "Bar".to_string(), + kind: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + namespace: "Win32.Test".to_string(), + }, + MetadataTypeInfo { + name: "TestType".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "field1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + // TODO: Add more fields to verify bitfields, and const fields. + MetadataFieldInfo { + name: "field2_0".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((0, 1)), + }, + MetadataFieldInfo { + name: "field2_1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((1, 1)), + }, + MetadataFieldInfo { + name: "field3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }, + is_const: true, + bitfield: None, + }, + MetadataFieldInfo { + name: "field4".to_string(), + ty: MetadataTypeKind::Pointer { + is_pointee_const: false, + is_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: "Win32.Test".to_string(), + name: "Bar".to_string(), + }), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Foo".to_string(), + }, + ]; + let importer = WindowsMetadataImporter::new_with_info(info); + let x86 = CoreArchitecture::by_name("x86").expect("No x86 architecture"); + let platform = Platform::by_name("windows-x86").expect("No windows-x86 platform"); + let type_libraries = importer.import(&platform).expect("Failed to import types"); + assert_eq!(type_libraries.len(), 1); + let til = type_libraries.first().expect("No type libraries"); + assert_eq!(til.named_types().len(), 1); + let first_ty = til + .named_types() + .iter() + .next() + .expect("No types in library"); + assert_eq!(first_ty.name.to_string(), "TestType"); + assert_eq!(first_ty.ty.type_class(), TypeClass::StructureTypeClass); + let first_ty_struct = first_ty + .ty + .get_structure() + .expect("Type is not a structure"); + assert_eq!(first_ty_struct.members().len(), 5); + let mut structure_fields = first_ty_struct.members().iter(); + + for member in first_ty_struct.members() { + println!(" +{}: {}", member.offset, member.name.to_string()) + } + + // TODO: Finish this! + assert!(false); + // let first_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(first_member.name.to_string(), "field1"); + // assert_eq!(first_member.ty, TypeClass::IntegerTypeClass); + // let second_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(second_member.name.to_string(), "field2_0"); + // assert_eq!(second_member.type.type_class(), TypeClass::IntegerTypeClass); + // let third_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(third_member.name.to_string(), "field2_1"); + // assert_eq!(third_member.type.type_class(), TypeClass::IntegerTypeClass); + // let fourth_member = structure_fields.next().expect("No fields in structure"); + } +} diff --git a/plugins/bntl_utils/src/winmd/info.rs b/plugins/bntl_utils/src/winmd/info.rs new file mode 100644 index 0000000000..4db30c7a72 --- /dev/null +++ b/plugins/bntl_utils/src/winmd/info.rs @@ -0,0 +1,430 @@ +//! Metadata information extracted from Windows metadata files. +//! +//! While we could use the direct representation, this is easier to work with. + +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Default, Clone)] +pub struct MetadataInfo { + pub types: Vec, + pub functions: Vec, + pub constants: Vec, +} + +impl MetadataInfo { + /// Partitions the metadata into a map of libraries, where each library contains types and functions + /// that belong to that library. This is used when mapping metadata info to type libraries. + pub fn partitioned(&self) -> PartitionedMetadataInfo { + let mut result_map: HashMap = HashMap::new(); + + // Map of namespace to module names that use it. + let mut namespace_dependencies: HashMap> = HashMap::new(); + for func in &self.functions { + if let Some(import) = &func.import_info { + namespace_dependencies + .entry(func.namespace.clone()) + .or_default() + .insert(import.module.name.clone()); + } + } + + let namespace_to_library_name = |ns: &str| -> LibraryName { + match namespace_dependencies.get(ns) { + Some(modules) if modules.len() == 1 => { + LibraryName::Module(modules.iter().next().unwrap().clone()) + } + _ => LibraryName::Namespace(ns.to_string()), + } + }; + + for func in &self.functions { + let dest_lib = match &func.import_info { + Some(info) => LibraryName::Module(info.module.name.clone()), + None => LibraryName::Namespace(func.namespace.clone()), + }; + let entry = result_map.entry(dest_lib.clone()).or_default(); + func.ty.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.functions.push(func.clone()); + } + + for ty in &self.types { + let dest_lib = namespace_to_library_name(&ty.namespace); + let entry = result_map.entry(dest_lib.clone()).or_default(); + ty.kind.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.types.push(ty.clone()); + } + + for constant in &self.constants { + let dest_lib = namespace_to_library_name(&constant.namespace); + let entry = result_map.entry(dest_lib.clone()).or_default(); + constant.ty.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.constants.push(constant.clone()); + } + + PartitionedMetadataInfo { + libraries: result_map, + } + } + + pub fn create_constant_enums(&self) -> Vec { + // Group constants by their type, if there are multiple constants with the same type, we + // will make an enum out of them, once that is done, we will take overlapping constants + // and prioritize certain namespaces over others. + // TODO: Add some more structured types here, this is a crazy map. + let mut grouped_constants: HashMap< + (String, String), + HashMap>, + > = HashMap::new(); + for constant in &self.constants { + let MetadataTypeKind::Reference { name, namespace } = &constant.ty else { + // TODO: We should optionally provide a way to group constants like these into an enumeration. + // Skipping constant `WDS_MC_TRACE_VERBOSE` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_INFO` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_WARNING` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_ERROR` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_FATAL` with non-reference type `Integer { size: Some(4), is_signed: false }` + tracing::debug!( + "Skipping constant `{}` with non-reference type `{:?}`", + constant.name, + constant.ty + ); + continue; + }; + grouped_constants + .entry((namespace.clone(), name.clone())) + .or_default() + .entry(constant.value) + .or_default() + .push(constant.clone()); + } + + let mut enums = Vec::new(); + for ((enum_namespace, enum_name), mapped_values) in grouped_constants { + let mut variants = Vec::new(); + for (_, group_variants) in mapped_values { + let sorted_group_variants = + sort_metadata_constants_by_proximity(&enum_namespace, group_variants); + let enum_variants: Vec<_> = sorted_group_variants + .iter() + .map(|info| (info.name.clone(), info.value)) + .collect(); + variants.extend(enum_variants); + } + + let enum_kind = MetadataTypeKind::Enum { + ty: Box::new(MetadataTypeKind::Void), + variants, + }; + + enums.push(MetadataTypeInfo { + name: enum_name, + kind: enum_kind, + namespace: enum_namespace, + }); + } + enums + } + + #[allow(dead_code)] + fn update_stale_references(&mut self) { + let mut valid_type_map = HashMap::new(); + for ty in self.types.iter() { + valid_type_map.insert(ty.name.clone(), ty.clone()); + } + + for ty in self.types.iter_mut() { + ty.kind.visit_references_mut(&mut |node| { + let MetadataTypeKind::Reference { name, namespace } = node else { + tracing::error!( + "`visit_references_mut` did not return a reference! {:?}", + node + ); + return; + }; + if let Some(survivor) = valid_type_map.get(name) { + if namespace != &survivor.namespace { + tracing::debug!( + "Updating stale namespace reference `{}` to `{}` for `{}`", + namespace, + survivor.namespace, + name + ); + *namespace = survivor.namespace.clone(); + } + } + }); + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum LibraryName { + /// A synthetic library with no associated module name. + /// + /// The shared library is "synthetic" in the sense that a binary view cannot reference it directly. + Namespace(String), + /// A real module with a name (e.g. "info.dll"), these libraries can be referenced directly by a binary view. + Module(String), +} + +#[derive(Debug, Clone, Default)] +pub struct LibraryInfo { + pub metadata: MetadataInfo, + /// A map of externally referenced names to their library names. + /// + /// This is required when resolving type references to other libraries. + pub external_references: HashMap, +} + +#[derive(Debug, Default)] +pub struct PartitionedMetadataInfo { + pub libraries: HashMap, +} + +// TODO: ModuleRef (computable from ModuleInfo and the underlying core module) +// TODO: Put a ModuleRef in all places where a module is associated. +#[derive(Debug, Clone)] +pub struct MetadataModuleInfo { + /// The modules name on disk, this is used to determine the imported + /// function name when loading type information from a type library. + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct MetadataTypeInfo { + pub name: String, + pub kind: MetadataTypeKind, + /// The namespace of the type, e.x. "Windows.Win32.Foundation" + /// + /// This is used to help determine what library this information belongs to. When we go to import + /// this information (along with others), we will build a tree of information where each node + /// corresponds to the namespace, and each child node corresponds to a sub-namespace. Then import + /// info will be enumerated to determine if the type can only ever belong to a single import module + /// if the type is only used in a single module, we will place it in that type library. If the namespace + /// can reference more than one module, we will place it in a common type library named after + /// the namespace itself, it can only ever be referenced by another type library and as such should + /// only contain types and no functions. + /// + /// For more information see [`PartitionedMetadataInfo`]. + pub namespace: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetadataTypeKind { + Void, + Bool { + // NOTE: Weird optional, if None we actually default the size to integer size! + size: Option, + }, + Integer { + size: Option, + is_signed: bool, + }, + Character { + size: usize, + }, + Float { + size: usize, + }, + Pointer { + is_const: bool, + is_pointee_const: bool, + target: Box, + }, + Array { + element: Box, + count: usize, + }, + Struct { + fields: Vec, + is_packed: bool, + }, + Union { + fields: Vec, + }, + Enum { + ty: Box, + variants: Vec<(String, u64)>, + }, + Function { + params: Vec, + return_type: Box, + is_vararg: bool, + }, + Reference { + // TODO: Generics may also be passed here. + /// The namespace of the referenced type, e.x. "Windows.Win32.Foundation" + namespace: String, + /// The referenced type name, e.x. "BOOL" + name: String, + }, +} + +impl MetadataTypeKind { + pub(crate) fn visit_references(&self, callback: &mut F) + where + F: FnMut(&str, &str), + { + match self { + MetadataTypeKind::Reference { namespace, name } => { + callback(namespace, name); + } + MetadataTypeKind::Pointer { target, .. } => { + target.visit_references(callback); + } + MetadataTypeKind::Array { element, .. } => { + element.visit_references(callback); + } + MetadataTypeKind::Struct { fields, .. } => { + for field in fields { + field.ty.visit_references(callback); + } + } + MetadataTypeKind::Enum { ty, .. } => { + ty.visit_references(callback); + } + MetadataTypeKind::Function { + params, + return_type, + .. + } => { + for param in params { + param.ty.visit_references(callback); + } + return_type.visit_references(callback); + } + _ => {} + } + } + + #[allow(dead_code)] + pub(crate) fn visit_references_mut(&mut self, callback: &mut F) + where + F: FnMut(&mut MetadataTypeKind), + { + match self { + MetadataTypeKind::Reference { .. } => { + callback(self); + } + MetadataTypeKind::Pointer { target, .. } => { + target.visit_references_mut(callback); + } + MetadataTypeKind::Array { element, .. } => { + element.visit_references_mut(callback); + } + MetadataTypeKind::Struct { fields, .. } | MetadataTypeKind::Union { fields, .. } => { + for field in fields { + field.ty.visit_references_mut(callback); + } + } + MetadataTypeKind::Enum { ty, .. } => { + ty.visit_references_mut(callback); + } + MetadataTypeKind::Function { + params, + return_type, + .. + } => { + for param in params { + param.ty.visit_references_mut(callback); + } + return_type.visit_references_mut(callback); + } + _ => {} + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataFieldInfo { + pub name: String, + pub ty: MetadataTypeKind, + pub is_const: bool, + /// This is only set for bitfields, The first value is the bit position within the associated byte, + /// and the second is the bit width. + /// + /// NOTE: The bit position can never be greater than `7`. + pub bitfield: Option<(u8, u8)>, + // TODO: Attributes ( virtual, static, etc...) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataParameterInfo { + pub name: String, + pub ty: MetadataTypeKind, + // TODO: Attributes (in, out, etc...) +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub enum MetadataImportMethod { + ByName(String), + ByOrdinal(u32), +} + +#[derive(Debug, Clone)] +pub struct MetadataImportInfo { + #[allow(dead_code)] + pub method: MetadataImportMethod, + pub module: MetadataModuleInfo, +} + +#[derive(Debug, Clone)] +pub struct MetadataFunctionInfo { + pub name: String, + /// This will only ever be [`MetadataTypeKind::Function`]. + pub ty: MetadataTypeKind, + pub namespace: String, + pub import_info: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataConstantInfo { + pub name: String, + pub namespace: String, + pub ty: MetadataTypeKind, + pub value: u64, +} + +pub fn sort_metadata_constants_by_proximity( + reference: &str, + mut candidates: Vec, +) -> Vec { + let ref_parts: Vec<&str> = reference.split('.').collect(); + candidates.sort_by_cached_key(|info| { + // Extract the namespace string from the metadata info + let ns = &info.namespace; + let cand_parts = ns.split('.'); + + let score = ref_parts + .iter() + .zip(cand_parts) + .take_while(|(a, b)| *a == b) + .count(); + + // Sort by highest score first, then alphabetically by namespace + (std::cmp::Reverse(score), ns.clone()) + }); + candidates +} diff --git a/plugins/bntl_utils/src/winmd/translate.rs b/plugins/bntl_utils/src/winmd/translate.rs new file mode 100644 index 0000000000..01ea54a550 --- /dev/null +++ b/plugins/bntl_utils/src/winmd/translate.rs @@ -0,0 +1,654 @@ +//! Translate windows metadata into a self-contained structure, for later use. + +use super::info::{ + MetadataConstantInfo, MetadataFieldInfo, MetadataFunctionInfo, MetadataImportInfo, + MetadataImportMethod, MetadataInfo, MetadataModuleInfo, MetadataParameterInfo, + MetadataTypeInfo, MetadataTypeKind, +}; +use std::collections::{HashMap, HashSet}; +use thiserror::Error; +use windows_metadata::reader::TypeCategory; +use windows_metadata::{ + AsRow, FieldAttributes, HasAttributes, MethodCallAttributes, Type, TypeAttributes, Value, +}; + +pub const BITFIELD_ATTR: &str = "NativeBitfieldAttribute"; +pub const CONST_ATTR: &str = "ConstAttribute"; +pub const FNPTR_ATTR: &str = "UnmanagedFunctionPointerAttribute"; +pub const _STRUCT_SIZE_ATTR: &str = "StructSizeFieldAttribute"; +pub const API_CONTRACT_ATTR: &str = "ApiContractAttribute"; + +#[derive(Error, Debug)] +pub enum TranslationError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("the attribute '{0}' is not supported")] + UnsupportedAttribute(String), +} + +pub struct WindowsMetadataTranslator { + // TODO: Allow this to be customized by user. + /// Replace references to a given name with a different one. + /// + /// This allows you to move types to a different namespace or rename them and be certain all + /// references to that type are updated. + remapped_references: HashMap<(&'static str, &'static str), (&'static str, &'static str)>, +} + +impl WindowsMetadataTranslator { + pub fn new() -> Self { + // TODO: Move this to a static array. + let mut remapped_references = HashMap::new(); + remapped_references.insert(("System", "Guid"), ("Windows.Win32.Foundation", "Guid")); + Self { + remapped_references, + } + } + + pub fn translate( + &self, + files: Vec, + ) -> Result { + if files.is_empty() { + return Err(TranslationError::NoFiles); + } + let index = windows_metadata::reader::TypeIndex::new(files); + self.translate_index(&index) + } + + pub fn translate_index( + &self, + index: &windows_metadata::reader::TypeIndex, + ) -> Result { + let mut functions = Vec::new(); + let mut types = Vec::new(); + let mut constants = Vec::new(); + + // TODO: Move this somewhere else? + // Add synthetic types here. + types.extend([ + MetadataTypeInfo { + name: "Guid".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Data1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data2".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data4".to_string(), + ty: MetadataTypeKind::Array { + element: Box::new(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + count: 8, + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HANDLE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HINSTANCE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HMODULE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "UNICODE_STRING".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Length".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "MaximumLength".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Buffer".to_string(), + ty: MetadataTypeKind::Reference { + namespace: "Windows.Win32.Foundation".to_string(), + name: "PWSTR".to_string(), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "BOOLEAN".to_string(), + kind: MetadataTypeKind::Bool { size: Some(1) }, + namespace: "Windows.Win32.Security".to_string(), + }, + MetadataTypeInfo { + name: "BOOL".to_string(), + // BOOL is integer sized, not char sized like a typical bool value. + kind: MetadataTypeKind::Bool { size: None }, + namespace: "Windows.Win32.Security".to_string(), + }, + ]); + + for entry in index.types() { + match entry.category() { + TypeCategory::Interface => { + let (interface_ty, interface_vtable_ty) = self.translate_interface(&entry)?; + types.push(interface_ty); + types.push(interface_vtable_ty); + } + TypeCategory::Class => { + let (cls_functions, cls_constants) = self.translate_class(&entry)?; + functions.extend(cls_functions); + constants.extend(cls_constants); + } + TypeCategory::Enum => { + types.push(self.translate_enum(&entry)?); + } + TypeCategory::Struct => { + // Skip marker type structures. + if entry.has_attribute(API_CONTRACT_ATTR) { + continue; + } + types.push(self.translate_struct(&entry)?); + } + TypeCategory::Delegate => { + types.push(self.translate_delegate(&entry)?); + } + TypeCategory::Attribute => { + // We will pull attributes directly from the other entries. + } + } + } + + // Remove duplicate types within the same namespace, the first one wins. This is what allows + // us to override types by placing the overrides in the type list before traversing the index. + let mut tracked_names = HashSet::<(String, String)>::new(); + types.retain(|ty| { + let ty_name = (ty.namespace.clone(), ty.name.clone()); + tracked_names.insert(ty_name) + }); + + Ok(MetadataInfo { + types, + functions, + constants, + }) + } + + pub fn translate_struct( + &self, + structure: &windows_metadata::reader::TypeDef, + ) -> Result { + let mut fields = Vec::new(); + + let nested: Result, _> = structure + .index() + .nested(structure.clone()) + .map(|n| { + // TODO: Are all nested fields a struct? + let nested_ty = self.translate_struct(&n)?; + Ok((n.name().to_string(), nested_ty)) + }) + .collect(); + let nested = nested?; + + for field in structure.fields() { + let mut field_ty = self.translate_type(&field.ty())?; + // TODO: This is kinda ugly. + // Handle nested structures by unwrapping the reference. + let mut nested_ty = None; + field_ty.visit_references(&mut |_, name| { + nested_ty = nested.get(name).cloned().map(|n| n.kind); + }); + field_ty = nested_ty.unwrap_or(field_ty); + + // Bitfields are special, they are a "fake" field that we need to look at the attributes of + // to unwrap the real fields that are contained within the storage type. + if field.has_attribute(BITFIELD_ATTR) { + for bitfield in field.attributes() { + let bitfield_values = bitfield.value(); + let mut values = bitfield_values.iter(); + let Some((_, Value::Utf8(bitfield_name))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_pos))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_width))) = values.next() else { + continue; + }; + // is_private, is_public, is_virtual + fields.push(MetadataFieldInfo { + name: bitfield_name.clone(), + ty: field_ty.clone(), + is_const: field.has_attribute(CONST_ATTR), + bitfield: Some((*bitfield_pos as u8, *bitfield_width as u8)), + }); + } + } else { + fields.push(MetadataFieldInfo { + name: field.name().to_string(), + ty: field_ty, + is_const: field.has_attribute(CONST_ATTR), + bitfield: None, + }); + } + } + + let mut is_packed = false; + if let Some(_layout) = structure.class_layout() { + is_packed = _layout.packing_size() == 1; + } + + // ExplicitLayout seems to denote a union layout. + let kind = if structure.flags().contains(TypeAttributes::ExplicitLayout) { + MetadataTypeKind::Union { fields } + } else { + MetadataTypeKind::Struct { fields, is_packed } + }; + + Ok(MetadataTypeInfo { + name: structure.name().to_string(), + kind, + namespace: structure.namespace().to_string(), + }) + } + + pub fn translate_class( + &self, + class: &windows_metadata::reader::TypeDef, + ) -> Result<(Vec, Vec), TranslationError> { + let namespace = class.namespace().to_string(); + let mut functions = Vec::new(); + for method in class.methods() { + match self.translate_method(&method) { + Ok(mut func) => { + func.namespace = namespace.clone(); + functions.push(func); + } + Err(e) => tracing::warn!("Failed to translate method {}: {}", method.name(), e), + } + } + + let mut constants = Vec::new(); + for field in class.fields() { + if let Some(constant) = field + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + { + constants.push(MetadataConstantInfo { + name: field.name().to_string(), + namespace: namespace.clone(), + ty: self.translate_type(&field.ty())?, + value: constant, + }); + } else { + tracing::debug!("Field {} is not a constant, skipping...", field.name()); + } + } + + Ok((functions, constants)) + } + + pub fn translate_method( + &self, + method: &windows_metadata::reader::MethodDef, + ) -> Result { + // TODO: Pass generics here? generic_params seems always empty? Even windows-rs doesn't use it. + let signature = method.signature(&[]); + let func_params: Result, TranslationError> = method + .params() + .filter(|p| !p.name().is_empty()) + .zip(signature.types) + .map(|(param, param_ty)| { + Ok(MetadataParameterInfo { + name: param.name().to_string(), + ty: self.translate_type(¶m_ty)?, + }) + }) + .collect(); + let func_ty = MetadataTypeKind::Function { + params: func_params?, + return_type: Box::new(self.translate_type(&signature.return_type)?), + is_vararg: signature.flags.contains(MethodCallAttributes::VARARG), + }; + + let import_info = method + .impl_map() + .map(|impl_map| self.import_info_from_map(&impl_map)); + + Ok(MetadataFunctionInfo { + name: method.name().to_string(), + ty: func_ty, + // NOTE: This will be set by the associated class entry once returned. + namespace: "".to_string(), + import_info, + }) + } + + pub fn translate_delegate( + &self, + delegate: &windows_metadata::reader::TypeDef, + ) -> Result { + if !delegate.has_attribute(FNPTR_ATTR) { + return Err(TranslationError::UnsupportedAttribute( + FNPTR_ATTR.to_string(), + )); + } + let invoke_method = delegate + .methods() + .find(|m| m.name() == "Invoke") + .expect("Invoke method not found"); + let translated_invoke_method = self.translate_method(&invoke_method)?; + Ok(MetadataTypeInfo { + name: delegate.name().to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(translated_invoke_method.ty), + }, + namespace: delegate.namespace().to_string(), + }) + } + + pub fn translate_interface( + &self, + interface: &windows_metadata::reader::TypeDef, + ) -> Result<(MetadataTypeInfo, MetadataTypeInfo), TranslationError> { + let mut vtable_fields = Vec::new(); + for meth in interface.methods() { + let meth_ty = self.translate_method(&meth)?; + vtable_fields.push(MetadataFieldInfo { + name: meth.name().to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(meth_ty.ty), + }, + is_const: false, + bitfield: None, + }) + } + + let interface_ns = interface.namespace(); + let interface_ty = MetadataTypeInfo { + name: interface.name().to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![MetadataFieldInfo { + name: "vtable".to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: interface_ns.to_string(), + name: format!("{}VTable", interface.name()), + }), + }, + is_const: false, + bitfield: None, + }], + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + let interface_vtable_ty = MetadataTypeInfo { + name: format!("{}VTable", interface.name()), + kind: MetadataTypeKind::Struct { + fields: Vec::new(), + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + Ok((interface_ty, interface_vtable_ty)) + } + + pub fn translate_enum( + &self, + _enum: &windows_metadata::reader::TypeDef, + ) -> Result { + let mut variants = Vec::new(); + let mut last_constant = 0; + let mut enum_ty = MetadataTypeKind::Integer { + size: None, + is_signed: true, + }; + for variant in _enum.fields() { + if variant.flags().contains(FieldAttributes::RTSpecialName) { + // Skip the hidden "value__" field. + continue; + } + // Pull the enums type from the constant if it exists. + // Otherwise, we will fall back to void and use a default type when importing. + if let Some(constant) = variant.constant() { + enum_ty = self.translate_type(&constant.ty())?; + } + let variant_constant = variant + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + .unwrap_or(last_constant); + let variant_name = variant.name().to_string(); + variants.push((variant_name, variant_constant)); + last_constant = variant_constant; + } + Ok(MetadataTypeInfo { + name: _enum.name().to_string(), + kind: MetadataTypeKind::Enum { + ty: Box::new(enum_ty), + variants, + }, + namespace: _enum.namespace().to_string(), + }) + } + + pub fn translate_type(&self, ty: &Type) -> Result { + match ty { + Type::Void => Ok(MetadataTypeKind::Void), + Type::Bool => Ok(MetadataTypeKind::Bool { size: Some(1) }), + Type::Char => Ok(MetadataTypeKind::Character { size: 1 }), + Type::I8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: true, + }), + Type::U8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + Type::I16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }), + Type::U16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }), + Type::I32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }), + Type::U32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }), + Type::I64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: true, + }), + Type::U64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: false, + }), + Type::F32 => Ok(MetadataTypeKind::Float { size: 4 }), + Type::F64 => Ok(MetadataTypeKind::Float { size: 8 }), + Type::ISize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: true, + }), + Type::USize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: false, + }), + Type::Name(name) => { + if let Some((remapped_ns, remapped_name)) = + self.remapped_references.get(&(&name.namespace, &name.name)) + { + Ok(MetadataTypeKind::Reference { + namespace: remapped_ns.to_string(), + name: remapped_name.to_string(), + }) + } else { + Ok(MetadataTypeKind::Reference { + namespace: name.namespace.clone(), + name: name.name.clone(), + }) + } + } + Type::PtrMut(target, _) => Ok(MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(self.translate_type(target)?), + }), + Type::PtrConst(target, _) => { + Ok(MetadataTypeKind::Pointer { + is_const: false, + // TODO: I think this might be pointee const? + is_pointee_const: true, + target: Box::new(self.translate_type(target)?), + }) + } + Type::ArrayFixed(elem_ty, count) => Ok(MetadataTypeKind::Array { + element: Box::new(self.translate_type(elem_ty)?), + count: *count, + }), + other => Err(TranslationError::UnhandledType(format!("{:?}", other))), + } + } + + pub fn import_info_from_map( + &self, + map: &windows_metadata::reader::ImplMap, + ) -> MetadataImportInfo { + MetadataImportInfo { + method: MetadataImportMethod::ByName(map.import_name().to_string()), + module: MetadataModuleInfo { + name: map.import_scope().name().to_string(), + }, + } + } + + pub fn value_to_u64(&self, value: &Value) -> Option { + match value { + Value::Bool(b) => Some(*b as u64), + Value::U8(i) => Some(*i as u64), + Value::I8(i) => Some(*i as u64), + Value::U16(i) => Some(*i as u64), + Value::I16(i) => Some(*i as u64), + Value::U32(i) => Some(*i as u64), + Value::I32(i) => Some(*i as u64), + Value::U64(i) => Some(*i), + Value::I64(i) => Some(*i as u64), + _ => None, + } + } +} diff --git a/plugins/dwarf/dwarf_export/Cargo.toml b/plugins/dwarf/dwarf_export/Cargo.toml index 74cd774ee1..2abcecd4e1 100644 --- a/plugins/dwarf/dwarf_export/Cargo.toml +++ b/plugins/dwarf/dwarf_export/Cargo.toml @@ -3,6 +3,7 @@ name = "dwarf_export" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/dwarf_import/Cargo.toml b/plugins/dwarf/dwarf_import/Cargo.toml index d01c60b152..be06fbc28f 100644 --- a/plugins/dwarf/dwarf_import/Cargo.toml +++ b/plugins/dwarf/dwarf_import/Cargo.toml @@ -3,6 +3,7 @@ name = "dwarf_import" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/dwarf_import/src/helpers.rs b/plugins/dwarf/dwarf_import/src/helpers.rs index c7855b250c..541fdc4249 100644 --- a/plugins/dwarf/dwarf_import/src/helpers.rs +++ b/plugins/dwarf/dwarf_import/src/helpers.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::{str::FromStr, sync::mpsc}; use crate::{DebugInfoBuilderContext, ReaderType}; @@ -590,10 +589,8 @@ pub(crate) fn find_sibling_debug_file(view: &BinaryView) -> Option { return None; } - let full_file_path = view.file().filename().to_string(); - - let debug_file = PathBuf::from(format!("{}.debug", full_file_path)); - let dsym_folder = PathBuf::from(format!("{}.dSYM", full_file_path)); + let debug_file = view.file().file_path().with_extension("debug"); + let dsym_folder = view.file().file_path().with_extension("dSYM"); // Find sibling debug file if debug_file.exists() && debug_file.is_file() { @@ -624,13 +621,12 @@ pub(crate) fn find_sibling_debug_file(view: &BinaryView) -> Option { // Look for dSYM // TODO: look for dSYM in project if dsym_folder.exists() && dsym_folder.is_dir() { - let filename = Path::new(&full_file_path) - .file_name() - .unwrap_or(OsStr::new("")); - - let dsym_file = dsym_folder.join("Contents/Resources/DWARF/").join(filename); // TODO: should this just pull any file out? Can there be multiple files? - if dsym_file.exists() { - return Some(dsym_file.to_string_lossy().to_string()); + if let Some(filename) = view.file().file_path().file_name() { + // TODO: should this just pull any file out? Can there be multiple files? + let dsym_file = dsym_folder.join("Contents/Resources/DWARF/").join(filename); + if dsym_file.exists() { + return Some(dsym_file.to_string_lossy().to_string()); + } } } diff --git a/plugins/dwarf/dwarfdump/Cargo.toml b/plugins/dwarf/dwarfdump/Cargo.toml index cd13206a1b..7af0fa38af 100644 --- a/plugins/dwarf/dwarfdump/Cargo.toml +++ b/plugins/dwarf/dwarfdump/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Kyle Martin "] edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/shared/Cargo.toml b/plugins/dwarf/shared/Cargo.toml index 19cb963e13..e1404c74dd 100644 --- a/plugins/dwarf/shared/Cargo.toml +++ b/plugins/dwarf/shared/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Kyle Martin "] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/plugins/idb_import/Cargo.toml b/plugins/idb_import/Cargo.toml index 9ba241d264..ad2d2f6a9a 100644 --- a/plugins/idb_import/Cargo.toml +++ b/plugins/idb_import/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Rubens Brandao "] edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/idb_import/src/lib.rs b/plugins/idb_import/src/lib.rs index f285f554dc..0137f198bf 100644 --- a/plugins/idb_import/src/lib.rs +++ b/plugins/idb_import/src/lib.rs @@ -27,8 +27,10 @@ impl CustomDebugInfoParser for IDBDebugInfoParser { project_file.name().as_str().ends_with(".i64") || project_file.name().as_str().ends_with(".idb") } else { - view.file().filename().as_str().ends_with(".i64") - || view.file().filename().as_str().ends_with(".idb") + view.file() + .file_path() + .extension() + .map_or(false, |ext| ext == "i64" || ext == "idb") } } @@ -55,7 +57,10 @@ impl CustomDebugInfoParser for TILDebugInfoParser { if let Some(project_file) = view.file().project_file() { project_file.name().as_str().ends_with(".til") } else { - view.file().filename().as_str().ends_with(".til") + view.file() + .file_path() + .extension() + .map_or(false, |ext| ext == "til") } } diff --git a/plugins/pdb-ng/Cargo.toml b/plugins/pdb-ng/Cargo.toml index 08776df22b..44cf5f03cb 100644 --- a/plugins/pdb-ng/Cargo.toml +++ b/plugins/pdb-ng/Cargo.toml @@ -3,6 +3,7 @@ name = "pdb-import-plugin" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/pdb-ng/src/lib.rs b/plugins/pdb-ng/src/lib.rs index 86ae1cdd07..61ff54b29d 100644 --- a/plugins/pdb-ng/src/lib.rs +++ b/plugins/pdb-ng/src/lib.rs @@ -617,7 +617,7 @@ impl CustomDebugInfoParser for PDBParser { } // Try in the same directory as the file - let mut potential_path = PathBuf::from(view.file().filename().to_string()); + let mut potential_path = view.file().file_path(); potential_path.pop(); potential_path.push(&info.file_name); if potential_path.exists() { diff --git a/plugins/svd/Cargo.toml b/plugins/svd/Cargo.toml index e4cdc5d88c..eafc87074e 100644 --- a/plugins/svd/Cargo.toml +++ b/plugins/svd/Cargo.toml @@ -3,6 +3,7 @@ name = "svd_ninja" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib", "lib"] diff --git a/plugins/warp/Cargo.toml b/plugins/warp/Cargo.toml index 3a623e0ab4..13516c50c2 100644 --- a/plugins/warp/Cargo.toml +++ b/plugins/warp/Cargo.toml @@ -3,6 +3,7 @@ name = "warp_ninja" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["lib", "cdylib"] diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs index 41aab42345..3df2dbf8ed 100644 --- a/plugins/warp/src/cache.rs +++ b/plugins/warp/src/cache.rs @@ -79,6 +79,6 @@ pub struct CacheDestructor; impl ObjectDestructor for CacheDestructor { fn destruct_view(&self, view: &BinaryView) { clear_type_ref_cache(view); - tracing::debug!("Removed WARP caches for {:?}", view.file().filename()); + tracing::debug!("Removed WARP caches for {}", view.file()); } } diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs index 0c6a2fd5b0..2a7488193e 100644 --- a/plugins/warp/src/plugin/create.rs +++ b/plugins/warp/src/plugin/create.rs @@ -23,11 +23,7 @@ impl SaveFileField { let default_name = match file.project_file() { None => { // Not in a project, use the file name directly. - file.filename() - .split('/') - .last() - .unwrap_or("file") - .to_string() + file.display_name() } Some(project_file) => project_file.name(), }; diff --git a/plugins/workflow_objc/Cargo.toml b/plugins/workflow_objc/Cargo.toml index 948fc2de26..07ef9bbefa 100644 --- a/plugins/workflow_objc/Cargo.toml +++ b/plugins/workflow_objc/Cargo.toml @@ -3,6 +3,7 @@ name = "workflow_objc" version = "0.1.0" edition = "2021" license = "BSD-3-Clause" +publish = false [lib] crate-type = ["staticlib", "cdylib"] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 17cb2f0dc4..2a03e5acfb 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ name = "binaryninja" version = "0.1.0" authors = ["Ryan Snyder ", "Kyle Martin "] edition = "2021" -rust-version = "1.83.0" +rust-version = "1.91.1" license = "Apache-2.0" [features] diff --git a/rust/README.md b/rust/README.md index 60c4847c25..55dccdff65 100644 --- a/rust/README.md +++ b/rust/README.md @@ -45,7 +45,7 @@ More examples can be found in [here](https://github.com/Vector35/binaryninja-api ### Requirements -- Having BinaryNinja installed (and your license registered) +- Having [Binary Ninja] installed (and your license registered) - For headless operation you must have a headless supporting license. - Clang - Rust @@ -65,7 +65,7 @@ binaryninjacore-sys = { git = "https://github.com/Vector35/binaryninja-api.git", ``` `build.rs`: -```doctestinjectablerust +```rust fn main() { let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH").expect("DEP_BINARYNINJACORE_PATH not specified"); @@ -112,14 +112,27 @@ pub extern "C" fn CorePluginInit() -> bool { } ``` -Examples for writing a plugin can be found [here](https://github.com/Vector35/binaryninja-api/tree/dev/plugins). +Examples for writing a plugin can be found [here](https://github.com/Vector35/binaryninja-api/tree/dev/rust/plugin_examples) and [here](https://github.com/Vector35/binaryninja-api/tree/dev/plugins). + +#### Sending Logs + +To send logs from your plugin to Binary Ninja, you can use the [tracing](https://docs.rs/tracing/latest/tracing/) crate. +At the beginning of your plugin's initialization routine, register the tracing subscriber with [`crate::tracing_init!`], +for more details see the documentation of that macro. + +#### Plugin Compatibility + +A built plugin can only be loaded into a compatible Binary Ninja version, this is determined by the ABI version of the +plugin. The ABI version is located at the top of the `binaryninjacore.h` header file, and as such plugins should pin +their binary ninja dependency to a specific tag or commit hash. See the cargo documentation [here](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choice-of-commit) +for more details. ### Write a Standalone Executable If you have a headless supporting license, you are able to use Binary Ninja as a regular dynamically loaded library. -Standalone executables must initialize the core themselves. `binaryninja::headless::init()` to initialize the core, and -`binaryninja::headless::shutdown()` to shutdown the core. Prefer using `binaryninja::headless::Session` as it will +Standalone executables must initialize the core themselves. [`crate::headless::init()`] to initialize the core, and +[`crate::headless::shutdown()`] to shutdown the core. Prefer using [`crate::headless::Session`] as it will shut down for you once it is dropped. `main.rs`: @@ -131,6 +144,12 @@ fn main() { } ``` +#### Capturing Logs + +To capture logs from Binary Ninja, you can use the [tracing](https://docs.rs/tracing/latest/tracing/) crate. Before initializing +the core but after registering your tracing subscriber, register a [`crate::tracing::TracingLogListener`], for more details see +the documentation for that type. + ## Offline Documentation Offline documentation can be generated like any other rust crate, using `cargo doc`. @@ -161,18 +180,6 @@ it will likely confuse someone else, and you should make an issue or ask for gui #### Attribution -This project makes use of: - - [log] ([log license] - MIT) - - [rayon] ([rayon license] - MIT) - - [thiserror] ([thiserror license] - MIT) - - [serde_json] ([serde_json license] - MIT) - -[log]: https://github.com/rust-lang/log -[log license]: https://github.com/rust-lang/log/blob/master/LICENSE-MIT -[rayon]: https://github.com/rayon-rs/rayon -[rayon license]: https://github.com/rayon-rs/rayon/blob/master/LICENSE-MIT -[thiserror]: https://github.com/dtolnay/thiserror -[thiserror license]: https://github.com/dtolnay/thiserror/blob/master/LICENSE-MIT -[serde_json]: https://github.com/serde-rs/json -[serde_json license]: https://github.com/serde-rs/json/blob/master/LICENSE-MIT -[Binary Ninja]: https://binary.ninja \ No newline at end of file +For attribution, please refer to the [Rust Licenses section](https://docs.binary.ninja/about/open-source.html#rust-licenses) of the user documentation. + +[Binary Ninja]: https://binary.ninja/ \ No newline at end of file diff --git a/rust/examples/decompile.rs b/rust/examples/decompile.rs index 6ba9cbf415..155de81cbf 100644 --- a/rust/examples/decompile.rs +++ b/rust/examples/decompile.rs @@ -42,7 +42,7 @@ pub fn main() { .load(&filename) .expect("Couldn't open file!"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/disassemble.rs b/rust/examples/disassemble.rs index ce3ff76569..df33aadec4 100644 --- a/rust/examples/disassemble.rs +++ b/rust/examples/disassemble.rs @@ -39,7 +39,7 @@ pub fn main() { .load(&filename) .expect("Couldn't open file!"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/high_level_il.rs b/rust/examples/high_level_il.rs index a099437a20..57b735e963 100644 --- a/rust/examples/high_level_il.rs +++ b/rust/examples/high_level_il.rs @@ -14,7 +14,7 @@ fn main() { .load("/bin/cat") .expect("Couldn't open `/bin/cat`"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/medium_level_il.rs b/rust/examples/medium_level_il.rs index c9412d089c..e2c0edee5f 100644 --- a/rust/examples/medium_level_il.rs +++ b/rust/examples/medium_level_il.rs @@ -14,7 +14,7 @@ fn main() { .load("/bin/cat") .expect("Couldn't open `/bin/cat`"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/plugin_examples/README.md b/rust/plugin_examples/README.md new file mode 100644 index 0000000000..1f49df99bc --- /dev/null +++ b/rust/plugin_examples/README.md @@ -0,0 +1,10 @@ +# Plugin Examples + +These are examples of plugins that can be used to extend Binary Ninja's functionality. Each directory contains a crate +that when built produces a shared library that can then be placed into the `plugins` directory of your Binary Ninja +installation. + +For more information on installing plugins, refer to the user documentation [here](https://docs.binary.ninja/guide/plugins.html#using-plugins). + +For more examples of plugins, see the [plugins directory](https://github.com/Vector35/binaryninja-api/tree/dev/plugins) +which contains a number of plugins bundled with Binary Ninja. \ No newline at end of file diff --git a/rust/plugin_examples/data_renderer/Cargo.toml b/rust/plugin_examples/data_renderer/Cargo.toml index 5911392fcf..2887b738b6 100644 --- a/rust/plugin_examples/data_renderer/Cargo.toml +++ b/rust/plugin_examples/data_renderer/Cargo.toml @@ -2,6 +2,7 @@ name = "example_data_renderer" version = "0.1.0" edition = "2021" +publish = false [lib] crate-type = ["cdylib"] diff --git a/rust/src/background_task.rs b/rust/src/background_task.rs index 1e9933551c..94cc336858 100644 --- a/rust/src/background_task.rs +++ b/rust/src/background_task.rs @@ -24,14 +24,40 @@ use crate::string::*; pub type Result = result::Result; +/// An RAII guard for [`BackgroundTask`] to finish the task when dropped. +pub struct OwnedBackgroundTaskGuard { + pub(crate) task: Ref, +} + +impl OwnedBackgroundTaskGuard { + pub fn cancel(&mut self) { + self.task.cancel(); + } + + pub fn is_cancelled(&self) -> bool { + self.task.is_cancelled() + } + + pub fn set_progress_text(&mut self, text: &str) { + self.task.set_progress_text(text); + } +} + +impl Drop for OwnedBackgroundTaskGuard { + fn drop(&mut self) { + self.task.finish(); + } +} + /// A [`BackgroundTask`] does not actually execute any code, only act as a handler, primarily to query /// the status of the task, and to cancel the task. /// -/// If you are looking to execute code in the background consider using rusts threading API, or if you -/// want the core to execute the task on a worker thread, use the [`crate::worker_thread`] API. +/// If you are looking to execute code in the background, consider using rusts threading API, or if you +/// want the core to execute the task on a worker thread, instead use the [`crate::worker_thread`] API. /// -/// NOTE: If you do not call [`BackgroundTask::finish`] or [`BackgroundTask::cancel`] the task will -/// persist even _after_ it has been dropped. +/// NOTE: If you do not call [`BackgroundTask::finish`] or [`BackgroundTask::cancel`], the task will +/// persist even _after_ it has been dropped, use [`OwnedBackgroundTaskGuard`] to ensure the task is +/// finished, see [`BackgroundTask::enter`] for usage. #[derive(PartialEq, Eq, Hash)] pub struct BackgroundTask { pub(crate) handle: *mut BNBackgroundTask, @@ -52,6 +78,15 @@ impl BackgroundTask { unsafe { Ref::new(Self { handle }) } } + /// Creates a [`OwnedBackgroundTaskGuard`] that is responsible for finishing the background task + /// once dropped. Because the status of a task does not dictate the underlying objects' lifetime, + /// this can be safely done without requiring exclusive ownership. + pub fn enter(&self) -> OwnedBackgroundTaskGuard { + OwnedBackgroundTaskGuard { + task: self.to_owned(), + } + } + pub fn can_cancel(&self) -> bool { unsafe { BNCanCancelBackgroundTask(self.handle) } } diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index cdf90c07e3..4e35dc8c11 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -2134,6 +2134,12 @@ pub trait BinaryViewExt: BinaryViewBase { unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()) } } + fn type_libraries(&self) -> Array { + let mut count = 0; + let result = unsafe { BNGetBinaryViewTypeLibraries(self.as_ref().handle, &mut count) }; + unsafe { Array::new(result, count, ()) } + } + /// Make the contents of a type library available for type/import resolution fn add_type_library(&self, library: &TypeLibrary) { unsafe { BNAddBinaryViewTypeLibrary(self.as_ref().handle, library.as_raw()) } @@ -2145,9 +2151,9 @@ pub trait BinaryViewExt: BinaryViewBase { NonNull::new(result).map(|h| unsafe { TypeLibrary::ref_from_raw(h) }) } - /// Should be called by custom py:py:class:`BinaryView` implementations - /// when they have successfully imported an object from a type library (eg a symbol's type). - /// Values recorded with this function will then be queryable via [BinaryViewExt::lookup_imported_object_library]. + /// Should be called by custom [`BinaryView`] implementations when they have successfully + /// imported an object from a type library (eg a symbol's type). Values recorded with this + /// function will then be queryable via [`BinaryViewExt::lookup_imported_object_library`]. /// /// * `lib` - Type Library containing the imported type /// * `name` - Name of the object in the type library @@ -2173,23 +2179,23 @@ pub trait BinaryViewExt: BinaryViewBase { QualifiedName::free_raw(raw_name); } - /// Recursively imports a type from the specified type library, or, if - /// no library was explicitly provided, the first type library associated with the current [BinaryView] - /// that provides the name requested. + /// Recursively imports a type from the specified type library, or, if no library was + /// explicitly provided, the first type library associated with the current [`BinaryView`] that + /// provides the name requested. /// - /// This may have the impact of loading other type libraries as dependencies on other type libraries are lazily resolved - /// when references to types provided by them are first encountered. + /// This may have the impact of loading other type libraries as dependencies on other type + /// libraries are lazily resolved when references to types provided by them are first encountered. /// - /// Note that the name actually inserted into the view may not match the name as it exists in the type library in - /// the event of a name conflict. To aid in this, the [Type] object returned is a `NamedTypeReference` to - /// the deconflicted name used. + /// Note that the name actually inserted into the view may not match the name as it exists in + /// the type library in the event of a name conflict. To aid in this, the [`Type`] object + /// returned is a `NamedTypeReference` to the deconflicted name used. fn import_type_library>( &self, name: T, - mut lib: Option, + lib: Option<&TypeLibrary>, ) -> Option> { let mut lib_ref = lib - .as_mut() + .as_ref() .map(|l| unsafe { l.as_raw() } as *mut _) .unwrap_or(std::ptr::null_mut()); let mut raw_name = QualifiedName::into_raw(name.into()); @@ -2200,22 +2206,23 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively imports an object from the specified type library, or, if - /// no library was explicitly provided, the first type library associated with the current [BinaryView] - /// that provides the name requested. + /// Recursively imports an object (function) from the specified type library, or, if no library was + /// explicitly provided, the first type library associated with the current [`BinaryView`] that + /// provides the name requested. /// - /// This may have the impact of loading other type libraries as dependencies on other type libraries are lazily resolved - /// when references to types provided by them are first encountered. + /// This may have the impact of loading other type libraries as dependencies on other type + /// libraries are lazily resolved when references to types provided by them are first encountered. /// - /// .. note:: If you are implementing a custom BinaryView and use this method to import object types, - /// you should then call [BinaryViewExt::record_imported_object_library] with the details of where the object is located. + /// NOTE: If you are implementing a custom [`BinaryView`] and use this method to import object types, + /// you should then call [BinaryViewExt::record_imported_object_library] with the details of + /// where the object is located. fn import_type_object>( &self, name: T, - mut lib: Option, + lib: Option<&TypeLibrary>, ) -> Option> { let mut lib_ref = lib - .as_mut() + .as_ref() .map(|l| unsafe { l.as_raw() } as *mut _) .unwrap_or(std::ptr::null_mut()); let mut raw_name = QualifiedName::into_raw(name.into()); @@ -2226,7 +2233,7 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively imports a type interface given its GUID. + /// Recursively imports a [`Type`] given its GUID from available type libraries. fn import_type_by_guid(&self, guid: &str) -> Option> { let guid = guid.to_cstr(); let result = @@ -2234,7 +2241,7 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively exports `type_obj` into `lib` as a type with name `name` + /// Recursively exports `type_obj` into `lib` as a type with name `name`. /// /// As other referenced types are encountered, they are either copied into the destination type library or /// else the type library that provided the referenced type is added as a dependency for the destination library. @@ -2256,10 +2263,10 @@ pub trait BinaryViewExt: BinaryViewBase { QualifiedName::free_raw(raw_name); } - /// Recursively exports `type_obj` into `lib` as a type with name `name` + /// Recursively exports `type_obj` into `lib` as a type with name `name`. /// /// As other referenced types are encountered, they are either copied into the destination type library or - /// else the type library that provided the referenced type is added as a dependency for the destination library. + /// else the type library that provided the referenced type is added as a dependency for the destination library. fn export_object_to_library>( &self, lib: &TypeLibrary, @@ -2469,8 +2476,14 @@ impl BinaryView { Ref::new(Self { handle }) } - pub fn from_path(meta: &mut FileMetadata, file_path: impl AsRef) -> Result> { - let file = file_path.as_ref().to_cstr(); + /// Construct the raw binary view from the given metadata. Before calling this make sure you have + /// a valid file path set for the [`FileMetadata`]. It is required that the [`FileMetadata::file_path`] + /// exist on the local filesystem. + pub fn from_metadata(meta: &FileMetadata) -> Result> { + if !meta.file_path().exists() { + return Err(()); + } + let file = meta.file_path().to_cstr(); let handle = unsafe { BNCreateBinaryDataViewFromFilename(meta.handle, file.as_ptr() as *mut _) }; @@ -2481,6 +2494,15 @@ impl BinaryView { unsafe { Ok(Ref::new(Self { handle })) } } + /// Construct the raw binary view from the given `file_path` and metadata. + /// + /// This will implicitly set the metadata file path and then construct the view. If the metadata + /// already has the desired file path, use [`BinaryView::from_metadata`] instead. + pub fn from_path(meta: &FileMetadata, file_path: impl AsRef) -> Result> { + meta.set_file_path(file_path.as_ref()); + Self::from_metadata(meta) + } + pub fn from_accessor( meta: &FileMetadata, file: &mut FileAccessor, diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs index 993e6bc9c4..dbe2b8b9c3 100644 --- a/rust/src/collaboration/file.rs +++ b/rust/src/collaboration/file.rs @@ -57,10 +57,10 @@ impl RemoteFile { RemoteFile::get_for_local_database(&database) } - pub fn core_file(&self) -> Result { + pub fn core_file(&self) -> Result, ()> { let result = unsafe { BNRemoteFileGetCoreFile(self.handle.as_ptr()) }; NonNull::new(result) - .map(|handle| unsafe { ProjectFile::from_raw(handle) }) + .map(|handle| unsafe { ProjectFile::ref_from_raw(handle) }) .ok_or(()) } @@ -584,6 +584,7 @@ impl PartialEq for RemoteFile { self.id() == other.id() } } + impl Eq for RemoteFile {} impl ToOwned for RemoteFile { @@ -594,6 +595,9 @@ impl ToOwned for RemoteFile { } } +unsafe impl Send for RemoteFile {} +unsafe impl Sync for RemoteFile {} + unsafe impl RefCountable for RemoteFile { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { diff --git a/rust/src/collaboration/folder.rs b/rust/src/collaboration/folder.rs index 797bcdd4de..3ae84b6424 100644 --- a/rust/src/collaboration/folder.rs +++ b/rust/src/collaboration/folder.rs @@ -1,5 +1,6 @@ use super::{Remote, RemoteProject}; use binaryninjacore_sys::*; +use std::fmt::Debug; use std::ptr::NonNull; use crate::project::folder::ProjectFolder; @@ -125,11 +126,22 @@ impl RemoteFolder { } } +impl Debug for RemoteFolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteFolder") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .finish() + } +} + impl PartialEq for RemoteFolder { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } + impl Eq for RemoteFolder {} impl ToOwned for RemoteFolder { @@ -140,6 +152,9 @@ impl ToOwned for RemoteFolder { } } +unsafe impl Send for RemoteFolder {} +unsafe impl Sync for RemoteFolder {} + unsafe impl RefCountable for RemoteFolder { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { diff --git a/rust/src/collaboration/project.rs b/rust/src/collaboration/project.rs index fa46e477cd..cfcc1db48d 100644 --- a/rust/src/collaboration/project.rs +++ b/rust/src/collaboration/project.rs @@ -1,4 +1,5 @@ use std::ffi::c_void; +use std::fmt::Debug; use std::path::PathBuf; use std::ptr::NonNull; use std::time::SystemTime; @@ -870,11 +871,22 @@ impl RemoteProject { //} } +impl Debug for RemoteProject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteProject") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .finish() + } +} + impl PartialEq for RemoteProject { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } + impl Eq for RemoteProject {} impl ToOwned for RemoteProject { @@ -885,6 +897,9 @@ impl ToOwned for RemoteProject { } } +unsafe impl Send for RemoteProject {} +unsafe impl Sync for RemoteProject {} + unsafe impl RefCountable for RemoteProject { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { diff --git a/rust/src/collaboration/remote.rs b/rust/src/collaboration/remote.rs index c1ad66786c..03c808bc22 100644 --- a/rust/src/collaboration/remote.rs +++ b/rust/src/collaboration/remote.rs @@ -312,6 +312,10 @@ impl Remote { &self, mut progress: F, ) -> Result<(), ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + let success = unsafe { BNRemotePullProjects( self.handle.as_ptr(), diff --git a/rust/src/custom_binary_view.rs b/rust/src/custom_binary_view.rs index 13a124e9da..4cd499314c 100644 --- a/rust/src/custom_binary_view.rs +++ b/rust/src/custom_binary_view.rs @@ -18,6 +18,7 @@ use binaryninjacore_sys::*; pub use binaryninjacore_sys::BNModificationStatus as ModificationStatus; +use std::fmt::Debug; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::os::raw::c_void; @@ -381,6 +382,15 @@ impl BinaryViewTypeBase for BinaryViewType { } } +impl Debug for BinaryViewType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BinaryViewType") + .field("name", &self.name()) + .field("long_name", &self.long_name()) + .finish() + } +} + impl CoreArrayProvider for BinaryViewType { type Raw = *mut BNBinaryViewType; type Context = (); diff --git a/rust/src/file_metadata.rs b/rust/src/file_metadata.rs index 27ceb4af25..f642775003 100644 --- a/rust/src/file_metadata.rs +++ b/rust/src/file_metadata.rs @@ -20,7 +20,7 @@ use binaryninjacore_sys::*; use binaryninjacore_sys::{BNCreateDatabaseWithProgress, BNOpenExistingDatabaseWithProgress}; use std::ffi::c_void; use std::fmt::{Debug, Display, Formatter}; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::progress::ProgressCallback; use crate::project::file::ProjectFile; @@ -46,12 +46,15 @@ impl FileMetadata { Self::ref_from_raw(unsafe { BNCreateFileMetadata() }) } - pub fn with_filename(name: &str) -> Ref { + /// Build a [`FileMetadata`] with the given `path`, this is uncommon as you are likely to want to + /// open a [`BinaryView`] + pub fn with_file_path(path: &Path) -> Ref { let ret = FileMetadata::new(); - ret.set_filename(name); + ret.set_file_path(path); ret } + /// Closes the [`FileMetadata`] allowing any [`BinaryView`] parented to it to be freed. pub fn close(&self) { unsafe { BNCloseFile(self.handle); @@ -63,22 +66,124 @@ impl FileMetadata { SessionId(raw) } - pub fn filename(&self) -> String { + /// The path to the [`FileMetadata`] on disk. + /// + /// This will not point to the original file on disk, in the event that the file was saved + /// as a BNDB. When a BNDB is opened, the FileMetadata will contain the file path to the database. + /// + /// If you need the original binary file path, use [`FileMetadata::original_file_path`] instead. + /// + /// If you just want a name to present to the user, use [`FileMetadata::display_name`]. + pub fn file_path(&self) -> PathBuf { unsafe { let raw = BNGetFilename(self.handle); - BnString::into_string(raw) + PathBuf::from(BnString::into_string(raw)) } } - pub fn set_filename(&self, name: &str) { + // TODO: To prevent issues we will not allow users to set the file path as it really should be + // TODO: derived at construction and not modified later. + /// Set the files path on disk. + /// + /// This should always be a valid path. + pub(crate) fn set_file_path(&self, name: &Path) { let name = name.to_cstr(); - unsafe { BNSetFilename(self.handle, name.as_ptr()); } } - pub fn modified(&self) -> bool { + /// The display name of the file. Useful for presenting to the user. Can differ from the original + /// name of the file and can be overridden with [`FileMetadata::set_display_name`]. + pub fn display_name(&self) -> String { + let raw_name = unsafe { + let raw = BNGetDisplayName(self.handle); + BnString::into_string(raw) + }; + // Sometimes this display name may return a full path, which is not the intended purpose. + raw_name + .split('/') + .next_back() + .unwrap_or(&raw_name) + .to_string() + } + + /// Set the display name of the file. + /// + /// This can be anything and will not be used for any purpose other than presentation. + pub fn set_display_name(&self, name: &str) { + let name = name.to_cstr(); + unsafe { + BNSetDisplayName(self.handle, name.as_ptr()); + } + } + + /// The path to the original file on disk, if any. + /// + /// It may not be present if the BNDB was saved without it or cleared via [`FileMetadata::clear_original_file_path`]. + /// + /// Only prefer this over [`FileMetadata::file_path`] if you require the original binary location. + pub fn original_file_path(&self) -> Option { + let raw_name = unsafe { + let raw = BNGetOriginalFilename(self.handle); + PathBuf::from(BnString::into_string(raw)) + }; + // If the original file path is empty, or the original file path is pointing to the same file + // as the database itself, we know the original file path does not exist. + if raw_name.as_os_str().is_empty() + || self.is_database_backed() && raw_name == self.file_path() + { + None + } else { + Some(raw_name) + } + } + + /// Set the original file path inside the database. Useful if it has since been cleared from the + /// database, or you have moved the original file. + pub fn set_original_file_path(&self, path: &Path) { + let name = path.to_cstr(); + unsafe { + BNSetOriginalFilename(self.handle, name.as_ptr()); + } + } + + /// Clear the original file path inside the database. This is useful since the original file path + /// may be sensitive information you wish to not share with others. + pub fn clear_original_file_path(&self) { + unsafe { + BNSetOriginalFilename(self.handle, std::ptr::null()); + } + } + + /// The non-filesystem path that describes how this file was derived from the container + /// transform system, detailing the sequence of transform steps and selection names. + /// + /// NOTE: Returns `None` if this [`FileMetadata`] was not processed by the transform system and + /// does not differ from that of the "physical" file path reported by [`FileMetadata::file_path`]. + pub fn virtual_path(&self) -> Option { + unsafe { + let raw = BNGetVirtualPath(self.handle); + let path = BnString::into_string(raw); + // For whatever reason the core may report there being a virtual path as the file path. + // In the case where that occurs, we wish not to report there being one to the user. + match path.is_empty() || path == self.file_path() { + true => None, + false => Some(path), + } + } + } + + /// Sets the non-filesystem path that describes how this file was derived from the container + /// transform system. + pub fn set_virtual_path(&self, path: &str) { + let path = path.to_cstr(); + unsafe { + BNSetVirtualPath(self.handle, path.as_ptr()); + } + } + + pub fn is_modified(&self) -> bool { unsafe { BNIsFileModified(self.handle) } } @@ -372,9 +477,10 @@ impl FileMetadata { impl Debug for FileMetadata { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("FileMetadata") - .field("filename", &self.filename()) + .field("file_path", &self.file_path()) + .field("display_name", &self.display_name()) .field("session_id", &self.session_id()) - .field("modified", &self.modified()) + .field("is_modified", &self.is_modified()) .field("is_analysis_changed", &self.is_analysis_changed()) .field("current_view_type", &self.current_view()) .field("current_offset", &self.current_offset()) @@ -385,7 +491,7 @@ impl Debug for FileMetadata { impl Display for FileMetadata { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.filename()) + f.write_str(&self.display_name()) } } diff --git a/rust/src/qualified_name.rs b/rust/src/qualified_name.rs index 62da86b859..aed045a06c 100644 --- a/rust/src/qualified_name.rs +++ b/rust/src/qualified_name.rs @@ -193,6 +193,15 @@ impl From for QualifiedName { } } +impl From for QualifiedName { + fn from(value: BnString) -> Self { + Self { + items: vec![value.to_string_lossy().to_string()], + separator: String::from("::"), + } + } +} + impl From<&str> for QualifiedName { fn from(value: &str) -> Self { Self::from(value.to_string()) diff --git a/rust/src/symbol.rs b/rust/src/symbol.rs index 4239dd4d70..6c368f7c5f 100644 --- a/rust/src/symbol.rs +++ b/rust/src/symbol.rs @@ -267,6 +267,16 @@ impl Symbol { unsafe { BNGetSymbolAddress(self.handle) } } + /// Get the symbols ordinal, this will return `None` if the symbol ordinal is `0`. + pub fn ordinal(&self) -> Option { + let ordinal = unsafe { BNGetSymbolOrdinal(self.handle) }; + if ordinal == u64::MIN { + None + } else { + Some(ordinal) + } + } + pub fn auto_defined(&self) -> bool { unsafe { BNIsSymbolAutoDefined(self.handle) } } diff --git a/rust/src/types.rs b/rust/src/types.rs index 4f6f82d2ee..2528237fb9 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -123,6 +123,22 @@ impl TypeBuilder { self } + /// Set the width of the type. + /// + /// Typically only done for named type references, which will not have their width set otherwise. + pub fn set_width(&self, width: usize) -> &Self { + unsafe { BNTypeBuilderSetWidth(self.handle, width) } + self + } + + /// Set the alignment of the type. + /// + /// Typically only done for named type references, which will not have their alignment set otherwise. + pub fn set_alignment(&self, alignment: usize) -> &Self { + unsafe { BNTypeBuilderSetAlignment(self.handle, alignment) } + self + } + pub fn set_pointer_base(&self, base_type: PointerBaseType, base_offset: i64) -> &Self { unsafe { BNSetTypeBuilderPointerBase(self.handle, base_type, base_offset) } self @@ -391,7 +407,7 @@ impl TypeBuilder { } /// Create a named type reference [`TypeBuilder`]. Analogous to [`Type::named_type`]. - pub fn named_type(type_reference: NamedTypeReference) -> Self { + pub fn named_type(type_reference: &NamedTypeReference) -> Self { let mut is_const = Conf::new(false, MIN_CONFIDENCE).into(); let mut is_volatile = Conf::new(false, MIN_CONFIDENCE).into(); unsafe { @@ -541,6 +557,7 @@ impl Type { // TODO: We need to decide on a public type to represent type width. // TODO: The api uses both `u64` and `usize`, pick one or a new type! + /// The size of the type in bytes. pub fn width(&self) -> u64 { unsafe { BNGetTypeWidth(self.handle) } } diff --git a/rust/src/types/library.rs b/rust/src/types/library.rs index 89f5e48f43..d1a0274039 100644 --- a/rust/src/types/library.rs +++ b/rust/src/types/library.rs @@ -363,3 +363,6 @@ unsafe impl CoreArrayProviderInner for TypeLibrary { Guard::new(Self::from_raw(NonNull::new(*raw).unwrap()), context) } } + +unsafe impl Send for TypeLibrary {} +unsafe impl Sync for TypeLibrary {} diff --git a/rust/src/types/parser.rs b/rust/src/types/parser.rs index 1ee071882c..9bcdf41aa9 100644 --- a/rust/src/types/parser.rs +++ b/rust/src/types/parser.rs @@ -2,6 +2,7 @@ use binaryninjacore_sys::*; use std::ffi::{c_char, c_void}; use std::fmt::Debug; +use std::path::PathBuf; use std::ptr::NonNull; use crate::platform::Platform; @@ -83,10 +84,18 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_directories: &[PathBuf], ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); + let options: Vec<_> = options.iter().map(|o| o.to_cstr()).collect(); + let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); + let include_directories: Vec<_> = include_directories + .iter() + .map(|d| d.clone().to_cstr()) + .collect(); + let include_directories_raw: Vec<*const c_char> = + include_directories.iter().map(|d| d.as_ptr()).collect(); let mut result = std::ptr::null_mut(); let mut errors = std::ptr::null_mut(); let mut error_count = 0; @@ -97,10 +106,10 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options.as_ptr() as *const *const c_char, - options.len(), - include_dirs.as_ptr() as *const *const c_char, - include_dirs.len(), + options_raw.as_ptr(), + options_raw.len(), + include_directories_raw.as_ptr(), + include_directories_raw.len(), &mut result, &mut errors, &mut error_count, @@ -123,11 +132,19 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_directories: &[PathBuf], auto_type_source: &str, ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); + let options: Vec<_> = options.iter().map(|o| o.to_cstr()).collect(); + let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); + let include_directories: Vec<_> = include_directories + .iter() + .map(|d| d.clone().to_cstr()) + .collect(); + let include_directories_raw: Vec<*const c_char> = + include_directories.iter().map(|d| d.as_ptr()).collect(); let auto_type_source = BnString::new(auto_type_source); let mut raw_result = BNTypeParserResult::default(); let mut errors = std::ptr::null_mut(); @@ -139,10 +156,10 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options.as_ptr() as *const *const c_char, - options.len(), - include_dirs.as_ptr() as *const *const c_char, - include_dirs.len(), + options_raw.as_ptr(), + options_raw.len(), + include_directories_raw.as_ptr(), + include_directories_raw.len(), auto_type_source.as_ptr(), &mut raw_result, &mut errors, @@ -222,7 +239,7 @@ pub trait TypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_dirs: &[PathBuf], ) -> Result>; /// Parse an entire block of source into types, variables, and functions @@ -241,7 +258,7 @@ pub trait TypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_dirs: &[PathBuf], auto_type_source: &str, ) -> Result>; @@ -540,7 +557,7 @@ unsafe extern "C" fn cb_preprocess_source( let includes_raw = unsafe { std::slice::from_raw_parts(include_dirs, include_dir_count) }; let includes: Vec<_> = includes_raw .iter() - .filter_map(|&r| raw_to_string(r)) + .filter_map(|&r| Some(PathBuf::from(raw_to_string(r)?))) .collect(); match ctxt.preprocess_source( &raw_to_string(source).unwrap(), @@ -600,7 +617,7 @@ unsafe extern "C" fn cb_parse_types_from_source( let includes_raw = unsafe { std::slice::from_raw_parts(include_dirs, include_dir_count) }; let includes: Vec<_> = includes_raw .iter() - .filter_map(|&r| raw_to_string(r)) + .filter_map(|&r| Some(PathBuf::from(raw_to_string(r)?))) .collect(); match ctxt.parse_types_from_source( &raw_to_string(source).unwrap(), diff --git a/view/minidump/Cargo.toml b/view/minidump/Cargo.toml index a39e9172a5..adca858006 100644 --- a/view/minidump/Cargo.toml +++ b/view/minidump/Cargo.toml @@ -3,6 +3,7 @@ name = "minidump_bn" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"]