+15
| --- | ||
| codecov: | ||
| notify: | ||
| after_n_builds: 1 | ||
| require_ci_to_pass: false | ||
| coverage: | ||
| precision: 2 | ||
| round: down | ||
| range: 50..75 | ||
| comment: | ||
| layout: "header, diff" | ||
| behavior: default | ||
| require_changes: false |
| branch: true | ||
| llvm: true | ||
| ignore-not-existing: true | ||
| filter: covered | ||
| output-type: lcov | ||
| output-path: ./lcov.info | ||
| prefix-dir: /home/user/build/ |
| name: ci | ||
| on: | ||
| pull_request: {} | ||
| push: | ||
| branches: | ||
| - master | ||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v2 | ||
| - name: Generate lockfile | ||
| run: cargo generate-lockfile | ||
| - name: Setup Cache | ||
| uses: actions/cache@v2 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | ||
| - run: cargo build --all-features | ||
| test-unit: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v2 | ||
| - name: Select Nighly Toolchain | ||
| uses: actions-rs/toolchain@v1 | ||
| with: | ||
| toolchain: nightly | ||
| override: true | ||
| - name: Install rustfmt | ||
| shell: bash | ||
| run: rustup component add rustfmt | ||
| - name: Unit tests with all features | ||
| uses: actions-rs/cargo@v1 | ||
| with: | ||
| command: test | ||
| args: --all-features --no-fail-fast | ||
| env: | ||
| CARGO_INCREMENTAL: '0' | ||
| RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' | ||
| RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' | ||
| - name: Coverage | ||
| uses: actions-rs/grcov@v0.1 | ||
| with: | ||
| config: .github/grcov.yml | ||
| - name: Upload Results | ||
| uses: codecov/codecov-action@v2 |
+467
| # This file is automatically @generated by Cargo. | ||
| # It is not intended for manual editing. | ||
| version = 4 | ||
| [[package]] | ||
| name = "ansi_term" | ||
| version = "0.12.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" | ||
| dependencies = [ | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "atty" | ||
| version = "0.2.14" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" | ||
| dependencies = [ | ||
| "hermit-abi", | ||
| "libc", | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "autocfg" | ||
| version = "0.1.8" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" | ||
| dependencies = [ | ||
| "autocfg 1.5.0", | ||
| ] | ||
| [[package]] | ||
| name = "autocfg" | ||
| version = "1.5.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" | ||
| [[package]] | ||
| name = "bitflags" | ||
| version = "1.3.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | ||
| [[package]] | ||
| name = "block-buffer" | ||
| version = "0.10.4" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||
| dependencies = [ | ||
| "generic-array", | ||
| ] | ||
| [[package]] | ||
| name = "cfg-if" | ||
| version = "1.0.4" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | ||
| [[package]] | ||
| name = "clap" | ||
| version = "2.34.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" | ||
| dependencies = [ | ||
| "ansi_term", | ||
| "atty", | ||
| "bitflags", | ||
| "strsim", | ||
| "textwrap", | ||
| "unicode-width 0.1.14", | ||
| "vec_map", | ||
| ] | ||
| [[package]] | ||
| name = "cloudabi" | ||
| version = "0.0.3" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" | ||
| dependencies = [ | ||
| "bitflags", | ||
| ] | ||
| [[package]] | ||
| name = "console" | ||
| version = "0.16.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" | ||
| dependencies = [ | ||
| "encode_unicode", | ||
| "libc", | ||
| "once_cell", | ||
| "unicode-width 0.2.2", | ||
| "windows-sys", | ||
| ] | ||
| [[package]] | ||
| name = "cpufeatures" | ||
| version = "0.2.17" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | ||
| dependencies = [ | ||
| "libc", | ||
| ] | ||
| [[package]] | ||
| name = "crypto-common" | ||
| version = "0.1.7" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | ||
| dependencies = [ | ||
| "generic-array", | ||
| "typenum", | ||
| ] | ||
| [[package]] | ||
| name = "dialoguer" | ||
| version = "0.3.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "1ad1c29a0368928e78c551354dbff79f103a962ad820519724ef0d74f1c62fa9" | ||
| dependencies = [ | ||
| "console", | ||
| "lazy_static", | ||
| "tempfile", | ||
| ] | ||
| [[package]] | ||
| name = "digest" | ||
| version = "0.10.7" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||
| dependencies = [ | ||
| "block-buffer", | ||
| "crypto-common", | ||
| ] | ||
| [[package]] | ||
| name = "encode_unicode" | ||
| version = "1.0.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" | ||
| [[package]] | ||
| name = "fuchsia-cprng" | ||
| version = "0.1.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" | ||
| [[package]] | ||
| name = "generic-array" | ||
| version = "0.14.7" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | ||
| dependencies = [ | ||
| "typenum", | ||
| "version_check", | ||
| ] | ||
| [[package]] | ||
| name = "hermit-abi" | ||
| version = "0.1.19" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" | ||
| dependencies = [ | ||
| "libc", | ||
| ] | ||
| [[package]] | ||
| name = "kernel32-sys" | ||
| version = "0.2.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" | ||
| dependencies = [ | ||
| "winapi 0.2.8", | ||
| "winapi-build", | ||
| ] | ||
| [[package]] | ||
| name = "lazy_static" | ||
| version = "1.5.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" | ||
| [[package]] | ||
| name = "libc" | ||
| version = "0.2.180" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" | ||
| [[package]] | ||
| name = "once_cell" | ||
| version = "1.21.3" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | ||
| [[package]] | ||
| name = "rand" | ||
| version = "0.3.23" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" | ||
| dependencies = [ | ||
| "libc", | ||
| "rand 0.4.6", | ||
| ] | ||
| [[package]] | ||
| name = "rand" | ||
| version = "0.4.6" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" | ||
| dependencies = [ | ||
| "fuchsia-cprng", | ||
| "libc", | ||
| "rand_core 0.3.1", | ||
| "rdrand", | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "rand" | ||
| version = "0.6.5" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" | ||
| dependencies = [ | ||
| "autocfg 0.1.8", | ||
| "libc", | ||
| "rand_chacha", | ||
| "rand_core 0.4.2", | ||
| "rand_hc", | ||
| "rand_isaac", | ||
| "rand_jitter", | ||
| "rand_os", | ||
| "rand_pcg", | ||
| "rand_xorshift", | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "rand_chacha" | ||
| version = "0.1.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" | ||
| dependencies = [ | ||
| "autocfg 0.1.8", | ||
| "rand_core 0.3.1", | ||
| ] | ||
| [[package]] | ||
| name = "rand_core" | ||
| version = "0.3.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" | ||
| dependencies = [ | ||
| "rand_core 0.4.2", | ||
| ] | ||
| [[package]] | ||
| name = "rand_core" | ||
| version = "0.4.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" | ||
| [[package]] | ||
| name = "rand_hc" | ||
| version = "0.1.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" | ||
| dependencies = [ | ||
| "rand_core 0.3.1", | ||
| ] | ||
| [[package]] | ||
| name = "rand_isaac" | ||
| version = "0.1.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" | ||
| dependencies = [ | ||
| "rand_core 0.3.1", | ||
| ] | ||
| [[package]] | ||
| name = "rand_jitter" | ||
| version = "0.1.4" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" | ||
| dependencies = [ | ||
| "libc", | ||
| "rand_core 0.4.2", | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "rand_os" | ||
| version = "0.1.3" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" | ||
| dependencies = [ | ||
| "cloudabi", | ||
| "fuchsia-cprng", | ||
| "libc", | ||
| "rand_core 0.4.2", | ||
| "rdrand", | ||
| "winapi 0.3.9", | ||
| ] | ||
| [[package]] | ||
| name = "rand_pcg" | ||
| version = "0.1.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" | ||
| dependencies = [ | ||
| "autocfg 0.1.8", | ||
| "rand_core 0.4.2", | ||
| ] | ||
| [[package]] | ||
| name = "rand_xorshift" | ||
| version = "0.1.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" | ||
| dependencies = [ | ||
| "rand_core 0.3.1", | ||
| ] | ||
| [[package]] | ||
| name = "rdrand" | ||
| version = "0.4.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" | ||
| dependencies = [ | ||
| "rand_core 0.3.1", | ||
| ] | ||
| [[package]] | ||
| name = "redox_syscall" | ||
| version = "0.1.57" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" | ||
| [[package]] | ||
| name = "rshc" | ||
| version = "0.2.0" | ||
| dependencies = [ | ||
| "clap", | ||
| "dialoguer", | ||
| "rand 0.6.5", | ||
| "sha2", | ||
| ] | ||
| [[package]] | ||
| name = "sha2" | ||
| version = "0.10.9" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | ||
| dependencies = [ | ||
| "cfg-if", | ||
| "cpufeatures", | ||
| "digest", | ||
| ] | ||
| [[package]] | ||
| name = "strsim" | ||
| version = "0.8.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" | ||
| [[package]] | ||
| name = "tempfile" | ||
| version = "2.2.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "11ce2fe9db64b842314052e2421ac61a73ce41b898dc8e3750398b219c5fc1e0" | ||
| dependencies = [ | ||
| "kernel32-sys", | ||
| "libc", | ||
| "rand 0.3.23", | ||
| "redox_syscall", | ||
| "winapi 0.2.8", | ||
| ] | ||
| [[package]] | ||
| name = "textwrap" | ||
| version = "0.11.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" | ||
| dependencies = [ | ||
| "unicode-width 0.1.14", | ||
| ] | ||
| [[package]] | ||
| name = "typenum" | ||
| version = "1.19.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" | ||
| [[package]] | ||
| name = "unicode-width" | ||
| version = "0.1.14" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" | ||
| [[package]] | ||
| name = "unicode-width" | ||
| version = "0.2.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" | ||
| [[package]] | ||
| name = "vec_map" | ||
| version = "0.8.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" | ||
| [[package]] | ||
| name = "version_check" | ||
| version = "0.9.5" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | ||
| [[package]] | ||
| name = "winapi" | ||
| version = "0.2.8" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" | ||
| [[package]] | ||
| name = "winapi" | ||
| version = "0.3.9" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" | ||
| dependencies = [ | ||
| "winapi-i686-pc-windows-gnu", | ||
| "winapi-x86_64-pc-windows-gnu", | ||
| ] | ||
| [[package]] | ||
| name = "winapi-build" | ||
| version = "0.1.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" | ||
| [[package]] | ||
| name = "winapi-i686-pc-windows-gnu" | ||
| version = "0.4.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | ||
| [[package]] | ||
| name = "winapi-x86_64-pc-windows-gnu" | ||
| version = "0.4.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | ||
| [[package]] | ||
| name = "windows-link" | ||
| version = "0.2.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | ||
| [[package]] | ||
| name = "windows-sys" | ||
| version = "0.61.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | ||
| dependencies = [ | ||
| "windows-link", | ||
| ] |
| { | ||
| "git": { | ||
| "sha1": "dee69082700053cbfd6e00049cf8008544a82236" | ||
| } | ||
| } | ||
| "sha1": "a099eff25143a4a9241071b407414353abd8c04d" | ||
| }, | ||
| "path_in_vcs": "" | ||
| } |
+26
-9
@@ -6,8 +6,7 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| # with all versions of Cargo and also rewrite `path` dependencies | ||
| # to registry (e.g. crates.io) dependencies | ||
| # to registry (e.g., crates.io) dependencies. | ||
| # | ||
| # If you believe there's an error in this file please file an | ||
| # issue against the rust-lang/cargo repository. If you're | ||
| # editing this file be aware that the upstream Cargo.toml | ||
| # will likely look very different (and much more reasonable) | ||
| # If you are reading this file be aware that the original Cargo.toml | ||
| # will likely look very different (and much more reasonable). | ||
| # See Cargo.toml.orig for the original contents. | ||
@@ -17,10 +16,25 @@ [package] | ||
| name = "rshc" | ||
| version = "0.1.3" | ||
| version = "0.2.0" | ||
| authors = ["Yukang <moorekang@gmail.com>"] | ||
| build = false | ||
| autolib = false | ||
| autobins = false | ||
| autoexamples = false | ||
| autotests = false | ||
| autobenches = false | ||
| description = "rshc: Rust compile shell script(or expect script) to Rust code and binary." | ||
| homepage = "https://github.com/chenyukang/rshc" | ||
| readme = "README.md" | ||
| keywords = ["shell", "binary", "script"] | ||
| keywords = [ | ||
| "shell", | ||
| "binary", | ||
| "script", | ||
| ] | ||
| license = "MIT" | ||
| repository = "https://github.com/chenyukang/rshc" | ||
| [[bin]] | ||
| name = "rshc" | ||
| path = "src/main.rs" | ||
| [dependencies.clap] | ||
@@ -32,3 +46,6 @@ version = "2" | ||
| [dependencies.rust-crypto] | ||
| version = "^0.2" | ||
| [dependencies.rand] | ||
| version = "0.6.1" | ||
| [dependencies.sha2] | ||
| version = "0.10" |
+1
-1
| #!/bin/ruby | ||
| puts ARGV.inspect | ||
| puts ARGV.length | ||
+65
-1
| # rshc | ||
| [](https://travis-ci.com/chenyukang/rshc) | ||
| [](https://github.com/chenyukang/rshc/actions/workflows/ci.yml) | ||
| [](https://codecov.io/gh/chenyukang/rshc) | ||
| rshc: Compile shell script(or expect script) to Rust code and binary. | ||
@@ -51,1 +53,63 @@ | ||
| ``` | ||
| ## Security Hardening | ||
| rshc applies multiple layers of security hardening to protect compiled scripts from reverse engineering and tampering. | ||
| ### 1. RC4 Stream Cipher Encryption | ||
| The script source code is encrypted using the **RC4 (Arc4)** stream cipher with a randomly generated 128-character key. The encrypted script is embedded in the binary as a byte array and only decrypted at runtime immediately before execution. | ||
| ### 2. Key Obfuscation (XOR Mask) | ||
| The RC4 encryption key is never stored in plaintext. Instead, it is split into two byte arrays — `key_mask` (random bytes) and `key_masked` (key XOR mask). At runtime, the original key is reconstructed via `key = key_mask XOR key_masked`. This prevents extraction of the encryption key through `strings` or hex editors. | ||
| ### 3. Secure Script Execution via Stdin Pipe | ||
| Instead of passing the decrypted script as a command-line argument (which would expose it in `ps aux` and `/proc/*/cmdline`), the binary writes the script to the interpreter's **stdin** via `Stdio::piped()`. Process listings only show the interpreter name (e.g., `bash -s`), not the script content. | ||
| ### 4. SHA-256 Password Hashing with Salt | ||
| When password protection is enabled (`-p` flag), the password is **never stored in the binary**. Instead, a random 16-byte salt is generated, and the SHA-256 hash of `password + salt` is stored. At runtime, the user's input is hashed with the same salt and compared. This makes rainbow table attacks infeasible. The SHA-256 implementation in the generated binary is self-contained (no external crate dependency). | ||
| ### 5. Password Input Masking | ||
| Password input uses **termios raw mode** to disable terminal echo. Each character typed displays an asterisk (`*`), and backspace is supported. Ctrl-C restores the terminal state before exiting. This prevents shoulder-surfing and terminal history leakage. | ||
| ### 6. Symbol Stripping and Size Optimization | ||
| Generated binaries are compiled with `rustc -C strip=symbols -C opt-level=z`, which removes the symbol table and debug information, and optimizes for minimal binary size. This significantly raises the difficulty of static analysis and disassembly. | ||
| ### 7. Interpreter String Encryption | ||
| Interpreter names (e.g., `bash`, `ruby`, `python`, `expect`) are **XOR-encoded** with a random mask byte at compile time. At runtime, an `obf_decode()` function reconstructs the strings. This prevents identification of the target interpreter through `strings` analysis of the binary. | ||
| ### 8. Error Message Obfuscation | ||
| All user-facing strings (e.g., password prompts, error messages) and internal interpreter comparison strings are stored as **XOR-encoded byte arrays** with fixed masks. The `.expect()` calls are replaced with silent `exit(1)` on failure, preventing error messages from leaking implementation details. | ||
| ### 9. Memory Zeroing (Volatile Writes) | ||
| All sensitive data is **securely zeroed** after use with `std::ptr::write_volatile()` to prevent compiler optimization from eliding the zeroing. This covers: | ||
| - User password input and hash intermediate data | ||
| - Reconstructed RC4 key and its XOR components (`key_mask`, `key_masked`) | ||
| - RC4 cipher internal state (256-byte S-box, index pointers) | ||
| - Decrypted script content (both `Vec<u8>` and `String` forms) | ||
| ### 10. Anti-Debugging Detection | ||
| The generated binary runs a multi-layered debugger detection routine **before any decryption** occurs: | ||
| | Check | Platform | Mechanism | | ||
| |-------|----------|-----------| | ||
| | `DYLD_INSERT_LIBRARIES` / `LD_PRELOAD` | macOS + Linux | Detects dynamic library injection / function hooking | | ||
| | `ptrace(PT_DENY_ATTACH)` | macOS | Prevents debuggers from attaching to the process | | ||
| | `sysctl` P_TRACED flag | macOS | Queries the kernel for the process's traced status | | ||
| | `ptrace(PTRACE_TRACEME)` | Linux | Fails if another debugger is already attached | | ||
| | `/proc/self/status` TracerPid | Linux | Reads the tracer PID from the proc filesystem | | ||
| All detections exit silently with code 1, leaking no information about why execution was refused. | ||
| ### 11. Binary Self-Checksum (Integrity Verification) | ||
| After compilation, the **SHA-256 hash** of the entire binary is computed and appended to the file (32 bytes). At runtime, the binary reads itself via `std::env::current_exe()`, splits off the last 32 bytes as the expected checksum, and recomputes the hash of the remaining content. If the checksums do not match (indicating patching, injection, or corruption), the binary exits silently. This protects against post-compilation binary patching attacks. |
+1
-1
@@ -60,3 +60,3 @@ extern crate clap; | ||
| }; | ||
| util::gen_and_compile(&file, &rs_file, &pass); | ||
| util::gen_and_compile(&file, &rs_file, &pass).unwrap(); | ||
| } |
+762
-46
@@ -1,3 +0,2 @@ | ||
| use crypto::rc4::Rc4; | ||
| use crypto::symmetriccipher::SynchronousStreamCipher; | ||
| use std::error::Error; | ||
| use std::fs; | ||
@@ -8,25 +7,33 @@ use std::fs::File; | ||
| use std::process::Command; | ||
| use sha2::{Sha256, Digest}; | ||
| mod template; | ||
| fn encode(input: String, key: String) -> Vec<u8> { | ||
| return encode_vec(input.as_bytes().to_vec(), key); | ||
| #[cfg(debug_assertions)] | ||
| fn rand_string(_len: u32) -> String { | ||
| String::from("rand_string_in_test_cfg") | ||
| } | ||
| fn encode_vec(input: Vec<u8>, key: String) -> Vec<u8> { | ||
| let mut rc4 = Rc4::new(key.as_bytes()); | ||
| let mut output: Vec<u8> = repeat(0).take(input.len()).collect(); | ||
| rc4.process(&input, &mut output); | ||
| return output.to_vec(); | ||
| #[cfg(not(debug_assertions))] | ||
| fn rand_string(len: u32) -> String { | ||
| const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ | ||
| abcdefghijklmnopqrstuvwxyz\ | ||
| 0123456789"; | ||
| return (0..len) | ||
| .map(|_| { | ||
| let i = rand::random::<usize>() % CHARSET.len(); | ||
| CHARSET[i] as char | ||
| }) | ||
| .collect(); | ||
| } | ||
| pub fn find_interp(content: &String) -> (String, String) { | ||
| fn find_interp(content: &str) -> (String, String) { | ||
| if content.starts_with("#!") { | ||
| let lines: Vec<&str> = content.split("\n").collect(); | ||
| let first: Vec<&str> = lines[0].split(" ").collect(); | ||
| if first.len() < 2 { | ||
| let lines: Vec<&str> = content.split('\n').collect(); | ||
| let first: Vec<&str> = lines[0].trim().split(' ').collect(); | ||
| if first.is_empty() { | ||
| (String::from("bash"), content.to_owned()) | ||
| } else { | ||
| let interp = String::from( | ||
| first[first.len() - 2] | ||
| .split("/") | ||
| first[0] | ||
| .split('/') | ||
| .collect::<Vec<&str>>() | ||
@@ -44,6 +51,10 @@ .last() | ||
| fn compile_it(file: &String) { | ||
| fn compile_it(file: &str) { | ||
| println!("compile it ... {}", file); | ||
| let bin_path = file.replace(".rs", ""); | ||
| let output = Command::new("rustc") | ||
| .arg(file) | ||
| .arg("-o").arg(&bin_path) | ||
| .arg("-C").arg("strip=symbols") | ||
| .arg("-C").arg("opt-level=z") | ||
| .output() | ||
@@ -54,12 +65,21 @@ .expect("failed to compile"); | ||
| let stderr = output.stderr; | ||
| if stdout.len() > 0 { | ||
| if !stdout.is_empty() { | ||
| println!("{}", String::from_utf8_lossy(&stdout)); | ||
| } | ||
| if stderr.len() > 0 { | ||
| if !stderr.is_empty() { | ||
| println!("{}", String::from_utf8_lossy(&stderr)); | ||
| } | ||
| if output.status.success() { | ||
| // Append self-checksum: SHA-256 of the binary appended to its end | ||
| let bin_data = fs::read(&bin_path).expect("failed to read compiled binary"); | ||
| let hash = sha256(&bin_data); | ||
| let mut f = fs::OpenOptions::new() | ||
| .append(true) | ||
| .open(&bin_path) | ||
| .expect("failed to open binary for checksum append"); | ||
| f.write_all(&hash).expect("failed to append checksum"); | ||
| println!( | ||
| "compiled success, try it with: ./{}", | ||
| file.replace(".rs", "") | ||
| bin_path | ||
| ); | ||
@@ -71,21 +91,120 @@ } else { | ||
| pub fn gen_and_compile(file: &str, rs_file: &str, pass: &str) { | ||
| let content = fs::read_to_string(file).expect("Failed to read source file"); | ||
| // we need to encode it latter | ||
| let _encoded = encode(content.clone(), "hello".to_string()); | ||
| let (interp, content) = find_interp(&content); | ||
| //println!("{}", content); | ||
| let encoded_str = format!("vec!{:?}", content.as_bytes()); | ||
| #[cfg(debug_assertions)] | ||
| fn rand_bytes(len: usize) -> Vec<u8> { | ||
| vec![0x42; len] | ||
| } | ||
| #[cfg(not(debug_assertions))] | ||
| fn rand_bytes(len: usize) -> Vec<u8> { | ||
| (0..len).map(|_| rand::random::<u8>()).collect() | ||
| } | ||
| fn sha256(data: &[u8]) -> [u8; 32] { | ||
| let mut hasher = Sha256::new(); | ||
| hasher.update(data); | ||
| hasher.finalize().into() | ||
| } | ||
| pub fn gen_and_compile(file: &str, rs_file: &str, pass: &str) -> Result<(), Box<dyn Error>> { | ||
| let source = fs::read_to_string(file).expect("Failed to read source file"); | ||
| let (interp, striped) = find_interp(&source); | ||
| let rand_key = rand_string(128); | ||
| let encoded_vec = Arc4::new(rand_key.as_bytes()).trans_str(&striped); | ||
| let encoded_str = format!("vec!{:?}", encoded_vec); | ||
| // Key obfuscation: split key into key_mask XOR key_masked | ||
| let key_bytes = rand_key.as_bytes(); | ||
| let key_mask = rand_bytes(key_bytes.len()); | ||
| let key_masked: Vec<u8> = key_bytes | ||
| .iter() | ||
| .zip(key_mask.iter()) | ||
| .map(|(k, m)| k ^ m) | ||
| .collect(); | ||
| let key_mask_str = format!("vec!{:?}", key_mask); | ||
| let key_masked_str = format!("vec!{:?}", key_masked); | ||
| // Password protection: store salted SHA-256 hash instead of the password itself | ||
| let (pass_salt, pass_hash) = if !pass.is_empty() { | ||
| let salt = rand_bytes(16); | ||
| let mut data = Vec::new(); | ||
| data.extend_from_slice(pass.as_bytes()); | ||
| data.extend_from_slice(&salt); | ||
| let hash = sha256(&data); | ||
| (salt, hash.to_vec()) | ||
| } else { | ||
| (vec![], vec![]) | ||
| }; | ||
| let pass_salt_str = format!("vec!{:?}", pass_salt); | ||
| let pass_hash_str = format!("vec!{:?}", pass_hash); | ||
| // Interpreter string obfuscation: XOR with random mask byte | ||
| let interp_mask_byte = rand_bytes(1)[0] | 1; // ensure non-zero | ||
| let interp_enc: Vec<u8> = interp.as_bytes().iter().map(|b| b ^ interp_mask_byte).collect(); | ||
| let interp_enc_str = format!("vec!{:?}", interp_enc); | ||
| let prog = template::prog() | ||
| .replace("{ script_code }", &encoded_str) | ||
| .replace("{ pass }", &pass) | ||
| .replace("{ interp }", &interp); | ||
| .replace("{ key_mask }", &key_mask_str) | ||
| .replace("{ key_masked }", &key_masked_str) | ||
| .replace("{ pass_salt }", &pass_salt_str) | ||
| .replace("{ pass_hash }", &pass_hash_str) | ||
| .replace("{ interp_enc }", &interp_enc_str) | ||
| .replace("{ interp_mask }", &format!("0x{:02x}", interp_mask_byte)); | ||
| File::create(rs_file) | ||
| .unwrap() | ||
| .write_all(prog.as_bytes()) | ||
| .unwrap(); | ||
| File::create(rs_file)?.write_all(prog.as_bytes())?; | ||
| compile_it(&rs_file.to_string()); | ||
| Ok(()) | ||
| } | ||
| pub struct Arc4 { | ||
| i: u8, | ||
| j: u8, | ||
| state: [u8; 256], | ||
| } | ||
| impl Arc4 { | ||
| pub fn new(key: &[u8]) -> Arc4 { | ||
| assert!(!key.is_empty() && key.len() <= 256); | ||
| let mut rc4 = Arc4 { | ||
| i: 0, | ||
| j: 0, | ||
| state: [0; 256], | ||
| }; | ||
| for (i, x) in rc4.state.iter_mut().enumerate() { | ||
| *x = i as u8; | ||
| } | ||
| let mut j: u8 = 0; | ||
| for i in 0..256 { | ||
| j = j | ||
| .wrapping_add(rc4.state[i]) | ||
| .wrapping_add(key[i % key.len()]); | ||
| rc4.state.swap(i, j as usize); | ||
| } | ||
| rc4 | ||
| } | ||
| fn next(&mut self) -> u8 { | ||
| self.i = self.i.wrapping_add(1); | ||
| self.j = self.j.wrapping_add(self.state[self.i as usize]); | ||
| self.state.swap(self.i as usize, self.j as usize); | ||
| self.state[(self.state[self.i as usize].wrapping_add(self.state[self.j as usize])) as usize] | ||
| } | ||
| fn encode_vec(&mut self, input: &[u8], output: &mut [u8]) { | ||
| assert!(input.len() == output.len()); | ||
| for (x, y) in input.iter().zip(output.iter_mut()) { | ||
| *y = *x ^ self.next(); | ||
| } | ||
| } | ||
| pub fn trans_vec(&mut self, input: &[u8]) -> Vec<u8> { | ||
| let mut out: Vec<u8> = repeat(0).take(input.len()).collect(); | ||
| self.encode_vec(input, &mut out); | ||
| out.to_vec() | ||
| } | ||
| pub fn trans_str(&mut self, str: &str) -> Vec<u8> { | ||
| self.trans_vec(&str.as_bytes().to_vec()) | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
@@ -114,2 +233,7 @@ mod tests { | ||
| let text = String::from("#!/bin/ruby "); | ||
| let (interp, _) = find_interp(&text); | ||
| println!("interp: {}", interp); | ||
| assert!(interp == "ruby"); | ||
| let text = String::from("send 1 2 3"); | ||
@@ -120,7 +244,8 @@ let (interp, _) = find_interp(&text); | ||
| } | ||
| #[test] | ||
| fn test_encode_decode() { | ||
| let content = String::from("ahah, this is hello world!"); | ||
| let encoded = encode(content.clone(), "hello".to_string()); | ||
| let decoded = encode_vec(encoded, "hello".to_string()); | ||
| let encoded = Arc4::new(b"hello").trans_str(&content.clone()); | ||
| let decoded = Arc4::new(b"hello").trans_vec(&encoded); | ||
| let result = String::from_utf8_lossy(&decoded); | ||
@@ -131,20 +256,27 @@ assert!(result == content); | ||
| #[test] | ||
| fn test_compile_run() { | ||
| let exe = env::current_exe().unwrap(); | ||
| let mut elems: Vec<&str> = exe.to_str().unwrap().split("/").collect(); | ||
| unsafe { | ||
| elems.set_len(elems.len() - 4); | ||
| } | ||
| let path = elems.join("/") + "/examples/"; | ||
| env::set_current_dir(Path::new(&path)).is_ok(); | ||
| let files = fs::read_dir(path.to_owned()).unwrap(); | ||
| fn test_compile_run() -> Result<(), Box<dyn Error>> { | ||
| let dir = env::current_dir()?; | ||
| let path = format!("{}/examples", dir.display()); | ||
| env::set_current_dir(Path::new(&path)).unwrap(); | ||
| let files = fs::read_dir(path.to_owned())?; | ||
| for file in files { | ||
| let p = file.unwrap().path(); | ||
| let s = p.to_str().unwrap(); | ||
| if !s.ends_with(".out") && s.contains(".") { | ||
| // Only compile known script types (.sh, .rb), skip .out, .rs, and other files | ||
| if s.ends_with(".sh") || s.ends_with(".rb") { | ||
| let out = format!("{}.out", s.replace(".", "_")); | ||
| println!("out: {} {}", s, out); | ||
| gen_and_compile(s, &out.to_owned(), ""); | ||
| gen_and_compile(s, &out.to_owned(), "")?; | ||
| } | ||
| } | ||
| let output = Command::new("./7_rb") | ||
| .args(vec!["1", "2", "3"]) | ||
| .output() | ||
| .expect("failed to execute"); | ||
| let out = String::from_utf8_lossy(&output.stdout); | ||
| println!("now out: {}", out); | ||
| assert!(out.trim() == "[\"1\", \"2\", \"3\"]"); | ||
| Ok(()) | ||
| } | ||
@@ -185,6 +317,590 @@ | ||
| for t in tests.iter() { | ||
| let result = encode(t.input.to_string(), t.key.to_string()); | ||
| let result = Arc4::new(t.key.to_string().as_bytes()).trans_str(&t.input.to_string()); | ||
| assert!(result == t.output); | ||
| } | ||
| } | ||
| #[test] | ||
| fn test_key_obfuscation_xor_roundtrip() { | ||
| // Verify that key_mask XOR key_masked correctly reconstructs the original key | ||
| let original_key = b"rand_string_in_test_cfg"; | ||
| let key_mask = rand_bytes(original_key.len()); | ||
| let key_masked: Vec<u8> = original_key | ||
| .iter() | ||
| .zip(key_mask.iter()) | ||
| .map(|(k, m)| k ^ m) | ||
| .collect(); | ||
| // Reconstruct key (same logic as in generated binary) | ||
| let reconstructed: Vec<u8> = key_mask | ||
| .iter() | ||
| .zip(key_masked.iter()) | ||
| .map(|(m, d)| m ^ d) | ||
| .collect(); | ||
| assert_eq!(reconstructed, original_key.to_vec()); | ||
| } | ||
| #[test] | ||
| fn test_key_obfuscation_no_plaintext_in_output() { | ||
| // Verify that the generated .rs file does NOT contain the plaintext key | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/2.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_obf_check.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let generated = fs::read_to_string(&out_rs).unwrap(); | ||
| let key = "rand_string_in_test_cfg"; | ||
| // The plaintext key string should NOT appear as a quoted string in the generated code | ||
| assert!( | ||
| !generated.contains(&format!("\"{}\"", key)), | ||
| "Generated .rs file should not contain the plaintext key as a string literal" | ||
| ); | ||
| // key_mask and key_masked byte arrays should be present instead | ||
| assert!( | ||
| generated.contains("key_mask"), | ||
| "Generated .rs file should contain key_mask" | ||
| ); | ||
| assert!( | ||
| generated.contains("key_masked"), | ||
| "Generated .rs file should contain key_masked" | ||
| ); | ||
| // Clean up | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| #[test] | ||
| fn test_key_obfuscation_decrypt_still_works() { | ||
| // End-to-end: encrypt with original key, then decrypt using XOR-reconstructed key | ||
| let content = "echo hello world"; | ||
| let original_key = "test_key_12345"; | ||
| // Encrypt | ||
| let encrypted = Arc4::new(original_key.as_bytes()).trans_str(&content.to_string()); | ||
| // Simulate obfuscation | ||
| let key_bytes = original_key.as_bytes(); | ||
| let key_mask = rand_bytes(key_bytes.len()); | ||
| let key_masked: Vec<u8> = key_bytes | ||
| .iter() | ||
| .zip(key_mask.iter()) | ||
| .map(|(k, m)| k ^ m) | ||
| .collect(); | ||
| // Reconstruct key and decrypt (same as generated binary does) | ||
| let reconstructed_key: Vec<u8> = key_mask | ||
| .iter() | ||
| .zip(key_masked.iter()) | ||
| .map(|(m, d)| m ^ d) | ||
| .collect(); | ||
| let decrypted = Arc4::new(&reconstructed_key).trans_vec(&encrypted); | ||
| let result = String::from_utf8(decrypted).unwrap(); | ||
| assert_eq!(result, content); | ||
| } | ||
| #[test] | ||
| fn test_sha256_known_vectors() { | ||
| // Test against known SHA-256 test vectors | ||
| // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 | ||
| let empty_hash = sha256(b""); | ||
| assert_eq!( | ||
| empty_hash, | ||
| [ | ||
| 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, | ||
| 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, | ||
| 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, | ||
| ] | ||
| ); | ||
| // SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad | ||
| let abc_hash = sha256(b"abc"); | ||
| assert_eq!( | ||
| abc_hash, | ||
| [ | ||
| 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, | ||
| 0xae, 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, | ||
| 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad, | ||
| ] | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_password_hash_not_in_output() { | ||
| // Verify that the generated .rs file contains hash/salt, not plaintext password | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/2.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_pass_hash_check.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "my_secret_password").unwrap(); | ||
| let generated = fs::read_to_string(&out_rs).unwrap(); | ||
| // Plaintext password must NOT appear anywhere in the generated code | ||
| assert!( | ||
| !generated.contains("my_secret_password"), | ||
| "Generated .rs file must not contain plaintext password" | ||
| ); | ||
| // Should contain hash-based fields instead | ||
| assert!(generated.contains("pass_salt"), "Should contain pass_salt"); | ||
| assert!(generated.contains("pass_hash"), "Should contain pass_hash"); | ||
| // Clean up | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| // ===== find_interp additional tests ===== | ||
| #[test] | ||
| fn test_find_interp_env_shebang() { | ||
| // #!/usr/bin/env python3 — find_interp extracts "env" (last path component) | ||
| // This documents current behavior: env-style shebangs return "env" | ||
| let text = "#!/usr/bin/env python3\nprint('hello')"; | ||
| let (interp, body) = find_interp(text); | ||
| assert_eq!(interp, "env"); | ||
| assert_eq!(body, "print('hello')"); | ||
| } | ||
| #[test] | ||
| fn test_find_interp_strips_shebang_line() { | ||
| // The returned content should NOT include the shebang line itself | ||
| let text = "#!/bin/bash\necho line1\necho line2"; | ||
| let (interp, body) = find_interp(text); | ||
| assert_eq!(interp, "bash"); | ||
| assert_eq!(body, "echo line1\necho line2"); | ||
| assert!(!body.contains("#!"), "Shebang line should be stripped from body"); | ||
| } | ||
| #[test] | ||
| fn test_find_interp_no_shebang_preserves_content() { | ||
| // Without shebang, content should be returned unchanged | ||
| let text = "echo hello\necho world"; | ||
| let (interp, body) = find_interp(text); | ||
| assert_eq!(interp, "bash"); | ||
| assert_eq!(body, text); | ||
| } | ||
| #[test] | ||
| fn test_find_interp_usr_bin_env_bash() { | ||
| // #!/usr/bin/env bash — current behavior extracts "env" | ||
| let text = "#!/usr/bin/env bash\nset -e\necho ok"; | ||
| let (interp, body) = find_interp(text); | ||
| assert_eq!(interp, "env"); | ||
| assert_eq!(body, "set -e\necho ok"); | ||
| } | ||
| // ===== Arc4 additional tests ===== | ||
| #[test] | ||
| fn test_arc4_empty_input() { | ||
| let result = Arc4::new(b"key").trans_str(&String::new()); | ||
| assert!(result.is_empty()); | ||
| } | ||
| #[test] | ||
| fn test_arc4_single_byte() { | ||
| let encrypted = Arc4::new(b"key").trans_str(&String::from("A")); | ||
| assert_eq!(encrypted.len(), 1); | ||
| // Decrypt and verify roundtrip | ||
| let decrypted = Arc4::new(b"key").trans_vec(&encrypted); | ||
| assert_eq!(decrypted, b"A"); | ||
| } | ||
| #[test] | ||
| fn test_arc4_large_data_roundtrip() { | ||
| // Test with a large payload (4KB) | ||
| let content: String = (0..4096).map(|i| (b'A' + (i % 26) as u8) as char).collect(); | ||
| let key = b"a_longer_key_for_testing"; | ||
| let encrypted = Arc4::new(key).trans_str(&content); | ||
| assert_eq!(encrypted.len(), 4096); | ||
| let decrypted = Arc4::new(key).trans_vec(&encrypted); | ||
| assert_eq!(String::from_utf8(decrypted).unwrap(), content); | ||
| } | ||
| #[test] | ||
| fn test_arc4_different_keys_produce_different_output() { | ||
| let plaintext = "same input data"; | ||
| let enc1 = Arc4::new(b"key_alpha").trans_str(&plaintext.to_string()); | ||
| let enc2 = Arc4::new(b"key_beta").trans_str(&plaintext.to_string()); | ||
| assert_ne!(enc1, enc2, "Different keys should produce different ciphertext"); | ||
| } | ||
| #[test] | ||
| fn test_arc4_same_key_same_output() { | ||
| let plaintext = "deterministic output"; | ||
| let enc1 = Arc4::new(b"fixed_key").trans_str(&plaintext.to_string()); | ||
| let enc2 = Arc4::new(b"fixed_key").trans_str(&plaintext.to_string()); | ||
| assert_eq!(enc1, enc2, "Same key should produce identical ciphertext"); | ||
| } | ||
| // ===== SHA-256 additional tests ===== | ||
| #[test] | ||
| fn test_sha256_longer_input() { | ||
| // SHA-256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") | ||
| // = 248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1 | ||
| let hash = sha256(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"); | ||
| assert_eq!( | ||
| hash, | ||
| [ | ||
| 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, 0xe5, 0xc0, 0x26, 0x93, 0x0c, | ||
| 0x3e, 0x60, 0x39, 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67, 0xf6, 0xec, | ||
| 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1, | ||
| ] | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_sha256_with_salt_different_salts() { | ||
| // Same password with different salts should produce different hashes | ||
| let pass = b"password123"; | ||
| let salt1 = vec![1u8; 16]; | ||
| let salt2 = vec![2u8; 16]; | ||
| let mut data1 = pass.to_vec(); | ||
| data1.extend_from_slice(&salt1); | ||
| let hash1 = sha256(&data1); | ||
| let mut data2 = pass.to_vec(); | ||
| data2.extend_from_slice(&salt2); | ||
| let hash2 = sha256(&data2); | ||
| assert_ne!(hash1, hash2, "Different salts should produce different hashes"); | ||
| } | ||
| // ===== gen_and_compile tests ===== | ||
| #[test] | ||
| fn test_generated_binary_runs_correctly() { | ||
| // Compile a simple echo script and verify it produces correct output | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/3.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_gen_run.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| let output = Command::new(&bin_path) | ||
| .args(&["hello", "world"]) | ||
| .output() | ||
| .expect("failed to execute generated binary"); | ||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| assert!(stdout.contains("First arg: hello"), "Got: {}", stdout); | ||
| assert!(stdout.contains("Second arg: world"), "Got: {}", stdout); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| #[test] | ||
| fn test_generated_binary_is_stripped() { | ||
| // Verify the compiled binary has symbols stripped (smaller size, fewer symbols) | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/2.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_strip_check.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| // Check that nm finds very few (or no) symbols | ||
| let nm_output = Command::new("nm") | ||
| .arg(&bin_path) | ||
| .output(); | ||
| match nm_output { | ||
| Ok(output) => { | ||
| let stderr = String::from_utf8_lossy(&output.stderr); | ||
| // On stripped binaries, nm typically reports "no symbols" or very few | ||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| let symbol_count = stdout.lines().count(); | ||
| assert!( | ||
| symbol_count < 200 || stderr.contains("no symbols"), | ||
| "Binary should have few symbols after stripping, got {} symbols", | ||
| symbol_count | ||
| ); | ||
| } | ||
| Err(_) => { | ||
| // nm not available, skip this check | ||
| } | ||
| } | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| #[test] | ||
| fn test_no_password_generates_empty_hash() { | ||
| // When no password is given, pass_salt and pass_hash should be empty vecs | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/2.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_no_pass.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let generated = fs::read_to_string(&out_rs).unwrap(); | ||
| // Empty password should produce empty vec![] for both salt and hash | ||
| assert!( | ||
| generated.contains("let mut pass_salt: Vec<u8> = vec![];"), | ||
| "Empty password should produce empty pass_salt" | ||
| ); | ||
| assert!( | ||
| generated.contains("let pass_hash: Vec<u8> = vec![];"), | ||
| "Empty password should produce empty pass_hash" | ||
| ); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(out_rs.replace(".rs", "")); | ||
| } | ||
| #[test] | ||
| fn test_template_contains_all_placeholders() { | ||
| let tmpl = template::prog(); | ||
| assert!(tmpl.contains("{ script_code }"), "Missing script_code placeholder"); | ||
| assert!(tmpl.contains("{ key_mask }"), "Missing key_mask placeholder"); | ||
| assert!(tmpl.contains("{ key_masked }"), "Missing key_masked placeholder"); | ||
| assert!(tmpl.contains("{ pass_salt }"), "Missing pass_salt placeholder"); | ||
| assert!(tmpl.contains("{ pass_hash }"), "Missing pass_hash placeholder"); | ||
| assert!(tmpl.contains("{ interp_enc }"), "Missing interp_enc placeholder"); | ||
| assert!(tmpl.contains("{ interp_mask }"), "Missing interp_mask placeholder"); | ||
| } | ||
| #[test] | ||
| fn test_template_has_stdin_pipe() { | ||
| // Verify template uses Stdio::piped() instead of passing script as argument | ||
| let tmpl = template::prog(); | ||
| assert!( | ||
| tmpl.contains("Stdio::piped()"), | ||
| "Template should use stdin pipe for security" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("write_all"), | ||
| "Template should write script to stdin" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_template_has_secure_zero() { | ||
| // Verify template includes memory zeroing for sensitive data | ||
| let tmpl = template::prog(); | ||
| assert!( | ||
| tmpl.contains("write_volatile"), | ||
| "Template should use write_volatile for secure zeroing" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("secure_zero"), | ||
| "Template should have secure_zero function" | ||
| ); | ||
| // Verify all sensitive data is zeroed | ||
| assert!( | ||
| tmpl.contains("secure_zero_vec(&mut rand_key)"), | ||
| "RC4 key should be zeroed after use" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("secure_zero_vec(&mut key_mask)"), | ||
| "key_mask should be zeroed after use" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("secure_zero_vec(&mut key_masked)"), | ||
| "key_masked should be zeroed after use" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("cipher.zeroize()"), | ||
| "Arc4 state should be zeroed after decryption" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_generated_file_no_script_plaintext() { | ||
| // Verify the original script content is NOT readable in the generated .rs file | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/3.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_no_plain.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let generated = fs::read_to_string(&out_rs).unwrap(); | ||
| // The original script content should be encrypted, not plaintext | ||
| assert!( | ||
| !generated.contains("First arg:"), | ||
| "Generated file should not contain plaintext script content" | ||
| ); | ||
| assert!( | ||
| !generated.contains("Second arg:"), | ||
| "Generated file should not contain plaintext script content" | ||
| ); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(out_rs.replace(".rs", "")); | ||
| } | ||
| #[test] | ||
| fn test_interp_not_plaintext_in_output() { | ||
| // Verify that interpreter name is XOR-encoded, not plaintext in generated code | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/3.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_interp_obf.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let generated = fs::read_to_string(&out_rs).unwrap(); | ||
| // The plaintext interpreter name should NOT appear as a string literal | ||
| assert!( | ||
| !generated.contains("\"bash\""), | ||
| "Generated file should not contain plaintext interpreter string" | ||
| ); | ||
| // Should use obf_decode instead | ||
| assert!( | ||
| generated.contains("obf_decode"), | ||
| "Generated file should use obf_decode for interpreter" | ||
| ); | ||
| // Error messages should NOT be plaintext | ||
| assert!( | ||
| !generated.contains("\"Password: \""), | ||
| "Generated file should not contain plaintext Password prompt" | ||
| ); | ||
| assert!( | ||
| !generated.contains("\"Invalid password!\""), | ||
| "Generated file should not contain plaintext error message" | ||
| ); | ||
| assert!( | ||
| !generated.contains("\"failed to execute\""), | ||
| "Generated file should not contain plaintext error message" | ||
| ); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(out_rs.replace(".rs", "")); | ||
| } | ||
| #[test] | ||
| fn test_template_has_anti_debug() { | ||
| let tmpl = template::prog(); | ||
| assert!( | ||
| tmpl.contains("detect_debugger()"), | ||
| "Template should call detect_debugger in main" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("PT_DENY_ATTACH"), | ||
| "Template should use PT_DENY_ATTACH on macOS" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("PTRACE_TRACEME"), | ||
| "Template should use PTRACE_TRACEME on Linux" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("P_TRACED"), | ||
| "Template should check P_TRACED sysctl flag" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("TracerPid"), | ||
| "Template should check /proc/self/status TracerPid" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("DYLD_INSERT_LIBRARIES"), | ||
| "Template should detect DYLD_INSERT_LIBRARIES" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("LD_PRELOAD"), | ||
| "Template should detect LD_PRELOAD" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_template_has_verify_integrity() { | ||
| let tmpl = template::prog(); | ||
| assert!( | ||
| tmpl.contains("verify_integrity()"), | ||
| "Template should call verify_integrity in main" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("current_exe"), | ||
| "Template should read own executable path" | ||
| ); | ||
| assert!( | ||
| tmpl.contains("split_at"), | ||
| "Template should split binary to separate checksum" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn test_binary_checksum_appended() { | ||
| // Verify that compiled binary has 32 bytes of SHA-256 appended | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/2.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_checksum.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| let data = fs::read(&bin_path).unwrap(); | ||
| // Binary must be longer than 32 bytes | ||
| assert!(data.len() > 32); | ||
| // Last 32 bytes should be SHA-256 of the preceding content | ||
| let (body, stored_hash) = data.split_at(data.len() - 32); | ||
| let computed = sha256(body); | ||
| assert_eq!( | ||
| &computed[..], stored_hash, | ||
| "Appended checksum should match SHA-256 of binary body" | ||
| ); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| #[test] | ||
| fn test_tampered_binary_exits() { | ||
| // Verify that a tampered binary detects corruption and exits with code 1 | ||
| let manifest_dir = env!("CARGO_MANIFEST_DIR"); | ||
| let script = format!("{}/examples/3.sh", manifest_dir); | ||
| let out_rs = format!("{}/examples/test_tamper.rs", manifest_dir); | ||
| gen_and_compile(&script, &out_rs, "").unwrap(); | ||
| let bin_path = out_rs.replace(".rs", ""); | ||
| // Tamper: flip some bytes in the middle of the binary | ||
| let mut data = fs::read(&bin_path).unwrap(); | ||
| let mid = data.len() / 2; | ||
| data[mid] ^= 0xFF; | ||
| data[mid + 1] ^= 0xFF; | ||
| fs::write(&bin_path, &data).unwrap(); | ||
| // Run tampered binary — should exit with code 1 (integrity check failure) | ||
| let output = Command::new(&bin_path) | ||
| .args(&["hello", "world"]) | ||
| .output() | ||
| .expect("failed to run tampered binary"); | ||
| assert!( | ||
| !output.status.success(), | ||
| "Tampered binary should exit with non-zero status" | ||
| ); | ||
| assert!( | ||
| output.stdout.is_empty(), | ||
| "Tampered binary should produce no output" | ||
| ); | ||
| // Clean up | ||
| let _ = fs::remove_file(&out_rs); | ||
| let _ = fs::remove_file(&bin_path); | ||
| } | ||
| } |
+391
-29
| pub fn prog() -> &'static str { | ||
| let prog = r###" | ||
| r###" | ||
| use std::io; | ||
| use std::io::Read; | ||
| use std::iter::repeat; | ||
| use std::io::Write; | ||
@@ -9,45 +11,405 @@ use std::process; | ||
| fn run_process(iterp: &String, prog: &String, args: &Vec<String>) { | ||
| let prog = format!("{}", prog.to_owned()); | ||
| //println!("{}", prog); | ||
| let opt = if iterp == "ruby" { "-e" } else { "-c" }; | ||
| let output = Command::new(iterp) | ||
| .arg(opt) | ||
| .arg(prog) | ||
| /// Zero out a byte slice using volatile writes to prevent compiler optimization | ||
| fn secure_zero(buf: &mut [u8]) { | ||
| for byte in buf.iter_mut() { | ||
| unsafe { std::ptr::write_volatile(byte, 0); } | ||
| } | ||
| } | ||
| /// Zero out a Vec<u8> and drop it | ||
| fn secure_zero_vec(v: &mut Vec<u8>) { | ||
| secure_zero(v.as_mut_slice()); | ||
| v.clear(); | ||
| } | ||
| /// Zero out a String's underlying buffer and drop it | ||
| fn secure_zero_string(s: &mut String) { | ||
| unsafe { | ||
| secure_zero(s.as_bytes_mut()); | ||
| } | ||
| s.clear(); | ||
| } | ||
| /// Decode XOR-obfuscated byte array at runtime | ||
| fn obf_decode(data: &[u8], mask: u8) -> String { | ||
| String::from_utf8(data.iter().map(|b| b ^ mask).collect()).unwrap_or_default() | ||
| } | ||
| /// Anti-debug: detect debuggers and library injection, exit silently if found | ||
| fn detect_debugger() { | ||
| // 1. Check for injected libraries (common hooking technique) | ||
| for var in &["DYLD_INSERT_LIBRARIES", "LD_PRELOAD"] { | ||
| if std::env::var(var).is_ok() { | ||
| std::process::exit(1); | ||
| } | ||
| } | ||
| // 2. macOS: PT_DENY_ATTACH prevents debugger from attaching | ||
| #[cfg(target_os = "macos")] | ||
| { | ||
| extern "C" { | ||
| fn ptrace(request: i32, pid: i32, addr: *mut u8, data: i32) -> i32; | ||
| } | ||
| const PT_DENY_ATTACH: i32 = 31; | ||
| unsafe { ptrace(PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0); } | ||
| } | ||
| // 3. macOS: sysctl check for P_TRACED flag | ||
| #[cfg(target_os = "macos")] | ||
| { | ||
| extern "C" { | ||
| fn sysctl(name: *const i32, namelen: u32, oldp: *mut u8, oldlenp: *mut usize, newp: *const u8, newlen: usize) -> i32; | ||
| fn getpid() -> i32; | ||
| } | ||
| // CTL_KERN=1, KERN_PROC=14, KERN_PROC_PID=1 | ||
| let mib: [i32; 4] = [1, 14, 1, unsafe { getpid() }]; | ||
| let mut info = [0u8; 752]; // kinfo_proc buffer (oversized for safety) | ||
| let mut size: usize = info.len(); | ||
| let ret = unsafe { | ||
| sysctl(mib.as_ptr(), 4, info.as_mut_ptr(), &mut size, std::ptr::null(), 0) | ||
| }; | ||
| if ret == 0 { | ||
| // kp_proc.p_flag at offset 32 (i32) | ||
| let p_flag = i32::from_ne_bytes([info[32], info[33], info[34], info[35]]); | ||
| const P_TRACED: i32 = 0x00000800; | ||
| if p_flag & P_TRACED != 0 { | ||
| std::process::exit(1); | ||
| } | ||
| } | ||
| } | ||
| // 4. Linux: PTRACE_TRACEME detection | ||
| #[cfg(target_os = "linux")] | ||
| { | ||
| extern "C" { | ||
| fn ptrace(request: u32, pid: u32, addr: *mut u8, data: *mut u8) -> i64; | ||
| } | ||
| const PTRACE_TRACEME: u32 = 0; | ||
| let ret = unsafe { ptrace(PTRACE_TRACEME, 0, std::ptr::null_mut(), std::ptr::null_mut()) }; | ||
| if ret == -1 { | ||
| std::process::exit(1); | ||
| } | ||
| // Detach self so child processes still work | ||
| const PTRACE_DETACH: u32 = 17; | ||
| unsafe { ptrace(PTRACE_DETACH, 0, std::ptr::null_mut(), std::ptr::null_mut()); } | ||
| } | ||
| // 5. Linux: check /proc/self/status for TracerPid | ||
| #[cfg(target_os = "linux")] | ||
| { | ||
| if let Ok(status) = std::fs::read_to_string("/proc/self/status") { | ||
| for line in status.lines() { | ||
| if line.starts_with("TracerPid:") { | ||
| let pid_str = line.trim_start_matches("TracerPid:").trim(); | ||
| if pid_str != "0" { | ||
| std::process::exit(1); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /// Verify binary integrity: the last 32 bytes are SHA-256 of the preceding content | ||
| fn verify_integrity() { | ||
| let exe = match std::env::current_exe() { | ||
| Ok(p) => p, | ||
| Err(_) => return, | ||
| }; | ||
| let data = match std::fs::read(&exe) { | ||
| Ok(d) => d, | ||
| Err(_) => return, | ||
| }; | ||
| if data.len() <= 32 { | ||
| std::process::exit(1); | ||
| } | ||
| let (body, expected) = data.split_at(data.len() - 32); | ||
| let actual = sha256(body); | ||
| if actual[..] != expected[..] { | ||
| std::process::exit(1); | ||
| } | ||
| } | ||
| #[cfg(unix)] | ||
| fn read_password_masked() -> String { | ||
| use std::os::unix::io::AsRawFd; | ||
| #[repr(C)] | ||
| #[derive(Clone)] | ||
| struct Termios { | ||
| c_iflag: u64, | ||
| c_oflag: u64, | ||
| c_cflag: u64, | ||
| c_lflag: u64, | ||
| c_cc: [u8; 20], | ||
| c_ispeed: u64, | ||
| c_ospeed: u64, | ||
| } | ||
| extern "C" { | ||
| fn tcgetattr(fd: i32, termios: *mut Termios) -> i32; | ||
| fn tcsetattr(fd: i32, action: i32, termios: *const Termios) -> i32; | ||
| } | ||
| let stdin_fd = io::stdin().as_raw_fd(); | ||
| let mut orig = unsafe { std::mem::zeroed::<Termios>() }; | ||
| unsafe { tcgetattr(stdin_fd, &mut orig) }; | ||
| let mut raw = orig.clone(); | ||
| // Disable ICANON (canonical mode) and ECHO | ||
| raw.c_lflag &= !(0x00000008 | 0x00000002); // ~(ECHO | ICANON) on macOS/Linux | ||
| // Set VMIN=1, VTIME=0 for reading one byte at a time | ||
| raw.c_cc[16] = 1; // VMIN | ||
| raw.c_cc[17] = 0; // VTIME | ||
| unsafe { tcsetattr(stdin_fd, 0, &raw) }; | ||
| let mut password = String::new(); | ||
| let mut buf = [0u8; 1]; | ||
| loop { | ||
| if io::stdin().read_exact(&mut buf).is_err() { | ||
| break; | ||
| } | ||
| match buf[0] { | ||
| b'\n' | b'\r' => { | ||
| eprint!("\n"); | ||
| break; | ||
| } | ||
| 127 | 8 => { | ||
| // Backspace / Delete | ||
| if !password.is_empty() { | ||
| password.pop(); | ||
| eprint!("\x08 \x08"); | ||
| io::stderr().flush().ok(); | ||
| } | ||
| } | ||
| 3 => { | ||
| // Ctrl-C | ||
| unsafe { tcsetattr(stdin_fd, 0, &orig) }; | ||
| eprint!("\n"); | ||
| process::exit(1); | ||
| } | ||
| c => { | ||
| password.push(c as char); | ||
| eprint!("*"); | ||
| io::stderr().flush().ok(); | ||
| } | ||
| } | ||
| } | ||
| unsafe { tcsetattr(stdin_fd, 0, &orig) }; | ||
| password | ||
| } | ||
| pub struct Arc4 { | ||
| i: u8, | ||
| j: u8, | ||
| state: [u8; 256], | ||
| } | ||
| impl Arc4 { | ||
| pub fn new(key: &[u8]) -> Arc4 { | ||
| assert!(key.len() >= 1 && key.len() <= 256); | ||
| let mut rc4 = Arc4 { | ||
| i: 0, | ||
| j: 0, | ||
| state: [0; 256], | ||
| }; | ||
| for (i, x) in rc4.state.iter_mut().enumerate() { | ||
| *x = i as u8; | ||
| } | ||
| let mut j: u8 = 0; | ||
| for i in 0..256 { | ||
| j = j | ||
| .wrapping_add(rc4.state[i]) | ||
| .wrapping_add(key[i % key.len()]); | ||
| rc4.state.swap(i, j as usize); | ||
| } | ||
| rc4 | ||
| } | ||
| fn next(&mut self) -> u8 { | ||
| self.i = self.i.wrapping_add(1); | ||
| self.j = self.j.wrapping_add(self.state[self.i as usize]); | ||
| self.state.swap(self.i as usize, self.j as usize); | ||
| let k = self.state | ||
| [(self.state[self.i as usize].wrapping_add(self.state[self.j as usize])) as usize]; | ||
| k | ||
| } | ||
| fn encode_vec(&mut self, input: &[u8], output: &mut [u8]) { | ||
| assert!(input.len() == output.len()); | ||
| for (x, y) in input.iter().zip(output.iter_mut()) { | ||
| *y = *x ^ self.next(); | ||
| } | ||
| } | ||
| pub fn trans_vec(&mut self, input: &Vec<u8>) -> Vec<u8> { | ||
| let mut out: Vec<u8> = repeat(0).take(input.len()).collect(); | ||
| self.encode_vec(input, &mut out); | ||
| return out.to_vec(); | ||
| } | ||
| pub fn trans_str(&mut self, str: &String) -> Vec<u8> { | ||
| return self.trans_vec(&str.as_bytes().to_vec()); | ||
| } | ||
| /// Securely zero out the internal RC4 state | ||
| pub fn zeroize(&mut self) { | ||
| for byte in self.state.iter_mut() { | ||
| unsafe { std::ptr::write_volatile(byte, 0); } | ||
| } | ||
| unsafe { | ||
| std::ptr::write_volatile(&mut self.i, 0); | ||
| std::ptr::write_volatile(&mut self.j, 0); | ||
| } | ||
| } | ||
| } | ||
| fn sha256(data: &[u8]) -> [u8; 32] { | ||
| let k: [u32; 64] = [ | ||
| 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, | ||
| 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, | ||
| 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, | ||
| 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, | ||
| 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, | ||
| 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, | ||
| 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, | ||
| 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, | ||
| ]; | ||
| let mut h: [u32; 8] = [ | ||
| 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, | ||
| 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, | ||
| ]; | ||
| let bit_len = (data.len() as u64) * 8; | ||
| let mut msg = data.to_vec(); | ||
| msg.push(0x80); | ||
| while msg.len() % 64 != 56 { | ||
| msg.push(0); | ||
| } | ||
| msg.extend_from_slice(&bit_len.to_be_bytes()); | ||
| for chunk in msg.chunks(64) { | ||
| let mut w = [0u32; 64]; | ||
| for i in 0..16 { | ||
| w[i] = u32::from_be_bytes([chunk[4*i], chunk[4*i+1], chunk[4*i+2], chunk[4*i+3]]); | ||
| } | ||
| for i in 16..64 { | ||
| let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3); | ||
| let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10); | ||
| w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1); | ||
| } | ||
| let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) = | ||
| (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]); | ||
| for i in 0..64 { | ||
| let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); | ||
| let ch = (e & f) ^ ((!e) & g); | ||
| let t1 = hh.wrapping_add(s1).wrapping_add(ch).wrapping_add(k[i]).wrapping_add(w[i]); | ||
| let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); | ||
| let maj = (a & b) ^ (a & c) ^ (b & c); | ||
| let t2 = s0.wrapping_add(maj); | ||
| hh = g; g = f; f = e; e = d.wrapping_add(t1); | ||
| d = c; c = b; b = a; a = t1.wrapping_add(t2); | ||
| } | ||
| for (i, v) in [a, b, c, d, e, f, g, hh].iter().enumerate() { | ||
| h[i] = h[i].wrapping_add(*v); | ||
| } | ||
| } | ||
| let mut out = [0u8; 32]; | ||
| for (i, v) in h.iter().enumerate() { | ||
| out[4*i..4*i+4].copy_from_slice(&v.to_be_bytes()); | ||
| } | ||
| out | ||
| } | ||
| fn run_process(iterp: &str, prog: &String, args: &Vec<String>) { | ||
| let mut cmd = Command::new(iterp); | ||
| // Obfuscated interpreter name comparisons | ||
| let m: u8 = 0x55; | ||
| let ruby_s: [u8; 4] = [0x27, 0x20, 0x37, 0x2c]; | ||
| let python_s: [u8; 6] = [0x25, 0x2c, 0x21, 0x3d, 0x3a, 0x3b]; | ||
| let expect_s: [u8; 6] = [0x30, 0x2d, 0x25, 0x30, 0x36, 0x21]; | ||
| let ruby = obf_decode(&ruby_s, m); | ||
| let python = obf_decode(&python_s, m); | ||
| let expect = obf_decode(&expect_s, m); | ||
| if iterp == ruby || iterp.contains(&python) { | ||
| cmd.arg("-"); | ||
| } else if iterp == expect { | ||
| cmd.arg("-f").arg("-"); | ||
| } else { | ||
| cmd.arg("-s"); | ||
| } | ||
| let mut child = cmd | ||
| .args(args) | ||
| .stdin(Stdio::inherit()) | ||
| .stdin(Stdio::piped()) | ||
| .stdout(Stdio::inherit()) | ||
| .stderr(Stdio::inherit()) | ||
| .output() | ||
| .expect("failed to execute process"); | ||
| //println!("status: {}", output.status.code().unwrap()); | ||
| std::process::exit(output.status.code().unwrap()); | ||
| .spawn() | ||
| .unwrap_or_else(|_| std::process::exit(1)); | ||
| { | ||
| let stdin = child.stdin.as_mut().unwrap_or_else(|| std::process::exit(1)); | ||
| stdin.write_all(prog.as_bytes()).unwrap_or_else(|_| std::process::exit(1)); | ||
| } | ||
| let status = child.wait().unwrap_or_else(|_| std::process::exit(1)); | ||
| std::process::exit(status.code().unwrap_or(1)); | ||
| } | ||
| fn main() { | ||
| detect_debugger(); | ||
| verify_integrity(); | ||
| let prog = { script_code }; | ||
| let pass = "{ pass }"; | ||
| let iterp = "{ interp }"; | ||
| if pass.len() != 0 { | ||
| let mut input = String::new(); | ||
| print!("Password: "); | ||
| let mut key_mask: Vec<u8> = { key_mask }; | ||
| let mut key_masked: Vec<u8> = { key_masked }; | ||
| let mut pass_salt: Vec<u8> = { pass_salt }; | ||
| let pass_hash: Vec<u8> = { pass_hash }; | ||
| // Interpreter stored as XOR-encoded bytes | ||
| let interp_enc: Vec<u8> = { interp_enc }; | ||
| let interp_mask: u8 = { interp_mask }; | ||
| let iterp = obf_decode(&interp_enc, interp_mask); | ||
| if !pass_hash.is_empty() { | ||
| // Obfuscated prompt | ||
| let prompt: [u8; 10] = [0xfa, 0xcb, 0xd9, 0xd9, 0xdd, 0xc5, 0xd8, 0xce, 0x90, 0x8a]; | ||
| print!("{}", obf_decode(&prompt, 0xAA)); | ||
| io::stdout().flush().ok(); | ||
| io::stdin() | ||
| .read_line(&mut input) | ||
| .ok() | ||
| .expect("Couldn't read password"); | ||
| if input.trim() != pass { | ||
| println!("Invalid password!"); | ||
| let mut input = read_password_masked(); | ||
| let mut data = Vec::new(); | ||
| data.extend_from_slice(input.as_bytes()); | ||
| data.extend_from_slice(&pass_salt); | ||
| let mut input_hash = sha256(&data); | ||
| let matched = input_hash[..] == pass_hash[..]; | ||
| // Zero sensitive password data immediately | ||
| secure_zero_string(&mut input); | ||
| secure_zero_vec(&mut data); | ||
| secure_zero(&mut input_hash); | ||
| if !matched { | ||
| // Obfuscated error | ||
| let err: [u8; 17] = [0xe3, 0xc4, 0xdc, 0xcb, 0xc6, 0xc3, 0xce, 0x8a, 0xda, 0xcb, 0xd9, 0xd9, 0xdd, 0xc5, 0xd8, 0xce, 0x8b]; | ||
| println!("{}", obf_decode(&err, 0xAA)); | ||
| process::exit(1); | ||
| } | ||
| } | ||
| let prog_str = String::from_utf8(prog).unwrap(); | ||
| //println!("running ...:\n {}", prog_str); | ||
| secure_zero_vec(&mut pass_salt); | ||
| // Reconstruct key from obfuscated parts | ||
| let mut rand_key: Vec<u8> = key_mask.iter().zip(key_masked.iter()).map(|(m, d)| m ^ d).collect(); | ||
| // Zero key components immediately | ||
| secure_zero_vec(&mut key_mask); | ||
| secure_zero_vec(&mut key_masked); | ||
| // Decrypt script | ||
| let mut cipher = Arc4::new(&rand_key); | ||
| secure_zero_vec(&mut rand_key); | ||
| let mut prog_vec = cipher.trans_vec(&prog); | ||
| cipher.zeroize(); | ||
| let mut prog_str = String::from_utf8(prog_vec.clone()).unwrap(); | ||
| secure_zero_vec(&mut prog_vec); | ||
| let mut args = env::args().collect::<Vec<_>>(); | ||
| args.drain(0..1); | ||
| run_process(&iterp.to_string(), &prog_str, &args); | ||
| // Zero decrypted script (reached only if run_process doesn't exit) | ||
| secure_zero_string(&mut prog_str); | ||
| } | ||
| "###; | ||
| return prog; | ||
| "### | ||
| } |
-10
| language: rust | ||
| rust: | ||
| - stable | ||
| - beta | ||
| - nightly | ||
| matrix: | ||
| allow_failures: | ||
| - rust: nightly | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet