├── tests ├── build.ps1 └── test_coff.py ├── setup.py ├── README.md ├── LICENSE ├── cough ├── string_table.py ├── __init__.py ├── symbol.py ├── file.py └── section.py └── .gitignore /tests/build.ps1: -------------------------------------------------------------------------------- 1 | $target = "x64" 2 | $tools_version = "14.10.25017" 3 | $winsdk_version = "10.0.15063.0" 4 | 5 | $tools_root = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\BuildTools\VC\Tools\MSVC\$tools_version" 6 | $winsdk_lib_root = "${env:ProgramFiles(x86)}\Windows Kits\10\Lib\$winsdk_version" 7 | $env:LIB = "$tools_root\lib\x64;$winsdk_lib_root\ucrt\$target;$winsdk_lib_root\um\$target" 8 | 9 | $tools_bin = "$tools_root\bin\HostX64\$target" 10 | $link = "$tools_bin\link.exe" 11 | 12 | & $link /nologo /subsystem:console msvcrt.lib ucrt.lib $args 13 | exit $LastExitCode 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | from setuptools import setup 4 | 5 | root = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | with open(os.path.join(root, 'README.md')) as readme_file: 8 | long_description = readme_file.read() 9 | 10 | setup( 11 | name='cough', 12 | version='0.2.1', 13 | description='Write COFF object files', 14 | long_description=long_description, 15 | author='David D. Dorfman', 16 | author_email='d3dave@users.noreply.github.com', 17 | url='https://github.com/d3dave/cough', 18 | packages=['cough'], 19 | license='MIT', 20 | keywords='coff pe obj build development', 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'Intended Audience :: Developers', 24 | 'Topic :: Software Development', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Operating System :: Microsoft :: Windows' 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cough 2 | ===== 3 | 4 | A library for building COFF object files. 5 | 6 | 7 | Tutorial 8 | -------- 9 | 10 | Start with the ObjectModule class: 11 | 12 | module = ObjectModule() 13 | 14 | Now, let's create a '.text' section: 15 | 16 | section = Section(b'.text', SectionFlags.MEM_EXECUTE) 17 | 18 | Add a bit of code: 19 | 20 | section.data = b'\x29\xC0\xC3' # return 0 21 | section.size_of_raw_data = len(section.data) 22 | 23 | Good enough, let's add it to our module: 24 | 25 | module.sections.append(section) 26 | 27 | To make use of that bit of code, we are going to need an exported symbol: 28 | 29 | main = SymbolRecord(b'main', section_number=1, storage_class=StorageClass.EXTERNAL) 30 | 31 | Set the value to the offset in the section: 32 | 33 | main.value = 0 34 | 35 | And add it to our module: 36 | 37 | module.symbols.append(main) 38 | 39 | That's enough, let's write our module to a file: 40 | 41 | with open('test.obj', 'wb') as obj_file: 42 | obj_file.write(module.get_buffer()) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David D. Dorfman 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 | -------------------------------------------------------------------------------- /cough/string_table.py: -------------------------------------------------------------------------------- 1 | class StringTable: 2 | """ 3 | Layout: 4 | +-----------------+ 5 | | Size of table | 6 | +-----------------+ 7 | | Strings | 8 | +-----------------+ 9 | Size is in bytes and contains the 4 bytes required to write it. 10 | """ 11 | def __init__(self): 12 | self._strings = [] 13 | 14 | @staticmethod 15 | def _check(value): 16 | if not isinstance(value, bytes): 17 | raise ValueError('value must be an encoded string') 18 | 19 | def __len__(self): 20 | return len(self._strings) 21 | 22 | def __getitem__(self, item): 23 | return self._strings[item] 24 | 25 | def __setitem__(self, key, value): 26 | self._check(value) 27 | self._strings[key] = value 28 | 29 | def __contains__(self, item): 30 | return item in self._strings 31 | 32 | def __iter__(self): 33 | return iter(self._strings) 34 | 35 | def append(self, item): 36 | self._check(item) 37 | self._strings.append(item) 38 | 39 | def pack(self): 40 | sizeof_strtab_size = 4 41 | total_size_in_bytes = sizeof_strtab_size + sum(len(s) + 1 for s in self._strings) 42 | buffer = bytearray() 43 | buffer += total_size_in_bytes.to_bytes(sizeof_strtab_size, 'little', signed=False) 44 | for s in self._strings: 45 | buffer += s + b'\0' 46 | return bytes(buffer) 47 | -------------------------------------------------------------------------------- /cough/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | cough - A library for building COFF object files. 3 | 4 | Start with the ObjectModule class: 5 | 6 | >>> module = ObjectModule() 7 | 8 | Now, let's create a '.text' section: 9 | 10 | >>> section = Section(b'.text', SectionFlags.MEM_EXECUTE) 11 | 12 | Add a bit of code: 13 | 14 | >>> section.data = b'\x29\xC0\xC3' # return 0 15 | ... section.size_of_raw_data = len(section.data) 16 | 17 | Good enough, let's add it to our module: 18 | 19 | >>> module.sections.append(section) 20 | 21 | To make use of that bit of code, we are going to need an exported symbol: 22 | 23 | >>> main = SymbolRecord(b'main', section_number=1, storage_class=StorageClass.EXTERNAL) 24 | 25 | Set the value to the offset in the section: 26 | 27 | >>> main.value = 0 28 | 29 | And add it to our module: 30 | 31 | >>> module.symbols.append(main) 32 | 33 | That's enough, let's write our module to a file: 34 | 35 | >>> with open('test.obj', 'wb') as obj_file: 36 | ... obj_file.write(module.get_buffer()) 37 | """ 38 | __author__ = 'David D. Dorfman' 39 | 40 | from .file import * 41 | from .section import * 42 | from .symbol import * 43 | 44 | 45 | __all__ = [ 46 | 'ObjectModule', 47 | 'MachineType', 48 | 'FileHeader', 49 | 'SpecialSectionNumber', 50 | 'StorageClass', 51 | 'SectionFlags', 52 | 'ComplexType', 53 | 'BaseType', 54 | 'SymbolRecord', 55 | 'mktype', 56 | 'Section', 57 | 'StringTable', 58 | 'Relocation' 59 | ] 60 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /cough/symbol.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import struct 3 | 4 | 5 | class SpecialSectionNumber(enum.IntEnum): 6 | UNDEFINED = 0 7 | ABSOLUTE = -1 8 | DEBUG = -2 9 | 10 | 11 | class StorageClass(enum.IntEnum): 12 | END_OF_FUNCTION = -1 13 | NULL = 0 14 | AUTOMATIC = 1 15 | EXTERNAL = 2 16 | STATIC = 3 17 | REGISTER = 4 18 | EXTERNAL_DEF = 5 19 | LABEL = 6 20 | UNDEFINED_LABEL = 7 21 | MEMBER_OF_STRUCT = 8 22 | ARGUMENT = 9 23 | STRUCT_TAG = 10 24 | MEMBER_OF_UNION = 11 25 | UNION_TAG = 12 26 | TYPE_DEFINITION = 13 27 | UNDEFINED_STATIC = 14 28 | ENUM_TAG = 15 29 | MEMBER_OF_ENUM = 16 30 | REGISTER_PARAM = 17 31 | BIT_FIELD = 18 32 | BLOCK = 100 33 | FUNCTION = 101 34 | END_OF_STRUCT = 102 35 | FILE = 103 36 | SECTION = 104. 37 | WEAK_EXTERNAL = 105 38 | CLR_TOKEN = 107 39 | 40 | 41 | class BaseType(enum.IntEnum): 42 | NULL = 0 43 | VOID = 1 44 | CHAR = 2 45 | SHORT = 3 46 | INT = 4 47 | LONG = 5 48 | FLOAT = 6 49 | DOUBLE = 7 50 | STRUCT = 8 51 | UNION = 9 52 | ENUM = 10 53 | MOE = 11 54 | BYTE = 12 55 | WORD = 13 56 | UINT = 14 57 | DWORD = 15 58 | 59 | 60 | class ComplexType(enum.IntEnum): 61 | NULL = 0 62 | POINTER = 1 63 | FUNCTION = 2 64 | ARRAY = 3 65 | 66 | 67 | def mktype(base, comp): 68 | return (comp << 8) + base 69 | 70 | 71 | class SymbolRecord: 72 | record_struct = struct.Struct('<8sLhHBB') 73 | 74 | def __init__(self, name, typ=None, section_number=SpecialSectionNumber.UNDEFINED, storage_class=StorageClass.NULL): 75 | self.name = name 76 | self.value = None 77 | self.section_number = section_number 78 | self.type = typ or 0 79 | self.storage_class = storage_class 80 | self.aux_records = [] 81 | 82 | def pack(self): 83 | packed_aux_records = b''.join(self.aux_records) 84 | if len(packed_aux_records) % 18 != 0: 85 | raise ValueError('auxiliary records length must be a multiple of 18') 86 | return self.record_struct.pack( 87 | self.name, 88 | self.value, 89 | self.section_number, 90 | self.type, 91 | self.storage_class, 92 | len(self.aux_records) 93 | ) + packed_aux_records 94 | -------------------------------------------------------------------------------- /tests/test_coff.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import subprocess 4 | 5 | import cough 6 | 7 | TESTS_DIR = os.path.dirname(__file__) 8 | BUILD_SCRIPT = os.path.join(TESTS_DIR, 'build.ps1') 9 | 10 | 11 | def test_coff(): 12 | module = cough.ObjectModule() 13 | 14 | # mov rax, 0; ret 15 | sec_aaaa = cough.Section(b'aaaa', cough.SectionFlags.MEM_EXECUTE, b'\x48\xC7\xC0\x00\x00\x00\x00\xC3') 16 | module.sections.append(sec_aaaa) 17 | 18 | sym1 = cough.SymbolRecord(b'main', section_number=1, storage_class=cough.StorageClass.EXTERNAL) 19 | sym1.value = 0 # offset 0 20 | module.symbols.append(sym1) 21 | 22 | file_buffer = module.get_buffer() 23 | with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as file: 24 | file.write(file_buffer) 25 | base, _ = os.path.splitext(file.name) 26 | exe_path = f'{base}.exe' 27 | subprocess.run(['PowerShell.exe', BUILD_SCRIPT, file.name, '/out:' + '"' + exe_path + '"'], check=True) 28 | subprocess.run([exe_path], check=True) 29 | 30 | 31 | def test_reloc(): 32 | module = cough.ObjectModule() 33 | 34 | instructions = [ 35 | b'\x48\x83\xEC\x28', # sub rsp, 28h 36 | b'\xB9\x41\x00\x00\x00', # mov ecx, 41h 37 | b'\xE8\x00\x00\x00\x00', # call putchar 38 | b'\x48\x83\xC4\x28', # add rsp, 28h 39 | b'\x29\xC0', # sub eax, eax 40 | b'\xC3'] # ret 41 | sec_aaaa = cough.Section(b'aaaa', cough.SectionFlags.MEM_EXECUTE, b''.join(instructions)) 42 | sec_aaaa.number_of_relocations = 1 43 | putchar_reloc = cough.Relocation() 44 | putchar_reloc.virtual_address = 10 45 | putchar_reloc.symbol_table_index = 1 46 | putchar_reloc.type = 0x04 # REL32 47 | sec_aaaa.relocations.append(putchar_reloc) 48 | module.sections.append(sec_aaaa) 49 | 50 | sym1 = cough.SymbolRecord(b'main', section_number=1, storage_class=cough.StorageClass.EXTERNAL) 51 | sym1.value = 0 # offset 0 52 | module.symbols.append(sym1) 53 | 54 | putchar_sym = cough.SymbolRecord(b'putchar', storage_class=cough.StorageClass.EXTERNAL) 55 | putchar_sym.value = 0 56 | module.symbols.append(putchar_sym) 57 | 58 | file_buffer = module.get_buffer() 59 | with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as file: 60 | file.write(file_buffer) 61 | base, _ = os.path.splitext(file.name) 62 | exe_path = f'{base}.exe' 63 | subprocess.run(['PowerShell.exe', BUILD_SCRIPT, file.name, '/out:' + '"' + exe_path + '"'], check=True) 64 | proc = subprocess.run([exe_path], stdout=subprocess.PIPE, check=True) 65 | assert proc.stdout == b'A' 66 | 67 | 68 | def test_uninit_before_init(): 69 | module = cough.ObjectModule() 70 | 71 | sec_uninit = cough.Section(b'uninit', cough.SectionFlags.CNT_UNINITIALIZED_DATA) 72 | sec_uninit.size_of_raw_data = 0x400 73 | module.sections.append(sec_uninit) 74 | 75 | sec_init = cough.Section(b'init', 0, b'\xAA\xBB\xCC\xDD') 76 | module.sections.append(sec_init) 77 | 78 | assert module.get_buffer() 79 | -------------------------------------------------------------------------------- /cough/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Overall COFF file structure 3 | """ 4 | import time 5 | import enum 6 | import struct 7 | 8 | from .string_table import StringTable 9 | 10 | 11 | class MachineType(enum.IntEnum): 12 | UNKNOWN = 0x0 13 | AM33 = 0x1d3 14 | AMD64 = 0x8664 15 | ARM = 0x1c0 16 | ARM64 = 0xaa64 17 | ARMNT = 0x1c4 18 | EBC = 0xebc 19 | I386 = 0x14c 20 | IA64 = 0x200 21 | M32R = 0x9041 22 | MIPS16 = 0x266 23 | MIPSFPU = 0x366 24 | MIPSFPU16 = 0x466 25 | POWERPC = 0x1f0 26 | POWERPCFP = 0x1f1 27 | R4000 = 0x166 28 | RISCV32 = 0x5032 29 | RISCV64 = 0x5064 30 | RISCV128 = 0x5128 31 | SH3 = 0x1a2 32 | SH3DSP = 0x1a3 33 | SH4 = 0x1a6 34 | SH5 = 0x1a8 35 | THUMB = 0x1c2 36 | WCEMIPSV2 = 0x169 37 | 38 | 39 | class FileHeader: 40 | """ 41 | Offset Size Field 42 | ===================================== 43 | 0 2 Machine 44 | 2 2 NumberOfSections 45 | 4 4 TimeDateStamp 46 | 8 4 PointerToSymbolTable 47 | 12 4 NumberOfSymbols 48 | 16 2 SizeOfOptionalHeader 49 | 18 2 Characteristics 50 | 51 | Machine: 52 | Target machine type. 53 | NumberOfSections: 54 | Indicates the size of the section table, which immediately follows the headers. 55 | TimeDateStamp: 56 | Indicates when the file was created. The low 32 bits of the number of seconds since 1970-01-01 00:00. 57 | PointerToSymbolTable: 58 | The file offset of the COFF symbol table, or zero if no COFF symbol table is present. 59 | NumberOfSymbols: 60 | The number of entries in the symbol table. 61 | This data can be used to locate the string table, which immediately follows the symbol table. 62 | SizeOfOptionalHeader: 63 | The size of the optional header, which is required for executable files but not for object files. 64 | Characteristics: 65 | The flags that indicate the attributes of the file. 66 | """ 67 | struct = struct.Struct('