# helper: check exclusion is_excluded() { local p="$1" for pref in "${EXCLUDE_PREFIXES[@]}"; do if [[ "$p" == "$pref"* ]]; then return 0 fi done return 1 }
# helper: read first N bytes (prefers dd, falls back to head) read_head_bytes() { local file="$1" local n="$2" # attempt dd if dd if="$file" bs=1 count="$n" 2>/dev/null | od -An -t x1 | grep -q .; then dd if="$file" bs=1 count="$n" 2>/dev/null return 0 fi # fallback head if head -c "$n" "$file" 2>/dev/null | od -An -t x1 | grep -q .; then head -c "$n" "$file" 2>/dev/null return 0 fi return 1 }
# helper: check ELF magic is_elf_by_magic() { local f="$1" if read_head_bytes "$f" 4 2>/dev/null | od -An -t x1 | tr -s ' ' | sed 's/^ *//' | grep -qi '^7f 45 4c 46'; then return 0 fi return 1 }
# helper: extract printable strings from first N bytes (approx strings, length >= minlen) extract_printables_head() { local f="$1" local out="$2" local n=${3:-4096} local minlen=${4:-8} # read N bytes, keep printable ASCII (tab/newline/CR and 0x20-0x7E) read_head_bytes "$f" "$n" 2>/dev/null | tr -cd '\11\12\15\40-\176' | \ awk -v minlen="$minlen" ' { line=$0 # break by non-word-ish boundaries, but keep punctuation n=split(line, a, /[^[:alnum:][:punct:]]/) for(i=1;i<=n;i++){ if(length(a[i])>=minlen) print a[i] } }' | sed -n '1,1000p' > "$out" 2>/dev/null || true }
# iterate roots for root in "${ROOTS[@]}"; do if [ ! -e "$root" ]; then echo "[!] skip missing root: $root" | tee -a "${LOG}" continue fi
# find files and symlinks (avoid following mounts via -xdev) find "$root" -xdev \( -type f -o -type l \) -print0 2>/dev/null | while IFS= read -r -d '' f; do # skip excluded prefixes if is_excluded "$f"; then continue fi
hashval="" if [ -f "$f" ] && [ -n "$HASHCMD" ]; then # compute hash, ignore errors if h=$($HASHCMD --binary -- "$f" 2>/dev/null | awk '{print $1}'); then hashval="$h" fi elif [ -L "$f" ]; then target=$(readlink -m -- "$f" 2>/dev/null || echo "") hashval="symlink->${target}" fi
# Determine nature: text/binary/symlink/elf nature="unknown" if [ -L "$f" ]; then nature="symlink" elif [ -f "$f" ]; then # quick binary test: presence of NUL char if grep -q $'\x00' "$f" 2>/dev/null; then nature="binary" else nature="text" fi fi
# If ELF by magic, produce head + strings + hex if [ -f "$f" ] && is_elf_by_magic "$f"; then nature="elf" safe=$(echo "$f" | sed 's/[^A-Za-z0-9._-]/_/g') outbase="${ELF_DIR}/${safe}" mkdir -p "$(dirname "$outbase")" # head binary if read_head_bytes "$f" 4096 >/dev/null 2>&1; then read_head_bytes "$f" 4096 > "${outbase}.head.bin" 2>/dev/null || true fi # hexdump of head if [ -f "${outbase}.head.bin" ]; then od -An -tx1 "${outbase}.head.bin" > "${outbase}.head.hex" 2>/dev/null || true fi # extract printable-like strings from head extract_printables_head "$f" "${outbase}.strings.txt" 4096 8 fi
# write manifest line (escape '|' in path) esc_path=$(printf "%s" "$f" | sed 's/|/\\|/g') printf "%s|%s|%s|%s|%s|%s|%s\n" "$esc_path" "$mode" "$uidgid" "$size" "$mtime" "${hashval}" "$nature" >> "${MANIFEST}" done done
# capture process list if command -v ps >/dev/null 2>&1; then ps -ef > "${OUTDIR}/ps_list.txt" 2>/dev/null || true fi
# package manager inventory if command -v dpkg >/dev/null 2>&1; then dpkg -l > "${OUTDIR}/dpkg_list.txt" 2>/dev/null || true fi if command -v synopkg >/dev/null 2>&1; then synopkg list > "${OUTDIR}/synopkg_list.txt" 2>/dev/null || true fi
echo "[*] Packaging results into ${PACKFILE} ..." | tee -a "${LOG}" tar -czf "${PACKFILE}" -C /tmp "$(basename "${OUTDIR}")" echo "[*] Done. Tarball: ${PACKFILE}" | tee -a "${LOG}" echo "If you want to scp it to the analysis host, run on host:" echo " scp root@<DSM_IP>:${PACKFILE} ~/analysis/"
#!/usr/bin/env bash # analyze_patch_dsm.sh - flexible: accepts either extracted collector directories or tarballs # Usage: # ./analyze_patch_dsm.sh VULN_DIR_OR_TARBALL PATCHED_DIR_OR_TARBALL OUTDIR # # Behavior: # - If first two args are directories containing manifest.csv, use them directly (no extraction). # - Otherwise treat them as tar.gz and extract into workdir. # set -euo pipefail IFS=$'\n\t' export LC_ALL=C
if [ "$#" -ne 3 ]; then echo "Usage: $0 VULN_DIR_OR_TARBALL PATCHED_DIR_OR_TARBALL OUTPUT_DIR" exit 2 fi
A="$1" B="$2" OUTDIR="$3"
# Ensure PATH includes common locations where diffoscope may live export PATH="$PATH:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/snap/bin:/home/$USER/.local/bin"
# Helper: check if path is directory containing manifest.csv is_extracted_dir() { local d="$1" [ -d "$d" ] && [ -f "$d/manifest.csv" ] }
# Prepare V_TOP and P_TOP if is_extracted_dir "$A" && is_extracted_dir "$B"; then echo "[*] Both arguments are directories with manifest.csv; using them directly." V_TOP="$(cd "$A" && pwd -P)" P_TOP="$(cd "$B" && pwd -P)" else # treat as tarballs, extract into work echo "[*] Treating inputs as tarballs; extracting into $WORK ..." rm -rf "$WORK"/* mkdir -p "$WORK/v" "$WORK/p" tar -xzf "$A" -C "$WORK/v" tar -xzf "$B" -C "$WORK/p" # find extracted top dirs V_TOP=$(find "$WORK/v" -maxdepth 2 -type d -name "patch_collect_*" | head -n1 || true) P_TOP=$(find "$WORK/p" -maxdepth 2 -type d -name "patch_collect_*" | head -n1 || true) if [ -z "$V_TOP" ] || [ -z "$P_TOP" ]; then echo "[!] Could not find extracted patch_collect_* directories under $WORK" echo " Check that the tarballs contain top-level patch_collect_* directories or pass exploded directories instead." exit 3 fi fi
V_MANIFEST="$V_TOP/manifest.csv" P_MANIFEST="$P_TOP/manifest.csv" if [ ! -f "$V_MANIFEST" ] || [ ! -f "$P_MANIFEST" ]; then echo "[!] manifest.csv missing in one of the inputs:" echo " V_MANIFEST=$V_MANIFEST" echo " P_MANIFEST=$P_MANIFEST" exit 4 fi
# Ensure modified list unique and present if [ -f "$REPORT/modified_files.txt" ]; then sort -u "$REPORT/modified_files.txt" -o "$REPORT/modified_files.txt" || true else touch "$REPORT/modified_files.txt" fi
# Keywords for suspect scoring (extendable) KEYWORDS="account|Auth|entry.cgi|getenv|setenv|system|popen|exec|sprintf|snprintf|passwd|authenticator|sanitize|escape"
# Robust diffoscope detection: allow override via DIFFOSCOPE_BIN env var DIFFOSCOPE_BIN="${DIFFOSCOPE_BIN:-$(command -v diffoscope 2>/dev/null || true)}" if [ -z "$DIFFOSCOPE_BIN" ] && [ -x "/home/$USER/.local/bin/diffoscope" ]; then DIFFOSCOPE_BIN="/home/$USER/.local/bin/diffoscope" fi if [ -z "$DIFFOSCOPE_BIN" ] && [ -x "/usr/local/bin/diffoscope" ]; then DIFFOSCOPE_BIN="/usr/local/bin/diffoscope" fi if [ -z "$DIFFOSCOPE_BIN" ] && [ -x "/snap/bin/diffoscope" ]; then DIFFOSCOPE_BIN="/snap/bin/diffoscope" fi
if [ -n "$DIFFOSCOPE_BIN" ]; then echo "[*] diffoscope found: $DIFFOSCOPE_BIN" USE_DIFFOSCOPE=1 else echo "[*] diffoscope not found in PATH; fallback to head.bin/strings comparison" USE_DIFFOSCOPE=0 fi
safe_name=$(echo "$rel" | sed 's/[^A-Za-z0-9._-]/_/g')
# If diffoscope available, run it (non-fatal) if [ "$USE_DIFFOSCOPE" -eq 1 ]; then out_html="$REPORT/diffoscope/${safe_name}.html" echo "[*] diffoscope: comparing $vfull vs $pfull -> $out_html" "$DIFFOSCOPE_BIN" --html "$out_html" "$vfull" "$pfull" >/dev/null 2>&1 || echo "[!] diffoscope failed on $rel (continuing)" fi
# ELF head compare: collector saved head bins in elf_info/<safe>.head.bin vuln_head="$V_TOP/elf_info/${safe_name}.head.bin" patched_head="$P_TOP/elf_info/${safe_name}.head.bin" vuln_strings="$V_TOP/elf_info/${safe_name}.strings.txt" patched_strings="$P_TOP/elf_info/${safe_name}.strings.txt"
if [ -f "$vuln_head" ] || [ -f "$patched_head" ]; then echo "[*] head compare for $rel" outdir="$REPORT/elf_head_diff/${safe_name}" mkdir -p "$outdir" [ -f "$vuln_head" ] && cp "$vuln_head" "$outdir/vuln.head.bin" [ -f "$patched_head" ] && cp "$patched_head" "$outdir/patched.head.bin" # produce hex previews if [ -f "$outdir/vuln.head.bin" ]; then xxd -g 1 "$outdir/vuln.head.bin" | sed -n '1,200p' > "$outdir/vuln.head.hex" || true fi if [ -f "$outdir/patched.head.bin" ]; then xxd -g 1 "$outdir/patched.head.bin" | sed -n '1,200p' > "$outdir/patched.head.hex" || true fi if [ -f "$outdir/vuln.head.bin" ] && [ -f "$outdir/patched.head.bin" ]; then if cmp -s "$outdir/vuln.head.bin" "$outdir/patched.head.bin"; then echo "HEAD_IDENTICAL" > "$outdir/head_cmp.txt" else echo "HEAD_DIFFER" > "$outdir/head_cmp.txt" fi fi
# prepare strings fallback (may be empty files) if [ -f "$vuln_strings" ]; then cp "$vuln_strings" "$outdir/vuln.strings.txt"; else touch "$outdir/vuln.strings.txt"; fi if [ -f "$patched_strings" ]; then cp "$patched_strings" "$outdir/patched.strings.txt"; else touch "$outdir/patched.strings.txt"; fi
# produce small comm diff of printable strings (first 200 lines) comm -3 <(sort -u "$outdir/vuln.strings.txt") <(sort -u "$outdir/patched.strings.txt") > "$outdir/strings_comm3.txt" 2>/dev/null || true head -n 200 "$outdir/strings_comm3.txt" > "$outdir/strings_diff.txt" 2>/dev/null || true
# keyword scoring (safe numeric defaults) kcount_vuln=0 kcount_patch=0 if [ -s "$outdir/vuln.strings.txt" ]; then kcount_vuln=$(grep -Eio "$KEYWORDS" "$outdir/vuln.strings.txt" | wc -l || true) fi if [ -s "$outdir/patched.strings.txt" ]; then kcount_patch=$(grep -Eio "$KEYWORDS" "$outdir/patched.strings.txt" | wc -l || true) fi kcount_vuln=${kcount_vuln:-0} kcount_patch=${kcount_patch:-0} total_k=$((kcount_vuln + kcount_patch)) suspect_score["$rel"]=$total_k fi
done < "$REPORT/modified_files.txt"
# write top suspects sorted by score { echo "Top suspects (score file path)" for k in "${!suspect_score[@]}"; do echo -e "${suspect_score[$k]}\t$k" done | sort -rn -k1,1 } > "$REPORT/top_suspects.txt"
# produce index.html cat > "$REPORT/index.html" <<'HTML' <html> <head><meta charset="utf-8"><title>Patch Diff Report</title></head> <body> <h1>Patch Diff Report</h1> <ul> <li><a href="summary.txt">summary.txt</a></li> <li><a href="added_files.txt">added_files.txt</a></li> <li><a href="removed_files.txt">removed_files.txt</a></li> <li><a href="modified_files.txt">modified_files.txt</a></li> <li><a href="top_suspects.txt">top_suspects.txt</a></li> </ul> <hr> <p>Per-file artifacts are in elf_head_diff/ and diffoscope/ (if generated)</p> </body></html> HTML
# copy lists to report dir (idempotent) for f in summary added_files removed_files modified_files top_suspects; do src="$REPORT/${f}.txt" # ensure created [ -f "$REPORT/${f}.txt" ] || touch "$REPORT/${f}.txt" done
echo "[*] Analysis complete. Report directory: $REPORT" echo "[*] Open $REPORT/index.html or inspect $REPORT/top_suspects.txt"