From ac51cc70aa844c3d36e9c1e82e7270c1c92932c6 Mon Sep 17 00:00:00 2001 From: NikitolProject Date: Wed, 25 Feb 2026 02:26:05 +0300 Subject: [PATCH] feat(06-02): add XOR header bootstrapping and encrypted TOC support to Shell decoder - Add XOR_KEY_HEX constant and hex_to_bin() helper (xxd + od fallback) - Replace magic check with XOR bootstrapping: read 40 bytes, XOR if mismatch - Write de-XORed header to temp file for field parsing - Add TOC decryption via openssl enc when flags bit 1 is set - Switch TOC parsing loop from $ARCHIVE to $TOC_FILE variable - Update HMAC verification to construct IV from parsed hex (not archive position) - All 7 cross-validation tests pass (Rust pack -> Shell decode -> SHA-256 match) --- shell/decode.sh | 127 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 25 deletions(-) diff --git a/shell/decode.sh b/shell/decode.sh index aaab589..c61c591 100755 --- a/shell/decode.sh +++ b/shell/decode.sh @@ -102,17 +102,69 @@ if ! printf 'test' | openssl dgst -sha256 -mac HMAC -macopt hexkey:00 >/dev/null fi # ------------------------------------------------------- -# Header parsing (FORMAT.md Section 4) +# XOR obfuscation key (FORMAT.md Section 9.1) # ------------------------------------------------------- -# Header is 40 bytes at offset 0x00 -magic_hex=$(read_hex "$ARCHIVE" 0 4) +XOR_KEY_HEX="a53c960fe17b4dc8" + +# ------------------------------------------------------- +# hex_to_bin +# Write binary data from a hex string to a file. +# ------------------------------------------------------- +hex_to_bin() { + if [ "$HAS_XXD" = "1" ]; then + printf '%s' "$1" | xxd -r -p > "$2" + else + _htb_hex="$1" + _htb_i=0 + _htb_len=${#_htb_hex} + : > "$2" + while [ "$_htb_i" -lt "$_htb_len" ]; do + _htb_byte=$(printf '%s' "$_htb_hex" | cut -c$((_htb_i + 1))-$((_htb_i + 2))) + printf "\\$(printf '%03o' "0x$_htb_byte")" >> "$2" + _htb_i=$((_htb_i + 2)) + done + fi +} + +# ------------------------------------------------------- +# Header parsing with XOR bootstrapping (FORMAT.md Section 9.1, Section 10) +# ------------------------------------------------------- +# Read 40-byte header as hex string (80 hex chars) +raw_header_hex=$(read_hex "$ARCHIVE" 0 40) +magic_hex=$(printf '%.8s' "$raw_header_hex") if [ "$magic_hex" != "00ea7263" ]; then - printf 'Invalid archive: bad magic bytes (got %s)\n' "$magic_hex" >&2 - exit 1 + # Attempt XOR de-obfuscation + header_hex="" + byte_idx=0 + while [ "$byte_idx" -lt 40 ]; do + hex_pos=$((byte_idx * 2)) + # Extract this byte from raw header (2 hex chars) + raw_byte=$(printf '%s' "$raw_header_hex" | cut -c$((hex_pos + 1))-$((hex_pos + 2))) + # Extract key byte (cyclic) + key_pos=$(( (byte_idx % 8) * 2 )) + key_byte=$(printf '%s' "$XOR_KEY_HEX" | cut -c$((key_pos + 1))-$((key_pos + 2))) + # XOR + xored=$(printf '%02x' "$(( 0x$raw_byte ^ 0x$key_byte ))") + header_hex="${header_hex}${xored}" + byte_idx=$((byte_idx + 1)) + done + + # Verify magic after XOR + magic_hex=$(printf '%.8s' "$header_hex") + if [ "$magic_hex" != "00ea7263" ]; then + printf 'Invalid archive: bad magic bytes (got %s)\n' "$magic_hex" >&2 + exit 1 + fi +else + header_hex="$raw_header_hex" fi -version_hex=$(read_hex "$ARCHIVE" 4 1) +# Write de-XORed header to temp file for field parsing +hex_to_bin "$header_hex" "$TMPDIR/header.bin" + +# Parse header fields from de-XORed temp file +version_hex=$(read_hex "$TMPDIR/header.bin" 4 1) version=$(printf '%d' "0x${version_hex}") if [ "$version" -ne 1 ]; then @@ -120,66 +172,87 @@ if [ "$version" -ne 1 ]; then exit 1 fi -flags_hex=$(read_hex "$ARCHIVE" 5 1) +flags_hex=$(read_hex "$TMPDIR/header.bin" 5 1) flags=$(printf '%d' "0x${flags_hex}") -file_count=$(read_le_u16 "$ARCHIVE" 6) -toc_offset=$(read_le_u32 "$ARCHIVE" 8) -toc_size=$(read_le_u32 "$ARCHIVE" 12) +file_count=$(read_le_u16 "$TMPDIR/header.bin" 6) +toc_offset=$(read_le_u32 "$TMPDIR/header.bin" 8) +toc_size=$(read_le_u32 "$TMPDIR/header.bin" 12) +toc_iv_hex=$(read_hex "$TMPDIR/header.bin" 16 16) printf 'Archive: %d files\n' "$file_count" +# ------------------------------------------------------- +# TOC decryption (FORMAT.md Section 9.2) +# ------------------------------------------------------- +toc_encrypted=$(( flags & 2 )) + +if [ "$toc_encrypted" -ne 0 ]; then + # Extract encrypted TOC to temp file + dd if="$ARCHIVE" bs=1 skip="$toc_offset" count="$toc_size" of="$TMPDIR/toc_enc.bin" 2>/dev/null + + # Decrypt TOC + openssl enc -d -aes-256-cbc -nosalt \ + -K "$KEY_HEX" -iv "$toc_iv_hex" \ + -in "$TMPDIR/toc_enc.bin" -out "$TMPDIR/toc_dec.bin" + + TOC_FILE="$TMPDIR/toc_dec.bin" + TOC_BASE_OFFSET=0 +else + TOC_FILE="$ARCHIVE" + TOC_BASE_OFFSET=$toc_offset +fi + # ------------------------------------------------------- # TOC parsing loop (FORMAT.md Section 5) # ------------------------------------------------------- -pos=$toc_offset +pos=$TOC_BASE_OFFSET extracted=0 i=0 while [ "$i" -lt "$file_count" ]; do # -- name_length (u16 LE) -- - name_length=$(read_le_u16 "$ARCHIVE" "$pos") + name_length=$(read_le_u16 "$TOC_FILE" "$pos") pos=$((pos + 2)) # -- filename (raw UTF-8 bytes) -- - filename=$(dd if="$ARCHIVE" bs=1 skip="$pos" count="$name_length" 2>/dev/null) + filename=$(dd if="$TOC_FILE" bs=1 skip="$pos" count="$name_length" 2>/dev/null) pos=$((pos + name_length)) # -- original_size (u32 LE) -- - original_size=$(read_le_u32 "$ARCHIVE" "$pos") + original_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- compressed_size (u32 LE) -- - compressed_size=$(read_le_u32 "$ARCHIVE" "$pos") + compressed_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- encrypted_size (u32 LE) -- - encrypted_size=$(read_le_u32 "$ARCHIVE" "$pos") + encrypted_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- data_offset (u32 LE) -- - data_offset=$(read_le_u32 "$ARCHIVE" "$pos") + data_offset=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- iv (16 bytes as hex) -- - iv_toc_pos=$pos - iv_hex=$(read_hex "$ARCHIVE" "$pos" 16) + iv_hex=$(read_hex "$TOC_FILE" "$pos" 16) pos=$((pos + 16)) # -- hmac (32 bytes as hex) -- - hmac_hex=$(read_hex "$ARCHIVE" "$pos" 32) + hmac_hex=$(read_hex "$TOC_FILE" "$pos" 32) pos=$((pos + 32)) # -- sha256 (32 bytes as hex) -- - sha256_hex=$(read_hex "$ARCHIVE" "$pos" 32) + sha256_hex=$(read_hex "$TOC_FILE" "$pos" 32) pos=$((pos + 32)) # -- compression_flag (1 byte as hex) -- - compression_flag=$(read_hex "$ARCHIVE" "$pos" 1) + compression_flag=$(read_hex "$TOC_FILE" "$pos" 1) pos=$((pos + 1)) # -- padding_after (u16 LE) -- - padding_after=$(read_le_u16 "$ARCHIVE" "$pos") + padding_after=$(read_le_u16 "$TOC_FILE" "$pos") pos=$((pos + 2)) # ======================================================= @@ -190,9 +263,13 @@ while [ "$i" -lt "$file_count" ]; do dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/null # b. Verify HMAC (if available) + # HMAC input = IV (16 bytes) || ciphertext + # IV comes from the parsed TOC entry (iv_hex), not from an archive position if [ "$SKIP_HMAC" = "0" ]; then + # Write IV bytes to temp file from parsed hex + hex_to_bin "$iv_hex" "$TMPDIR/iv.bin" computed_hmac=$( { - dd if="$ARCHIVE" bs=1 skip="$iv_toc_pos" count=16 2>/dev/null + cat "$TMPDIR/iv.bin" cat "$TMPDIR/ct.bin" } | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' ) @@ -239,7 +316,7 @@ while [ "$i" -lt "$file_count" ]; do extracted=$((extracted + 1)) # Clean up temp files for next iteration - rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin" + rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin" "$TMPDIR/iv.bin" i=$((i + 1)) done