diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d55479..b306765 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,18 +41,18 @@ jobs: # clone everything that we need - uses: actions/checkout@v4 with: - repository: FAForever/FA-Binary-Patches - ref: 'master' path: . - uses: actions/checkout@v4 with: - path: fa-python-binary-patcher + repository: FAForever/FA-Binary-Patches + ref: 'master' + path: FA-Binary-Patches # install everything that we need - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.14" - name: Install MinGW i686 run: choco install mingw --x86 -y --no-progress @@ -60,17 +60,13 @@ jobs: - name: Update PATH run: echo C:/ProgramData/mingw64/mingw32/bin/ >> $env:GITHUB_PATH - - name: Install fa-python-binary-patcher - run: | - pip install -r ./fa-python-binary-patcher/requirements.txt - # download base executable - name: Download base executable run: | - curl -L "https://content.faforever.com/build/ForgedAlliance_base.exe" -o ForgedAlliance_base.exe + curl -L "https://content.faforever.com/build/ForgedAlliance_base.exe" -o FA-Binary-Patches/ForgedAlliance_base.exe # patch it, if it works then we're good - name: Patch base executable run: | - mkdir build - python ./fa-python-binary-patcher/main.py "$(pwd)" clang++ ld g++ + mkdir FA-Binary-Patches/build + cd fa-python-binary-patcher & python main.py config_example.json diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..b48e285 --- /dev/null +++ b/config_example.json @@ -0,0 +1,58 @@ +{ + "target_folder_path": "FA-Binary-Patches", + "input_name": "ForgedAlliance_base.exe", + "output_name": "ForgedAlliance_exxt.exe", + "clang": "clang++.exe", + "gcc": "g++.exe", + "linker": "ld.exe", + "clang_flags": [ + "-pipe", + "-m32", + "-O3", + "-nostdlib", + "-Werror", + "-masm=intel", + "-std=c++20", + "-march=core2" + ], + "gcc_flags": [ + "-pipe", + "-m32", + "-Os", + "-fno-exceptions", + "-nostdlib", + "-nostartfiles", + "-fpermissive", + "-masm=intel", + "-std=c++20", + "-march=core2", + "-mfpmath=both" + ], + "asm_flags": [ + "-pipe", + "-m32", + "-Os", + "-fno-exceptions", + "-nostdlib", + "-nostartfiles", + "-w", + "-fpermissive", + "-masm=intel", + "-std=c++20", + "-march=core2", + "-mfpmath=both" + ], + "functions": { + "_atexit": "0xA8211E", + "__Znwj": "0xA825B9", + "__ZdlPvj": "0x958C40", + "__imp__GetModuleHandleA@4": "0xC0F378", + "__imp__GetProcAddress@8": "0xC0F48C", + "___CxxFrameHandler3": "0xA8958C", + "___std_terminate": "0xA994FB", + "??_7type_info@@6B@": "0xD72A88", + "__CxxThrowException@8": "0x00A89950", + "_memset": "0xA89110", + "__invoke_watson": "0x00A848E8" + } +} \ No newline at end of file diff --git a/main.py b/main.py index 398a860..fdd8f8d 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,6 @@ if __name__ == "__main__": start = time.time() - patcher.patch(*sys.argv) + patcher.patch(*sys.argv[1:]) end = time.time() print(f"Patched in {end-start:.2f}s") diff --git a/patcher/Config.py b/patcher/Config.py new file mode 100644 index 0000000..f6ef500 --- /dev/null +++ b/patcher/Config.py @@ -0,0 +1,70 @@ +import json +from pathlib import Path +from dataclasses import dataclass, field +from typing import Self + + +@dataclass +class Config: + path: Path + target_folder_path: Path + build_folder_path: Path + input_name: str = "ForgedAlliance_base.exe" + output_name: str = "ForgedAlliance_exxt.exe" + + clang_path: Path = "clang++" + gcc_path: Path = "g++" + linker_path: Path = "ld" + + clang_flags: tuple[str] = () + gcc_flags: tuple[str] = () + asm_flags: tuple[str] = () + + functions: dict[str, str] = field(default_factory=dict) + + @classmethod + def load_from_json(cls, path: Path) -> Self: + path = Path(path).resolve() + with open(path, 'r') as f: + config = json.load(f) + + return cls( + path=path, + target_folder_path=config.get("target_folder_path", path.parent), + build_folder_path=config.get("build_folder_path"), + input_name=config.get("input_name", Config.input_name), + output_name=config.get("output_name", Config.output_name), + clang_path=config.get("clang", Config.clang_path), + gcc_path=config.get("gcc", Config.gcc_path), + linker_path=config.get("linker", Config.linker_path), + clang_flags=config.get("clang_flags", Config.clang_flags), + gcc_flags=config.get("gcc_flags", Config.gcc_flags), + asm_flags=config.get("asm_flags", Config.asm_flags), + functions=config.get("functions", {}), + ) + + def __post_init__(self): + self.target_folder_path = Path(self.target_folder_path) + + if not self.target_folder_path.is_absolute(): + self.target_folder_path = self.path.parent / self.target_folder_path + + self.build_folder_path = Path(self.build_folder_path)\ + if self.build_folder_path \ + else self.target_folder_path / "build" + + self.clang_path = Path(self.clang_path) + self.gcc_path = Path(self.gcc_path) + self.linker_path = Path(self.linker_path) + + if not self.build_folder_path.is_absolute(): + raise ValueError( + "build_folder_path must be an absolute path to folder") + + @property + def input_path(self) -> Path: + return self.target_folder_path / self.input_name + + @property + def output_path(self) -> Path: + return self.build_folder_path / self.output_name diff --git a/patcher/PEData.py b/patcher/PEData.py index d369937..6dcbb95 100644 --- a/patcher/PEData.py +++ b/patcher/PEData.py @@ -76,4 +76,4 @@ def find_sect(self, name: str) -> Optional[PESect]: for sect in self.sects: if sect.name == name: return sect - return None + raise Exception(f"Couldn't find section {name}") diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 6d760f5..c749cab 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -3,24 +3,20 @@ import os from pathlib import Path import re +import json from typing import Optional import struct import itertools from patcher import Hook +from .Config import Config +from string.templatelib import Template +import shlex -CLANG_FLAGS = " ".join(["-pipe -m32 -Os -nostdlib -Werror -masm=intel -std=c++20 -march=core2 -c", - ]) -GCC_FLAGS = " ".join(["-pipe -m32 -Os -fno-exceptions -nostdlib -nostartfiles -fpermissive -masm=intel -std=c++20 -march=core2 -mfpmath=both", - ]) -GCC_FLAGS_ASM = " ".join(["-pipe -m32 -Os -fno-exceptions -nostdlib -nostartfiles -w -fpermissive -masm=intel -std=c++20 -march=core2 -mfpmath=both", - ]) SECT_SIZE = 0x80000 ASM_RE = re.compile(r"(asm\(\"(0[xX][0-9a-fA-F]{1,8})\"\);)", re.IGNORECASE) -CALL_RE = re.compile( - r"((call|jmp|je|jne)\s+(0[xX][0-9a-fA-F]{1,8}))", re.IGNORECASE) SPACES_RE = re.compile(" +") @@ -61,48 +57,11 @@ def read_files_contents(dir_path: Path, paths: list[str]) -> dict[str, list[str] return files_contents -def preprocess_lines(files_contents: dict[str, list[str]]) -> tuple[list[str], dict[str, str]]: - new_lines = [] - address_names = {} - for file_name, contents in files_contents.items(): - file_lines = [] - file_addresses = {} - for line in contents: - matches = CALL_RE.finditer(line) - new_line = line - for match in matches: - full_s = match.group(0) - address = match.group(3) - address_name = "_" + address[1::] - s_start, s_end = match.span() - file_addresses[address_name] = address - new_line = new_line[:s_start] + \ - full_s.replace(address, address_name) + new_line[s_end:] - file_lines.append(new_line) - if len(file_addresses) == 0: - new_lines.append(f"#include \"{file_name}\"\n") - else: - new_lines.extend(file_lines) - address_names |= file_addresses - return new_lines, address_names - - def create_sections_file(path: Path, address_map: dict[str, str]): HEADER = """ OUTPUT_FORMAT(pei-i386) OUTPUT(section.pe) - """ - FUNC_NAMES = """ -_atexit = 0xA8211E; -__Znwj = 0xA825B9; -__ZdlPvj = 0x958C40; -"__imp__GetModuleHandleA@4" = 0xC0F378; -"__imp__GetProcAddress@8" = 0xC0F48C; -"___CxxFrameHandler3" = 0xA8958C; -"___std_terminate" = 0xA994FB; /*idk addr*/ -"??_7type_info@@6B@" = 0xD72A88; -"__CxxThrowException@8" = 0x00A89950; """ SECTIONS = """ SECTIONS { @@ -133,18 +92,11 @@ def create_sections_file(path: Path, address_map: dict[str, str]): with open(path, "w") as f: f.write(HEADER) - f.write(FUNC_NAMES) for name, address in address_map.items(): f.write(f"\"{name}\" = {address};\n") f.write(SECTIONS) -def create_cxx_sections_file(path, address_map): - with open(path, "w") as f: - for name, address in address_map.items(): - f.write(f"\"{name}\" = {address};\n") - - def parse_sect_map(file_path: Path) -> dict[str, str]: addresses: dict[str, str] = {} with open(file_path, "r") as f: @@ -303,15 +255,31 @@ def scan_for_headers_in_section(sections_path: Path): return folders -def run_system(command: str) -> int: +def run_system(template: Template) -> int: + if not isinstance(template, Template): + raise TypeError("Expected Template, got " + type(template).__name__) + + + command = [] + for part in template: + if isinstance(part, str): + command.append(part) + else: + value = part.value + s_value = str(value) + if isinstance(value, Path): + command.append(f"\"{s_value}\"" if ' ' in s_value else s_value) + else: + command.append(s_value) + command = "".join(command) print(command) return os.system(command.replace("\n", " ")) -def patch(_, target_folder, clang_compiler_path, linker_path, gcc_compiler_path, * args): - target_path = Path(target_folder) +def patch(config_path): + config = Config.load_from_json(Path(config_path)) - base_pe = PEData(target_path / "ForgedAlliance_base.exe") + base_pe = PEData(config.input_path) new_v_offset = 0 new_f_offset = 0 @@ -324,51 +292,37 @@ def align(v, a): new_v_offset = align(new_v_offset, base_pe.sectalign) new_f_offset = align(new_f_offset, base_pe.filealign) - print(f"Image base: {base_pe.imgbase + new_v_offset - 0x1000:x}") + image_base = base_pe.imgbase + new_v_offset - 0x1000 + print(f"Image base: {image_base:x}") - section_folder_path = target_path / "section" - build_folder_path = target_path / "build" + section_folder_path = config.target_folder_path / "section" + include_folder_path = config.target_folder_path / "include" + hooks_folder_path = config.target_folder_path / "hooks" + build_folder_path = config.build_folder_path paths = find_patch_files(section_folder_path) - # files_contents = read_files_contents(f"{target_path}/section/", paths) - # files_contents, address_names = preprocess_lines(files_contents) - with open(section_folder_path / "main.cpp", "w") as main_file: for path in paths: main_file.writelines(f"#include \"{path}\"\n") - # main_file.writelines(files_contents) function_addresses = { - name: name for name in scan_header_files(target_path)} - - cxx_files_contents = read_files_contents(section_folder_path, list_files_at( - section_folder_path, "**/*.cxx", ["main.cxx"])) - cxx_files_contents, cxx_address_names = preprocess_lines( - cxx_files_contents) + name: name for name in scan_header_files(config.target_folder_path)} with open(section_folder_path / "main.cxx", "w") as main_file: - main_file.writelines(cxx_files_contents) - - folders = scan_for_headers_in_section(section_folder_path) - includes = " ".join((f"-I ../section/{folder}/" for folder in folders)) + for path in list_files_at(section_folder_path, "**/*.cxx", ["main.cxx"]): + main_file.writelines(f"#include \"{path}\"\n") if run_system( - f"""cd {build_folder_path} & - {clang_compiler_path} {CLANG_FLAGS} - -I ../include/ {includes} - ../section/main.cxx -o clangfile.o"""): + t"""{config.clang_path} -c {" ".join(config.clang_flags)} -I {include_folder_path} {section_folder_path / "main.cxx"} -o {build_folder_path / "clangfile.o"}"""): raise Exception("Errors occurred during building of cxx files") - create_sections_file(target_path / "section.ld", - function_addresses | cxx_address_names) + create_sections_file(build_folder_path / "section.ld", + function_addresses | config.functions) if run_system( - f"""cd {build_folder_path} & - {gcc_compiler_path} {GCC_FLAGS} - -I ../include/ {includes} - -Wl,-T,../section.ld,--image-base,{base_pe.imgbase + new_v_offset - 0x1000},-s,-Map,sectmap.txt,-o,section.pe - ../section/main.cpp"""): - raise Exception("Errors occurred during building of patch files") + t""" cd {build_folder_path} & + {config.gcc_path} {" ".join(config.gcc_flags)} -I {include_folder_path} -Wl,-T,section.ld,--image-base,{image_base},-s,-Map,sectmap.txt,-o,section.pe {section_folder_path / "main.cpp"}"""): + raise Exception("Errors occurred during building of patch files") remove_files_at(build_folder_path, "**/*.o") @@ -382,26 +336,26 @@ def create_defines_file(path: Path, addresses: dict[str, str]): ]) for name, address in addresses.items(): f.write(f"#define {name} {address}\n") - create_defines_file(target_path / "define.h", addresses) + create_defines_file(config.target_folder_path / "define.h", addresses) def generate_hook_files(folder_path: Path): for file_path in list_files_at(folder_path, "**/*.hook"): - hook = Hook. load_hook(folder_path/file_path) + hook = Hook.load_hook(folder_path/file_path) hook_path = file_path.replace(os.sep, "_") + ".cpp" print(f"Generating {hook_path}") with open(folder_path/hook_path, "w") as f: f.write(hook.to_cpp()) - generate_hook_files(target_path/"hooks") + generate_hook_files(config.target_folder_path/"hooks") if run_system( - f"""cd {build_folder_path} & - {gcc_compiler_path} -c {GCC_FLAGS_ASM} ../hooks/*.cpp"""): - raise Exception("Errors occurred during building of hooks files") + t"""cd {build_folder_path} & + {config.gcc_path} -c {" ".join(config.asm_flags)} {hooks_folder_path / "*.cpp"}"""): + raise Exception("Errors occurred during building of hooks files") hooks: list[COFFData] = [] for path in list_files_at(build_folder_path, "**/*.o"): - coff_data = COFFData(build_folder_path / path, f"build/{path}") + coff_data = COFFData(build_folder_path / path, path) for sect in coff_data.sects: if len(sect.name) >= 8: raise Exception(f"sect name too long {sect.name}") @@ -416,10 +370,10 @@ def generate_hook_files(folder_path: Path): ssize = section_pe.sects[-1].v_offset + \ section_pe.sects[-1].v_size + section_pe.sects[0].v_offset - with open(target_path / "patch.ld", "w") as pld: + with open(build_folder_path / "patch.ld", "w") as pld: pld.writelines([ "OUTPUT_FORMAT(pei-i386)\n", - "OUTPUT(build/patch.pe)\n", + "OUTPUT(patch.pe)\n", ]) for name, address in addresses.items(): @@ -453,10 +407,11 @@ def generate_hook_files(folder_path: Path): " }\n", "}" ]) + if run_system( - f"""cd {target_path} & - {linker_path} -T patch.ld --image-base {base_pe.imgbase} -s -Map build/patchmap.txt"""): - raise Exception("Errors occurred during linking") + t"""cd {build_folder_path} & + {config.linker_path} -T patch.ld --image-base {base_pe.imgbase} -s -Map patchmap.txt"""): + raise Exception("Errors occurred during linking") base_file_data = bytearray(base_pe.data) @@ -501,10 +456,11 @@ def replace_data(new_data, offset): replace_data(section_pe.data[s.f_offset:s.f_offset+s.f_size], nsect.f_offset+s.v_offset-section_pe.sects[0].v_offset) - apply_sig_patches(target_path / "SigPatches.txt", base_file_data) + apply_sig_patches(config.target_folder_path / + "SigPatches.txt", base_file_data) def save_new_base_data(data: bytearray): - with open(target_path / "ForgedAlliance_exxt.exe", "wb") as nf: + with open(config.output_path, "wb") as nf: sect_count = len(base_pe.sects) nf.write(data) nf.seek(base_pe.offset+0x6) @@ -521,4 +477,4 @@ def save_new_base_data(data: bytearray): save_new_base_data(base_file_data) remove_files_at(build_folder_path, "**/*.o") - remove_files_at(target_path/"hooks", "*.hook.cpp") + remove_files_at(hooks_folder_path, "*.hook.cpp")