From 7496b8d33372750123864a952fa5c5afbb48b4c1 Mon Sep 17 00:00:00 2001 From: wr0belj Date: Sat, 27 Jun 2026 15:57:44 +0200 Subject: [PATCH] test: add compiler, COFF, CLI coverage and CI workflow --- .github/workflows/tests.yml | 154 +++++++++++++++ .gitignore | 2 + Cargo.lock | 182 ++++++++++++++++- Cargo.toml | 5 + src/asm.rs | 99 ++++++++++ src/cimport.rs | 105 ++++++++++ src/coff.rs | 89 +++++++++ src/encoder.rs | 102 ++++++++++ src/polymorphism.rs | 61 ++++++ tests/cli_smoke.rs | 86 ++++++++ tests/coff_smoke.rs | 187 ++++++++++++++++++ tests/debug_map_golden.rs | 49 +++++ tests/fixtures/coff/cna_bof.etpy | 15 ++ tests/fixtures/coff/generic_coff.etpy | 3 + tests/fixtures/coff/hashed_imports_bof.etpy | 7 + tests/fixtures/coff/minimal_bof.etpy | 3 + tests/fixtures/coff/poly_bof.etpy | 10 + tests/fixtures/debug_map.dbg | 13 ++ tests/fixtures/debug_map.etpy | 12 ++ tests/fixtures/negative/bad_struct_field.etpy | 9 + .../negative/extern_in_shellcode.etpy | 6 + tests/fixtures/negative/index_str.etpy | 4 + tests/fixtures/negative/mem_alloc_gc_off.etpy | 4 + tests/fixtures/shellcode/arithmetic.bin | Bin 0 -> 152 bytes tests/fixtures/shellcode/arithmetic.etpy | 6 + tests/fixtures/shellcode/branching.bin | Bin 0 -> 176 bytes tests/fixtures/shellcode/branching.etpy | 9 + .../shellcode/direct_syscall_seeded.bin | Bin 0 -> 820 bytes .../shellcode/direct_syscall_seeded.etpy | 5 + tests/fixtures/shellcode/function_call.bin | Bin 0 -> 176 bytes tests/fixtures/shellcode/function_call.etpy | 7 + .../shellcode/indirect_syscall_seeded.bin | Bin 0 -> 1683 bytes .../shellcode/indirect_syscall_seeded.etpy | 5 + tests/fixtures/shellcode/inline_asm.bin | Bin 0 -> 72 bytes tests/fixtures/shellcode/inline_asm.etpy | 11 ++ tests/fixtures/shellcode/loop_sum.bin | Bin 0 -> 192 bytes tests/fixtures/shellcode/loop_sum.etpy | 9 + tests/fixtures/shellcode/message_box.bin | Bin 0 -> 857 bytes tests/fixtures/shellcode/message_box.etpy | 4 + tests/fixtures/shellcode/poly_seeded.bin | Bin 0 -> 168 bytes tests/fixtures/shellcode/poly_seeded.etpy | 8 + tests/fixtures/shellcode/return_literal.bin | Bin 0 -> 32 bytes tests/fixtures/shellcode/return_literal.etpy | 3 + .../shellcode/stack_strings_seeded.bin | Bin 0 -> 160 bytes .../shellcode/stack_strings_seeded.etpy | 4 + .../fixtures/shellcode/startupinfo_struct.bin | Bin 0 -> 344 bytes .../shellcode/startupinfo_struct.etpy | 10 + tests/fixtures/shellcode/static_data.bin | Bin 0 -> 112 bytes tests/fixtures/shellcode/static_data.etpy | 7 + tests/fixtures/shellcode/str_format.bin | Bin 0 -> 1368 bytes tests/fixtures/shellcode/str_format.etpy | 6 + tests/fixtures/shellcode/string_layout.bin | Bin 0 -> 94 bytes tests/fixtures/shellcode/string_layout.etpy | 4 + .../fixtures/shellcode/strings_xor_seeded.bin | Bin 0 -> 276 bytes .../shellcode/strings_xor_seeded.etpy | 7 + tests/fixtures/shellcode/try_raise.bin | Bin 0 -> 1169 bytes tests/fixtures/shellcode/try_raise.etpy | 9 + tests/fixtures/shellcode/win32_struct.bin | Bin 0 -> 1250 bytes tests/fixtures/shellcode/win32_struct.etpy | 10 + tests/fixtures/shellcode/winapi_call.bin | Bin 0 -> 902 bytes tests/fixtures/shellcode/winapi_call.etpy | 7 + tests/shellcode_golden.rs | 163 +++++++++++++++ tests/typecheck_negative.rs | 73 +++++++ tools/bof-loader/src/args.rs | 2 + tools/bof-loader/src/coff.rs | 125 ++++++++++++ tools/entc-debug/src/dap.rs | 6 + 66 files changed, 1706 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/cli_smoke.rs create mode 100644 tests/coff_smoke.rs create mode 100644 tests/debug_map_golden.rs create mode 100644 tests/fixtures/coff/cna_bof.etpy create mode 100644 tests/fixtures/coff/generic_coff.etpy create mode 100644 tests/fixtures/coff/hashed_imports_bof.etpy create mode 100644 tests/fixtures/coff/minimal_bof.etpy create mode 100644 tests/fixtures/coff/poly_bof.etpy create mode 100644 tests/fixtures/debug_map.dbg create mode 100644 tests/fixtures/debug_map.etpy create mode 100644 tests/fixtures/negative/bad_struct_field.etpy create mode 100644 tests/fixtures/negative/extern_in_shellcode.etpy create mode 100644 tests/fixtures/negative/index_str.etpy create mode 100644 tests/fixtures/negative/mem_alloc_gc_off.etpy create mode 100644 tests/fixtures/shellcode/arithmetic.bin create mode 100644 tests/fixtures/shellcode/arithmetic.etpy create mode 100644 tests/fixtures/shellcode/branching.bin create mode 100644 tests/fixtures/shellcode/branching.etpy create mode 100644 tests/fixtures/shellcode/direct_syscall_seeded.bin create mode 100644 tests/fixtures/shellcode/direct_syscall_seeded.etpy create mode 100644 tests/fixtures/shellcode/function_call.bin create mode 100644 tests/fixtures/shellcode/function_call.etpy create mode 100644 tests/fixtures/shellcode/indirect_syscall_seeded.bin create mode 100644 tests/fixtures/shellcode/indirect_syscall_seeded.etpy create mode 100644 tests/fixtures/shellcode/inline_asm.bin create mode 100644 tests/fixtures/shellcode/inline_asm.etpy create mode 100644 tests/fixtures/shellcode/loop_sum.bin create mode 100644 tests/fixtures/shellcode/loop_sum.etpy create mode 100644 tests/fixtures/shellcode/message_box.bin create mode 100644 tests/fixtures/shellcode/message_box.etpy create mode 100644 tests/fixtures/shellcode/poly_seeded.bin create mode 100644 tests/fixtures/shellcode/poly_seeded.etpy create mode 100644 tests/fixtures/shellcode/return_literal.bin create mode 100644 tests/fixtures/shellcode/return_literal.etpy create mode 100644 tests/fixtures/shellcode/stack_strings_seeded.bin create mode 100644 tests/fixtures/shellcode/stack_strings_seeded.etpy create mode 100644 tests/fixtures/shellcode/startupinfo_struct.bin create mode 100644 tests/fixtures/shellcode/startupinfo_struct.etpy create mode 100644 tests/fixtures/shellcode/static_data.bin create mode 100644 tests/fixtures/shellcode/static_data.etpy create mode 100644 tests/fixtures/shellcode/str_format.bin create mode 100644 tests/fixtures/shellcode/str_format.etpy create mode 100644 tests/fixtures/shellcode/string_layout.bin create mode 100644 tests/fixtures/shellcode/string_layout.etpy create mode 100644 tests/fixtures/shellcode/strings_xor_seeded.bin create mode 100644 tests/fixtures/shellcode/strings_xor_seeded.etpy create mode 100644 tests/fixtures/shellcode/try_raise.bin create mode 100644 tests/fixtures/shellcode/try_raise.etpy create mode 100644 tests/fixtures/shellcode/win32_struct.bin create mode 100644 tests/fixtures/shellcode/win32_struct.etpy create mode 100644 tests/fixtures/shellcode/winapi_call.bin create mode 100644 tests/fixtures/shellcode/winapi_call.etpy create mode 100644 tests/shellcode_golden.rs create mode 100644 tests/typecheck_negative.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3c13292 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,154 @@ +name: Tests + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Entropia tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install stable Rust + run: rustup toolchain install stable --profile minimal + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run test suites + shell: bash + run: | + set +e + + summary_file="test-summary.md" + : > "$summary_file" + + add_summary() { + printf "%s\n" "$1" | tee -a "$GITHUB_STEP_SUMMARY" "$summary_file" > /dev/null + } + + add_summary "## Test summary" + add_summary "" + add_summary "| Suite | Command | Result |" + add_summary "| --- | --- | --- |" + + failures=0 + + run_suite() { + local name="$1" + shift + local cmd="$*" + + echo "::group::$name" + echo "$ $cmd" + "$@" + local status=$? + echo "::endgroup::" + + if [ "$status" -eq 0 ]; then + printf "| %s | \`%s\` | PASS |\n" "$name" "$cmd" | tee -a "$GITHUB_STEP_SUMMARY" "$summary_file" > /dev/null + else + printf "| %s | \`%s\` | FAIL (%s) |\n" "$name" "$cmd" "$status" | tee -a "$GITHUB_STEP_SUMMARY" "$summary_file" > /dev/null + failures=$((failures + 1)) + fi + } + + run_suite "entropykit unit tests" cargo test -p entropykit --bin entc + run_suite "shellcode goldens" cargo test -p entropykit --test shellcode_golden + run_suite "COFF smoke tests" cargo test -p entropykit --test coff_smoke + run_suite "CLI smoke tests" cargo test -p entropykit --test cli_smoke + run_suite "negative compiler tests" cargo test -p entropykit --test typecheck_negative + run_suite "debug map golden" cargo test -p entropykit --test debug_map_golden + run_suite "BOF loader tests" cargo test -p bof-loader + run_suite "remaining workspace crates" cargo test --workspace --exclude entropykit --exclude bof-loader + + add_summary "" + if [ "$failures" -eq 0 ]; then + add_summary "**Result:** all test suites passed." + else + add_summary "**Result:** $failures test suite(s) failed." + fi + + echo "$failures" > .test-failures + + - name: Publish PR test summary + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const fs = require('fs'); + + if (!fs.existsSync('test-summary.md')) { + core.info('No test-summary.md found; skipping PR comment.'); + return; + } + + const marker = ''; + const body = `${marker}\n${fs.readFileSync('test-summary.md', 'utf8')}`; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + per_page: 100, + }); + + const previous = comments.data.find(comment => + comment.user?.type === 'Bot' && comment.body?.includes(marker) + ); + + if (previous) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: previous.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + + - name: Fail if any suite failed + if: always() + shell: bash + run: | + failures="$(cat .test-failures 2>/dev/null || echo 1)" + if [ "$failures" -ne 0 ]; then + echo "$failures test suite(s) failed." + exit 1 + fi diff --git a/.gitignore b/.gitignore index ac3485f..6f70322 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ example/*.cna *.x64.o *.x86.o *.dbg +!tests/fixtures/**/*.bin +!tests/fixtures/**/*.dbg /tmp_build/ build/ diff --git a/Cargo.lock b/Cargo.lock index af3d50c..1e54010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,33 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bof-loader" @@ -16,6 +43,29 @@ dependencies = [ "bof-loader", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "entc-debug" version = "0.1.0" @@ -37,6 +87,38 @@ dependencies = [ [[package]] name = "entropykit" version = "0.1.0" +dependencies = [ + "assert_cmd", + "bof-loader", + "tempfile", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] [[package]] name = "iced-x86" @@ -59,12 +141,57 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -83,6 +210,31 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "serde" version = "1.0.228" @@ -137,12 +289,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "windows-metadata" version = "0.60.0" diff --git a/Cargo.toml b/Cargo.toml index 9e09d99..59ff5ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,8 @@ path = "src/main.rs" [profile.release] opt-level = 3 lto = true + +[dev-dependencies] +assert_cmd = "2" +bof-loader = { path = "tools/bof-loader" } +tempfile = "3" diff --git a/src/asm.rs b/src/asm.rs index 9703b72..788ab2e 100644 --- a/src/asm.rs +++ b/src/asm.rs @@ -366,3 +366,102 @@ fn validate_ident(s: &str) -> Result<(), String> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_labels_db_and_operand_counts() { + assert!(matches!( + parse_asm_line("loop_top:").unwrap(), + AsmBody::Label(name) if name == "loop_top" + )); + assert!(matches!( + parse_asm_line("db 0x41, 66, 255").unwrap(), + AsmBody::Db(bytes) if bytes == vec![0x41, 66, 0xff] + )); + assert!(matches!( + parse_asm_line("ret").unwrap(), + AsmBody::Op0 { mnem } if mnem == "ret" + )); + assert!(matches!( + parse_asm_line("call %target").unwrap(), + AsmBody::Op1 { mnem, op: AsmOperand::Sym(name) } if mnem == "call" && name == "target" + )); + } + + #[test] + fn parses_register_immediate_and_symbol_operands() { + assert!(matches!( + parse_asm_line("mov rax, 0x1234").unwrap(), + AsmBody::Op2 { + mnem, + dst: AsmOperand::Reg(Reg64::Rax), + src: AsmOperand::Imm(0x1234), + } if mnem == "mov" + )); + assert!(matches!( + parse_asm_line("cmovnz r10, r11").unwrap(), + AsmBody::Op2 { + mnem, + dst: AsmOperand::Reg(Reg64::R10), + src: AsmOperand::Reg(Reg64::R11), + } if mnem == "cmovnz" + )); + assert!(matches!( + parse_asm_line("jmp done").unwrap(), + AsmBody::Op1 { mnem, op: AsmOperand::Sym(name) } if mnem == "jmp" && name == "done" + )); + } + + #[test] + fn parses_memory_operands() { + let AsmBody::Op2 { src: AsmOperand::Mem(mem), .. } = + parse_asm_line("mov rax, [rbp - 0x20]").unwrap() + else { + panic!("expected memory source"); + }; + assert!(matches!(mem.base, Some(AsmMemBase::Reg(Reg64::Rbp)))); + assert!(mem.index.is_none()); + assert_eq!(mem.disp, -0x20); + assert_eq!(mem.seg, None); + + let AsmBody::Op2 { src: AsmOperand::Mem(mem), .. } = + parse_asm_line("mov r9, [rdx + rcx*8 + 16]").unwrap() + else { + panic!("expected indexed memory source"); + }; + assert!(matches!(mem.base, Some(AsmMemBase::Reg(Reg64::Rdx)))); + assert!(matches!(mem.index, Some((Reg64::Rcx, 8)))); + assert_eq!(mem.disp, 16); + } + + #[test] + fn parses_segmented_and_symbol_memory_operands() { + let AsmBody::Op2 { src: AsmOperand::Mem(mem), .. } = + parse_asm_line("mov rax, gs:[0x60]").unwrap() + else { + panic!("expected segmented memory source"); + }; + assert!(mem.base.is_none()); + assert_eq!(mem.disp, 0x60); + assert_eq!(mem.seg, Some(Segment::Gs)); + + let AsmBody::Op2 { src: AsmOperand::Mem(mem), .. } = + parse_asm_line("lea rax, [%local + 8]").unwrap() + else { + panic!("expected symbolic memory source"); + }; + assert!(matches!(mem.base, Some(AsmMemBase::Sym(name)) if name == "local")); + assert_eq!(mem.disp, 8); + } + + #[test] + fn rejects_malformed_operands() { + assert!(parse_asm_line("mov rax, [rbx +").unwrap_err().contains("unmatched")); + assert!(parse_asm_line("mov rax, cs:[0x60]").unwrap_err().contains("unknown operand")); + assert!(parse_asm_line("mov rax, [rbx + rcx*3]").unwrap_err().contains("scale")); + assert!(parse_asm_line("db 0x100").unwrap_err().contains("out of range")); + assert!(parse_asm_line("mov rax, rbx, rcx").unwrap_err().contains("got 3")); + } +} diff --git a/src/cimport.rs b/src/cimport.rs index 01367e5..ba39381 100644 --- a/src/cimport.rs +++ b/src/cimport.rs @@ -685,3 +685,108 @@ const PRIMITIVES: &[(&str, &str)] = &[ ("intptr_t", "i64"), ("FILE", "u64"), ]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_integer_defines_and_struct_fields() { + let parsed = parse_header_text( + r#" + #define CONST_A 0x2a + #define CONST_NEG -7 + typedef struct tagDemo { + unsigned int count; + char* name; + unsigned char bytes[4]; + } DEMO; + "#, + ); + + assert!(parsed.statics.iter().any(|s| + s.name == "CONST_A" && s.ty == "u32" && matches!(s.init, Some(Expr::Int(0x2a))) + )); + assert!(parsed.statics.iter().any(|s| + s.name == "CONST_NEG" && s.ty == "u32" && matches!(s.init, Some(Expr::Int(-7))) + )); + + let demo = parsed.structs.iter().find(|s| s.name == "DEMO").unwrap(); + assert!(!demo.is_union); + assert_eq!( + demo.fields, + vec![ + ("count".to_string(), "u32".to_string()), + ("name".to_string(), "u64".to_string()), + ("bytes".to_string(), "u8[4]".to_string()), + ], + ); + } + + #[test] + fn parses_unions_and_nested_anonymous_records() { + let parsed = parse_header_text( + r#" + typedef union tagValue { + unsigned int u; + void* p; + } VALUE; + + typedef struct tagOuter { + int id; + union { + unsigned short s; + unsigned int i; + } inner; + } OUTER; + "#, + ); + + let value = parsed.structs.iter().find(|s| s.name == "VALUE").unwrap(); + assert!(value.is_union); + assert_eq!( + value.fields, + vec![ + ("u".to_string(), "u32".to_string()), + ("p".to_string(), "u64".to_string()), + ], + ); + + let outer = parsed.structs.iter().find(|s| s.name == "OUTER").unwrap(); + assert_eq!(outer.fields[0], ("id".to_string(), "i32".to_string())); + assert_eq!(outer.fields[1].0, "inner"); + let inner_ty = &outer.fields[1].1; + let inner = parsed.structs.iter().find(|s| &s.name == inner_ty).unwrap(); + assert!(inner.is_union); + assert_eq!( + inner.fields, + vec![ + ("s".to_string(), "u16".to_string()), + ("i".to_string(), "u32".to_string()), + ], + ); + } + + #[test] + fn rejects_non_integer_define_values() { + let err = parse_header_text_err("#define NAME other_symbol\n"); + assert!(err.contains("must be followed by an integer literal")); + } + + fn parse_header_text(src: &str) -> CImport { + let file = write_header(src); + parse_header(file.path().to_str().unwrap()).unwrap() + } + + fn parse_header_text_err(src: &str) -> String { + let file = write_header(src); + parse_header(file.path().to_str().unwrap()).unwrap_err() + } + + fn write_header(src: &str) -> tempfile::NamedTempFile { + use std::io::Write; + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(src.as_bytes()).unwrap(); + file + } +} diff --git a/src/coff.rs b/src/coff.rs index 22c367f..1bdba82 100644 --- a/src/coff.rs +++ b/src/coff.rs @@ -293,3 +293,92 @@ pub fn emit(obj: &CoffObject) -> Result, String> { let _ = strtab_offset; Ok(out) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn emits_minimal_text_object() { + let mut obj = CoffObject::new(); + obj.text.bytes.push(0xC3); + obj.intern("go", SymKind::TextExtern, 0); + + let bytes = emit(&obj).unwrap(); + + assert_eq!(u16_at(&bytes, 0), IMAGE_FILE_MACHINE_AMD64); + assert_eq!(u16_at(&bytes, 2), 1); + assert_eq!(u32_at(&bytes, 12), 1); + + let text_header = 20; + assert_eq!(&bytes[text_header..text_header + 5], b".text"); + assert_eq!(u32_at(&bytes, text_header + 16), 1); + let raw_off = u32_at(&bytes, text_header + 20) as usize; + assert_eq!(bytes[raw_off], 0xC3); + + let symtab = u32_at(&bytes, 8) as usize; + assert_eq!(symbol_name(&bytes, symtab, 0), "go"); + assert_eq!(u32_at(&bytes, symtab + 8), 0); + assert_eq!(i16_at(&bytes, symtab + 12), 1); + assert_eq!(u16_at(&bytes, symtab + 14), IMAGE_SYM_DTYPE_FUNCTION); + assert_eq!(bytes[symtab + 16], IMAGE_SYM_CLASS_EXTERNAL); + } + + #[test] + fn emits_relocations_and_long_symbol_names() { + let mut obj = CoffObject::new(); + obj.text.bytes.extend_from_slice(&[0xE8, 0, 0, 0, 0]); + let import = obj.intern("__imp_KERNEL32$VeryLongImportedFunction", SymKind::Undefined, 0); + obj.text.relocs.push(CoffReloc { + at: 1, + sym: import, + ty: IMAGE_REL_AMD64_REL32, + }); + + let bytes = emit(&obj).unwrap(); + let text_header = 20; + assert_eq!(u16_at(&bytes, text_header + 32), 1); + + let reloc_off = u32_at(&bytes, text_header + 24) as usize; + assert_eq!(u32_at(&bytes, reloc_off), 1); + assert_eq!(u32_at(&bytes, reloc_off + 4), import as u32); + assert_eq!(u16_at(&bytes, reloc_off + 8), IMAGE_REL_AMD64_REL32); + + assert_eq!( + symbol_name(&bytes, u32_at(&bytes, 8) as usize, import), + "__imp_KERNEL32$VeryLongImportedFunction", + ); + } + + fn u16_at(bytes: &[u8], off: usize) -> u16 { + u16::from_le_bytes(bytes[off..off + 2].try_into().unwrap()) + } + + fn i16_at(bytes: &[u8], off: usize) -> i16 { + i16::from_le_bytes(bytes[off..off + 2].try_into().unwrap()) + } + + fn u32_at(bytes: &[u8], off: usize) -> u32 { + u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap()) + } + + fn symbol_name(bytes: &[u8], symtab: usize, idx: usize) -> String { + let n_symbols = u32_at(bytes, 12) as usize; + let strtab = symtab + 18 * n_symbols; + let base = symtab + 18 * idx; + let raw = &bytes[base..base + 8]; + if raw[0..4] == [0, 0, 0, 0] { + let off = u32_at(raw, 4) as usize; + let start = strtab + off; + let end = bytes[start..] + .iter() + .position(|&b| b == 0) + .map(|n| start + n) + .unwrap_or(bytes.len()); + std::str::from_utf8(&bytes[start..end]).unwrap().to_string() + } else { + let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len()); + std::str::from_utf8(&raw[..end]).unwrap().to_string() + } + } +} diff --git a/src/encoder.rs b/src/encoder.rs index e815d90..86977be 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -1275,3 +1275,105 @@ pub struct CoffArtifact { /// table entry. pub code_labels: HashMap, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_core_register_instructions() { + let mut enc = Encoder::new(); + enc.mov_r64_imm64(Reg64::Rax, 0x1122_3344_5566_7788); + enc.mov_r64_r64(Reg64::R9, Reg64::Rax); + enc.add_r64_r64(Reg64::Rax, Reg64::R9); + enc.sub_r64_r64(Reg64::R9, Reg64::Rax); + enc.cmp_r64_r64(Reg64::Rax, Reg64::R9); + enc.test_r64_r64(Reg64::R9, Reg64::R9); + enc.push_r64(Reg64::R9); + enc.pop_r64(Reg64::R9); + enc.call_r64(Reg64::R9); + enc.jmp_r64(Reg64::R9); + enc.ret(); + + assert_eq!( + enc.code, + vec![ + 0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, + 0x49, 0x89, 0xC1, + 0x4C, 0x01, 0xC8, + 0x49, 0x29, 0xC1, + 0x4C, 0x39, 0xC8, + 0x4D, 0x85, 0xC9, + 0x41, 0x51, + 0x41, 0x59, + 0x41, 0xFF, 0xD1, + 0x41, 0xFF, 0xE1, + 0xC3, + ], + ); + } + + #[test] + fn encodes_immediates_and_memory_addressing() { + let mut enc = Encoder::new(); + enc.sub_r64_imm32(Reg64::Rsp, 0x20); + enc.add_r64_imm32(Reg64::Rsp, 0x1234); + enc.lea_r64_r64disp(Reg64::Rax, Reg64::Rbp, -8); + enc.mov_r64_rbp_disp(Reg64::R10, -16); + enc.mov_rbp_disp_r64(-24, Reg64::R10); + enc.mov_rax_gs_qword(0x60); + + assert_eq!( + enc.code, + vec![ + 0x48, 0x83, 0xEC, 0x20, + 0x48, 0x81, 0xC4, 0x34, 0x12, 0x00, 0x00, + 0x48, 0x8D, 0x85, 0xF8, 0xFF, 0xFF, 0xFF, + 0x4C, 0x8B, 0x95, 0xF0, 0xFF, 0xFF, 0xFF, + 0x4C, 0x89, 0x95, 0xE8, 0xFF, 0xFF, 0xFF, + 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, + ], + ); + } + + #[test] + fn finalize_patches_code_rel32() { + let mut enc = Encoder::new(); + enc.call_label("target"); + enc.ret(); + enc.place_code_label("target"); + enc.ret(); + + let bytes = enc.finalize().unwrap(); + assert_eq!(bytes, vec![0xE8, 0x01, 0x00, 0x00, 0x00, 0xC3, 0xC3, 0x90]); + } + + #[test] + fn finalize_patches_data_rel32() { + let mut enc = Encoder::new(); + enc.lea_r64_data(Reg64::Rax, "msg"); + enc.ret(); + enc.add_string("msg", "A"); + + let bytes = enc.finalize().unwrap(); + assert_eq!( + bytes, + vec![0x48, 0x8D, 0x05, 0x01, 0x00, 0x00, 0x00, 0xC3, b'A', 0x00], + ); + } + + #[test] + fn finalize_reports_unresolved_targets() { + let mut missing_code = Encoder::new(); + missing_code.call_label("missing"); + assert!(missing_code.finalize().unwrap_err().contains("unresolved code label")); + + let mut missing_data = Encoder::new(); + missing_data.lea_r64_data(Reg64::Rax, "missing"); + assert!(missing_data.finalize().unwrap_err().contains("unresolved data label")); + + let mut external = Encoder::new(); + external.call_extern("__imp_KERNEL32$ExitProcess"); + assert!(external.finalize().unwrap_err().contains("External symbols are only valid")); + } +} diff --git a/src/polymorphism.rs b/src/polymorphism.rs index 64763a1..6ce2e8e 100644 --- a/src/polymorphism.rs +++ b/src/polymorphism.rs @@ -950,3 +950,64 @@ pub fn emit_decryptor(enc: &mut Encoder) { enc.pop_r64(Reg64::Rax); enc.ret(); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn poly_transform_is_deterministic_for_same_seed() { + assert_eq!(transformed_code(0x1234), transformed_code(0x1234)); + } + + #[test] + fn poly_transform_can_vary_across_seeds() { + let baseline = transformed_code(1); + assert!( + (2..64).any(|seed| transformed_code(seed) != baseline), + "expected at least one seed to alter the poly output", + ); + } + + #[test] + fn poly_transform_preserves_code_length() { + let mut enc = sample_encoder(); + let original_len = enc.code.len(); + let mut rng = Rng::new(0x1234); + transform(&mut enc, &OpsecConfig { poly: true, ..OpsecConfig::default() }, &mut rng); + assert_eq!(enc.code.len(), original_len); + } + + #[test] + fn strings_xor_encrypts_data_and_publishes_key() { + let mut enc = Encoder::new(); + enc.mark_rdata_position("__rdata_start"); + enc.add_rdata_raw_named("msg", b"plaintext"); + let original = enc.rdata.clone(); + + let mut rng = Rng::new(0x1234); + transform(&mut enc, &OpsecConfig { strings_xor: true, ..OpsecConfig::default() }, &mut rng); + + assert_ne!(&enc.rdata[..original.len()], original.as_slice()); + assert_eq!(enc.rdata.len(), original.len() + 8); + } + + fn transformed_code(seed: u64) -> Vec { + let mut enc = sample_encoder(); + let mut rng = Rng::new(seed); + transform(&mut enc, &OpsecConfig { poly: true, ..OpsecConfig::default() }, &mut rng); + enc.code + } + + fn sample_encoder() -> Encoder { + let mut enc = Encoder::new(); + enc.xor_r64_r64(Reg64::Rax, Reg64::Rax); + enc.mov_r64_r64(Reg64::Rcx, Reg64::Rax); + enc.test_r64_r64(Reg64::Rdx, Reg64::Rdx); + enc.nop(); + enc.nop(); + enc.nop(); + enc.ret(); + enc + } +} diff --git a/tests/cli_smoke.rs b/tests/cli_smoke.rs new file mode 100644 index 0000000..f8332d8 --- /dev/null +++ b/tests/cli_smoke.rs @@ -0,0 +1,86 @@ +use assert_cmd::prelude::*; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[test] +fn bof_type_rewrites_bin_output_extension() { + let temp = tempfile::tempdir().expect("create temp output dir"); + let requested = temp.path().join("artifact.bin"); + + Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(fixture_path("coff/minimal_bof.etpy")) + .arg("--type=bof") + .arg("--gc=off") + .arg("-o") + .arg(&requested) + .assert() + .success(); + + assert!(!requested.exists(), "BOF output should not keep .bin suffix"); + assert!(temp.path().join("artifact.x64.o").exists()); +} + +#[test] +fn debug_flag_emits_dbg_sidecar() { + let temp = tempfile::tempdir().expect("create temp output dir"); + let out = temp.path().join("debug.bin"); + + Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(fixture_path("debug_map.etpy")) + .arg("--type=standard") + .arg("--gc=off") + .arg("--debug") + .arg("-o") + .arg(&out) + .assert() + .success(); + + let dbg = temp.path().join("debug.dbg"); + assert!(dbg.exists(), "expected --debug to emit {}", dbg.display()); +} + +#[test] +fn unknown_argument_fails_with_stable_message() { + let stderr = Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(fixture_path("shellcode/return_literal.etpy")) + .arg("--definitely-not-real") + .assert() + .failure() + .get_output() + .stderr + .clone(); + + assert!(String::from_utf8_lossy(&stderr).contains("unknown arg: --definitely-not-real")); +} + +#[test] +fn non_etpy_input_fails_with_extension_message() { + let temp = tempfile::tempdir().expect("create temp output dir"); + let input = temp.path().join("not_source.txt"); + std::fs::write(&input, "fn main() -> int { ret 0; }\n").expect("write temp input"); + + let stderr = Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(&input) + .assert() + .failure() + .get_output() + .stderr + .clone(); + + assert!(String::from_utf8_lossy(&stderr).contains("does not end in `.etpy`")); +} + +fn fixture_path(relative: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(relative) +} diff --git a/tests/coff_smoke.rs b/tests/coff_smoke.rs new file mode 100644 index 0000000..ab4ba62 --- /dev/null +++ b/tests/coff_smoke.rs @@ -0,0 +1,187 @@ +use assert_cmd::prelude::*; +use bof_loader::coff::ParsedCoff; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[test] +fn entc_emits_parseable_minimal_bof_coff() { + let obj = compile_bof_fixture("minimal_bof", &[]).obj; + let parsed = ParsedCoff::parse(&obj).expect("parse emitted COFF"); + + assert!(parsed.n_sections >= 1, "expected at least .text section"); + let text = parsed.section(0); + assert_eq!(text.name, ".text"); + assert!(!text.raw.is_empty(), "expected non-empty .text"); + + let mut saw_go = false; + for idx in 0..parsed.n_symbols { + if parsed.symbol_name(idx) == "go" { + saw_go = true; + assert_eq!(parsed.symbol_section(idx), 1, "go should live in .text"); + assert_eq!(parsed.symbol_value(idx), 0, "go should be the entry symbol"); + } + } + assert!(saw_go, "expected emitted COFF to contain `go` symbol"); +} + +#[test] +fn hashed_imports_removes_winapi_externals_but_keeps_beacon_externals() { + let obj = compile_bof_fixture("hashed_imports_bof", &["--opsec=hashed_imports"]).obj; + let parsed = ParsedCoff::parse(&obj).expect("parse emitted COFF"); + let symbols = symbol_names(&parsed); + + assert!( + symbols.iter().any(|name| name == "__imp_BeaconPrintf"), + "Beacon API imports should stay external: {symbols:?}", + ); + assert!( + !symbols.iter().any(|name| name == "__imp_KERNEL32$GetCurrentProcessId"), + "hashed_imports should remove WinAPI externals: {symbols:?}", + ); + assert!( + text_reloc_symbol_names(&parsed).iter().any(|name| name == "__imp_BeaconPrintf"), + "expected a text relocation against BeaconPrintf", + ); +} + +#[test] +fn bof_command_emits_cna_sidecar() { + let compiled = compile_bof_fixture("cna_bof", &[]); + let cna = compiled.cna.expect("expected [BofCommand] to emit .cna sidecar"); + + assert!(cna.contains("alias golden_cmd")); + assert!(cna.contains("bof_pack($bid, \"iz\", $pid, $name)")); + assert!(cna.contains("script_resource(\"cna_bof.x64.o\")")); + assert!(cna.contains("Usage: golden_cmd ")); +} + +#[test] +fn entc_emits_parseable_generic_coff() { + let obj = compile_object_fixture("generic_coff", "--type=coff", &[]).object; + let parsed = ParsedCoff::parse(&obj).expect("parse emitted generic COFF"); + let symbols = symbol_names(&parsed); + + assert!(symbols.iter().any(|name| name == "go")); + assert_eq!(parsed.section(0).name, ".text"); + assert!(!parsed.section(0).raw.is_empty()); +} + +#[test] +fn opsec_bof_stays_parseable_coff() { + let obj = compile_object_fixture( + "poly_bof", + "--type=bof", + &["--opsec=poly,nop_sled", "--seed=0x1234"], + ) + .object; + let parsed = ParsedCoff::parse(&obj).expect("parse emitted OPSEC BOF COFF"); + assert!(symbol_names(&parsed).iter().any(|name| name == "go")); + assert!(!parsed.section(0).raw.is_empty()); +} + +#[test] +fn debug_sidecar_is_emitted_for_object_target() { + let compiled = compile_object_fixture("generic_coff", "--type=coff", &["--debug"]); + let dbg = compiled.dbg.expect("expected --debug to emit .dbg sidecar for COFF"); + + assert!(dbg.contains("# entropy debug map")); + assert!(dbg.contains("ret")); + assert!(dbg.contains("tests/fixtures/coff/generic_coff.etpy") + || dbg.contains("generic_coff.etpy")); +} + +struct CompiledBof { + obj: Vec, + cna: Option, +} + +struct CompiledObject { + object: Vec, + dbg: Option, +} + +fn compile_bof_fixture(name: &str, extra_args: &[&str]) -> CompiledBof { + let temp = tempfile::tempdir().expect("create temp output dir"); + let out = temp.path().join(format!("{name}.x64.o")); + let src = fixture_dir().join(format!("{name}.etpy")); + + let mut cmd = Command::cargo_bin("entc").expect("resolve entc test binary"); + let output = cmd + .arg("compile") + .arg(&src) + .arg("--type=bof") + .arg("--gc=off") + .arg("-o") + .arg(&out) + .args(extra_args) + .assert() + .success() + .get_output() + .clone(); + + let obj = std::fs::read(&out).unwrap_or_else(|err| { + panic!( + "read compiled BOF `{}` after entc success\nstderr:\n{}error: {err}", + out.display(), + String::from_utf8_lossy(&output.stderr), + ) + }); + let cna_path = temp.path().join(format!("{name}.cna")); + let cna = std::fs::read_to_string(&cna_path).ok(); + CompiledBof { obj, cna } +} + +fn compile_object_fixture(name: &str, type_arg: &str, extra_args: &[&str]) -> CompiledObject { + let temp = tempfile::tempdir().expect("create temp output dir"); + let suffix = if type_arg == "--type=bof" { "x64.o" } else { "obj" }; + let out = temp.path().join(format!("{name}.{suffix}")); + let src = fixture_dir().join(format!("{name}.etpy")); + + let output = Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(&src) + .arg(type_arg) + .arg("--gc=off") + .arg("-o") + .arg(&out) + .args(extra_args) + .assert() + .success() + .get_output() + .clone(); + + let object = std::fs::read(&out).unwrap_or_else(|err| { + panic!( + "read compiled object `{}` after entc success\nstderr:\n{}error: {err}", + out.display(), + String::from_utf8_lossy(&output.stderr), + ) + }); + let dbg = std::fs::read_to_string(temp.path().join(format!("{name}.dbg"))).ok(); + CompiledObject { object, dbg } +} + +fn symbol_names(parsed: &ParsedCoff<'_>) -> Vec { + (0..parsed.n_symbols) + .map(|idx| parsed.symbol_name(idx)) + .collect() +} + +fn text_reloc_symbol_names(parsed: &ParsedCoff<'_>) -> Vec { + let text = parsed.section(0); + text.relocs + .chunks_exact(10) + .map(|reloc| { + let idx = u32::from_le_bytes(reloc[4..8].try_into().unwrap()); + parsed.symbol_name(idx) + }) + .collect() +} + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("coff") +} diff --git a/tests/debug_map_golden.rs b/tests/debug_map_golden.rs new file mode 100644 index 0000000..cf1715b --- /dev/null +++ b/tests/debug_map_golden.rs @@ -0,0 +1,49 @@ +use assert_cmd::prelude::*; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[test] +fn debug_map_matches_golden_text() { + let temp = tempfile::tempdir().expect("create temp output dir"); + let out = temp.path().join("debug_map.bin"); + let src = fixture_path("debug_map.etpy"); + + let output = Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(&src) + .arg("--type=standard") + .arg("--gc=off") + .arg("--debug") + .arg("-o") + .arg(&out) + .assert() + .success() + .get_output() + .clone(); + + let dbg_path = temp.path().join("debug_map.dbg"); + let actual = std::fs::read_to_string(&dbg_path).unwrap_or_else(|err| { + panic!( + "read debug map `{}` after entc success\nstderr:\n{}error: {err}", + dbg_path.display(), + String::from_utf8_lossy(&output.stderr), + ) + }); + let expected = std::fs::read_to_string(fixture_path("debug_map.dbg")) + .expect("read debug_map.dbg golden"); + + assert_eq!(normalize_paths(&actual), normalize_paths(&expected)); +} + +fn fixture_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(name) +} + +fn normalize_paths(s: &str) -> String { + s.replace('\\', "/") + .replace(&format!("{}/", env!("CARGO_MANIFEST_DIR").replace('\\', "/")), "") +} diff --git a/tests/fixtures/coff/cna_bof.etpy b/tests/fixtures/coff/cna_bof.etpy new file mode 100644 index 0000000..4b78f4f --- /dev/null +++ b/tests/fixtures/coff/cna_bof.etpy @@ -0,0 +1,15 @@ +use bof; + +[BofArgs] +struct TestArgs { + pid: int, + name: char*, +} + +[BofCommand("golden_cmd")] +fn go(args: char*, len: int) -> void { + var a: TestArgs; + bof_parse_TestArgs(args, len, &a); + BeaconPrintf(0, str.format("pid={d} name={s}\n", a.pid, a.name)); + ret; +} diff --git a/tests/fixtures/coff/generic_coff.etpy b/tests/fixtures/coff/generic_coff.etpy new file mode 100644 index 0000000..7769a4d --- /dev/null +++ b/tests/fixtures/coff/generic_coff.etpy @@ -0,0 +1,3 @@ +fn go(args: char*, len: int) -> void { + ret; +} diff --git a/tests/fixtures/coff/hashed_imports_bof.etpy b/tests/fixtures/coff/hashed_imports_bof.etpy new file mode 100644 index 0000000..ecea2cb --- /dev/null +++ b/tests/fixtures/coff/hashed_imports_bof.etpy @@ -0,0 +1,7 @@ +use bof; + +fn go(args: char*, len: int) -> void { + var h: u64 = Kernel32.GetCurrentProcessId(); + BeaconPrintf(0, str.format("pid={d}\n", (int)h)); + ret; +} diff --git a/tests/fixtures/coff/minimal_bof.etpy b/tests/fixtures/coff/minimal_bof.etpy new file mode 100644 index 0000000..7769a4d --- /dev/null +++ b/tests/fixtures/coff/minimal_bof.etpy @@ -0,0 +1,3 @@ +fn go(args: char*, len: int) -> void { + ret; +} diff --git a/tests/fixtures/coff/poly_bof.etpy b/tests/fixtures/coff/poly_bof.etpy new file mode 100644 index 0000000..809387c --- /dev/null +++ b/tests/fixtures/coff/poly_bof.etpy @@ -0,0 +1,10 @@ +use bof; + +fn go(args: char*, len: int) -> void { + var a: int = 0; + var b: int = 5; + if a != b { + BeaconPrintf(0, "poly bof\n"); + } + ret; +} diff --git a/tests/fixtures/debug_map.dbg b/tests/fixtures/debug_map.dbg new file mode 100644 index 0000000..e98c18d --- /dev/null +++ b/tests/fixtures/debug_map.dbg @@ -0,0 +1,13 @@ +# entropy debug map for tests/fixtures/debug_map.etpy +# offset line:col kind source +00000008 7:5 var tests/fixtures/debug_map.etpy +00000036 9:9 asm tests/fixtures/debug_map.etpy +00000036 9:9 asm tests/fixtures/debug_map.etpy +0000003d 11:5 ret tests/fixtures/debug_map.etpy +0000005f 2:5 var tests/fixtures/debug_map.etpy +00000087 3:5 ret tests/fixtures/debug_map.etpy + +# variables +# name : type : loc : line_start..line_end +a : int : rbp-0x8 : 7..max +y : int : rbp-0x10 : 2..max diff --git a/tests/fixtures/debug_map.etpy b/tests/fixtures/debug_map.etpy new file mode 100644 index 0000000..37a7ff7 --- /dev/null +++ b/tests/fixtures/debug_map.etpy @@ -0,0 +1,12 @@ +fn helper(x: int) -> int { + var y: int = x + 1; + ret y; +} + +fn main() -> int { + var a: int = helper(4); + asm { + mov rax, %a + } + ret a; +} diff --git a/tests/fixtures/negative/bad_struct_field.etpy b/tests/fixtures/negative/bad_struct_field.etpy new file mode 100644 index 0000000..b2a2c7f --- /dev/null +++ b/tests/fixtures/negative/bad_struct_field.etpy @@ -0,0 +1,9 @@ +struct Pair { + left: int, + right: int, +} + +fn main() -> int { + var p: Pair; + ret p.missing; +} diff --git a/tests/fixtures/negative/extern_in_shellcode.etpy b/tests/fixtures/negative/extern_in_shellcode.etpy new file mode 100644 index 0000000..cac2ec1 --- /dev/null +++ b/tests/fixtures/negative/extern_in_shellcode.etpy @@ -0,0 +1,6 @@ +extern fn BeaconPrintf(kind: int, msg: char*) -> void; + +fn main() -> int { + BeaconPrintf(0, (char*)"nope"); + ret 0; +} diff --git a/tests/fixtures/negative/index_str.etpy b/tests/fixtures/negative/index_str.etpy new file mode 100644 index 0000000..1182f6a --- /dev/null +++ b/tests/fixtures/negative/index_str.etpy @@ -0,0 +1,4 @@ +fn main() -> int { + var s: str = "not indexable"; + ret (int)s[0]; +} diff --git a/tests/fixtures/negative/mem_alloc_gc_off.etpy b/tests/fixtures/negative/mem_alloc_gc_off.etpy new file mode 100644 index 0000000..e78ac37 --- /dev/null +++ b/tests/fixtures/negative/mem_alloc_gc_off.etpy @@ -0,0 +1,4 @@ +fn main() -> int { + var p: u64 = mem.alloc(16); + ret (int)p; +} diff --git a/tests/fixtures/shellcode/arithmetic.bin b/tests/fixtures/shellcode/arithmetic.bin new file mode 100644 index 0000000000000000000000000000000000000000..3816f9f4311ef614e05c39a8ee65cf521d4bad4a GIT binary patch literal 152 zcmWIb=zQwY{Kmm!2Rj1 int { + var a: int = 7; + var b: int = 5; + var c: int = (a + b) * 3; + ret c - 4; +} diff --git a/tests/fixtures/shellcode/branching.bin b/tests/fixtures/shellcode/branching.bin new file mode 100644 index 0000000000000000000000000000000000000000..ae3308c67d567dc5cbc7cc53b3986ab191167e43 GIT binary patch literal 176 zcmWIb=zQwY{Kmjz2R8!*cyzY@`2YXE#|}<}zz2{(H&{FXsG0-F1`Bl_^k_c95#eEZ zf`9%2{%r?5S`YBIXaM!$Q>=O7r4~@hOJ#^UkU=m*!4|^|1*&5LDlt6Z(fKC!@B|0| E0C*5oxc~qF literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/branching.etpy b/tests/fixtures/shellcode/branching.etpy new file mode 100644 index 0000000..ea3e082 --- /dev/null +++ b/tests/fixtures/shellcode/branching.etpy @@ -0,0 +1,9 @@ +fn main() -> int { + var a: int = 11; + var b: int = 9; + if a > b { + ret a - b; + } else { + ret b - a; + } +} diff --git a/tests/fixtures/shellcode/direct_syscall_seeded.bin b/tests/fixtures/shellcode/direct_syscall_seeded.bin new file mode 100644 index 0000000000000000000000000000000000000000..d807aec7e7d2435f0991841500eaf5e7b2676543 GIT binary patch literal 820 zcmaJQiQ6{qJx))H3Z=JBEqnQV%4X`!#3`(^b*oIEH&$GR|6Mm z5~%^-P1be(k@nXUIKkzqY2aJHzY6ON>V^~Oi-~d>)42VCmK;pRoxlKchdNvllbBf3 zloT3@M|R@TE$S>FJ|}{Y!3hok{{*}VbbUX5gAm=Ob_ivVEr(>{ZGu;GcW{@wS9f1y zWTHGQ6|BBQE1|21kF>D}JOG@ly@9#enPg{lHpvoficRBDg-sBFk$G7n?#fvW=b45< z7f)qhVnlz?(gi$fLp-05m{wi_MsUW?$_wC4ga0!4Z`gL#m^YMX9gm?#OFpRFLyqVx zO@@K49`M063GN3ZuxgsV(&|=Q-Rg!GkG`k&QMBUj!FtmRmfPbxxO;=P|Fj3iNX^|V zJyo1bEx;MZrvh8Ve2+E`l1r}9NcE}Dgjd~T7Y(|L1wnPdy-P_FU9c{7tL#_pJlw+l F^$$HW{n!8i literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/direct_syscall_seeded.etpy b/tests/fixtures/shellcode/direct_syscall_seeded.etpy new file mode 100644 index 0000000..f0f5018 --- /dev/null +++ b/tests/fixtures/shellcode/direct_syscall_seeded.etpy @@ -0,0 +1,5 @@ +fn main() -> int { + var delay: i64 = -10000; + Ntdll.NtDelayExecution(0, &delay); + ret 0; +} diff --git a/tests/fixtures/shellcode/function_call.bin b/tests/fixtures/shellcode/function_call.bin new file mode 100644 index 0000000000000000000000000000000000000000..0dd34f2afad17ccdc55dbeb6ba7f5992638e386e GIT binary patch literal 176 zcmWIb=zQwY{6+zY4Lo+RFhGDuXN!sgkm<354M{`;B+}gjk?EeI0^+|A00}l9F?h)Y zWEdXs=zJ42jdAG!pO!> G00ICg=}3M6 literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/function_call.etpy b/tests/fixtures/shellcode/function_call.etpy new file mode 100644 index 0000000..c4ae045 --- /dev/null +++ b/tests/fixtures/shellcode/function_call.etpy @@ -0,0 +1,7 @@ +fn add_twice(a: int, b: int) -> int { + ret a + b + b; +} + +fn main() -> int { + ret add_twice(4, 6); +} diff --git a/tests/fixtures/shellcode/indirect_syscall_seeded.bin b/tests/fixtures/shellcode/indirect_syscall_seeded.bin new file mode 100644 index 0000000000000000000000000000000000000000..8138cad30590681529245e5b00d3614921570c59 GIT binary patch literal 1683 zcmbVMO=uKJ6t0*V6^*uH0v;6R5ZQQH%?d)mfQ3eDq$h;cL^m4UG1FkQ&A>Pjb5mv- zQkIPo0)l$b%Yy7tOaz4;H`x#s1Yy@*$Weu9@t_AWIcWR6uC}M6E`kL;Rqws8zV}hlNYXWcy-LtolJ~#Jm55bmn~DCv+TGZcYIrTy2!)<`xpw1+7+N zp`3i1WUP965p1QY7=Z^3Ypn?Rs&BGe2eUnJ;U-&fLx3>g4+;II+oUCmFdP*)?!uj= z@v!a#v-E&yn=!^J#&|_I6{FlpkwrOr&+8cTpaLCI(matrH+d_e-G40~ziDeNjMr2Z z<VahYktl?|~e;5B5oljMr_cYa0Dc*~*-vvZkP96wQI5Q(T$^BW{*JN5}Gi{Fy_bB8Ys^N-o z8qV0*{1anH1Yk(8}wD2q5ECwO%PkQ&Tb2ZXT7Tn%rOf>!id0i;0 z#3g~a!px5xKB!+6Lg)KZmnVd7_OH;lPPdq_k9uF|%C<7ju7ADR^$I|EKI5y+LYAy7Va1rl{ zrAw7m`rLfnIv=;*Ph~JN&U!$Ry(s;;NR+%f&qem85qeqzV{64*Mwr@=A_k{{RO%{z znYW3qsJkhgP1$~#vXb+{ZJLLox`F0}qW%pMx_=DjwERmWo)e<}6QZmUodIH6xGVmK zg8To}{#o!Y-KLp9`xIOWAOX{g&tZQ=uouMZuDJ14wwn=b8Ylf0N>eTT2tUmG_@SV` zqqGK#IotjbAL}e~+s^lAi&oD-p*WPaGuFv`VVIq;3WJut<);8-=Ce6-;AkOR80KuZ zRqQI{`*|)`uuh#~ajKn`ogMBtWAzUe2l9gpIdCnL&kfm@$PVUg>kA$P{9WrKE2}hX literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/indirect_syscall_seeded.etpy b/tests/fixtures/shellcode/indirect_syscall_seeded.etpy new file mode 100644 index 0000000..f0f5018 --- /dev/null +++ b/tests/fixtures/shellcode/indirect_syscall_seeded.etpy @@ -0,0 +1,5 @@ +fn main() -> int { + var delay: i64 = -10000; + Ntdll.NtDelayExecution(0, &delay); + ret 0; +} diff --git a/tests/fixtures/shellcode/inline_asm.bin b/tests/fixtures/shellcode/inline_asm.bin new file mode 100644 index 0000000000000000000000000000000000000000..e2138acab194ff4b609241122ad14ef8435f817c GIT binary patch literal 72 zcmWIb=zQwY{Kmjz2Llv%bhiHZ|NpUKuN;`9-VJu4^IFA E03JmkJpcdz literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/inline_asm.etpy b/tests/fixtures/shellcode/inline_asm.etpy new file mode 100644 index 0000000..43b03f6 --- /dev/null +++ b/tests/fixtures/shellcode/inline_asm.etpy @@ -0,0 +1,11 @@ +fn main() -> int { + var out: u64 = 0; + asm { + mov rax, 0x11 + add rax, 0x22 + marker: + db 0x90, 0x90 + mov %out, rax + } + ret (int)out; +} diff --git a/tests/fixtures/shellcode/loop_sum.bin b/tests/fixtures/shellcode/loop_sum.bin new file mode 100644 index 0000000000000000000000000000000000000000..918d11d070c951d9474bd2693d8822260ba75ebc GIT binary patch literal 192 zcmWIb=zQwY{Kmjz2O|RncyzY@`2YXE#||i;0VwbRB+v~O4*;s>@Yul$SAEc<`3OgZ zhvfX@jVT==JD?DrP)^)lh^Jkkx@)^Rf=&0+5L>nSiDn M9`NXV6MJ|90Jn8ovj6}9 literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/loop_sum.etpy b/tests/fixtures/shellcode/loop_sum.etpy new file mode 100644 index 0000000..7cfcd54 --- /dev/null +++ b/tests/fixtures/shellcode/loop_sum.etpy @@ -0,0 +1,9 @@ +fn main() -> int { + var i: int = 1; + var sum: int = 0; + while i <= 5 { + sum = sum + i; + i = i + 1; + } + ret sum; +} diff --git a/tests/fixtures/shellcode/message_box.bin b/tests/fixtures/shellcode/message_box.bin new file mode 100644 index 0000000000000000000000000000000000000000..0361919dfd6123b30ccd5898c94cb78b7d8cd141 GIT binary patch literal 857 zcmaJRKj2k77LtM&1dJ(V@#BJxcT+|4CBhQYR-L2LUdfI5|1$)c1!Pl(*k? zy~{&INj5-iO*OMP(xveU5bGemO8XQ=-ALc4tX2_bDj!wHMR3uL44`&kM=K*3l@GO` z6&uVXH#4bq)y*S6E2B@qjShhL1Y#T1+FpG2L25&-#IOytHB`#Z+dUA zGFgpN2h$hSdh9ClgCUoZM}TvMqt8rV`#U2N5E6LrX8VySwe~?y8naeNZch(28FV_S;^ty&liSujgv`rd`-fHoV>B zS;4u21vo<`N|Jm^f_H=>^0kJTi;-VJX@fiT98qidx9?NJxq=Kte-leuyyt=(bhE9# z9mk1|<7MDj19#5?BIbV%;=zrHRSew;)GxS~iO9o;{;C9%d_ST7V5Zsv48m$!0OnJMRu(wJr5WV&t{#iDYzXjq{wgFH8D MoG};ZF#o^)03pWy)c^nh literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/message_box.etpy b/tests/fixtures/shellcode/message_box.etpy new file mode 100644 index 0000000..c8a16e0 --- /dev/null +++ b/tests/fixtures/shellcode/message_box.etpy @@ -0,0 +1,4 @@ +fn main() -> int { + User32.MessageBoxA(0, "golden body", "golden title", 0); + ret 0; +} diff --git a/tests/fixtures/shellcode/poly_seeded.bin b/tests/fixtures/shellcode/poly_seeded.bin new file mode 100644 index 0000000000000000000000000000000000000000..6a3ff8078b62c71ae93c5ac05f7972dc9338ef91 GIT binary patch literal 168 zcmd;=Z(#s|G=6y(28Ia}LOnX4dNjW=@Yn$s1`-~ftv~+%|L?Jb7b*f2_y7{<22%k* z)f_-JSWWi{kLDvB5gwK&_@^G=-*&)5@c@5|F31pM#XDr-CLM&RXFLJ5 int { + var a: int = 0; + var b: int = 13; + if b != a { + a = b + 29; + } + ret a; +} diff --git a/tests/fixtures/shellcode/return_literal.bin b/tests/fixtures/shellcode/return_literal.bin new file mode 100644 index 0000000000000000000000000000000000000000..69c492dbf52436dae90d9c5c5b848f6916a3a042 GIT binary patch literal 32 icmWIb=zQwY{6@iJhZX|_ykr8>h6g-4-^3oCFaZFp> int { + ret 42; +} diff --git a/tests/fixtures/shellcode/stack_strings_seeded.bin b/tests/fixtures/shellcode/stack_strings_seeded.bin new file mode 100644 index 0000000000000000000000000000000000000000..1aa248d8a8cc174efd3761bf70ce9e036b4d4ec3 GIT binary patch literal 160 zcmWIb=zQwY{3gJ2w}5<-O$!SHgU62KZ07j)D#ax}hBrJqTW|dT|KDRrQW;P5ca`*f zh}Z+LSWPv1(2~r=B8b=vuvm4K0N50W*awhUFT{w>)*oOM3{VgNw3fr88zS0y(4+YX YN0bNuHWtB`Oh6Te2Ru68#2%gi041+Xvj6}9 literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/stack_strings_seeded.etpy b/tests/fixtures/shellcode/stack_strings_seeded.etpy new file mode 100644 index 0000000..600cbf4 --- /dev/null +++ b/tests/fixtures/shellcode/stack_strings_seeded.etpy @@ -0,0 +1,4 @@ +fn main() -> int { + var s: char* = (char*)"stack strings golden marker"; + ret (int)s[0]; +} diff --git a/tests/fixtures/shellcode/startupinfo_struct.bin b/tests/fixtures/shellcode/startupinfo_struct.bin new file mode 100644 index 0000000000000000000000000000000000000000..46cdbf4dc8f629abef9a4c8ac9f2c3e8ffaf2b60 GIT binary patch literal 344 zcmWIb=zQwY_+|nF1B2lKkIvQ!|Ns97QZpdb0tmGNLT!LhJ0R2n2z3HNU4T$GAk+g0 z^#VeDfKWd`)Q$q6O&}29(fo$PqZex7L67Dm91)$3AQ_JxTp$Vvu&Yr3%6RM$0Vx0i z>}phiG9JCGc{mh!bT&$WRe-JUZUC}T4C-!B0E&2YLY(Encmmm$?gp^w2+5aBK*gZY QdlP#&HLs*7zo3!<0NB{4C;$Ke literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/startupinfo_struct.etpy b/tests/fixtures/shellcode/startupinfo_struct.etpy new file mode 100644 index 0000000..fa5312b --- /dev/null +++ b/tests/fixtures/shellcode/startupinfo_struct.etpy @@ -0,0 +1,10 @@ +use_c "stdlib/win32/threading.h"; + +fn main() -> int { + var si: STARTUPINFOA; + si.cb = (u32)sizeof(STARTUPINFOA); + si.dwX = 10; + si.dwY = 20; + si.lpTitle = (void*)"entropy"; + ret (int)si.cb + (int)si.dwX + (int)si.dwY; +} diff --git a/tests/fixtures/shellcode/static_data.bin b/tests/fixtures/shellcode/static_data.bin new file mode 100644 index 0000000000000000000000000000000000000000..061519d2d7b9797dadfdf0f5792343c054beef7c GIT binary patch literal 112 zcmWIb=zQwY{Kmjz2Rj1o7lq>zyPX)0RTJ_BW(Zx literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/static_data.etpy b/tests/fixtures/shellcode/static_data.etpy new file mode 100644 index 0000000..efe0ff7 --- /dev/null +++ b/tests/fixtures/shellcode/static_data.etpy @@ -0,0 +1,7 @@ +static counter: u64 = 7; + +fn main() -> int { + var current: u64 = counter; + counter = current + 3; + ret (int)counter; +} diff --git a/tests/fixtures/shellcode/str_format.bin b/tests/fixtures/shellcode/str_format.bin new file mode 100644 index 0000000000000000000000000000000000000000..48936eb2407b9819a5a9bf6b0c96bffaca8ecc5d GIT binary patch literal 1368 zcmeHG&r4KM6n^SRkk&I)Xp!(1GvKPUA{uDGYe@ISn;0mSeyEMjvn+Mw%|s(DN@ga! z-Fa!$BC17;b}j=)C}PZ#LI@11h06$hA!Ms&Hod-k-)IUg`UAS~xaWN5JLi1&-1|By z`%2TREh}pkibCz`r&ZWkw&=Pp;0T>n%8Rm$zZv#cq(vehd>%<~4ol?w%Rfk)K)6>D z1Wmd*uNc4Y7bOhtB^o^EZxeBQ$2}p$ZfwHlHr_%5ntmOD!0VpCHBVsMRymr;kJ2AnNUEP!| zQudjrkBAbsud0JjOu!6h->DfJ#tGY318ny?HU^`EPiZpLP#scTnVtCRXmCl*HXuwz$$DY93)C$*%NT|3&O0x#6F%V=5o8}%^9`rr?gq#GCBY*OwEoEItlrqF8GPc&>ge=O=MM(~T88OBpF_;f`0 zYW^9;PVLYG@qk#-VBZY(gDyEz=4Hv5!59i^#&hzwfhqcUkziAEyFBNNa3(QmMNbzr zeY~_eTG||))55{GYPJbfoSvp%RnoLm;n+C6p_V&sL$I~rY!{qa=#q;57ybaD`UX78s_PNf*&C0sAQ)Baj2@3sLL;3m;O)); zAnr$*E^my@q3pCoKcQA0OwcC(O1rZ`6BBjdiKpL8`baP7A?M`9O-;o|O)Hv=TgvEQ zEELcW9gJz0%v7i)CG3 int { + var name: char* = (char*)"operator"; + var s: str = str.format("pid={d} hex=0x{x} name={s}", 1234, 0xBEEF, name); + User32.MessageBoxA(0, s, "format", 0); + ret 0; +} diff --git a/tests/fixtures/shellcode/string_layout.bin b/tests/fixtures/shellcode/string_layout.bin new file mode 100644 index 0000000000000000000000000000000000000000..54c02374f39b4d611b7a9f1db20c8fc38413bcb3 GIT binary patch literal 94 zcmWIb=zQwY{Kmkem(`Mifx)A*^~eAJ|2=j9**uxVhOi0ZuDatRXR7lUyNlDF9$W1KDPAy^p02&)62LJ#7 literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/string_layout.etpy b/tests/fixtures/shellcode/string_layout.etpy new file mode 100644 index 0000000..e7afe6b --- /dev/null +++ b/tests/fixtures/shellcode/string_layout.etpy @@ -0,0 +1,4 @@ +fn main() -> int { + var s: char* = (char*)"entropy golden marker"; + ret (int)s[0]; +} diff --git a/tests/fixtures/shellcode/strings_xor_seeded.bin b/tests/fixtures/shellcode/strings_xor_seeded.bin new file mode 100644 index 0000000000000000000000000000000000000000..cfe3ef5525bca8e7aaac88e0c2703b86c788133d GIT binary patch literal 276 zcmWIb=zQwY{Knu#3j+g#M=$Fg5Y^fGxB><$Tmp2tm$;N@yTml*0tN~>D0PPnB5*de#HLrJkS@8k&Y2jk int { + var s: char* = (char*)"xor encrypted golden marker"; + if s[0] == (u8)0x78 { + ret 1; + } + ret 0; +} diff --git a/tests/fixtures/shellcode/try_raise.bin b/tests/fixtures/shellcode/try_raise.bin new file mode 100644 index 0000000000000000000000000000000000000000..d4af215acd0e7c711c307e0285d2230185254931 GIT binary patch literal 1169 zcmb_bPe>F|7$0?)BL9q`3qb}GwBW_cIxXa3DCteihFEI<)YP4WwYsrN=^@J21)s0M zJQd}^OQ(*4Zq#9IO%f^aP!=BgN8pr)x~&9FzwgZ`26gMf%=i6%-|v0z`{sSq#P}DM zT06M0F7%9t6;kn< z0x3S`tC=YMK;iU)I$Tb25q?Wp)V!lpKC+DKD|5A3rHyI-hv*h zEhYlti?l`Dq*_gTQiCzX1m#uC$Y*u92iIzfM-7D4nJEU#?7Ek9>pXAruO|P&mK<5; zC8?RlXUM9<9+(*grx-(d9Y(#q(*x%SjNz6QBbC*Rq4llN^{vraEgE^DruRUK?pf+t zAxk+0j*ae3)jw$)g0)#^2Wc{>OD={Q(j`QR2ShjrXp4BF!8du`Jai^s$EcRWTMhrh zoKV#_5XKDM1!?f-p5iCC)uIhWtEadw74yt4{RTpJU(4{y-jb`FUX6-{Jkfh>T4uNjBh8yZ{;NlE_mpkhjJd8l!)W42>E7w zbO1mO{TKx^_#$%QzsX&;lD$^^@S**&cw8|P9Wk@JGue?G)Rp5_e?v0SrN?4PtFKS_ qLkP0j>grD!&KP+T)zPsHDjLu literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/try_raise.etpy b/tests/fixtures/shellcode/try_raise.etpy new file mode 100644 index 0000000..bdff272 --- /dev/null +++ b/tests/fixtures/shellcode/try_raise.etpy @@ -0,0 +1,9 @@ +fn main() -> int { + var got: int = 0; + try { + raise 0x44; + } catch err { + got = err; + } + ret got; +} diff --git a/tests/fixtures/shellcode/win32_struct.bin b/tests/fixtures/shellcode/win32_struct.bin new file mode 100644 index 0000000000000000000000000000000000000000..ab0db31784b86dd8be95c1af4f94c5c2d008d1ad GIT binary patch literal 1250 zcma)*Pe>GT6vs#1Xh~}n6+9HoA;CkG7DNjPO=7)?*;vRwo8{kjp+BNB7U<1oUD#jK zklms>cnrD(k-JcbF-;61^k4~IgbH@TgWc9k(|f-+hJr)`!{`0^zW00c`~9XJ#1BYq z#!3{Wuv)4pO)iMc@^9ZqtT8qi1;!Sm$WVN#3^OVjbqtNsz-VNgV_aYWBgTj``WUwv zBaAV|1S89sVmxI`GhQ-YGx7|VvB+3vtT8qi1;!SmNXR$hw$KxU^kx{GU}I(>Js*yX z6lE}>`L$zZWX)^)>%yeVw~`zm*5ujQaf8-r_XG&pzDur=4OKzDEszF=)EmF(cp9$xFD^ru`~LzM6XjNN}1jVid2xh?-JYK)Mn5s)YFel zQ^(N*ao{b&5FbrJiV;$j?I_N&f|?p)0i45qXzBi{ESD#SH^-^%l+iEPs~;^7U-~y1 z^j^J3@78b16Pdb~w1$#{XHK6?3=S&haDT$QGhp>w*u<>=9zF S<$+}-hol74MuOV^^Z5fSZ2O4- literal 0 HcmV?d00001 diff --git a/tests/fixtures/shellcode/win32_struct.etpy b/tests/fixtures/shellcode/win32_struct.etpy new file mode 100644 index 0000000..a3ba365 --- /dev/null +++ b/tests/fixtures/shellcode/win32_struct.etpy @@ -0,0 +1,10 @@ +use_c "stdlib/win32/toolhelp.h"; + +fn main() -> int { + var pe: PROCESSENTRY32; + pe.dwSize = (u32)sizeof(PROCESSENTRY32); + pe.th32ProcessID = (u32)Kernel32.GetCurrentProcessId(); + pe.szExeFile[0] = (u8)0x45; + pe.szExeFile[1] = (u8)0x54; + ret (int)pe.dwSize + (int)pe.szExeFile[0]; +} diff --git a/tests/fixtures/shellcode/winapi_call.bin b/tests/fixtures/shellcode/winapi_call.bin new file mode 100644 index 0000000000000000000000000000000000000000..4f645f203c96eaaa52cab1f94c6afc7a46611905 GIT binary patch literal 902 zcma)4O=uHA6y7!wi%lJ+33yQ0LkqpAjh+G)97^b0*+@Xs`j^(!?4dQbuvM`qEt`dm z)4>P|q8_}9Cm|{5VQXtq3PKOsLyuDEN)PsyTwLEwl3Kxo1H12i@B7}HH}fU~Y!jUA z{;jiZnud-Z#D+rBTh@WOwIB$9E0adD0~WOA0=9^nj!TgEM4ou0;(_at;&EhUHCVuE zY5^BUtj^qousmBN@y-N2<}wh>tcg4?hO%tJ(V4=+)iU=|!5M!*m31 zqR#>z?!7Kk_ygIiDPHMl4E|Eq)y%|qQoMziW%Ac)iAnV>pan57DF?!8Ci`Ks9a!^K zo0CdY!E3QDq=R-R< zew#1w^d@QUv>(a1=O2)oKcmGS+=vYE7x>rwlQML6loFtFA83!jqg z`aU9H`*zS6>|;amOe#VxMYf?>SBe!7U2^a=_^Q&?#PIQnm@GttZ&>T8Ob}hUf65BG zvb*RtUY)btN5|+W&C+XXaOLt%%bv0dr%#>87Ydp=oy(gO<95!TrP`2nJ8e(jp!vLQ e&CF int { + var pid: u64 = Kernel32.GetCurrentProcessId(); + if pid == 0 { + Kernel32.ExitProcess(1); + } + ret (int)pid; +} diff --git a/tests/shellcode_golden.rs b/tests/shellcode_golden.rs new file mode 100644 index 0000000..7aa4192 --- /dev/null +++ b/tests/shellcode_golden.rs @@ -0,0 +1,163 @@ +use assert_cmd::prelude::*; +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::Command; + +macro_rules! golden_test { + ($test_name:ident, $fixture:literal $(, $arg:literal)* $(,)?) => { + #[test] + fn $test_name() { + assert_fixture_matches_golden($fixture, &[$($arg),*]); + } + }; +} + +golden_test!(return_literal_matches_golden, "return_literal"); +golden_test!(arithmetic_matches_golden, "arithmetic"); +golden_test!(branching_matches_golden, "branching"); +golden_test!(loop_sum_matches_golden, "loop_sum"); +golden_test!(function_call_matches_golden, "function_call"); +golden_test!(inline_asm_matches_golden, "inline_asm"); +golden_test!(static_data_matches_golden, "static_data"); +golden_test!(string_layout_matches_golden, "string_layout"); +golden_test!(winapi_call_matches_golden, "winapi_call"); +golden_test!(win32_struct_matches_golden, "win32_struct"); +golden_test!(message_box_matches_golden, "message_box"); +golden_test!(str_format_matches_golden, "str_format"); +golden_test!(startupinfo_struct_matches_golden, "startupinfo_struct"); +golden_test!(try_raise_matches_golden, "try_raise"); +golden_test!( + stack_strings_seeded_matches_golden, + "stack_strings_seeded", + "--opsec=stack_strings", + "--seed=0x1234", +); +golden_test!( + strings_xor_seeded_matches_golden, + "strings_xor_seeded", + "--opsec=strings_xor", + "--seed=0x1234", +); +golden_test!( + direct_syscall_seeded_matches_golden, + "direct_syscall_seeded", + "--opsec=direct_syscalls", + "--seed=0x1234", +); +golden_test!( + indirect_syscall_seeded_matches_golden, + "indirect_syscall_seeded", + "--opsec=indirect_syscalls", + "--seed=0x1234", +); +golden_test!( + poly_seeded_matches_golden, + "poly_seeded", + "--opsec=poly,nop_sled", + "--seed=0x1234", +); + +#[test] +fn seeded_opsec_is_deterministic() { + let first = compile_fixture("poly_seeded", &["--opsec=poly,nop_sled", "--seed=0x1234"]); + let second = compile_fixture("poly_seeded", &["--opsec=poly,nop_sled", "--seed=0x1234"]); + assert_bin_eq("poly_seeded deterministic rebuild", &first, &second); +} + +#[test] +fn different_opsec_seeds_change_output() { + let first = compile_fixture("poly_seeded", &["--opsec=poly,nop_sled", "--seed=0x1234"]); + let second = compile_fixture("poly_seeded", &["--opsec=poly,nop_sled", "--seed=0x5678"]); + assert_ne!( + first, second, + "expected poly_seeded shellcode to differ when compiled with different seeds", + ); +} + +fn assert_fixture_matches_golden(name: &str, extra_args: &[&str]) { + let actual = compile_fixture(name, extra_args); + let expected = std::fs::read(golden_path(name)) + .unwrap_or_else(|err| panic!("read golden `{name}`: {err}")); + assert_bin_eq(name, &actual, &expected); +} + +fn compile_fixture(name: &str, extra_args: &[&str]) -> Vec { + let temp = tempfile::tempdir().expect("create temp output dir"); + let out = temp.path().join(format!("{name}.bin")); + let src = source_path(name); + + let mut cmd = Command::cargo_bin("entc").expect("resolve entc test binary"); + cmd.arg("compile") + .arg(&src) + .arg("--type=standard") + .arg("--gc=off") + .arg("-o") + .arg(&out) + .args(extra_args); + + let output = cmd.assert().success().get_output().clone(); + std::fs::read(&out).unwrap_or_else(|err| { + panic!( + "read compiled output `{}` after entc success\nstderr:\n{}error: {err}", + out.display(), + String::from_utf8_lossy(&output.stderr), + ) + }) +} + +fn source_path(name: &str) -> PathBuf { + fixture_dir().join(format!("{name}.etpy")) +} + +fn golden_path(name: &str) -> PathBuf { + fixture_dir().join(format!("{name}.bin")) +} + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("shellcode") +} + +fn assert_bin_eq(label: &str, actual: &[u8], expected: &[u8]) { + if actual == expected { + return; + } + + let first_diff = actual + .iter() + .zip(expected.iter()) + .position(|(a, e)| a != e) + .unwrap_or_else(|| actual.len().min(expected.len())); + let start = first_diff.saturating_sub(8); + let end = (first_diff + 16).min(actual.len().max(expected.len())); + + panic!( + "{label}: shellcode bytes differ at offset 0x{first_diff:x}\n\ + actual len: {}\n\ + expected len: {}\n\ + actual: {}\n\ + expected: {}", + actual.len(), + expected.len(), + hex_window(actual, start, end), + hex_window(expected, start, end), + ); +} + +fn hex_window(bytes: &[u8], start: usize, end: usize) -> String { + let mut out = String::new(); + for i in start..end { + if i == start { + let _ = write!(out, "{i:08x}:"); + } + match bytes.get(i) { + Some(byte) => { + let _ = write!(out, " {byte:02x}"); + } + None => out.push_str(" --"), + } + } + out +} diff --git a/tests/typecheck_negative.rs b/tests/typecheck_negative.rs new file mode 100644 index 0000000..30371ba --- /dev/null +++ b/tests/typecheck_negative.rs @@ -0,0 +1,73 @@ +use assert_cmd::prelude::*; +use std::path::{Path, PathBuf}; +use std::process::Command; + +struct NegativeCase { + name: &'static str, + args: &'static [&'static str], + stderr_contains: &'static str, +} + +const CASES: &[NegativeCase] = &[ + NegativeCase { + name: "bad_struct_field", + args: &[], + stderr_contains: "struct `Pair` has no field `missing`", + }, + NegativeCase { + name: "index_str", + args: &[], + stderr_contains: "cannot index a value of type `str`", + }, + NegativeCase { + name: "extern_in_shellcode", + args: &[], + stderr_contains: "can only be called when compiled", + }, + NegativeCase { + name: "mem_alloc_gc_off", + args: &["--gc=off"], + stderr_contains: "build was configured with `--gc=off`", + }, +]; + +#[test] +fn invalid_programs_fail_with_stable_diagnostics() { + for case in CASES { + let stderr = compile_negative(case); + let stderr = String::from_utf8_lossy(&stderr); + assert!( + stderr.contains(case.stderr_contains), + "{}: expected stderr to contain `{}`\nactual stderr:\n{}", + case.name, + case.stderr_contains, + stderr, + ); + } +} + +fn compile_negative(case: &NegativeCase) -> Vec { + let temp = tempfile::tempdir().expect("create temp output dir"); + let out = temp.path().join(format!("{}.bin", case.name)); + + Command::cargo_bin("entc") + .expect("resolve entc test binary") + .arg("compile") + .arg(fixture_dir().join(format!("{}.etpy", case.name))) + .arg("--type=standard") + .arg("-o") + .arg(&out) + .args(case.args) + .assert() + .failure() + .get_output() + .stderr + .clone() +} + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("negative") +} diff --git a/tools/bof-loader/src/args.rs b/tools/bof-loader/src/args.rs index 4811a06..37b2420 100644 --- a/tools/bof-loader/src/args.rs +++ b/tools/bof-loader/src/args.rs @@ -5,12 +5,14 @@ //! byte sequence the BOF will see in its `args` buffer. The flag //! vocabulary matches the `bof-runner` CLI: //! +//! ```text //! --args RAW raw bytes, no length prefix. For BOFs that //! treat `args` as a C string. //! --zarg STR `bof_pack "z"` - `[u32 length LE][bytes][NUL]`. //! --iarg N `bof_pack "i"` - `[u32 BE]` (network byte order). //! --sarg N `bof_pack "s"` - `[u16 BE]`. //! --barg @FILE `bof_pack "b"` - `[u32 length LE][bytes]`. +//! ``` //! //! Order on the input list determines order in the output buffer - //! the BOF reads with `BeaconDataExtract` / `BeaconDataInt` etc. in diff --git a/tools/bof-loader/src/coff.rs b/tools/bof-loader/src/coff.rs index d5a6039..973289a 100644 --- a/tools/bof-loader/src/coff.rs +++ b/tools/bof-loader/src/coff.rs @@ -108,3 +108,128 @@ pub struct SectionInfo<'a> { pub relocs: &'a [u8], pub characteristics: u32, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_minimal_text_object() { + let bytes = minimal_coff(false); + let parsed = ParsedCoff::parse(&bytes).unwrap(); + + assert_eq!(parsed.n_sections, 1); + assert_eq!(parsed.n_symbols, 1); + + let text = parsed.section(0); + assert_eq!(text.name, ".text"); + assert_eq!(text.raw, &[0xC3]); + assert!(text.relocs.is_empty()); + + assert_eq!(parsed.symbol_name(0), "go"); + assert_eq!(parsed.symbol_value(0), 0); + assert_eq!(parsed.symbol_section(0), 1); + } + + #[test] + fn parses_relocations_and_long_symbol_names() { + let bytes = minimal_coff(true); + let parsed = ParsedCoff::parse(&bytes).unwrap(); + + let text = parsed.section(0); + assert_eq!(text.relocs.len(), 10); + assert_eq!(u32_at(text.relocs, 0), 1); + assert_eq!(u32_at(text.relocs, 4), 0); + assert_eq!(u16_at(text.relocs, 8), IMAGE_REL_AMD64_REL32); + + assert_eq!(parsed.symbol_name(0), "__imp_KERNEL32$VeryLongImportedFunction"); + assert_eq!(parsed.symbol_section(0), 0); + } + + #[test] + fn rejects_non_amd64_objects() { + let mut bytes = minimal_coff(false); + bytes[0..2].copy_from_slice(&0x014cu16.to_le_bytes()); + assert!(ParsedCoff::parse(&bytes).err().unwrap().contains("expected AMD64")); + } + + fn minimal_coff(long_symbol_with_reloc: bool) -> Vec { + let raw = if long_symbol_with_reloc { + vec![0xE8, 0, 0, 0, 0] + } else { + vec![0xC3] + }; + let raw_off = 20 + 40; + let reloc_off = raw_off + raw.len(); + let n_relocs = if long_symbol_with_reloc { 1u16 } else { 0u16 }; + let symtab_off = reloc_off + (n_relocs as usize * 10); + + let mut out = Vec::new(); + out.extend_from_slice(&IMAGE_FILE_MACHINE_AMD64.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&(symtab_off as u32).to_le_bytes()); + out.extend_from_slice(&1u32.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + + let mut name = [0u8; 8]; + name[..5].copy_from_slice(b".text"); + out.extend_from_slice(&name); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&(raw.len() as u32).to_le_bytes()); + out.extend_from_slice(&(raw_off as u32).to_le_bytes()); + let reloc_ptr = if n_relocs == 0 { 0 } else { reloc_off as u32 }; + out.extend_from_slice(&reloc_ptr.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&n_relocs.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0x6050_0020u32.to_le_bytes()); + + out.extend_from_slice(&raw); + + if long_symbol_with_reloc { + out.extend_from_slice(&1u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&IMAGE_REL_AMD64_REL32.to_le_bytes()); + } + + let mut strtab = Vec::new(); + strtab.extend_from_slice(&4u32.to_le_bytes()); + if long_symbol_with_reloc { + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&4u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&0i16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.push(2); + out.push(0); + + strtab.extend_from_slice(b"__imp_KERNEL32$VeryLongImportedFunction"); + strtab.push(0); + let len = strtab.len() as u32; + strtab[0..4].copy_from_slice(&len.to_le_bytes()); + } else { + let mut sym_name = [0u8; 8]; + sym_name[..2].copy_from_slice(b"go"); + out.extend_from_slice(&sym_name); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&1i16.to_le_bytes()); + out.extend_from_slice(&0x20u16.to_le_bytes()); + out.push(2); + out.push(0); + } + + out.extend_from_slice(&strtab); + out + } + + fn u16_at(bytes: &[u8], off: usize) -> u16 { + u16::from_le_bytes(bytes[off..off + 2].try_into().unwrap()) + } + + fn u32_at(bytes: &[u8], off: usize) -> u32 { + u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap()) + } +} diff --git a/tools/entc-debug/src/dap.rs b/tools/entc-debug/src/dap.rs index 70c801e..0c6ce58 100644 --- a/tools/entc-debug/src/dap.rs +++ b/tools/entc-debug/src/dap.rs @@ -1916,6 +1916,7 @@ fn arm_pause(_session: &Arc, _join: &thread::JoinHandle<()>) -> b /// shellcode paths. Same-process reads are fine because the tracer /// shares its address space with the DAP server (they're threads in /// the same process). +#[cfg(windows)] unsafe fn try_read_bytes(addr: usize, out: &mut [u8]) -> usize { use windows_sys::Win32::System::Memory::{ VirtualQuery, MEMORY_BASIC_INFORMATION, @@ -1950,6 +1951,11 @@ unsafe fn try_read_bytes(addr: usize, out: &mut [u8]) -> usize { copied } +#[cfg(not(windows))] +unsafe fn try_read_bytes(_addr: usize, _out: &mut [u8]) -> usize { + 0 +} + // ---------------------------------------------------------------- base64 /// Tiny base64 encoder for readMemory responses. Inlined to avoid a