├── .gitignore ├── LICENSE ├── QuickPatch.py ├── README.md ├── core.py ├── gdbQuickPatch.py ├── requirements.txt └── tests ├── patch_me.c └── patch_me_pie /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrea Sindoni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QuickPatch.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from argparse import RawTextHelpFormatter 3 | from core import Core 4 | from core import FilesOp 5 | 6 | import sys 7 | import os 8 | 9 | 10 | def main(): 11 | parser = ArgumentParser(formatter_class=RawTextHelpFormatter, description="""QuickPatch - a simple patching tool""", epilog="""examples: 12 | python3 QuickPatch.py -A x86-64 -a 'mov rax,0x10; add rax, 0x69' -o 0x3b2 -b binary -of out_binary 13 | python3 QuickPatch.py -A x86-64 -o 0x3b2 -b binary 14 | python3 QuickPatch.py -A x86-64 -a 'mov rax,0x10; add rax, 0x69' 15 | python3 QuickPatch.py -A x86-64 -d '0x55,0x48,0x8b,0x05,0xb8,0x13,0x00,0x00' 16 | python3 QuickPatch.py -A x86-64 -o 0x40b -b binary -p "0x90,0x90" -of out_binary 17 | """) 18 | 19 | parser.add_argument('-b', '--binary', dest='binary', help='binary to disassemble/patch') 20 | parser.add_argument('-A', '--arch', dest='architecture', required=True, help='supported architecture values [x86-32, x86-64, arm, arm64]') 21 | parser.add_argument('-a', '--assembly', dest='assembly', help='assemble/patch the instructions') 22 | parser.add_argument('-d', '--disassembly', dest='disassembly', help='disassemble the bytes') 23 | parser.add_argument('-p', '--patch_bytes', dest='patch_bytes', help='patch the bytes') 24 | parser.add_argument('-dl', '--disas_len', dest='disas_len', help='number of the instructions to disassemble, default value is 16') 25 | parser.add_argument('-o', '--offset', dest='offset', help='position in the binary of the bytes to disassemble/patch') 26 | parser.add_argument('-of', '--out_file', dest='out_file', help='output file, by defaut a file with a random name will be created') 27 | parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0 - 20/10/2019', help='display the version') 28 | 29 | options = parser.parse_args() 30 | 31 | binary = options.binary 32 | arch = options.architecture 33 | disassembly = options.disassembly 34 | disassembly_len = options.disas_len 35 | assembly = options.assembly 36 | patch_bytes = options.patch_bytes 37 | offset = options.offset 38 | out_file = options.out_file 39 | 40 | try: 41 | core_obj = Core() 42 | 43 | if core_obj.arch(arch): 44 | print("Architecture not supported! Supported architectures are: [x86-32, x86-64, arm, arm64]") 45 | return 46 | 47 | if not disassembly_len: 48 | disassembly_len = 0x10 49 | else: 50 | disassembly_len = int(disassembly_len, 16) 51 | 52 | if binary and offset: 53 | offset = int(offset, 16) 54 | file_obj = FilesOp(binary) 55 | file_obj.check_file() 56 | file_obj.read_file() 57 | if assembly or patch_bytes: #patch binary 58 | if not out_file: 59 | out_file = file_obj.generate_random() 60 | if patch_bytes: 61 | patch = core_obj.get_bytes(patch_bytes) 62 | file_obj.patch_file(out_file, offset, patch) 63 | print("[*] Bytes: {} (len: {})".format(patch_bytes, len(patch_bytes))) 64 | else: 65 | patch = core_obj.assemble(assembly, True) 66 | patch = bytearray(patch) 67 | file_obj.patch_file(out_file, offset, patch) 68 | else: #disassembly binary 69 | data = file_obj.disas_from_offset(offset) 70 | core_obj.disassemble(data, disassembly_len, offset, True) 71 | elif assembly: 72 | core_obj.assemble(assembly, True) 73 | elif disassembly: 74 | core_obj.disassemble(disassembly, disassembly_len, 0, False) 75 | else: 76 | print("In order to patch or disassembly a binary file, the options --binary --offset and --assembly are required") 77 | except Exception as e: 78 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 79 | sys.exit() 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickPatch 2 | 3 | ## Tool description 4 | [QuickPatch](https://github.com/invictus1306/QuickPatch) is mainly a GDB plug-in giving users the ability to patch an ELF file quickly, just by write the instructions to patch. 5 | 6 | With *QuickPatch* is also possible to patch/disassemble a binary file for the architectures x86-32 x86-64 arm and arm64. 7 | 8 | It is based on [Capstone](https://www.capstone-engine.org/) and [Keystone](http://www.keystone-engine.org/). 9 | 10 | ## Features 11 | These are the features implemented so far: 12 | * `program_patch` -- patch a program --> *GDB command* 13 | * `memory_patch` -- patch a program in memory (not persistent) --> *GDB command* 14 | * `get_bytes` -- get opcode of instructions --> *GDB command* 15 | * Disassemble from file 16 | * Patch a file 17 | * Disassemble from user inputs 18 | * Assemble from user inputs 19 | 20 | ## Requirements 21 | * [Capstone](https://www.capstone-engine.org/) 22 | * [Keystone](http://www.keystone-engine.org/) 23 | * GDB 7.x+ (python 2.7.x) 24 | * Python 3.x 25 | 26 | ## Usage 27 | It is possible to use the program: 28 | * From GDB 29 | * From command line 30 | 31 | ### From GDB 32 | ``` 33 | $ wget https://github.com/invictus1306/QuickPatch/raw/master/gdbQuickPatch.py -O ~/gdbQuickPatch.py 34 | $ echo "source ~/gdbQuickPatch.py" >> ~/.gdbinit 35 | ``` 36 | 37 | List of available commands: 38 | * program_patch 39 | * memory_patch 40 | * get_bytes 41 | 42 | Get usage for a specific command: 43 | ``` 44 | gdb> help program_patch 45 | Patch instructions -> program_patch
e.g. 46 | gdb> program_patch "xor rax,rax;nop" 0x400123 out_bin 47 | gdb> program_patch "0x90,0x90" 0x400123 out_bin 48 | gdb> help memory_patch 49 | Memory patch instruction -> memory_patch
e.g. 50 | gdb> memory_patch "xor rax,rax;nop" 0x400123 51 | gdb> memory_patch "0x90,0x90" 0x400123 52 | gdb> help get_bytes 53 | Get the instruction opcodes -> get_bytes e.g. 54 | gdb> get_bytes "xor rax,rax;nop" 55 | ``` 56 | 57 | ### From command line 58 | ``` 59 | $ git clone https://github.com/invictus1306/QuickPatch.git 60 | $ cd QuickPatch 61 | $ python3 QuickPatch.py --help 62 | usage: QuickPatch.py [-h] [-b BINARY] -A ARCHITECTURE [-a ASSEMBLY] 63 | [-d DISASSEMBLY] [-p PATCH_BYTES] [-dl DISAS_LEN] 64 | [-o OFFSET] [-of OUT_FILE] [-v] 65 | 66 | QuickPatch - a simple patching tool 67 | 68 | optional arguments: 69 | -h, --help show this help message and exit 70 | -b BINARY, --binary BINARY 71 | binary to disassemble/patch 72 | -A ARCHITECTURE, --arch ARCHITECTURE 73 | supported architecture values [x86-32, x86-64, arm, arm64] 74 | -a ASSEMBLY, --assembly ASSEMBLY 75 | assemble/patch the instructions 76 | -d DISASSEMBLY, --disassembly DISASSEMBLY 77 | disassemble the bytes 78 | -p PATCH_BYTES, --patch_bytes PATCH_BYTES 79 | patch the bytes 80 | -dl DISAS_LEN, --disas_len DISAS_LEN 81 | number of the instructions to disassemble, default value is 16 82 | -o OFFSET, --offset OFFSET 83 | position in the binary of the bytes to disassemble/patch 84 | -of OUT_FILE, --out_file OUT_FILE 85 | output file, by defaut a file with a random name will be created 86 | -v, --version display the version 87 | 88 | examples: 89 | python3 QuickPatch.py -A x86-64 -a 'mov rax,0x10; add rax, 0x69' -o 0x3b2 -b binary -of out_binary 90 | python3 QuickPatch.py -A x86-64 -o 0x3b2 -b binary 91 | python3 QuickPatch.py -A x86-64 -a 'mov rax,0x10; add rax, 0x69' 92 | python3 QuickPatch.py -A x86-64 -d '0x55,0x48,0x8b,0x05,0xb8,0x13,0x00,0x00' 93 | python3 QuickPatch.py -A x86-64 -o 0x40b -b binary -p "0x90,0x90" -of out_binary 94 | 95 | ``` 96 | 97 | ### GDB: patch a program 98 | With the `program_patch` GDB command we can patch a program in a persistent way. 99 | 100 | We have to specify: 101 | * instructions or bytes to patch 102 | * the address of in the binary at run-time 103 | * the name of the output file 104 | 105 | ``` 106 | gdb> program_patch
107 | ``` 108 | For example (instructions to patch) 109 | ``` 110 | gdb> program_patch "xor rax,rax;xor rbx,rbx" 0x400123 out_bin 111 | ``` 112 | For example (bytes to patch) 113 | ``` 114 | gdb> program_patch "0x48,0x31,0xc0,0x48,0x31,0xdb" 0x400123 out_bin 115 | ``` 116 | 117 | ### GDB: patch a program in memory (not persistent) 118 | With the `memory_patch` GDB command we can patch a program not in a persistent way, but only in memory. 119 | 120 | We have to specify: 121 | * instructions or bytes to patch 122 | * the address of in the binary at run-time 123 | 124 | For example (instructions to patch) 125 | ``` 126 | gdb> program_patch "xor rax,rax;xor rbx,rbx" 0x400123 127 | ``` 128 | For example (bytes to patch) 129 | ``` 130 | gdb> program_patch "0x48,0x31,0xc0,0x48,0x31,0xdb" 0x400123 131 | ``` 132 | 133 | ### GDB: get opcode of instructions 134 | With the `get_bytes` GDB command we can get the opcodes of instructions. 135 | 136 | We have to specify: 137 | * instructions 138 | 139 | ``` 140 | gdb> get_bytes 141 | 142 | gdb> get_bytes "xor rax,rax;xor rbx,rbx" 143 | 144 | ``` 145 | 146 | ### Disassemble from file 147 | ``` 148 | $ python3 QuickPatch.py -A x86-64 -o 0x98f -b ./tests/patch_me_pie -dl 5 149 | 0x98f: 85c0 test eax, eax 150 | 0x991: 7513 jne 0x9a6 151 | 0x993: 488d3dce000000 lea rdi, [rip + 0xce] 152 | 0x99a: e8e1fdffff call 0x780 153 | 0x99f: b800000000 mov eax, 0 154 | ``` 155 | 156 | ### Patch a file 157 | #### patch with instructions 158 | ``` 159 | $ python3 QuickPatch.py -A x86-64 -a 'nop;nop;nop;nop' -o 0x98f -b ./tests/patch_me_pie -of patched_1 160 | [*] Instructions: ['nop', 'nop', 'nop', 'nop'] (len: 4) 161 | [*] Encoding: 0x90 0x90 0x90 0x90 (len: 4) 162 | [*] File patched_1 with patch is created 163 | ``` 164 | #### patch with bytes 165 | 166 | ``` 167 | python3 QuickPatch.py -A x86-64 -p '0x90,0x90,0x90,0x90' -o 0x98f -b ./tests/patch_me_pie -of patched 168 | [*] File patched with patch is created 169 | [*] Bytes: 0x90,0x90,0x90,0x90 (len: 19) 170 | ``` 171 | 172 | ### Disassemble from user inputs 173 | ``` 174 | $ python3 QuickPatch.py -A x86-64 -d '0x55,0x48,0x8b,0x05,0xb8,0x13,0x00,0x00' 175 | 0x0: 55 push rbp 176 | 0x1: 488b05b8130000 mov rax, qword ptr [rip + 0x13b8] 177 | ``` 178 | 179 | ### Assemble from user inputs 180 | ``` 181 | $ python3 QuickPatch.py -A x86-32 -a 'mov eax,0x10; add eax, 0x69' 182 | [*] Instructions: ['mov eax,0x10', ' add eax, 0x69'] (len: 2) 183 | [*] Encoding: 0xb8 0x10 0x0 0x0 0x0 0x83 0xc0 0x69 (len: 8) 184 | ``` 185 | 186 | ## Article 187 | A small article that describe how to use it: 188 | * QuickPatch [article](https://invictus1306.github.io/vulnerabilities/2019/10/20/quickpatch.html) 189 | 190 | -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | from capstone import * 2 | from keystone import * 3 | 4 | import sys 5 | import os 6 | import stat 7 | import binascii 8 | import datetime 9 | 10 | 11 | class Arch: 12 | def __init__(self): 13 | self.cs = "" 14 | self.ks = "" 15 | 16 | def arch(self, arch_type): 17 | if arch_type == "x86-32": 18 | self.cs = Cs(CS_ARCH_X86, CS_MODE_32) 19 | self.ks = Ks(KS_ARCH_X86, KS_MODE_32) 20 | elif arch_type == "x86-64": 21 | self.cs = Cs(CS_ARCH_X86, CS_MODE_64) 22 | self.ks = Ks(KS_ARCH_X86, KS_MODE_64) 23 | elif arch_type == "arm": 24 | self.cs = Cs(CS_ARCH_ARM, CS_MODE_ARM) 25 | self.ks = Ks(KS_ARCH_ARM, KS_MODE_ARM) 26 | elif arch_type == "arm64": 27 | self.cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM) 28 | self.ks = Ks(KS_ARCH_ARM64, KS_MODE_ARM) 29 | else: 30 | return 1 31 | 32 | return 0 33 | 34 | 35 | class FilesOp: 36 | def __init__(self, binary): 37 | self.binary = binary 38 | self.fd = "" 39 | self.data = "" 40 | 41 | 42 | def read_file(self): 43 | try: 44 | self.fd = open(self.binary, 'rb') 45 | self.data = self.fd.read() 46 | self.size = self.get_file_size(self.binary) 47 | except Exception as e: 48 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 49 | sys.exit() 50 | 51 | def disas_from_offset(self, offset): 52 | try: 53 | with open(self.binary, 'rb') as fd: 54 | self.fd.seek(offset, 0) 55 | data_from_offset = self.fd.read() 56 | self.fd.close() 57 | return data_from_offset 58 | except Exception as e: 59 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 60 | sys.exit() 61 | 62 | def patch_file(self, filename, offset, patch): 63 | try: 64 | with open(filename, 'wb') as fd: 65 | fd.write(self.data) 66 | fd.seek(offset) 67 | fd.write(patch) 68 | fd.close() 69 | self.fd.close() 70 | new_size = self.get_file_size(filename) 71 | if new_size != self.size: 72 | print("[*] The size of the new file {} is different than the one of original file {}".format(new_size, self.size)) 73 | statinfo = os.stat(filename) 74 | os.chmod(filename, statinfo.st_mode | stat.S_IEXEC) 75 | except Exception as e: 76 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 77 | sys.exit() 78 | 79 | print("[*] File {} with patch is created".format(filename)) 80 | 81 | def get_file_size(self, binary): 82 | try: 83 | statinfo = os.stat(binary) 84 | return statinfo.st_size 85 | except Exception as e: 86 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 87 | sys.exit() 88 | 89 | def check_file(self): 90 | try: 91 | if not os.path.exists(self.binary): 92 | print("file not found {}".format(self.binary)) 93 | sys.exit() 94 | except Exception as e: 95 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 96 | sys.exit() 97 | 98 | def generate_random(self): 99 | try: 100 | unique_filename = "out" + str(datetime.datetime.now().date()) + '_' + str(datetime.datetime.now().time()).replace(':', '.') 101 | out_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), unique_filename) 102 | return out_file 103 | except Exception as e: 104 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 105 | sys.exit() 106 | 107 | 108 | class Core(Arch): 109 | def __init__(self): 110 | Arch.__init__(self) 111 | 112 | def disassemble(self, code, disassembly_len, offset, mode=False): 113 | try: 114 | if mode == False: 115 | code = self.get_bytes(code) 116 | for i in self.cs.disasm(code, offset, disassembly_len): 117 | print("{0:}: {1:16} {2:5} {3:16}".format(hex(i.address), ''.join(format(x, '02x') for x in i.bytes), i.mnemonic, i.op_str)) 118 | except (CsError, ValueError, Exception) as e: 119 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 120 | 121 | def assemble(self, code, mode=False): 122 | try: 123 | encoding, count = self.ks.asm(code) 124 | if count > 0: 125 | if mode: 126 | print("[*] Instructions: {} (len: {})\n[*] Encoding: {} (len: {})".format(code.split(";"), count, ' '.join(hex(x) for x in encoding), len(encoding))) 127 | else: 128 | print("[*] %s = %s (number of statements: %u)" %(code, encoding, count)) 129 | return encoding 130 | except (KsError, ValueError, Exception) as e: 131 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 132 | 133 | def get_bytes(self, code): 134 | try: 135 | code_bytes = b'' 136 | code = code.replace(" ", "").replace("0x", "") 137 | code = code.split(",") 138 | for i in code: 139 | code_bytes += binascii.unhexlify(i) 140 | return code_bytes 141 | except Exception as e: 142 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 143 | sys.exit() 144 | -------------------------------------------------------------------------------- /gdbQuickPatch.py: -------------------------------------------------------------------------------- 1 | from capstone import * 2 | from keystone import * 3 | 4 | import sys 5 | import os 6 | import stat 7 | import gdb 8 | import binascii 9 | 10 | 11 | archs_32_gdb = ["i386:x64-32", "i386:x64-32:nacl", "i386:x64-32:intel"] 12 | archs_64_gdb = ["i386:x86-64:intel", "i386:x86-64", "i386:x86-64:nacl"] 13 | 14 | 15 | class Arch(object): 16 | def __init__(self): 17 | self.cs = "" 18 | self.ks = "" 19 | 20 | def arch(self, arch_type): 21 | if arch_type == "x86-32": 22 | self.cs = Cs(CS_ARCH_X86, CS_MODE_32) 23 | self.ks = Ks(KS_ARCH_X86, KS_MODE_32) 24 | elif arch_type == "x86-64": 25 | self.cs = Cs(CS_ARCH_X86, CS_MODE_64) 26 | self.ks = Ks(KS_ARCH_X86, KS_MODE_64) 27 | else: 28 | return 1 29 | 30 | return 0 31 | 32 | 33 | class FilesOp: 34 | def __init__(self, binary): 35 | self.binary = binary 36 | self.fd = "" 37 | self.data = "" 38 | 39 | def read_file(self): 40 | try: 41 | self.fd = open(self.binary, 'rb') 42 | self.data = self.fd.read() 43 | self.size = self.get_file_size(self.binary) 44 | except Exception as e: 45 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 46 | sys.exit() 47 | 48 | def disas_from_offset(self, offset): 49 | try: 50 | with open(self.binary, 'rb') as fd: 51 | self.fd.seek(offset, 0) 52 | data_from_offset = self.fd.read() 53 | self.fd.close() 54 | return data_from_offset 55 | except Exception as e: 56 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 57 | sys.exit() 58 | 59 | def patch_file(self, filename, offset, patch): 60 | try: 61 | with open(filename, 'wb') as fd: 62 | fd.write(self.data) 63 | fd.seek(offset) 64 | fd.write(patch) 65 | fd.close() 66 | self.fd.close() 67 | 68 | new_size = self.get_file_size(filename) 69 | if new_size != self.size: 70 | print("The size of the new file {} is different than the one of original file {}".format(new_size, self.size)) 71 | statinfo = os.stat(filename) 72 | os.chmod(filename, statinfo.st_mode | stat.S_IEXEC) 73 | except Exception as e: 74 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 75 | sys.exit() 76 | 77 | print("[*] File {} with patch is created".format(filename)) 78 | 79 | def check_file(self): 80 | try: 81 | if not os.path.exists(self.binary): 82 | print("file not found {}".format(self.binary)) 83 | sys.exit() 84 | except Exception as e: 85 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 86 | sys.exit() 87 | 88 | def get_file_size(self, binary): 89 | try: 90 | statinfo = os.stat(binary) 91 | return statinfo.st_size 92 | except Exception as e: 93 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 94 | sys.exit() 95 | 96 | class Core(Arch): 97 | def __init__(self): 98 | super(Arch, self).__init__() 99 | 100 | def disassemble(self, code, disassembly_len, mode=False): 101 | ''' currently no command uses the disassemble method ''' 102 | try: 103 | if mode == False: 104 | code = self.get_bytes(code) 105 | for i in self.cs.disasm(code, 0x00, disassembly_len): 106 | print("{0:}: {1:16} {2:5} {3:16}".format(i.address, ''.join(format(x, '02x') for x in i.bytes), i.mnemonic, i.op_str)) 107 | except (CsError, ValueError, Exception) as e: 108 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 109 | sys.exit() 110 | 111 | def assemble(self, code, mode=False): 112 | try: 113 | encoding, count = self.ks.asm(code) 114 | if count > 0: 115 | if mode: 116 | print("[*] Instructions: {} (len: {})\n[*] Encoding: {} (len: {})".format(code.split(";"), count, ' '.join(hex(x) for x in encoding), len(encoding))) 117 | else: 118 | print("[*] %s = %s (number of statements: %u)" %(code, encoding, count)) 119 | return encoding 120 | except (KsError, ValueError, Exception) as e: 121 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 122 | sys.exit() 123 | 124 | def get_bytes(self, code): 125 | try: 126 | code_bytes = b'' 127 | code = code.replace(" ", "").replace("0x", "") 128 | code = code.split(",") 129 | for i in code: 130 | code_bytes += binascii.unhexlify(i) 131 | return code_bytes 132 | except Exception as e: 133 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 134 | sys.exit() 135 | 136 | class Patch(gdb.Command): 137 | """ Patch instructions -> program_patch
e.g. 138 | gdb> program_patch "xor rax,rax;nop" 0x400123 out_bin 139 | gdb> program_patch "0x90,0x90" 0x400123 out_bin""" 140 | 141 | def __init__(self): 142 | super(Patch, self).__init__("program_patch", gdb.COMMAND_SUPPORT) 143 | 144 | def invoke(self, args, from_tty): 145 | print("[*] program_patch command is called") 146 | print("-----------------------------------------------------") 147 | argv = gdb.string_to_argv (args) 148 | if len (argv) != 3: 149 | raise gdb.GdbError ("Error! Please enter the instructions that you want to patch (e.g (gdb) program_patch \"push rbp; mov rax, 0x10\" 0x400123 out_bin_file)") 150 | 151 | arch_keystone = get_arch() 152 | 153 | instructions = argv[0] 154 | address = argv[1] 155 | out_file = argv[2] 156 | 157 | patch(instructions, address, out_file, arch_keystone) 158 | 159 | return 160 | 161 | 162 | class GetBytes(gdb.Command): 163 | """ Get the instruction opcodes -> e.g. get_bytes "xor rax,rax;nop" """ 164 | 165 | def __init__(self): 166 | super(GetBytes, self).__init__("get_bytes", gdb.COMMAND_SUPPORT) 167 | 168 | def invoke(self, args, from_tty): 169 | print("[*] get_bytes command is called") 170 | print("-----------------------------------------------------") 171 | argv = gdb.string_to_argv (args) 172 | if len (argv) != 1: 173 | raise gdb.GdbError ("Error! Please enter the instructions (e.g get_bytes \'push rbp;nop\')") 174 | 175 | arch_keystone = get_arch() 176 | 177 | instructions = argv[0] 178 | get_bytes(instructions, arch_keystone) 179 | 180 | return 181 | 182 | 183 | class MemoryGdbPatch(gdb.Command): 184 | """ Memory patch instruction -> memory_patch
e.g. 185 | gdb> memory_patch "xor rax,rax;nop" 0x400123 186 | gdb> memory_patch "0x90,0x90" 0x400123""" 187 | 188 | def __init__(self): 189 | super(MemoryGdbPatch, self).__init__("memory_patch", gdb.COMMAND_SUPPORT) 190 | 191 | def invoke(self, args, from_tty): 192 | print("[*] memory_patch command is called") 193 | print("-----------------------------------------------------") 194 | argv = gdb.string_to_argv (args) 195 | if len (argv) != 2: 196 | raise gdb.GdbError ("Error! Please enter the instructions that you want to patch in gdb (e.g memory_patch \"push rbp; mov rax, 0x10\") 0x400123") 197 | 198 | arch_keystone = get_arch() 199 | 200 | instructions = argv[0] 201 | address = argv[1] 202 | 203 | memory_patch(instructions, address, arch_keystone) 204 | print("[*] Memory is successfully patched at address {}".format(address)) 205 | 206 | return 207 | 208 | Patch() 209 | GetBytes() 210 | MemoryGdbPatch() 211 | 212 | 213 | def runnning_gdb(): 214 | return gdb.selected_inferior().pid 215 | 216 | 217 | def get_core_obj(arch): 218 | core_obj = Core() 219 | 220 | if core_obj.arch(arch): 221 | print("Architecture not supported!") 222 | sys.exit() 223 | 224 | return core_obj 225 | 226 | 227 | def get_arch(): 228 | try: 229 | if runnning_gdb(): 230 | arch_str = gdb.selected_frame().architecture() 231 | arch_str = arch_str.name() 232 | else: 233 | arch = gdb.execute("show architecture", to_string=True).rstrip() 234 | arch_str = arch.split()[-1].replace(")", "") 235 | 236 | arch_keystone = "" 237 | print("[*] Arch is " + arch_str) 238 | 239 | for arch_gdb in archs_32_gdb: 240 | if arch_str == arch_gdb: 241 | arch_keystone = "x86-32" 242 | 243 | for arch_gdb in archs_64_gdb: 244 | if arch_str == arch_gdb: 245 | arch_keystone = "x86-64" 246 | 247 | if arch_keystone == "": 248 | print("Architecture not supported!") 249 | sys.exit() 250 | 251 | return arch_keystone 252 | except Exception as e: 253 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 254 | sys.exit() 255 | 256 | 257 | def patch(instructions, address, out_file, arch): 258 | try: 259 | if not runnning_gdb(): 260 | print("In order to patch the binary, you need to run GDB") 261 | return 262 | 263 | #file to patch 264 | proc = gdb.execute("info proc", to_string=True) 265 | file_to_patch = proc.split()[4].replace("'", "") 266 | 267 | #calc offset 268 | #pc = gdb.selected_frame().read_register("pc") 269 | #pc_str = str(pc).split()[0] 270 | proc_map = gdb.execute("info proc mapping", to_string=True) 271 | index = proc_map.find("\n\n") 272 | proc_map = proc_map[index+1:] 273 | proc_map = proc_map.split() 274 | address_base = proc_map[7] 275 | module_name = proc_map[11] 276 | print("[*] Address " + address_base + " module name " + module_name + " address is " + address) 277 | offset = int(address, 16) - int(address_base, 16) 278 | print("[*] Offset is {}".format((hex(offset)))) 279 | 280 | core_obj = get_core_obj(arch) 281 | 282 | if file_to_patch and offset: 283 | file_obj = FilesOp(file_to_patch) 284 | file_obj.check_file() 285 | file_obj.read_file() 286 | #patch binary 287 | start_check = instructions.replace("0x", "")[0:2] 288 | if start_check.isdigit(): 289 | patch = core_obj.get_bytes(instructions) 290 | file_obj.patch_file(out_file, offset, patch) 291 | else: 292 | patch = core_obj.assemble(instructions, True) 293 | patch = bytearray(patch) 294 | file_obj.patch_file(out_file, offset, patch) 295 | except Exception as e: 296 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 297 | sys.exit() 298 | 299 | 300 | def get_bytes(instructions, arch): 301 | try: 302 | core_obj = get_core_obj(arch) 303 | core_obj.assemble(instructions, True) 304 | except Exception as e: 305 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 306 | sys.exit() 307 | 308 | 309 | def memory_patch(instructions, address, arch): 310 | try: 311 | if not runnning_gdb(): 312 | print("In order to patch the binary, you need to run GDB") 313 | return 314 | 315 | address = int(address, 16) 316 | core_obj = get_core_obj(arch) 317 | #get pc 318 | #pc = gdb.selected_frame().read_register("pc") 319 | #pc_str = str(pc).split()[0] 320 | start_check = instructions.replace("0x", "")[0:2] 321 | if start_check.isdigit(): 322 | patch = core_obj.get_bytes(instructions) 323 | print("[*] Bytes: {} (len: {})".format(instructions, len(patch))) 324 | else: 325 | patch = core_obj.assemble(instructions, True) 326 | 327 | for i in range(0, len(patch)): 328 | gdb.execute("set *(char*)({}+{}) = {}".format(hex(address), i, patch[i])) 329 | except Exception as e: 330 | print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 331 | sys.exit() 332 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | capstone==4.0.1 2 | keystone-engine==0.9.1.post3 3 | -------------------------------------------------------------------------------- /tests/patch_me.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define PASSWORD "patch_me" 6 | #define SIZE 256 7 | 8 | int main(void) { 9 | char buffer[SIZE]; 10 | 11 | printf("Password: "); 12 | 13 | fgets(buffer, SIZE, stdin); 14 | 15 | if (strncmp(buffer, PASSWORD, strlen(buffer)-1) == 0) { 16 | printf("Password correct!\n"); 17 | return 0; 18 | } 19 | printf("Password incorrect!\n"); 20 | 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /tests/patch_me_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invictus1306/QuickPatch/38dacf50a46b9e6c3476135001c59d4102ddab72/tests/patch_me_pie --------------------------------------------------------------------------------