Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoSign in
Socket

rshc

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rshc - cargo Package Compare versions

Comparing version
0.1.3
to
0.2.0
+15
.codecov.yml
---
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
# 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",
]
+4
-3
{
"git": {
"sha1": "dee69082700053cbfd6e00049cf8008544a82236"
}
}
"sha1": "a099eff25143a4a9241071b407414353abd8c04d"
},
"path_in_vcs": ""
}

@@ -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"
#!/bin/ruby
puts ARGV.inspect
puts ARGV.length
# rshc
[![Build Status](https://travis-ci.com/chenyukang/rshc.svg?branch=master)](https://travis-ci.com/chenyukang/rshc)
[![ci](https://github.com/chenyukang/rshc/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/chenyukang/rshc/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/chenyukang/rshc/branch/master/graph/badge.svg?token=AAQREZTXYK)](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.

@@ -60,3 +60,3 @@ extern crate clap;

};
util::gen_and_compile(&file, &rs_file, &pass);
util::gen_and_compile(&file, &rs_file, &pass).unwrap();
}

@@ -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);
}
}
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;
"###
}
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