From 26578c45bb87da420e567b8f44d489847b882877 Mon Sep 17 00:00:00 2001 From: delthas Date: Wed, 29 May 2019 19:06:00 +0800 Subject: [PATCH] initial commit --- .clang-format | 61 +++ .gitignore | 9 + .gitmodules | 3 + CMakeLists.txt | 43 ++ LICENSE | 21 + README.md | 60 +++ autopunch-address/CMakeLists.txt | 12 + autopunch-address/address.c | 13 + autopunch-loader/go.mod | 14 + autopunch-loader/go.sum | 17 + autopunch-loader/loader.c | 13 + autopunch-loader/loader.go | 745 +++++++++++++++++++++++++++++++ autopunch-loader/loader.h | 10 + autopunch-loader/loader.manifest | 15 + autopunch-packer/go.mod | 3 + autopunch-packer/packer.go | 69 +++ autopunch-relay/relay.go | 155 +++++++ detours | 1 + doc/screen.jpg | Bin 0 -> 44354 bytes inject.c | 495 ++++++++++++++++++++ 20 files changed, 1759 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 autopunch-address/CMakeLists.txt create mode 100644 autopunch-address/address.c create mode 100644 autopunch-loader/go.mod create mode 100644 autopunch-loader/go.sum create mode 100644 autopunch-loader/loader.c create mode 100644 autopunch-loader/loader.go create mode 100644 autopunch-loader/loader.h create mode 100644 autopunch-loader/loader.manifest create mode 100644 autopunch-packer/go.mod create mode 100644 autopunch-packer/packer.go create mode 100644 autopunch-relay/relay.go create mode 160000 detours create mode 100644 doc/screen.jpg create mode 100644 inject.c diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..0a6cbc5 --- /dev/null +++ b/.clang-format @@ -0,0 +1,61 @@ +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: No +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: true +ColumnLimit: 160 +CompactNamespaces: true +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +FixNamespaceComments: true +IncludeBlocks: Regroup +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: false +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 2 +UseTab: Always diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6427537 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.exe +*.dll +.vs +.idea +cmake-* +CMakeSettings.json* +/autopunch-loader/dll*.go +/autopunch-loader/address.go +*.syso diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e77cd7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "detours"] + path = detours + url = https://github.com/microsoft/Detours.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..88d4f82 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.12) +project(autopunch C CXX) + +set(CMAKE_C_STANDARD 11) + +# /W4 /WX /Zi /MT /Gy /Gm- /Zl /Od + +# DETOURS_TARGET_PROCESSOR=X64 +# DETOURS_OPTION_PROCESSOR=X86 +# DETOURS_OPTION_BITS=32 + +add_library(autopunch MODULE + detours/src/detours.cpp + detours/src/modules.cpp + detours/src/disasm.cpp + detours/src/image.cpp + detours/src/creatwth.cpp + detours/src/disolx86.cpp + detours/src/disolx64.cpp + detours/src/disolia64.cpp + detours/src/disolarm.cpp + detours/src/disolarm64.cpp + inject.c) +target_include_directories(autopunch PRIVATE detours/src) +target_link_libraries(autopunch PRIVATE ws2_32) +target_compile_definitions(autopunch PRIVATE -DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WARNINGS) +target_compile_options(autopunch PRIVATE + "$<$:/MTd>" + "$<$>:/MT>") + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(TARGET_FILE_BITS 64) +else() + set(TARGET_FILE_BITS 86) +endif() +if(CMAKE_BUILD_TYPE STREQUAL Debug) + set(TARGET_FILE_BUILD dbg) +else() + set(TARGET_FILE_BUILD rel) +endif() + +add_custom_target(autopunch_copy DEPENDS autopunch COMMAND_EXPAND_LISTS VERBATIM + COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_SOURCE_DIR}/../autopunch-loader/autopunch.x${TARGET_FILE_BITS}.${TARGET_FILE_BUILD}.dll) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b501b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 delthas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..af7f8ba --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# autopunch ![](https://img.shields.io/github/downloads/delthas/autopunch/total.svg?style=flat-square) + +**This program lets you host & connect to friends to play peer-to-peer games without having to open or redirect any port. This does not add any latency to the connection compared to redirecting ports manually.** + +**You can play with users that don't use autopunch without compability issues and can always leave it enabled. However, the tool will only do its magic if both peers use autopunch.** + +*Technical details: autopunch injects itself into a process and detours some winsock calls (sendto, recvfrom, ...) to rewrite addresses so that they appear to be internal ports rather than external ports. It additionally performs hole punching by using a STUN-like relay which helps know internal ports of other users.* + +## How to use + +- **Download the [latest version for Windows 64 bits](https://github.com/delthas/autopunch/releases/latest/download/autopunch.win64.exe) (use this link, not any other download button)** +- **Start your peer-to-peer game** *(for Touhou Hisoutensoku players: also run SokuRoll now, if needed)* +- **Double-click the downloaded executable file to run it**; there is no setup or anything so put the file somewhere you'll remember (doesn't have to be in the game folder) +- If a Windows Defender SmartScreen popup appears, click on "More information", then click on "Run anyway" +- If a Windows Firewall popup appears, check the "Private networks" and "Public networks" checkboxes, then click on "Allow access" +- If prompted for an update, just wait, everything will be updated and restart automatically +- **Click on the game you wish to play in the list, then click "Punch!"**; the window will close and "autopunch" will appear in the game window title *(just like SokuRoll)* +- **Play!** Host on any port, or connect to the IP and port the host gives you just as usual. +- You can host and connect to peer with or without using autopunch: no compatibility issues, you can always leave it running. +- **However, if a host didn't forward its ports, both peers will need autopunch, not just the one hosting!** + +![](doc/screen.jpg) + +### Troubleshooting + +- If you experience any issue when using autopunch, make sure that both peers are running autopunch. *For Hisoutensoku players: if using SokuRoll, run it before running the tool.* +- For some very rare users having a very old or cheap Internet router, or playing at a work office, autopunch might just not work *when they are hosting*. Try switching who hosts if this happens. +- If you have any other issue or feedback, either contact me on Discord at `cc#6439` or [open an issue on Github](https://github.com/delthas/autopunch/issues/new). **When doing that, please check the Debug checkbox, punch and play again, close the game and send me the log file.** + +## Advanced usage + +- No command-line flags, no advanced usage. If you need anything specific open an issue or ask me. + +## Building + +Quick overview of the components of the project: +- `inject.c`: the core autopunch DLL, that is injected into a game; has both 32 and 64 bit versions, release and debug versions; +- `address`: a tiny program to get the address of the `LoadLibraryW` function of the WoW `Kernel32` module; this works because ASLR is only done once per reboot; +- `packer`: a tiny program to pack a binary file as a string in a Go source file so that it can be included in an executable; +- `relay`: the STUN-like relay used by autopunch; +- `loader`: the program that has the GUI and autoupdates, and starts the injection of the autopunch DLL; the final executable packs the 4 autopunch DLLs, the address executable, and a Win32 app manifest. + +**Do not use `go get`. Clone manually.** + +Preparation: +- make sure you pulled all submodules; +- install an `i686-w64-mingw32-gcc` toolchain (for example with MSYS2); +- install the Microsoft Visual C++ Build Tools; +- install CMake; +- install Go; +- install https://github.com/akavel/rsrc (and add $GOPATH/bin to PATH if needed). + +Build order: +- build `address` with CMake, compiling with `i686-w64-mingw32-gcc`, profile MinSizeRel; +- build `packer` with Go; `go install` it / add it to your PATH; +- build `inject.c` (at the root) with CMake four times: Debug and Release, for Visual C++ Build Tools x86 and x64; use the `autopunch_copy` target to put the DLLs in the right folder for next steps; +- generate `loader` with: `go generate loader.go`; +- build `loader`, targeting GOOS=windows GOARCH=amd64, with: `go build -ldflags="-H windowsgui -s -w -X main.version=" -tags walk_use_cgo`; `version` is e.g. `v0.0.1`. + +Building the relay (Go) is straightforward. diff --git a/autopunch-address/CMakeLists.txt b/autopunch-address/CMakeLists.txt new file mode 100644 index 0000000..53a6a65 --- /dev/null +++ b/autopunch-address/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.12) +project(autopunch C CXX) + +set(CMAKE_C_STANDARD 11) + +# compile with i386 mingw +add_executable(address address.c) +target_compile_definitions(address PRIVATE -DWIN32_LEAN_AND_MEAN) +target_link_options(address PRIVATE -mwindows $<$:-s> $<$:-s>) + +add_custom_target(address_copy DEPENDS address COMMAND_EXPAND_LISTS VERBATIM + COMMAND ${CMAKE_COMMAND} -E copy $ "${CMAKE_SOURCE_DIR}/address.exe") diff --git a/autopunch-address/address.c b/autopunch-address/address.c new file mode 100644 index 0000000..56caed1 --- /dev/null +++ b/autopunch-address/address.c @@ -0,0 +1,13 @@ +#include + +int main(int argc, char **argv) { + if(argc != 3) { + return 0; + } + HMODULE module = LoadLibraryA(argv[1]); + if(!module) { + return 0; + } + FARPROC proc = GetProcAddress(module, argv[2]); + return (int)proc; +} diff --git a/autopunch-loader/go.mod b/autopunch-loader/go.mod new file mode 100644 index 0000000..5b07efd --- /dev/null +++ b/autopunch-loader/go.mod @@ -0,0 +1,14 @@ +module github.com/delthas/autopunch/autopunch-loader + +go 1.12 + +require ( + github.com/akutz/sortfold v0.2.1 + github.com/lxn/walk v0.0.0-20190605135739-7223200e844a + github.com/lxn/win v0.0.0-20190529120726-270e6e4be94d // indirect + github.com/machinebox/progress v0.2.0 + github.com/matryer/is v1.2.0 // indirect + golang.org/x/sys v0.0.0-20190608050228-5b15430b70e3 + golang.org/x/text v0.3.2 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect +) diff --git a/autopunch-loader/go.sum b/autopunch-loader/go.sum new file mode 100644 index 0000000..01309e3 --- /dev/null +++ b/autopunch-loader/go.sum @@ -0,0 +1,17 @@ +github.com/akutz/sortfold v0.2.1 h1:u9x3FC6oM+6gZKEVNRnmVafJgappwrv9YqpELQCYViI= +github.com/akutz/sortfold v0.2.1/go.mod h1:m1NArmessx+/3z2N8MiiTjq79A3WwZwDDiZ7eeD4jHA= +github.com/lxn/walk v0.0.0-20190605135739-7223200e844a h1:CtnHTpYxiq8reYXJtZd1eUor+NEN29ZLHHT3z3nFams= +github.com/lxn/walk v0.0.0-20190605135739-7223200e844a/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20190529120726-270e6e4be94d h1:SJyuddpDtiXMTcJmzC0OuphQdSZCabKW4iQWSE9FFR4= +github.com/lxn/win v0.0.0-20190529120726-270e6e4be94d/go.mod h1:oO6+4g3P1GcPAG7LPffwn8Ye0cxW0goh0sUZ6+lRFPs= +github.com/machinebox/progress v0.2.0 h1:7z8+w32Gy1v8S6VvDoOPPBah3nLqdKjr3GUly18P8Qo= +github.com/machinebox/progress v0.2.0/go.mod h1:hl4FywxSjfmkmCrersGhmJH7KwuKl+Ueq9BXkOny+iE= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +golang.org/x/sys v0.0.0-20190608050228-5b15430b70e3 h1:xUZPeCzQtkdgRi9RjXIA+3w3RdyDLPqiaJlza5Fqpog= +golang.org/x/sys v0.0.0-20190608050228-5b15430b70e3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= diff --git a/autopunch-loader/loader.c b/autopunch-loader/loader.c new file mode 100644 index 0000000..37dcae6 --- /dev/null +++ b/autopunch-loader/loader.c @@ -0,0 +1,13 @@ +#include "loader.h" + +BOOL cEnumWindowCallbackList(HWND handle, LPARAM data) { + void enumWindowCallbackList(void *handle, void *data); + enumWindowCallbackList((void*)handle, (void*)data); + return TRUE; +} + +BOOL cEnumWindowCallbackSetName(HWND handle, LPARAM data) { + void enumWindowCallbackSetName(void *handle, void *data); + enumWindowCallbackSetName((void*)handle, (void*)data); + return TRUE; +} diff --git a/autopunch-loader/loader.go b/autopunch-loader/loader.go new file mode 100644 index 0000000..86583e2 --- /dev/null +++ b/autopunch-loader/loader.go @@ -0,0 +1,745 @@ +// +build windows +//go:generate rsrc -arch=amd64 -manifest loader.manifest -o rsrc.syso +//go:generate packer autopunch.x86.dbg.dll dll86Dbg.go dllData86Dbg +//go:generate packer autopunch.x64.dbg.dll dll64Dbg.go dllData64Dbg +//go:generate packer autopunch.x86.rel.dll dll86Rel.go dllData86Rel +//go:generate packer autopunch.x64.rel.dll dll64Rel.go dllData64Rel +//go:generate packer ..\autopunch-address\address.exe address.go addressData + +package main + +/* +#include "loader.h" +*/ +import "C" +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "reflect" + "sort" + "strconv" + "strings" + "time" + "unicode/utf16" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/akutz/sortfold" + "github.com/lxn/walk" + . "github.com/lxn/walk/declarative" + + "github.com/machinebox/progress" +) + +const ( + DllName64Dbg = "autopunch.x64.dbg.dll" + DllName86Dbg = "autopunch.x86.dbg.dll" + DllName64Rel = "autopunch.x64.rel.dll" + DllName86Rel = "autopunch.x86.rel.dll" +) + +var processExcludes = []string{"explorer.exe", "firefox.exe", "cmd.exe", "mintty.exe"} +var processPreferreds = []string{"th123.exe"} + +var ( + dllKernel = windows.NewLazyDLL("kernel32.dll") + dllUser = windows.NewLazyDLL("user32.dll") + dllVersions = windows.NewLazyDLL("version.dll") + procQueryFullProcessImageName = dllKernel.NewProc("QueryFullProcessImageNameW") + procVirtualAllocEx = dllKernel.NewProc("VirtualAllocEx") + procVirtualFreeEx = dllKernel.NewProc("VirtualFreeEx") + procWriteProcessMemory = dllKernel.NewProc("WriteProcessMemory") + procCreateRemoteThread = dllKernel.NewProc("CreateRemoteThread") + procGetExitCodeThread = dllKernel.NewProc("GetExitCodeThread") + procVerQueryValue = dllVersions.NewProc("VerQueryValueW") + procGetFileVersionInfoSize = dllVersions.NewProc("GetFileVersionInfoSizeW") + procGetFileVersionInfo = dllVersions.NewProc("GetFileVersionInfoW") + procGetWindowThreadProcessId = dllUser.NewProc("GetWindowThreadProcessId") + procGetWindowTextLength = dllUser.NewProc("GetWindowTextLengthW") + procGetWindowText = dllUser.NewProc("GetWindowTextW") + procSetWindowText = dllUser.NewProc("SetWindowTextW") + procIsWindowVisible = dllUser.NewProc("IsWindowVisible") + procEnumWindows = dllUser.NewProc("EnumWindows") +) + +type processModel struct { + walk.ListModelBase + items []processItem +} + +func (m *processModel) ItemCount() int { + return len(m.items) +} + +func (m *processModel) Value(index int) interface{} { + return m.items[index].name +} + +type processItem struct { + name string + preferred bool + path string + pid uint32 +} + +type processItems []processItem + +func (p processItems) Len() int { + return len(p) +} + +func (p processItems) Less(i, j int) bool { + if p[i].preferred && !p[j].preferred { + return true + } + if !p[i].preferred && p[j].preferred { + return false + } + return sortfold.CompareFold(p[i].name, p[j].name) <= 0 +} + +func (p processItems) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} + +type module struct { + name string + description string + win map[uint32][]string // processId -> windows texts +} + +//export enumWindowCallbackList +func enumWindowCallbackList(handle unsafe.Pointer, data unsafe.Pointer) { + r, _, _ := procIsWindowVisible.Call(uintptr(handle)) + if r == 0 { + return + } + r, _, err := procGetWindowTextLength.Call(uintptr(handle)) + if r == 0 { + if err == windows.ERROR_SUCCESS { + return + } + panic(err) + } + l := int(r) + textUtf16 := make([]uint16, l+1) + r, _, err = procGetWindowText.Call(uintptr(handle), uintptr(unsafe.Pointer(&textUtf16[0])), uintptr(l+1)) + if r == 0 { + if err == windows.ERROR_SUCCESS { + return + } + panic(err) + } + text := string(utf16.Decode(textUtf16[:l])) + + var processId uint32 + r, _, err = procGetWindowThreadProcessId.Call(uintptr(handle), uintptr(unsafe.Pointer(&processId))) + if r == 0 { + panic(err) + } + wins := *(*map[uint32][]string)(unsafe.Pointer(data)) + wins[processId] = append(wins[processId], text) +} + +func refresh(model *processModel, lb *walk.ListBox) { + defer func() { + if r := recover(); r != nil { + dialog("Process listing failed", "Process listing failed! This should not happen.\nError: "+r.(error).Error(), walk.MsgBoxIconError) + mw.Close() + } + }() + + modules := make(map[string]module) // process path -> + wins := make(map[uint32][]string) // processId -> windows texts + + r, _, err := procEnumWindows.Call(uintptr(C.cEnumWindowCallbackList), uintptr(unsafe.Pointer(&wins))) + if r == 0 { + panic(err) + } + + handleSnapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + panic(err) + } + entry := windows.ProcessEntry32{ + Size: uint32(unsafe.Sizeof(windows.ProcessEntry32{})), + } + err = windows.Process32First(handleSnapshot, &entry) + if err != nil { + panic(err) + } +outer: + for windows.Process32Next(handleSnapshot, &entry) == nil { + win, ok := wins[entry.ProcessID] + if !ok { + continue + } + + exeNameLen := len(entry.ExeFile) + for i, c := range entry.ExeFile { + if c == 0 { + exeNameLen = i + break + } + } + exeName := string(utf16.Decode(entry.ExeFile[:exeNameLen])) + + for _, processExclude := range processExcludes { + if strings.EqualFold(processExclude, exeName) { + continue outer + } + } + + handleProcess, err := windows.OpenProcess(0x1FFFFF /* PROCESS_ALL_ACCESS */, false, entry.ProcessID) + if err != nil { // fails if run as administrator + continue + } + exePathUtf16 := make([]uint16, windows.MAX_PATH) + var exePathLen = uint32(len(exePathUtf16)) + r, _, err = procQueryFullProcessImageName.Call(uintptr(handleProcess), 0, uintptr(unsafe.Pointer(&exePathUtf16[0])), uintptr(unsafe.Pointer(&exePathLen))) + if r == 0 { + panic(err) + } + windows.CloseHandle(handleProcess) + exePath := string(utf16.Decode(exePathUtf16[:exePathLen])) + + m, ok := modules[exePath] + if !ok { + m = module{ + name: exeName, + win: make(map[uint32][]string), + } + + var handle uint32 + r, _, err := procGetFileVersionInfoSize.Call(uintptr(unsafe.Pointer(&exePathUtf16[0])), uintptr(unsafe.Pointer(&handle))) + if r != 0 { + l := int(r) + versionData := make([]byte, l) + r, _, err = procGetFileVersionInfo.Call(uintptr(unsafe.Pointer(&exePathUtf16[0])), uintptr(unsafe.Pointer(&handle)), uintptr(l), uintptr(unsafe.Pointer(&versionData[0]))) + if r == 0 { + panic(err) + } + var lang *struct { + language uint16 + codepage uint16 + } + subBlock := utf16.Encode([]rune("\\VarFileInfo\\Translation" + "\x00")) + var blockLen uint32 + r, _, err = procVerQueryValue.Call(uintptr(unsafe.Pointer(&versionData[0])), uintptr(unsafe.Pointer(&subBlock[0])), uintptr(unsafe.Pointer(&lang)), uintptr(unsafe.Pointer(&blockLen))) + if r != 0 && blockLen > 0 { + subBlock = utf16.Encode([]rune(fmt.Sprintf("\\StringFileInfo\\%04x%04x\\FileDescription"+"\x00", lang.language, lang.codepage))) + var descriptionUtf16 *uint16 + r, _, err = procVerQueryValue.Call(uintptr(unsafe.Pointer(&versionData[0])), uintptr(unsafe.Pointer(&subBlock[0])), uintptr(unsafe.Pointer(&descriptionUtf16)), uintptr(unsafe.Pointer(&blockLen))) + if r != 0 && blockLen > 1 { + description := string(utf16.Decode(*(*[]uint16)(unsafe.Pointer(&reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(descriptionUtf16)), + Len: int(blockLen - 1), + Cap: int(blockLen - 1), + })))) + m.description = description + } + } + } + if m.description == "" { + m.description = exeName + } + modules[exePath] = m + } + for _, v := range win { + m.win[entry.ProcessID] = append(m.win[entry.ProcessID], v) + } + } + + items := make([]processItem, 0, 16) + for path, m := range modules { + for pid, wins := range m.win { + for _, win := range wins { + preferred := false + for _, processPreferred := range processPreferreds { + if strings.EqualFold(processPreferred, m.name) { + preferred = true + break + } + } + items = append(items, processItem{ + name: m.description + " - " + win, + path: path, + pid: pid, + preferred: preferred, + }) + } + } + } + sort.Sort(processItems(items)) + model.items = items + mw.Synchronize(func() { + model.PublishItemsReset() + if len(items) > 0 { + lb.SetCurrentIndex(0) + } + }) +} + +func inject(pid uint32, debug bool) { + err, pretty := doInject(pid, debug) + if err == nil { + mw.Close() + return + } + dialog("Injection failed!", pretty+"\n"+err.Error(), walk.MsgBoxIconError) +} + +//export enumWindowCallbackSetName +func enumWindowCallbackSetName(handle unsafe.Pointer, data unsafe.Pointer) { + pid := *(*uint32)(data) + var windowPid uint32 + r, _, _ := procGetWindowThreadProcessId.Call(uintptr(handle), uintptr(unsafe.Pointer(&windowPid))) + if r == 0 { + return + } + if windowPid != pid { + return + } + r, _, _ = procIsWindowVisible.Call(uintptr(handle)) + if r == 0 { + return + } + r, _, _ = procGetWindowTextLength.Call(uintptr(handle)) + if r == 0 { + return + } + l := int(r) + textUtf16 := make([]uint16, l+1) + r, _, _ = procGetWindowText.Call(uintptr(handle), uintptr(unsafe.Pointer(&textUtf16[0])), uintptr(l+1)) + if r == 0 { + return + } + + text := string(utf16.Decode(textUtf16[:l])) + " + autopunch " + version + "\x00" + textUtf16 = utf16.Encode([]rune(text)) + _, _, _ = procSetWindowText.Call(uintptr(handle), uintptr(unsafe.Pointer(&textUtf16[0]))) +} + +func doInject(pid uint32, debug bool) (error, string) { + handleProcess, err := windows.OpenProcess(0x1FFFFF /* PROCESS_ALL_ACCESS */, false, pid) + if err != nil { + return err, "Failed opening process!" + } + defer windows.CloseHandle(handleProcess) + + var wow64 bool + err = windows.IsWow64Process(handleProcess, &wow64) + if err != nil { + return err, "Failed getting process bitness!" + } + var loadLibraryAddr uintptr + var dllPath string + if wow64 { + f, err := ioutil.TempFile("", "address-*.exe") + if err != nil { + return err, "Failed opening temporary address process file!" + } + _, err = f.Write(addressData) + if err != nil { + return err, "Failed writing to temporary address process file!" + } + f.Close() + addressPath := f.Name() + + cmd := exec.Command(addressPath, "kernel32.dll", "LoadLibraryW") + err = cmd.Start() + if err != nil { + return err, "Failed starting address process!" + } + err = cmd.Wait() + if exitErr, ok := err.(*exec.ExitError); !ok { + return err, "Failed running address process!" + } else { + code := exitErr.ExitCode() + if code == -1 { + return errors.New("address process failed to start (exit code -1)"), "Failed starting address process!" + } + if code == 0 { + return errors.New("address process failed finding address (exit code 0)"), "Failed finding library address!" + } + loadLibraryAddr = uintptr(code) + } + + if os.Getenv("AUTOPUNCH_DLL_FILE") == "1" { + var dllName string + if debug { + dllName = DllName86Dbg + } else { + dllName = DllName86Rel + } + var err error + dllPath, err = filepath.Abs(dllName) + if err != nil { + return err, "Failed finding path to local inject (x86) library!" + } + } else { + f, err := ioutil.TempFile("", "autopunch.*.dll") + if err != nil { + return err, "Failed opening temporary inject (x86) library file!" + } + var dllData []byte + if debug { + dllData = dllData86Dbg + } else { + dllData = dllData86Rel + } + _, err = f.Write(dllData) + if err != nil { + return err, "Failed writing to temporary inject (x86) library file!" + } + f.Close() + dllPath = f.Name() + } + } else { + loadLibraryAddr = dllKernel.NewProc("LoadLibraryW").Addr() + + if os.Getenv("AUTOPUNCH_DLL_FILE") == "1" { + var dllName string + if debug { + dllName = DllName64Dbg + } else { + dllName = DllName64Rel + } + var err error + dllPath, err = filepath.Abs(dllName) + if err != nil { + return err, "Failed finding path to local inject (x64) library!" + } + } else { + f, err := ioutil.TempFile("", "autopunch.*.dll") + if err != nil { + return err, "Failed opening temporary inject (x64) library file!" + } + var dllData []byte + if debug { + dllData = dllData64Dbg + } else { + dllData = dllData64Rel + } + _, err = f.Write(dllData) + if err != nil { + return err, "Failed writing to temporary inject (x64) library file!" + } + f.Close() + dllPath = f.Name() + } + } + + if _, err := os.Stat(dllPath); err != nil { + return err, "Failed finding temporary inject library file!" + } + dllPathC := utf16.Encode([]rune(dllPath + "\x00")) + + dllAddr, _, err := procVirtualAllocEx.Call(uintptr(handleProcess), 0, uintptr(len(dllPathC)*2), windows.MEM_COMMIT, windows.PAGE_READWRITE) + if dllAddr == 0 { + return err, "Failed allocating memory in process!" + } + defer procVirtualFreeEx.Call(uintptr(handleProcess), dllAddr, 0, windows.MEM_RELEASE) + r, _, err := procWriteProcessMemory.Call(uintptr(handleProcess), dllAddr, uintptr(unsafe.Pointer(&dllPathC[0])), uintptr(len(dllPathC)*2), 0) + if r == 0 { + return err, "Failed writing to process memory!" + } + r, _, err = procCreateRemoteThread.Call(uintptr(handleProcess), 0, 0, loadLibraryAddr, dllAddr, 0, 0) + if r == 0 { + return err, "Failed creating thread in process memory!" + } + handleThread := windows.Handle(r) + event, err := windows.WaitForSingleObject(handleThread, 15000) + if event == windows.WAIT_FAILED { + return err, "Failed waiting for thread!" + } + if event == uint32(windows.WAIT_TIMEOUT) { + return errors.New("WAIT_TIMEOUT"), "Failed while waiting for thread: timeout!" + } + if event != windows.WAIT_OBJECT_0 { + return err, "Failed waiting for thread: unknown error!" + } + defer windows.CloseHandle(handleThread) + var handleDll windows.Handle + r, _, err = procGetExitCodeThread.Call(uintptr(handleThread), uintptr(unsafe.Pointer(&handleDll))) + if r == 0 { + return err, "Failed getting thread exit code!" + } + if handleDll == 0 { + return errors.New("dll handle is nil"), "Failed loading library in process!" + } + + // ignore error, too late to show an error message + _, _, _ = procEnumWindows.Call(uintptr(C.cEnumWindowCallbackSetName), uintptr(unsafe.Pointer(&pid))) + + return nil, "" +} + +func update() bool { + httpClient := http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { + dialer := net.Dialer{Timeout: 5 * time.Second} + return dialer.DialContext(ctx, network, addr) + }, + }, + } + r, err := httpClient.Get("https://api.github.com/repos/delthas/autopunch/releases") + if err != nil { + // throw error even if the user is just disconnected from the internet + dialog("Warning", "Error while checking for updates.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + var releases []struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Assets []struct { + Name string `json:"name"` + DownloadUrl string `json:"browser_download_url"` + Size int64 `json:"size"` + } `json:"assets"` + } + var dw bytes.Buffer + dr := io.TeeReader(r.Body, &dw) + decoder := json.NewDecoder(dr) + err = decoder.Decode(&releases) + r.Body.Close() + if err != nil { + ioutil.ReadAll(dr) + var message struct { + Message string `json:"message"` + } + decoder = json.NewDecoder(&dw) + errMessage := decoder.Decode(&message) + if errMessage != nil { + dialog("Warning", "Error while processing updates information.\nError: "+err.Error(), walk.MsgBoxIconWarning) + } else { + dialog("Warning", "Error while processing updates information.\nError message: "+message.Message, walk.MsgBoxIconWarning) + } + return false + } + for _, v := range releases { + if v.TagName == version { + return false + } + for _, asset := range v.Assets { + r, err = httpClient.Get(asset.DownloadUrl) + if err != nil { + dialog("Warning", "Error while downloading update.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + f, err := ioutil.TempFile("", "") + if err != nil { + r.Body.Close() + dialog("Warning", "Error while creating file for downloading update.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + pr := progress.NewReader(r.Body) + + var done bool + var dw *walk.Dialog + var lb *walk.Label + var pb *walk.ProgressBar + mw.Synchronize(func() { + _, err = Dialog{ + AssignTo: &dw, + Title: "autopunch " + version + " (by delthas)", + MinSize: Size{Width: 300, Height: 150}, + Size: Size{Width: 400, Height: 200}, + Layout: VBox{}, + Children: []Widget{ + Label{ + AssignTo: &lb, + Text: "Update found! Downlading update...\nAutopunch will restart itself automatically when finished.", + }, + ProgressBar{ + AssignTo: &pb, + MinValue: 0, + MaxValue: 100000, + }, + }, + }.Run(mw) + if !done { + os.Exit(0) // good enough for now + } + }) + go func() { + ctx := context.Background() + progressChan := progress.NewTicker(ctx, pr, asset.Size, 100*time.Millisecond) + for p := range progressChan { + if pb != nil { + pb.Synchronize(func() { + pb.SetValue(int(p.Percent() * float64(pb.MaxValue()) / 100)) + }) + } + if lb != nil { + text := fmt.Sprintf("Update found! Downlading update, remaining: %v\nAutopunch will restart itself automatically when finished.", p.Remaining().Round(time.Second)) + lb.Synchronize(func() { + lb.SetText(text) + }) + } + } + }() + _, err = io.Copy(f, pr) + done = true + r.Body.Close() + f.Close() + dw.Close(0) + mw.SetVisible(false) + if err != nil { + dialog("Warning", "Error while downloading update to file.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + + renamePath := "" + for i := 0; i < 10; i++ { + renamePath = filepath.Join(os.TempDir(), "autopunch.old."+strconv.Itoa(1000000000 + rand.Intn(1000000000))[1:]+".exe") + err = os.Rename(autopunchPath, renamePath) + if err == nil { + break + } + } + if err != nil { + dialog("Warning", "Error while updating, when moving current file.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + + err = os.Rename(f.Name(), autopunchPath) + if err != nil { + // try moving the old file back in case of error + _ = os.Rename(renamePath, autopunchPath) + dialog("Warning", "Error while updating, when moving downloaded file.\nError: "+err.Error(), walk.MsgBoxIconWarning) + return false + } + + go func() { + cmd := exec.Command(autopunchPath, os.Args[1:]...) + cmd.Env = append(os.Environ(), "AUTOPUNCH_OLD="+renamePath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + }() + return true + } + } + return false +} + +func dialog(title string, description string, style walk.MsgBoxStyle) { + if mw != nil && mw.Visible() { + walk.MsgBox(mw, title, description, style) + } else { + walk.MsgBox(nil, title, description, style) + } +} + +const customVersion = "[Custom Build]" + +var version string +var autopunchPath string + +var mw *walk.MainWindow + +func main() { + rand.Seed(time.Now().UnixNano()) + exe, err := os.Executable() + if err != nil { + dialog("Warning", "Finding autopunch file failed! The game won't be able to update.", walk.MsgBoxIconWarning) + } else { + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + dialog("Warning", "Finding autopunch file failed (resolving symlinks)! The game won't be able to update.", walk.MsgBoxIconWarning) + } else { + autopunchPath = exe + processExcludes = append(processExcludes, filepath.Base(exe)) + } + } + + if oldPath := os.Getenv("AUTOPUNCH_OLD"); oldPath != "" { + // cleanup old update file, ignore error + _ = os.Remove(oldPath) + } + + model := &processModel{} + + var cb *walk.CheckBox + var lb *walk.ListBox + + if version == "" { + version = customVersion + } + + err = MainWindow{ + AssignTo: &mw, + Visible: false, + Title: "autopunch " + version + " (by delthas)", + MinSize: Size{Width: 600, Height: 400}, + Size: Size{Width: 800, Height: 600}, + Layout: VBox{}, + Children: []Widget{ + ListBox{ + AssignTo: &lb, + Model: model, + OnItemActivated: func() { + go func() { + inject(model.items[lb.CurrentIndex()].pid, cb.Checked()) + }() + }, + }, + PushButton{ + Text: "Refresh", + OnClicked: func() { + go func() { + refresh(model, lb) + model.ItemsReset() + }() + }, + }, + PushButton{ + Text: "Punch!", + OnClicked: func() { + go func() { + inject(model.items[lb.CurrentIndex()].pid, cb.Checked()) + }() + }, + }, + CheckBox{ + AssignTo: &cb, + Text: "Debug Logs (if you have issues)", + Checked: false, + }, + }, + }.Create() + if err != nil { + panic(err) + } + + go func() { + if version != customVersion && autopunchPath != "" { + if update() { + mw.Close() + return + } + } + + refresh(model, lb) + mw.Synchronize(func() { + mw.SetVisible(true) + }) + }() + + mw.Run() +} diff --git a/autopunch-loader/loader.h b/autopunch-loader/loader.h new file mode 100644 index 0000000..d7d04e0 --- /dev/null +++ b/autopunch-loader/loader.h @@ -0,0 +1,10 @@ +#ifndef LOADER_H +#define LOADER_H + +#define WIN32_LEAN_AND_MEAN +#include + +BOOL cEnumWindowCallbackList(HWND handle, LPARAM data); +BOOL cEnumWindowCallbackSetName(HWND handle, LPARAM data); + +#endif //LOADER_H diff --git a/autopunch-loader/loader.manifest b/autopunch-loader/loader.manifest new file mode 100644 index 0000000..13f8ef4 --- /dev/null +++ b/autopunch-loader/loader.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + diff --git a/autopunch-packer/go.mod b/autopunch-packer/go.mod new file mode 100644 index 0000000..b716f9f --- /dev/null +++ b/autopunch-packer/go.mod @@ -0,0 +1,3 @@ +module github.com/delthas/autopunch/autopunch-packer + +go 1.12 diff --git a/autopunch-packer/packer.go b/autopunch-packer/packer.go new file mode 100644 index 0000000..29f3666 --- /dev/null +++ b/autopunch-packer/packer.go @@ -0,0 +1,69 @@ +package main + +import ( + "bufio" + "io" + "os" + "text/template" +) + +func main() { + if len(os.Args) != 4 { + panic("syntax: ") + } + r, err := os.Open(os.Args[1]) + if err != nil { + panic(err) + } + defer r.Close() + br := bufio.NewReader(r) + w, err := os.Create(os.Args[2]) + if err != nil { + panic(err) + } + defer w.Close() + bw := bufio.NewWriter(w) + defer bw.Flush() + + err = template.Must(template.New("").Parse(prefix)).Execute(bw, struct { + Name string + }{ + Name: os.Args[3], + }) + if err != nil { + panic(err) + } + + for { + b, err := br.ReadByte() + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + bw.WriteString(hex[b]) + } + + err = template.Must(template.New("").Parse(suffix)).Execute(bw, struct { + Name string + }{ + Name: os.Args[3], + }) + if err != nil { + panic(err) + } +} + +const prefix = `// Code generated by autopunch-packer. DO NOT EDIT. + +package main + +var {{.Name}} = []byte("` + +const suffix = `") + +var _ = {{.Name}} +` + +var hex = []string{"\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07", "\\x08", "\\x09", "\\x0A", "\\x0B", "\\x0C", "\\x0D", "\\x0E", "\\x0F", "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17", "\\x18", "\\x19", "\\x1A", "\\x1B", "\\x1C", "\\x1D", "\\x1E", "\\x1F", "\\x20", "\\x21", "\\x22", "\\x23", "\\x24", "\\x25", "\\x26", "\\x27", "\\x28", "\\x29", "\\x2A", "\\x2B", "\\x2C", "\\x2D", "\\x2E", "\\x2F", "\\x30", "\\x31", "\\x32", "\\x33", "\\x34", "\\x35", "\\x36", "\\x37", "\\x38", "\\x39", "\\x3A", "\\x3B", "\\x3C", "\\x3D", "\\x3E", "\\x3F", "\\x40", "\\x41", "\\x42", "\\x43", "\\x44", "\\x45", "\\x46", "\\x47", "\\x48", "\\x49", "\\x4A", "\\x4B", "\\x4C", "\\x4D", "\\x4E", "\\x4F", "\\x50", "\\x51", "\\x52", "\\x53", "\\x54", "\\x55", "\\x56", "\\x57", "\\x58", "\\x59", "\\x5A", "\\x5B", "\\x5C", "\\x5D", "\\x5E", "\\x5F", "\\x60", "\\x61", "\\x62", "\\x63", "\\x64", "\\x65", "\\x66", "\\x67", "\\x68", "\\x69", "\\x6A", "\\x6B", "\\x6C", "\\x6D", "\\x6E", "\\x6F", "\\x70", "\\x71", "\\x72", "\\x73", "\\x74", "\\x75", "\\x76", "\\x77", "\\x78", "\\x79", "\\x7A", "\\x7B", "\\x7C", "\\x7D", "\\x7E", "\\x7F", "\\x80", "\\x81", "\\x82", "\\x83", "\\x84", "\\x85", "\\x86", "\\x87", "\\x88", "\\x89", "\\x8A", "\\x8B", "\\x8C", "\\x8D", "\\x8E", "\\x8F", "\\x90", "\\x91", "\\x92", "\\x93", "\\x94", "\\x95", "\\x96", "\\x97", "\\x98", "\\x99", "\\x9A", "\\x9B", "\\x9C", "\\x9D", "\\x9E", "\\x9F", "\\xA0", "\\xA1", "\\xA2", "\\xA3", "\\xA4", "\\xA5", "\\xA6", "\\xA7", "\\xA8", "\\xA9", "\\xAA", "\\xAB", "\\xAC", "\\xAD", "\\xAE", "\\xAF", "\\xB0", "\\xB1", "\\xB2", "\\xB3", "\\xB4", "\\xB5", "\\xB6", "\\xB7", "\\xB8", "\\xB9", "\\xBA", "\\xBB", "\\xBC", "\\xBD", "\\xBE", "\\xBF", "\\xC0", "\\xC1", "\\xC2", "\\xC3", "\\xC4", "\\xC5", "\\xC6", "\\xC7", "\\xC8", "\\xC9", "\\xCA", "\\xCB", "\\xCC", "\\xCD", "\\xCE", "\\xCF", "\\xD0", "\\xD1", "\\xD2", "\\xD3", "\\xD4", "\\xD5", "\\xD6", "\\xD7", "\\xD8", "\\xD9", "\\xDA", "\\xDB", "\\xDC", "\\xDD", "\\xDE", "\\xDF", "\\xE0", "\\xE1", "\\xE2", "\\xE3", "\\xE4", "\\xE5", "\\xE6", "\\xE7", "\\xE8", "\\xE9", "\\xEA", "\\xEB", "\\xEC", "\\xED", "\\xEE", "\\xEF", "\\xF0", "\\xF1", "\\xF2", "\\xF3", "\\xF4", "\\xF5", "\\xF6", "\\xF7", "\\xF8", "\\xF9", "\\xFA", "\\xFB", "\\xFC", "\\xFD", "\\xFE", "\\xFF"} diff --git a/autopunch-relay/relay.go b/autopunch-relay/relay.go new file mode 100644 index 0000000..e965541 --- /dev/null +++ b/autopunch-relay/relay.go @@ -0,0 +1,155 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "log" + "net" + "time" +) + +const flushInterval = 15 * time.Second + +type connection struct { + port int + natPort int + time time.Time +} + +var connections = make(map[[4]byte][]connection) + +const version = "0.0.1" + +const defaultPort = 14763 + +func main() { + fmt.Println("autopunch relay v" + version) + fmt.Println() + + var port int + flag.IntVar(&port, "port", defaultPort, "relay listen port") + flag.Parse() + + c, err := net.ListenUDP("udp4", &net.UDPAddr{ + Port: port, + }) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + flushTime := time.Now() + + buffer := make([]byte, 4096) + for { + now := time.Now() + if now.Sub(flushTime) > flushInterval { + flushTime = now + for k, v := range connections { + for i := 0; i < len(v); i++ { + c := v[i] + if now.Sub(c.time) > flushInterval { + v[i] = v[len(v)-1] + i-- + v = v[:len(v)-1] + } + } + if len(v) == 0 { + delete(connections, k) + } + } + } + n, addr, err := c.ReadFromUDP(buffer) + if err != nil { + // err is thrown if the buffer is too small + continue + } + if n != 2 && n != 8 { + continue + } + var senderIp [4]byte + if senderIpSlice := addr.IP.To4(); senderIpSlice == nil { + continue + } else { + copy(senderIp[:], senderIpSlice) + } + if n == 2 { + senderPort := int(binary.BigEndian.Uint16(buffer[:2])) + addConnection(senderIp, senderPort, addr.Port) + } else if n == 8 { + senderPort := int(binary.BigEndian.Uint16(buffer[:2])) + addConnection(senderIp, senderPort, addr.Port) + + var ip [4]byte + copy(ip[:], buffer[2:6]) + v, ok := connections[ip] + if !ok { + continue + } + port := int(binary.BigEndian.Uint16(buffer[6:8])) + + var target *connection + + for _, conn := range v { + if conn.port == port { // client sending to internal server port + target = &conn + } + } + if target == nil { + for _, conn := range v { + if conn.natPort == port { // client sending to external server port + target = &conn + } + } + } + if target == nil { // client sending to unknown address + continue + } + + var payload [8]byte + + // send nat mapping to client + payload[0] = byte(target.port >> 8) + payload[1] = byte(target.port) + payload[2] = byte(target.natPort >> 8) + payload[3] = byte(target.natPort) + copy(payload[4:8], ip[:]) + c.WriteToUDP(payload[:], addr) + + // send nat mapping to server + payload[0] = byte(senderPort >> 8) + payload[1] = byte(senderPort) + payload[2] = byte(addr.Port >> 8) + payload[3] = byte(addr.Port) + copy(payload[4:8], senderIp[:]) + c.WriteToUDP(payload[:], &net.UDPAddr{ + IP: net.IP(ip[:]), + Port: target.natPort, + }) + } + } +} + +func addConnection(ip [4]byte, port int, natPort int) { + var v []connection + if c, ok := connections[ip]; !ok { + v = make([]connection, 0, 4) + connections[ip] = v + } else { + v = c + } + for i, c := range v { + if c.port != port { + continue + } + v[i].natPort = natPort + v[i].time = time.Now() + return + } + connections[ip] = append(v, connection{ + port: port, + natPort: natPort, + time: time.Now(), + }) +} diff --git a/detours b/detours new file mode 160000 index 0000000..e4bfd6b --- /dev/null +++ b/detours @@ -0,0 +1 @@ +Subproject commit e4bfd6b03e50de46b47abfbd1e46b384f0c5f833 diff --git a/doc/screen.jpg b/doc/screen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ccf568b1e99b45dc799ebf5dcd9ca6a55297d03e GIT binary patch literal 44354 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 R9P0MD^RD5*%=<6B{|B-93AO+L literal 0 HcmV?d00001 diff --git a/inject.c b/inject.c new file mode 100644 index 0000000..6cfdf19 --- /dev/null +++ b/inject.c @@ -0,0 +1,495 @@ +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NDEBUG + #define DEBUG 1 + #define DEBUG_LOG(fmt, ...) \ + fprintf(debug, "%d: " fmt "\n", __LINE__, ##__VA_ARGS__); \ + fflush(debug); + #define DEBUG_ADDR(fmt, addr, ...) \ + fprintf(debug, "%d: " fmt " %d.%d.%d.%d\n", __LINE__, ##__VA_ARGS__, addr.S_un.S_un_b.s_b1, addr.S_un.S_un_b.s_b2, addr.S_un.S_un_b.s_b3, addr.S_un.S_un_b.s_b4); \ + fflush(debug); +#else + #define DEBUG 0 + #define DEBUG_LOG(fmt, ...); + #define DEBUG_ADDR(fmt, addr, ...); +#endif + +#define WARN(fmt, ...) { \ + size_t needed = _snwprintf(NULL, 0, fmt, ##__VA_ARGS__); \ + wchar_t *buf = malloc((needed + 1) * 2); \ + _snwprintf(buf, (needed + 1), fmt, ##__VA_ARGS__); \ + MessageBoxW(NULL, buf, L"autopunch", MB_ICONWARNING | MB_OK); \ + free(buf); \ +} + +const int relay_ip = (188 << 24) | (226 << 16) | (135 << 8) | 111; +const int relay_port = 14763; +struct sockaddr_in relay_addr; +const char punch_payload[] = {0}; + +FILE *debug; + +struct mapping { + struct sockaddr_in addr; + u_short port; + clock_t last_send; + clock_t last_refresh; + bool refresh; +}; + +struct transient_peer { + struct sockaddr_in addr; + clock_t last; +}; + +struct socket_data { + SOCKET s; + HANDLE mutex; + u_short port; + bool closed; + struct mapping *mappings; + size_t mappings_len; + size_t mappings_cap; + struct transient_peer *transient_peers; + size_t transient_peers_len; + size_t transient_peers_cap; +}; + +struct socket_data *sockets; +size_t sockets_len; +size_t sockets_cap; +HANDLE sockets_mutex; + +HANDLE relay_thread; +bool relay_close; + +int(WINAPI *actual_recvfrom)(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen) = recvfrom; +int(WINAPI *actual_sendto)(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen) = sendto; +int(WINAPI *actual_bind)(SOCKET s, const struct sockaddr *name, int namelen) = bind; +int(WINAPI *actual_closesocket)(SOCKET s) = closesocket; + +DWORD WINAPI relay(void *data) { + while (!relay_close) { + WaitForSingleObject(sockets_mutex, INFINITE); + clock_t now = clock(); + for (int i = 0; i < sockets_len; ++i) { + struct socket_data *socket_data = &sockets[i]; + WaitForSingleObject(socket_data->mutex, INFINITE); + if (socket_data->closed) { + DEBUG_LOG("relay remove closed socket socket=%zu mappings=%zu closed=%d, len=%zu cap=%zu", socket_data->s, (size_t)socket_data->mappings, socket_data->closed, + socket_data->mappings_len, socket_data->mappings_cap) + ReleaseMutex(socket_data->mutex); + CloseHandle(socket_data->mutex); + free(socket_data->mappings); + free(socket_data->transient_peers); + sockets[i--] = sockets[--sockets_len]; + continue; + } + char buf[] = {((char*)&(socket_data->port))[0], ((char*)&(socket_data->port))[1]}; + DEBUG_LOG("relay sendto i=%d local port=%u socket=%zu %d %d", i, ntohs(socket_data->port), socket_data->s, buf[0], buf[1]) + actual_sendto(socket_data->s, buf, sizeof(buf), 0, (struct sockaddr *)&relay_addr, sizeof(relay_addr)); + for (int j = 0; j < socket_data->mappings_len; ++j) { + struct mapping *mapping = &socket_data->mappings[j]; + clock_t wait_refresh = (now - mapping->last_refresh) / CLOCKS_PER_SEC; + DEBUG_LOG("relay mappinglist i=%d j=%d wait_refresh=%ld", i, j, wait_refresh) + if (wait_refresh > 10) { // drop old mapping + DEBUG_LOG("relay mappinglist timeout %d %d %ld", i, j, wait_refresh) + socket_data->mappings[j--] = socket_data->mappings[--socket_data->mappings_len]; + continue; + } + clock_t wait_send = (now - mapping->last_send) / CLOCKS_PER_SEC; + if (mapping->refresh && wait_send > 1) { // refresh mapping + DEBUG_LOG("relay mappinglist refresh i=%d j=%d wait=%ld socket=%zu", i, j, wait_send, socket_data->s) + actual_sendto(socket_data->s, punch_payload, sizeof(punch_payload), 0, (struct sockaddr *)&mapping->addr, sizeof(mapping->addr)); + mapping->last_send = clock(); + } + } + ReleaseMutex(socket_data->mutex); + } + ReleaseMutex(sockets_mutex); + if (relay_close) { + return 0; + } + Sleep(500); + } + return 0; +} + +int WINAPI my_recvfrom(SOCKET s, char *out_buf, int len, int flags, struct sockaddr *from, int *fromlen) { + struct sockaddr_in *addr = (struct sockaddr_in *)from; + struct socket_data *socket_data = NULL; + WaitForSingleObject(sockets_mutex, INFINITE); + for (int i = 0; i < sockets_len; ++i) { + socket_data = &sockets[i]; + if (socket_data->s == s) { + break; + } + } + ReleaseMutex(sockets_mutex); + if (!socket_data) { + DEBUG_LOG("recvfrom error unknown socket") + return actual_recvfrom(s, out_buf, len, flags, from, fromlen); // TODO error handling + } + + char *buf; + if(len < 8) { + buf = malloc(8); + } else { + buf = out_buf; + } + DEBUG_LOG("recvfrom_start len=%d flags=%d", len, flags) + while (true) { + int n = actual_recvfrom(s, buf, len > 8 ? len : 8, flags, from, fromlen); + DEBUG_LOG("recvfrom actual n=%d", n) + if (n < 0) { + int err = WSAGetLastError(); + if (err == WSAECONNRESET) { // ignore connection reset errors (can happen if relay is down) + DEBUG_LOG("recvfrom skipped error=%d", err) + continue; + } + DEBUG_LOG("recvfrom error=%d", err) + return err; + } + if (addr->sin_addr.s_addr == relay_addr.sin_addr.s_addr && addr->sin_port == relay_addr.sin_port) { + if (n % 8) { + DEBUG_LOG("recvfrom error receive size n=%d", n) + continue; + } + WaitForSingleObject(socket_data->mutex, INFINITE); + clock_t now = clock(); + u_short port_internal = *((u_short*)(&buf[0])); + u_short port = *((u_short*)(&buf[2])); + struct in_addr addr = {.s_addr = *(u_long*)(&buf[4])}; + DEBUG_LOG("recvfrom mapping port=%d nat_port=%d", ntohs(port_internal), ntohs(port)) + for (int j = 0; j < socket_data->mappings_len; ++j) { + struct mapping *mapping = &socket_data->mappings[j]; + if (mapping->addr.sin_addr.s_addr != addr.s_addr) { + continue; + } + if (mapping->port != port_internal) { + continue; + } + DEBUG_LOG("recvfrom mapping replaced old_port=%d old_nat_port=%d", ntohs(mapping->port), ntohs(mapping->addr.sin_port)) + mapping->addr.sin_port = port; + mapping->last_refresh = now; + mapping->last_send = now; + goto outer; + } + if (socket_data->mappings_len == socket_data->mappings_cap) { + socket_data->mappings_cap = socket_data->mappings_cap ? socket_data->mappings_cap * 2 : 8; + socket_data->mappings = realloc(socket_data->mappings, socket_data->mappings_cap * sizeof(socket_data->mappings[0])); + } + socket_data->mappings[socket_data->mappings_len++] = (struct mapping) { + .addr = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = port, + .sin_addr.s_addr = addr.s_addr, + }, + .port = port_internal, + .last_refresh = now, + .last_send = now, + .refresh = true, + }; + DEBUG_LOG("recvfrom mapping added mappings_len=%zu mappings_cap=%zu", socket_data->mappings_len, socket_data->mappings_cap) + outer:; + ReleaseMutex(socket_data->mutex); + continue; + } + + WaitForSingleObject(socket_data->mutex, INFINITE); + for (int i = 0; i < socket_data->transient_peers_len; ++i) { + struct transient_peer *peer = &socket_data->transient_peers[i]; + if (peer->addr.sin_addr.s_addr != addr->sin_addr.s_addr) { + continue; + } + if (peer->addr.sin_port != addr->sin_port) { + DEBUG_LOG("recvfrom transient port mismatch transient=%d dest=%d", ntohs(peer->addr.sin_port), ntohs(addr->sin_port)) + continue; + } + socket_data->transient_peers[i--] = socket_data->transient_peers[--socket_data->transient_peers_len]; + + if (socket_data->mappings_len == socket_data->mappings_cap) { + socket_data->mappings_cap = socket_data->mappings_cap ? socket_data->mappings_cap * 2 : 8; + socket_data->mappings = realloc(socket_data->mappings, socket_data->mappings_cap * sizeof(socket_data->mappings[0])); + } + clock_t now = clock(); + socket_data->mappings[socket_data->mappings_len++] = (struct mapping) { + .addr = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = addr->sin_port, + .sin_addr.s_addr = addr->sin_addr.s_addr, + }, + .port = addr->sin_port, + .last_refresh = now, + .last_send = now, + .refresh = false, + }; + DEBUG_LOG("recvfrom matched transient added mappings_len=%zu mappings_cap=%zu", socket_data->mappings_len, socket_data->mappings_cap) + } + + if(n == sizeof(punch_payload) && !memcmp(punch_payload, buf, n)) { + DEBUG_LOG("recvfrom skipped received punch payload") + continue; + } + + for (int i = 0; i < socket_data->mappings_len; ++i) { + struct mapping *mapping = &socket_data->mappings[i]; + if (mapping->addr.sin_addr.s_addr != addr->sin_addr.s_addr) { + continue; + } + if (mapping->addr.sin_port != addr->sin_port) { + DEBUG_LOG("recvmfrom mapping port mismatch mapping=%d dest=%d", ntohs(mapping->addr.sin_port), ntohs(addr->sin_port)) + continue; + } + DEBUG_LOG("recvmfrom mapping used old=%d replaced=%d", ntohs(addr->sin_port), ntohs(mapping->addr.sin_port)) + addr->sin_port = mapping->port; + break; + } + ReleaseMutex(socket_data->mutex); + + DEBUG_ADDR("recvfrom actualdata n=%d buf[0]=%d from=%d@", addr->sin_addr, n, buf[0], addr->sin_port) + if(out_buf != buf) { + memcpy(out_buf, buf, n); + } + return n; + } +} + +int WINAPI my_sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen) { + if(len) { + DEBUG_LOG("sendto start socket=%zu len=%d buf[0]=%d", s, len, buf[0]) + } else { + DEBUG_LOG("sendto start socket=%zu len=%d (empty)", s, len) + } + if (to->sa_family != AF_INET) { + DEBUG_LOG("sendto mapping sa_family != AF_INET: %d", to->sa_family) + return actual_sendto(s, buf, len, flags, to, tolen); + } + struct socket_data *socket_data = NULL; + WaitForSingleObject(sockets_mutex, INFINITE); + for (int i = 0; i < sockets_len; ++i) { + socket_data = &sockets[i]; + if (socket_data->s == s) { + break; + } + } + ReleaseMutex(sockets_mutex); + if (!socket_data) { + DEBUG_LOG("sendto error unknown socket") + return actual_sendto(s, buf, len, flags, to, tolen); // TODO error? + } + WaitForSingleObject(socket_data->mutex, INFINITE); + const struct sockaddr_in *dest = (const struct sockaddr_in *)to; + for (int i = 0; i < socket_data->mappings_len; ++i) { + struct mapping *mapping = &socket_data->mappings[i]; + if (mapping->addr.sin_addr.s_addr != dest->sin_addr.s_addr) { + continue; + } + if (mapping->port != dest->sin_port) { + DEBUG_LOG("sendto mapping port mismatch mapping=%d dest=%d", ntohs(mapping->port), ntohs(dest->sin_port)) + continue; + } + clock_t now = clock(); + mapping->last_refresh = now; + mapping->last_send = now; + DEBUG_LOG("sendto mapping used old=%d replaced=%d", ntohs(dest->sin_port), ntohs(mapping->addr.sin_port)) + int r = actual_sendto(s, buf, len, flags, (struct sockaddr *)mapping, tolen); + ReleaseMutex(socket_data->mutex); + return r; + } + DEBUG_LOG("sendto unknown mapping for port=%d sending to peer and relay", ntohs(dest->sin_port)) + int r = actual_sendto(s, buf, len, flags, to, tolen); + + int refresh = -1; + clock_t now = clock(); + for (int i = 0; i < socket_data->transient_peers_len; ++i) { + struct transient_peer *peer = &socket_data->transient_peers[i]; + if((now - peer->last) / CLOCKS_PER_SEC > 10) { // transient peer timeout + socket_data->transient_peers[i--] = socket_data->transient_peers[--socket_data->transient_peers_len]; + continue; + } + if (peer->addr.sin_addr.s_addr != dest->sin_addr.s_addr) { + continue; + } + if (peer->addr.sin_port != dest->sin_port) { + DEBUG_LOG("sendto transient port mismatch transient=%d dest=%d", ntohs(peer->addr.sin_port), ntohs(dest->sin_port)) + continue; + } + refresh = (now - peer->last) * 1000 / CLOCKS_PER_SEC > 500; + if(refresh) { + peer->last = now; + } + DEBUG_LOG("sendto transient found, refresh=%d now=%ld last=%ld", refresh, now, peer->last) + } + if(refresh == -1) { + if (socket_data->transient_peers_len == socket_data->transient_peers_cap) { + socket_data->transient_peers_cap = socket_data->transient_peers_cap ? socket_data->transient_peers_cap * 2 : 8; + socket_data->transient_peers = realloc(socket_data->transient_peers, socket_data->transient_peers_cap * sizeof(socket_data->transient_peers[0])); + } + socket_data->transient_peers[socket_data->transient_peers_len++] = (struct transient_peer) { + .addr = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = dest->sin_port, + .sin_addr.s_addr = dest->sin_addr.s_addr, + }, + .last = now, + }; + refresh = true; + DEBUG_LOG("sendto transient created len=%zu cap=%zu", socket_data->transient_peers_len, socket_data->transient_peers_cap) + } + + if(refresh) { + char relay_buf[] = {((char*)&(socket_data->port))[0], ((char*)&(socket_data->port))[1], dest->sin_addr.S_un.S_un_b.s_b1, dest->sin_addr.S_un.S_un_b.s_b2, + dest->sin_addr.S_un.S_un_b.s_b3, dest->sin_addr.S_un.S_un_b.s_b4, ((char*)&(dest->sin_port))[0], ((char*)&(dest->sin_port))[1]}; + actual_sendto(s, relay_buf, sizeof(relay_buf), 0, (struct sockaddr *)&relay_addr, sizeof(relay_addr)); + DEBUG_LOG("sendto transient refreshed; sent to relay") + } + ReleaseMutex(socket_data->mutex); + return r; +} + +int WINAPI my_bind(SOCKET s, const struct sockaddr *name, int namelen) { + DEBUG_LOG("bind start for socket=%zu", s) + if (name) { + DEBUG_ADDR("bind request ip is %d @ ", ((const struct sockaddr_in *)name)->sin_addr, ntohs(((const struct sockaddr_in *)name)->sin_port)) + } else { + DEBUG_LOG("bind request ip is null") + } + int r = actual_bind(s, name, namelen); + if (r) { + DEBUG_LOG("bind actual failed %d", r) + return r; + } + struct sockaddr_in local_addr; + socklen_t local_addr_len = sizeof(local_addr); + if (getsockname(s, (struct sockaddr *)&local_addr, &local_addr_len)) { + DEBUG_LOG("bind getsockname error %d", WSAGetLastError()) + return r; + } + if (local_addr.sin_family != AF_INET) { + DEBUG_LOG("bind getsockname socket is not AF_INET %d", local_addr.sin_family) + return r; + } + DEBUG_LOG("bind local port is %d %d", local_addr.sin_family, ntohs(local_addr.sin_port)) + WaitForSingleObject(sockets_mutex, INFINITE); + if (sockets_len == sockets_cap) { + sockets_cap = sockets_cap ? sockets_cap * 2 : 8; + sockets = realloc(sockets, sockets_cap * sizeof(sockets[0])); + } + sockets[sockets_len++] = (struct socket_data){ + .s = s, + .port = local_addr.sin_port, + .mutex = CreateMutex(NULL, FALSE, NULL), + }; + DEBUG_LOG("bind add socket socket=%zu mappings=%zu closed=%d, len=%zu cap=%zu", sockets[sockets_len - 1].s, (size_t)sockets[sockets_len - 1].mappings, + sockets[sockets_len - 1].closed, sockets[sockets_len - 1].mappings_len, sockets[sockets_len - 1].mappings_cap) + DEBUG_LOG("bind added socket %zu, len=%zu cap=%zu", s, sockets_len, sockets_cap) + ReleaseMutex(sockets_mutex); + return r; +} + +int WSAAPI my_closesocket(SOCKET s) { + DEBUG_LOG("close start socket=%zu", s) + int r = actual_closesocket(s); + if (r) { + DEBUG_LOG("close error=%d", WSAGetLastError()); + } + struct socket_data *socket_data = NULL; + WaitForSingleObject(sockets_mutex, INFINITE); + for (int i = 0; i < sockets_len; ++i) { + socket_data = &sockets[i]; + if (socket_data->s == s) { + break; + } + } + ReleaseMutex(sockets_mutex); + if (!socket_data) { + DEBUG_LOG("close error unknown socket, err=%d", r) + return r; + } + WaitForSingleObject(socket_data->mutex, INFINITE); + socket_data->closed = true; + ReleaseMutex(socket_data->mutex); + DEBUG_LOG("close end") + return r; +} + +const wchar_t *inject_log_prefix = L"\\inject."; +const wchar_t *inject_log_suffix = L"\\.log"; + +void load() { + if(DEBUG) { + srand(time(NULL)); + wchar_t *desktop_path = malloc((MAX_PATH + 1) * 2); + SHGetSpecialFolderPathW(HWND_DESKTOP, desktop_path, CSIDL_DESKTOP, FALSE); + wchar_t *path = malloc((MAX_PATH + 1) * 2); + _snwprintf(path, MAX_PATH + 1, L"%ls\\inject.%d.log", desktop_path, rand() % 1000000); + free(desktop_path); + WARN(L"Injected autopunch with debug!\nPath to debug is: %ls", path) + debug = _wfopen(path, L"w"); + free(path); + } + + DEBUG_LOG("load_start") + + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourAttach((void **)&actual_recvfrom, my_recvfrom); + DetourAttach((void **)&actual_sendto, my_sendto); + DetourAttach((void **)&actual_bind, my_bind); + DetourAttach((void **)&actual_closesocket, my_closesocket); + DetourTransactionCommit(); + + u_long relay_ip_net = htonl(relay_ip); + u_short relay_port_net = htons(relay_port); + relay_addr = (struct sockaddr_in){.sin_family = AF_INET, .sin_port = relay_port_net, .sin_addr.s_addr = relay_ip_net}; + + sockets_mutex = CreateMutex(NULL, FALSE, NULL); + relay_thread = CreateThread(NULL, 0, relay, NULL, 0, NULL); + + DEBUG_LOG("load_end") +} + +void unload() { + DEBUG_LOG("unload_start") + + relay_close = true; + WaitForSingleObject(sockets_mutex, INFINITE); + CloseHandle(relay_thread); + ReleaseMutex(sockets_mutex); + CloseHandle(sockets_mutex); + DEBUG_LOG("unload_free %zu %zu %zu", sockets_len, sockets_cap, (size_t)sockets) + free(sockets); + + DEBUG_LOG("unload_detours") + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourDetach((void **)&actual_recvfrom, my_recvfrom); + DetourDetach((void **)&actual_sendto, my_sendto); + DetourDetach((void **)&actual_bind, my_bind); + DetourDetach((void **)&actual_closesocket, my_closesocket); + DetourTransactionCommit(); + + DEBUG_LOG("unload_end") + fclose(debug); +} + +BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) { + if (dwReason == DLL_PROCESS_ATTACH) { + load(); + } else if (dwReason == DLL_PROCESS_DETACH) { + unload(); + } + return TRUE; +}