From ff3ca4dd3e29cced2770a2d51a9e4f498412e40a Mon Sep 17 00:00:00 2001 From: Kent Slaney Date: Mon, 10 Jun 2024 17:48:51 -0700 Subject: [PATCH] promptless dll patch --- .SRCINFO | 1 + PKGBUILD | 10 +-- dll_patch.py | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 dll_patch.py diff --git a/.SRCINFO b/.SRCINFO index bfbf7ac6376b..29017d96e3f2 100644 --- a/.SRCINFO +++ b/.SRCINFO @@ -10,6 +10,7 @@ pkgbase = bakkesmod-steam makedepends = mingw-w64-gcc makedepends = mingw-w64-headers makedepends = mingw-w64-winpthreads + makedepends = python source = https://github.com/bakkesmodorg/BakkesModInjectorCpp/releases/latest/download/BakkesModSetup.exe source = https://github.com/bakkesmodorg/BakkesModInjectorCpp/releases/latest/download/BakkesModInjectorWin7.zip source = https://api.github.com/repos/bakkesmodorg/BakkesModInjectorCpp/zipball/master diff --git a/PKGBUILD b/PKGBUILD index 3a80d92c399d..be32bbf5c3cd 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -8,7 +8,7 @@ url="https://bakkesmod.com/" license=('GPL') groups=() depends=() -makedepends=('mingw-w64-binutils' 'mingw-w64-crt' 'mingw-w64-gcc' 'mingw-w64-headers' 'mingw-w64-winpthreads') +makedepends=('mingw-w64-binutils' 'mingw-w64-crt' 'mingw-w64-gcc' 'mingw-w64-headers' 'mingw-w64-winpthreads' 'python') optdepends=() source=( "https://github.com/bakkesmodorg/BakkesModInjectorCpp/releases/latest/download/BakkesModSetup.exe" @@ -40,7 +40,7 @@ build() { DllInjector dllInjector; std::wstring ps = L"RocketLeague.exe"; std::filesystem::path ws = - "C:\\users\\steamuser\\Application Data\\bakkesmod\\bakkesmod\\dll\\bakkesmod.dll"; + "C:\\users\\steamuser\\Application Data\\bakkesmod\\bakkesmod\\dll\\bakkesmod_promptless.dll"; dllInjector.InjectDLL(ps, ws); return 0; } @@ -81,8 +81,9 @@ package() { else WINEPREFIX="$compat/pfx/" "$proton/bin/wine64" "$srcdir/BakkesModSetup.exe" fi + python "$srcpath/dll_patch.py" "$bm_pfx/bakkesmod/dll" - cp -f "$srcdir/BakkesMod.exe" "$srcdir/inject.exe" "$bm_pfx" + cp -f "$srcdir/BakkesMod.exe" "$srcdir/inject.exe" "$srcpath/dll_patch.py" "$bm_pfx" echo "$paths" > "$bm_pfx/runner.sh" chmod a+x "$bm_pfx/runner.sh" cat <<" EOF" >> "$bm_pfx/runner.sh" @@ -94,6 +95,7 @@ package() { # setup=`find "$HOME/.steam/steam/userdata" -name "localconfig.vdf"` # setup=`grep 252950 -A 10 "$setup" | grep LaunchOptions | sed 's@^\([^"]*"\)\{3\}@@'` # echo "set launch options to: \"$bm_pfx/launch.sh & $setup" - # I recommend the -NoKeyboardUI option + # # above is still broken, with update status unclear + # # unrelated: I recommend the -NoKeyboardUI option } diff --git a/dll_patch.py b/dll_patch.py new file mode 100644 index 000000000000..21d0871faef8 --- /dev/null +++ b/dll_patch.py @@ -0,0 +1,175 @@ +lea64 = b'\x48\x8d' +call64 = b'\xFF' +little = lambda x: int.from_bytes(x, byteorder='little') + +# iterate over matches for lea64 +def leas(f, start=None, end=None, op=lea64): + match = f.find(op, start, end) + while match != -1: + yield match + match = f.find(op, match + 1, end) + +# give the position and loaded virtual address for each lea match +def addr(f, start=None, end=None, op=lea64, reg=1): + arg = len(op) + reg + for i in leas(f, start, end, op): + yield [i, i + arg + 4 + little(f[i + arg: i + arg + 4])] + +# VA offsets for .text and .rdata +def offsets(f): + # .text567_size_want + off = (f.index(b".text"), f.index(b".rdata")) + return [a - b for a, b in zip( + [little(f[i + 0xc : i + 0x10]) for i in off], + [little(f[i + 0x14 : i + 0x18]) for i in off], + )] + +targets = [ + b'Could not verify RL version', + b'Dunno how the user got here', + b'User wishes to inject anyway', + ] + +# gives storage locations of target phrases accounting for offsets +def rel32(f): + text, rdata = offsets(f) + return [f.index(i) + rdata - text for i in targets] + +# gives positions of references to the target phrases +def refs(f, rel=None, it=None): + default = rel is None and it is None + rel, it = rel or rel32(f), it or addr(f) + res = tuple(zip(*[(rel.index(x), i) for i, x in it if x in rel])) + if default: + assert res[0] == (0, 1, 2, 0, 1, 2) + return (res[1][:3], res[1][3:]) + return res + +jcc32 = [b'\x0F' + int.to_bytes(i, 1, 'little') for i in range(0x80, 0x90)] +jcc8 = list(bytes(range(0x70, 0x80))) + +# gives "inject anyway" jmp RVAs +def branches(f, ref=None): + ref = ref or refs(f) + def inner(a, b): + seq = f[a : b] + op32 = [seq.index(i) for i in jcc32 if i in seq] + op8 = [seq.index(i) for i in jcc8 if i in seq] + loc32 = [a + i + 6 + little(seq[i + 2 : i + 6]) for i in op32] + loc8 = [a + i + 2 + little(seq[i + 1 : i + 2]) for i in op8] + return loc8 + loc32 + return [max(m for m in inner(i, j) if m <= k) for i, j, k in ref] + +optionals = ( + (2, "Magic"), + (1, "MajorLinkerVersion"), + (1, "MinorLinkerVersion"), + (4, "SizeOfCode"), + (4, "SizeOfInitializedData"), + (4, "SizeOfUninitializedData"), + (4, "AddressOfEntryPoint"), + (4, "BaseOfCode"), + (8, "ImageBase"), + (4, "SectionAlignment"), + (4, "FileAlignment"), + (2, "MajorOperatingSystemVersion"), + (2, "MinorOperatingSystemVersion"), + (2, "MajorImageVersion"), + (2, "MinorImageVersion"), + (2, "MajorSubsystemVersion"), + (2, "MinorSubsystemVersion"), + (4, "Win32VersionValue"), + (4, "SizeOfImage"), + (4, "SizeOfHeaders"), + (4, "CheckSum"), + (2, "Subsystem"), + (2, "DllCharacteristics"), + (8, "SizeOfStackReserve"), + (8, "SizeOfStackCommit"), + (8, "SizeOfHeapReserve"), + (8, "SizeOfHeapCommit"), + (4, "LoaderFlags"), + (4, "NumberOfRvaAndSizes"), +) + +def header(name, offset=0): + for size, field in optionals: + if field == name: + return (offset, offset + size) + offset += size + raise Exception() + +# gives offset and size of DLL directory +def idata(f, off=None): + off = off or offsets(f) + magic = f.index(b'\x0b\x02') + iaddr = header("NumberOfRvaAndSizes", magic + 0xc) + isize = header("NumberOfRvaAndSizes", magic + 0x10) + return little(f[slice(*iaddr)]) - off[1], little(f[slice(*isize)]) + +ox = lambda x: tuple(map(hex, x)) + +# gives IAT start and end +def user32(f, dlls=None, size=None, off=None): + off = off or offsets(f) + dlls, size = idata(f, off) if dlls is None else (dlls, size) + rows = [dlls + i for i in range(0, size, 20)] + rvas = [little(f[i + 0xc : i + 0x10]) for i in rows] + tabs = [little(f[i + 0x10 : i + 0x14]) for i in rows] + matching = b'USER32.dll' + substr = lambda i: f[i - off[1] : i - off[1] + len(matching)] + ptrs = [n for n, i in enumerate(rvas) if i != 0 and substr(i) == matching] + assert len(ptrs) == 1 + ptr = ptrs[0] + end = min(i for i in tabs if i > tabs[ptr]) + return tabs[ptr] - off[1], end - off[1] + +# gives start of 8 byte IAT line as a .text RVA +def fns(f, iat=None, end=None, off=None): + off = off or offsets(f) + iat, end = user32(f, off=off) if iat is None else (dlls, size) + rvas = [little(f[i : i + 4]) for i in range(iat, end, 8)] + vas = [i - off[1] for i in rvas if i != 0] + hints = [f[i : i + 2] for i in vas] + names = [f[i + 2 : f.index(b'\x00', i + 2)] for i in vas] + return iat + names.index(b'MessageBoxA') * 8 + off[1] - off[0] + +def shunts(f, dbg=True): + loads, msg = refs(f), fns(f) + calls = [refs(f, (msg,), addr(f, *ref[:2], call64))[1][0] for ref in loads] + skips = branches(f, loads) + paired = tuple(zip(calls, skips)) + if dbg: + print(tuple(map(ox, paired))) + return paired + +jmp = lambda x: b'\xE9' + int.to_bytes(x, 4, "little") + +def mask(f, space=6): + prev, out = 0, b'' + for pos, end in shunts(f): + update = jmp(end - pos - 5) + assert len(update) == 5 + out += f[prev:pos] + update + b'\x90' * (space - len(update)) + prev = pos + space + out += f[prev:] + assert len(f) == len(out) + return out + +def dll(path, dest=None): + global Path + from pathlib import Path + path = Path(path) + path = path if path.is_file() else path / "bakkesmod.dll" + with open(path, "rb") as fp: + f = fp.read() + f = mask(f) + dest = path.parents[0] if dest is None else dest + dest = dest if dest.is_file() else dest / "bakkesmod_promptless.dll" + with open(dest, "wb") as fp: + fp.write(f) + +if __name__ == "__main__": + import sys + dll(*sys.argv[1:]) +