├── .gitignore ├── LICENSE ├── README.md ├── get-sublime ├── patches.py ├── reghex_patcher.py ├── reghex_patcher_full.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reghex_patcher 2 | 3 | A patcher working on hex bytes with support for x64 CALL instruction 4 | 5 | - `hex_patcher` (in branch `hex`) is based on the first version of sublime-text-4-patcher by @rainbowpigeon 6 | - refactor the computation of referenced offset in x64 CALL instruction 7 | - support multiple set of patterns (for various apps, versions ...) 8 | - use md5 hash on target file to detect which patterns should be applied 9 | - replace real byte pattern with wildcards, so script-kiddies can't use this 10 | - `reghex_x64_patcher` (in branch `reghex_x64`) has better support for x64 architecture: 11 | - use `reghex` instead of hex bytes and wildcards ("?") 12 | - use regex look-ahead instead of `offset` into hex bytes 13 | - use regex alternatives ("|") to combine similar patterns 14 | - generic detection of app, OS ... using lists of `reghex` 15 | - compute count of NOP bytes based on pre-defined instruction lengths 16 | - compute referenced offset based on pre-defined instruction lengths 17 | - `reghex_patcher` (in branch `reghex`) improve support for multiple architectures 18 | - remove computation for x64 architecture (for referenced offset, count of NOP bytes) 19 | - use regex look-ahead intead of pre-defined instruction lengths (for computation of referenced offset) 20 | - use pre-defined patch bytes intead of computation for count of NOP bytes 21 | - better detection of architecture, app, OS ... using 1 list of `reghex` 22 | - `reghex_patcher_full` (in branch `main`) add detection of references from/to matched reghex patterns 23 | - show referenced memory address in matched patterns 24 | - show all references to matched patterns 25 | - show the function that contains these references 26 | - allow multiple sub-patterns (each can be patched individually) 27 | - detect constant in instructions by showing matched bytes (when patch bytes is empty) 28 | - use `arch`/`os` in patterns to specify targeted architure and operating system 29 | - detect architure and operating system from input file/content 30 | - compute memory address of references in x64/arm64 instructions 31 | - extract/patch executable files from MacOS universal binary 32 | 33 | # what is reghex 34 | 35 | - `reghex` is a regex with hex bytes, such as `E8 . . . . (?=C3)`. Hex bytes are 2 hex-digit tokens separated by word boundaries. 36 | - the purpose of `reghex` is to enable the power of regex when searching for patterns in binary data 37 | 38 | - `reghex` should be converted to regex by escaping hex bytes, and then used in verbose mode (with flag X) 39 | - unescaped spaces are ignored in `reghex` as well as in regex 40 | 41 | # usage 42 | 43 | - you must provide list of Fix in class Fixes 44 | - example: `Fix(name="hotfix", reghex="CA FE BA BE", patch="FA CE")` will replace hex bytes `CA FE` with `FA CE` 45 | - there are some pre-defined patches: `nop5`, `ret`, `ret0`, `ret1` ... 46 | 47 | # credits 48 | 49 | - @leogx9r for signatures and patching logic 50 | - @rainbowpigeon for the first version of sublime-text-4-patcher 51 | -------------------------------------------------------------------------------- /get-sublime: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ $1 != text && $1 != merge ]]; then 3 | echo ERROR: unknown app: $1; exit 1 4 | fi 5 | case $2 in 6 | all ) 7 | for os in mac win linux linux-arm64; do echo ==== $os ====; $0 $1 $os $3; done; 8 | exit ;; 9 | mac ) 10 | package=sublime_$1_build_$3_mac.zip 11 | file="Sublime ${1^}.app/Contents/MacOS/sublime_$1" 12 | ;; 13 | win ) 14 | package=sublime_$1_build_$3_x64.zip 15 | file=sublime_$1.exe 16 | ;; 17 | linux) 18 | package=sublime-$1-$3-1-x86_64.pkg.tar.xz 19 | file=opt/sublime_$1/sublime_$1 20 | ;; 21 | linux-arm64) 22 | package=sublime-$1-$3-1-aarch64.pkg.tar.xz 23 | file=opt/sublime_$1/sublime_$1 24 | ;; 25 | * ) echo ERROR: unknown platform: $2; exit 2 ;; 26 | esac 27 | 28 | target=sublime_$1_$2_$3 29 | if [[ -f $target ]]; then 30 | echo ERROR: $target already exists; exit 3 31 | fi 32 | # ST download links: https://www.sublimetext.com/download_thanks 33 | # SM download links: https://www.sublimemerge.com/download_thanks 34 | 35 | case $package in 36 | *.zip) 37 | # mount remote archive to a local path with FUSE (https://github.com/higlass/simple-httpfs) 38 | ps -C simple-httpfs || simple-httpfs --schema=https $HOME/fuse-mounts 39 | package=$HOME/fuse-mounts/download.sublimetext.com/$package.. 40 | # wget --continue https://download.sublimetext.com/$package 41 | # unzip $package "$file" 42 | 7zz e $package "$file" 43 | # 7-zip can extract file without archived file path (and show progress of extraction) 44 | ;; 45 | * ) 46 | wget --continue https://download.sublimetext.com/$package 47 | tar --strip-components=2 -xf $package "$file" 48 | # skipping file path in archive (opt/sublime_$1) 49 | ;; 50 | esac 51 | mv -v ${file##*/} $target 52 | -------------------------------------------------------------------------------- /patches.py: -------------------------------------------------------------------------------- 1 | # tagged_fixes and detections are used by function FindFixes in reghex_patcher_full.py 2 | 3 | import collections 4 | 5 | zero4 = "00 " * 4 6 | # for x64 CPU 7 | nop = "90" 8 | nop5 = "90 " * 5 # nop over E8 .{4} (call [dword]) 9 | ret = "C3" # ret 10 | ret0 = "48 31 C0 C3" # xor rax, rax; ret 11 | ret1 = "48 31 C0 48 FF C0 C3" # xor rax, rax; inc rax; ret 12 | ret119 = "48 C7 C0 19 01 00 00 C3" # mov rax, 0x119; ret 13 | # comparison of detection methods: 14 | # - detection of instructions in code section is the least stable among versions, platforms (arm64, amd64) and OS (Windows, Linux, macOS) 15 | # - detection of constants in code section is more stable among versions and OS, but different among platforms (arm64, amd64) 16 | # - detection of constants in data section is the most stable among versions, and more similar among platforms and OS, but difficult to create an effective patch 17 | # Notes on tuple Fix: 18 | # - before making a regex search with fix.reghex, hex digits pairs are converted to hex-escape format and all spaces are removed 19 | # - fix.name is splitted to set label for offsets of matching groups (from fix.reghex) 20 | # - fix.ref is unused, any matching groups that has 4 bytes will be check if it's a reference to a string/function 21 | # - fix.patch can be a string or a list of strings to patch each matching group 22 | # - fix.arch is used to match the architecture (amd64 or arm64) 23 | # - fix.look_behind is used to find the function that contains the matching groups 24 | # - fix.test is used for testing any fix 25 | Fix = collections.namedtuple('Fix', 'name reghex patch ref arch os look_behind test', defaults=('', '', '', None, None, None, False, False)) # reghex is regex with hex bytes 26 | st_wind_fixes = [ 27 | ] 28 | st_linux_fixes = [ 29 | ] 30 | st_macos_fixes = [ 31 | ] 32 | st_macos_fixes_arm64 = [ 33 | ] 34 | st_linux_fixes_arm64 = [ 35 | ] 36 | sm_wind_fixes = [ 37 | ] 38 | sm_linux_fixes = [ 39 | ] 40 | sm_macos_fixes = [ 41 | ] 42 | sm_macos_fixes_arm64 = [ 43 | ] 44 | sm_linux_fixes_arm64 = [ 45 | ] 46 | st_fixes = [ 47 | ] 48 | sm_fixes = [ 49 | ] 50 | st_blacklist_fixes = [ 51 | ] 52 | string_detections = [ # detect string in data & code sections 53 | ] 54 | startup_fixes = [ 55 | ] 56 | ref_detections = [ # detection for references to found number, string and function 57 | # `look_behind` reghex should only match 1 byte (to avoid taking too many bytes that could belong to the next occurrence) 58 | Fix(name="amd64", reghex=r"(?<= .{4} 48 C7 06 | .{3} C7 44 . . | (?:. C7 [05 85]|C7 84 .) .{4} ) . |" ## move qword [rsi], ?; move [rcx+rdx+?], ?; move [r?p+????], ?; mov [r?+r?+????], ? 59 | + r"(?<= 48 69 C0 ) . |" ## imul rax, rax, 3600 60 | + r"(?<= . 48 81 7D . | 48 81 7C 24 . ) . |" ## cmp qword [rbp+?], ?; cmp qword [rsp+?], ? 61 | + r"(?<= . . 8D [81 87] | . 41 8D 8F | 41 8D 8C 24 | . . 81 [FC FF] ) . |" ## lea eax, [r?+?]; lea ecx, [r15+?]; lea ecx, [r12+?]; cmp edi, ?; cmp r?d, ? 62 | + r"(?<= . 41 BE | 41 81 C6 | . 81 [C1 C5 C7 F8-FF] | 8D 9C 09 ) . |" ## mov e?, ?; mov r14d, ?; add r14d, ?; add e?, ?; cmp e?, ?; lea ebx, [rcx+rcx+?] 63 | + r"(?<= 8A [80-84 86-8C 8E-94 96-97] | . [B8-BB BD-BF 3D] ) . |" ## mov ?l, byte ptr [r? + ?]; mov e?, ?; mov r?, ?; cmp eax, ? 64 | + r"(?<= . . [E8 E9] | 0F B6 [88 B0] | (?:[48 4C] 8D | 0F 10) [05 0D 15 1D 25 2D 35 3D] ) .", ## call ?; jmp ?; lea r?, [rip + ?]; movups xmm0, xmmword ptr [rip + ?] 65 | arch="amd64", look_behind=True), 66 | Fix(name="arm64", reghex=r". (?=. . [10 30 50 70 94 97 14 17]) | (?<=.{4} [90 B0 D0 F0] | [90 B0 D0 F0] .{3} [^91]) . (?=. . 91)" ## adr ? ; bl ? ; b ? ; adrp x?, ? ; add x?, x?, ? 67 | + r"| . (?=. [80-9F] [12 52]) | (?<=.{4} [80-9F] 52 | [80-9F] 52 .{4}) . (?=. [A0-BF] 72)", 68 | arch="arm64", look_behind=True), 69 | ] 70 | st_delay_fixes = [ # extend the delay period 71 | ] 72 | sm_delay_fixes = [ # extend the delay period 73 | ] 74 | st_sm_remote_check_fixes = [ # data section fixes can be applied on all platforms 75 | ] 76 | st_license_check_fixes = [ # data section fixes can be applied on all platforms 77 | ] 78 | sm_license_check_fixes = [ # data section fixes can be applied on all platforms 79 | ] 80 | tagged_fixes = [ 81 | (["SublimeText" , ], startup_fixes + st_fixes), 82 | (["SublimeMerge", ], startup_fixes + sm_fixes), 83 | 84 | (["SublimeText" , "amd64", "windows"], st_wind_fixes ), 85 | (["SublimeText" , "amd64", "osx" ], st_macos_fixes ), 86 | (["SublimeText" , "arm64", "osx" ], st_macos_fixes_arm64), 87 | (["SublimeText" , "arm64", "linux" ], st_linux_fixes_arm64), 88 | (["SublimeText" , "amd64", "linux" ], st_linux_fixes ), 89 | 90 | (["SublimeMerge", "amd64", "windows"], sm_wind_fixes ), 91 | (["SublimeMerge", "amd64", "osx" ], sm_macos_fixes ), 92 | (["SublimeMerge", "arm64", "osx" ], sm_macos_fixes_arm64), 93 | (["SublimeMerge", "arm64", "linux" ], sm_linux_fixes_arm64), 94 | (["SublimeMerge", "amd64", "linux" ], sm_linux_fixes ), 95 | 96 | (["SublimeText" , ], string_detections + ref_detections), 97 | (["SublimeMerge", ], string_detections + ref_detections), 98 | # ([ "SublimeText" , ], st_blacklist_fixes + st_delay_fixes), 99 | # ([ "SublimeMerge", ], sm_blacklist_fixes + sm_delay_fixes), 100 | # ([ "SublimeText" , ], st_sm_remote_check_fixes + st_license_check_fixes), 101 | # ([ "SublimeMerge", ], st_sm_remote_check_fixes + sm_license_check_fixes), 102 | ] 103 | detections = [ 104 | # Fix(name="SublimeText", reghex=r"/updates/4/(?:stable|dev)_update_check\?version=\d+&platform=(\w+)&arch=(\w+)"), # arch: arm64, x64, x32 105 | # Fix(name="SublimeMerge", reghex=r"/updates/(?:stable|dev)_update_check\?version=\d+&platform=(\w+)&arch=(\w+)"), # platform: windows, osx, linux 106 | Fix(name="SublimeText", reghex=r"/updates/4/\w+_update_check\?version=\d+&platform=\w+&arch=\w+"), 107 | Fix(name="SublimeMerge", reghex=r"/updates/\w+_update_check\?version=\d+&platform=\w+&arch=\w+"), 108 | ] 109 | 110 | # sm: /updates/stable_update_check?version=2059&platform=linux&arch=x64 111 | # sm: /updates/dev_update_check?version=2058&platform=linux&arch=arm64 112 | # sm: /updates/dev_update_check?version=2058&platform=windows&arch=x64 113 | # sm: /updates/dev_update_check?version=2058&platform=osx&arch=x64 114 | 115 | # st: /updates/4/stable_update_check?version=4113&platform=osx&arch=arm64 116 | # st: /updates/4/dev_update_check?version=4114&platform=osx&arch=x64 117 | # st: /updates/4/dev_update_check?version=4114&platform=linux&arch=x64 118 | # st: /updates/4/dev_update_check?version=4114&platform=windows&arch=x64 119 | 120 | # /src/sublime_text/release_notes/ 121 | # /src/sublime_merge/release_notes/ 122 | # This will revert Sublime Text to an unregistered state 123 | # This will revert Sublime Merge to an unregistered state 124 | # Sublime Text build %s 125 | # Sublime Merge build %s 126 | # Sublime Text Build %s 127 | # Sublime Merge Build %s 128 | # You can purchase one from https://www.sublimetext.com/buy 129 | # You can purchase a license at https://www.sublimemerge.com 130 | # A new version of Sublime Merge is available, download now? 131 | 132 | # uniform vec2 viewport; 133 | # uniform vec2 position; 134 | # uniform vec2 size; 135 | 136 | # #if defined(ROUNDED) || defined(BORDERED) 137 | # #ifdef SUPERSAMPLE 138 | # // 8x supersample rounded/bordered 139 | # // This is set up for 16x supersample in a grid, with half the samples commented out, giving us 8x 140 | # vec4 accumulator = vec4(0); 141 | -------------------------------------------------------------------------------- /reghex_patcher.py: -------------------------------------------------------------------------------- 1 | credits = "[-] ---- RegHex Patcher by Destitute-Streetdwelling-Guttersnipe (Credits to leogx9r & rainbowpigeon for signatures and patching logic)" 2 | 3 | import re, sys 4 | 5 | def main(): 6 | print(credits) 7 | input_file = sys.argv[1] if len(sys.argv) > 1 else exit(f"Usage: {sys.argv[0]} input_file output_file") 8 | output_file = sys.argv[2] if len(sys.argv) > 2 else input_file 9 | PatchFile(input_file, output_file) 10 | 11 | def PatchFile(input_file, output_file): 12 | with open(input_file, 'rb') as file: 13 | data = bytearray(file.read()) 14 | Patch(data) 15 | with open(output_file, "wb") as file: 16 | file.write(data) 17 | print(f"[+] Patched file saved to {output_file}") 18 | 19 | def FindRegHex(fix, data, showMatchedText = False): 20 | regex = bytes(re.sub(r"\b([0-9a-fA-F]{2})\b", r"\\x\1", fix.reghex), encoding='utf-8') # escape hex bytes 21 | matches = list(re.finditer(regex, data, re.DOTALL | re.VERBOSE))[:10] # only 10 matches 22 | for m in matches: print("[-] Found at {}: pattern {} {}".format(hex(m.start()), fix.name, m.group(0) if showMatchedText else '')) 23 | return matches 24 | 25 | def Patch(data): 26 | for fix in FindFixes(data): 27 | matches = FindRegHex(fix, data) 28 | for match in matches: 29 | offset = match.start() 30 | if fix.is_ref: offset = RelativeOffset(offset, data) 31 | print(f"[+] Patch at {hex(offset)}: {fix.patch}") 32 | patch = bytes.fromhex(fix.patch) 33 | data[offset : offset + len(patch)] = patch 34 | print(f"[!] Can not find pattern: {fix.name} {fix.reghex}\n" if len(matches) == 0 else '') 35 | 36 | def RelativeOffset(offset, data): 37 | relative_address = int.from_bytes(data[offset : offset + 4], byteorder='little') # address size is 4 bytes 38 | return (offset + 4 + relative_address) & 0xFFFFFFFF 39 | 40 | def FindFixes(data): 41 | detected = set() 42 | for fix in Fixes.detections: 43 | for m in FindRegHex(fix, data, True): 44 | detected |= set([ fix.name, *m.groups() ]) 45 | print(f"[+] Detected tags: {detected}\n") 46 | for tags, fixes in Fixes.tagged_fixes: 47 | if set(tags) == detected: return fixes 48 | exit("[!] Can not find fixes for target file") 49 | 50 | # this class can be extracted into a file 51 | class Fixes: 52 | import collections 53 | 54 | # for x64 CPU 55 | nop5 = "90 " * 5 # nop over E8 .{4} (call [dword]) 56 | ret = "C3" # ret 57 | ret0 = "48 31 C0 C3" # xor rax, rax; ret 58 | ret1 = "48 31 C0 48 FF C0 C3" # xor rax, rax; inc rax; ret 59 | ret119 = "48 C7 C0 19 01 00 00 C3" # mov rax, 0x119; ret 60 | # for ARM64 CPU 61 | _ret = "C0 03 5F D6" # ret 62 | _ret0 = "E0 03 1F AA C0 03 5F D6" # mov x0, xzr; ret 63 | _nop = "1F 20 03 D5" # nop 64 | 65 | Fix = collections.namedtuple('Fix', 'name reghex patch is_ref', defaults=('', '', nop5, False)) # reghex is regex with hex bytes 66 | 67 | st_wind_fixes = [ 68 | Fix(name="license_check", reghex="(?<= E8 ) . . . . . . . . . . . . .", patch=ret0, is_ref=True), 69 | Fix(name="blacklist_check", reghex="(?<= . . . . . . ) E8 . . . . (48|49) . ."), # 48 for dev, 49 for stable 70 | ] 71 | st_linux_fixes = [ 72 | Fix(name="license_check", reghex="(?<= E8 ) . . . . . . . . . . . . .", patch=ret0, is_ref=True), 73 | Fix(name="blacklist_check", reghex="E8 . . . . . . . . . . . ."), 74 | ] 75 | st_macos_fixes = [ 76 | Fix(name="license_check", reghex="(?<= E8 ) . . . . . . . . . . . . .", patch=ret0, is_ref=True), 77 | Fix(name="blacklist_check", reghex="E8 . . . . . . . . . . . . . ."), 78 | ] 79 | st_macos_fixes_arm64 = [ 80 | Fix(name="license_check", reghex=". . . . . . . .", patch=_ret0), 81 | Fix(name="blacklist_check", reghex=". . . . . . . . . . . . . . . . . . . .", patch=_nop), 82 | ] 83 | sm_wind_fixes = [ 84 | Fix(name="server_validate", reghex="55 . . . . . . . . . . . . . . . . . . . . . . . . . .", patch=ret1), 85 | Fix(name="blacklist_check", reghex="(?<= . . . . . . ) E8 . . . . . . . . . ."), 86 | ] 87 | sm_linux_fixes = [ 88 | Fix(name="server_validate", reghex="55 . . . . . . . . . . .", patch=ret1), 89 | Fix(name="blacklist_check", reghex="E8 . . . . . . . . . . . ."), 90 | ] 91 | sm_macos_fixes = [ 92 | Fix(name="server_validate", reghex="55 . . . . . . . . . . . . . . . . .", patch=ret1), 93 | Fix(name="blacklist_check", reghex="E8 . . . . . . . . . . . . . ."), 94 | ] 95 | sm_macos_fixes_arm64 = [ 96 | Fix(name="license_check", reghex=". . . . . . . .", patch=_ret0), 97 | Fix(name="blacklist_check", reghex=". . . . . . . . . . . . . . . . . . . .", patch=_nop), 98 | ] 99 | st_blacklist_fixes = [ 100 | Fix(name="blacklisted_license_0D9497", reghex="97 94 0D 00", patch="00 00 00 00"), # EA7E-890007 TwitterInc 101 | Fix(name="license_server", reghex="license\.sublimehq\.com", patch=b"license.localhost.\x00\x00\x00".hex(' ')) 102 | ] 103 | tagged_fixes = [ 104 | ([b"x64", "SublimeText" , b"windows"], st_wind_fixes ), 105 | ([b"x64", "SublimeText" , b"arm64", b"osx" ], st_macos_fixes + st_macos_fixes_arm64), 106 | ([b"x64", "SublimeText" , b"linux" ], st_linux_fixes), 107 | ([b"x64", "SublimeMerge", b"windows"], sm_wind_fixes ), 108 | ([b"x64", "SublimeMerge", b"arm64", b"osx" ], sm_macos_fixes + sm_macos_fixes_arm64), 109 | ([b"x64", "SublimeMerge", b"linux" ], sm_linux_fixes), 110 | ([ "SublimeText" , ], st_blacklist_fixes ), 111 | ([ "SublimeMerge", ], st_blacklist_fixes ), 112 | ] 113 | detections = [ 114 | Fix(name="SublimeText", reghex=r"/updates/4/\w+_update_check\?version=\d+&platform=(\w+)&arch=(\w+)"), # arch: x64, arm64 115 | Fix(name="SublimeMerge", reghex=r"/updates/\w+_update_check\?version=\d+&platform=(\w+)&arch=(\w+)"), # platform: windows, osx, linux 116 | # Fix(name="SublimeText", reghex=r"/updates/4/\w+_update_check\?version=\d+&platform=\w+&arch=\w+"), 117 | # Fix(name="SublimeMerge", reghex=r"/updates/\w+_update_check\?version=\d+&platform=\w+&arch=\w+"), 118 | ] 119 | 120 | if __name__ == "__main__": 121 | main() 122 | -------------------------------------------------------------------------------- /reghex_patcher_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | credits = "RegHex Patcher by Destitute-Streetdwelling-Guttersnipe (Thanks to leogx9r & rainbowpigeon for inspiration)" 3 | import re, sys, struct, io, patches as Fixes 4 | 5 | def main(argv): 6 | if len(argv) <= 1: exit(f"[-] ---- {credits}\nUsage: {argv[0]} [input_file [output_file]]") 7 | if (onlyTest := argv[1] == '-t'): argv.remove('-t') # only use fixes with test=True (if option '-t' exists) 8 | 9 | input_file = argv[1] if argv[1] != '-' else sys.stdin.fileno() # read from stdin if input_file is a hyphen 10 | with open(input_file, 'rb') as file: UnpackAndPatch(data := bytearray(file.read()), onlyTest) 11 | 12 | output_file = argv[2] if len(argv) > 2 else exit() # discard patched data if output_file is omitted 13 | with open(output_file, "wb") as file: file.write(data) and print(f"[+] Saved to {output_file}") 14 | 15 | def FindRegHex(reghex, data): 16 | regex = bytes(re.sub(r"\b([0-9a-fA-F]{2})\b", r"\\x\1", reghex), 'utf-8') # escape hex bytes 17 | return re.finditer(regex.replace(b' ', b''), data, re.DOTALL) # remove all spaces 18 | 19 | def FindRegHexOnce(reghex, data): return next(FindRegHex(reghex, data), None) 20 | 21 | def PatchByteSlice(patched, offset = 0, end = None, onlyTest = False): 22 | refs, file = {}, FileInfo(patched[offset : end], offset) # reset refs for each file 23 | for fix in FindFixes(file): ApplyFix(fix, patched, file, refs) if onlyTest in [False, fix.test] else None # only use fixes with test=True (if option '-t' exists) 24 | 25 | def ApplyFix(fix, patched, file, refs, match = None, fn = None, ref0 = None): 26 | for match in FindRegHex(fix.reghex, file.data): 27 | for i in range(1, len(match.groups()) + 1) or range(1): # loop through all matched groups 28 | p0 = p = Position(file, offset=match.start(i)) # offset is -1 when a group is not found 29 | if p0.address and (fix.look_behind or (i > 0 and len(match.group(i)) == 4)): # find referenced address from any 4-byte group 30 | p = Position(file, address=Ref2Address(p0.address, p0.offset, file)) 31 | if p0.address and not fix.look_behind: 32 | h = ''.join(fix.patch[i-1:i]) if isinstance(fix.patch, list) else fix.patch # non-existent element in array fix.patch is considered to be an empty string 33 | if not refs.get(p.address) and h == '\r': break # skip a match if referenced address is not found earlier and its patch is '\r' 34 | ref0 = refs[p0.address] = fix.name if i == 0 else '.'.join(fix.name.split('.')[0:i+1:i]) # extract part 0 and part i from fix.name if i > 0 35 | if not refs.get(p.address): refs[p.address] = '.'+fix.name.split('.')[i] # extract part i from fix.name 36 | if p.address == p0.address: ref0 += f" : {match.group(i).hex(' ')}" # show matched bytes unless referenced address is found from match bytes 37 | if fix.patch != '': PatchAtOffset(p.offset, file, patched, h, p.ref_info(p0, ref0)) 38 | else: fn = FindNearestFunction(file, refs, p0, p, fn) 39 | if fix.patch != '' and (not match or not ref0): print(f"[!] Cannot find pattern: {fix.name} {fix.reghex}") 40 | 41 | def FindNearestFunction(file, refs, p0, p, fn): 42 | if (ref0 := refs.get(p0.address)) or (ref := refs.get(p.address)): # look behind if p0 or p is in refs 43 | diff = fn != (fn := LastFunction(file, fn or Position(file, offset=0), p0)) # find function containing this match 44 | print("[-] Found fn " + ['-' * len(fn.info), fn.info][diff] + f" <- {p.ref_info(p0, ref0 or '-'+ref)}") # show fn.info when a new function is found 45 | return fn 46 | 47 | def PatchAtOffset(offset, file, patched, h, ref_info): 48 | print(f"[+] Patch at {ref_info} => {h}" if len(h)>=2 else f"[-] Found at {ref_info}") 49 | if (b := bytes.fromhex(h)) and offset: patched[(o := offset + file.base_offset) : o + len(b)] = b # has no effect if h is empty or h contains spaces 50 | 51 | AMD64, ARM64 = 'amd64', 'arm64' # arch x86-64, arch AArch64 52 | 53 | def LastFunction(file, start, end, last = None): # file: FileInfo, start: Position, end: Position 54 | function_reghex = { # reghex for function epilogue and function prologue 55 | AMD64: r"(?:(?:C3|EB .|[E8 E9] .{4}) [66]*(?:90|CC|0F 0B|0F 1F [00 40 44 80 84] [00]*)* | 00{8})" ## (ret | jmp ? | call ?) (nop | int3 | ud2) 56 | + r"( (48 89 54 24 .)? ( [53 55-57] | 41 [54-57] | 48 8B EC | 48 89 E5 | 48 [81 83] EC )+ )", ## mov qword[rsp+?], rdx; (push r? | mov rbp, rsp | sub rsp, ?) 57 | ARM64: r"(?:(?:C0 03 5F D6 | .{3} [14 17 94 97]) (?:1F 20 03 D5)* | 00{8}) ((. 03 1E AA .{3} [94 97] FE 03 . AA)?" ## mov x?, x30 ; bl ? ; mov x30, x? 58 | + r"( FF . . D1 | [F4 F6 F8 FA FC FD] . . [A9 F9] | [E9 EB] . . 6D | FD . . 91 )+)", ## sub sp, sp, ? ; stp x?, x?, [sp + ?] ; add x29, sp, ? 59 | }[file.arch] # die on unknown arch 60 | for m in FindRegHex(function_reghex, file.data[start.offset:end.offset]): last = m 61 | return Position(file, offset=start.offset + last.start(1)) if last else start 62 | 63 | class Position: 64 | def __init__(self, file, address = None, offset = None): 65 | a = self.address = address if address != None else ConvertOffsetToAddress(file.sections, offset) 66 | o = self.offset = offset if offset != None else ConvertAddressToOffset(file.sections, address) 67 | self.info = f"a:{a or 0:6x} " + (f"o:{o + file.base_offset:6x}" if o else '') 68 | def ref_info(self, p0, ref): 69 | return f"{p0.info} -> {self.info if self.address != p0.address else '':{len(p0.info)}} {ref}" # keep length unchanged for output alignment 70 | 71 | def Ref2Address(base, offset, file): 72 | byte_array = file.data[offset-8 : offset+4] 73 | if file.arch == ARM64 and base % 4 == 0: # PC relative instructions of arm64 74 | (instr2,) = struct.unpack("> msb else number 76 | bits2number = lambda bits, skips, count: (bits >> skips) & ((1 << count) - 1) # skip some LSB and extract some bits 77 | if m := FindRegHexOnce(r"(.{3} [90 B0 D0 F0]) (.{3} [^91])? .{3} 91$", byte_array): # ADRP & ADD instructions 78 | (instr,) = struct.unpack("> 12 << 12 # clear 12 LSB 83 | return page_address + extend_sign(page_offset << 12, 32) + imm12 84 | elif m := FindRegHexOnce(r"[80-9F] (?:(12)|52)$", byte_array): # MOVN/MOV instruction 85 | value = bits2number(instr2, 5, 16) # discard 11 MSB, discard 5 LSB 86 | return ~value if m.group(1) else value # invert bits in case of MOVN 87 | elif m := FindRegHexOnce(r"(. . [80-9F] 52) (.{3} [^72])? . . [A0-BF] 72$", byte_array): # MOV & MOVK instruction 88 | (instr,) = struct.unpack("2L", data[:(start := 4*2)]) if len(data) >= 4*2 else (0, 0) 123 | if magic == 0xCAFEBABE: # FAT_MAGIC of MacOS universal binary 124 | while (num_archs := num_archs - 1) >= 0: 125 | (cpu_type, _, offset, size, _) = struct.unpack(">5L", data[start : (start := start + 4*5)]) 126 | PatchByteSlice(data, offset, offset + size, onlyTest) # if cpu_type == 0x100000c else None 127 | else: PatchByteSlice(data, 0, None, onlyTest) 128 | 129 | def FileInfo(data = b'', base_offset = 0): # FileInfo is a singleton object 130 | if re.search(b"^MZ", data): 131 | import pefile # pip3 install pefile 132 | FileInfo.os, pe = 'windows', pefile.PE(data=data, fast_load=True) 133 | FileInfo.arch = { 0x8664: AMD64, 0xAA64: ARM64 }[pe.FILE_HEADER.Machine] # die on unknown arch 134 | FileInfo.sections = [(pe.OPTIONAL_HEADER.ImageBase + s.VirtualAddress, s.PointerToRawData, s.SizeOfRawData) for s in pe.sections] 135 | elif re.search(b"^\x7FELF", data): 136 | from elftools.elf.elffile import ELFFile # pip3 install pyelftools 137 | FileInfo.os, elf = 'linux', ELFFile(io.BytesIO(data)) 138 | FileInfo.arch = { 'EM_X86_64': AMD64, 'EM_AARCH64': ARM64 }[elf.header['e_machine']] # die on unknown arch 139 | FileInfo.sections = [(s.header['sh_addr'], s.header['sh_offset'], s.header['sh_size']) for s in elf.iter_sections()] 140 | elif re.search(b"^\xCF\xFA\xED\xFE", data): 141 | from macho_parser.macho_parser import MachO # pip3 install git+https://github.com/Destitute-Streetdwelling-Guttersnipe/macho_parser.git 142 | FileInfo.os, macho = 'osx', MachO(mm=data) # macho_parser was patched to use bytearray (instead of reading from file) 143 | FileInfo.arch = { 0x1000007: AMD64, 0x100000c: ARM64 }[macho.get_header().cputype] # die on unknown arch 144 | FileInfo.sections = [(s.addr, s.offset, s.size) for s in macho.get_sections()] 145 | # with open(sys.argv[1] + "_" + FileInfo.arch, "wb") as f: f.write(data) # store detected file 146 | else: exit("[!] ---- Cannot detect file type: " + data[:8].hex(' ')) 147 | print(f"\n[+] ---- at o:{base_offset:x} Executable for {FileInfo.os} {FileInfo.arch}") 148 | FileInfo.data, FileInfo.base_offset = data, base_offset 149 | return FileInfo 150 | 151 | def ConvertAddressToOffset(sections, position): return ConvertOffsetToAddress(sections, position, src=0, dst=1) 152 | def ConvertOffsetToAddress(sections, position, src=1, dst=0, size=2): # 0 is index of address, 1 is index of offset 153 | for s in sections: 154 | if position and s[src] and 0 <= position - s[src] < s[size]: return position - s[src] + s[dst] 155 | 156 | if __name__ == "__main__": main(sys.argv) 157 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pefile 2 | pyelftools 3 | macho_parser @ git+https://github.com/Destitute-Streetdwelling-Guttersnipe/macho_parser.git 4 | --------------------------------------------------------------------------------