From f33dee689817906a1b58d0a08ba3f6966520415a Mon Sep 17 00:00:00 2001 From: Smile Date: Wed, 15 Jul 2026 12:22:46 +0800 Subject: [PATCH 1/2] Add ARM32 EABI5 cross-platform packing support --- .gitignore | 3 +- ARM32_EABI5_HANDOFF.md | 166 +++++++++++++++ ARM64_PACKER_HANDOFF.md | 300 +++++++++++++++++++++++++++ CHANGES_FROM_UPSTREAM.md | 117 +++++++++++ CHANGES_FROM_UPSTREAM.zh-CN.md | 109 ++++++++++ Makefile | 334 +++++++++++++++++++++++------- README.md | 367 ++++++++++----------------------- include/common.h | 74 ++++++- include/elf64.h | 63 +++++- loader/loader.c | 14 +- packer/crypto.c | 62 +++--- packer/elf64.c | 36 +++- packer/obfuscation.c | 23 ++- packer/packer.c | 24 ++- stubgen/stubgen.c | 290 ++++++++++++++++++++------ tests/arm32_eabi5_test.sh | 82 ++++++++ tests/platform_layout_test.sh | 37 ++++ tests/runtime_guard_test.sh | 24 +++ tests/unit_test.sh | 91 ++++---- 19 files changed, 1723 insertions(+), 493 deletions(-) create mode 100644 ARM32_EABI5_HANDOFF.md create mode 100644 ARM64_PACKER_HANDOFF.md create mode 100644 CHANGES_FROM_UPSTREAM.md create mode 100644 CHANGES_FROM_UPSTREAM.zh-CN.md create mode 100644 tests/arm32_eabi5_test.sh create mode 100644 tests/platform_layout_test.sh create mode 100644 tests/runtime_guard_test.sh diff --git a/.gitignore b/.gitignore index 1899660..3289803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build -.vscode \ No newline at end of file +.vscode +.codex_tmp diff --git a/ARM32_EABI5_HANDOFF.md b/ARM32_EABI5_HANDOFF.md new file mode 100644 index 0000000..bf20cd4 --- /dev/null +++ b/ARM32_EABI5_HANDOFF.md @@ -0,0 +1,166 @@ +# hARMless ARM32/EABI5 Packer Handoff + +Date: 2026-07-15 + +## Purpose + +This handoff is for integrating hARMless into a project whose build output is +ARM32/EABI5 ELF. + +The supported workflow is: + +- Build ARM32/EABI5 target artifacts. +- Run host-native x86_64 WSL tools to pack an ARM32/EABI5 ELF. +- Run the packed output on native ARM32 Linux or ARM64 Linux with 32-bit ARM + compatibility enabled. + +## Build Outputs + +Run: + +```bash +make all32 +make verify-build32 +``` + +Expected outputs: + +```text +build/ARM32_EABI5/packer +build/ARM32_EABI5/loader +build/ARM32_EABI5/stubgen +build/X86_X64/packer-arm32 +build/X86_X64/stubgen +``` + +On x86_64 WSL: + +- `build/X86_X64/packer-arm32` validates and encrypts ARM32/EABI5 input ELF. +- `build/X86_X64/stubgen` patches the ARM32 loader and emits the final binary. +- `build/ARM32_EABI5/loader` is embedded into the final packed executable. + +## Dependencies On x86_64 WSL + +```bash +sudo dpkg --add-architecture armhf +sudo apt-get update +sudo apt-get install -y \ + gcc-arm-linux-gnueabihf \ + binutils-arm-linux-gnueabihf \ + qemu-user \ + libssl-dev:amd64 \ + libssl-dev:armhf \ + zlib1g-dev:armhf \ + libzstd-dev:armhf +``` + +Or: + +```bash +make install-deps32 +``` + +## Pack Command + +Use this command from the hARMless repository root: + +```bash +make pack32 INPUT=/path/to/input_arm32_eabi5_elf OUTPUT=/path/to/output_packed_arm32_eabi5_elf +``` + +Equivalent direct command sequence: + +```bash +./build/X86_X64/packer-arm32 /path/to/input_arm32_eabi5_elf /tmp/payload.packed +./build/X86_X64/stubgen \ + ./build/ARM32_EABI5/loader \ + /tmp/payload.packed \ + /path/to/output_packed_arm32_eabi5_elf +rm -f /tmp/payload.packed +``` + +The final output should report as ARM32/EABI5: + +```bash +file /path/to/output_packed_arm32_eabi5_elf +readelf -h /path/to/output_packed_arm32_eabi5_elf | grep "Version5 EABI" +``` + +## Consuming Project Wrapper + +Minimal wrapper for another project: + +```bash +#!/bin/bash +set -euo pipefail + +HARMLESS_ROOT=/path/to/hARMless +INPUT_ARM32_ELF="$1" +OUTPUT_ARM32_PACKED="$2" +TMP_PACKED="${OUTPUT_ARM32_PACKED}.packed" + +file "$INPUT_ARM32_ELF" | grep -q "ARM" +readelf -h "$INPUT_ARM32_ELF" | grep -q "Version5 EABI" + +"$HARMLESS_ROOT/build/X86_X64/packer-arm32" "$INPUT_ARM32_ELF" "$TMP_PACKED" +"$HARMLESS_ROOT/build/X86_X64/stubgen" \ + "$HARMLESS_ROOT/build/ARM32_EABI5/loader" \ + "$TMP_PACKED" \ + "$OUTPUT_ARM32_PACKED" +rm -f "$TMP_PACKED" +``` + +## Validation Evidence + +Local x86_64 WSL: + +```bash +make all32 +make verify-build32 +make test32 +``` + +Validated output types: + +```text +build/ARM32_EABI5/packer: ELF 32-bit LSB pie executable, ARM, EABI5 +build/ARM32_EABI5/loader: ELF 32-bit LSB executable, ARM, EABI5, statically linked +build/ARM32_EABI5/stubgen: ELF 32-bit LSB executable, ARM, EABI5, statically linked +build/X86_X64/packer-arm32: ELF 64-bit LSB pie executable, x86-64 +build/X86_X64/stubgen: ELF 64-bit LSB executable, x86-64, statically linked +``` + +Runtime validation on ARM64 Linux with 32-bit ARM compatibility: + +- Original ARM32/EABI5 static payload wrote `/tmp/hARMless_arm32_payload_marker`. +- Packed ARM32/EABI5 payload also wrote the same marker. +- Both returned status 0. + +Observed packed output: + +```text +ELF 32-bit LSB executable, ARM, EABI5 version 1, statically linked, no section header +``` + +## qemu-user Note + +`qemu-arm` is useful for checking that an unprotected ARM32/EABI5 binary starts, +but it is not a reliable validator for this packed loader. The loader reads its +own executable image and includes anti-analysis checks; qemu-user can alter +`/proc/self/exe` behavior or trigger those checks. Use native ARM32 Linux or +ARM64 Linux with 32-bit ARM compatibility for runtime acceptance. + +## Important Implementation Notes + +- ARM32/EABI5 support uses ELF32 symbol/section parsing in `stubgen`. +- ARM32 loader uses ARM EABI direct syscalls. +- ARM32 target disables the io_uring write path and uses the plain write path. +- AES was changed to AES-256-CTR so encrypted payload size always matches the + original ELF size across ARM32 and ARM64. + +## Current Boundaries + +- ARM32 target is `arm-linux-gnueabihf` / EABI5. +- Hard-float ARMHF is the validated ABI. +- The packed output is a Linux ARM32 executable, not Android APK/so packaging. +- Runtime validation still needs a native/compat ARM environment, not only qemu. diff --git a/ARM64_PACKER_HANDOFF.md b/ARM64_PACKER_HANDOFF.md new file mode 100644 index 0000000..5fda51e --- /dev/null +++ b/ARM64_PACKER_HANDOFF.md @@ -0,0 +1,300 @@ +# hARMless ARM64 Packer Handoff + +Date: 2026-07-15 + +Note: if the consuming project outputs ARM32/EABI5 ELF, use +`ARM32_EABI5_HANDOFF.md` instead of this ARM64-specific handoff. + +## Purpose + +This handoff documents the current usable result from this checkout for reuse by +another project: + +- Build succeeds on the current x86_64 WSL host. +- Build succeeds and runtime validation passes on an ARM64 Linux server. +- The supported mainline use case is packing ARM64 ELF files. +- On x86_64 WSL, host-native tools pack ARM64 ELF files by combining the ARM64 + loader stub with encrypted payload data. + +No unrelated x86 target workflow is required for the current integration. + +## Source State + +- Repository: `litemars/hARMless` +- Synced upstream commit: `46b3630` +- Local branch: `main` +- Upstream status at handoff: local `HEAD` matches `origin/main` + +Local changes on top of upstream provide: + +- Platform-labeled build output directories. +- x86_64 WSL host tools for ARM64 ELF packing. +- ARM64 loader and ARM64-native tools. +- Smoke tests for platform layout and pack flow. +- ARM64 runtime test that runs only on ARM64 Linux. +- README usage updated for the ARM64 packing workflow. + +## Output Layout + +After running `make all`, the build outputs are: + +```text +build/ARM64/packer +build/ARM64/loader +build/ARM64/stubgen +build/X86_X64/packer +build/X86_X64/stubgen +``` + +On x86_64 WSL: + +- `build/X86_X64/packer` runs on the x86_64 WSL host. +- `build/X86_X64/stubgen` runs on the x86_64 WSL host. +- `build/ARM64/loader` is embedded into the final packed ARM64 executable. +- The final packed output is ARM64 ELF and must run on ARM64 Linux. + +On ARM64 Linux: + +- `build/ARM64/packer`, `build/ARM64/loader`, and `build/ARM64/stubgen` run + natively. + +## Dependencies + +### x86_64 Ubuntu/WSL Host + +```bash +sudo dpkg --add-architecture arm64 +sudo apt-get update +sudo apt-get install -y \ + gcc-aarch64-linux-gnu \ + binutils-aarch64-linux-gnu \ + libssl-dev:amd64 \ + libssl-dev:arm64 \ + zlib1g-dev:arm64 \ + libzstd-dev:arm64 +``` + +The local WSL apt source has been changed to Tsinghua mirrors and verified with: + +```bash +sudo apt-get update -qq +``` + +### ARM64 Linux Host + +```bash +sudo apt-get update +sudo apt-get install -y \ + build-essential \ + make \ + gcc \ + file \ + binutils \ + pkg-config \ + libssl-dev \ + zlib1g-dev \ + libzstd-dev +``` + +## Build Commands + +Run from the repository root. + +```bash +make clean +make all +make verify-build +``` + +Expected x86_64 WSL result: + +- `build/ARM64/packer`: ARM aarch64 ELF +- `build/ARM64/loader`: ARM aarch64 ELF, statically linked +- `build/ARM64/stubgen`: ARM aarch64 ELF, statically linked +- `build/X86_X64/packer`: x86-64 ELF +- `build/X86_X64/stubgen`: x86-64 ELF, statically linked + +Expected ARM64 Linux result: + +- `build/ARM64/packer`: ARM aarch64 ELF +- `build/ARM64/loader`: ARM aarch64 ELF, statically linked +- `build/ARM64/stubgen`: ARM aarch64 ELF, statically linked + +## Packing Commands + +### Pack ARM64 ELF on x86_64 WSL + +Use this in the current local WSL environment: + +```bash +make pack INPUT=/path/to/input_arm64_elf OUTPUT=/path/to/output_packed_arm64_elf +``` + +Equivalent direct commands: + +```bash +./build/X86_X64/packer /path/to/input_arm64_elf /tmp/payload.packed +./build/X86_X64/stubgen ./build/ARM64/loader /tmp/payload.packed /path/to/output_packed_arm64_elf +``` + +Run the generated output on ARM64 Linux: + +```bash +chmod +x /path/to/output_packed_arm64_elf +/path/to/output_packed_arm64_elf +``` + +### Pack ARM64 ELF on ARM64 Linux + +```bash +make pack INPUT=/path/to/input_arm64_elf OUTPUT=/path/to/output_packed_arm64_elf +./output_packed_arm64_elf +``` + +Equivalent direct commands: + +```bash +./build/ARM64/packer /path/to/input_arm64_elf /tmp/payload.packed +./build/ARM64/stubgen ./build/ARM64/loader /tmp/payload.packed /path/to/output_packed_arm64_elf +``` + +## Validation Evidence + +### Local x86_64 WSL + +Commands run successfully: + +```bash +make clean +make all +make verify-build +make test +``` + +The smoke test packed `build/ARM64/loader` using the x86_64 host tools and +verified the generated output as ARM64 ELF. + +Observed file types: + +```text +build/ARM64/packer: ELF 64-bit LSB pie executable, ARM aarch64 +build/ARM64/loader: ELF 64-bit LSB executable, ARM aarch64, statically linked +build/ARM64/stubgen: ELF 64-bit LSB executable, ARM aarch64, statically linked +build/X86_X64/packer: ELF 64-bit LSB pie executable, x86-64 +build/X86_X64/stubgen: ELF 64-bit LSB executable, x86-64, statically linked +``` + +### ARM64 Linux Server + +Test directory: + +```text +/root/hARMless-codex-test +``` + +Commands run successfully: + +```bash +make clean +make all +make verify-build +make test-runtime +``` + +Runtime test result: + +- `/bin/ls` was packed on ARM64 Linux. +- The packed executable ran successfully. +- Version output matched the original `/bin/ls`. +- Basic `ls /` behavior matched the original command. + +Observed ARM64 file types: + +```text +build/ARM64/packer: ELF 64-bit LSB pie executable, ARM aarch64 +build/ARM64/loader: ELF 64-bit LSB executable, ARM aarch64, statically linked +build/ARM64/stubgen: ELF 64-bit LSB executable, ARM aarch64, statically linked +``` + +## Integration Guidance for Another Project + +Recommended integration mode: + +1. Keep this repository as a tool submodule or external tool directory. +2. Build it once per target environment with `make all`. +3. From the consuming project, call `make pack` or call `packer` and `stubgen` + directly. +4. Treat the packed output as an ARM64 Linux executable artifact. + +Minimal wrapper command for x86_64 WSL: + +```bash +HARMLESS_ROOT=/path/to/hARMless +"$HARMLESS_ROOT/build/X86_X64/packer" "$INPUT_ARM64_ELF" "$OUTPUT_ARM64_ELF.packed" +"$HARMLESS_ROOT/build/X86_X64/stubgen" \ + "$HARMLESS_ROOT/build/ARM64/loader" \ + "$OUTPUT_ARM64_ELF.packed" \ + "$OUTPUT_ARM64_ELF" +rm -f "$OUTPUT_ARM64_ELF.packed" +``` + +Minimal wrapper command for ARM64 Linux: + +```bash +HARMLESS_ROOT=/path/to/hARMless +"$HARMLESS_ROOT/build/ARM64/packer" "$INPUT_ARM64_ELF" "$OUTPUT_ARM64_ELF.packed" +"$HARMLESS_ROOT/build/ARM64/stubgen" \ + "$HARMLESS_ROOT/build/ARM64/loader" \ + "$OUTPUT_ARM64_ELF.packed" \ + "$OUTPUT_ARM64_ELF" +rm -f "$OUTPUT_ARM64_ELF.packed" +``` + +The consuming project should validate input with `file` before calling the +packer: + +```bash +file "$INPUT_ARM64_ELF" | grep -q "ARM aarch64" +``` + +## Runtime and Output Notes + +- The packed output is a standalone ARM64 ELF executable. +- `stubgen` strips section headers from the generated output. +- Each generated output receives randomized polymorphic material, including: + - random magic value + - randomized loader filler + - randomized padding + - syscall-table re-keying + - string-block re-keying + - packed-header blinding +- The packed executable self-deletes when run, so tests regenerate it before + each execution check. + +## Boundaries + +- Current supported payload target: ARM64 ELF. +- Current x86_64 support is for host-side packing tools, not for producing + x86-64 packed payloads in this handoff. +- No credentials are required by the consuming project. +- Temporary files should be kept under the consuming project's own temp + directory or under this repository's `.codex_tmp` if testing inside this repo. + +## Quick Acceptance Checklist + +For a consuming project, acceptance is complete when these pass: + +```bash +make -C /path/to/hARMless all +make -C /path/to/hARMless verify-build +make -C /path/to/hARMless pack \ + INPUT=/path/to/input_arm64_elf \ + OUTPUT=/path/to/output_packed_arm64_elf +file /path/to/output_packed_arm64_elf | grep -q "ARM aarch64" +``` + +On ARM64 Linux, also run: + +```bash +/path/to/output_packed_arm64_elf +``` diff --git a/CHANGES_FROM_UPSTREAM.md b/CHANGES_FROM_UPSTREAM.md new file mode 100644 index 0000000..463b202 --- /dev/null +++ b/CHANGES_FROM_UPSTREAM.md @@ -0,0 +1,117 @@ +# Changes From Upstream + +This document describes the maintained differences between this repository and +the upstream [`litemars/hARMless`](https://github.com/litemars/hARMless) +project. The current work is based on upstream commit `46b3630`. + +## Upgrade Summary + +This fork extends the upstream ARM64-oriented implementation into a repeatable +cross-platform build and packing workflow for both ARM64 and ARM32/EABI5 Linux +ELF executables. + +| Area | Upstream baseline | This fork | +| --- | --- | --- | +| Payload targets | ARM64 and upstream x86 work | ARM64 and ARM32/EABI5 integration targets | +| Build host | Primarily target-native | x86_64 WSL cross-build plus target-native builds | +| Output layout | Shared build outputs | Platform-labelled `build/ARM64`, `build/ARM32_EABI5`, and `build/X86_X64` | +| ARM32 ELF | Not supported by the loader/stub pipeline | ELF32 ARM/EABI5 validation, packing, loader, and symbol handling | +| Encryption length | AES mode could change encrypted length | AES-256-CTR preserves the exact payload length | +| Verification | Basic project tests | Build-layout, runtime-guard, ARM64, and ARM32/EABI5 smoke tests | +| Integration docs | General project README | Architecture-specific handoff and upgrade documents | + +## Main Differences + +### ARM32/EABI5 support + +- Adds ELF32 structures, ARM machine detection, and target validation. +- Adds ARM EABI direct syscall wrappers and the ARM32 syscall table. +- Adds ELF32 symbol lookup and section scrubbing in `stubgen`. +- Builds a statically linked ARM32/EABI5 loader with + `arm-linux-gnueabihf`. +- Adds host-native `build/X86_X64/packer-arm32` for packing ARM32 payloads + from x86_64 Linux or WSL. +- Uses the plain write path for ARM32 instead of the optional `io_uring` path. + +The validated ARM32 ABI is hard-float ARMHF, EABI5. Android APK and shared +library packaging are outside the current scope. + +### Cross-platform build layout + +The Makefile separates executables by runtime platform: + +```text +build/ARM64/packer +build/ARM64/loader +build/ARM64/stubgen +build/ARM32_EABI5/packer +build/ARM32_EABI5/loader +build/ARM32_EABI5/stubgen +build/X86_X64/packer +build/X86_X64/packer-arm32 +build/X86_X64/stubgen +``` + +New high-level targets include: + +```bash +make all +make verify-build +make pack INPUT=/path/to/arm64.elf OUTPUT=/path/to/arm64.packed + +make all32 +make verify-build32 +make pack32 INPUT=/path/to/arm32-eabi5.elf OUTPUT=/path/to/arm32-eabi5.packed +``` + +### Payload encryption correction + +The AES layer now uses AES-256-CTR with a zero IV. CTR mode keeps ciphertext +length equal to plaintext length, which prevents the stored payload length and +CRC range from diverging for payload sizes that require block padding under +AES-ECB. ChaCha20 and RC4 remain as additional layers. + +This changes the packed payload format. A loader and packer from the same +revision should always be used together; previously generated files should be +regenerated when adopting this fork. + +### Polymorphic output handling + +The existing output randomization flow is applied to both ELF32 and ELF64 +loaders. Generated files receive randomized magic and filler data, syscall and +string-table re-keying, packed-header blinding, symbol scrubbing, and section +header removal. + +### Test and integration coverage + +The fork adds reusable checks for: + +- Platform-labelled build artifacts and executable formats. +- Runtime-test architecture guards. +- ARM64 build and packing from x86_64 WSL. +- ARM32/EABI5 build, packing, and ELF format verification. + +Runtime acceptance has also been performed on an ARM64 Linux host: + +- A packed ARM64 `/bin/ls` executed and matched the original behavior. +- An ARM32/EABI5 static payload and its packed form both ran through the + host's 32-bit ARM compatibility layer and produced the expected marker. + +`qemu-arm` is useful for basic unprotected executable checks, but it is not the +runtime acceptance environment for the packed loader because self-image and +anti-analysis behavior can differ under user-mode emulation. + +## Documentation Added + +- `ARM64_PACKER_HANDOFF.md`: ARM64 build, packing, validation, and integration. +- `ARM32_EABI5_HANDOFF.md`: ARM32/EABI5 build, packing, validation, and + integration. +- `CHANGES_FROM_UPSTREAM.md`: English fork upgrade notes. +- `CHANGES_FROM_UPSTREAM.zh-CN.md`: Chinese fork upgrade notes. + +## Upstream Synchronization + +Keep `origin` or another dedicated remote pointed at +`litemars/hARMless`. Merge or rebase upstream changes only after running both +architecture test paths, because changes to packed headers, crypto order, +loader markers, or symbol locations affect the packer-loader contract. diff --git a/CHANGES_FROM_UPSTREAM.zh-CN.md b/CHANGES_FROM_UPSTREAM.zh-CN.md new file mode 100644 index 0000000..4b6f252 --- /dev/null +++ b/CHANGES_FROM_UPSTREAM.zh-CN.md @@ -0,0 +1,109 @@ +# 相对原版的差异与升级点 + +本文档说明本仓库相对上游 +[`litemars/hARMless`](https://github.com/litemars/hARMless) 的维护差异。当前修改基于 +上游提交 `46b3630`。 + +## 升级概览 + +本分支在上游 ARM64 实现基础上,补齐了可重复使用的交叉编译和加壳流程,使其可以 +处理 ARM64 与 ARM32/EABI5 Linux ELF 可执行程序。 + +| 项目 | 上游基线 | 本仓库升级 | +| --- | --- | --- | +| 目标程序 | ARM64 及上游已有的 x86 工作 | 当前集成目标为 ARM64 和 ARM32/EABI5 | +| 编译平台 | 以目标平台原生编译为主 | 支持 x86_64 WSL 交叉编译和目标平台原生编译 | +| 产物目录 | 共用构建产物 | 按平台分为 `build/ARM64`、`build/ARM32_EABI5`、`build/X86_X64` | +| ARM32 ELF | 加载器和 stub 流程不支持 | 支持 ELF32 ARM/EABI5 检查、加壳、加载及符号处理 | +| 加密长度 | AES 模式可能改变密文长度 | AES-256-CTR 保持加密前后长度一致 | +| 验证方式 | 项目基础测试 | 增加目录布局、运行保护、ARM64、ARM32/EABI5 冒烟测试 | +| 集成文档 | 通用 README | 增加分架构交接文档和中英文升级说明 | + +## 主要差异 + +### ARM32/EABI5 支持 + +- 增加 ELF32 数据结构、ARM 机器类型识别和目标格式校验。 +- 增加 ARM EABI 直接系统调用封装及 ARM32 系统调用表。 +- `stubgen` 增加 ELF32 符号定位和节区清理能力。 +- 使用 `arm-linux-gnueabihf` 构建静态链接的 ARM32/EABI5 loader。 +- 增加可在 x86_64 Linux/WSL 运行的 + `build/X86_X64/packer-arm32`,用于处理 ARM32 输入文件。 +- ARM32 loader 使用普通写入路径,不启用可选的 `io_uring` 写入路径。 + +当前验证过的 ARM32 ABI 是 hard-float ARMHF、EABI5。Android APK 和动态库封装不在 +当前范围内。 + +### 分平台构建目录 + +Makefile 按可执行程序实际运行平台拆分产物: + +```text +build/ARM64/packer +build/ARM64/loader +build/ARM64/stubgen +build/ARM32_EABI5/packer +build/ARM32_EABI5/loader +build/ARM32_EABI5/stubgen +build/X86_X64/packer +build/X86_X64/packer-arm32 +build/X86_X64/stubgen +``` + +主要构建和加壳命令: + +```bash +make all +make verify-build +make pack INPUT=/path/to/arm64.elf OUTPUT=/path/to/arm64.packed + +make all32 +make verify-build32 +make pack32 INPUT=/path/to/arm32-eabi5.elf OUTPUT=/path/to/arm32-eabi5.packed +``` + +### 加密长度修正 + +AES 层改为使用零 IV 的 AES-256-CTR。CTR 模式能保证密文长度与原始数据长度一致, +避免 AES-ECB 块填充导致记录的 payload 长度、CRC 校验范围和实际密文长度不一致。 +ChaCha20 和 RC4 仍作为附加加密层保留。 + +这项修改会改变加壳数据格式。必须使用同一版本的 packer 和 loader;采用本分支后, +建议重新生成以前的加壳文件。 + +### 多态输出处理 + +原有输出随机化流程已扩展到 ELF32 和 ELF64 loader。每次生成的文件包含随机 magic、 +填充和 padding,以及系统调用表和字符串表重新加密、头部字段隐藏、符号清理和节区头 +移除。 + +### 测试和集成验证 + +本分支增加以下可重复执行的检查: + +- 分平台构建产物和 ELF 格式检查。 +- 运行测试的目标架构保护。 +- 在 x86_64 WSL 中构建并加壳 ARM64 ELF。 +- 在 x86_64 WSL 中构建、加壳并检查 ARM32/EABI5 ELF。 + +同时已在 ARM64 Linux 主机完成运行验证: + +- 加壳后的 ARM64 `/bin/ls` 可以运行,行为与原程序一致。 +- ARM32/EABI5 静态测试程序及其加壳版本均可通过主机的 32 位 ARM 兼容层运行,并 + 生成预期标记。 + +`qemu-arm` 可用于未加壳程序的基础启动检查,但不作为加壳 loader 的最终验收环境, +因为用户态模拟下的自身镜像读取和反分析行为可能与原生环境不同。 + +## 新增文档 + +- `ARM64_PACKER_HANDOFF.md`:ARM64 构建、加壳、验证和项目集成说明。 +- `ARM32_EABI5_HANDOFF.md`:ARM32/EABI5 构建、加壳、验证和项目集成说明。 +- `CHANGES_FROM_UPSTREAM.md`:英文升级差异说明。 +- `CHANGES_FROM_UPSTREAM.zh-CN.md`:中文升级差异说明。 + +## 后续同步上游 + +建议始终保留一个专门指向 `litemars/hARMless` 的远端。合并或变基上游更新后,需要 +重新执行 ARM64 和 ARM32 两套测试,因为 packed header、加密顺序、loader 标记或符号 +位置的变化都可能影响 packer 与 loader 之间的配套关系。 diff --git a/Makefile b/Makefile index 5e42653..5b14e51 100644 --- a/Makefile +++ b/Makefile @@ -1,129 +1,313 @@ -# Compiler detection UNAME_M := $(shell uname -m) -CC := gcc +TARGET_TRIPLET ?= aarch64-linux-gnu +ARM32_TRIPLET ?= arm-linux-gnueabihf +PKG_CONFIG ?= pkg-config +HOST_CC ?= gcc +TARGET_ARCH_FLAGS := -DTARGET_ARM64 +HOST_TARGET_FLAGS := -DTARGET_ARM64 +ARM32_TARGET_ARCH_FLAGS := -DTARGET_ARM32 -# Target architecture: defaults to host arch, override with ARCH=arm64 or ARCH=x86_64 ifeq ($(UNAME_M), x86_64) - ARCH ?= x86_64 + HOST_BUILD_NAME := X86_X64 + TARGET_CC ?= $(TARGET_TRIPLET)-gcc + TARGET_READELF := $(TARGET_TRIPLET)-readelf + TARGET_PKG_CONFIG_LIBDIR ?= /usr/lib/$(TARGET_TRIPLET)/pkgconfig + ARM32_CC ?= $(ARM32_TRIPLET)-gcc + ARM32_READELF := $(ARM32_TRIPLET)-readelf + ARM32_PKG_CONFIG_LIBDIR ?= /usr/lib/$(ARM32_TRIPLET)/pkgconfig else ifeq ($(UNAME_M), aarch64) - ARCH ?= arm64 + HOST_BUILD_NAME := ARM64 + TARGET_CC ?= $(HOST_CC) + TARGET_READELF := readelf + ARM32_CC ?= $(ARM32_TRIPLET)-gcc + ARM32_READELF := $(ARM32_TRIPLET)-readelf + ARM32_PKG_CONFIG_LIBDIR ?= /usr/lib/$(ARM32_TRIPLET)/pkgconfig else - $(error Cannot auto-detect ARCH from host '$(UNAME_M)'. Set ARCH=arm64 or ARCH=x86_64 explicitly) + $(error Cannot auto-detect host '$(UNAME_M)'. Build on x86_64 WSL or ARM64 Linux) endif -ifeq ($(ARCH), x86_64) - ifeq ($(UNAME_M), x86_64) - TARGET_CC := gcc # native x86-64 build - else - TARGET_CC := x86_64-linux-gnu-gcc # cross-compile from ARM64 - endif - TARGET_ARCH_FLAGS := -DTARGET_X86_64 -else ifeq ($(ARCH), arm64) - ifeq ($(UNAME_M), x86_64) - TARGET_CC := aarch64-linux-gnu-gcc # cross-compile from x86-64 - else - TARGET_CC := gcc # native ARM64 build - endif - TARGET_ARCH_FLAGS := -DTARGET_ARM64 -else - $(error Unknown ARCH '$(ARCH)'. Use ARCH=arm64 or ARCH=x86_64) +TARGET_PKG_CONFIG_ENV := +ifneq ($(TARGET_PKG_CONFIG_LIBDIR),) +TARGET_PKG_CONFIG_ENV := PKG_CONFIG_LIBDIR=$(TARGET_PKG_CONFIG_LIBDIR) +endif +ARM32_PKG_CONFIG_ENV := +ifneq ($(ARM32_PKG_CONFIG_LIBDIR),) +ARM32_PKG_CONFIG_ENV := PKG_CONFIG_LIBDIR=$(ARM32_PKG_CONFIG_LIBDIR) endif -# Compiler flags CFLAGS := -Wall -Wextra -O2 -std=c99 -# Write method: choose one of -DCOPY_WITH_MMAP, -DCOPY_WITH_IO_URING, or neither (plain write syscall) TARGET_CFLAGS := -Wall -Wextra -O2 -std=c99 -static -DCOPY_WITH_IO_URING +ARM32_TARGET_CFLAGS := -Wall -Wextra -O2 -std=c99 -static -marm LDFLAGS := -static -# OpenSSL flags - prefer shared libraries to avoid static linking warnings -OPENSSL_CFLAGS := $(shell pkg-config --cflags openssl 2>/dev/null || echo "") -OPENSSL_LDFLAGS := $(shell pkg-config --libs openssl 2>/dev/null || echo "-lssl -lcrypto") +HOST_OPENSSL_CFLAGS := $(shell $(PKG_CONFIG) --cflags openssl 2>/dev/null || echo "") +HOST_OPENSSL_LDFLAGS := $(shell $(PKG_CONFIG) --libs openssl 2>/dev/null || echo "-lssl -lcrypto") +TARGET_OPENSSL_CFLAGS := $(shell $(TARGET_PKG_CONFIG_ENV) $(PKG_CONFIG) --cflags openssl 2>/dev/null || echo "") +TARGET_OPENSSL_LDFLAGS := $(shell $(TARGET_PKG_CONFIG_ENV) $(PKG_CONFIG) --libs openssl 2>/dev/null || echo "-lssl -lcrypto") +ARM32_OPENSSL_CFLAGS := $(shell $(ARM32_PKG_CONFIG_ENV) $(PKG_CONFIG) --cflags openssl 2>/dev/null || echo "") +ARM32_OPENSSL_LDFLAGS := $(shell $(ARM32_PKG_CONFIG_ENV) $(PKG_CONFIG) --libs openssl 2>/dev/null || echo "-lssl -lcrypto") -# Security flags SECURITY_FLAGS := -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE STEALTH_FLAGS := -fomit-frame-pointer -fno-asynchronous-unwind-tables -fno-stack-protector -# Directories INCLUDE_DIR := include PACKER_DIR := packer LOADER_DIR := loader STUBGEN_DIR := stubgen BUILD_DIR := build +ARM64_BUILD_DIR := $(BUILD_DIR)/ARM64 +ARM32_BUILD_DIR := $(BUILD_DIR)/ARM32_EABI5 +HOST_BUILD_DIR := $(BUILD_DIR)/$(HOST_BUILD_NAME) +BUILD_DIRS := $(ARM64_BUILD_DIR) +ifneq ($(HOST_BUILD_DIR),$(ARM64_BUILD_DIR)) +BUILD_DIRS += $(HOST_BUILD_DIR) +endif + +ARM64_PACKER_BIN := $(ARM64_BUILD_DIR)/packer +ARM64_LOADER_BIN := $(ARM64_BUILD_DIR)/loader +ARM64_STUBGEN_BIN := $(ARM64_BUILD_DIR)/stubgen +ARM32_PACKER_BIN := $(ARM32_BUILD_DIR)/packer +ARM32_LOADER_BIN := $(ARM32_BUILD_DIR)/loader +ARM32_STUBGEN_BIN := $(ARM32_BUILD_DIR)/stubgen + +ifeq ($(HOST_BUILD_NAME), ARM64) +HOST_PACKER_BIN := $(ARM64_PACKER_BIN) +HOST_ARM32_PACKER_BIN := $(ARM64_BUILD_DIR)/packer-arm32 +HOST_STUBGEN_BIN := $(ARM64_STUBGEN_BIN) +HOST_TOOL_BINS := +HOST_ARM32_TOOL_BINS := $(HOST_ARM32_PACKER_BIN) +else +HOST_PACKER_BIN := $(HOST_BUILD_DIR)/packer +HOST_ARM32_PACKER_BIN := $(HOST_BUILD_DIR)/packer-arm32 +HOST_STUBGEN_BIN := $(HOST_BUILD_DIR)/stubgen +HOST_TOOL_BINS := $(HOST_PACKER_BIN) $(HOST_STUBGEN_BIN) +HOST_ARM32_TOOL_BINS := $(HOST_ARM32_PACKER_BIN) $(HOST_STUBGEN_BIN) +endif -# Output binaries -PACKER_BIN := $(BUILD_DIR)/packer -LOADER_BIN := $(BUILD_DIR)/loader -STUBGEN_BIN := $(BUILD_DIR)/stubgen +PACKER_BIN := $(HOST_PACKER_BIN) +LOADER_BIN := $(ARM64_LOADER_BIN) +STUBGEN_BIN := $(HOST_STUBGEN_BIN) +ALL_BINS := $(ARM64_PACKER_BIN) $(ARM64_LOADER_BIN) $(ARM64_STUBGEN_BIN) $(HOST_TOOL_BINS) +ALL32_BINS := $(ARM32_PACKER_BIN) $(ARM32_LOADER_BIN) $(ARM32_STUBGEN_BIN) $(HOST_ARM32_TOOL_BINS) -# Enhanced source files (including obfuscation) PACKER_SOURCES := $(PACKER_DIR)/packer.c $(PACKER_DIR)/crypto.c $(PACKER_DIR)/obfuscation.c LOADER_SOURCES := $(LOADER_DIR)/loader.c $(LOADER_DIR)/memexec.c $(LOADER_DIR)/polymorph.c $(LOADER_DIR)/strings.c $(PACKER_DIR)/elf64.c $(PACKER_DIR)/crypto.c $(PACKER_DIR)/obfuscation.c -# stubgen now parses the loader ELF to locate symbols and shares is_elf64 STUBGEN_SOURCES := $(STUBGEN_DIR)/stubgen.c $(PACKER_DIR)/elf64.c -# Include paths INCLUDES := -I$(INCLUDE_DIR) -# Default target -all: $(BUILD_DIR) $(PACKER_BIN) $(LOADER_BIN) $(STUBGEN_BIN) +.PHONY: all all32 clean test test-smoke test-runtime test32 install-deps install-deps32 verify-build verify-build32 verify-pack-tools verify-pack-tools32 pack pack32 -# Create build directory -$(BUILD_DIR): - mkdir -p $(BUILD_DIR) +all: $(BUILD_DIRS) $(ALL_BINS) + +all32: $(ARM32_BUILD_DIR) $(HOST_BUILD_DIR) $(ALL32_BINS) + +verify-build: all + @echo "Verifying ARM64 build artifacts..." + @for bin in $(ARM64_PACKER_BIN) $(ARM64_LOADER_BIN) $(ARM64_STUBGEN_BIN); do \ + if [ ! -x "$$bin" ]; then \ + echo "ERROR: missing executable artifact: $$bin"; \ + exit 1; \ + fi; \ + if ! file "$$bin" | grep -q "ARM aarch64"; then \ + file "$$bin"; \ + exit 1; \ + fi; \ + if ! $(TARGET_READELF) -h "$$bin" | grep -q "Machine:[[:space:]]*AArch64"; then \ + $(TARGET_READELF) -h "$$bin"; \ + exit 1; \ + fi; \ + done + @if $(TARGET_READELF) -d $(ARM64_LOADER_BIN) 2>/dev/null | grep -q NEEDED; then \ + echo "ERROR: $(ARM64_LOADER_BIN) must be statically linked"; \ + exit 1; \ + fi + @if $(TARGET_READELF) -d $(ARM64_STUBGEN_BIN) 2>/dev/null | grep -q NEEDED; then \ + echo "ERROR: $(ARM64_STUBGEN_BIN) must be statically linked"; \ + exit 1; \ + fi + @if [ "$(HOST_BUILD_NAME)" != "ARM64" ]; then \ + echo "Verifying $(HOST_BUILD_NAME) host tools..."; \ + for bin in $(HOST_PACKER_BIN) $(HOST_STUBGEN_BIN); do \ + if [ ! -x "$$bin" ]; then \ + echo "ERROR: missing executable artifact: $$bin"; \ + exit 1; \ + fi; \ + if ! file "$$bin" | grep -q "x86-64"; then \ + file "$$bin"; \ + exit 1; \ + fi; \ + done; \ + fi + @echo "Build artifacts verified." + +verify-pack-tools: all + @echo "Checking pack tool runtime..." + @$(PACKER_BIN) >/dev/null 2>&1; status=$$?; \ + if [ $$status -eq 126 ] || [ $$status -eq 127 ]; then \ + echo "ERROR: $(PACKER_BIN) cannot run on this host."; \ + exit 1; \ + fi + @$(STUBGEN_BIN) >/dev/null 2>&1; status=$$?; \ + if [ $$status -eq 126 ] || [ $$status -eq 127 ]; then \ + echo "ERROR: $(STUBGEN_BIN) cannot run on this host."; \ + exit 1; \ + fi + @echo "Pack tools are executable on this host." + +verify-build32: all32 + @echo "Verifying ARM32/EABI5 build artifacts..." + @for bin in $(ARM32_PACKER_BIN) $(ARM32_LOADER_BIN) $(ARM32_STUBGEN_BIN); do \ + if [ ! -x "$$bin" ]; then \ + echo "ERROR: missing executable artifact: $$bin"; \ + exit 1; \ + fi; \ + if ! file "$$bin" | grep -q "ARM"; then \ + file "$$bin"; \ + exit 1; \ + fi; \ + if ! $(ARM32_READELF) -h "$$bin" | grep -q "Machine:[[:space:]]*ARM"; then \ + $(ARM32_READELF) -h "$$bin"; \ + exit 1; \ + fi; \ + done + @if $(ARM32_READELF) -d $(ARM32_LOADER_BIN) 2>/dev/null | grep -q NEEDED; then \ + echo "ERROR: $(ARM32_LOADER_BIN) must be statically linked"; \ + exit 1; \ + fi + @if $(ARM32_READELF) -d $(ARM32_STUBGEN_BIN) 2>/dev/null | grep -q NEEDED; then \ + echo "ERROR: $(ARM32_STUBGEN_BIN) must be statically linked"; \ + exit 1; \ + fi + @if [ "$(HOST_BUILD_NAME)" != "ARM32_EABI5" ]; then \ + echo "Verifying $(HOST_BUILD_NAME) ARM32 pack tools..."; \ + for bin in $(HOST_ARM32_PACKER_BIN) $(HOST_STUBGEN_BIN); do \ + if [ ! -x "$$bin" ]; then \ + echo "ERROR: missing executable artifact: $$bin"; \ + exit 1; \ + fi; \ + if ! file "$$bin" | grep -q "x86-64\\|ARM aarch64"; then \ + file "$$bin"; \ + exit 1; \ + fi; \ + done; \ + fi + @echo "ARM32/EABI5 build artifacts verified." -# Build packer -$(PACKER_BIN): $(PACKER_SOURCES) - $(CC) $(CFLAGS) $(SECURITY_FLAGS) $(TARGET_ARCH_FLAGS) $(OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(OPENSSL_LDFLAGS) +verify-pack-tools32: all32 + @echo "Checking ARM32 pack tool runtime..." + @$(HOST_ARM32_PACKER_BIN) >/dev/null 2>&1; status=$$?; \ + if [ $$status -eq 126 ] || [ $$status -eq 127 ]; then \ + echo "ERROR: $(HOST_ARM32_PACKER_BIN) cannot run on this host."; \ + exit 1; \ + fi + @$(HOST_STUBGEN_BIN) >/dev/null 2>&1; status=$$?; \ + if [ $$status -eq 126 ] || [ $$status -eq 127 ]; then \ + echo "ERROR: $(HOST_STUBGEN_BIN) cannot run on this host."; \ + exit 1; \ + fi + @echo "ARM32 pack tools are executable on this host." -# Build loader -$(LOADER_BIN): $(LOADER_SOURCES) - $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(OPENSSL_LDFLAGS) 2>/dev/null || $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(OPENSSL_LDFLAGS) -lzstd -lz +$(BUILD_DIRS) $(ARM32_BUILD_DIR): + mkdir -p $@ -# Build stub generator -$(STUBGEN_BIN): $(STUBGEN_SOURCES) - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) +$(ARM64_PACKER_BIN): $(PACKER_SOURCES) | $(ARM64_BUILD_DIR) + $(TARGET_CC) $(CFLAGS) $(SECURITY_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) -# Advanced packing presets -pack: $(PACKER_BIN) $(LOADER_BIN) $(STUBGEN_BIN) +$(ARM64_LOADER_BIN): $(LOADER_SOURCES) | $(ARM64_BUILD_DIR) + $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) 2>/dev/null || $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) -lzstd -lz + +$(ARM64_STUBGEN_BIN): $(STUBGEN_SOURCES) | $(ARM64_BUILD_DIR) + $(TARGET_CC) $(CFLAGS) $(TARGET_ARCH_FLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) + +$(ARM32_PACKER_BIN): $(PACKER_SOURCES) | $(ARM32_BUILD_DIR) + $(ARM32_CC) $(CFLAGS) $(SECURITY_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) + +$(ARM32_LOADER_BIN): $(LOADER_SOURCES) | $(ARM32_BUILD_DIR) + $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) 2>/dev/null || $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) -lzstd -lz + +$(ARM32_STUBGEN_BIN): $(STUBGEN_SOURCES) | $(ARM32_BUILD_DIR) + $(ARM32_CC) $(CFLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) + +ifneq ($(HOST_TOOL_BINS),) +$(HOST_PACKER_BIN): $(PACKER_SOURCES) | $(HOST_BUILD_DIR) + $(HOST_CC) $(CFLAGS) $(SECURITY_FLAGS) $(HOST_TARGET_FLAGS) $(HOST_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(HOST_OPENSSL_LDFLAGS) + +$(HOST_STUBGEN_BIN): $(STUBGEN_SOURCES) | $(HOST_BUILD_DIR) + $(HOST_CC) $(CFLAGS) $(HOST_TARGET_FLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) +endif + +$(HOST_ARM32_PACKER_BIN): $(PACKER_SOURCES) | $(HOST_BUILD_DIR) + $(HOST_CC) $(CFLAGS) $(SECURITY_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(HOST_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(HOST_OPENSSL_LDFLAGS) + +pack: verify-pack-tools + @if [ -z "$(INPUT)" ] || [ -z "$(OUTPUT)" ]; then \ + echo "Usage: make pack INPUT= OUTPUT="; \ + exit 1; \ + fi + "$(PACKER_BIN)" "$(INPUT)" "$(OUTPUT).packed" + "$(STUBGEN_BIN)" "$(LOADER_BIN)" "$(OUTPUT).packed" "$(OUTPUT)" + @echo "Output packed ARM64 binary created: $(OUTPUT)" + +pack32: verify-pack-tools32 @if [ -z "$(INPUT)" ] || [ -z "$(OUTPUT)" ]; then \ - echo "Usage: make pack INPUT= OUTPUT="; \ + echo "Usage: make pack32 INPUT= OUTPUT="; \ exit 1; \ fi - $(PACKER_BIN) $(INPUT) $(OUTPUT).packed - $(STUBGEN_BIN) $(LOADER_BIN) $(OUTPUT).packed $(OUTPUT) - @echo "Output packed binary created: $(OUTPUT)" + "$(HOST_ARM32_PACKER_BIN)" "$(INPUT)" "$(OUTPUT).packed" + "$(HOST_STUBGEN_BIN)" "$(ARM32_LOADER_BIN)" "$(OUTPUT).packed" "$(OUTPUT)" + @echo "Output packed ARM32/EABI5 binary created: $(OUTPUT)" -# Clean build artifacts clean: rm -rf $(BUILD_DIR) rm -f *.packed rm -f test_* bench_* -test: - @echo "Running tests..." - @if [ -x tests/unit_test.sh ]; then \ - cd tests && ./unit_test.sh; \ - else \ - echo "No tests found: tests/unit_test.sh"; \ - exit 1; \ - fi +test: test-smoke + +test-smoke: + @echo "Running build and pack smoke tests..." + @bash tests/runtime_guard_test.sh + @bash tests/platform_layout_test.sh + +test-runtime: + @echo "Running ARM64 runtime tests..." + @bash tests/unit_test.sh + +test32: + @echo "Running ARM32/EABI5 smoke and runtime tests..." + @bash tests/arm32_eabi5_test.sh -# Install dependencies install-deps: - @echo "Installing cross-compilation and OpenSSL dependencies for ARCH=$(ARCH)..." + @echo "Installing ARM64 packing dependencies for host $(UNAME_M)..." @if command -v apt-get >/dev/null 2>&1; then \ - sudo apt-get update && \ - if [ "$(ARCH)" = "x86_64" ] && [ "$(UNAME_M)" != "x86_64" ]; then \ - sudo apt-get install -y gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu libssl-dev; \ - elif [ "$(ARCH)" = "arm64" ] && [ "$(UNAME_M)" = "x86_64" ]; then \ - sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libssl-dev; \ + if [ "$(UNAME_M)" = "x86_64" ]; then \ + sudo dpkg --add-architecture arm64 && \ + sudo apt-get update && \ + sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libssl-dev:amd64 libssl-dev:arm64 zlib1g-dev:arm64 libzstd-dev:arm64; \ else \ - sudo apt-get install -y gcc libssl-dev; \ + sudo apt-get update && \ + sudo apt-get install -y build-essential libssl-dev zlib1g-dev libzstd-dev; \ fi; \ elif command -v yum >/dev/null 2>&1; then \ sudo yum install -y gcc openssl-devel; \ elif command -v pacman >/dev/null 2>&1; then \ sudo pacman -S gcc openssl; \ else \ - echo "Please install cross-compilation and OpenSSL tools manually"; \ - fi \ No newline at end of file + echo "Please install ARM64 cross-compilation and OpenSSL tools manually"; \ + fi + +install-deps32: + @echo "Installing ARM32/EABI5 packing dependencies for host $(UNAME_M)..." + @if command -v apt-get >/dev/null 2>&1; then \ + if [ "$(UNAME_M)" = "x86_64" ]; then \ + sudo dpkg --add-architecture armhf && \ + sudo apt-get update && \ + sudo apt-get install -y gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf qemu-user libssl-dev:amd64 libssl-dev:armhf zlib1g-dev:armhf libzstd-dev:armhf; \ + else \ + sudo apt-get update && \ + sudo apt-get install -y gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf qemu-user libssl-dev zlib1g-dev libzstd-dev; \ + fi; \ + else \ + echo "Please install ARM32/EABI5 cross-compilation, qemu-user and OpenSSL tools manually"; \ + fi diff --git a/README.md b/README.md index 7d3008b..ee529db 100644 --- a/README.md +++ b/README.md @@ -1,320 +1,175 @@ -# 🛡️ hARMless +# hARMless -![License](https://img.shields.io/badge/license-MIT-blue.svg) -![Platform](https://img.shields.io/badge/platform-ARM64%20%7C%20x86--64%20Linux-green.svg) -![Build](https://img.shields.io/badge/build-passing-brightgreen.svg) -![Stars](https://img.shields.io/github/stars/litemars/hARMless?style=social) +[Changes from upstream](CHANGES_FROM_UPSTREAM.md) | [相对原版的差异与升级点](CHANGES_FROM_UPSTREAM.zh-CN.md) +ELF packer/loader for Linux security research. This checkout supports the +current integration targets: +- ARM64 ELF packing. +- ARM32/EABI5 ELF packing. +- x86_64 WSL host-side packing tools for both targets. -**An ELF Packer/Loader for ARM64 and x86-64 Linux Binaries** +## Build Outputs -A comprehensive security research tool that encrypts ARM64 or x86-64 ELF executables using multi-layer encryption and provides runtime in-memory execution without writing the original binary to disk. +Run from the repository root: ---- +```bash +make all # ARM64 target +make all32 # ARM32/EABI5 target +``` -## Features +Output layout: -- **Multi-Architecture ELF Support**: ARM64 (AArch64) and x86-64 Linux binaries; target selected at build time with `ARCH=arm64` (default) or `ARCH=x86_64` -- **Multi-Layer Encryption**: Triple encryption using AES-256, ChaCha20, and RC4 -- **Memory Execution**: Runtime decryption and execution entirely in memory using `memfd_create` -- **Code Obfuscation**: Advanced obfuscation techniques for anti-analysis -- **CRC32 Verification**: Integrity checking to detect tampering -- **Self-Contained**: Packed binaries are completely standalone -- **Core Dump Prevention**: Prevents memory dumps using `setrlimit` -- **Secure Memory Wiping**: Multi-pass memory erasure for sensitive data -- **Direct Syscalls**: Bypasses userland hooks for enhanced stealth -- **Polymorphic Loader**: Every packed binary is bytewise unique — randomized magic, filler, padding, and symbol table scrubbing at stub-generation time +| Directory | Platform | Purpose | +| --- | --- | --- | +| `build/ARM64` | ARM64 Linux | ARM64 packer, loader, and stubgen | +| `build/ARM32_EABI5` | ARM32 Linux EABI5 | ARM32 packer, loader, and stubgen | +| `build/X86_X64` | x86_64 Linux/WSL | Host-native packers and stubgen | ---- +On x86_64 WSL: -## 🚀 Quick Start +- `build/X86_X64/packer` packs ARM64 ELF payloads. +- `build/X86_X64/packer-arm32` packs ARM32/EABI5 ELF payloads. +- `build/X86_X64/stubgen` combines packed data with either target loader. -```bash -# Clone the repository -git clone https://github.com/litemars/hARMless.git -cd hARMless +## Dependencies -# Build for the host architecture (auto-detected) -make all +### x86_64 Ubuntu/WSL -# Override to cross-compile for a different target -make all ARCH=arm64 -make all ARCH=x86_64 +ARM64 target dependencies: -# Pack a binary (ARCH must match what the loader was built for) -make pack INPUT=/bin/ls OUTPUT=packed_ls - -# Run the packed binary on an x86-64 Linux machine -./packed_ls +```bash +sudo dpkg --add-architecture arm64 +sudo apt-get update +sudo apt-get install -y \ + gcc-aarch64-linux-gnu \ + binutils-aarch64-linux-gnu \ + libssl-dev:amd64 \ + libssl-dev:arm64 \ + zlib1g-dev:arm64 \ + libzstd-dev:arm64 ``` ---- +ARM32/EABI5 target dependencies: -## 📦 Installation +```bash +sudo dpkg --add-architecture armhf +sudo apt-get update +sudo apt-get install -y \ + gcc-arm-linux-gnueabihf \ + binutils-arm-linux-gnueabihf \ + qemu-user \ + libssl-dev:amd64 \ + libssl-dev:armhf \ + zlib1g-dev:armhf \ + libzstd-dev:armhf +``` -### Prerequisites +Project targets are also available: -- **Linux system** (ARM64 or x86-64) or a cross-compilation toolchain -- **GCC** — native or the appropriate cross-compiler (see table below) -- **Make** -- **OpenSSL** (`libssl-dev`) — required for AES-256 and ChaCha20 via the EVP API -- **Standard development tools** (`git`, `build-essential`) +```bash +make install-deps +make install-deps32 +``` -| Host → Target | Compiler needed | -|---------------|----------------| -| x86-64 → x86-64 | `gcc` (native) | -| ARM64 → ARM64 | `gcc` (native) | -| x86-64 → ARM64 | `aarch64-linux-gnu-gcc` | -| ARM64 → x86-64 | `x86_64-linux-gnu-gcc` | +## Build And Verify -### Build Steps +ARM64: ```bash -# 1. Clone the repository -git clone https://github.com/litemars/hARMless.git -cd hARMless - -# 2a. Build for ARM64 (default) +make clean make all - -# 2b. Build for x86-64 -make all ARCH=x86_64 - -# This creates: -# - build/packer : Binary packer (validates ELF machine type for chosen ARCH) -# - build/loader : Stub loader (compiled for the chosen ARCH) -# - build/stubgen : Stub generator (host-native, arch-agnostic) +make verify-build +make test ``` -### Cross-Compilation +ARM32/EABI5: ```bash -# x86-64 host → ARM64 target (existing behaviour, ARCH=arm64 is the default) -sudo apt-get install gcc-aarch64-linux-gnu libssl-dev -make all ARCH=arm64 - -# ARM64 host → x86-64 target -sudo apt-get install gcc-x86-64-linux-gnu libssl-dev -make all ARCH=x86_64 - -# Or let install-deps handle it: -make install-deps ARCH=x86_64 -make all ARCH=x86_64 +make all32 +make verify-build32 +make test32 ``` ---- +`make test32` is a local build/pack/format smoke test. `qemu-user` is not a +reliable runtime validator for the packed loader because `/proc/self/exe` and +anti-analysis checks can differ from native execution. Use native ARM32 Linux or +ARM64 Linux with 32-bit ARM compat enabled for payload runtime validation. -## 📖 Usage +## Pack Commands -### Basic Packing +### Pack ARM64 ELF On x86_64 WSL ```bash -# Pack an ARM64 binary -make pack INPUT=your_arm64_binary OUTPUT=packed_binary - -# Alternative: Use tools directly -./build/packer your_arm64_binary packed_data -./build/stubgen ./build/loader packed_data packed_binary +make pack INPUT=/path/to/input_arm64_elf OUTPUT=/path/to/output_packed_arm64_elf ``` -### Running Packed Binaries +Equivalent direct commands: ```bash -# Simply execute the packed binary -./packed_binary - -# The packed binary will: -# 1. Run anti-debug and anti-sandbox checks -# 2. Read its own embedded encrypted data -# 3. Decrypt the original ELF in memory -# 4. Verify integrity with CRC32 -# 5. Create a masqueraded memfd and write the ELF into it -# 6. Delete itself from disk (unlink) -# 7. Execute directly from memory via /proc/self/fd/ +./build/X86_X64/packer /path/to/input_arm64_elf /tmp/payload.packed +./build/X86_X64/stubgen ./build/ARM64/loader /tmp/payload.packed /path/to/output_packed_arm64_elf ``` -### Test +### Pack ARM32/EABI5 ELF On x86_64 WSL ```bash -# Testing using /bin/ls - -make test -# Output: packed_binary: packed_ls - +make pack32 INPUT=/path/to/input_arm32_eabi5_elf OUTPUT=/path/to/output_packed_arm32_eabi5_elf ``` ---- - -## Technical Details +Equivalent direct commands: -### Encryption Pipeline - -The packer uses a **triple-layer encryption** approach: - -1. **AES-256-ECB**: First encryption pass (OpenSSL EVP) -2. **ChaCha20**: Modern stream cipher for additional security (OpenSSL EVP) -3. **RC4 Stream Cipher**: Final obfuscation layer - -``` -Original Binary → AES-256 → ChaCha20 → RC4 → Packed Data +```bash +./build/X86_X64/packer-arm32 /path/to/input_arm32_eabi5_elf /tmp/payload.packed +./build/X86_X64/stubgen ./build/ARM32_EABI5/loader /tmp/payload.packed /path/to/output_packed_arm32_eabi5_elf ``` -**Key Generation**: Cryptographically secure random keys from `/dev/urandom` (256 bits per layer) - - -### In-Memory Write Paths +### Native Target Hosts -Three selectable methods for writing the decrypted ELF into the memfd (chosen at compile time): +On ARM64 Linux: -| Method | Flag | Kernel Requirement | -|--------|------|--------------------| -| `io_uring` (default) | `-DCOPY_WITH_IO_URING` | ≥ 5.1 | -| `mmap` | `-DCOPY_WITH_MMAP` | Any | -| `write(2)` | (neither flag) | Any | - -### Polymorphic Engine - -Every invocation of `stubgen` produces a bytewise-unique packed binary, even when packing the same input: - -| Mutation | Mechanism | -|----------|-----------| -| Random magic | 32-bit `g_packed_magic` patched to a fresh value from `/dev/urandom`; packed header is synchronized | -| Random filler | 256-byte `g_pack_polymorph` array in `.data` overwritten with random bytes | -| Random padding | 0–4095 bytes of random junk inserted between loader stub and payload | -| SC table re-keying | `hARMless_sc[]` re-encoded with a fresh random `g_sc_xor_key` so syscall numbers differ in every binary | -| String block re-keying | All 241 obfuscated string bytes in `g_obf_str_block` re-encoded with a fresh random `g_str_xor_key` | -| Header OTP blinding | Pack header body (140 bytes) XOR'd with the first 140 bytes of `g_pack_polymorph` as a one-time pad | -| Symbol scrub | `.symtab`/`.strtab` sections overwritten with random data; ELF section header fields zeroed | - -The result: no two packed outputs share the same byte pattern, defeating static hash-based signatures. - -### XOR-Obfuscated Syscall Table - -Syscall numbers are stored in a `volatile` array (`hARMless_sc[]`) XOR-encoded with the key `0xDEADBEEF` at compile time. At stub-generation time, `stubgen` re-encodes the entire table with a fresh random key written into `g_sc_xor_key`, so syscall numbers differ in every packed binary. They are decoded inline at each call site via `hARMless_sc[i] ^ g_sc_xor_key`, preventing static analysis tools from recovering syscall identifiers. - -### XOR-Obfuscated String Block - -All anti-debug and process-masquerade strings (tool names, hypervisor signatures, env var names, process titles) are stored in a single contiguous `g_obf_str_block[]` array, XOR-encoded with `g_str_xor_key`. At stub-generation time, `stubgen` re-encodes the entire block with a fresh random byte key, making every binary's string patterns unique and independent of one another. - -### Memory Safety - -- **Secure Wiping**: 3-pass overwrite (zeros, ones, random) with volatile access to prevent compiler optimization -- **No Disk Writes**: Original binary never touches filesystem -- **Self-Deletion**: Loader calls `unlink()` on itself before executing the payload -- **ASLR Compatible**: Position-independent code; random address slot reserved via `mmap(NULL)` before execution - ---- - -## Security Features - -### Core Dump Prevention - -```c -setrlimit(RLIMIT_CORE, &(struct rlimit){0, 0}); +```bash +make all +make pack INPUT=/path/to/input_arm64_elf OUTPUT=/path/to/output_packed_arm64_elf ``` -Ensures sensitive memory is never written to disk, even during crashes. - -### Integrity Verification - -CRC32 checksums detect any tampering with: -- Encrypted payload -- Decryption keys -- Loader code - -### Anti-Analysis - -- **No debug symbols**: Stripped binaries; section header table zeroed at stub-generation time -- **Obfuscated control flow**: Reduces reverse engineering surface -- **Obfuscated syscall numbers**: Stored XOR'd with `0xDEADBEEF`, decoded at each call site -- **Direct syscalls**: Evades LD_PRELOAD and EDR hooks -- **Noise delays**: CPU-bound xorshift loops seeded from the ASLR stack address; cannot be skipped by sandbox time-acceleration and emit no recognizable timing syscall -- **Process context probes**: `getpid`, `getppid`, and `prctl(PR_GET_NAME)` woven between critical operations; results used in a runtime condition to avoid trivial dead-code elimination by an analyst -- **In-memory execution**: No `/tmp` artifacts - ---- +On ARM32/EABI5 Linux: -## Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Original Binary │ -└────────────────────┬────────────────────────────────────┘ - │ - ▼ - ┌───────────────────────┐ - │ Packer (packer.c) │ - │ - Read ELF │ - │ - Generate keys │ - │ - Triple encrypt │ - │ - Compute CRC32 │ - └───────────┬───────────┘ - │ - ▼ - ┌───────────────────────┐ - │ Packed Data File │ - │ [encrypted payload] │ - └───────────┬───────────┘ - │ - ▼ - ┌───────────────────────┐ - │ Stub Generator │ - │ (stubgen.c) │ - │ - Patch magic/filler │ - │ - Scrub symbols │ - │ - Insert padding │ - │ - Append payload │ - └───────────┬───────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────┐ -│ Packed Binary (Output) │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ Loader Stub (loader.c) │ │ -│ │ - Anti-debug / anti-sandbox checks │ │ -│ │ - Decrypt (RC4 → ChaCha20 → AES-256) │ │ -│ │ - Verify CRC32 │ │ -│ │ - Create masqueraded memfd │ │ -│ │ - Unlink self │ │ -│ │ - Execute via /proc/self/fd/ │ │ -│ └──────────────────────────────────────────────┘ │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ [random padding] Encrypted Payload + Header │ │ -│ └──────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────┘ - │ - ▼ - ┌───────────────────────┐ - │ Runtime Execution │ - │ (in-memory only) │ - └───────────────────────┘ +```bash +make all32 +make pack32 INPUT=/path/to/input_arm32_eabi5_elf OUTPUT=/path/to/output_packed_arm32_eabi5_elf ``` +## Validation Status ---- - -## 🤝 Contributing - -Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - +Validated on x86_64 WSL: ---- +- ARM64 cross-build and pack smoke test. +- ARM32/EABI5 cross-build and pack smoke test. +- Output format checks for `ARM aarch64` and `ARM, EABI5`. +Validated on an ARM64 Linux server: -**⚠️ Legal Notice**: This tool is intended for: -- Authorized penetration testing -- Security research and education -- Red team operations -- Malware analysis +- ARM64 packed `/bin/ls` runs successfully and matches original behavior. +- ARM32/EABI5 static payload runs directly through the server's 32-bit ARM + compatibility layer. +- ARM32/EABI5 packed payload runs and writes the expected runtime marker. -**Unauthorized use is prohibited and may be illegal.** +## Technical Notes ---- +- AES layer uses AES-256-CTR to preserve payload length for arbitrary ELF sizes. +- Additional encryption layers remain ChaCha20 and RC4. +- `stubgen` supports both ELF32 and ELF64 loaders. +- Packed outputs receive randomized magic, filler, padding, syscall-table + re-keying, string-block re-keying, header blinding, and section-header strip. +- Temporary test artifacts are written under `.codex_tmp`. -## 📄 License +## Legal Notice -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This tool is intended for authorized security research, controlled red-team +testing, and education. Unauthorized use against systems or software you do not +own or have permission to test may be illegal. ---- +## License +MIT. See `LICENSE`. diff --git a/include/common.h b/include/common.h index e45b892..c329f2b 100644 --- a/include/common.h +++ b/include/common.h @@ -16,12 +16,20 @@ #include #include -#if defined(__aarch64__) +#if defined(TARGET_ARM32) +#define PACKED_MAGIC 0x41523332 /* "AR32" */ +#elif defined(TARGET_ARM64) +#define PACKED_MAGIC 0x41524D36 /* "ARM6" */ +#elif defined(TARGET_X86_64) +#define PACKED_MAGIC 0x58363446 /* "X64F" */ +#elif defined(__arm__) +#define PACKED_MAGIC 0x41523332 /* "AR32" */ +#elif defined(__aarch64__) #define PACKED_MAGIC 0x41524D36 /* "ARM6" */ #elif defined(__x86_64__) #define PACKED_MAGIC 0x58363446 /* "X64F" */ #else -#error "Unsupported architecture: only aarch64 and x86_64 are supported" +#error "Unsupported architecture: only arm, aarch64 and x86_64 are supported" #endif typedef struct { @@ -186,6 +194,9 @@ struct uring_params { #elif defined(__x86_64__) #define io_read_barrier() __asm__ __volatile__("lfence" ::: "memory") #define io_write_barrier() __asm__ __volatile__("sfence" ::: "memory") +#elif defined(__arm__) +#define io_read_barrier() __asm__ __volatile__("dmb ish" ::: "memory") +#define io_write_barrier() __asm__ __volatile__("dmb ish" ::: "memory") #endif #endif /* COPY_WITH_IO_URING */ @@ -257,6 +268,65 @@ static inline long syscall6(long number, long arg1, long arg2, long arg3, long a return ret; } +#elif defined(__arm__) + +static inline long syscall1(long number, long arg1) { + register long r7 __asm__("r7") = number; + register long r0 __asm__("r0") = arg1; + __asm__ volatile ( + "svc 0\n" + : "+r"(r0) + : "r"(r7) + : "memory" + ); + return r0; +} + +static inline long syscall2(long number, long arg1, long arg2) { + register long r7 __asm__("r7") = number; + register long r0 __asm__("r0") = arg1; + register long r1 __asm__("r1") = arg2; + __asm__ volatile ( + "svc 0\n" + : "+r"(r0) + : "r"(r7), "r"(r1) + : "memory" + ); + return r0; +} + +static inline long syscall3(long number, long arg1, long arg2, long arg3) { + register long r7 __asm__("r7") = number; + register long r0 __asm__("r0") = arg1; + register long r1 __asm__("r1") = arg2; + register long r2 __asm__("r2") = arg3; + __asm__ volatile ( + "svc 0\n" + : "+r"(r0) + : "r"(r7), "r"(r1), "r"(r2) + : "memory" + ); + return r0; +} + +static inline long syscall6(long number, long arg1, long arg2, long arg3, + long arg4, long arg5, long arg6) { + register long r7 __asm__("r7") = number; + register long r0 __asm__("r0") = arg1; + register long r1 __asm__("r1") = arg2; + register long r2 __asm__("r2") = arg3; + register long r3 __asm__("r3") = arg4; + register long r4 __asm__("r4") = arg5; + register long r5 __asm__("r5") = arg6; + __asm__ volatile ( + "svc 0\n" + : "+r"(r0) + : "r"(r7), "r"(r1), "r"(r2), "r"(r3), "r"(r4), "r"(r5) + : "memory" + ); + return r0; +} + #elif defined(__x86_64__) /* diff --git a/include/elf64.h b/include/elf64.h index a793390..901429a 100644 --- a/include/elf64.h +++ b/include/elf64.h @@ -3,7 +3,13 @@ #include -// ELF64 structures for ARM64 +// ELF32/ELF64 structures used by the packer and stub generator. +typedef uint16_t Elf32_Half; +typedef uint32_t Elf32_Word; +typedef uint32_t Elf32_Addr; +typedef uint32_t Elf32_Off; +typedef int32_t Elf32_Sword; + typedef uint16_t Elf64_Half; typedef uint32_t Elf64_Word; typedef uint64_t Elf64_Addr; @@ -30,6 +36,7 @@ typedef int64_t Elf64_Sxword; // ELF Machine types #define EM_NONE 0 +#define EM_ARM 40 // ARM32 EABI #define EM_X86_64 62 // x86-64 #define EM_AARCH64 183 // ARM64 @@ -55,6 +62,23 @@ typedef int64_t Elf64_Sxword; #define PF_R 0x4 +typedef struct { + unsigned char e_ident[EI_NIDENT]; + Elf32_Half e_type; + Elf32_Half e_machine; + Elf32_Word e_version; + Elf32_Addr e_entry; + Elf32_Off e_phoff; + Elf32_Off e_shoff; + Elf32_Word e_flags; + Elf32_Half e_ehsize; + Elf32_Half e_phentsize; + Elf32_Half e_phnum; + Elf32_Half e_shentsize; + Elf32_Half e_shnum; + Elf32_Half e_shstrndx; +} Elf32_Ehdr; + typedef struct { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; @@ -72,6 +96,17 @@ typedef struct { Elf64_Half e_shstrndx; } Elf64_Ehdr; +typedef struct { + Elf32_Word p_type; + Elf32_Off p_offset; + Elf32_Addr p_vaddr; + Elf32_Addr p_paddr; + Elf32_Word p_filesz; + Elf32_Word p_memsz; + Elf32_Word p_flags; + Elf32_Word p_align; +} Elf32_Phdr; + typedef struct { Elf64_Word p_type; @@ -85,6 +120,19 @@ typedef struct { } Elf64_Phdr; +typedef struct { + Elf32_Word sh_name; + Elf32_Word sh_type; + Elf32_Word sh_flags; + Elf32_Addr sh_addr; + Elf32_Off sh_offset; + Elf32_Word sh_size; + Elf32_Word sh_link; + Elf32_Word sh_info; + Elf32_Word sh_addralign; + Elf32_Word sh_entsize; +} Elf32_Shdr; + typedef struct { Elf64_Word sh_name; Elf64_Word sh_type; @@ -106,6 +154,15 @@ typedef struct { #define SHT_NOBITS 8 // ELF64 symbol table entry +typedef struct { + Elf32_Word st_name; + Elf32_Addr st_value; + Elf32_Word st_size; + unsigned char st_info; + unsigned char st_other; + Elf32_Half st_shndx; +} Elf32_Sym; + typedef struct { Elf64_Word st_name; // index into associated string table unsigned char st_info; @@ -115,9 +172,13 @@ typedef struct { Elf64_Xword st_size; } Elf64_Sym; +int is_elf32(const void* data); +int is_elf32_arm(const void* data); int is_elf64(const void* data); int is_elf64_arm64(const void* data); int is_elf64_x86_64(const void* data); +int is_target_elf(const void* data); +const char* target_elf_name(void); void print_elf64_header(const Elf64_Ehdr* ehdr); #endif // ELF64_H diff --git a/loader/loader.c b/loader/loader.c index bb66493..44168a8 100644 --- a/loader/loader.c +++ b/loader/loader.c @@ -270,10 +270,12 @@ int main(int argc, char* argv[], char* envp[]) { header = find_packed_header(self_data, self_size); if (!header) { + DBG("packed header not found\n"); secure_memory_wipe(self_data, self_size); free(self_data); return 1; } + DBG("packed header found\n"); { uint8_t* hb = (uint8_t*)header; @@ -283,6 +285,7 @@ int main(int argc, char* argv[], char* envp[]) { } if (comprehensive_anti_debug_check()) { + DBG("anti-debug pre-decrypt triggered\n"); secure_memory_wipe(self_data, self_size); free(self_data); exit(0); @@ -290,12 +293,14 @@ int main(int argc, char* argv[], char* envp[]) { encrypted_data = (uint8_t*)header + sizeof(pack_header_t); if (encrypted_data + header->packed_size > self_data + self_size) { + DBG("packed payload out of bounds\n"); secure_memory_wipe(self_data, self_size); free(self_data); return 1; } decrypted_data = malloc(header->original_size); if (!decrypted_data) { + DBG("decrypted allocation failed\n"); secure_memory_wipe(self_data, self_size); free(self_data); return 1; @@ -306,13 +311,15 @@ int main(int argc, char* argv[], char* envp[]) { multi_layer_decrypt(decrypted_data, header->original_size, header); calculated_crc = crc32(decrypted_data, header->original_size); if (calculated_crc != header->crc32) { + DBG("crc check failed\n"); secure_memory_wipe(decrypted_data, header->original_size); secure_memory_wipe(self_data, self_size); free(decrypted_data); free(self_data); return 1; } - if (!is_elf64(decrypted_data)) { + if (!is_target_elf(decrypted_data)) { + DBG("target ELF check failed\n"); secure_memory_wipe(decrypted_data, header->original_size); secure_memory_wipe(self_data, self_size); free(decrypted_data); @@ -320,13 +327,16 @@ int main(int argc, char* argv[], char* envp[]) { return 1; } if (comprehensive_anti_debug_check()) { + DBG("anti-debug post-decrypt triggered\n"); secure_memory_wipe(decrypted_data, header->original_size); secure_memory_wipe(self_data, self_size); free(decrypted_data); free(self_data); exit(0); } + DBG("executing payload from memory\n"); if (execute_from_memory(decrypted_data, header->original_size, argv, envp) < 0) { + DBG("execute_from_memory failed\n"); secure_memory_wipe(decrypted_data, header->original_size); secure_memory_wipe(self_data, self_size); free(decrypted_data); @@ -340,4 +350,4 @@ int main(int argc, char* argv[], char* envp[]) { free(self_data); return 0; -} \ No newline at end of file +} diff --git a/packer/crypto.c b/packer/crypto.c index 2d8503f..5470000 100644 --- a/packer/crypto.c +++ b/packer/crypto.c @@ -28,6 +28,28 @@ const volatile uint32_t hARMless_sc[SC_TABLE_LEN] = { [SC_IDX_IO_URING_ENTER] = 426u ^ SC_XOR_KEY, [SC_IDX_IO_URING_REGISTER] = 427u ^ SC_XOR_KEY, }; +#elif defined(__arm__) +const volatile uint32_t hARMless_sc[SC_TABLE_LEN] = { + [SC_IDX_READ] = 3u ^ SC_XOR_KEY, + [SC_IDX_WRITE] = 4u ^ SC_XOR_KEY, + [SC_IDX_OPEN] = 5u ^ SC_XOR_KEY, + [SC_IDX_CLOSE] = 6u ^ SC_XOR_KEY, + [SC_IDX_MMAP] = 192u ^ SC_XOR_KEY, /* mmap2 on ARM EABI */ + [SC_IDX_MUNMAP] = 91u ^ SC_XOR_KEY, + [SC_IDX_EXECVE] = 11u ^ SC_XOR_KEY, + [SC_IDX_MEMFD_CREATE] = 385u ^ SC_XOR_KEY, + [SC_IDX_FTRUNCATE] = 93u ^ SC_XOR_KEY, + [SC_IDX_LSEEK] = 19u ^ SC_XOR_KEY, + [SC_IDX_MPROTECT] = 125u ^ SC_XOR_KEY, + [SC_IDX_PTRACE] = 26u ^ SC_XOR_KEY, + [SC_IDX_GETPID] = 20u ^ SC_XOR_KEY, + [SC_IDX_GETPPID] = 64u ^ SC_XOR_KEY, + [SC_IDX_PRCTL] = 172u ^ SC_XOR_KEY, + [SC_IDX_MSYNC] = 144u ^ SC_XOR_KEY, + [SC_IDX_IO_URING_SETUP] = 425u ^ SC_XOR_KEY, + [SC_IDX_IO_URING_ENTER] = 426u ^ SC_XOR_KEY, + [SC_IDX_IO_URING_REGISTER] = 427u ^ SC_XOR_KEY, +}; #elif defined(__x86_64__) const volatile uint32_t hARMless_sc[SC_TABLE_LEN] = { [SC_IDX_READ] = 0u ^ SC_XOR_KEY, @@ -51,7 +73,7 @@ const volatile uint32_t hARMless_sc[SC_TABLE_LEN] = { [SC_IDX_IO_URING_REGISTER] = 427u ^ SC_XOR_KEY, }; #else -#error "Unsupported architecture: only aarch64 and x86_64 are supported" +#error "Unsupported architecture: only arm, aarch64 and x86_64 are supported" #endif __attribute__((used)) @@ -155,18 +177,20 @@ void rc4_encrypt_decrypt(const uint8_t* key, size_t key_len, const uint8_t* inpu rc4_crypt(&ctx, input, output, len); } -// OpenSSL-based AES-256 +// OpenSSL-based AES-256 in CTR mode. CTR preserves the input length for +// arbitrary ELF sizes; EVP's padded ECB mode would require storing padding. void aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key) { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); if (!ctx) return; - if (EVP_EncryptInit_ex(ctx, EVP_aes_256_ecb(), NULL, key, NULL) != 1) { + uint8_t iv[16] = {0}; + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, key, iv) != 1) { EVP_CIPHER_CTX_free(ctx); return; } int out_len; - uint8_t* output = malloc(len + 16); // Padding space + uint8_t* output = malloc(len); if (!output) { EVP_CIPHER_CTX_free(ctx); return; @@ -181,39 +205,13 @@ void aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key) { int final_len; EVP_EncryptFinal_ex(ctx, output + out_len, &final_len); - memcpy(data, output, len); // Copy back (ECB mode preserves length) + memcpy(data, output, len); free(output); EVP_CIPHER_CTX_free(ctx); } void aes256_decrypt(uint8_t* data, size_t len, const uint8_t* key) { - EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); - if (!ctx) return; - - if (EVP_DecryptInit_ex(ctx, EVP_aes_256_ecb(), NULL, key, NULL) != 1) { - EVP_CIPHER_CTX_free(ctx); - return; - } - - int out_len; - uint8_t* output = malloc(len + 16); - if (!output) { - EVP_CIPHER_CTX_free(ctx); - return; - } - - if (EVP_DecryptUpdate(ctx, output, &out_len, data, len) != 1) { - free(output); - EVP_CIPHER_CTX_free(ctx); - return; - } - - int final_len; - EVP_DecryptFinal_ex(ctx, output + out_len, &final_len); - - memcpy(data, output, len); - free(output); - EVP_CIPHER_CTX_free(ctx); + aes256_encrypt(data, len, key); } // OpenSSL-based ChaCha20 diff --git a/packer/elf64.c b/packer/elf64.c index 6361c02..aaa6f1d 100644 --- a/packer/elf64.c +++ b/packer/elf64.c @@ -11,6 +11,20 @@ int is_elf64(const void* data) { ident[4] == ELFCLASS64; } +int is_elf32(const void* data) { + const unsigned char* ident = (const unsigned char*)data; + return ident[0] == 0x7f && + ident[1] == 'E' && + ident[2] == 'L' && + ident[3] == 'F' && + ident[4] == ELFCLASS32; +} + +int is_elf32_arm(const void* data) { + const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)data; + return is_elf32(data) && ehdr->e_machine == EM_ARM; +} + int is_elf64_arm64(const void* data) { const Elf64_Ehdr* ehdr = (const Elf64_Ehdr*)data; return is_elf64(data) && ehdr->e_machine == EM_AARCH64; @@ -21,6 +35,26 @@ int is_elf64_x86_64(const void* data) { return is_elf64(data) && ehdr->e_machine == EM_X86_64; } +int is_target_elf(const void* data) { +#if defined(TARGET_ARM32) + return is_elf32_arm(data); +#elif defined(TARGET_X86_64) + return is_elf64_x86_64(data); +#else + return is_elf64_arm64(data); +#endif +} + +const char* target_elf_name(void) { +#if defined(TARGET_ARM32) + return "ARM32/EABI5"; +#elif defined(TARGET_X86_64) + return "x86-64"; +#else + return "ARM64"; +#endif +} + void print_elf64_header(const Elf64_Ehdr* ehdr) { printf("ELF Header:\n"); printf(" Entry point: 0x%lx\n", (unsigned long)ehdr->e_entry); @@ -30,4 +64,4 @@ void print_elf64_header(const Elf64_Ehdr* ehdr) { printf(" Type: %u\n", ehdr->e_type); printf(" Number of program headers: %u\n", ehdr->e_phnum); printf(" Number of section headers: %u\n", ehdr->e_shnum); -} \ No newline at end of file +} diff --git a/packer/obfuscation.c b/packer/obfuscation.c index 467f19e..93de47e 100644 --- a/packer/obfuscation.c +++ b/packer/obfuscation.c @@ -3,7 +3,24 @@ #include void strip_elf_metadata(uint8_t* data, size_t len) { - if (!data || len < sizeof(Elf64_Ehdr)) return; + if (!data || len < EI_NIDENT) return; + + if (is_elf32(data)) { + if (len < sizeof(Elf32_Ehdr)) return; + + Elf32_Ehdr* ehdr = (Elf32_Ehdr*)data; + ehdr->e_shoff = 0; + ehdr->e_shnum = 0; + ehdr->e_shentsize = 0; + ehdr->e_shstrndx = 0; + + for (int i = 7; i < EI_NIDENT; i++) { + ehdr->e_ident[i] = 0; + } + return; + } + + if (len < sizeof(Elf64_Ehdr)) return; if (!is_elf64(data)) return; Elf64_Ehdr* ehdr = (Elf64_Ehdr*)data; @@ -54,6 +71,7 @@ void noise_delay(unsigned max_ms) { * sequence does not reduce to a pair of identical info-query calls. */ void check_exec_context(void) { +#if defined(__aarch64__) volatile long pid = syscall1(__NR_getpid, 0); volatile long ppid = syscall1(__NR_getppid, 0); char pname[16]; @@ -62,6 +80,9 @@ void check_exec_context(void) { * hide_process_title was never called or prctl failed. */ if (pid <= 0 || ppid <= 0 || pname[0] == '\0') noise_delay(15); +#else + noise_delay(15); +#endif } void secure_memory_wipe(void* ptr, size_t size) { diff --git a/packer/packer.c b/packer/packer.c index 71d5c37..ae25538 100644 --- a/packer/packer.c +++ b/packer/packer.c @@ -55,6 +55,20 @@ int is_elf64(const void* data) { ehdr->e_ident[4] == ELFCLASS64); } +int is_elf32(const void* data) { + const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)data; + return (ehdr->e_ident[0] == ELFMAG0 && + ehdr->e_ident[1] == ELFMAG1 && + ehdr->e_ident[2] == ELFMAG2 && + ehdr->e_ident[3] == ELFMAG3 && + ehdr->e_ident[4] == ELFCLASS32); +} + +int is_elf32_arm(const void* data) { + const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)data; + return is_elf32(data) && ehdr->e_machine == EM_ARM; +} + int is_elf64_arm64(const void* data) { const Elf64_Ehdr* ehdr = (const Elf64_Ehdr*)data; return is_elf64(data) && ehdr->e_machine == EM_AARCH64; @@ -66,7 +80,9 @@ int is_elf64_x86_64(const void* data) { } static int is_elf64_target(const void* data) { -#if defined(TARGET_X86_64) +#if defined(TARGET_ARM32) + return is_elf32_arm(data); +#elif defined(TARGET_X86_64) return is_elf64_x86_64(data); #else return is_elf64_arm64(data); @@ -74,7 +90,9 @@ static int is_elf64_target(const void* data) { } static const char* target_arch_name(void) { -#if defined(TARGET_X86_64) +#if defined(TARGET_ARM32) + return "ARM32/EABI5"; +#elif defined(TARGET_X86_64) return "x86-64"; #else return "ARM64"; @@ -211,4 +229,4 @@ int main(int argc, char* argv[]) { free(encrypted_data); return 0; -} \ No newline at end of file +} diff --git a/stubgen/stubgen.c b/stubgen/stubgen.c index 786b934..061320a 100644 --- a/stubgen/stubgen.c +++ b/stubgen/stubgen.c @@ -42,7 +42,7 @@ static int slurp_file(const char* path, uint8_t** out_buf, size_t* out_size) { size_t n = fread(buf, 1, (size_t)sz, fp); fclose(fp); if (n != (size_t)sz) { - fprintf(stderr, "Error: short read on '%s' (%zu of %ld)\n", path, n, sz); + fprintf(stderr, "Error: short read on '%s' (%zu of %ld bytes)\n", path, n, sz); free(buf); return -1; } @@ -51,11 +51,6 @@ static int slurp_file(const char* path, uint8_t** out_buf, size_t* out_size) { return 0; } -/* - * get_random_bytes: fill buf with `len` cryptographically random bytes - * from /dev/urandom. Falls back to time/pid-seeded rand() only if the - * device cannot be opened (very unusual on Linux). - */ static int get_random_bytes(uint8_t* buf, size_t len) { FILE* urandom = fopen("/dev/urandom", "rb"); if (urandom) { @@ -63,9 +58,11 @@ static int get_random_bytes(uint8_t* buf, size_t len) { fclose(urandom); if (n == len) return 0; } - /* Fallback: explicitly inferior, but better than failing the build. */ static int seeded = 0; - if (!seeded) { srand((unsigned)(time(NULL) ^ getpid())); seeded = 1; } + if (!seeded) { + srand((unsigned)(time(NULL) ^ getpid())); + seeded = 1; + } for (size_t i = 0; i < len; i++) buf[i] = (uint8_t)(rand() & 0xFF); return 0; } @@ -78,34 +75,150 @@ static int strtab_streq(const char* strtab, size_t strtab_size, char c = strtab[offset + i]; char n = needle[i]; if (c != n) return 0; - if (c == '\0') return 1; /* both NUL means exact match */ + if (c == '\0') return 1; i++; } return 0; } -static int find_loader_symbol(const uint8_t* loader, size_t loader_size, - const char* sym_name, - size_t* out_offset, size_t* out_size) { +static int find_loader_symbol32(const uint8_t* loader, size_t loader_size, + const char* sym_name, + size_t* out_offset, size_t* out_size) { + if (loader_size < sizeof(Elf32_Ehdr)) { + fprintf(stderr, "Error: loader smaller than ELF32 header\n"); + return -1; + } + if (!is_elf32(loader)) { + fprintf(stderr, "Error: loader is not ELF32\n"); + return -1; + } + + const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)loader; + if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0) { + fprintf(stderr, "Error: loader has no section header table - " + "the target loader must be unstripped for stubgen\n"); + return -1; + } + if (ehdr->e_shentsize != sizeof(Elf32_Shdr)) { + fprintf(stderr, "Error: unexpected ELF32 section header size %u\n", + ehdr->e_shentsize); + return -1; + } + + size_t sht_size = (size_t)ehdr->e_shnum * ehdr->e_shentsize; + if (ehdr->e_shoff > loader_size || sht_size > loader_size - ehdr->e_shoff) { + fprintf(stderr, "Error: section header table out of file bounds\n"); + return -1; + } + const Elf32_Shdr* sections = (const Elf32_Shdr*)(loader + ehdr->e_shoff); + + const Elf32_Shdr* symtab = NULL; + for (size_t i = 0; i < ehdr->e_shnum; i++) { + if (sections[i].sh_type == SHT_SYMTAB) { + symtab = §ions[i]; + break; + } + } + if (!symtab) { + fprintf(stderr, "Error: loader has no .symtab - " + "the target loader must be unstripped for stubgen\n"); + return -1; + } + if (symtab->sh_entsize != sizeof(Elf32_Sym)) { + fprintf(stderr, "Error: unexpected ELF32 symbol entry size %u\n", + symtab->sh_entsize); + return -1; + } + if (symtab->sh_offset > loader_size || + symtab->sh_size > loader_size - symtab->sh_offset) { + fprintf(stderr, "Error: .symtab out of file bounds\n"); + return -1; + } + if (symtab->sh_link == 0 || symtab->sh_link >= ehdr->e_shnum) { + fprintf(stderr, "Error: .symtab.sh_link invalid\n"); + return -1; + } + + const Elf32_Shdr* strtab = §ions[symtab->sh_link]; + if (strtab->sh_type != SHT_STRTAB) { + fprintf(stderr, "Error: .symtab.sh_link does not point to a strtab\n"); + return -1; + } + if (strtab->sh_offset > loader_size || + strtab->sh_size > loader_size - strtab->sh_offset) { + fprintf(stderr, "Error: .strtab out of file bounds\n"); + return -1; + } + + const char* names = (const char*)(loader + strtab->sh_offset); + const Elf32_Sym* syms = (const Elf32_Sym*)(loader + symtab->sh_offset); + size_t nsyms = (size_t)(symtab->sh_size / sizeof(Elf32_Sym)); + + for (size_t i = 0; i < nsyms; i++) { + const Elf32_Sym* s = &syms[i]; + if (s->st_name == 0) continue; + if (s->st_shndx == 0 || s->st_shndx >= ehdr->e_shnum) continue; + if (!strtab_streq(names, (size_t)strtab->sh_size, + (size_t)s->st_name, sym_name)) continue; + + const Elf32_Shdr* sec = §ions[s->st_shndx]; + if (sec->sh_type == SHT_NOBITS) { + fprintf(stderr, "Error: symbol '%s' is in .bss (no file image) - " + "ensure it is initialized to a non-zero value\n", + sym_name); + return -1; + } + if (s->st_value < sec->sh_addr) { + fprintf(stderr, "Error: symbol '%s' value below section base\n", + sym_name); + return -1; + } + + size_t in_section = (size_t)(s->st_value - sec->sh_addr); + if (in_section >= sec->sh_size) { + fprintf(stderr, "Error: symbol '%s' beyond section end\n", sym_name); + return -1; + } + size_t file_off = (size_t)sec->sh_offset + in_section; + if (file_off > loader_size || s->st_size > loader_size - file_off) { + fprintf(stderr, "Error: symbol '%s' file range out of bounds\n", + sym_name); + return -1; + } + + *out_offset = file_off; + *out_size = (size_t)s->st_size; + return 0; + } + + fprintf(stderr, "Error: symbol '%s' not found in loader .symtab\n", sym_name); + return -1; +} + +static int find_loader_symbol64(const uint8_t* loader, size_t loader_size, + const char* sym_name, + size_t* out_offset, size_t* out_size) { if (loader_size < sizeof(Elf64_Ehdr)) { - fprintf(stderr, "Error: loader smaller than ELF header\n"); + fprintf(stderr, "Error: loader smaller than ELF64 header\n"); return -1; } if (!is_elf64(loader)) { fprintf(stderr, "Error: loader is not ELF64\n"); return -1; } + const Elf64_Ehdr* ehdr = (const Elf64_Ehdr*)loader; if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0) { - fprintf(stderr, "Error: loader has no section header table — " - "build/loader must be unstripped for stubgen\n"); + fprintf(stderr, "Error: loader has no section header table - " + "the target loader must be unstripped for stubgen\n"); return -1; } if (ehdr->e_shentsize != sizeof(Elf64_Shdr)) { - fprintf(stderr, "Error: unexpected section header size %u\n", + fprintf(stderr, "Error: unexpected ELF64 section header size %u\n", ehdr->e_shentsize); return -1; } + size_t sht_size = (size_t)ehdr->e_shnum * ehdr->e_shentsize; if (ehdr->e_shoff > loader_size || sht_size > loader_size - ehdr->e_shoff) { fprintf(stderr, "Error: section header table out of file bounds\n"); @@ -113,7 +226,6 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, } const Elf64_Shdr* sections = (const Elf64_Shdr*)(loader + ehdr->e_shoff); - /* Find .symtab by section type — more robust than name lookup. */ const Elf64_Shdr* symtab = NULL; for (size_t i = 0; i < ehdr->e_shnum; i++) { if (sections[i].sh_type == SHT_SYMTAB) { @@ -122,12 +234,12 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, } } if (!symtab) { - fprintf(stderr, "Error: loader has no .symtab — build/loader must " - "be unstripped for stubgen\n"); + fprintf(stderr, "Error: loader has no .symtab - " + "the target loader must be unstripped for stubgen\n"); return -1; } if (symtab->sh_entsize != sizeof(Elf64_Sym)) { - fprintf(stderr, "Error: unexpected symbol entry size %llu\n", + fprintf(stderr, "Error: unexpected ELF64 symbol entry size %llu\n", (unsigned long long)symtab->sh_entsize); return -1; } @@ -140,6 +252,7 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, fprintf(stderr, "Error: .symtab.sh_link invalid\n"); return -1; } + const Elf64_Shdr* strtab = §ions[symtab->sh_link]; if (strtab->sh_type != SHT_STRTAB) { fprintf(stderr, "Error: .symtab.sh_link does not point to a strtab\n"); @@ -159,15 +272,12 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, const Elf64_Sym* s = &syms[i]; if (s->st_name == 0) continue; if (s->st_shndx == 0 || s->st_shndx >= ehdr->e_shnum) continue; - if (!strtab_streq(names, (size_t)strtab->sh_size, (size_t)s->st_name, sym_name)) continue; - /* Match — translate virtual address to file offset via the - * containing section's sh_addr / sh_offset pair. */ const Elf64_Shdr* sec = §ions[s->st_shndx]; if (sec->sh_type == SHT_NOBITS) { - fprintf(stderr, "Error: symbol '%s' is in .bss (no file image) — " + fprintf(stderr, "Error: symbol '%s' is in .bss (no file image) - " "ensure it is initialized to a non-zero value\n", sym_name); return -1; @@ -177,6 +287,7 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, sym_name); return -1; } + size_t in_section = (size_t)(s->st_value - sec->sh_addr); if (in_section >= sec->sh_size) { fprintf(stderr, "Error: symbol '%s' beyond section end\n", sym_name); @@ -188,6 +299,7 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, sym_name); return -1; } + *out_offset = file_off; *out_size = (size_t)s->st_size; return 0; @@ -197,6 +309,86 @@ static int find_loader_symbol(const uint8_t* loader, size_t loader_size, return -1; } +static int find_loader_symbol(const uint8_t* loader, size_t loader_size, + const char* sym_name, + size_t* out_offset, size_t* out_size) { + if (loader_size < EI_NIDENT) { + fprintf(stderr, "Error: loader smaller than ELF ident\n"); + return -1; + } + if (is_elf32(loader)) { + return find_loader_symbol32(loader, loader_size, sym_name, + out_offset, out_size); + } + if (is_elf64(loader)) { + return find_loader_symbol64(loader, loader_size, sym_name, + out_offset, out_size); + } + fprintf(stderr, "Error: loader is not ELF32 or ELF64\n"); + return -1; +} + +static int scrub_symbols_and_strip_sections(uint8_t* loader_data, + size_t loader_size) { + if (loader_size < EI_NIDENT) return -1; + + if (is_elf32(loader_data)) { + Elf32_Ehdr* ehdr = (Elf32_Ehdr*)loader_data; + if (ehdr->e_shoff != 0 && ehdr->e_shnum != 0) { + if (ehdr->e_shentsize != sizeof(Elf32_Shdr)) return -1; + size_t sht_size = (size_t)ehdr->e_shnum * ehdr->e_shentsize; + if (ehdr->e_shoff > loader_size || + sht_size > loader_size - ehdr->e_shoff) return -1; + + Elf32_Shdr* sections = (Elf32_Shdr*)(loader_data + ehdr->e_shoff); + for (size_t i = 0; i < ehdr->e_shnum; i++) { + if (sections[i].sh_type != SHT_SYMTAB && + sections[i].sh_type != SHT_STRTAB) continue; + if (sections[i].sh_offset > loader_size) continue; + if (sections[i].sh_size > loader_size - sections[i].sh_offset) continue; + if (sections[i].sh_size == 0) continue; + (void)get_random_bytes(loader_data + sections[i].sh_offset, + (size_t)sections[i].sh_size); + } + } + + ehdr->e_shoff = 0; + ehdr->e_shnum = 0; + ehdr->e_shentsize = 0; + ehdr->e_shstrndx = 0; + return 0; + } + + if (is_elf64(loader_data)) { + Elf64_Ehdr* ehdr = (Elf64_Ehdr*)loader_data; + if (ehdr->e_shoff != 0 && ehdr->e_shnum != 0) { + if (ehdr->e_shentsize != sizeof(Elf64_Shdr)) return -1; + size_t sht_size = (size_t)ehdr->e_shnum * ehdr->e_shentsize; + if (ehdr->e_shoff > loader_size || + sht_size > loader_size - ehdr->e_shoff) return -1; + + Elf64_Shdr* sections = (Elf64_Shdr*)(loader_data + ehdr->e_shoff); + for (size_t i = 0; i < ehdr->e_shnum; i++) { + if (sections[i].sh_type != SHT_SYMTAB && + sections[i].sh_type != SHT_STRTAB) continue; + if (sections[i].sh_offset > loader_size) continue; + if (sections[i].sh_size > loader_size - sections[i].sh_offset) continue; + if (sections[i].sh_size == 0) continue; + (void)get_random_bytes(loader_data + sections[i].sh_offset, + (size_t)sections[i].sh_size); + } + } + + ehdr->e_shoff = 0; + ehdr->e_shnum = 0; + ehdr->e_shentsize = 0; + ehdr->e_shstrndx = 0; + return 0; + } + + return -1; +} + void print_usage(const char* program_name) { printf("Usage: %s \n", program_name); printf("\nCombines an unstripped loader with packed data and applies\n"); @@ -231,7 +423,6 @@ int main(int argc, char* argv[]) { goto cleanup; } - /* Locate required patch points in the loader. */ size_t magic_off = 0, magic_sz = 0; size_t poly_off = 0, poly_sz = 0; if (find_loader_symbol(loader_data, loader_size, "g_packed_magic", @@ -249,10 +440,9 @@ int main(int argc, char* argv[]) { goto cleanup; } - /* Locate optional syscall-table re-keying symbols. */ size_t sc_key_off = 0, sc_key_sz = 0; size_t sc_tab_off = 0, sc_tab_sz = 0; - int has_sc_rekey = 0; + int has_sc_rekey = 0; if (find_loader_symbol(loader_data, loader_size, "g_sc_xor_key", &sc_key_off, &sc_key_sz) == 0 && find_loader_symbol(loader_data, loader_size, "hARMless_sc", @@ -262,10 +452,9 @@ int main(int argc, char* argv[]) { has_sc_rekey = 1; } - /* Locate optional string-block re-keying symbols. */ size_t str_key_off = 0, str_key_sz = 0; size_t str_blk_off = 0, str_blk_sz = 0; - int has_str_rekey = 0; + int has_str_rekey = 0; if (find_loader_symbol(loader_data, loader_size, "g_str_xor_key", &str_key_off, &str_key_sz) == 0 && find_loader_symbol(loader_data, loader_size, "g_obf_str_block", @@ -275,31 +464,22 @@ int main(int argc, char* argv[]) { has_str_rekey = 1; } - /* Generate per-pack random material. */ - uint32_t new_magic = 0; + uint32_t new_magic = 0; uint32_t new_xor_key = 0; - uint8_t new_filler[POLYMORPH_FILLER_LEN]; - uint16_t pad_short = 0; - uint8_t padding[POLYMORPH_PADDING_MAX]; + uint8_t new_filler[POLYMORPH_FILLER_LEN]; + uint16_t pad_short = 0; + uint8_t padding[POLYMORPH_PADDING_MAX]; if (get_random_bytes((uint8_t*)&new_magic, sizeof(new_magic)) != 0) goto cleanup; if (get_random_bytes(new_filler, sizeof(new_filler)) != 0) goto cleanup; if (get_random_bytes((uint8_t*)&pad_short, sizeof(pad_short)) != 0) goto cleanup; if (get_random_bytes(padding, sizeof(padding)) != 0) goto cleanup; - size_t pad_len = (size_t)(pad_short & (POLYMORPH_PADDING_MAX - 1)); /* 0..4095 */ + size_t pad_len = (size_t)(pad_short & (POLYMORPH_PADDING_MAX - 1)); - /* Patch loader bytes in our local buffer (we own it; safe to mutate). */ memcpy(loader_data + magic_off, &new_magic, sizeof(new_magic)); memcpy(loader_data + poly_off, new_filler, sizeof(new_filler)); - - /* Patch the packed header's magic field. pack_header_t.magic is the - * very first field (offset 0), 4 bytes. */ memcpy(packed_data + 0, &new_magic, sizeof(new_magic)); - /* Blind the header body (bytes 4..sizeof-1) using the first - * (sizeof(pack_header_t)-4) bytes of new_filler as a per-pack OTP. - * new_filler has already been patched into g_pack_polymorph in the - * loader, so the loader can un-blind using g_pack_polymorph directly. */ { size_t hdr_body = sizeof(pack_header_t) - sizeof(uint32_t); for (size_t i = 0; i < hdr_body; i++) @@ -311,7 +491,7 @@ int main(int argc, char* argv[]) { memcpy(&old_xor_key, loader_data + sc_key_off, sizeof(old_xor_key)); if (get_random_bytes((uint8_t*)&new_xor_key, sizeof(new_xor_key)) != 0) goto cleanup; - size_t n_sc = sc_tab_sz / sizeof(uint32_t); + size_t n_sc = sc_tab_sz / sizeof(uint32_t); uint32_t* sc_tab = (uint32_t*)(loader_data + sc_tab_off); for (size_t i = 0; i < n_sc; i++) sc_tab[i] = (sc_tab[i] ^ old_xor_key) ^ new_xor_key; @@ -330,27 +510,11 @@ int main(int argc, char* argv[]) { loader_data[str_key_off] = new_str_key; } - - Elf64_Ehdr* ehdr = (Elf64_Ehdr*)loader_data; - { - Elf64_Shdr* sections = (Elf64_Shdr*)(loader_data + ehdr->e_shoff); - for (size_t i = 0; i < ehdr->e_shnum; i++) { - if (sections[i].sh_type != SHT_SYMTAB && - sections[i].sh_type != SHT_STRTAB) continue; - if (sections[i].sh_offset > loader_size) continue; - if (sections[i].sh_size > loader_size - sections[i].sh_offset) continue; - if (sections[i].sh_size == 0) continue; - (void)get_random_bytes(loader_data + sections[i].sh_offset, - (size_t)sections[i].sh_size); - } + if (scrub_symbols_and_strip_sections(loader_data, loader_size) != 0) { + fprintf(stderr, "Error: failed to scrub loader symbols\n"); + goto cleanup; } - ehdr->e_shoff = 0; - ehdr->e_shnum = 0; - ehdr->e_shentsize = 0; - ehdr->e_shstrndx = 0; - - /* Write output. */ FILE* out = fopen(output_file, "wb"); if (!out) { fprintf(stderr, "Error: cannot create '%s': %s\n", output_file, strerror(errno)); diff --git a/tests/arm32_eabi5_test.sh b/tests/arm32_eabi5_test.sh new file mode 100644 index 0000000..2f1e709 --- /dev/null +++ b/tests/arm32_eabi5_test.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_DIR="$ROOT_DIR/.codex_tmp/arm32" +SRC="$TMP_DIR/hello_arm32.c" +INPUT="$TMP_DIR/hello_arm32" +OUTPUT="$TMP_DIR/hello_arm32_packed" + +cleanup() { + rm -f "$SRC" "$INPUT" "$OUTPUT" "$OUTPUT.packed" +} + +trap cleanup EXIT +mkdir -p "$TMP_DIR" +cd "$ROOT_DIR" + +if ! command -v arm-linux-gnueabihf-gcc >/dev/null 2>&1; then + echo "SKIP: arm-linux-gnueabihf-gcc is not installed" + exit 0 +fi + +if ! command -v qemu-arm >/dev/null 2>&1; then + echo "SKIP: qemu-arm is not installed" + exit 0 +fi + +cat > "$SRC" <<'C_EOF' +#include + +int main(void) { + puts("hARMless-arm32-eabi5-ok"); + return 0; +} +C_EOF + +make all32 +make verify-build32 + +arm-linux-gnueabihf-gcc -static -O2 -o "$INPUT" "$SRC" + +if ! file "$INPUT" | grep -q "ARM"; then + file "$INPUT" + exit 1 +fi + +if ! readelf -h "$INPUT" | grep -q "Version5 EABI"; then + readelf -h "$INPUT" + exit 1 +fi + +make pack32 INPUT="$INPUT" OUTPUT="$OUTPUT" + +if ! file "$OUTPUT" | grep -q "ARM"; then + file "$OUTPUT" + exit 1 +fi + +if ! readelf -h "$OUTPUT" | grep -q "Version5 EABI"; then + readelf -h "$OUTPUT" + exit 1 +fi + +ORIGINAL_RESULT="$(qemu-arm "$INPUT")" +if [[ "$ORIGINAL_RESULT" != "hARMless-arm32-eabi5-ok" ]]; then + echo "ERROR: unexpected ARM32 original output: $ORIGINAL_RESULT" + exit 1 +fi + +if PACKED_RESULT="$(timeout 10s qemu-arm "$OUTPUT" 2>/dev/null)"; then + PACKED_STATUS=0 +else + PACKED_STATUS=$? +fi + +if [[ $PACKED_STATUS -eq 0 && "$PACKED_RESULT" == "hARMless-arm32-eabi5-ok" ]]; then + echo "[x] ARM32/EABI5 packed executable runs under qemu-arm" +else + echo "[x] ARM32/EABI5 packed executable was created" + echo "NOTE: qemu-user is not a reliable runtime validator for this loader; use native ARM32 or ARM64 32-bit compat for payload execution." +fi diff --git a/tests/platform_layout_test.sh b/tests/platform_layout_test.sh new file mode 100644 index 0000000..b14f05f --- /dev/null +++ b/tests/platform_layout_test.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +TMP_DIR="$ROOT_DIR/.codex_tmp" +OUTPUT="$TMP_DIR/platform_layout_packed_loader" +mkdir -p "$TMP_DIR" +trap 'rm -f "$OUTPUT" "$OUTPUT.packed"' EXIT + +make clean +make all + +for bin in build/ARM64/packer build/ARM64/loader build/ARM64/stubgen; do + if ! file "$bin" | grep -q "ARM aarch64"; then + file "$bin" 2>/dev/null || true + exit 1 + fi +done + +if [[ "$(uname -m)" == "x86_64" ]]; then + for bin in build/X86_X64/packer build/X86_X64/stubgen; do + if ! file "$bin" | grep -q "x86-64"; then + file "$bin" 2>/dev/null || true + exit 1 + fi + done +fi + +make pack INPUT=build/ARM64/loader OUTPUT="$OUTPUT" + +if ! file "$OUTPUT" | grep -q "ARM aarch64"; then + file "$OUTPUT" + exit 1 +fi diff --git a/tests/runtime_guard_test.sh b/tests/runtime_guard_test.sh new file mode 100644 index 0000000..c0a4c3d --- /dev/null +++ b/tests/runtime_guard_test.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +TMP_DIR="$ROOT_DIR/.codex_tmp" +LOG="$TMP_DIR/runtime-guard.log" +mkdir -p "$TMP_DIR" +trap 'rm -f "$LOG"' EXIT + +if ! grep -q '\.codex_tmp' tests/unit_test.sh; then + echo "ERROR: tests/unit_test.sh must place temporary runtime artifacts under .codex_tmp" + exit 1 +fi + +if [[ "$(uname -m)" != "aarch64" ]]; then + make test-runtime > "$LOG" 2>&1 + if ! grep -q "SKIP: ARM64 runtime tests require aarch64 Linux" "$LOG"; then + cat "$LOG" + exit 1 + fi +fi diff --git a/tests/unit_test.sh b/tests/unit_test.sh index cb1dd0c..5636e3f 100755 --- a/tests/unit_test.sh +++ b/tests/unit_test.sh @@ -1,91 +1,79 @@ #!/bin/bash -# Simple unit test for hARMless -# Tests packing and execution of /bin/ls +set -euo pipefail -create_packed_binary(){ - # The binary self delete every run, thus it needs to recreated for every test +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_DIR="$ROOT_DIR/.codex_tmp/runtime" +ARM64_DIR="$ROOT_DIR/build/ARM64" +PACKED_DATA="$TMP_DIR/test_ls.packed" +PACKED_BIN="$TMP_DIR/test_ls_packed" + +cleanup() { + rm -f "$PACKED_DATA" "$PACKED_BIN" +} + +create_packed_binary() { echo "Generating self-contained executable..." echo - ../build/stubgen ../build/loader test_ls.packed test_ls_packed - echo - if [[ ! -f test_ls_packed ]]; then + "$ARM64_DIR/stubgen" "$ARM64_DIR/loader" "$PACKED_DATA" "$PACKED_BIN" + echo + if [[ ! -f "$PACKED_BIN" ]]; then echo "ERROR: Packed executable not created" exit 1 fi } -set -e +trap cleanup EXIT +mkdir -p "$TMP_DIR" -echo "=== hARMless Test ===" +echo "=== hARMless ARM64 Runtime Test ===" echo "Testing with /bin/ls" echo -# Detect architecture and set ARCH accordingly -UNAME_M=$(uname -m) -case "$UNAME_M" in - aarch64) - ARCH=arm64 - ELF_PATTERN="ARM aarch64" - ;; - x86_64) - ARCH=x86_64 - ELF_PATTERN="x86-64" - ;; - *) - echo "ERROR: Unsupported architecture: $UNAME_M" - exit 1 - ;; -esac -echo "[x] Detected architecture: $ARCH" -echo +if [[ "$(uname -m)" != "aarch64" ]]; then + echo "SKIP: ARM64 runtime tests require aarch64 Linux (found $(uname -m))" + exit 0 +fi -# Check if /bin/ls exists and matches the current architecture if [[ ! -f /bin/ls ]]; then echo "ERROR: /bin/ls not found" exit 1 fi -if ! file /bin/ls | grep -q "$ELF_PATTERN"; then - echo "ERROR: /bin/ls is not a $ARCH binary (file says: $(file /bin/ls))" +if ! file /bin/ls | grep -q "ARM aarch64"; then + echo "ERROR: /bin/ls is not an ARM64 binary (file says: $(file /bin/ls))" exit 1 fi -echo "[x] Found $ARCH /bin/ls" +echo "[x] Found ARM64 /bin/ls" echo -# Build the tools echo "Building tools..." -cd .. -make clean && make all ARCH=$ARCH -cd tests +make -C "$ROOT_DIR" clean +make -C "$ROOT_DIR" all -# Test packing echo "Packing /bin/ls..." -../build/packer /bin/ls test_ls.packed +"$ARM64_DIR/packer" /bin/ls "$PACKED_DATA" -if [[ ! -f test_ls.packed ]]; then +if [[ ! -f "$PACKED_DATA" ]]; then echo "ERROR: Packed file not created" exit 1 fi echo -echo "[x] Created packed file: $(ls -lh test_ls.packed)" +echo "[x] Created packed file: $(ls -lh "$PACKED_DATA")" echo -# Test stub generation create_packed_binary echo -echo "[x] Created packed executable: $(ls -lh test_ls_packed)" +echo "[x] Created packed executable: $(ls -lh "$PACKED_BIN")" echo -chmod +x test_ls_packed +chmod +x "$PACKED_BIN" -# Test execution echo "Testing execution..." -timeout 10s ./test_ls_packed --version > /dev/null 2>&1 -if [[ $? -eq 0 ]]; then +if timeout 10s "$PACKED_BIN" --version > /dev/null 2>&1; then echo echo "[x] Packed executable runs successfully" echo @@ -96,10 +84,9 @@ fi create_packed_binary -# Test that output is similar to original echo "Comparing output with original..." ORIGINAL_OUTPUT=$(timeout 5s /bin/ls --version 2>/dev/null | head -1 || echo "ls version output") -PACKED_OUTPUT=$(timeout 5s ./test_ls_packed --version 2>/dev/null | head -1 || echo "packed ls version output") +PACKED_OUTPUT=$(timeout 5s "$PACKED_BIN" --version 2>/dev/null | head -1 || echo "packed ls version output") if [[ "$ORIGINAL_OUTPUT" == "$PACKED_OUTPUT" ]]; then echo @@ -113,10 +100,9 @@ fi create_packed_binary -# Test basic functionality echo "Testing basic ls functionality..." ORIGINAL_LS=$(timeout 5s /bin/ls / | wc -l) -PACKED_LS=$(timeout 5s ./test_ls_packed / | wc -l) +PACKED_LS=$(timeout 5s "$PACKED_BIN" / | wc -l) if [[ $ORIGINAL_LS -eq $PACKED_LS ]]; then echo @@ -126,12 +112,5 @@ else echo "WARNING: Different output count ($ORIGINAL_LS vs $PACKED_LS)" fi -# Uncomment the below to keep the packed binary -# create_packed_binary - -# Cleanup -echo "Cleaning up..." -rm -f test_ls.packed - echo echo "[x] Test completed successfully." From ec23285b7ce58c6d1dd9ea287394296af5b293bd Mon Sep 17 00:00:00 2001 From: Smile Date: Wed, 12 Aug 2026 13:38:46 +0800 Subject: [PATCH 2/2] Audit loader bounds and crypto failure handling --- ARM32_EABI5_HANDOFF.md | 16 ++++++++++ ARM64_PACKER_HANDOFF.md | 6 ++-- CHANGES_FROM_UPSTREAM.md | 17 ++++++++++ CHANGES_FROM_UPSTREAM.zh-CN.md | 13 ++++++++ Makefile | 11 +++++-- README.md | 13 ++++++++ include/common.h | 4 +-- include/crypto.h | 8 ++--- loader/loader.c | 58 +++++++++++++++++++++++++++------- loader/strings.c | 6 ++++ packer/crypto.c | 51 +++++++++++++++++++++--------- packer/packer.c | 49 ++++++++++++++++++++-------- tests/arm32_eabi5_test.sh | 18 +++++++++++ tests/runtime_guard_test.sh | 41 ++++++++++++++++++++++++ tests/unit_test.sh | 14 +++++--- 15 files changed, 270 insertions(+), 55 deletions(-) diff --git a/ARM32_EABI5_HANDOFF.md b/ARM32_EABI5_HANDOFF.md index bf20cd4..0a3ddea 100644 --- a/ARM32_EABI5_HANDOFF.md +++ b/ARM32_EABI5_HANDOFF.md @@ -142,6 +142,15 @@ Observed packed output: ELF 32-bit LSB executable, ARM, EABI5 version 1, statically linked, no section header ``` +Antminer L9 ARMHF cgminer format check from a read-only source input: + +```text +Input : C:\Users\Administrator\Documents\Antminer\artifacts\cgminer\l9-aml-vnish-armhf\cgminer +Output: .codex_tmp/antminer/l9-aml-cgminer-packed +Result: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, no section header +Flags : Version5 EABI, hard-float ABI +``` + ## qemu-user Note `qemu-arm` is useful for checking that an unprotected ARM32/EABI5 binary starts, @@ -155,6 +164,13 @@ ARM64 Linux with 32-bit ARM compatibility for runtime acceptance. - ARM32/EABI5 support uses ELF32 symbol/section parsing in `stubgen`. - ARM32 loader uses ARM EABI direct syscalls. - ARM32 target disables the io_uring write path and uses the plain write path. +- Packed executables are persistent by default. Running the packed `cgminer` or + another service binary does not delete the packed file from disk. +- The loader preserves the original `argv[0]` by default. This is required for + service-managed or path-sensitive ARM32/EABI5 binaries. +- Legacy self-delete is available only when the loader is explicitly rebuilt + with `SELF_DELETE=1`. `argv[0]` mutation is available only with + `MASQUERADE_ARGV0=1`; do not enable either for system service integration. - AES was changed to AES-256-CTR so encrypted payload size always matches the original ELF size across ARM32 and ARM64. diff --git a/ARM64_PACKER_HANDOFF.md b/ARM64_PACKER_HANDOFF.md index 5fda51e..4023f50 100644 --- a/ARM64_PACKER_HANDOFF.md +++ b/ARM64_PACKER_HANDOFF.md @@ -268,8 +268,10 @@ file "$INPUT_ARM64_ELF" | grep -q "ARM aarch64" - syscall-table re-keying - string-block re-keying - packed-header blinding -- The packed executable self-deletes when run, so tests regenerate it before - each execution check. +- The packed executable is persistent by default. Running it does not delete the + file from disk, and the loader preserves the original `argv[0]`. +- Legacy one-shot self-delete is opt-in at build time with `SELF_DELETE=1`. + `argv[0]` mutation is opt-in with `MASQUERADE_ARGV0=1`. ## Boundaries diff --git a/CHANGES_FROM_UPSTREAM.md b/CHANGES_FROM_UPSTREAM.md index 463b202..8aed949 100644 --- a/CHANGES_FROM_UPSTREAM.md +++ b/CHANGES_FROM_UPSTREAM.md @@ -17,6 +17,9 @@ ELF executables. | Output layout | Shared build outputs | Platform-labelled `build/ARM64`, `build/ARM32_EABI5`, and `build/X86_X64` | | ARM32 ELF | Not supported by the loader/stub pipeline | ELF32 ARM/EABI5 validation, packing, loader, and symbol handling | | Encryption length | AES mode could change encrypted length | AES-256-CTR preserves the exact payload length | +| Runtime persistence | Loader self-deletes during startup | Packed executables persist by default; self-delete is opt-in | +| Payload argv | Loader can rewrite `argv[0]` during process masquerading | Original `argv[0]` is preserved by default for service compatibility | +| Runtime validation | Payload sizes and crypto/I/O failures were weakly checked | Bounded size validation, 2 GiB input limit, and fail-closed transform handling | | Verification | Basic project tests | Build-layout, runtime-guard, ARM64, and ARM32/EABI5 smoke tests | | Integration docs | General project README | Architecture-specific handoff and upgrade documents | @@ -82,6 +85,20 @@ loaders. Generated files receive randomized magic and filler data, syscall and string-table re-keying, packed-header blinding, symbol scrubbing, and section header removal. +### Audit boundary + +The loader now rejects `original_size > packed_size`, checks the packed range +with bounded subtraction before copying, rejects inputs above the 32-bit header +limit, and propagates OpenSSL transform failures instead of continuing with a +possibly unencrypted or partially decrypted buffer. + +The format is still an obfuscation layer rather than a strong confidentiality +boundary. Keys are stored in each packed header and the loader necessarily holds +the plaintext ELF in memory before `execve`; the CRC32 is an integrity check for +accidental corruption, not an authenticated-encryption tag. A future protected +asset format should use an external device-bound key and AEAD while preserving a +separate compatibility path for existing packed files. + ### Test and integration coverage The fork adds reusable checks for: diff --git a/CHANGES_FROM_UPSTREAM.zh-CN.md b/CHANGES_FROM_UPSTREAM.zh-CN.md index 4b6f252..7c96c5a 100644 --- a/CHANGES_FROM_UPSTREAM.zh-CN.md +++ b/CHANGES_FROM_UPSTREAM.zh-CN.md @@ -16,6 +16,9 @@ | 产物目录 | 共用构建产物 | 按平台分为 `build/ARM64`、`build/ARM32_EABI5`、`build/X86_X64` | | ARM32 ELF | 加载器和 stub 流程不支持 | 支持 ELF32 ARM/EABI5 检查、加壳、加载及符号处理 | | 加密长度 | AES 模式可能改变密文长度 | AES-256-CTR 保持加密前后长度一致 | +| 运行持久性 | loader 启动时自删 | 加壳程序默认保留在磁盘上,自删改为显式可选 | +| payload 参数 | 进程伪装可能改写 `argv[0]` | 默认保留原始 `argv[0]`,兼容系统服务和依赖路径的程序 | +| 运行时校验 | payload 长度及加解密/文件错误处理不足 | 增加有界长度校验、2 GiB 输入限制和失败即停止处理 | | 验证方式 | 项目基础测试 | 增加目录布局、运行保护、ARM64、ARM32/EABI5 冒烟测试 | | 集成文档 | 通用 README | 增加分架构交接文档和中英文升级说明 | @@ -71,6 +74,16 @@ ChaCha20 和 RC4 仍作为附加加密层保留。 这项修改会改变加壳数据格式。必须使用同一版本的 packer 和 loader;采用本分支后, 建议重新生成以前的加壳文件。 +### 审计边界 + +loader 现在会拒绝 `original_size > packed_size`,在复制前用有界减法验证 +payload 范围,拒绝超过包头 32 位长度字段的输入,并在 OpenSSL 加解密或文件 +读取失败时停止处理,不再继续使用可能未加密或只完成部分解密的缓冲区。 + +当前格式仍属于混淆层,不是强资产保密边界:每个包的密钥保存在包头中,loader +执行前必然会在内存中持有明文 ELF,CRC32 只能检查偶发损坏,不能替代认证加密。 +后续高价值资产保护应使用设备绑定的外部密钥和 AEAD,同时为现有包保留独立兼容路径。 + ### 多态输出处理 原有输出随机化流程已扩展到 ELF32 和 ELF64 loader。每次生成的文件包含随机 magic、 diff --git a/Makefile b/Makefile index 5b14e51..cffc2bf 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,13 @@ CFLAGS := -Wall -Wextra -O2 -std=c99 TARGET_CFLAGS := -Wall -Wextra -O2 -std=c99 -static -DCOPY_WITH_IO_URING ARM32_TARGET_CFLAGS := -Wall -Wextra -O2 -std=c99 -static -marm LDFLAGS := -static +LOADER_FEATURE_FLAGS := +ifeq ($(SELF_DELETE),1) +LOADER_FEATURE_FLAGS += -DHARMLESS_SELF_DELETE +endif +ifeq ($(MASQUERADE_ARGV0),1) +LOADER_FEATURE_FLAGS += -DHARMLESS_MASQUERADE_ARGV0 +endif HOST_OPENSSL_CFLAGS := $(shell $(PKG_CONFIG) --cflags openssl 2>/dev/null || echo "") HOST_OPENSSL_LDFLAGS := $(shell $(PKG_CONFIG) --libs openssl 2>/dev/null || echo "-lssl -lcrypto") @@ -215,7 +222,7 @@ $(ARM64_PACKER_BIN): $(PACKER_SOURCES) | $(ARM64_BUILD_DIR) $(TARGET_CC) $(CFLAGS) $(SECURITY_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) $(ARM64_LOADER_BIN): $(LOADER_SOURCES) | $(ARM64_BUILD_DIR) - $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) 2>/dev/null || $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) -lzstd -lz + $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(LOADER_FEATURE_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) 2>/dev/null || $(TARGET_CC) $(TARGET_CFLAGS) $(STEALTH_FLAGS) $(LOADER_FEATURE_FLAGS) $(TARGET_ARCH_FLAGS) $(TARGET_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(TARGET_OPENSSL_LDFLAGS) -lzstd -lz $(ARM64_STUBGEN_BIN): $(STUBGEN_SOURCES) | $(ARM64_BUILD_DIR) $(TARGET_CC) $(CFLAGS) $(TARGET_ARCH_FLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) @@ -224,7 +231,7 @@ $(ARM32_PACKER_BIN): $(PACKER_SOURCES) | $(ARM32_BUILD_DIR) $(ARM32_CC) $(CFLAGS) $(SECURITY_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) $(ARM32_LOADER_BIN): $(LOADER_SOURCES) | $(ARM32_BUILD_DIR) - $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) 2>/dev/null || $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) -lzstd -lz + $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(LOADER_FEATURE_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) 2>/dev/null || $(ARM32_CC) $(ARM32_TARGET_CFLAGS) $(STEALTH_FLAGS) $(LOADER_FEATURE_FLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(ARM32_OPENSSL_CFLAGS) $(INCLUDES) -o $@ $^ $(ARM32_OPENSSL_LDFLAGS) -lzstd -lz $(ARM32_STUBGEN_BIN): $(STUBGEN_SOURCES) | $(ARM32_BUILD_DIR) $(ARM32_CC) $(CFLAGS) $(ARM32_TARGET_ARCH_FLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) diff --git a/README.md b/README.md index ee529db..8b52c0f 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,21 @@ Validated on an ARM64 Linux server: - AES layer uses AES-256-CTR to preserve payload length for arbitrary ELF sizes. - Additional encryption layers remain ChaCha20 and RC4. - `stubgen` supports both ELF32 and ELF64 loaders. +- Packed outputs are persistent by default: running a packed executable does not + remove the file from disk. +- The loader preserves the original `argv[0]` by default so service scripts, + multicall binaries, and path-sensitive programs can restart normally. +- Legacy one-shot behavior is opt-in at build time with `SELF_DELETE=1`. + `argv[0]` mutation is opt-in with `MASQUERADE_ARGV0=1`. - Packed outputs receive randomized magic, filler, padding, syscall-table re-keying, string-block re-keying, header blinding, and section-header strip. +- The loader rejects inconsistent payload lengths and fails closed when file I/O + or OpenSSL transforms fail. The current crypto API uses signed `int` lengths, + so inputs larger than 2 GiB are rejected before packing. +- This is obfuscation, not a privileged asset-confidentiality boundary: the + per-pack keys and decryption logic are shipped with the executable, and the + payload exists in memory before execution. High-value assets require an + external device-bound key and authenticated encryption in a future format. - Temporary test artifacts are written under `.codex_tmp`. ## Legal Notice diff --git a/include/common.h b/include/common.h index c329f2b..13cd76f 100644 --- a/include/common.h +++ b/include/common.h @@ -403,8 +403,8 @@ static inline void debug_print(const char *msg) { uint32_t crc32(const uint8_t* data, size_t len); void generate_random_key(uint8_t* key, size_t key_size); int comprehensive_anti_debug_check(); -void multi_layer_encrypt(uint8_t* data, size_t len, const pack_header_t* header); -void multi_layer_decrypt(uint8_t* data, size_t len, const pack_header_t* header); +int multi_layer_encrypt(uint8_t* data, size_t len, const pack_header_t* header); +int multi_layer_decrypt(uint8_t* data, size_t len, const pack_header_t* header); int execute_from_memory(const uint8_t* elf_data, size_t elf_size, char* const argv[], char* const envp[]); pack_header_t* find_packed_header(const uint8_t* data, size_t data_size); diff --git a/include/crypto.h b/include/crypto.h index 846cf1b..dcd3fcb 100644 --- a/include/crypto.h +++ b/include/crypto.h @@ -18,9 +18,9 @@ void rc4_crypt(rc4_context_t* ctx, const uint8_t* input, uint8_t* output, size_t void rc4_encrypt_decrypt(const uint8_t* key, size_t key_len, const uint8_t* input, uint8_t* output, size_t len); // Advanced cryptographic functions -void aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key); -void aes256_decrypt(uint8_t* data, size_t len, const uint8_t* key); -void chacha20_encrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce); -void chacha20_decrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce); +int aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key); +int aes256_decrypt(uint8_t* data, size_t len, const uint8_t* key); +int chacha20_encrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce); +int chacha20_decrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce); #endif diff --git a/loader/loader.c b/loader/loader.c index 44168a8..8ac90fa 100644 --- a/loader/loader.c +++ b/loader/loader.c @@ -199,14 +199,14 @@ int comprehensive_anti_debug_check() { } -void multi_layer_decrypt(uint8_t* data, size_t len, const pack_header_t* header) { - +int multi_layer_decrypt(uint8_t* data, size_t len, const pack_header_t* header) { rc4_encrypt_decrypt(header->tertiary_key, 32, data, data, len); - - chacha20_decrypt(data, len, header->secondary_key, header->nonce); + if (chacha20_decrypt(data, len, header->secondary_key, header->nonce) != 0) + return -1; - aes256_decrypt(data, len, header->primary_key); - + if (aes256_decrypt(data, len, header->primary_key) != 0) + return -1; + return 0; } pack_header_t* find_packed_header(const uint8_t* data, size_t data_size) { @@ -224,6 +224,18 @@ pack_header_t* find_packed_header(const uint8_t* data, size_t data_size) { return NULL; } +static void maybe_self_delete(const char* self_path) { +#ifdef HARMLESS_SELF_DELETE + if (self_path && self_path[0] != '\0') { + if (unlink(self_path) < 0) { + DBG("self-delete failed\n"); + } + } +#else + (void)self_path; +#endif +} + int main(int argc, char* argv[], char* envp[]) { FILE* self_fp; uint8_t* self_data; @@ -233,7 +245,6 @@ int main(int argc, char* argv[], char* envp[]) { uint8_t* decrypted_data; uint32_t calculated_crc; - unlink(argv[0]); prevent_core_dumps(); hide_process_title(argc, argv); @@ -244,9 +255,20 @@ int main(int argc, char* argv[], char* envp[]) { return 1; } - fseek(self_fp, 0, SEEK_END); - self_size = ftell(self_fp); - fseek(self_fp, 0, SEEK_SET); + if (fseek(self_fp, 0, SEEK_END) != 0) { + fclose(self_fp); + return 1; + } + long self_size_long = ftell(self_fp); + if (self_size_long <= 0) { + fclose(self_fp); + return 1; + } + self_size = (size_t)self_size_long; + if (fseek(self_fp, 0, SEEK_SET) != 0) { + fclose(self_fp); + return 1; + } if (self_size == 0 || self_size > SIZE_MAX / 2) { fclose(self_fp); @@ -268,6 +290,8 @@ int main(int argc, char* argv[], char* envp[]) { } fclose(self_fp); + maybe_self_delete(argc > 0 ? argv[0] : NULL); + header = find_packed_header(self_data, self_size); if (!header) { DBG("packed header not found\n"); @@ -292,7 +316,10 @@ int main(int argc, char* argv[], char* envp[]) { } encrypted_data = (uint8_t*)header + sizeof(pack_header_t); - if (encrypted_data + header->packed_size > self_data + self_size) { + size_t payload_offset = (size_t)(encrypted_data - self_data); + if (payload_offset > self_size || + header->original_size > header->packed_size || + (size_t)header->packed_size > self_size - payload_offset) { DBG("packed payload out of bounds\n"); secure_memory_wipe(self_data, self_size); free(self_data); @@ -308,7 +335,14 @@ int main(int argc, char* argv[], char* envp[]) { memcpy(decrypted_data, encrypted_data, header->original_size); - multi_layer_decrypt(decrypted_data, header->original_size, header); + if (multi_layer_decrypt(decrypted_data, header->original_size, header) != 0) { + DBG("decryption failed\n"); + secure_memory_wipe(decrypted_data, header->original_size); + secure_memory_wipe(self_data, self_size); + free(decrypted_data); + free(self_data); + return 1; + } calculated_crc = crc32(decrypted_data, header->original_size); if (calculated_crc != header->crc32) { DBG("crc check failed\n"); diff --git a/loader/strings.c b/loader/strings.c index dfdfbff..6edce6b 100644 --- a/loader/strings.c +++ b/loader/strings.c @@ -78,6 +78,7 @@ int create_masqueraded_memfd(void) { } void hide_process_title(int argc, char* argv[]) { +#ifdef HARMLESS_MASQUERADE_ARGV0 if (argc > 0 && argv && argv[0]) { size_t orig_len = strlen(argv[0]); memset(argv[0], 0, orig_len); @@ -89,4 +90,9 @@ void hide_process_title(int argc, char* argv[]) { argv[0][copy_len] = '\0'; syscall3(__NR_prctl, PR_SET_NAME, (long)name, 0); } +#else + (void)argc; + (void)argv; + syscall3(__NR_prctl, PR_SET_NAME, (long)get_random_innocent_name(), 0); +#endif } diff --git a/packer/crypto.c b/packer/crypto.c index 5470000..026d95b 100644 --- a/packer/crypto.c +++ b/packer/crypto.c @@ -179,45 +179,57 @@ void rc4_encrypt_decrypt(const uint8_t* key, size_t key_len, const uint8_t* inpu // OpenSSL-based AES-256 in CTR mode. CTR preserves the input length for // arbitrary ELF sizes; EVP's padded ECB mode would require storing padding. -void aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key) { +int aes256_encrypt(uint8_t* data, size_t len, const uint8_t* key) { + if ((!data && len != 0) || !key) return -1; + if (len == 0) return 0; + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); - if (!ctx) return; + if (!ctx) return -1; uint8_t iv[16] = {0}; if (EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, key, iv) != 1) { EVP_CIPHER_CTX_free(ctx); - return; + return -1; } int out_len; uint8_t* output = malloc(len); if (!output) { EVP_CIPHER_CTX_free(ctx); - return; + return -1; } if (EVP_EncryptUpdate(ctx, output, &out_len, data, len) != 1) { free(output); EVP_CIPHER_CTX_free(ctx); - return; + return -1; } int final_len; - EVP_EncryptFinal_ex(ctx, output + out_len, &final_len); + if (EVP_EncryptFinal_ex(ctx, output + out_len, &final_len) != 1 || + (size_t)(out_len + final_len) != len) { + free(output); + EVP_CIPHER_CTX_free(ctx); + return -1; + } memcpy(data, output, len); free(output); EVP_CIPHER_CTX_free(ctx); + return 0; } -void aes256_decrypt(uint8_t* data, size_t len, const uint8_t* key) { - aes256_encrypt(data, len, key); +int aes256_decrypt(uint8_t* data, size_t len, const uint8_t* key) { + return aes256_encrypt(data, len, key); } // OpenSSL-based ChaCha20 -void chacha20_encrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce) { +int chacha20_encrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce) { + if ((!data && len != 0) || !key || !nonce) return -1; + if (len == 0) return 0; + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); - if (!ctx) return; + if (!ctx) return -1; // ChaCha20 uses 32-byte key, 16-byte IV (nonce + counter) uint8_t iv[16] = {0}; @@ -225,28 +237,37 @@ void chacha20_encrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8 if (EVP_EncryptInit_ex(ctx, EVP_chacha20(), NULL, key, iv) != 1) { EVP_CIPHER_CTX_free(ctx); - return; + return -1; } int out_len; uint8_t* output = malloc(len); if (!output) { EVP_CIPHER_CTX_free(ctx); - return; + return -1; } if (EVP_EncryptUpdate(ctx, output, &out_len, data, len) != 1) { free(output); EVP_CIPHER_CTX_free(ctx); - return; + return -1; + } + + int final_len; + if (EVP_EncryptFinal_ex(ctx, output + out_len, &final_len) != 1 || + (size_t)(out_len + final_len) != len) { + free(output); + EVP_CIPHER_CTX_free(ctx); + return -1; } memcpy(data, output, len); free(output); EVP_CIPHER_CTX_free(ctx); + return 0; } -void chacha20_decrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce) { +int chacha20_decrypt(uint8_t* data, size_t len, const uint8_t* key, const uint8_t* nonce) { // ChaCha20 is symmetric - chacha20_encrypt(data, len, key, nonce); + return chacha20_encrypt(data, len, key, nonce); } diff --git a/packer/packer.c b/packer/packer.c index ae25538..b7b5009 100644 --- a/packer/packer.c +++ b/packer/packer.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -29,21 +30,18 @@ void generate_random_key(uint8_t* key, size_t key_size) { } } -void multi_layer_encrypt(uint8_t* data, size_t len, const pack_header_t* header) { +int multi_layer_encrypt(uint8_t* data, size_t len, const pack_header_t* header) { // Layer 1: AES-256 - - aes256_encrypt(data, len, header->primary_key); - + if (aes256_encrypt(data, len, header->primary_key) != 0) + return -1; // Layer 2: ChaCha20 - - chacha20_encrypt(data, len, header->secondary_key, header->nonce); - + if (chacha20_encrypt(data, len, header->secondary_key, header->nonce) != 0) + return -1; // Layer 3: RC4 - rc4_encrypt_decrypt(header->tertiary_key, 32, data, data, len); - + return 0; } int is_elf64(const void* data) { @@ -124,11 +122,27 @@ int main(int argc, char* argv[]) { return 1; } - fseek(input_fp, 0, SEEK_END); - size_t file_size = ftell(input_fp); - fseek(input_fp, 0, SEEK_SET); + if (fseek(input_fp, 0, SEEK_END) != 0) { + fprintf(stderr, "Error: Cannot seek input file '%s'\n", input_file); + fclose(input_fp); + return 1; + } + long file_size_long = ftell(input_fp); + if (file_size_long <= 0 || + (unsigned long long)file_size_long > UINT32_MAX || + (unsigned long long)file_size_long > INT_MAX) { + fprintf(stderr, "Error: Input file must be between 1 byte and 2 GiB\n"); + fclose(input_fp); + return 1; + } + size_t file_size = (size_t)file_size_long; + if (fseek(input_fp, 0, SEEK_SET) != 0) { + fprintf(stderr, "Error: Cannot rewind input file '%s'\n", input_file); + fclose(input_fp); + return 1; + } - if (file_size == 0 || file_size > SIZE_MAX / 2) { + if (file_size > SIZE_MAX / 2) { fprintf(stderr, "Error: Input file is empty or too large\n"); fclose(input_fp); return 1; @@ -186,7 +200,14 @@ int main(int argc, char* argv[]) { generate_random_key(header.nonce, 16); generate_random_key(header.salt, 16); - multi_layer_encrypt(encrypted_data, file_size, &header); + if (multi_layer_encrypt(encrypted_data, file_size, &header) != 0) { + fprintf(stderr, "Error: encryption failed\n"); + secure_memory_wipe(file_data, file_size); + secure_memory_wipe(encrypted_data, file_size); + free(file_data); + free(encrypted_data); + return 1; + } FILE* output_fp = fopen(output_file, "wb"); if (!output_fp) { diff --git a/tests/arm32_eabi5_test.sh b/tests/arm32_eabi5_test.sh index 2f1e709..8c0962e 100644 --- a/tests/arm32_eabi5_test.sh +++ b/tests/arm32_eabi5_test.sh @@ -62,6 +62,11 @@ if ! readelf -h "$OUTPUT" | grep -q "Version5 EABI"; then exit 1 fi +if [[ ! -x "$OUTPUT" ]]; then + echo "ERROR: packed ARM32 executable is missing or not executable" + exit 1 +fi + ORIGINAL_RESULT="$(qemu-arm "$INPUT")" if [[ "$ORIGINAL_RESULT" != "hARMless-arm32-eabi5-ok" ]]; then echo "ERROR: unexpected ARM32 original output: $ORIGINAL_RESULT" @@ -75,8 +80,21 @@ else fi if [[ $PACKED_STATUS -eq 0 && "$PACKED_RESULT" == "hARMless-arm32-eabi5-ok" ]]; then + if [[ ! -x "$OUTPUT" ]]; then + echo "ERROR: ARM32 packed executable disappeared after first run" + exit 1 + fi + SECOND_RESULT="$(timeout 10s qemu-arm "$OUTPUT" 2>/dev/null)" + if [[ "$SECOND_RESULT" != "hARMless-arm32-eabi5-ok" ]]; then + echo "ERROR: ARM32 packed executable failed second run: $SECOND_RESULT" + exit 1 + fi echo "[x] ARM32/EABI5 packed executable runs under qemu-arm" else + if [[ ! -x "$OUTPUT" ]]; then + echo "ERROR: ARM32 packed executable disappeared after runtime attempt" + exit 1 + fi echo "[x] ARM32/EABI5 packed executable was created" echo "NOTE: qemu-user is not a reliable runtime validator for this loader; use native ARM32 or ARM64 32-bit compat for payload execution." fi diff --git a/tests/runtime_guard_test.sh b/tests/runtime_guard_test.sh index c0a4c3d..9f8eb85 100644 --- a/tests/runtime_guard_test.sh +++ b/tests/runtime_guard_test.sh @@ -15,6 +15,47 @@ if ! grep -q '\.codex_tmp' tests/unit_test.sh; then exit 1 fi +if ! grep -q 'HARMLESS_SELF_DELETE' loader/loader.c; then + echo "ERROR: loader self-delete must remain behind HARMLESS_SELF_DELETE" + exit 1 +fi + +if grep -q '^[[:space:]]*unlink(argv\[0\]);' loader/loader.c; then + echo "ERROR: loader must not unconditionally unlink argv[0]" + exit 1 +fi + +if ! grep -q 'HARMLESS_MASQUERADE_ARGV0' loader/strings.c; then + echo "ERROR: argv[0] mutation must remain behind HARMLESS_MASQUERADE_ARGV0" + exit 1 +fi + +if ! grep -q 'original_size > header->packed_size' loader/loader.c; then + echo "ERROR: loader must reject an original size larger than the packed payload" + exit 1 +fi + +if ! grep -q 'self_size - payload_offset' loader/loader.c; then + echo "ERROR: loader must validate payload size using bounded subtraction" + exit 1 +fi + +if ! grep -q 'UINT32_MAX' packer/packer.c; then + echo "ERROR: packer must reject inputs larger than the 32-bit header fields" + exit 1 +fi + +if ! grep -q '^int aes256_encrypt' include/crypto.h || \ + ! grep -q '^int chacha20_encrypt' include/crypto.h; then + echo "ERROR: crypto transform failures must be reported to callers" + exit 1 +fi + +if ! grep -q 'PACKED_BIN' tests/unit_test.sh || ! grep -q '\[\[ ! -x "$PACKED_BIN" \]\]' tests/unit_test.sh; then + echo "ERROR: ARM64 runtime test must assert packed executable persistence" + exit 1 +fi + if [[ "$(uname -m)" != "aarch64" ]]; then make test-runtime > "$LOG" 2>&1 if ! grep -q "SKIP: ARM64 runtime tests require aarch64 Linux" "$LOG"; then diff --git a/tests/unit_test.sh b/tests/unit_test.sh index 5636e3f..2fc7ed1 100755 --- a/tests/unit_test.sh +++ b/tests/unit_test.sh @@ -23,6 +23,13 @@ create_packed_binary() { fi } +assert_packed_still_exists() { + if [[ ! -x "$PACKED_BIN" ]]; then + echo "ERROR: Packed executable disappeared or is not executable" + exit 1 + fi +} + trap cleanup EXIT mkdir -p "$TMP_DIR" @@ -81,12 +88,12 @@ else echo "ERROR: Packed executable failed to run" exit 1 fi - -create_packed_binary +assert_packed_still_exists echo "Comparing output with original..." ORIGINAL_OUTPUT=$(timeout 5s /bin/ls --version 2>/dev/null | head -1 || echo "ls version output") PACKED_OUTPUT=$(timeout 5s "$PACKED_BIN" --version 2>/dev/null | head -1 || echo "packed ls version output") +assert_packed_still_exists if [[ "$ORIGINAL_OUTPUT" == "$PACKED_OUTPUT" ]]; then echo @@ -98,11 +105,10 @@ else echo " Packed: $PACKED_OUTPUT" fi -create_packed_binary - echo "Testing basic ls functionality..." ORIGINAL_LS=$(timeout 5s /bin/ls / | wc -l) PACKED_LS=$(timeout 5s "$PACKED_BIN" / | wc -l) +assert_packed_still_exists if [[ $ORIGINAL_LS -eq $PACKED_LS ]]; then echo