From 8d92f25b8c69ed7d9f367071a83a755c6f13bd31 Mon Sep 17 00:00:00 2001 From: Daniel Seiller Date: Tue, 4 Aug 2020 18:05:34 +0200 Subject: [PATCH] Lots of Updates (expand for more): - Started implementing new parser for chunked data - Started documenting data formats - Started dissector for network protocol - Added AI-Graph renderer (converts .pth files to python data you can import into Blender) - Added Script to convert savefile to JSON - Added (old) parser for chunked data format - Added basic parser for LFVF data section (Vertex Data) - Added script to analyze and filter read trace generated with frida script - Added various Frida scripts --- .gitignore | 2 + NOTES.md | 108 ++++-- README.md | 37 +- ScrapHacks/.vscode/c_cpp_properties.json | 3 +- ScrapHacks/.vscode/settings.json | 8 +- ScrapHacks/.vscode/tasks.json | 21 ++ ScrapHacks/CMakeLists.txt | 92 +++-- ScrapHacks/README.md | 34 +- ScrapHacks/build.bat | 13 +- ScrapHacks/src/D3D8_Hook.hpp | 15 +- ScrapHacks/src/Hook.hpp | 59 +-- ScrapHacks/src/Py_Mod.hpp | 74 ++++ ScrapHacks/src/Py_Utils.hpp | 2 +- ScrapHacks/src/REPL.hpp | 120 +++++- ScrapHacks/src/ScrapHack.cpp | 83 +++-- ScrapHacks/src/Scrapland.hpp | 26 +- ScrapHacks/src/Util.hpp | 2 +- ScrapHacks/src/dllmain.cpp | 3 +- ScrapHacks/src/python/ScrapHack.py | 23 ++ {lib => ScrapHacks/src/python}/dbg.py | 166 +++++++-- config.yml | 443 +++++++++++++++++------ file_formats/ai_path.md | 22 ++ file_formats/chunked.md | 126 +++++++ file_formats/packed.md | 15 + frida/frida_hook_net.js | 60 +++ frida/frida_hook_read_trace.js | 174 +++++++++ frida/frida_inject_net.py | 56 +++ frida/frida_inject_read_trace.py | 59 +++ frida/frida_mem_mon.js | 8 + frida/frida_mem_mon.py | 22 ++ frida/frida_stalker_test.js | 43 +++ frida/frida_stalker_test.py | 67 ++++ parse_save.py | 21 -- r2_analyze.py | 154 ++++---- tools/analyze_read_trace.py | 68 ++++ tools/binvis.py | 57 +++ tools/dissect_net.py | 149 ++++++++ tools/packed.ksy | 46 +++ tools/parse_LFVF.py | 122 +++++++ tools/parse_chunked.py | 117 ++++++ tools/parse_chunked_new.py | 255 +++++++++++++ tools/rbingrep.py | 31 ++ tools/render_ai_path.py | 50 +++ tools/save_to_json.py | 27 ++ scrapper.py => tools/scrapper.py | 23 +- tools/server.py | 45 +++ tools/test.bv | 4 + 47 files changed, 2744 insertions(+), 411 deletions(-) create mode 100644 ScrapHacks/.vscode/tasks.json create mode 100644 ScrapHacks/src/Py_Mod.hpp create mode 100644 ScrapHacks/src/python/ScrapHack.py rename {lib => ScrapHacks/src/python}/dbg.py (73%) create mode 100644 file_formats/ai_path.md create mode 100644 file_formats/chunked.md create mode 100644 file_formats/packed.md create mode 100644 frida/frida_hook_net.js create mode 100644 frida/frida_hook_read_trace.js create mode 100644 frida/frida_inject_net.py create mode 100644 frida/frida_inject_read_trace.py create mode 100644 frida/frida_mem_mon.js create mode 100644 frida/frida_mem_mon.py create mode 100644 frida/frida_stalker_test.js create mode 100644 frida/frida_stalker_test.py delete mode 100644 parse_save.py create mode 100644 tools/analyze_read_trace.py create mode 100644 tools/binvis.py create mode 100644 tools/dissect_net.py create mode 100644 tools/packed.ksy create mode 100644 tools/parse_LFVF.py create mode 100644 tools/parse_chunked.py create mode 100644 tools/parse_chunked_new.py create mode 100644 tools/rbingrep.py create mode 100644 tools/render_ai_path.py create mode 100644 tools/save_to_json.py rename scrapper.py => tools/scrapper.py (89%) create mode 100644 tools/server.py create mode 100644 tools/test.bv diff --git a/.gitignore b/.gitignore index 367998a..ac1b952 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,5 @@ __pycache__/ ScrapHacks/build/* ScrapHacks/src/D3D8_VMT.hpp .vscode/c_cpp_properties.json +tools/*.log +frida/*.mp \ No newline at end of file diff --git a/NOTES.md b/NOTES.md index e33c026..75dceb7 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,6 +1,6 @@ # Infos -- Engine: ScrapEngine +- Engine: ScrapEngine/Mercury Engine - Ingame Scripting Language: Python 1.5.2 - Interesting memory locations and functions are noted in `config.yml` @@ -8,6 +8,8 @@ - `-console`: open external console window on start - `-wideWindow`: start game in widescreen mode +- `-dedicated`: start in mutliplayer dedicated server mode (needs to be used with `-server`) +- `-server`: start in multiplayer server mode # Functions identified: @@ -23,11 +25,11 @@ ## External Console (Scenegraph Debugging?) (Handler @ `0x5f9520`): -* `listar luces` -* `listar` -* `arbol` (Patch Scrap.exe@offset 0x314bc9 replace 0x20 with 0x00 (or just type `arbol ` with a space at the end)) -* `mem` -* `ver uniones` +* `listar luces` List lights in scene +* `listar` list models in scene +* `arbol ` show details for model +* `mem` (doesn't do anything?) +* `ver uniones` * Easter Eggs: * `imbecil` * `idiota` @@ -67,12 +69,11 @@ unsigned long hash(const unsigned char *s) ## Other Functions: -Check `r2_analyze.py` for full list +Check `config.yml` for full list ## File Index struct @ `0x7fcbec` ```cpp - struct FileEntry { uint32_t offset; uint32_t size; @@ -87,7 +88,7 @@ struct FileIDX { }; ``` -## Packed Index struct (array @ `0x7fc1b0`) +## Packed Index struct (array of 0x80 entries @ `0x7fc1b0`) ```cpp struct PackedIDX { void** VMT; @@ -111,7 +112,10 @@ struct CPP_Callback { } ``` -## Game engine Variables Pointer @ `0x7FBE4C` + +## Game engine Variables Hashtable @ `0x7fbe50` + +## Game engine Variables @ `0x7fbe4c` Structure: @@ -130,20 +134,21 @@ struct GameVar { Types -| Value | Type | -| ------ | ----------------------- | -| `0x10` | const char* | -| `0x20` | int32_t | -| `0x30` | User Control Definition | -| `0x40` | float | -| `0x60` | Callback function | +| Value | Type | +|-------|-----------------| +| `0x1` | const char* | +| `0x2` | int32_t | +| `0x3` | List of Defines | +| `0x4` | float | +| `0x5` | function | +| `0x6` | Script function | ## Game World/State Pointer @ `0x7fe944` Points to World struct | Offset | Type | Description | -| ------ | ------------------------ | -------------------------------------- | +|--------|--------------------------|----------------------------------------| | 0x0000 | `void**` | Virtual Method Table | | 0x0004 | `uint32_t` | Size of Entity Hashtable | | 0x0008 | `void**` | Pointer to Entity Hashtable | @@ -172,6 +177,15 @@ Points to World struct | 0x2238 | `???` | Used in `World_Init` | | 0x2254 | `float` | Used in `World_Init` | +## cPyEntity structure + +| Offset | Type | Description | +|--------|----------|----------------------| +| 0x0000 | `void**` | Virtual Method Table | +| 0x0004 | `char*` | Name | +| 0x0008 | `void*` | ??? | + + ## Entity Hash Table Hash-function used: [PJW](https://en.wikipedia.org/wiki/PJW_hash_function) (Same parameters as the example implementation) @@ -189,7 +203,7 @@ struct HT_Entry { Data format: | Offset | Type | Description | -| ------ | ------------- | ------------------------ | +|--------|---------------|--------------------------| | 0x0 | `void**` | Virtual Method Table (?) | | 0x4 | `const char*` | name as string | | 0x14 | `void*` | pointer to self (why?) | @@ -204,21 +218,57 @@ Attributes: - `OnDeath` - `OnDamage` -# File Formats +# Netplay protocol (WIP) -## .packed File Format: +Game Info Packet ``` -Header: - "BFPK\0\0\0\0" - Int32ul: number of files - for each file: - Int32ul: path length - String: path - Int32ul: size - Int32ul: offset in file +Server 'B':FZ (0/10) Ver 1.0 at 192.168.99.1:28086 +[0-3] header/ID? +[4-5] port (16-bit) +[6-7] max_players (16-bit) +[8-9] curr_player (16-bit) +[10-x] server name (char*) + + 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF +0019fdc0 ba ce 00 01 b6 6d 0a 00 00 00 42 00 30 fe 19 00 .....m....B.0... +0019fdd0 ff ff ff ff 27 2b b3 9b c7 3e bb 00 9c af 29 00 ....'+...>....). +0019fde0 db 69 00 00 00 00 00 00 00 00 44 65 61 74 68 4d .i........DeathM +0019fdf0 61 74 63 68 00 00 00 00 ff ff 46 5a 00 4a 91 f0 atch......FZ.J.. +0019fe00 92 8b 57 4e 7f 00 00 00 10 21 fe 38 0d ae 00 00 ..WN.....!.8.... +0019fe10 f0 ce f3 36 a0 e8 0b 77 a0 e8 ...6...w.. ``` + +Player Join Packet + +``` +[0-3] header/ID? +[6-x] Player name + + 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF +09c9dfe8 7f 47 00 00 00 0e 55 6e 6e 61 6d 65 64 20 50 6c .G....Unnamed Pl +09c9dff8 61 79 65 72 06 53 42 6f 73 73 31 b9 00 07 50 5f ayer.SBoss1...P_ +09c9e008 42 65 74 74 79 06 4d 42 4f 53 53 31 06 4d 42 4f Betty.MBOSS1.MBO +09c9e018 53 53 31 00 00 10 30 2c 31 35 2c 30 2c 30 2c 31 SS1...0,15,0,0,1 +09c9e028 35 2c 31 35 2c 31 02 00 00 00 5,15,1.... +``` + +| Message | Description | +|------------------------------------------|-------------------------------------------------------------------| +| `5c68625c32383230395c73637261706c616e64` | "Scrapland Server" announcement broadcast (`\hb\28209\scrapland`) | +| `7f01000007` | Retrieve Game info | +| `48423d35323932322c3235363a323830383600` | Connection Information (`HB=52922,256:28086`) | + +# [Notes](NOTES.md) + +## File Formats + +- [Chunked](file_formats/chunked.md) +- [Packed](file_formats/packed.md) +- [AI Pathfinding Graph](file_formats/ai_path.md) + + # Virtual Method Tables: check `r2_analyze.py` for full list diff --git a/README.md b/README.md index 481f59d..a1f9575 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ # Scrapland Reverse Engineering notes and tools -## Scripts: -* `parse_save.py`: Dumps information extracted from Save file + +## Note! + +All memory addresses are only valid for an unprotected `Scrap.exe` v1.0 with a SHA1 checksum of `d2dde960e8eca69d60c2e39a439088b75f0c89fa` , other version will crash if the memory offsets don't match and you try to inject ScrapHacks + +[Computer Bild Spiele Issue 2006/08](https://archive.org/download/cbs-2006-08-coverdisc/) Contains a full version of the game which was used as the basis for this project + +## Scripts + +* `tools/rbingrep.py`: Search for pattern in all files and generate radare2 script to find all references (currently configured to search for chunked file section headers) +* `frida/`: Scripts for use with Frida +* `parse_chunked.py`: WIP Parser for the game's chunked data format (Models, Animations, Maps) +* `save_to_json.py`: Convert game save to JSON * `scrapper.py`: Extractor and Repacker for *.packed files, needs the `construct` and `tqdm` python modules and python 3.x - Run `scrapper.py -h` for help * `r2_analyze.py`: uses radare2 to parse and label a lot of interesting stuff in the `Scrap.exe` binary * `lib/dbg.py`: general Script for poking around inside the game's scripting system - - Run `import dbg` inside the Game's Console, - this will load all builtin modules and enable godmode + - Run `import dbg;dbg.init()` inside the Game's Console, + this will load all builtin modules, ScrapHacks and enable godmode - The dbg module also enables writing to the ingame console using `print ` and defines two global functions s_write() and e_write() for writing to the Ingame Console's Stdout and Stderr Stream - `dbg.menu()` Displays the Game's built in Debug Menu (doesn't work properly) @@ -24,9 +35,15 @@ WIP Memory hacking library # Tools used: -- [Python 3](https://python.org/) + [Construct](https://construct.readthedocs.io/en/latest/) -- [IDA](https://www.hex-rays.com/products/ida/index.shtml) and [x32dbg](https://x64dbg.com/) -- [Reclass.NET](https://github.com/ReClassNET/ReClass.NET) -- [HxD](https://mh-nexus.de/en/hxd/) -- [Kaitai Struct](http://kaitai.io/) -- [Radare2](https://www.radare.org/) + [Cutter](https://cutter.re/) \ No newline at end of file +- Binary parsing: + - [HxD](https://mh-nexus.de/en/hxd/) for initial file analysis + - [Python 3](https://python.org/) + [Construct](https://construct.readthedocs.io/en/latest/) for binary parsing + - [Kaitai Struct](http://kaitai.io/) for binary parsing +- Static analysis: + - [IDA](https://www.hex-rays.com/products/ida/index.shtml) initialy used, later replaced by radare2 and Cutter + - [radare2](https://www.radare.org/) + - [Cutter](https://cutter.re/) +- Dynamic analysis: + - [x64dbg](https://x64dbg.com/) for dynamic analysis + - [Reclass.NET](https://github.com/ReClassNET/ReClass.NET) to analyze structures and classes in memory + - [Frida](https://frida.re/) for tracing and instrumenting functions \ No newline at end of file diff --git a/ScrapHacks/.vscode/c_cpp_properties.json b/ScrapHacks/.vscode/c_cpp_properties.json index 26cf26b..b1f3b8d 100644 --- a/ScrapHacks/.vscode/c_cpp_properties.json +++ b/ScrapHacks/.vscode/c_cpp_properties.json @@ -17,8 +17,7 @@ "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "msvc-x86", - "configurationProvider": "vector-of-bool.cmake-tools", - "compileCommands": "${workspaceFolder}/build/compile_commands.json" + "configurationProvider": "vector-of-bool.cmake-tools" } ], "version": 4 diff --git a/ScrapHacks/.vscode/settings.json b/ScrapHacks/.vscode/settings.json index 6d52be3..4a17900 100644 --- a/ScrapHacks/.vscode/settings.json +++ b/ScrapHacks/.vscode/settings.json @@ -78,5 +78,11 @@ "xtr1common": "cpp", "xtree": "cpp", "xutility": "cpp" - } + }, + "cmake.generator": "NMake Makefiles", + "cmake.preferredGenerators": [ + "NMake Makefiles", + "Ninja", + "Unix Makefiles" + ] } \ No newline at end of file diff --git a/ScrapHacks/.vscode/tasks.json b/ScrapHacks/.vscode/tasks.json new file mode 100644 index 0000000..f8e0c13 --- /dev/null +++ b/ScrapHacks/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "${workspaceFolder}\\build.bat", + "problemMatcher": [ + "$msCompile" + ] + }, + { + "label": "clean", + "type": "shell", + "command": "rm -Force -Recurse ${workspaceFolder}\\build", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/ScrapHacks/CMakeLists.txt b/ScrapHacks/CMakeLists.txt index 9d96ca6..9745e8a 100644 --- a/ScrapHacks/CMakeLists.txt +++ b/ScrapHacks/CMakeLists.txt @@ -1,6 +1,5 @@ cmake_minimum_required(VERSION 3.1) cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) - project(ScrapHacks VERSION 1.0 DESCRIPTION "Scrapland memory hacking library" @@ -13,6 +12,8 @@ if(NOT IS_ABSOLUTE "${SCRAPLAND_DIR}" OR NOT EXISTS "${SCRAPLAND_DIR}") message(FATAL_ERROR "Scrapland installation folder not found!") endif() +message(STATUS "Scrapland found at ${SCRAPLAND_DIR}") + message(STATUS "Checking Scrap.exe hash") file(SHA1 "${SCRAPLAND_DIR}/Bin/Scrap.exe" SCRAP_EXE_HASH) @@ -20,14 +21,26 @@ if(NOT ${SCRAP_EXE_HASH} STREQUAL "d2dde960e8eca69d60c2e39a439088b75f0c89fa") message(FATAL_ERROR "Scrap.exe hash miss match!") endif() +# ============================== +# "${SCRAPLAND_DIR}/Bin/Scrap.exe" +add_custom_target( + run + COMMAND "Scrap.exe" + WORKING_DIRECTORY "${SCRAPLAND_DIR}/Bin" + VERBATIM +) +add_dependencies(run install) +# ============================== + +set(COMPONENT "ScrapHacks") set(FETCHCONTENT_QUIET 0) set(CMAKE_BUILD_TYPE "RelMinSize") set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}") -set(ASMJIT_EMBED true) -set(ASMTK_EMBED true) -set(ZYDIS_BUILD_TOOLS false) -set(ZYDIS_BUILD_EXAMPLES false) - +set(ASMJIT_EMBED ON CACHE INTERNAL "") +set(ASMTK_EMBED ON CACHE INTERNAL "") +set(ZYDIS_BUILD_TOOLS OFF CACHE INTERNAL "") +set(ZYDIS_BUILD_EXAMPLES OFF CACHE INTERNAL "") +set(toml11_BUILD_TEST OFF CACHE INTERNAL "") if(WIN32) if(MSVC) @@ -44,54 +57,67 @@ endif(WIN32) include(FetchContent) + +FetchContent_Declare( + Python + PREFIX ${CMAKE_CURRENT_BINARY_DIR} + URL https://www.python.org/ftp/python/src/py152.tgz + URL_HASH SHA1=2d648d07b1aa1aab32a3a24851c33715141779b9 +) + FetchContent_Declare( DirectX PREFIX ${CMAKE_CURRENT_BINARY_DIR} - URL - https://archive.org/download/DirectX.8.0a.SDK_includes_libs_only/DirectX.8.0a.SDK.zip - URL_HASH SHA1=39f168336d0df92ff14d62d5e3aef1b9e3191312) - -FetchContent_MakeAvailable(DirectX) + URL https://archive.org/download/DirectX.8.0a.SDK_includes_libs_only/DirectX.8.0a.SDK.zip + URL_HASH SHA1=39f168336d0df92ff14d62d5e3aef1b9e3191312 +) FetchContent_Declare( ASM_JIT PREFIX ${CMAKE_CURRENT_BINARY_DIR} GIT_REPOSITORY git@github.com:asmjit/asmjit.git GIT_SHALLOW true - GIT_PROGRESS true + INSTALL_COMMAND "" + CONFIGURE_COMMAND "" ) -FetchContent_MakeAvailable(ASM_JIT) - FetchContent_Declare( ASM_TK PREFIX ${CMAKE_CURRENT_BINARY_DIR} GIT_REPOSITORY git@github.com:asmjit/asmtk.git GIT_SHALLOW true - GIT_PROGRESS true + INSTALL_COMMAND "" ) -FetchContent_MakeAvailable(ASM_TK) - -set(ASMJIT_DIR ${asm_jit_SOURCE_DIR}) - -include(${asm_tk_SOURCE_DIR}/CMakeLists.txt) - - FetchContent_Declare( Zydis PREFIX ${CMAKE_CURRENT_BINARY_DIR} GIT_REPOSITORY git@github.com:zyantific/zydis.git GIT_SHALLOW true - GIT_PROGRESS true + INSTALL_COMMAND "" ) -# FetchContent_MakeAvailable(Zydis) +FetchContent_MakeAvailable(Python DirectX ASM_JIT ASM_TK Zydis) -include_directories(AFTER ${directx_SOURCE_DIR}/8.0/include/ ${ASMTK_INCLUDE_DIRS} ${ASMJIT_INCLUDE_DIRS}) +set(ASMJIT_DIR ${asm_jit_SOURCE_DIR}) + +include(${asm_tk_SOURCE_DIR}/CMakeLists.txt) + + +message(STATUS "Python 1.5.2: ${python_SOURCE_DIR}") +message(STATUS "DX8: ${directx_SOURCE_DIR}") +message(STATUS "Zydis: ${zydis_SOURCE_DIR}") + +include_directories(AFTER + ${directx_SOURCE_DIR}/8.0/include/ + ${python_SOURCE_DIR}/Include/ + ${ASMTK_INCLUDE_DIRS} + ${ASMJIT_INCLUDE_DIRS} + ${toml_SOURCE_DIR} +) link_directories(AFTER ${directx_SOURCE_DIR}/8.0/lib/) -find_package(Python3 3.6 REQUIRED COMPONENTS Interpreter) +find_package(Python3 REQUIRED COMPONENTS Interpreter) add_custom_command( OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/D3D8_VMT.hpp @@ -111,7 +137,10 @@ add_library(ScrapHack SHARED ${ASMJIT_SRC} ) + set_target_properties(ScrapHack PROPERTIES SUFFIX ".pyd") +set_target_properties(ScrapHack PROPERTIES PREFIX "_") + add_dependencies(ScrapHack D3D8_VMT) # add_dependencies(ScrapHack Python152) # add_dependencies(ScrapHack Python152_Bin) @@ -119,10 +148,13 @@ target_link_libraries(ScrapHack d3d8 d3dx8 dxerr8 - gdiplus + # gdiplus # PYTHON15 - # Zydis + Zydis legacy_stdio_definitions) -install(TARGETS ScrapHack RUNTIME DESTINATION ${SCRAPLAND_DIR}/lib) -target_compile_features(ScrapHack PUBLIC cxx_std_11) +install(TARGETS ScrapHack RUNTIME DESTINATION ${SCRAPLAND_DIR}/lib COMPONENT ScrapHacks) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/python/ScrapHack.py DESTINATION ${SCRAPLAND_DIR}/lib COMPONENT ScrapHacks) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/python/dbg.py DESTINATION ${SCRAPLAND_DIR}/lib COMPONENT ScrapHacks) + +target_compile_features(ScrapHack PUBLIC cxx_std_17) diff --git a/ScrapHacks/README.md b/ScrapHacks/README.md index bc2ec9a..304fcfc 100644 --- a/ScrapHacks/README.md +++ b/ScrapHacks/README.md @@ -1,10 +1,11 @@ ## Features - read and write memory +- disassemble memory (using zydis) - change DirectX state - Draw DirectX overlay (still need to make a useful overlay) - Dump various data structures to the console -- Assemble and execute code on the fly +- Assemble and execute code on the fly (using asmtk) - Can be controlled via keyboard shortcuts (TODO: allow defining own shortcuts for commands) ## Prerequisites @@ -30,7 +31,34 @@ This will find the Games's installation folder, verify that the version you have - type `import ScrapHack` - type `$help` -## Notes +## Config file keys -(this has only been tested with a (cracked/unpacked/de-obfuscated) `Scrap.exe` v1.0 with a SHA1 checksum of `d2dde960e8eca69d60c2e39a439088b75f0c89fa` , other version will crash if the memory offsets don't match) +- patches.asm: map of address->list of assembly instructions +- patches.hex: map of address->hex bytes +Example: + +```json +{ + "patches": { + "hex": { + "0xDEADBEEF": "BADFOODDEADFEED" + }, + "asm": { + "0xBADF00D": [ + "pushad", + "call 0xf00dbabe", + "popad", + "mov eax, 0x42", + "ret" + ] + }, + } +} +``` + +## Third-Party components used + +- Zydis disassembler +- asmJIT/asmTK assembler +- nlohmann/json JSON-parser \ No newline at end of file diff --git a/ScrapHacks/build.bat b/ScrapHacks/build.bat index d367289..16f4935 100644 --- a/ScrapHacks/build.bat +++ b/ScrapHacks/build.bat @@ -5,6 +5,15 @@ if "%VSINSTALLDIR%"=="" ( call "%%i" x86 ) ) -if not exist build cmake -G"NMake Makefiles" -B build -cmake --build build --target install +if "%VSINSTALLDIR%"=="" ( + echo "VSINSTALLDIR" not set something is wrong! +) else ( + if not exist build cmake -G"NMake Makefiles" -B build + if "%1"=="--run" ( + cmake --build build --target run + ) else ( + cmake --build build --target install + ) +) + endlocal \ No newline at end of file diff --git a/ScrapHacks/src/D3D8_Hook.hpp b/ScrapHacks/src/D3D8_Hook.hpp index 6334785..3ac2412 100644 --- a/ScrapHacks/src/D3D8_Hook.hpp +++ b/ScrapHacks/src/D3D8_Hook.hpp @@ -147,6 +147,13 @@ void unhook_d3d8() { hooked=false; } +map *dx_hooks = new map({ + {VMT_IDirect3DDevice8::m_EndScene,H_EndScene}, + {VMT_IDirect3DDevice8::m_BeginScene,H_BeginScene}, + {VMT_IDirect3DDevice8::m_DrawIndexedPrimitive,H_DrawIndexedPrimitive}, + {VMT_IDirect3DDevice8::m_SetLight,H_SetLight}, +}); + void hook_d3d8() { if (hooked) { return; @@ -162,11 +169,9 @@ void hook_d3d8() { hFont = CreateFontA(15, 0, 0, 0, FW_EXTRABOLD, 0, 0, 0, ANSI_CHARSET, 0, 0, 0, 0, "Lucida Console"); hBrush = CreateSolidBrush(D3DCOLOR_ARGB(25, 0, 0, 0)); - Hook::addr(GetVTable(dev)[VMT_IDirect3DDevice8::m_EndScene], H_EndScene); - Hook::addr(GetVTable(dev)[VMT_IDirect3DDevice8::m_BeginScene], H_BeginScene); - Hook::addr(GetVTable(dev)[VMT_IDirect3DDevice8::m_DrawIndexedPrimitive], - H_DrawIndexedPrimitive); - Hook::addr(GetVTable(dev)[VMT_IDirect3DDevice8::m_SetLight], H_SetLight); + for (auto h: *dx_hooks) { + Hook::addr(GetVTable(dev)[h.first], h.second); + } hooked=true; return; } diff --git a/ScrapHacks/src/Hook.hpp b/ScrapHacks/src/Hook.hpp index d898648..59412bc 100644 --- a/ScrapHacks/src/Hook.hpp +++ b/ScrapHacks/src/Hook.hpp @@ -9,9 +9,10 @@ using namespace std; -/* + vector make_trampoline(uintptr_t orig,uintptr_t hook) { using namespace asmjit; + vector ret; JitRuntime rt; CodeHolder code; CodeInfo ci=rt.codeInfo(); @@ -23,9 +24,15 @@ vector make_trampoline(uintptr_t orig,uintptr_t hook) { code.resolveUnresolvedLinks(); code.relocateToBase(orig); size_t code_size=code.sectionById(0)->buffer().size(); - code.copyFlattenedData((void*)orig, code_size, CodeHolder::kCopyWithPadding); + uint8_t* buffer=new uint8_t[code_size]; + code.copyFlattenedData((void*)buffer, code_size, CodeHolder::kCopyWithPadding); + for (size_t i=0;i> hooks; public: Hook(void *func, void *detour) { - // TODO: build jmp_bytes using asmjit uintptr_t dest = reinterpret_cast(detour); uintptr_t src = reinterpret_cast(func); this->orig = func; this->detour = detour; - this->jmp_bytes[0] = 0x68; // push - this->jmp_bytes[1] = (dest >> 0) & 0xff; - this->jmp_bytes[2] = (dest >> 8) & 0xff; - this->jmp_bytes[3] = (dest >> 16) & 0xff; - this->jmp_bytes[4] = (dest >> 24) & 0xff; - this->jmp_bytes[5] = 0xC3; // ret - VirtualQuery(func, &mbi, sizeof(mbi)); + vector code = make_trampoline(src,dest); + this->orig_bytes = new uint8_t[code.size()]; + this->jmp_bytes = new uint8_t[code.size()]; + this->size = code.size(); + this->enabled = false; + uint8_t* func_b = reinterpret_cast(this->orig); + for (size_t i=0;isize;++i) { + this->orig_bytes[i]=func_b[i]; + this->jmp_bytes[i]=code[i]; + } + VirtualQuery(this->orig, &mbi, sizeof(mbi)); VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, &mbi.Protect); - memcpy(this->orig_bytes, this->orig, 1 + 4 + 1); VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, NULL); - this->enabled = false; + cout<<"Constructed hook from "<size<disable(); } + + static void addr(uintptr_t _addr, void *detour) { + Hook::addr(reinterpret_cast(_addr),detour); + } + static void addr(void *addr, void *detour) { cout << "Hooking: [" << addr << " -> " << detour << "]" << endl; uintptr_t key = reinterpret_cast(detour); @@ -105,23 +120,23 @@ class Hook { void disable() { if (this->enabled) { - // cout << "Disabling: [" << this->orig << " <- " << this->detour << - // "]" - // << endl; + cout << "Disabling: [" << this->orig << " <- " << this->detour << + "]" + << endl; VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, NULL); - memcpy(this->orig, this->orig_bytes, 1 + 4 + 1); + memcpy(this->orig, this->orig_bytes, this->size); VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, NULL); this->enabled = false; } } void enable() { if (!this->enabled) { - // cout << "Enabling: [" << this->orig << " -> " << this->detour << - // "]" << endl; + cout << "Enabling: [" << this->orig << " -> " << this->detour << + "]" << endl; VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, NULL); - memcpy(this->orig, this->jmp_bytes, 1 + 4 + 1); + memcpy(this->orig, this->jmp_bytes, this->size); VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, NULL); this->enabled = true; } diff --git a/ScrapHacks/src/Py_Mod.hpp b/ScrapHacks/src/Py_Mod.hpp new file mode 100644 index 0000000..fcc2f3d --- /dev/null +++ b/ScrapHacks/src/Py_Mod.hpp @@ -0,0 +1,74 @@ +#pragma once +#include "Scrapland.hpp" + +using namespace std; + +static void *py_asm(void *self, void *args) +{ + void* ret; + void *addr = nullptr; + char *asm_code; + if (!PyArg_ParseTuple(args, "s|i", &asm_code, &addr)) + { + return nullptr; + } + string code(asm_code); + size_t data_size=asm_size(split(code,';')); + if (addr==nullptr) { + addr = malloc(data_size); + if (addr==nullptr) { + scrap_log(ERR_COLOR, "ERROR: "); + scrap_log(ERR_COLOR, "malloc() failed"); + scrap_log(ERR_COLOR, "\n"); + return Py_BuildValue("s",nullptr); + } + char ptr[255]; + snprintf(ptr,255,"Buffer @ %p\n",(void*)addr); + scrap_log(INFO_COLOR,ptr); + ret=Py_BuildValue("(l,l)", data_size, addr); + } else { + ret=Py_BuildValue("(l,l)", data_size, addr); + } + assemble(split(code,';'),reinterpret_cast(addr)); + return ret; +} + +static void *py_dasm(void *self, void *args) { + // TODO: return list of (addr,asm) + return nullptr; +} + +static void *py_read_mem(void *self, void *args) +{ + //TODO: implement reading memory and return data as hex string + void* ret = nullptr; + void *addr = nullptr; + size_t size = 256; + char *data; + if (!PyArg_ParseTuple(args, "ii", &addr, &size)) + { + return nullptr; + } + char* buffer=new char[size*2]; + for (size_t i=0;i #include #include - +// #include #include "Structures.hpp" using namespace std; diff --git a/ScrapHacks/src/REPL.hpp b/ScrapHacks/src/REPL.hpp index 1724e8a..f0a2e3a 100644 --- a/ScrapHacks/src/REPL.hpp +++ b/ScrapHacks/src/REPL.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "Scrapland.hpp" #include "Util.hpp" @@ -40,17 +41,20 @@ size_t assemble(vector assembly,uint64_t base) { for (string line:assembly) { if (err = p.parse((line+"\n").c_str())) { snprintf(err_msg,1024,"PARSE ERROR: [%s] %08x (%s)\n",line.c_str(), err, DebugUtils::errorAsString(err)); + cerr< assembly,uint64_t base) { MEMORY_BASIC_INFORMATION mbi; if (VirtualQuery((void*)base, &mbi, sizeof(mbi)) == 0) { + cerr<<"ERROR: "<< GetLastErrorAsString() < assembly,uint64_t base) { }; if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, &mbi.Protect)) { + cerr<<"ERROR: "<< GetLastErrorAsString() < assembly) { return assemble(assembly,0); } +string disassemble(void* addr, size_t num,bool compact) { + stringstream ret; + ZyanU64 z_addr = reinterpret_cast(addr); + ZydisDecoder decoder; + ZydisFormatter formatter; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_ADDRESS_WIDTH_32); + ZydisFormatterInit(&formatter, ZYDIS_FORMATTER_STYLE_INTEL); + + ZydisDecodedInstruction instruction; + while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, addr, -1, &instruction))) + { + char buffer[256]; + ZydisFormatterFormatInstruction(&formatter, &instruction, buffer, sizeof(buffer), z_addr); + if (!compact) { + ret<< "[" << std::hex << setfill('0') << setw(8) << z_addr << "]: "; + } + ret << buffer; + + if (compact) { + ret<<"; "; + } else { + ret<(z_addr += instruction.length); + num--; + if (num==0) { + break; + } + } + return ret.str(); +} struct Command { t_cmd_func func; @@ -217,6 +255,46 @@ get_protection(void *addr) { return mbi.Protect; } +void cmd_disassemble(Command* cmd, vector args) { + MEMORY_BASIC_INFORMATION mbi; + if (args.size()<1) { + scrap_log(ERR_COLOR, cmd->usage); + scrap_log(ERR_COLOR, "\n"); + return; + } + uintptr_t addr = UINTPTR_MAX; + size_t size = 0xff; + try { + addr = stoull(args[0], 0, 16); + if (args.size()>1) { + size = stoull(args[1]); + } + } catch (exception e) { + scrap_log(ERR_COLOR, "ERROR: "); + scrap_log(ERR_COLOR, e.what()); + scrap_log(ERR_COLOR, "\n"); + return; + } + void *mptr = reinterpret_cast(addr); + if (VirtualQuery(mptr, &mbi, sizeof(mbi)) == 0) { + scrap_log(ERR_COLOR, "ERROR: "); + scrap_log(ERR_COLOR, GetLastErrorAsString()); + scrap_log(ERR_COLOR, "\n"); + return; + }; + if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, + &mbi.Protect)) { + scrap_log(ERR_COLOR, "ERROR: "); + scrap_log(ERR_COLOR, GetLastErrorAsString()); + scrap_log(ERR_COLOR, "\n"); + return; + }; + string dasm = disassemble(mptr, size, false); + scrap_log(INFO_COLOR, dasm); + VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, NULL); + return; +} + void cmd_exec(Command* cmd, vector args) { void *addr; MEMORY_BASIC_INFORMATION mbi; @@ -339,7 +417,7 @@ void cmd_read(Command* cmd,vector args) { return; }; string hxd = hexdump_s(mptr, size); - scrap_log(INFO_COLOR, hxd.c_str()); + scrap_log(INFO_COLOR, hxd); VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, NULL); if (buffer) { free(buffer); @@ -399,22 +477,26 @@ void cmd_dx8(Command* cmd,vector args) { } void cmd_dump_stack(Command* cmd, vector args) { + stringstream ret; void** stack=(void**)_AddressOfReturnAddress(); cout<<"ESP: "< args) { << meth.second->ml_meth << endl; } } - scrap_log(INFO_COLOR,out.str().c_str()); + scrap_log(INFO_COLOR,out.str()); } void cmd_dump_vars(Command* cmd, vector args) { @@ -437,7 +519,7 @@ void cmd_dump_vars(Command* cmd, vector args) { out<name<< "[" <type <<","<< (uint16_t)var->subtype << std::dec << "]: " << var->desc<<" ("<value<<", "<default<<")"<next; } - scrap_log(INFO_COLOR,out.str().c_str()); + scrap_log(INFO_COLOR,out.str()); } void cmd_dump_ents(Command* cmd,vector args) { @@ -446,7 +528,7 @@ void cmd_dump_ents(Command* cmd,vector args) { dump_ht(ptr>(P_WORLD, O_ENTS), &out); out << "Entity Lists:" << endl; dump_ht(ptr>(P_WORLD, O_ENTLISTS), &out); - scrap_log(INFO_COLOR,out.str().c_str()); + scrap_log(INFO_COLOR,out.str()); return; } @@ -478,10 +560,14 @@ void cmd_disable_overlay(Command* cmd,vector args) { void cmd_print_alarm(Command* cmd,vector args) { stringstream out; - float *alarm = ptr(P_WORLD, O_ALARM); - float *alarm_grow = ptr(P_WORLD, O_ALARM_GROW); - out << "Alarm: " << alarm[0] << " + " << alarm_grow[0] << endl; - scrap_log(INFO_COLOR,out.str().c_str()); + float alarm = ptr(P_WORLD, O_ALARM)[0]; + float alarm_grow = ptr(P_WORLD, O_ALARM_GROW)[0]; + if (alarm_grow<0) { + out << "Alarm: " << alarm << " - " << alarm_grow << endl; + } else { + out << "Alarm: " << alarm << " + " << alarm_grow << endl; + } + scrap_log(INFO_COLOR,out.str()); return; } @@ -528,6 +614,7 @@ void cmd_asm(Command* cmd, vector args) { assemble(split(code,';'),buffer_addr); } + void cmd_help(Command* cmd,vector args); static REPL* repl=new REPL( @@ -538,6 +625,7 @@ static REPL* repl=new REPL( {"exec",new Command(cmd_exec,"Usage: $exec ","Start a new thread at the specified address")}, {"asm",new Command(cmd_asm,"Usage: $asm [addr] ;;...","Assemble instructions at address, if no address is given allocate memory and assemble code into that")}, {"stack",new Command(cmd_dump_stack,"Usage: $mem stack","Dump stack contents")}, + {"dasm",new Command(cmd_disassemble,"Usage: $mem dasm [num_inst]","Disassemble memory at address")}, })}, {"unload",new Command(cmd_unload,"Usage: $unload","Unload ScrapHacks")}, {"dx8",new Command(cmd_dx8,"Usage: $dx8 ","Manipulate DirectX 8 functions and state",{ diff --git a/ScrapHacks/src/ScrapHack.cpp b/ScrapHacks/src/ScrapHack.cpp index 5cb9e23..7c35e1b 100644 --- a/ScrapHacks/src/ScrapHack.cpp +++ b/ScrapHacks/src/ScrapHack.cpp @@ -6,7 +6,8 @@ #include #include #include - +#include +// #include #include using namespace std; @@ -18,6 +19,7 @@ using namespace std; #include "Scrapland.hpp" #include "Structures.hpp" #include "Util.hpp" +#include "Py_Mod.hpp" bool initialized = false; bool running = true; @@ -28,12 +30,14 @@ void DllUnload(); int hooked_console(const char *); void hook_exit(); -void setup_hooks() { - Hook::addr(reinterpret_cast(P_SCRAP_EXIT), hook_exit); - Hook::addr(reinterpret_cast(P_CON_HANDLER), hooked_console); +void setup_hooks() +{ + Hook::addr(P_SCRAP_EXIT, hook_exit); + Hook::addr(P_CON_HANDLER, hooked_console); } -void MainLoop() { +void MainLoop() +{ setup_hooks(); overlay = true; cout << "[*] Starting main Loop" << endl; @@ -49,42 +53,56 @@ void MainLoop() { cout << "[ F ] \"Handbrake\" (*Will* crash the game after some time!)" << endl; - while (running) { + while (running) + { Sleep(100); - while (key_down('F')) { + while (key_down('F')) + { scrap_exec("dbg.brake()"); } - if (key_down_norepeat(VK_F3)) { + if (key_down_norepeat(VK_F3)) + { break; } - if (key_down_norepeat(VK_F7)) { + if (key_down_norepeat(VK_F7)) + { int32_t *money = ptr(P_WORLD, O_MONEY); money[0] = 0x7fffffff; } - if (key_down_norepeat(VK_F9)) { + if (key_down_norepeat(VK_F8)) + { + cout << "Not yet implemented" << endl; + } + + if (key_down_norepeat(VK_F9)) + { cout << "Entities:" << endl; dump_ht(ptr>(P_WORLD, O_ENTS)); cout << "Entity Lists:" << endl; dump_ht(ptr>(P_WORLD, O_ENTLISTS)); } - if (key_down_norepeat(VK_F10)) { + if (key_down_norepeat(VK_F10)) + { scrap_exec("dbg.settrace()"); } } FreeLibraryAndExitThread(hMod, 0); } -void InitConsole() { +void InitConsole() +{ char me[1024]; - GetModuleFileName(hMod, me, 1024); + GetModuleFileNameA(hMod, me, 1024); SetupConsole(me); } -int hooked_console(const char *cmd) { +int hooked_console(const char *cmd) +{ typedef decltype(&hooked_console) t_func; - if (cmd[0] == '$') { + if (cmd[0] == '$') + { handle_command(++cmd); return 0; } @@ -93,7 +111,8 @@ int hooked_console(const char *cmd) { return ret; } -void hook_exit() { +void hook_exit() +{ typedef decltype(&hook_exit) t_func; shared_ptr hook = Hook::get(hook_exit); DllUnload(); @@ -102,7 +121,8 @@ void hook_exit() { return; } -void DllInit(HMODULE mod) { +void DllInit(HMODULE mod) +{ hMod = mod; char mfn[1024]; GetModuleFileNameA(0, mfn, 1024); @@ -119,36 +139,23 @@ void DllInit(HMODULE mod) { CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MainLoop, NULL, 0, 0); cout << "[*] Starting message pump" << endl; MSG msg; - while (GetMessage(&msg, NULL, 0, 0)) { + while (GetMessage(&msg, NULL, 0, 0)) + { TranslateMessage(&msg); DispatchMessage(&msg); } return; } -void *H_port_FixupExtension(char *name, char *filename) { - cout<<"FixupExtension: "<(0x5a9ca0), H_port_FixupExtension); - Hook::addr(reinterpret_cast(0x5cdb00), - H_PyEval_CallObjectWithKeywords); + InitPyMod(); } -void DllUnload() { +void DllUnload() +{ SetConsoleCtrlHandler(NULL, false); unhook_d3d8(); Hook::clear(); diff --git a/ScrapHacks/src/Scrapland.hpp b/ScrapHacks/src/Scrapland.hpp index 76d731b..db2f438 100644 --- a/ScrapHacks/src/Scrapland.hpp +++ b/ScrapHacks/src/Scrapland.hpp @@ -1,6 +1,6 @@ #pragma once #include "Structures.hpp" - +#include "Util.hpp" #include using namespace std; @@ -17,14 +17,21 @@ using namespace std; #define P_PY_MODS 0x79C698 // FUNCTION ADDRESSES +// ENGINE INTERNALS #define P_CON_HANDLER 0x402190 #define P_SCRAP_LOG 0x4134C0 #define P_SCRAP_EXEC 0x5a8390 #define P_SCRAP_EXIT 0x4010c0 #define P_D3DCHECK 0x602a70 #define P_D3DDEV 0x852914 +// PYTHON FUNCTIONS #define P_Py_InitModule 0x5A8FB0 #define P_PyArg_ParseTuple 0x5bb9d0 +#define P_PyBuildValue 0x5a90f0 +#define P_PyList_New 0x5b3120 +#define P_PyTuple_New 0x5b91a0 +#define P_PyList_SetItem 0x5b3240 +#define P_PyTuple_SetItem 0x5b92a0 #define MSG_COLOR scrap_RGB(128,0,255) @@ -41,14 +48,20 @@ uint32_t scrap_RGB(uint8_t r,uint8_t g,uint8_t b) { typedef int(_cdecl *t_scrap_log)(unsigned int color, const char *message); typedef int(_cdecl *t_scrap_exec)(const char *code); typedef int(_cdecl *t_PyArg_ParseTuple)(void *PyObj, char *format, ...); +typedef void*(_cdecl *t_Py_BuildValue)(char *format, ...); typedef int(_cdecl *t_Py_InitModule)(const char *name, void *methods, const char *doc, void *passthrough, int module_api_version); +typedef void*(_cdecl *t_PyList_New)(char *format, ...); +typedef void*(_cdecl *t_PyTuple_New)(char *format, ...); +typedef void*(_cdecl *t_PyList_SetItem)(char *format, ...); +typedef void*(_cdecl *t_PyList_SetItem)(char *format, ...); // GLOBAL FUNCTIONS auto scrap_exec = (t_scrap_exec)P_SCRAP_EXEC; -auto pyarg_parsetuple = (t_PyArg_ParseTuple)P_PyArg_ParseTuple; -auto py_initmodule = (t_Py_InitModule)P_Py_InitModule; +auto PyArg_ParseTuple = (t_PyArg_ParseTuple)P_PyArg_ParseTuple; +auto Py_BuildValue = (t_Py_BuildValue)P_PyBuildValue; +auto Py_InitModule = (t_Py_InitModule)P_Py_InitModule; int scrap_log(unsigned int color,const char* msg) { return ((t_scrap_log)P_SCRAP_LOG)(color,msg); @@ -58,7 +71,6 @@ int scrap_log(uint8_t r,uint8_t g,uint8_t b,const char* msg) { return ((t_scrap_log)P_SCRAP_LOG)(scrap_RGB(r,g,b),msg); } - int scrap_log(unsigned int color,string msg) { return ((t_scrap_log)P_SCRAP_LOG)(color,msg.c_str()); } @@ -67,6 +79,12 @@ int scrap_log(uint8_t r,uint8_t g,uint8_t b,string msg) { return ((t_scrap_log)P_SCRAP_LOG)(scrap_RGB(r,g,b),msg.c_str()); } +int scrap_err() { + string err("Error: "); + err+=GetLastErrorAsString(); + err+="\n"; + return scrap_log(ERR_COLOR,err); +} size_t size_ht(HashTable *ht) { size_t cnt = 0; diff --git a/ScrapHacks/src/Util.hpp b/ScrapHacks/src/Util.hpp index ccc7a1c..21494e1 100644 --- a/ScrapHacks/src/Util.hpp +++ b/ScrapHacks/src/Util.hpp @@ -26,7 +26,7 @@ string GetLastErrorAsString() { size_t m_size = FormatMessageA( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + NULL, errorMessageID, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (LPSTR)&messageBuffer, 0, NULL); string message(messageBuffer, m_size); LocalFree(messageBuffer); diff --git a/ScrapHacks/src/dllmain.cpp b/ScrapHacks/src/dllmain.cpp index ae3eed1..b53bfcb 100644 --- a/ScrapHacks/src/dllmain.cpp +++ b/ScrapHacks/src/dllmain.cpp @@ -12,9 +12,10 @@ HANDLE hThread = INVALID_HANDLE_VALUE; bool loaded = false; HMODULE mod = nullptr; -DLL_EXPORT void initScrapHack() { +DLL_EXPORT void init_ScrapHack() { DllPreInit(); if (!loaded) { + Sleep(1000); hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)DllInit, mod, 0, 0); CloseHandle(hThread); diff --git a/ScrapHacks/src/python/ScrapHack.py b/ScrapHacks/src/python/ScrapHack.py new file mode 100644 index 0000000..14803e5 --- /dev/null +++ b/ScrapHacks/src/python/ScrapHack.py @@ -0,0 +1,23 @@ +# ScrapHack python interface +import _ScrapHack + + +class Mem: + def __init__(self): + return + + def __getitem__(self, key): + print("GI:", key) + + def __setitem__(self, key, value): + print("SI:", key, value) + +Mem = Mem() + +def asm(code,address=0): + """ + asm(code, address): + + Assemble code at address + """ + return _ScrapHack.asm(address, code) diff --git a/lib/dbg.py b/ScrapHacks/src/python/dbg.py similarity index 73% rename from lib/dbg.py rename to ScrapHacks/src/python/dbg.py index dc84ad1..a5783d7 100644 --- a/lib/dbg.py +++ b/ScrapHacks/src/python/dbg.py @@ -5,10 +5,12 @@ import MissionsFuncs import SScorer import Menu import sys + QC = quickconsole MF = MissionsFuncs last_frame = None - +level = 3 +initialized = 0 sys.path.append(".\\pylib\\Lib") sys.path.append(".\\pylib\\Libs") sys.path.append(".\\pylib") @@ -16,11 +18,18 @@ sys.path.append(".\\pylib") def reload(): sys.settrace(None) - sys.modules['__builtin__'].reload(sys.modules['dbg']) + sys.modules['__builtin__'].reload(sys.modules[__name__]) def dgb_info(): - SScorer.SetLabelText(`last_frame`, Scrap.GetTime() + 0.1) + if me: + try: + dbg_text = str(SVec.Mod(me.Vel)) + except: + dbg_text="" + else: + dbg_text = "" + SScorer.SetLabelText(dbg_text, Scrap.GetTime() + 0.1) Scrap.DeleteScheduledFuncs("dbg.dbg_info") Scrap.DeleteScheduledFuncs("dbg.dbg_info") Scrap.AddScheduledFunc(Scrap.GetTime()+0.1, dgb_info, (), "dbg.dbg_info") @@ -197,12 +206,11 @@ def helplib(): logfile_name = None print "Done!" - def enable_all_conv(): try: import CharConversor except ImportError: - print("CharConversor not available") + # print("CharConversor not available") return CharConversor.ConversionChars = list(CharConversor.ConversionChars) E = Scrap.GetFirst() @@ -244,20 +252,6 @@ def nuke(): E = Scrap.GetEntity(E.NextInSlot) -def test_func(): - E = Scrap.GetFirst() - me = Scrap.UsrEntity(0) - while E: - if E.Name == me.Name: - E = Scrap.GetEntity(E.NextInSlot) - try: - E.Money=1024*1024*1024 - # SAI.SetStateVehicle(8,me.Name,E.Name) - except: - pass - E = Scrap.GetEntity(E.NextInSlot) - - def become(name): import CharConversor enable_all_conv() @@ -299,7 +293,7 @@ def getall(): me = Scrap.UsrEntity(0) while E: try: - E.Pos = me.Pos + E.Descriptor = "HAXX!" except: pass E = Scrap.GetEntity(E.NextInSlot) @@ -308,6 +302,11 @@ def getall(): def god(e=None): if e == None: e = Scrap.UsrEntity(0) + if e: + try: + e.IsType("Car") + except: + return if e: if e.IsType("Car"): e.Ammo00 = SWeap.GetFAmmo(0, "Max") @@ -324,9 +323,18 @@ def god(e=None): elif e.IsType("WalkChar"): e.Energy = 1 e.Invulnerable = 1 + e.TimeSpeed = 2.0 + e.Mass = 100 + # Scrap.SetAlarm(0.0) + Scrap.SetAlarmGrow(-0.5) Scrap.DeleteScheduledFuncs("dbg.god") Scrap.DeleteScheduledFuncs("dbg.god") - Scrap.AddScheduledFunc(Scrap.GetTime(), god, (e,), "dbg.god") + Scrap.AddScheduledFunc(Scrap.GetTime() + 0.01, god, (e,), "dbg.god") + + +def ungod(): + for _ in range(1024): + Scrap.DeleteScheduledFuncs("dbg.god") def ultranuke(): @@ -337,11 +345,92 @@ def ultranuke(): (), "dbg.ultranuke") +def freeze(_=None): + QC.freeze() + Scrap.DeleteScheduledFuncs("dbg.freeze") + Scrap.DeleteScheduledFuncs("dbg.freeze") + Scrap.AddScheduledFunc(Scrap.GetTime()+0.1, freeze, (None,), "dbg.freeze") + + +def unfreeze(_): + Scrap.DeleteScheduledFuncs("dbg.freeze") + Scrap.DeleteScheduledFuncs("dbg.freeze") + QC.unfreeze() + + def brake(): if me: me.Vel = (0, 0, 0) +weaps_hacked = { + "Laser": { + "AmmoCost": 0, + "TimeDelay": 0, + }, + "Vulcan": { + "TimeDelay": 0.01, + "TimeDelayUPG": 0.01, + "AmmoCost": 0 + }, + "Devastator": { + "AmmoCost": 0, + "RechargeTime": 0, + "SpreadAngle": 0, + }, + "Tesla": { + "AmmoCost": 0, + }, + "ATPC": { + "AmmoCost": 0, + "UpgradeDelay": 0, + "Delay": 0, + }, + "Swarm": { + "AmmoCost1": 0, + "AmmoCost2": 0, + "AmmoCost3": 0, + "AmmoCost4": 0, + "Number1": 20, + "Number2": 20, + "Number3": 20, + "Number4": 20, + "TurnSpeed": 360000, + "TurnSpeedUPG": 360000, + "TimeDelay": 1.0, + }, + "Inferno": { + "AmmoCost": 1 + } +} + + +def weaphacks(): + for weapon, properties in weaps_hacked.items(): + for prop, value in properties.items(): + Scrap.Set(weapon+prop, value) + + +def unweaphacks(): + for weapon, properties in weaps_hacked.items(): + for prop, value in properties.items(): + Scrap.Set(weapon+prop, Scrap.Def(weapon+prop)) + + +def test_func(): + E = Scrap.GetFirst() + me = Scrap.UsrEntity(0) + while E: + if E.Name == me.Name: + E = Scrap.GetEntity(E.NextInSlot) + try: + E.Money = 1024*1024*1024 + # SAI.SetStateVehicle(8,me.Name,E.Name) + except: + pass + E = Scrap.GetEntity(E.NextInSlot) + + for _ in range(1024): Scrap.DeleteScheduledFuncs("dbg.dbg_info") Scrap.DeleteScheduledFuncs("dbg.god") @@ -354,18 +443,31 @@ for module in sys.builtin_module_names: exec("import " + module) sys.settrace(None) - -me = Scrap.UsrEntity(0) notrace() helplib() # settrace() -dgb_info() -enable_all_conv() -god() -Scrap.Set("debug", 3) -Scrap.Set("ShowConsoleLog", 1) -Scrap.Set("AlwaysFlushLog", 1) -Scrap.Set("PythonExecute", "import dbg") -exec("import QuickConsole;QuickConsole.debug=sys.modules['dbg']") -print "Debug Module loaded" + +def init(): + global me + global initialized + if initialized == 0: + from ScrapHack import Mem, asm + sys.modules[__name__].mem = Mem + sys.modules[__name__].asm = asm + me = Scrap.UsrEntity(0) + dgb_info() + enable_all_conv() + god() + Scrap.Set("debug", level) + Scrap.Set("ShowConsoleLog", 1) + Scrap.Set("AlwaysFlushLog", 1) + Scrap.Set("PythonExecute", "import dbg;dbg.init()") + Scrap.DeleteScheduledFuncs("dbg_init") + Scrap.DeleteScheduledFuncs("dbg_init") + Scrap.AddScheduledFunc(Scrap.GetTime()+1, init, (), "dbg_init") + initialized = 1 + + +exec("import QuickConsole;QuickConsole.dbg=sys.modules['dbg']") +print "Debug Module loaded use /dbg.init to initialize" diff --git a/config.yml b/config.yml index 42d34e2..4d096ab 100644 --- a/config.yml +++ b/config.yml @@ -1,15 +1,26 @@ notes: | 0x7faa4c: temp storage? - 0x7d2094: some reference count + 0x4039b0: fcn.handle_cli_opts? + 0x668007: ? comments: 0x6113f9: Check if Window exists flags: + 0x7fbfa0: P_HT_SaveVars + 0x7fbe50: P_HT_Eng_Vars + 0x8c8d60: P_Addr_master + 0x8c8d50: P_Addr_client + 0x7fa748: P_Socket + 0x8045dc: P_Socket_Server 0x7FE944: P_World + 0x792618: P_Eng3d_ver + 0x853a24: P_gWorld 0x7FBE4C: P_Vars 0x79C698: Py_Mods 0x852914: P_D3D8_Dev + 0x850258: P_D3D8_ZBuffer + 0x850408: P_D3D8_BackBuffer 0x7FCC00: N_Paks_opened 0x7fcbec: Hash_Index_Size 0x7fcbf0: P_Hash_Index @@ -36,7 +47,27 @@ flags: 0x7fadd8: is_python 0x7fc084: pak_lock 0x7fbe7c: current_language - 0x7d2094: py_refcnt_unk + 0x7d2094: refcnt_Py_None + 0x7fa830: P_Window + 0x7fadd0: P_PyExecute + 0x84d3ec: Py_Initialized + 0x8c8f10: Py_Debug + 0x84d3e8: Py_Verbose + 0x84db38: Py_Optimize + 0x84dd60: Py_interpr + 0x7fae38: Debug_Level + 0x7fae40: Console_Out_Buffer_132_23 + 0x7fbe20: Console_Curr_Line + 0x84db30: Py_Dummy + 0x8ca2c4: cmdline + 0x8c6350: module_filename + 0x8c6140: P_module_filename + 0x853954: P_D3DApp + 0x853091: N_Uniones + + # 0x7fbe24: + # 0x7fa778: + # 0x8c8d78: VMTs: 0x78d4d8: Py_entity @@ -56,133 +87,309 @@ VMTs: 0x7933ac: 3d_Gfx 0x7933a0: NodeFX +classes: + World: + types: - - "struct PyMethodDef { char *ml_name; void *ml_meth; int ml_flags; char *ml_doc;};" + - "struct PyMethodDef { char* ml_name; void* ml_meth; int ml_flags; char* ml_doc;};" - "struct GameVar { struct GameVar* next; const char* name; const char* desc; uint64_t d_type; void* value; void* def_value; };" - "struct HT_Entry { void* data; const char* key; struct HT_Entry* next;};" - "struct PakEntry { unsigned char* filename; bool locked; void* data; uint32_t seek;};" - "struct HashIndexEntry { uint32_t offset; uint32_t size; uint32_t status; const char* name; struct HashIndexEntry* next; };" - "struct HashIndex { uint32_t size; struct HashIndexEntry** data; };" - - "struct HashTableEntry { void* data; const char *key; struct HashTableEntry* next; };" + - "struct HashTableEntry { void* data; const char* key; struct HashTableEntry* next; };" - "struct HashTable { uint32_t size; struct HashTableEntry** data; };" - -function_signatures: - 0x5A8390: "int PyRun_SimpleString(const char* command);" - 0x5BB9D0: "int PyArg_ParseTuple(void* PyObj, char* format, ...);" - 0x413ee0: "int dbg_log(const char* fmt,...);" - 0x4134C0: "int write_log(unsigned int color, const char* msg);" - 0x47C1E0: "int ht_hash_ent_list(const char* str);" - 0x404BB0: "int ht_hash_ent(const char* str);" - 0x4016F0: "int reg_get_val(const char* value);" - 0x414280: "int prepare_html_log(const char* filename);" - 0x6597d0: "bool read_ini_entry(void* dest,const char* key, const char* section);" - 0x5A8FB0: "void* Py_InitModule(const char* name,void* methods);" - 0x5E3800: "int fopen_from_pak(const char* filename);" - 0x419950: "int fopen_2(const char* filename);" - 0x41AB50: "int open_pak(const char* filename, int unk_1,void* unk_ptr);" - 0x404460: "int register_c_callback(const char* name,void* func);" - 0x414070: "void throw_assertion_2(const char* check,const char* file,const char* date, unsigned int line);" - 0x5FBC50: "void throw_assertion_1(const char* check,const char* file, unsigned int line);" - 0x5BC140: "static char* convertsimple1(void *arg, char **p_format, void *p_va);" - 0x5E3800: "int32_t fopen_from_pak(const char* filename,const char* mode);" - 0x5a90f0: "void* Py_BuildValue(const char* format, ...);" - 0x5B9E70: "void* PyObject_GetAttrString(void* obj, const char* attr);" - + - "struct va_list { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; };" functions: - 0x6B1C70: strcmp - 0x5BB9D0: PyArg_ParseTuple - 0x5DD510: init_engine_3d - 0x401180: create_window - 0x401240: create_main_window - 0x4016F0: reg_get_val - 0x4134C0: write_log - 0x414280: prepare_html_log - 0x418220: get_version_info - 0x4137E0: write_html_log - 0x402190: handle_console_input - 0x5F9520: handle_render_console_input - 0x404A50: find_entity - 0x47C1E0: ht_hash_ent_list - 0x404BB0: ht_hash_ent - 0x404460: register_c_callback - 0x417470: load_game - 0x5E3800: fopen_from_pak - 0x5e3500: fopen - 0x403370: init_debug - 0x401770: init - 0x4026D0: init_py - 0x405B40: init_py_sub - 0x5A8FB0: Py_InitModule - 0x41AB50: open_pak - 0x5A8390: PyRun_SimpleString - 0x414570: setup_game_vars - 0x5FBC50: throw_assertion_1 - 0x414070: throw_assertion_2 - 0x5F7000: read_ini - 0x650F80: load_sm3 - 0x6665A0: load_m3d_1 - 0x666900: load_m3d_2 - 0x479B20: world_constructor - 0x479B40: init_world - 0x402510: deinit_world - 0x479870: make_world - 0x602A70: render_frame - 0x6B738C: handle_exception - 0x5B9E70: PyObject_GetAttrString - 0x413ee0: dbg_log - 0x5f75e0: init_d3d - 0x63a2f0: gdi_draw_line - 0x5e3250: read_stream - 0x5e3bb0: read_stream_wrapper - 0x50b9b0: init_scorer - 0x582e10: init_action_class_list - 0x528910: init_sound_sys - 0x5268d0: try_init_sound_sys - 0x404280: cPyFunction_set_func - 0x414680: load_config - 0x414810: save_config - 0x4f42a0: close_server_socket - 0x4f4d10: close_server - 0x4f48e0: close_client - 0x4f4fb0: is_server - 0x4f4a10: is_client - 0x4fac50: is_master - 0x526910: close_sound_sys - 0x526520: shutdown_sound_sys - 0x5dd700: close_3d_engine - 0x5a7320: close_window - 0x5dff20: set_exception_handler - 0x5a7f20: get_console_wnd - 0x5a73a0: show_console - 0x666c60: read_m3d - 0x417df0: snprintf - 0x5fc930: printf - 0x6597d0: read_ini_entry - 0x5fc0a0: engine_debug_log - 0x5a7440: create_console_window - 0x6114e0: setup_window - 0x404420: clear_functions - 0x405ca0: close_py_subsys - 0x50bcb0: close_scorer - 0x479b20: close_world - 0x582e70: close_action_class - 0x50b6a0: get_scorer - 0x50ea20: scorer_parse_type - 0x636580: list_models - 0x5a90f0: Py_BuildValue - 0x41c5a0: has_lst_file - 0x5a8e90: py_error - 0x5a9890: get_module_dict - 0x5c7bb0: get_current_thread - 0x5aa140: preload_lib - 0x413c10: sprintf - 0x405850: check_is_python - 0x47bf90: setup_ent_list - 0x474f80: ent_list_get_set + 0x6283a0: + name: load_emi + 0x4fa9f0: + name: send_pkt + 0x5ca9e0: + signature: void* PyFrame_New(void* thread_state, void* code_object,void* globals, void* locals) + name: PyFrame_New + 0x5bcae0: + signature: void PyErr_SetString(void* obj, const char* err_msg); + name: PyErr_SetString + 0x5cb040: + signature: void* eval_code2(void* dict, const char* key, void* item); + name: eval_code2 + 0x5e3c50: + convention: cdecl-thiscall-ms + name: read_int + 0x5e3b50: + convention: cdecl-thiscall-ms + name: read_block_header + 0x5c66d0: + signature: void initerrors(void* dict); + name: initerrors + 0x5bb370: + signature: int PyDict_SetItemString(void* dict, const char* key, void* item); + name: PyDict_SetItemString + 0x5b9960: + signature: void* PyObject_NEW(void* type, void* typeobj); + name: PyObject_NEW + 0x4145e0: + convention: cdecl-thiscall-ms + signature: bool get_config_var(char* name); + name: get_config_var + 0x413470: + signature: void init_logging(); + name: init_logging + 0x5a8040: + signature: void Py_Initialize(); + name: Py_Initialize + 0x5bb4e0: + name: PyModule_GetDict + signature: void* PyModule_GetDict(void*); + 0x5c6610: + name: _PyBuiltin_Init_1 + signature: void* _PyBuiltin_Init_1(); + 0x5b5db0: + name: PyString_FromString + signature: void* PyString_FromString(const char*); + 0x5ba3a0: + name: PyDict_New + signature: void* PyDict_New(); + 0x5c7bd0: + name: PyThreadState_Swap + signature: void* PyThreadState_Swap(void* new); + 0x5c7870: + name: PyInterpreterState_New + signature: void* PyInterpreterState_New(); + 0x5c79b0: + name: PyThreadState_New + signature: void* PyThreadState_New(void* interp); + 0x6ad1e9: + name: getenv + signature: char* getenv(char* var); + 0x401180: + name: create_window + 0x401240: + name: create_main_window + 0x4016f0: + name: reg_get_val + signature: int reg_get_val(const char* value); + 0x401770: + name: init + 0x402190: + name: handle_console_input + signature: int handle_console_input(const char* input); + 0x402510: + name: deinit_world + 0x4026d0: + name: init_py + 0x403370: + name: init_engine + 0x4037e0: + name: init_debug + 0x404280: + name: cPyFunction_set_func + 0x404420: + name: clear_functions + 0x404460: + name: register_c_callback + signature: int register_c_callback(const char* name,void* func); + 0x404a50: + name: find_entity + 0x404bb0: + name: ht_hash_ent + signature: int ht_hash_ent(const char* str); + 0x405850: + name: check_is_python + 0x405b40: + name: init_py_sub + 0x405ca0: + name: close_py_subsys + 0x4134c0: + signature: int write_log(unsigned int color, const char* msg); + name: write_log + 0x4137e0: + signature: void write_html_log(const char* fmt,...); + name: write_html_log + 0x413c10: + name: sprintf + 0x413ee0: + name: dbg_log + signature: int dbg_log(const char* fmt,...); + 0x414070: + name: throw_assertion_2 + signature: void throw_assertion_2(const char* check,const char* file,const char* date, unsigned int line); + 0x414280: + name: prepare_html_log + signature: int prepare_html_log(const char* filename); + 0x414570: + name: setup_game_vars + 0x414680: + name: load_config + 0x414810: + name: save_config + 0x417470: + name: load_game + 0x417df0: + name: snprintf_1 + 0x417d80: + name: snprintf_2 + 0x418220: + name: get_version_info + 0x419950: + name: fopen_2 + signature: int fopen_2(const char* filename); + 0x41ab50: + name: open_pak + signature: int open_pak(const char* filename, int unk_1,void* unk_ptr); + 0x41c5a0: + name: has_lst_file + signature: int has_lst_file(void* unk_ptr); + 0x474f80: + name: ent_list_get_set + signature: bool ent_list_get_set(const char* name); + 0x479870: + name: make_world + 0x479b20: + name: close_world + 0x479b40: + name: init_world + 0x47bf90: + convention: cdecl-thiscall-ms + name: create_ent_list + signature: bool create_ent_list(const char* name); + 0x47c1e0: + name: ht_hash_ent_list + signature: int ht_hash_ent_list(const char* str); + 0x4f42a0: + name: close_server_socket + 0x4f48e0: + name: close_client + 0x4f4a10: + name: is_client + 0x4f4d10: + name: close_server + 0x4f4fb0: + name: is_server + 0x4fac50: + name: is_master + 0x50b6a0: + name: get_scorer + 0x50b9b0: + name: init_scorer + 0x50bcb0: + name: close_scorer + 0x50ea20: + name: scorer_parse_type + 0x526520: + name: shutdown_sound_sys + 0x5268d0: + name: try_init_sound_sys + 0x526910: + name: close_sound_sys + 0x528910: + name: init_sound_sys + 0x582e10: + name: init_action_class_list + 0x582e70: + name: close_action_class + 0x5a7320: + name: close_window + 0x5a73a0: + name: show_console + 0x5a7440: + name: create_console_window + 0x5a7f20: + name: get_console_wnd + 0x5a8390: + name: PyRun_SimpleString + signature: int PyRun_SimpleString(const char* command); + 0x5a8e90: + name: Py_FatalError + signature: void Py_FatalError(const char* msg); + 0x5a8fb0: + name: Py_InitModule + signature: void* Py_InitModule(const char* name,void* methods); + 0x5a90f0: + name: Py_BuildValue + signature: void* Py_BuildValue(const char* format, ...); + 0x5a9890: + name: PyImport_GetModuleDict + signature: void* PyImport_GetModuleDict(); + 0x5aa140: + name: preload_lib + 0x5b9e70: + name: PyObject_GetAttrString + signature: void* PyObject_GetAttrString(void* obj, const char* attr); + 0x5bb9d0: + name: PyArg_ParseTuple + signature: int PyArg_ParseTuple(void* PyObj, char* format, ...); + 0x5bc140: + name: convertsimple1 + signature: static char* convertsimple1(void* arg, char** p_format, void* va_list); + 0x5bc0f0: + name: convertsimple + signature: static char* convertsimple(void* arg, char** p_format, char* msgbuf); + 0x5bbf60: + name: converttuple + signature: static char* converttuple(void* arg, char** p_format, void* va_list, int* levels, char* msgbuf, int toplevel); + 0x5bbee0: + name: convertitem + signature: static char* convertitem(void* arg, char** p_format, void* va_list, int* levels, char* msgbuf); + 0x5c7bb0: + name: PyThreadState_Get + signature: void* PyThreadState_Get(); + 0x5dd510: + name: init_engine_3d + 0x5dd700: + name: close_3d_engine + 0x5dff20: + name: set_exception_handler + 0x5e3250: + name: read_stream + 0x5e3500: + name: fopen + 0x5e3800: + name: fopen_from_pak + signature: int fopen_from_pak(const char* filename,const char* mode); + 0x5e3bb0: + name: read_stream_wrapper + 0x5f7000: + name: read_ini + 0x5f75e0: + name: init_d3d + 0x5f9520: + name: handle_render_console_input + 0x5fbc50: + name: throw_assertion_1 + signature: void throw_assertion_1(const char* check,const char* file, unsigned int line); + 0x5fc0a0: + name: engine_debug_log + 0x5fc930: + name: printf + 0x602a70: + name: render_frame + 0x6114e0: + name: setup_window + 0x636580: + name: list_models + 0x63a2f0: + name: gdi_draw_line + 0x650f80: + name: load_sm3 + 0x6597d0: + name: read_ini_entry + signature: bool read_ini_entry(void* dest,const char* key, const char* section); + 0x6665a0: + name: load_m3d_1 + 0x666900: + name: load_m3d_2 + 0x666c60: + name: read_m3d + 0x6b1c70: + name: strcmp + signature: bool strcmp(const char* s1,const char* s2); + 0x6b738c: + name: handle_exception script: | e asm.cmt.right = true - e cmd.stack = true e scr.utf8 = true e asm.describe = false e graph.cmtright = true diff --git a/file_formats/ai_path.md b/file_formats/ai_path.md new file mode 100644 index 0000000..dd07992 --- /dev/null +++ b/file_formats/ai_path.md @@ -0,0 +1,22 @@ +# Structure of Graph + +```cpp +template +struct Node { + float pos[n], +} + +template +struct Edge { + uint32_t num_edge_nodes, + Node nodes[], +} + +template +struct Graph { + uint32_t num_nodes, + Node nodes[], + uint32_t num_edges, + Edge edges[], +} +``` diff --git a/file_formats/chunked.md b/file_formats/chunked.md new file mode 100644 index 0000000..e906838 --- /dev/null +++ b/file_formats/chunked.md @@ -0,0 +1,126 @@ +# General Block format + +```cpp +struct Block { + unsigned char block_id[4], + uint32_t size, + unsigned char data[size], +} + +template +struct Block { + unsigned char block_id[4], + uint32_t size, + T data, +} +``` + +# Block IDs + + +| File Extension | Description | +|----------------|--------------------------| +| .cm3 | Animation file | +| .sm3 | 3d model file | +| .dum | Dummy (map object) file | +| .pth | AI Path | +| .emi | Emission maps/Materials? | +| .amc | ??? | + +| File ID | Chunk IDs | +|---------|--------------------------------------------------------------------------| +| AMC | AMC, CMSH, QUAD | +| CM3 | ANI, CM3, EVA, NAE, NAM, SCN | +| DUM | DUM, INI | +| EMI | EMI, LFVF, MAP, MAT, TRI | +| SM3 | ANI, CAM, INI, LFVF, LUZ, MAP, MAT, MD3D, NAE, NAM, PORT, SCN, SM3, SUEL | + +| Chunk ID | Description | +|----------|-----------------------------| +| ANI | Animation data? | +| AMC | Collision Data | +| CMSH | Mesh data? | +| INI | INI-Configuration data | +| LUZ | Lighting information | +| MAT | Material information | +| QUAD | Mesh data? | +| SCN | Scene data? | +| CAM | Camera info? | +| PORT | Light portals? | +| SUEL | Ground plane? | +| DUM | Dummy (map object) data | +| MAP | UV Map? | +| LFVF | FVF Vertex Data | +| TRI | Triangle strip definitions? | +| NAM,NAE | Animation Data? | + +# Format of Specific chunks + +## INI + +Configuration Data + +```cpp +struct INI { + uint32_t num_sections, + struct { + uint32_t num_lines, + struct { + uint32_t num_chars, + char line[num_chars] + } lines[num_lines], + } sections[num_sections] +} +``` + + +## LFVF + +DirectX Flexible Vertex Format Data + +```cpp +struct Vertex { // fields according to flags + float position[3], + float rhw, + float weights[3], + float normal[3], + float point_size, + uint32_t diffuse, //RGBA + uint32_t specular, //RGBA + float tex_coords[1 to 4][8] // ??? decided by flags? +} + +struct LFVF { + uint32_t unk, + uint32_t num_entries, + struct { + uint32_t FVF, // FVF vertex data configuration + uint32_t vert_size //?, + uint32_t num_verts, + Vertex vertices[num_vers] + } entry[num_entries] +} +``` + +## DUM + +Map object data + +```cpp +struct DUM { + uint32_t unk_1, + uint32_t num_dummies, + uint32_t unk_2, + struct { + uint32_t name_length, + char name[name_length], + float position[3], + float rotation[3], + uint32_t has_ini, + if (has_ini) { + Block ini + }, + uint32_t unk_1 // has_next? + } sections[num_sections] +} +``` \ No newline at end of file diff --git a/file_formats/packed.md b/file_formats/packed.md new file mode 100644 index 0000000..542feee --- /dev/null +++ b/file_formats/packed.md @@ -0,0 +1,15 @@ +# Format +```cpp +struct Packed { + unsigned char magic[4], // always BFPK + uint32_t version, + uint32_t number_of_files, + struct File { + uint32_t path_length, + char path[path_length], // latin1 encoding + uint32_t data_size, + uint32_t data_offset + } files[number_of_files], + char data[] +} +``` \ No newline at end of file diff --git a/frida/frida_hook_net.js b/frida/frida_hook_net.js new file mode 100644 index 0000000..27cc12f --- /dev/null +++ b/frida/frida_hook_net.js @@ -0,0 +1,60 @@ +var sendto = Module.getExportByName("WSOCK32.dll", "sendto") +var recvfrom = Module.getExportByName("WSOCK32.dll", "recvfrom") + +Interceptor.attach(ptr("0x004f9300"), { + onEnter: function (args) { + console.log("[SendUsrString]", JSON.stringify({ + data: args[0].readCString(), + dst: args[1].toInt32(), + chat: args[2].toInt32() + })); + } +}) + +Interceptor.attach(ptr(sendto), { + onEnter: function (args) { + this.socket = args[0]; + this.buffer = args[1]; + this.size = args[2].toInt32(); + this.flags = args[3].toInt32(); + this.sock_addr = args[4]; + this.to_len = args[5].toInt32(); + }, + onLeave: function (ret) { + var port = this.sock_addr.add(2).readU16(); + var addr = this.sock_addr.add(4).readU32(); + var data = Memory.readByteArray(this.buffer, ret.toInt32()) + send({ + type: "SEND", + ptr: this.buffer.toInt32(), + addr, + port + }, data); + return ret; + } +}) + +Interceptor.attach(ptr(recvfrom), { + onEnter: function (args) { + this.socket = args[0]; + this.buffer = args[1]; + this.size = args[2].toInt32(); + this.flags = args[3].toInt32(); + this.sock_addr = args[4]; + this.from_len = args[5].toInt32(); + }, + onLeave: function (ret) { + if (!ret.equals(ptr("0xffffffff"))) { + var port = this.sock_addr.add(2).readU16(); + var addr = this.sock_addr.add(4).readU32(); + var data = Memory.readByteArray(this.buffer, ret.toInt32()) + send({ + type: "RECV", + ptr: this.buffer.toInt32(), + addr, + port + }, data); + } + return ret; + } +}) \ No newline at end of file diff --git a/frida/frida_hook_read_trace.js b/frida/frida_hook_read_trace.js new file mode 100644 index 0000000..6918d88 --- /dev/null +++ b/frida/frida_hook_read_trace.js @@ -0,0 +1,174 @@ +var pak_files = {} +var ftypes = {} +var record=false; + +var current_block_id; +var filename; +var t0 = performance.now(); + +Interceptor.attach(ptr("0x5e3b50"), { //read_block_header + onEnter: function (args) { + filename = pak_files[this.context.ecx] || this.context.ecx; + current_block_id = args[0].readUtf8String(); + }, + + onLeave: function(ret) { + return ret; + } +}) + +// Interceptor.attach(ptr("0x5e3c50"), { // read_block_id +// onEnter: function (args) { +// var filename=pak_files[this.context.ecx]||this.context.ecx; +// var id=args[1].readUtf8String(); +// console.log("[+read_block("+filename+")]",id,args[1]); +// }, + +// // onLeave: function(ret) { +// // console.log("[-read_ini_block] Ret:",ret); +// // } +// }) + +Interceptor.attach(ptr("0x7B43B020"),{ + onEnter: function(args) { + var info={}; + info['this']=args[0]; + info['Length']=args[1]; + info['Usage']=args[2]; + info['FVF']=args[3]; + info['Pool']=args[4]; + info['ppVertexBuffer']=args[4]; + send({CreateVertexBuffer:info}); + } +}) + +Interceptor.attach(ptr("0x5e3bb0"), { // read_stream_wrapper + onEnter: function (args) { + this.args = {}; + this.args[0] = args[0]; + this.args[1] = args[1]; + this.timestamp = performance.now()-t0; + }, + onLeave: function (ret) { + var data=Memory.readByteArray(this.args[0],this.args[1].toInt32()); + var stack = Thread.backtrace(this.context,Backtracer.ACCURATE); + var obj={ + filename, + timestamp: this.timestamp, + block_id: current_block_id, + stack + }; + send(obj,data); + } +}) + + +Interceptor.attach(ptr("0x5e3800"), { // fopen_from_pak + onEnter: function (args) { + this.filename = args[0].readUtf8String(); + }, + onLeave: function (ret) { + if (ret != 0) { + pak_files[ret] = this.filename; + } + } +}) + +// Interceptor.attach(ptr("0x5e3c50"), { // read_block_id +// onEnter: function (args) { +// console.log("[+read]",args[0],args[1]); +// }, +// onLeave: function(ret) { +// console.log("[-read] Ret:",ret); +// } +// }) + +// Interceptor.attach(ptr("0x6665a0"), { // load_m3d_1 +// onEnter: function (args) { +// console.log("[M3D_1]",args[0].readUtf8String()); +// } +// }) + + +// Interceptor.attach(ptr("0x666900"), { // load_m3d_2 +// onEnter: function (args) { +// console.log("[M3D_2]",args[0].readUtf8String()); +// } +// }) + + +function dasm(addr, size) { + var size = size || 8; + var offset = 0; + var ret = []; + while (ret.length != size) { + var inst = Instruction.parse(ptr(addr).add(offset)); + ret.push(("[" + inst.address + "] " + inst.mnemonic + " " + inst.opStr).trim()); + offset += inst.size; + } + return ret; +} + +function r(addr, options) { + var options = options || {} + var max_depth = options.max_depth || 4; + var num = options.num || 4; + var ret = {}; + var vals = [ + "S8", + "U8", + "S16", + "U16", + "S32", + "U32", + "Float", + "Double", + "Pointer", + "CString", + "Utf8String", + "Utf16String", + "AnsiString" + ]; + vals.forEach(function (k) { + try { + ret[k] = ptr(addr)['read' + k]() + } catch (e) { + ret[k] = undefined; + } + }) + try { + ret["code"] = dasm(addr, 8); + } catch (e) { + ret["code"] = undefined; + } + + if (max_depth > 1) { + var p = {}; + var read_ptr = false; + for (var i = 0; i < num; ++i) { + if (ret["Pointer"] === undefined) { + continue; + } + p[i * Process.pointerSize] = r(ret["Pointer"].add(i * Process.pointerSize), { + max_depth: max_depth - 1, + num + }); + read_ptr = true; + } + if (read_ptr) { + ret["p"] = p; + } + } + return ret; +} + + +// function test() { +// for (var p = 0; p < 4; ++p) { +// var player = ptr(0x7FE944).readPointer().add(0x288 + p * 4).readPointer(); +// if (!player.isNull()) { +// console.log("Player " + (p+1) + ":", player); +// console.log(JSON.stringify(r(player),null,4)); +// } +// } +// } diff --git a/frida/frida_inject_net.py b/frida/frida_inject_net.py new file mode 100644 index 0000000..ba09b9d --- /dev/null +++ b/frida/frida_inject_net.py @@ -0,0 +1,56 @@ +import frida +import psutil +from binascii import hexlify +import subprocess as SP +import string +import ipaddress +from dissect_net import packet,printable_chars,hexdump,is_printable + +def on_message(msg, data=None): + if not data: + return + msg = msg["payload"] + IP = ipaddress.IPv4Address(msg["addr"]) + IP = ipaddress.IPv4Address(IP.packed[::-1]) + direction = msg["type"] + port = msg["port"] + ptr = msg["ptr"] + + with open("netlog.txt","a",encoding="utf8") as of: + print( + "{} {}:{} 0x{:x} {}".format(msg["type"], IP, port, ptr, str(hexlify(data),"utf8")), + file=of + ) + + if is_printable(data): + print(direction, addr, buffer_addr, data) + return + + try: + parsed_data = packet.parse(data) + print( + "{} {}:{} 0x{:x}".format(msg["type"], IP, port, ptr) + ) + print(hexdump(data)) + print(parsed_data) + print() + except Exception as e: + print(e) + pass + +def main(): + pid = frida.spawn(sys.argv[1:]) + session = frida.attach(pid) + session.enable_jit() + script = session.create_script(open("frida_hook_net.js").read()) + open(f"netlog.txt","w",encoding="utf8").close() + script.on("message", on_message) + script.load() + frida.resume(pid) + proc = psutil.Process(pid) + proc.wait() + session.detach() + + +if __name__ == "__main__": + main() diff --git a/frida/frida_inject_read_trace.py b/frida/frida_inject_read_trace.py new file mode 100644 index 0000000..ec888eb --- /dev/null +++ b/frida/frida_inject_read_trace.py @@ -0,0 +1,59 @@ +from __future__ import print_function +import frida +import os +import sys +import psutil +import binascii +import sqlite3 +import json +import time +import msgpack +from multiprocessing import JoinableQueue +import threading + + +q = JoinableQueue() + + +def db_worker(q): + with open("dump.mp", "wb") as of: + while True: + args = q.get() + if args is None: + q.task_done() + break + msgpack.dump(args, of) + q.task_done() + + +db_w = threading.Thread(target=db_worker, args=(q,)) + +db_w.start() + + +def on_message(msg, data): + filename = msg.get("payload", {}).get("filename", "").replace("\\", "/") + block_id = msg.get("payload", {}).get("block_id", "") + print(filename,block_id,data) + msg["payload"]["data"] = data + q.put(msg["payload"]) + + +def main(): + pid = frida.spawn(sys.argv[1:]) + session = frida.attach(pid) + script = session.create_script(open("frida_hook_read_trace.js").read()) + script.on("message", on_message) + script.load() + frida.resume(pid) + proc = psutil.Process(pid) + proc.wait() + session.detach() + q.put(None) + q.join() + q.close() + db_w.join() + + +if __name__ == "__main__": + main() diff --git a/frida/frida_mem_mon.js b/frida/frida_mem_mon.js new file mode 100644 index 0000000..ccb6537 --- /dev/null +++ b/frida/frida_mem_mon.js @@ -0,0 +1,8 @@ +MemoryAccessMonitor.enable({ + base: ptr("0x7fe944"), + size: 4 +}, { + onAccess: function (details) { + console.log(details.operation, details.from, details.address) + }, +}) \ No newline at end of file diff --git a/frida/frida_mem_mon.py b/frida/frida_mem_mon.py new file mode 100644 index 0000000..a1602ab --- /dev/null +++ b/frida/frida_mem_mon.py @@ -0,0 +1,22 @@ +import frida +import sys +import psutil + +def on_message(msg, data=None): + print(msg,data) + + +def main(): + pid = frida.spawn(sys.argv[1:]) + session = frida.attach(pid) + session.enable_jit() + script = session.create_script(open("frida_mem_mon.js").read()) + script.on("message", on_message) + script.load() + frida.resume(pid) + proc = psutil.Process(pid) + proc.wait() + session.detach() + +if __name__ == "__main__": + main() diff --git a/frida/frida_stalker_test.js b/frida/frida_stalker_test.js new file mode 100644 index 0000000..81d8542 --- /dev/null +++ b/frida/frida_stalker_test.js @@ -0,0 +1,43 @@ +var stalked_threads = []; +var excluded_modules = [] +var sent=false; +setInterval(() => { + Process.enumerateModules().forEach(mod => { + if (mod.name == "Scrap.exe") { + if (!sent) { + send({ + mod: mod + }) + sent=true; + } + return; + } + if (excluded_modules.indexOf(mod.name) == -1) { + Stalker.exclude(mod); + excluded_modules.push(mod.name); + } + }) + Process.enumerateThreads().forEach(thread => { + if (stalked_threads.indexOf(thread.id) != -1) { + return; + } + Stalker.follow(thread.id, { + events: { + call: true, + block: true, + compile: true, + ret: true, + exec: true + }, + onReceive: function (events) { + send({ + stalker: Stalker.parse(events, { + annotate: true, + stringify: true + }) + }); + } + }) + stalked_threads.push(thread.id); + }) +}, 0) \ No newline at end of file diff --git a/frida/frida_stalker_test.py b/frida/frida_stalker_test.py new file mode 100644 index 0000000..586dd90 --- /dev/null +++ b/frida/frida_stalker_test.py @@ -0,0 +1,67 @@ +import frida +import sys +import psutil +import subprocess as SP +import threading +from multiprocessing import JoinableQueue +import msgpack + + +q = JoinableQueue() + + +def db_worker(q): + events = 0 + with open("trace.mp", "wb") as of: + while True: + args = q.get() + if args is None: + q.task_done() + break + events += 1 + msgpack.dump(args, of) + q.task_done() + print("Wrote", events, "events") + + +db_w = threading.Thread(target=db_worker, args=(q,)) + +db_w.start() +modules = {} +mem_range = None + + +def on_message(msg, data=None): + global mem_range + data = msg["payload"] + if "stalker" in data: + for val in data["stalker"]: + q.put(val) + + +def main(): + pid = frida.spawn(sys.argv[1:]) + session = frida.attach(pid) + session.enable_jit() + script = session.create_script(open("frida_stalker_test.js").read()) + script.on("message", on_message) + script.load() + frida.resume(pid) + proc = psutil.Process(pid) + proc.wait() + session.detach() + q.put(None) + q.join() + q.close() + db_w.join() + + +""" +import msgpack as mp +from collections import Counter +data=list(mp.Unpacker(open("trace.mp","rb"), raw=False)) +Counter(v[1] for v in data).most_common(10) +""" + +if __name__ == "__main__": + main() diff --git a/parse_save.py b/parse_save.py deleted file mode 100644 index 3810f23..0000000 --- a/parse_save.py +++ /dev/null @@ -1,21 +0,0 @@ -import sys -from construct import * -from pprint import pprint - -ScrapSaveVar = Struct( - "name" / PascalString(Int32ul, encoding="utf-8"), - "data" / PascalString(Int32ul, encoding="utf-8"), -) -ScrapSave = "ScarpSaveGame" / Struct( - "title" / PascalString(Int32ul, encoding="utf-8"), - "id" / PascalString(Int32ul, encoding="utf-8"), - "data" / PrefixedArray(Int32ul, ScrapSaveVar), - Terminated, -) -with open(sys.argv[1], "rb") as sav_file: - save = ScrapSave.parse_stream(sav_file) - print("ID:", save.id) - print("Title:", save.title) - for var in save.data: - print(" {}: {}".format(var.name, var.data)) - diff --git a/r2_analyze.py b/r2_analyze.py index d5ac4b9..24d2ff0 100644 --- a/r2_analyze.py +++ b/r2_analyze.py @@ -4,19 +4,19 @@ import json from datetime import datetime import subprocess as SP from tqdm import tqdm -from pprint import pprint -import os import sys import yaml +tqdm_ascii = False + r2cmds = [] -x64_dbg_script=[] +x64_dbg_script = [] script_path = os.path.dirname(os.path.abspath(__file__)) scrap_exe = os.path.abspath(sys.argv[1]) scrapland_folder = os.path.abspath(os.path.dirname(scrap_exe)) -r2_script_path=os.path.join(scrapland_folder, "scrap_dissect.r2") -x64_dbg_script_path=os.path.join(scrapland_folder, "scrap_dissect.x32dbg.txt") -json_path=os.path.join(scrapland_folder, "scrap_dissect.json") +r2_script_path = os.path.join(scrapland_folder, "scrap_dissect.r2") +x64_dbg_script_path = os.path.join(scrapland_folder, "scrap_dissect.x32dbg.txt") +json_path = os.path.join(scrapland_folder, "scrap_dissect.json") assert os.path.isfile(scrap_exe), "File not found!" r2 = r2pipe.open(scrap_exe) @@ -24,19 +24,22 @@ file_hashes = r2.cmdj("itj") target_hashes = { "sha1": "d2dde960e8eca69d60c2e39a439088b75f0c89fa", "md5": "a934c85dca5ab1c32f05c0977f62e186", + "sha256": "24ef449322f28f87b702834f1a1aac003f885db6d68757ff29fad3ddba6c7b88", } assert file_hashes == target_hashes, "Hash mismatch" -def x64_dbg_label(addr,name,prefix=None): + +def x64_dbg_label(addr, name, prefix=None): global x64_dbg_script - if isinstance(addr,int): - addr=hex(addr) + if isinstance(addr, int): + addr = hex(addr) if prefix: x64_dbg_script.append(f'lbl {addr},"{prefix}.{name}"') else: x64_dbg_script.append(f'lbl {addr},"{name}"') + def r2_cmd(cmd): global r2, r2cmds r2cmds.append(cmd) @@ -54,13 +57,15 @@ def r2_cmdJ(cmd): r2cmds.append(cmd) return r2.cmdJ(cmd) -t_start=datetime.today() + +t_start = datetime.today() + def analysis(full=False): print("[*] Running analysis") - steps=[] + steps = [] if full: - steps=[ + steps = [ "e anal.dataref = true", # "e anal.esil = true", "e anal.jmp.after = true", @@ -72,52 +77,58 @@ def analysis(full=False): "e anal.vinfun = true", "e asm.anal = true", ] - steps+=["aaaaa"] + if full: + steps += ["aaaa"] + else: + steps += ["aaa"] for ac in steps: print(f"[*] Running '{ac}'") r2_cmd(f"{ac} 2>NUL") -with open(os.path.join(script_path,"config.yml")) as cfg: + +with open(os.path.join(script_path, "config.yml")) as cfg: print("[*] Loading config") - config = type("Config",(object,),yaml.load(cfg,Loader=yaml.SafeLoader)) + config = type("Config", (object,), yaml.load(cfg, Loader=yaml.SafeLoader)) for line in config.script.strip().splitlines(): r2_cmd(line) analysis(False) -for addr,comment in config.comments.items(): +for addr, comment in config.comments.items(): r2_cmd(f"CC {comment} @ {hex(addr)}") for t in config.types: r2_cmd(f'"td {t}"') for addr, name in config.flags.items(): - x64_dbg_label(addr,name,"loc") + x64_dbg_label(addr, name, "loc") r2_cmd(f"f loc.{name} 4 {hex(addr)}") -for addr, name in config.functions.items(): - x64_dbg_label(addr,name,"fcn") - r2_cmd(f"afr fcn.{name} {hex(addr)}") - -for addr,sig in config.function_signatures.items(): - r2_cmd(f'"afs {config.function_signatures[addr]}" @{hex(addr)}') - +for addr, func in config.functions.items(): + name, sig = func.get("name"), func.get("signature") + if name: + x64_dbg_label(addr, name, "fcn") + r2_cmd(f"afr fcn.{name} {hex(addr)}") + r2_cmd(f"afn fcn.{name} {hex(addr)}") + if sig: + sig = sig.replace(name, "fcn." + name) + r2_cmd(f'"afs {sig}" @{hex(addr)}') def vtables(): ret = {} print("[*] Analyzing VTables") vtables = r2_cmdJ("avj") - for c in tqdm(vtables, ascii=True): + for c in tqdm(vtables, ascii=tqdm_ascii): methods = [] - name=config.VMTs.get(c.offset,f"{c.offset:08x}") - x64_dbg_label(c.offset,name,"vmt") + name = config.VMTs.get(c.offset, f"{c.offset:08x}") + x64_dbg_label(c.offset, name, "vmt") r2_cmd(f"f vmt.{name} 4 {hex(c.offset)}") - for idx,m in enumerate(tqdm(c.methods, ascii=True, leave=False)): + for idx, m in enumerate(tqdm(c.methods, ascii=tqdm_ascii, leave=False)): methods.append(hex(m.offset)) - x64_dbg_label(m.offset,f"{name}.{idx}","fcn.vmt") + x64_dbg_label(m.offset, f"{name}.{idx}", "fcn.vmt") r2_cmd(f"afr fcn.vmt.{name}.{idx} {hex(m.offset)} 2>NUL") ret[hex(c.offset)] = methods return ret @@ -127,14 +138,14 @@ def c_callbacks(): print("[*] Parsing C Callbacks") funcs = {} res = r2_cmd("/r fcn.register_c_callback ~CALL[1]").splitlines() - for addr in tqdm(res, ascii=True): + for addr in tqdm(res, ascii=tqdm_ascii): r2_cmd(f"s {addr}") r2_cmd(f"so -3") func, name = r2_cmdJ(f"pdj 2") func = func.refs[0].addr name = r2_cmd(f"psz @{hex(name.refs[0].addr)}").strip() r2_cmd(f"afr fcn.callbacks.{name} {hex(func)} 2>NUL") - x64_dbg_label(func,f"{name}","fcn.callbacks") + x64_dbg_label(func, f"{name}", "fcn.callbacks") funcs[name] = hex(func) return funcs @@ -142,22 +153,22 @@ def c_callbacks(): def assertions(): assertions = {} for (n_args, a_addr) in [ - (4, "fcn.throw_assertion_1"), - (3, "fcn.throw_assertion_2"), + (3, "fcn.throw_assertion_1"), + (4, "fcn.throw_assertion_2"), ]: print(f"[*] Parsing C assertions for {a_addr}") res = r2_cmd(f"/r {a_addr} ~CALL[1]").splitlines() print() - for line in tqdm(res, ascii=True): + for line in tqdm(res, ascii=tqdm_ascii): addr = line.strip() r2_cmd(f"s {addr}") r2_cmd(f"so -{n_args}") - dis=r2_cmdJ(f"pij {n_args}") + dis = r2_cmdJ(f"pij {n_args}") if n_args == 4: - file, msg, date, line = dis + line, date, file, msg = dis elif n_args == 3: date = None - file, msg, line = dis + line, file, msg = dis try: file = r2_cmd(f"psz @{file.refs[0].addr}").strip() msg = r2_cmd(f"psz @{msg.refs[0].addr}").strip() @@ -180,32 +191,35 @@ def bb_refs(addr): ret = {} res = r2_cmd(f"/r {addr} ~fcn[0,1]").splitlines() print() - for ent in res: + for ent in tqdm(res, ascii=tqdm_ascii): func, hit = ent.split() ret[hit] = {"asm": [], "func": func} for ins in r2_cmdJ(f"pdbj @{hit}"): ret[hit]["asm"].append(ins.disasm) return ret + def world(): print("[*] Parsing World offsets") return bb_refs("loc.P_World") + def render(): print("[*] Parsing D3D_Device offsets") return bb_refs("loc.P_D3D8_Dev") + def py_mods(): print("[*] Parsing Python modules") res = r2_cmd("/r fcn.Py_InitModule ~CALL[1]").splitlines() print() py_mods = {} - for call_loc in tqdm(res, ascii=True): + for call_loc in tqdm(res, ascii=tqdm_ascii): r2_cmd(f"s {call_loc}") r2_cmd(f"so -3") args = r2_cmdJ("pdj 3") refs = [] - if not all([arg.type == "push" for arg in args]): + if not all(arg.type == "push" for arg in args): continue for arg in args: refs.append(hex(arg.val)) @@ -214,7 +228,7 @@ def py_mods(): name = r2_cmd(f"psz @{name}").strip() r2_cmd(f"s {methods}") r2_cmd(f"f py.{name} 4 {methods}") - x64_dbg_label(methods,f"{name}","py") + x64_dbg_label(methods, f"{name}", "py") py_mods[name] = {"methods_addr": methods, "doc": doc, "methods": {}} while True: m_name, m_func, _, m_doc = [v.value for v in r2_cmdJ(f"pfj xxxx")] @@ -223,14 +237,14 @@ def py_mods(): m_name, m_func, m_doc = map(hex, (m_name, m_func, m_doc)) m_name = r2_cmd(f"psz @{m_name}").strip() r2_cmd(f"f py.{name}.{m_name}.__doc__ 4 {m_doc}") - if int(m_doc,16)!=0: - x64_dbg_label(m_doc,f"{name}.{m_name}.__doc__","py") + if int(m_doc, 16) != 0: + x64_dbg_label(m_doc, f"{name}.{m_name}.__doc__", "py") m_doc = r2_cmd(f"psz @{m_doc}").strip() else: - m_doc=None + m_doc = None py_mods[name]["methods"][m_name] = {"addr": m_func, "doc": m_doc} r2_cmd(f"afr py.{name}.{m_name} {m_func} 2>NUL") - x64_dbg_label(m_func,f"{name}.{m_name}","fcn.py") + x64_dbg_label(m_func, f"{name}.{m_name}", "fcn.py") r2_cmd("s +16") return py_mods @@ -240,7 +254,7 @@ def game_vars(): print("[*] Parsing Game variables") res = r2_cmd("/r fcn.setup_game_vars ~CALL[1]").splitlines() print() - for line in tqdm(res, ascii=True): + for line in tqdm(res, ascii=tqdm_ascii): addr = line.strip() r2_cmd(f"s {addr}") args = r2_cmd("pdj -5") # seek and print disassembly @@ -259,27 +273,22 @@ def game_vars(): break if len(args_a) != 4: continue - if not all(["val" in v for v in args_a]): + if not all("val" in v for v in args_a): continue addr, name, _, desc = [v["val"] for v in args_a] name = r2_cmd(f"psz @{hex(name)}").strip() desc = r2_cmd(f"psz @{hex(desc)}").strip() addr = hex(addr) r2_cmd(f"f loc.gvar.{name} 4 {addr}") - x64_dbg_label(addr,f"{name}","loc.gvar") + x64_dbg_label(addr, f"{name}", "loc.gvar") ret[addr] = {"name": name, "desc": desc} return ret -ret = dict( - game_vars=game_vars(), - c_callbacks=c_callbacks(), - py_mods=py_mods(), - assertions=assertions(), - vtables=vtables(), - world=world(), - render=render(), -) +ret = {} +# world, render +for func in ["game_vars", "c_callbacks", "py_mods", "assertions", "vtables"]: + ret[func] = globals()[func]() analysis(True) @@ -288,7 +297,7 @@ with open(json_path, "w") as of: print("[+] Wrote scrap_dissect.json") -with open(x64_dbg_script_path,"w") as of: +with open(x64_dbg_script_path, "w") as of: of.write("\n".join(x64_dbg_script)) print("[+] Wrote scrap_dissect.x32dbg.txt") @@ -296,30 +305,37 @@ print("[+] Wrote scrap_dissect.x32dbg.txt") with open(r2_script_path, "w") as of: wcmds = [] for cmd in r2cmds: - record=True - for start in ["p","/","s"]: + if cmd == "avj": + continue + record = True + for start in ["p", "/", "s"]: if cmd.strip('"').startswith(start): - record=False + record = False if record: wcmds.append(cmd) of.write("\n".join(wcmds)) print("[+] Wrote scrap_dissect.r2") -r2.quit() -def start_program(cmdl,**kwargs): - if os.name=='nt': - return SP.Popen(['cmd','/c','start']+cmdl,**kwargs) +def start_program(cmdl, **kwargs): + if os.name == "nt": + return SP.Popen(["cmd", "/c", "start"] + cmdl, **kwargs) else: - return SP.Popen(cmdl,**kwargs) + return SP.Popen(cmdl, **kwargs) -print("[+] Analysis took:",datetime.today()-t_start) +print("[+] Analysis took:", datetime.today() - t_start) print("[+] Executing Cutter") try: - start_program(['cutter','-A','0','-i',r2_script_path,scrap_exe],cwd=scrapland_folder,shell=False) + start_program( + ["cutter", "-A", "0", "-i", r2_script_path, scrap_exe], + cwd=scrapland_folder, + shell=False, + ) except FileNotFoundError: print("[-] cutter not installed, falling back to r2") - start_program(['r2','-i',r2_script_path,scrap_exe],cwd=scrapland_folder,shell=False) \ No newline at end of file + start_program( + ["r2", "-i", r2_script_path, scrap_exe], cwd=scrapland_folder, shell=False + ) diff --git a/tools/analyze_read_trace.py b/tools/analyze_read_trace.py new file mode 100644 index 0000000..bd50645 --- /dev/null +++ b/tools/analyze_read_trace.py @@ -0,0 +1,68 @@ +import msgpack as mp +import sys +import os +from tqdm import tqdm +import struct +import binascii +import string +import re +from binascii import hexlify + +def gen(): + with open(sys.argv[1], "rb") as fh: + size = os.stat(sys.argv[1]).st_size + progbar = tqdm(total=size, unit="bytes", unit_scale=True, unit_divisor=1024) + pos = 0 + for entry in mp.Unpacker(fh, raw=True): + progbar.update(fh.tell() - pos) + pos = fh.tell() + for k in entry.copy(): + k_s = str(k, "utf8") + if k_s not in ["data", "stack", "timestamp"]: + entry[k] = str(entry.pop(k), "utf8") + entry[k_s] = entry.pop(k) + entry["stack"] = "|".join( + ["{:08X}".format(int(str(v, "utf8"), 16)) for v in entry["stack"][::-1]] + ) + yield entry + + + +def strdump(data): + printable_chars = set(bytes(string.printable, "ascii")) - set(b"\n\r\t\x0b\x0c") + return "".join(chr(c) if c in printable_chars else "." for c in data) + +def tohex(data): + return str(hexlify(data), "utf8").upper() + + +# best=sorted(tqdm(gen(),ascii=True),key=lambda v:len(v['data']),reverse=True) + +# def score(entry): +# return len(entry['data']) + +# def analyze(entry): +# data=entry['data'] +# entry['infos'] = { +# 'len':len(data), +# } +# for bo in "><": +# for t in "hHiIlLqQefd": +# fmt="{}{}".format(bo,t) +# if len(data)%struct.calcsize(fmt)==0: +# entry['infos'][fmt]=[v[0] for v in struct.iter_unpack(fmt,data)] +# return entry + +filters=[re.compile(s) for s in sys.argv[2:]] + +with open("all.log", "w") as of: + for entry in gen(): + fm=[(f.match(entry['filename']) is not None) for f in filters] + if filters and not any(fm): + continue + entry["data_len"] = len(entry["data"]) + entry["str"] = strdump(entry["data"]) + entry["data"] = tohex(entry["data"]) + print( + "{timestamp} {block_id} {filename} {data_len:08X} {data} {str}".format(**entry), file=of + ) diff --git a/tools/binvis.py b/tools/binvis.py new file mode 100644 index 0000000..1470a61 --- /dev/null +++ b/tools/binvis.py @@ -0,0 +1,57 @@ +import struct +from collections import OrderedDict, ChainMap + + +class LittleEndian: + byteorder = "<" + + +class BigEndian: + byteorder = ">" + + +class NativeEndian: + byteorder = "@" + + +class Field: + def __init__(self, struct_type=None, size=None, byteorder=None): + self.struct = struct_type + self.size = size + self.byteorder = byteorder + self.data = None + self.parsed = False + + def parse(self, data): + return + + +class ParserMeta(type): + def __new__(cls, name, bases, namespace, **kwargs): + if object in bases: + return type.__new__(cls, name, bases, dict(namespace)) + fields = [] + for item_name, item_value in namespace.items(): + if isinstance(item_value, Field): + fields.append(item_name) + ret = super().__new__(cls, name, bases, namespace) + ret._fields = fields + return ret + + @classmethod + def __prepare__(metacls, name, bases, **kwds): + return OrderedDict() + + +class Parser(metaclass=ParserMeta): + def __init__(self, data): + for field in self._fields: + print(field, getattr(self, field)) + + +class ChunkedHeader(Parser, LittleEndian): + size = Field("I") + data = Field(size=size) + + +print(ChunkedHeader(b"")) diff --git a/tools/dissect_net.py b/tools/dissect_net.py new file mode 100644 index 0000000..3672593 --- /dev/null +++ b/tools/dissect_net.py @@ -0,0 +1,149 @@ +from construct import * +from binascii import unhexlify +from collections import defaultdict, Counter +import string + + +class CustomError(SymmetricAdapter): + def __init__(self, msg): + super(SymmetricAdapter, self).__init__(Pass) + self._message = msg + + def _decode(self, obj, context, path): + # print("Error",path) + # print(str(context)) + msg = self._message.format(ctx=context, obj=obj) + raise ValidationError(message=msg, path=this.path) + + +paket_type = Enum( + Int8ub, + GetGameInfo=0x7F01, # 0x7f3d ? + Connect=0x7F47, + GameInfo=0xBACE, + LevelInfo=0x8017, + Announce=0x4842, + Disconnect=0x0F02, + UpdatePlayerInfo=0xC49, # ??? + # UpdatePlayerInfo=0x8a4c, + ChatIn=0x921E, + ChatOut=0x0A1E, + # Movement=0x802 +) + +paket_subtype = Enum( + Int8ub +) + + +packet_types = { + "Movement": Struct("data" / GreedyBytes), + "ChatIn": Struct( + "unk" / Int16ub, + "unk_2" / Int8ub, + "msg" / PascalString(Int8ub, "utf-8"), + "rest" / GreedyBytes, + ), + "ChatOut": Struct( + "unk" / Int16ub, + "unk_2" / Int8ub, + "msg" / PascalString(Int8ub, "utf-8"), + "rest" / GreedyBytes, + ), + "UpdatePlayerInfo": Struct( + "data" / GreedyBytes + # "name"/PascalString(Int32ub,"utf-8"), + # "ship"/PascalString(Int8ub,"utf-8"), + # "max_life"/Int8ub, + # "player_char"/PascalString(Int16ub,"utf-8"), + # "engines"/PascalString(Int8ub,"utf-8")[4], + # "weapons"/PascalString(Int8ub,"utf-8"), + # "team_id"/Int32ul + ), + "Announce": "info" / CString("utf-8"), + "GetGameInfo": Const(b"\x00\x00\x07"), + "Disconnect": Const(b"\x00\x0c\x02"), + "GameInfo": Struct( + "version_minor" / Int8ul, + "version_major" / Int8ul, + "port" / Int16ul, + "max_players" / Int16ul, + "curr_players" / Int16ul, + "name" / FixedSized(0x20, CString("utf-8")), + "mode" / FixedSized(0x10, CString("utf-8")), + "map" / Bytes(2), + "rest" / GreedyBytes, + ), + "Connect": Struct( + "name" / PascalString(Int32ub, "utf-8"), + "ship" / PascalString(Int8ub, "utf-8"), + "max_life" / Int8ub, + "player_char" / PascalString(Int16ub, "utf-8"), + "engines" / PascalString(Int8ub, "utf-8")[4], + "weapons" / PascalString(Int8ub, "utf-8"), + "team_id" / Int32ul, + ), + "LevelInfo": Struct( + "path" / PascalString(Int32ub, "utf-8"), + "mode" / PascalString(Int8ub, "utf-8"), + "rest" / GreedyBytes, + ), +} + +default = "Unknown ID" / Struct("data" / GreedyBytes) +# CustomError("Invalid ID: 0x{ctx.type:02x}") +packet = Struct( + "type" / Int8ub, + "subtype"/ Int8ub + # "data" / Switch(this.type, packet_types, default=default) +) + + +printable_chars = set(bytes(string.printable, "ascii")) - set(b"\n\r\t\x0b\x0c") + + +def is_printable(s): + return all(c in printable_chars for c in s.rstrip(b"\0")) + + +def hexdump(data, cols=16, offset=0): + lines = [] + while data: + hexdata = " ".join("{:02X}".format(v) for v in data[:cols]).ljust( + 3 * cols - 1, " " + ) + print_data = "".join( + [chr(v) if v in printable_chars else "." for v in data[:cols]] + ) + lines.append("{:04X} {} {}".format(offset, hexdata, print_data)) + offset += len(data[:cols]) + data = data[cols:] + return "\n".join(lines).strip() + + +def main(): + data_type = Counter() + with open("netlog.txt", "r") as netlog: + for line in netlog: + direction, addr, buffer_addr, data = line.strip().split() + data = unhexlify(data) + print(direction, addr, buffer_addr) + print(hexdump(data)) + print() + try: + parsed_data = packet.parse(data) + data_type["{0} {1:08b}:{2:08b} ({1:02X}:{2:02X})".format(direction, parsed_data.type,parsed_data.subtype)] += len(data) + except Exception: + pass + bar_width = 50 + label = "Data type (main:sub)" + print("=" * 10, label, "=" * 10) + max_v = max(data_type.values()) + total = sum(data_type.values()) + for k, v in sorted(data_type.items(), key=lambda v: v[1], reverse=True): + bar = ("#" * round((v / max_v) * bar_width)).ljust(bar_width, " ") + print(k, bar, "({}, {:.02%})".format(v, v / total)) + + +if __name__ == "__main__": + main() diff --git a/tools/packed.ksy b/tools/packed.ksy new file mode 100644 index 0000000..94f2ac3 --- /dev/null +++ b/tools/packed.ksy @@ -0,0 +1,46 @@ +meta: + id: packed + application: Scrapland + file-extension: packed + endian: le + xref: http://wiki.xentax.com/index.php/Scrapland_PACKED + license: MIT + encoding: latin1 + +seq: + - id: magic + contents: BFPK + doc: File Magic + - id: version + type: u2 + size: 4 + doc: Second File Magic + - id: num_files + type: u4 + doc: Number of files + - id: files + type: file_entry + repeat: expr + repeat-expr: num_files + doc: Directory entry for each file + +types: + file_entry: + seq: + - id: path_len + type: u4 + doc: Length of file path + - id: path + type: str + size: path_len + doc: File path + - id: size + type: u4 + doc: File size + - id: offset + type: u4 + doc: Absoulte File offset + instances: + data: + pos: offset + size: size \ No newline at end of file diff --git a/tools/parse_LFVF.py b/tools/parse_LFVF.py new file mode 100644 index 0000000..342e40f --- /dev/null +++ b/tools/parse_LFVF.py @@ -0,0 +1,122 @@ +import os +import json +from construct import * + +blocksize = 1024 * 4 + + +def search(pattern, path): + seen = set() + with open(path, "rb") as infile: + buffer = bytearray(infile.read(blocksize)) + while infile.peek(1): + for block in iter(lambda: infile.read(blocksize), b""): + buffer += block + buffer = buffer[-(blocksize * 2) :] + idx = buffer.find(pattern) + if idx != -1: + pos = (infile.tell() - blocksize * 2) + idx + if pos not in seen: + seen.add(pos) + return sorted(seen) + + +has_pos = [ + "D3DFVF_XYZ", + "D3DFVF_XYZRHW", +] + +num_blend = { + 'D3DFVF_XYZB1': 1, + 'D3DFVF_XYZB2': 2, + 'D3DFVF_XYZB3': 3, + 'D3DFVF_XYZB4': 4, +} + +Vertex = Struct( + "pos" / If(lambda ctx: ctx._._.fvf.position in has_pos, Float32l[3]), + "rhw" / If(lambda ctx: ctx._._.fvf.position == "D3DFVF_XYZRHW", Float32l), + "w_blend" / If(lambda ctx: num_blend.get(ctx._._.fvf.position,0)!=0, Int32ul), + "normal" / If(lambda ctx: ctx._._.fvf.flags.D3DFVF_NORMAL, Float32l[3]), + "diffuse" / If(lambda ctx: ctx._._.fvf.flags.D3DFVF_DIFFUSE, Int8ul[4]), + "specular" / If(lambda ctx: ctx._._.fvf.flags.D3DFVF_SPECULAR, Int8ul[4]), + "tex" / Float32l[this.num_tex_coords][this._._.fvf.num_tex], +) + +D3DFVF_POSITION_MASK = 0xE +D3DFVF_TEXCOUNT_MASK = 0xF00 +D3DFVF_TEXCOUNT_SHIFT = 8 + +FVF = "fvf" / Union( + 0, + "value" / Int32ul, + "num_tex" + / Computed( + lambda ctx: 1 + ((ctx.value & D3DFVF_TEXCOUNT_MASK) >> D3DFVF_TEXCOUNT_MASK) + ), + "position" + / Enum( + Computed(lambda ctx: (ctx.value & D3DFVF_POSITION_MASK)), + D3DFVF_XYZ=0x2, + D3DFVF_XYZRHW=0x4, + D3DFVF_XYZB1=0x6, + D3DFVF_XYZB2=0x8, + D3DFVF_XYZB3=0xA, + D3DFVF_XYZB4=0xC, + ), + "flags" + / FlagsEnum( + Int32ul, + D3DFVF_RESERVED0=0x1, + D3DFVF_NORMAL=0x10, + D3DFVF_PSIZE=0x20, + D3DFVF_DIFFUSE=0x40, + D3DFVF_SPECULAR=0x80, + ), +) + +LFVF_Data = Struct( + "unk" / Int32ul, + "num_entries"/Int32ul, + "data"/Struct( + FVF, + "unk_size" / Int32ul, + "vertices" / PrefixedArray(Int32ul, Vertex), + ) + # Terminated, +) + +LFVF = Struct( + Const(b"LFVF"), "size" / Int32ul, "data" / RestreamData(Bytes(this.size), LFVF_Data) +) + +files = [ + r"D:\Games\Deep Silver\Scrapland\extracted\Data.packed\models\skies\orbit\sky.sm3", + r"D:\Games\Deep Silver\Scrapland\extracted\Data.packed\models\chars\boss\boss.sm3", + r"D:\Games\Deep Silver\Scrapland\extracted\Data.packed\models\chars\dtritus\dtritus.sm3", + r"D:\Games\Deep Silver\Scrapland\extracted\Data.packed\levels\gdb\map\map3d.emi" +] + +vert_pos = {} + +for path in files: + name = os.path.split(path)[-1] + fh = open(path, "rb") + offsets = search(b"LFVF", path) + for offset in sorted(offsets): + fh.seek(offset) + print("Offset:", offset) + s = LFVF.parse_stream(fh) + print(s) + print("=" * 10) + continue + # # print(s) + # print(path, fh.tell(), list(s.unk_ints), list(s.data.unk), fh.read(8)) + # s = s.data + # vpos = [ + # tuple(p for p in v.pos) for v in s.vertices + # ] # leave vertices alone because we don't need to reproject shit :| + # vert_pos["{}@{}".format(name, hex(offset))] = vpos + # with open("LFVF_Data.json", "w") as of: + # json.dump(vert_pos, of) + # break diff --git a/tools/parse_chunked.py b/tools/parse_chunked.py new file mode 100644 index 0000000..7ca8acc --- /dev/null +++ b/tools/parse_chunked.py @@ -0,0 +1,117 @@ +from construct import * +import binascii +import os + +Chunked = LazyBound(lambda: struct) + + +class CustomError(SymmetricAdapter): + def __init__(self, msg): + super(SymmetricAdapter, self).__init__(Pass) + self._message = msg + + def _decode(self, obj, context, path): + # print("Error",path) + # print(str(context)) + msg = "Invalid ID: " + repr(context.id) + raise ValidationError(message=msg, path=this.path) + + +RGB = NamedTuple("RGB", "R G B", Int8ul[3]) + +RGBA = NamedTuple("RGBA", "R G B A", Int8ul[4]) + + +def make_chain(*sizes): + "utility function to make sequence of byte arrays" + return Sequence(*[Bytes(s) for s in sizes]) + + +child_nodes = "children" / Struct("num" / Int32ul, "nodes" / Chunked[this.num]) + +subchunks = { + b"SM3\0": Struct( + "unk" / Bytes(4), + "timestamp" / Timestamp(Int32ul, 1, 1970), + child_nodes, + "scene" / Chunked, + ), + b"SCN\0": Struct( + "version" / Int32ul, + "m3d_name" / PascalString(Int32ul, "utf8"), + "name" / PascalString(Int32ul, "utf8"), + child_nodes, + ), + b"INI\0": Struct( + "data" + / PrefixedArray(Int32ul, PrefixedArray(Int32ul, PascalString(Int32ul, "utf8"))), + "colors?" / Sequence(Int8ul, Int8ul, Int8ul, Int8ul, Float32l)[2], + "unk_data" / Bytes(0x18), + "unk_float" / Float32l, + "unk_int" / Int32ul, + child_nodes, + ), + b"EMI\0": Struct( + "version"/Int32ul, + "num_materials"/Int32ul, + "num_unk"/Int32ul, + "materials"/Chunked + ), + + b"MAT\0": Struct( + "tris"/Int32ul, + "name"/PascalString(Int32ul,"utf8"), + "idx"/Bytes(this.tris*4*4) + ), + + None: Bytes(lambda ctx:ctx.size), +} + +struct = Struct( + "id" / Bytes(4), + "size" / Int32ul, + "data" / Switch(this.id, subchunks, default=subchunks[None]), +) + + +def io_peek(fh, n): + p = fh.tell() + ret = fh.read(n) + fh.seek(p) + return ret + + +basedir = r"D:/Games/Deep Silver/Scrapland/extracted/Data.packed" + +files = [ + r"Models/Elements/AnilloEstructuraA/AnilloEstructuraA.SM3", + r"models/elements/antenaa/antenaa.lod1.sm3", + r"models/elements/abshield/anm/loop.cm3", + r"levels/fake/map/map3d.amc", + r"levels/shipedit/map/map3d.dum", + r"levels/menu/map/map3d.emi", + r"Models/Skies/Menu/Sky.SM3", + r"Levels/Menu/Map/Map3D.SM3", + r"Models/Elements/AnilloEstructuraD/AnilloEstructuraD.LOD1.SM3", + r"levels/menu/map/map3d.amc", + r"levels/menu/map/map3d.dum", + r"levels/menu/map/scenecamera/anm/loop.cm3", + r"models/chars/boss/boss.sm3", + r"models/chars/boss/anm/boss_walk.cm3", +] +for file in files: + file = os.path.join(basedir, file).replace("/","\\") + print() + print("#" * 3, file) + with open(file, "rb") as infile: + try: + data = struct.parse_stream(infile) + # assert infile.read()==b"","leftover data" + except Exception as ex: + print("Error:", ex) + data = None + if data: + print(data) + print("OFFSET:", hex(infile.tell())) + print("NEXT:", io_peek(infile, 16)) + print("NEXT:", binascii.hexlify(io_peek(infile, 16))) diff --git a/tools/parse_chunked_new.py b/tools/parse_chunked_new.py new file mode 100644 index 0000000..3cad3d7 --- /dev/null +++ b/tools/parse_chunked_new.py @@ -0,0 +1,255 @@ +import os +import sys +import struct +import string +from pprint import pprint +from io import BytesIO +from contextlib import contextmanager +from datetime import timedelta, datetime +import glob + +printable_chars = set(bytes(string.printable, "ascii")) - set(b"\n\r\t\x0b\x0c") + + +def hexdump(data, cols=16, offset=0, markers=None): + if markers is None: + markers = [] + lines = [] + while True: + hexdata = " ".join("{:02X}".format(v) for v in data[:cols]).ljust( + 3 * cols - 1, " " + ) + print_data = "".join( + [chr(v) if v in printable_chars else "." for v in data[:cols]] + ) + lines.append("{:04X} {} {}".format(offset, hexdata, print_data)) + offset += len(data[:cols]) + data = data[cols:] + if not data: + break + return "\n".join(lines).strip() + + +@contextmanager +def seek_to(fh, offset, pos=None): + if pos is None: + pos = fh.tell() + fh.seek(offset) + yield + fh.seek(pos) + +def read_array(s,fh): + ret=[] + count = read_struct("".format(rest)) + fh.seek(0) + return "".format(magic, len(fh.read())) + + def parse(self, magic, data, depth=0): + print("{}[{}] {} bytes".format(" " * self.depth, magic, len(data))) + self.depth += 1 + fh = BytesIO(data) + ret = getattr(self, magic, lambda fh: self._default(magic, fh))(fh) + pos = fh.tell() + leftover = len(fh.read()) + fh.seek(pos) + self.depth -= 1 + if leftover: + print("{}[{}] {} bytes unparsed".format(" " * self.depth, magic, leftover)) + if self.debug: + print(hexdump(fh.read(self.dump_size))) + rest = len(fh.read()) + if rest: + print("<{} more bytes>".format(rest)) + print("-" * 50) + return ret + + def parse_block(self, fh): + block = read_block(fh) + if block: + return self.parse(*block) + + # Block definitions + + def SM3(self, fh): + ret = {} + ret["unk_1"] = fh.read(4) # always F8156500 + ret["timestamp_2"] = datetime.fromtimestamp(read_struct("7I", fh)] + # ret["maps"]=[] + # for _ in range(ret["num_maps"]): + # ret["maps"].append(self.parse_block(fh)) + return {"maps": fh.read().count(b"MAP\0")} + + def MAP(self, fh): + ret = {} + ret["unk_1"] = read_struct(" +edges_= + +# pasted node and edges here + +edges=[] + +for edge in edges_: + for a,b in zip(edge,edge[1:]): + edges.append(a) + edges.append(b) + +nodes=[[p*0.0001 for p in node] for node in nodes] + +me = bpy.data.meshes.new("Test") + +nodes = np.array(list(ITT.chain.from_iterable(nodes))) + +me.vertices.add(len(nodes)//3) +me.vertices.foreach_set("co", nodes) +me.edges.add(len(edges)//2) +me.edges.foreach_set("vertices", np.array(edges)) + +me.update(calc_edges=True) +me.validate() + +ob = bpy.data.objects.new("Test", me) + +scene = bpy.context.scene +scene.collection.objects.link(ob) +""" \ No newline at end of file diff --git a/tools/save_to_json.py b/tools/save_to_json.py new file mode 100644 index 0000000..af3b70a --- /dev/null +++ b/tools/save_to_json.py @@ -0,0 +1,27 @@ +import sys +import os +from construct import * +import json + +save_data = {} + +ScrapSaveVar = Struct( + "name" / PascalString(Int32ul, encoding="windows-1252"), + "data" / PascalString(Int32ul, encoding="windows-1252"), +) +ScrapSave = "ScarpSaveGame" / Struct( + "title" / PascalString(Int32ul, encoding="windows-1252"), + "id" / PascalString(Int32ul, encoding="windows-1252"), + "data" / PrefixedArray(Int32ul, ScrapSaveVar), + Terminated, +) + +with open(sys.argv[1], "rb") as sav_file: + save = ScrapSave.parse_stream(sav_file) + save_data["id"] = save.id + save_data["title"] = save.title + save_data["data"] = {} + for var in save.data: + save_data["data"][var.name] = var.data + with open(os.path.basename(sys.argv[1]) + ".json", "w") as of: + json.dump(save_data, of, indent=4) diff --git a/scrapper.py b/tools/scrapper.py similarity index 89% rename from scrapper.py rename to tools/scrapper.py index 96dc6da..0e02047 100644 --- a/scrapper.py +++ b/tools/scrapper.py @@ -3,18 +3,29 @@ from collections import OrderedDict import glob import os import shutil -from construct import * +from construct import ( + Struct, + PascalString, + Int32ul, + Lazy, + Pointer, + Bytes, + this, + PrefixedArray, + Const, + Debugger +) from tqdm import tqdm -setglobalstringencoding(None) - ScrapFile = Struct( - "path" / PascalString(Int32ul), + "path" / PascalString(Int32ul, encoding="ascii"), "size" / Int32ul, "offset" / Int32ul, - "data" / OnDemandPointer(this.offset, Bytes(this.size)), + "data" / Lazy(Pointer(this.offset, Bytes(this.size))), +) +DummyFile = Struct( + "path" / PascalString(Int32ul, encoding="u8"), "size" / Int32ul, "offset" / Int32ul ) -DummyFile = Struct("path" / PascalString(Int32ul), "size" / Int32ul, "offset" / Int32ul) PackedHeader = Struct( Const(b"BFPK"), Const(b"\0\0\0\0"), "files" / PrefixedArray(Int32ul, ScrapFile) diff --git a/tools/server.py b/tools/server.py new file mode 100644 index 0000000..8efa1fe --- /dev/null +++ b/tools/server.py @@ -0,0 +1,45 @@ +import socket +import binascii +import select +from construct import * +from socketserver import BaseRequestHandler,UDPServer + +INFO = Struct( + "version_minor" / Int8ul, + "version_major" / Int8ul, + "port" / Int16ul, + "max_players" / Int16ul, + "curr_players" / Int16ul, + "name" / FixedSized(0x20, CString("utf-8")), + "mode" / FixedSized(0x10, CString("utf-8")), + "map" / Bytes(2), + "rest" / GreedyBytes, +) + +class ScrapHandler(BaseRequestHandler): + def handle(self): + data, socket = self.request + print(self.client_address,data) + socket.sendto(data, self.client_address) + +class ScrapSrv(UDPServer): + def __init__(self,port=5000): + super().__init__(("0.0.0.0",port),ScrapHandler) + +with ScrapSrv() as srv: + srv.serve_forever() + +exit() + +# sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + +while True: + rl, wl, xl = select.select([sock], [sock], [sock], 0.1) + if rl: + print(rl) + for sock in rl: + data, src = sock.recvfrom(1024) + print(src, data) + if data == b"\x7f\x01\x00\x00\x07": + game_info = INFO.build() + sock.sendto(game_info, src) diff --git a/tools/test.bv b/tools/test.bv new file mode 100644 index 0000000..67bbac6 --- /dev/null +++ b/tools/test.bv @@ -0,0 +1,4 @@ +< +magic s 4 +size I +data n $size \ No newline at end of file