Created
July 24, 2026 17:42
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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: