Skip to content

Instantly share code, notes, and snippets.

@Europia79
Created July 24, 2026 17:42
Show Gist options
  • Select an option

  • Save Europia79/936ad6e88f9c41408b588c3abd3f1f8a to your computer and use it in GitHub Desktop.

Select an option

Save Europia79/936ad6e88f9c41408b588c3abd3f1f8a to your computer and use it in GitHub Desktop.
IPS patch analyzer that does Binary Fingerprint Analysis to look for suspicious writes, and determine (in)compatibility with respect to Headers that change offsets.
#!/bin/bash
# cmp -l base hack | wc -l
# cmp -l base hack | while IFS= read -r line; do set $line; printf "%08X %02X %02X\n" $(($1 - 1)) 0$2 0$3 ; done
##########################################
# CONFIG #
##########################################
declare -gi max_per_record=32; # 0xFFFF
susreport_sfc="zsusreport_sfc.csv"; # OR NULL
susreport_smc="zsusreport_smc.csv"; # OR NULL
ips_file="patch.ips"; # patch file under inspection
sfc_file="rom.sfc"; # Unheadered, noninterleaved
smc_file="rom.smc"; # Headered, noninterleaved
ufo_file="rom.ufo"; # Headered, interleaved
gd3_file="sf32rom"; # Unheadered, interleaved
##########################################
# CONSTANTS #
##########################################
declare -gi ips_eof=0x454F46; # "0x454F46"="EOF"
declare -gi max_size=$max_per_record;
declare -gi max_len=$(( max_size * 2 ));
N=$'\n'
# -------------------------------------
# Automated Conversions via ucon64.exe:
# https://ucon64.sourceforge.io/
# -------------------------------------
# ucon64 --smc "$sfc_file";
# ucon64 --ufo "$sfc_file";
# ucon64 --gd3 "$sfc_file";
##########################################
# TRAP #
##########################################
function onExit() {
rc=$?;
printf "\e[1;33m%s\e[1;36m\n" "onExit()..." >&2
# FINAL REPORT:
echo "------------------------------";
echo "-----total writes: ${ips_count}";
echo ".sfc shared bytes: ${ips_com_sfc}";
echo ".smc shared bytes: ${ips_com_smc}";
# SUSPICIOUS WRITES: SFC
if (( ${#sfc_info_array[@]} > 0 ));
then
echo "${sfc_info_array[@]}" | tr ' ' $'\n' > "$susreport_sfc"
echo "------------------------------------";
echo "--Total Suspicious Writes to (.sfc): ${#sfc_info_array[@]}";
echo "------------------------------------";
echo "(Displaying the first ten results):";
cat "${susreport_sfc}" | head -n 10 | column -t -s "|" -N "NUM,@IPS,#IPS,#ROM,@ROM_ADDRESS" -R "NUM,#IPS";
echo "To view all results, do:";
echo "cat \"${susreport_sfc}\" | column -t -s \"|\" -N \"NUM,@IPS,#IPS,#ROM,@ROM_ADDRESS\" -R \"NUM,#IPS\"";
printf "Warning: offset %x is NOT a suspicious write!!!\n" $(( ips_eof - 1 ));
else
echo "----------------------------------------";
echo "There are no suspicious writes to (.sfc)";
echo "----------------------------------------";
fi
# SUSPICIOUS WRITES: SMC
if (( ${#smc_info_array[@]} > 0 ));
then
echo "${smc_info_array[@]}" | tr ' ' $'\n' > "$susreport_smc"
echo "------------------------------------";
echo "--Total Suspicious Writes to (.smc): ${#smc_info_array[@]}";
echo "------------------------------------";
echo "(Displaying the first ten results):";
cat "${susreport_smc}" | head -n 10 | column -t -s "|" -N "NUM,@IPS,#IPS,#ROM,@ROM_ADDRESS" -R "NUM,#IPS";
echo "To view all results, do:";
echo "cat \"${susreport_smc}\" | column -t -s \"|\" -N \"NUM,@IPS,#IPS,#ROM,@ROM_ADDRESS\" -R \"NUM,#IPS\"";
printf "Warning: offset %x is NOT a suspicious write!!!\n" $(( ips_eof - 1 ));
else
echo "----------------------------------------";
echo "There are no suspicious writes to (.smc)";
echo "----------------------------------------";
fi
printf "\e[1;33m%s\e[0m\n" "...done." >&2
echo -en "\007";
exit ${rc};
} >&2
trap onExit EXIT
declare -gi ips_index=5; # IPS INDEX
declare -gi ips_count=0; # TOTAL NUM IPS WRITES
declare -gi ips_com_sfc=0; # COMMON BYTES: SFC
declare -gi ips_com_smc=0; # COMMON BYTES: SMC
declare -ga sfc_info_array; # SUS IPS RECORDS(.sfc): EACH ENTRY IN PSV FORMAT
declare -ga smc_info_array; # SUS IPS RECORDS(.smc): EACH ENTRY IN PSV FORMAT
##########################################
# MAIN #
##########################################
while true
do
# DECLARE AND PARASE OFFSET & SIZE:
declare -i prev=$ips_index;
declare -i offset=0;
declare -i size=0;
declare -i rle_len=0;
declare rle_val="";
declare data="";
offset=0x$(xxd -ps -s ${ips_index} -l 3 "${ips_file}");
ips_index+=3;
size=0x$(xxd -ps -s ${ips_index} -l 2 "${ips_file}");
ips_index+=2;
# EOF CHECK:
if (( offset == ips_eof )); then echo "EOF"; break; fi;
# PARSE DATA:
if (( size == 0 ));
then
declare -i rle_len=0x$(xxd -ps -s ${ips_index} -l 2 "${ips_file}");
ips_index+=2;
rle_val=$(xxd -ps -s ${ips_index} -l 1 "${ips_file}");
ips_index+=1;
if (( rle_len == 0 )); then echo "INVALID RLE_LEN: Cannot be zero."; break; fi;
for z in $(seq 1 $rle_len); do data+="$rle_val"; done
size=$rle_len;
ips_count+=${size};
else
data=$(xxd -ps -s ${ips_index} -l ${size} "${ips_file}" | tr -d $'\n');
ips_index+=${size};
ips_count+=${size};
fi
if [ -z "$data" ]; then echo "NULL DATA"; break; fi;
# COMMON BYTES: SFC
declare -i num_bytes=0;
sfc_string="";
sfc_data=$(xxd -ps -s ${offset} -l ${size} "${sfc_file}" | tr -d $'\n');
for (( n=0; n < (( ${size} * 2 )); n+=2 ))
do
num_bytes+=1;
if [[ "${sfc_data:$n:2}" == "${data:$n:2}" ]]
then
ips_com_sfc+=1;
sfc_string+="${sfc_data:$n:2}";
else
sfc_string+="--";
fi
(( num_bytes >= max_size )) && break 1;
done
# FIRST SUS BYTE: SFC
if [[ ${sfc_data:0:2} == ${data:0:2} ]];
then
this_info="";
declare -i this_num;
this_num="${#sfc_info_array[@]}";
this_num=$(( this_num + 1 ));
this_ips_index=$(printf "%06x" "${prev}");
this_ips_val="${data:0:2}";
this_sfc_val="${sfc_data:0:2}";
declare -i loc=$(( offset + 0 ));
this_sfc_loc=$(printf "%06x" "${loc}");
this_info+="${this_num}.";
this_info+="|${this_ips_index}";
this_info+="|${this_ips_val}";
this_info+="|${this_sfc_val}";
this_info+="|${this_sfc_loc}";
sfc_info_array+=("${this_info}");
fi
# LAST SUS BYTE: SFC
if (( size > 1 )) && [[ ${sfc_data:(-2)} == ${data:(-2)} ]];
then
this_info="";
declare -i this_num;
this_num="${#sfc_info_array[@]}";
this_num=$(( this_num + 1 ));
this_ips_index=$(printf "%06x" "${prev}");
this_ips_val="${data:(-2)}";
this_sfc_val="${sfc_data:(-2)}";
declare -i loc=$(( offset + size - 1 ));
this_sfc_loc=$(printf "%06x" "${loc}");
this_info+="${this_num}.";
this_info+="|${this_ips_index}";
this_info+="|${this_ips_val}";
this_info+="|${this_sfc_val}";
this_info+="|${this_sfc_loc}";
sfc_info_array+=("${this_info}");
fi
# COMMON BYTES: SMC
num_bytes=0;
smc_string="";
smc_data=$(xxd -ps -s ${offset} -l ${size} "${smc_file}" | tr -d $'\n');
for (( n=0; n < (( ${size} * 2 )); n+=2 ))
do
num_bytes+=1;
if [[ "${smc_data:$n:2}" == "${data:$n:2}" ]]
then
ips_com_smc+=1;
smc_string+="${smc_data:$n:2}";
else
smc_string+="--";
fi
(( num_bytes >= max_size )) && break 1;
done
# -------------------------------------
# XX----------------YY :....SUS-Writes:
# -------------------------------------
# --XXXX--YYYY--ZZZZ-- :...DNA-Strands:
# --XXXX--YYYY----ZZ-- :.DNA-Fragments:
# --XXXX----YY----ZZ-- :...RNA-Strands:
# --XX------YY----ZZ-- :.RNA-Fragments:
# --XXXX--------YYYY-- :..Fingerprints:
# --XXXX----------YY-- :.Partialprints:
# --XXXX-------------- :....Footprints:
# --XX---------------- :.......Shadows:
# --00------------FF-- :........Ghosts:
# -------------------------------------
# :..Shared-Bytes:
# FIRST SUS BYTE: SMC
if [[ ${smc_data:0:2} == ${data:0:2} ]];
then
this_info="";
declare -i this_num;
this_num="${#smc_info_array[@]}";
this_num=$(( this_num + 1 ));
this_ips_index=$(printf "%06x" "${prev}");
this_ips_val="${data:0:2}";
this_smc_val="${smc_data:0:2}";
declare -i loc=$(( offset + 0 ));
this_smc_loc=$(printf "%06x" "${loc}");
this_info+="${this_num}.";
this_info+="|${this_ips_index}";
this_info+="|${this_ips_val}";
this_info+="|${this_smc_val}";
this_info+="|${this_smc_loc}";
smc_info_array+=("${this_info}");
fi
# LAST SUS BYTE: SMC
if (( size > 1 )) && [[ ${smc_data:(-2)} == ${data:(-2)} ]];
then
this_info="";
declare -i this_num;
this_num="${#smc_info_array[@]}";
this_num=$(( this_num + 1 ));
this_ips_index=$(printf "%06x" "${prev}");
this_ips_val="${data:(-2)}";
this_smc_val="${smc_data:(-2)}";
declare -i loc=$(( offset + size - 1 ));
this_smc_loc=$(printf "%06x" "${loc}");
this_info+="${this_num}.";
this_info+="|${this_ips_index}";
this_info+="|${this_ips_val}";
this_info+="|${this_smc_val}";
this_info+="|${this_smc_loc}";
smc_info_array+=("${this_info}");
fi
# IPS RECORD REPORT:
echo "------------------------------";
printf "patch->index: 0x%06x\n" ${prev};
printf "offset: 0x%06x\n" ${offset};
printf "size: 0x%x\n" ${size};
echo "data: '${data:0:$max_len}'";
echo ".sfc: '${sfc_string}'";
echo ".smc: '${smc_string}'";
echo "count: '${ips_count}'";
printf "patch->index: 0x%06x\n" ${ips_index};
done # DONE PARSING IPS PATCH
exit 0;
@Europia79

Europia79 commented Jul 24, 2026

Copy link
Copy Markdown
Author

This is just a quick & dirty "proof of concept" that I whipped up in Bash.

Right now, it is tightly coupled to some underlying assumptions:

Like, that you're wanting to compare a Headered source file against an Unheadered source file (like with SMC vs SFC on SNES).

Here's a sample report (for Chrono Trigger Refined): https://pastebin.com/raw/5VfQsE9L

Bytes hidden by dashes are not relevant: We are only looking for shared bytes (that leave evidence of the TRUE Parent-Clone Baserom & Source File). This is simply a consequence of the IPS specification:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment