├── .clang-format ├── .github └── workflows │ └── msvc_cmake.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── dll ├── CMakeLists.txt ├── concolic.cpp ├── concolic.hpp ├── dumper.cpp ├── imports.cpp ├── imports.hpp ├── log.cpp ├── log.hpp ├── main.cpp ├── misc.cpp ├── misc.hpp ├── relocations.cpp └── relocations.hpp └── src ├── CMakeLists.txt ├── main.cpp └── raii_proc.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: '-4' 3 | AlignConsecutiveMacros: 'true' 4 | AlignEscapedNewlines: Left 5 | AllowAllParametersOfDeclarationOnNextLine: 'false' 6 | AllowShortBlocksOnASingleLine: 'false' 7 | AllowShortCaseLabelsOnASingleLine: 'false' 8 | AllowShortFunctionsOnASingleLine: InlineOnly 9 | AllowShortIfStatementsOnASingleLine: Never 10 | AllowShortLambdasOnASingleLine: All 11 | AllowShortLoopsOnASingleLine: 'false' 12 | AlwaysBreakTemplateDeclarations: 'Yes' 13 | BreakBeforeBraces: Allman 14 | BreakBeforeTernaryOperators: 'false' 15 | ColumnLimit: '82' 16 | CompactNamespaces: 'false' 17 | ContinuationIndentWidth: '4' 18 | FixNamespaceComments: 'true' 19 | IncludeBlocks: Regroup 20 | IndentCaseLabels: 'true' 21 | IndentPPDirectives: AfterHash 22 | IndentWidth: '4' 23 | Language: Cpp 24 | MaxEmptyLinesToKeep: '1' 25 | NamespaceIndentation: None 26 | PointerAlignment: Left 27 | ReflowComments: 'true' 28 | SortUsingDeclarations: 'true' 29 | SpaceAfterLogicalNot: 'false' 30 | SpaceAfterTemplateKeyword: 'true' 31 | SpaceBeforeAssignmentOperators: 'true' 32 | SpaceBeforeCpp11BracedList: 'true' 33 | SpaceBeforeCtorInitializerColon: 'true' 34 | SpaceBeforeInheritanceColon: 'true' 35 | SpaceBeforeParens: ControlStatements 36 | SpaceBeforeRangeBasedForLoopColon: 'true' 37 | SpaceInEmptyParentheses: 'false' 38 | SpacesInAngles: 'false' 39 | SpacesInCStyleCastParentheses: 'false' 40 | SpacesInContainerLiterals: 'true' 41 | SpacesInParentheses: 'false' 42 | SpacesInSquareBrackets: 'false' 43 | Standard: Cpp11 44 | TabWidth: '4' 45 | UseTab: Never 46 | 47 | ... 48 | -------------------------------------------------------------------------------- /.github/workflows/msvc_cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | BUILD_TYPE: Release 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Download hadesmem v1.7.1 release zip 20 | run: | 21 | Invoke-WebRequest https://github.com/namreeb/hadesmem/releases/download/v1.7.1/hadesmem-v143-Release-x64.zip -OutFile hadesmem.zip 22 | Expand-Archive -Path hadesmem.zip -DestinationPath . 23 | del hadesmem.zip 24 | 25 | - name: Download Boost 26 | run: | 27 | Invoke-WebRequest https://archives.boost.io/release/1.73.0/source/boost_1_73_0.zip -OutFile boost.zip 28 | Expand-Archive -Path boost.zip -DestinationPath . 29 | 30 | - name: Configure CMake 31 | run: cmake -B ${{github.workspace}}/build -DBOOST_ROOT=boost_1_73_0 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/artifact 32 | 33 | - name: Build 34 | run: | 35 | cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 36 | cmake --install ${{github.workspace}}/build 37 | 38 | - name: Publish artifact 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: dumpwow 42 | path: ${{ github.workspace }}/artifact/**/* 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /CMakeSettings.json 3 | /hadesmem-*-* 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | foreach(policy 4 | CMP0074 # CMake 3.12 5 | ) 6 | if(POLICY ${policy}) 7 | cmake_policy(SET ${policy} NEW) 8 | endif() 9 | endforeach() 10 | 11 | set(CMAKE_CXX_STANDARD 14) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | set(CMAKE_DISABLE_SOURCE_CHANGES ON) 14 | set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) 15 | 16 | set(PROJECT_NAME dumpwow) 17 | 18 | project(${PROJECT_NAME}) 19 | 20 | # build in Release-mode by default if not explicitly set 21 | if( NOT CMAKE_BUILD_TYPE ) 22 | set(CMAKE_BUILD_TYPE "RelWithDebInfo") 23 | endif() 24 | 25 | if ("x_${CMAKE_BUILD_TYPE}" STREQUAL "x_Debug") 26 | set(HADESMEM_BUILD "Debug") 27 | else() 28 | set(HADESMEM_BUILD "Release") 29 | endif() 30 | 31 | if (CMAKE_SIZEOF_VOID_P EQUAL 8) 32 | set(HADESMEM_ARCH "x64") 33 | else() 34 | set(HADESMEM_ARCH "Win32") 35 | endif() 36 | 37 | if ("x_${HADESMEM_ROOT}" STREQUAL "x_") 38 | if (EXISTS "${CMAKE_SOURCE_DIR}/hadesmem-v${MSVC_TOOLSET_VERSION}-${HADESMEM_BUILD}-${HADESMEM_ARCH}") 39 | set(HADESMEM_ROOT "${CMAKE_SOURCE_DIR}/hadesmem-v${MSVC_TOOLSET_VERSION}-${HADESMEM_BUILD}-${HADESMEM_ARCH}") 40 | set(HADESMEM_LIB_DIR "${HADESMEM_ROOT}/lib") 41 | else() 42 | message(FATAL_ERROR "HADESMEM_ROOT not set. ${PROJECT_NAME} requires hadesmem, available at https://github.com/namreeb/hadesmem") 43 | endif() 44 | else() 45 | set(HADESMEM_LIB_DIR "${HADESMEM_ROOT}/build/vs/${HADESMEM_BUILD}/${HADESMEM_ARCH}") 46 | endif() 47 | 48 | if (NOT EXISTS "${HADESMEM_ROOT}/include/memory/hadesmem") 49 | message(FATAL_ERROR "hadesmem not found at ${HADESMEM_ROOT}") 50 | else() 51 | message(STATUS "hadesmem found at ${HADESMEM_ROOT}") 52 | endif() 53 | 54 | message(STATUS "hadesmem library directory: ${HADESMEM_LIB_DIR}") 55 | 56 | # threading library is required 57 | find_package(Threads REQUIRED) 58 | 59 | set(Boost_USE_STATIC_LIBS ON) 60 | set(Boost_USE_MULTITHREADED ON) 61 | set(Boost_USE_STATIC_RUNTIME OFF) 62 | 63 | find_package(Boost) 64 | 65 | if (BOOST_FOUND) 66 | message(STATUS "boost found at ${BOOST_ROOT}") 67 | else() 68 | message(FATAL_ERROR "boost not found") 69 | endif() 70 | 71 | add_definitions(-DUNICODE -D_UNICODE -D_SCL_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_WARNINGS -DASMJIT_STATIC -DASMJIT_BUILD_X86 -DHADESMEM_NO_PUGIXML) 72 | include_directories( 73 | "Include" 74 | "${CMAKE_CURRENT_SOURCE_DIR}" 75 | "${Boost_INCLUDE_DIR}" 76 | "${HADESMEM_ROOT}/include/memory/" 77 | "${HADESMEM_ROOT}/deps/udis86/udis86" 78 | "${HADESMEM_ROOT}/deps/asmjit/asmjit/src" 79 | ) 80 | 81 | link_directories("${HADESMEM_LIB_DIR}") 82 | 83 | add_subdirectory(dll) 84 | add_subdirectory(src) 85 | 86 | install(FILES 87 | "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" 88 | "${CMAKE_CURRENT_SOURCE_DIR}/README.md" 89 | DESTINATION "${CMAKE_INSTALL_PREFIX}") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 namreeb (legal@namreeb.org) http://github.com/namreeb/dumpwow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dumpwow 2 | 3 | Tool to dump unpacked and deobfuscated World of Warcraft binaries. 4 | 5 | Depends on the [hadesmem](https://github.com/namreeb/hadesmem/) library. 6 | -------------------------------------------------------------------------------- /dll/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(DLL_NAME unpacker) 2 | 3 | include_directories(Include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}) 4 | 5 | set(SOURCE_FILES 6 | concolic.cpp 7 | dumper.cpp 8 | imports.cpp 9 | relocations.cpp 10 | log.cpp 11 | main.cpp 12 | misc.cpp 13 | ) 14 | 15 | add_library(${DLL_NAME} SHARED ${SOURCE_FILES}) 16 | target_link_libraries(${DLL_NAME} shlwapi.lib asmjit.lib udis86.lib) 17 | 18 | install(TARGETS ${DLL_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}") 19 | -------------------------------------------------------------------------------- /dll/concolic.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 namreeb (legal@namreeb.org) http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "concolic.hpp" 26 | 27 | #include 28 | 29 | #include 30 | 31 | //#define DEBUG_CONCOLIC 32 | 33 | #ifdef DEBUG_CONCOLIC 34 | #include "log.hpp" 35 | #include 36 | #endif 37 | 38 | namespace 39 | { 40 | template 41 | T read(PVOID &address) 42 | { 43 | auto const ret_ptr = reinterpret_cast(address); 44 | 45 | address = reinterpret_cast( 46 | reinterpret_cast(address) + sizeof(T)); 47 | 48 | return *ret_ptr; 49 | } 50 | } 51 | 52 | bool conclic_begin(PVOID start, ConclicThreadContext &context) 53 | { 54 | memset(&context, 0, sizeof(context)); 55 | 56 | auto current = start; 57 | #ifdef DEBUG_CONCOLIC 58 | auto instruction_ea = current; 59 | #endif 60 | 61 | // 48:B8 5F8A36EDFA7F0000 -> mov rax,7FFAED368A5F 62 | if (read(current) != 0x48 || 63 | read(current) != 0xB8) 64 | { 65 | #ifdef DEBUG_CONCOLIC 66 | gLog << "ea: 0x" << instruction_ea << " initial mov not found" 67 | << std::endl; 68 | #endif 69 | return false; 70 | } 71 | 72 | context.rax = read(current); 73 | #ifdef DEBUG_CONCOLIC 74 | gLog << "ea: 0x" << instruction_ea << " initial mov rax, 0x" << std::hex 75 | << context.rax << std::endl; 76 | #endif 77 | 78 | do 79 | { 80 | #ifdef DEBUG_CONCOLIC 81 | instruction_ea = current; 82 | #endif 83 | auto const op1 = read(current); 84 | 85 | switch (op1) 86 | { 87 | // FFE0 -> jmp rax 88 | case 0xFF: 89 | { 90 | auto const result = read(current) == 0xE0; 91 | #ifdef DEBUG_CONCOLIC 92 | gLog << "returning " << (result ? "true" : "false") 93 | << ". rax: 0x" << std::hex << context.rax << std::dec 94 | << "\n" << std::endl; 95 | #endif 96 | return result; 97 | } 98 | // E9 98F4FFFF -> jmp -2920 99 | // E9 C3040000 -> jmp +1219 100 | case 0xE9: 101 | { 102 | auto const offset = read(current); 103 | current = reinterpret_cast( 104 | reinterpret_cast(current) + offset); 105 | #ifdef DEBUG_CONCOLIC 106 | gLog << "ea: 0x" << instruction_ea << " jump to 0x" << current 107 | << std::endl; 108 | #endif 109 | break; 110 | } 111 | case 0x48: 112 | { 113 | auto const op2 = read(current); 114 | switch (op2) 115 | { 116 | // 48:05 54AA1D29 -> add rax,291DAA54 117 | case 0x05: 118 | { 119 | auto const operand = read(current); 120 | #ifdef DEBUG_CONCOLIC 121 | gLog << "ea: 0x" << instruction_ea << " add " 122 | << std::hex << operand << " from " << context.rax 123 | << " to " << context.rax + operand << std::dec 124 | << std::endl; 125 | #endif 126 | context.rax += operand; 127 | break; 128 | } 129 | // 48:2D F78E5718 -> sub rax,18578EF7 130 | case 0x2D: 131 | { 132 | auto const operand = read(current); 133 | #ifdef DEBUG_CONCOLIC 134 | gLog << "ea: 0x" << instruction_ea << " sub " 135 | << std::hex << operand << " from " << context.rax 136 | << " to " << context.rax - operand << std::dec 137 | << std::endl; 138 | #endif 139 | context.rax -= operand; 140 | break; 141 | } 142 | // 48:35 04BE0778 -> xor rax,7807BE04 143 | case 0x35: 144 | { 145 | auto const operand = static_cast( 146 | read(current)); 147 | #ifdef DEBUG_CONCOLIC 148 | gLog << "ea: 0x" << instruction_ea << " xor " 149 | << std::hex << operand << " from " << context.rax 150 | << " to " << (context.rax ^ operand) << std::dec 151 | << std::endl; 152 | #endif 153 | context.rax ^= operand; 154 | break; 155 | } 156 | 157 | default: 158 | { 159 | #ifdef DEBUG_CONCOLIC 160 | gLog << "ea: 0x" << instruction_ea 161 | << " 0x48 op2 unrecognized: " << std::hex << op1 162 | << std::dec << std::endl; 163 | #endif 164 | return false; 165 | } 166 | } 167 | 168 | break; 169 | } 170 | case 0x49: 171 | { 172 | auto const op2 = read(current); 173 | switch (op2) 174 | { 175 | // 49 0f af c2 -> imul rax, r10 176 | case 0x0F: 177 | { 178 | auto const op3 = read(current); 179 | 180 | if (op3 != 0xAF) 181 | { 182 | #ifdef DEBUG_CONCOLIC 183 | gLog << "ea: 0x" << instruction_ea 184 | << " imul op3 unrecognized: " << std::hex 185 | << static_cast(op3) << std::dec 186 | << std::endl; 187 | #endif 188 | return false; 189 | } 190 | 191 | auto const op4 = read(current); 192 | 193 | if (op4 == 0xC2) 194 | { 195 | auto const signed_rax = static_cast( 196 | context.rax); 197 | auto const signed_r10 = static_cast( 198 | context.r10); 199 | 200 | #ifdef DEBUG_CONCOLIC 201 | gLog << "ea: 0x" << instruction_ea 202 | << " imul rax, r10 (0x" << std::hex 203 | << context.rax << " * 0x" << context.r10 204 | << " = 0x" << (signed_rax*signed_r10) << "), (" 205 | << std::dec << signed_rax << " * " 206 | << signed_r10 << " = " 207 | << (signed_rax*signed_r10) << ")" << std::endl; 208 | #endif 209 | 210 | context.rax = signed_rax * signed_r10; 211 | } 212 | else 213 | { 214 | #ifdef DEBUG_CONCOLIC 215 | gLog << "ea: 0x" << instruction_ea 216 | << " imul op4 unrecognized: " << std::hex 217 | << static_cast(op4) << std::dec 218 | << std::endl; 219 | #endif 220 | return false; 221 | } 222 | 223 | break; 224 | } 225 | // 49 ba bb 40 7d ba f7 07 31 3c -> mov r10, 3C3107F7BA7D40BB 226 | case 0xBA: 227 | { 228 | auto const operand = read(current); 229 | #ifdef DEBUG_CONCOLIC 230 | gLog << "ea: 0x" << instruction_ea << " r10 set to 0x" 231 | << std::hex << operand << std::dec << std::endl; 232 | #endif 233 | 234 | context.r10 = operand; 235 | break; 236 | } 237 | default: 238 | { 239 | #ifdef DEBUG_CONCOLIC 240 | gLog << "ea: 0x" << instruction_ea 241 | << " 0x49 op2 unrecognized: " << std::hex << op1 242 | << std::dec << std::endl; 243 | #endif 244 | return false; 245 | } 246 | } 247 | break; 248 | } 249 | 250 | default: 251 | { 252 | #ifdef DEBUG_CONCOLIC 253 | gLog << "ea: 0x" << instruction_ea << " op1 unrecognized: " 254 | << std::hex << op1 << std::dec << std::endl; 255 | #endif 256 | return false; 257 | } 258 | } 259 | } while (true); 260 | 261 | return true; 262 | } -------------------------------------------------------------------------------- /dll/concolic.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 namreeb (legal@namreeb.org) http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | 29 | #include 30 | 31 | struct ConclicThreadContext 32 | { 33 | std::uint64_t rax; 34 | std::uint64_t r10; 35 | }; 36 | 37 | bool conclic_begin(PVOID start, ConclicThreadContext &context); -------------------------------------------------------------------------------- /dll/dumper.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "imports.hpp" 26 | #include "log.hpp" 27 | #include "misc.hpp" 28 | #include "relocations.hpp" 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | namespace fs = std::experimental::filesystem; 48 | 49 | fs::path get_output_path(const fs::path& input_path); 50 | PVOID find_remapped_base(const hadesmem::Process& process, PVOID base); 51 | void repair_binary_in_memory(const fs::path& path, 52 | const hadesmem::Process& process, PVOID base, 53 | PVOID readonly_base, DWORD pe_size, 54 | std::vector& import_data); 55 | 56 | void do_dump(PVOID base, DWORD pe_size, PVOID init_func) 57 | { 58 | auto const readonly_base = base; 59 | auto const exe_path = get_exe_path(); 60 | auto const output_path = get_output_path(exe_path); 61 | const size_t init_func_rva = 62 | reinterpret_cast(init_func) - reinterpret_cast(base); 63 | 64 | const hadesmem::Process process(::GetCurrentProcessId()); 65 | 66 | // this will let us work with the writable part of memory 67 | base = find_remapped_base(process, base); 68 | 69 | std::vector import_data; 70 | repair_binary_in_memory(exe_path, process, base, readonly_base, pe_size, 71 | import_data); 72 | 73 | std::ofstream out(output_path, std::ios::binary); 74 | 75 | if (!out) 76 | throw std::runtime_error("Failed to open unpacked exe for writing"); 77 | 78 | // first, write PE header 79 | const hadesmem::PeFile pe_file(process, base, hadesmem::PeFileType::kImage, 80 | pe_size); 81 | const hadesmem::NtHeaders nt_header(process, pe_file); 82 | 83 | out.write(reinterpret_cast(base), 84 | static_cast(nt_header.GetSizeOfHeaders())); 85 | 86 | // second, write sections 87 | const hadesmem::SectionList section_list(process, pe_file); 88 | for (auto const& section : section_list) 89 | { 90 | // TODO: better logic to identify our section here 91 | if (section.GetName() == ".wowim") 92 | continue; 93 | 94 | auto const section_base = 95 | reinterpret_cast(base) + section.GetVirtualAddress(); 96 | out.write(section_base, section.GetSizeOfRawData()); 97 | } 98 | 99 | // third, write the new section created for imports 100 | if (!import_data.empty()) 101 | out.write(reinterpret_cast(&import_data[0]), 102 | import_data.size()); 103 | 104 | out.close(); 105 | } 106 | 107 | fs::path get_output_path(const fs::path& input_path) 108 | { 109 | auto const dir = input_path.parent_path(); 110 | auto const extension = input_path.extension(); 111 | auto const stem = input_path.stem(); 112 | 113 | return dir / (stem.string() + "_unpacked" + extension.string()); 114 | } 115 | 116 | PVOID find_remapped_base(const hadesmem::Process& process, PVOID base) 117 | { 118 | const hadesmem::RegionList region_list(process); 119 | 120 | const hadesmem::PeFile pe_file(process, base, hadesmem::PeFileType::kImage, 121 | 0); 122 | 123 | // find the PE header in the remapped location 124 | for (auto const& region : region_list) 125 | { 126 | if (region.GetState() == MEM_FREE) 127 | continue; 128 | 129 | if (region.GetAllocBase() != region.GetBase()) 130 | continue; 131 | 132 | if (region.GetSize() != pe_file.GetSize()) 133 | continue; 134 | 135 | if (memcmp(base, region.GetBase(), region.GetSize())) 136 | continue; 137 | 138 | #ifdef _DEBUG 139 | gLog << "\nRemapped base:\t\t0x" << std::hex << region.GetBase() 140 | << " protection: 0x" << region.GetProtect() << " size: 0x" 141 | << region.GetSize() << std::endl; 142 | #endif 143 | 144 | return region.GetBase(); 145 | } 146 | 147 | throw std::runtime_error( 148 | "find_remapped_base failed to find remapped location"); 149 | } 150 | 151 | std::vector read_from_exe(const fs::path& exe, size_t offset, 152 | size_t size) 153 | { 154 | std::vector result(size); 155 | 156 | std::ifstream in(exe, std::ios::binary); 157 | 158 | if (!in) 159 | throw std::runtime_error("Failed to read PE header from binary"); 160 | 161 | in.seekg(offset); 162 | in.read(reinterpret_cast(&result[0]), 163 | static_cast(result.size())); 164 | in.close(); 165 | 166 | return result; 167 | } 168 | 169 | void repair_binary_in_memory(const fs::path& path, 170 | const hadesmem::Process& process, PVOID base, 171 | PVOID readonly_base, DWORD pe_size, 172 | std::vector& import_data) 173 | { 174 | // read PE header and first chunk of data from EXE file, everything up to the 175 | // entry point 176 | auto const pe_exe = read_from_exe(path, 0, pe_size); 177 | 178 | // install EXE PE header into memory 179 | ::memcpy(base, &pe_exe[0], pe_exe.size()); 180 | 181 | // use the PE header read from the file 182 | const hadesmem::PeFile pe_file(process, base, hadesmem::PeFileType::kImage, 183 | pe_size); 184 | 185 | const hadesmem::NtHeaders nt_header(process, pe_file); 186 | hadesmem::SectionList section_list(process, pe_file); 187 | auto raw_data_pointer = nt_header.GetSizeOfHeaders(); 188 | 189 | // used later 190 | PVOID rdata = nullptr; 191 | 192 | DWORD text_raw_base = 0; 193 | DWORD text_base = 0; 194 | 195 | for (auto& section : section_list) 196 | { 197 | auto const virtual_size = 198 | round_up(section.GetVirtualSize(), nt_header.GetSectionAlignment()); 199 | 200 | auto const section_base = reinterpret_cast( 201 | reinterpret_cast(base) + section.GetVirtualAddress()); 202 | 203 | if (section.GetName() == ".rdata") 204 | rdata = section_base; 205 | else if (section.GetName() == ".text") 206 | { 207 | text_raw_base = section.GetPointerToRawData(); 208 | text_base = section.GetVirtualAddress(); 209 | } 210 | 211 | // for some reason the wow binary has section with unusual sizes. that's 212 | // fine, but lets clean it up by enforcing section alignment from the pe 213 | section.SetVirtualSize(virtual_size); 214 | 215 | // the unpacking may have enlarged the section. increase the raw size 216 | // to match the virtual size, and then remove as much zero-fill from 217 | // the end as we can. 218 | for (auto remain = virtual_size; remain != 0; --remain) 219 | { 220 | auto const curr = 221 | reinterpret_cast(section_base) + remain - 1; 222 | 223 | // we cannot shrink beyond this point 224 | if (*curr != 0x00) 225 | { 226 | auto const old_size = section.GetSizeOfRawData(); 227 | section.SetSizeOfRawData( 228 | round_up(remain, nt_header.GetFileAlignment())); 229 | break; 230 | } 231 | } 232 | 233 | // with sections changing size, we should also update the raw location 234 | section.SetPointerToRawData(raw_data_pointer); 235 | raw_data_pointer += section.GetSizeOfRawData(); 236 | 237 | section.UpdateWrite(); 238 | } 239 | 240 | if (!text_base) 241 | throw std::runtime_error("Could not find .text segment"); 242 | 243 | // recover from the .exe all data from the .text segment before the entry 244 | // point 245 | auto const exe_data_size = nt_header.GetAddressOfEntryPoint() - text_base; 246 | auto const exe_data = read_from_exe(path, text_raw_base, exe_data_size); 247 | 248 | ::memcpy( 249 | reinterpret_cast(reinterpret_cast(base) + text_base), 250 | &exe_data[0], exe_data.size()); 251 | 252 | auto const entry_point = 253 | reinterpret_cast(base) + nt_header.GetAddressOfEntryPoint(); 254 | 255 | if (*reinterpret_cast(entry_point) != 0xE9) 256 | throw std::runtime_error("Entry point should start with a JMP (0xE9)"); 257 | 258 | auto const new_ep_offset = 259 | *reinterpret_cast(entry_point + 1); 260 | 261 | auto const new_ep = 262 | reinterpret_cast(entry_point + new_ep_offset + 5); 263 | 264 | gLog << "True entry point:\t0x" << std::hex << new_ep << std::endl; 265 | 266 | // the second TLS callback will generate simple pointer decryption 267 | // trampolines to mask calls to imported DLL functions. to resolve 268 | // these calls we will perform a concolic execution of the trampolines 269 | // and see what function pointer results. 270 | if (rdata) 271 | rebuild_imports(process, pe_file, rdata, readonly_base, import_data); 272 | else 273 | gLog << "Did not find .rdata section. Skipping import resolution." 274 | << std::endl; 275 | 276 | // tls callbacks will point to the original (now unwritable) region of 277 | // memory. the addresses should be rebased to be consistent with the base 278 | // address in the PE header. we initialize this before applying 279 | // relocations on purpose! 280 | const hadesmem::TlsDir tls_dir(process, pe_file); 281 | 282 | // apply relocations to the new base address 283 | apply_relocations(base); 284 | 285 | auto p_current_callback = 286 | rebase(base, reinterpret_cast(tls_dir.GetAddressOfCallBacks())); 287 | 288 | auto const image_base = reinterpret_cast(nt_header.GetImageBase()); 289 | 290 | while (*p_current_callback) 291 | { 292 | try 293 | { 294 | *p_current_callback = rebase(image_base, *p_current_callback); 295 | } 296 | catch (const std::runtime_error&) 297 | { 298 | // rebase failed. probably because it already was rebased by 299 | // applying relocations. ignored. 300 | } 301 | 302 | ++p_current_callback; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /dll/imports.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "imports.hpp" 26 | 27 | #include "concolic.hpp" 28 | #include "log.hpp" 29 | #include "misc.hpp" 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | namespace 46 | { 47 | class ImportTable 48 | { 49 | private: 50 | const hadesmem::Process _process; 51 | const hadesmem::PeFile _pe_file; 52 | const DWORD _new_section_va; 53 | const DWORD _iat_va; 54 | 55 | std::vector _buffer; 56 | 57 | bool _finalized; 58 | 59 | struct ImportDirEntry 60 | { 61 | IMAGE_IMPORT_DESCRIPTOR header; 62 | std::string name; 63 | 64 | std::vector thunks; 65 | 66 | ImportDirEntry(const IMAGE_IMPORT_DESCRIPTOR& _header, 67 | const std::string& _name) 68 | : header(_header), name(_name) 69 | { 70 | } 71 | }; 72 | 73 | std::vector _import_dirs; 74 | 75 | // write a string to memory and return RVA 76 | // return the RVA for it 77 | std::uint64_t write(const std::string& str) 78 | { 79 | auto const ret = _buffer.size(); 80 | _buffer.resize(_buffer.size() + str.length() + 1); 81 | ::memcpy(&_buffer[ret], str.c_str(), str.length() + 1); 82 | return _new_section_va + ret; 83 | } 84 | 85 | std::uint64_t write(const std::wstring& str) 86 | { 87 | return write(wstring_to_string(str)); 88 | } 89 | 90 | template 91 | std::uint64_t write() 92 | { 93 | auto const ret = _buffer.size(); 94 | _buffer.resize(_buffer.size() + sizeof(T)); 95 | ::memset(&buffer[ret], 0, sizeof(T)); 96 | return _new_section_va + ret; 97 | } 98 | 99 | template 100 | std::uint64_t write(const T& value) 101 | { 102 | auto const ret = _buffer.size(); 103 | _buffer.resize(_buffer.size() + sizeof(T)); 104 | ::memcpy(&_buffer[ret], &value, sizeof(T)); 105 | return _new_section_va + ret; 106 | } 107 | 108 | template 109 | std::uint64_t write(const std::vector& vect) 110 | { 111 | if (vect.empty()) 112 | return _buffer.size(); 113 | 114 | auto const bytes = sizeof(T) * vect.size(); 115 | 116 | auto const ret = _buffer.size(); 117 | _buffer.resize(_buffer.size() + bytes); 118 | ::memcpy(&_buffer[ret], &vect[0], bytes); 119 | return _new_section_va + ret; 120 | } 121 | 122 | ImportDirEntry& get_import_dir(PVOID function) 123 | { 124 | const hadesmem::Region region(_process, function); 125 | const hadesmem::Module module( 126 | _process, reinterpret_cast(region.GetAllocBase())); 127 | auto const mod_name = wstring_to_string(module.GetName()); 128 | 129 | // if this function belongs to the same library as the last one, 130 | // return the last import directory 131 | if (!_import_dirs.empty() && _import_dirs.back().name == mod_name) 132 | return _import_dirs.back(); 133 | 134 | // if the import directory for this library exists anywhere else, 135 | // it means we have made a mistake somewhere along the way. 136 | for (auto& import_dir : _import_dirs) 137 | if (mod_name == import_dir.name) 138 | { 139 | gLog << "Import directory for 0x" << std::hex 140 | << reinterpret_cast(function) << " module \"" 141 | << mod_name << "\" has already been finalized" << std::endl; 142 | gLog << " alloc base: " << region.GetAllocBase() << std::endl; 143 | throw std::runtime_error("Import directory ordering failure"); 144 | } 145 | 146 | // create a new empty import directory for this module 147 | IMAGE_IMPORT_DESCRIPTOR imp_dir; 148 | ::memset(&imp_dir, 0, sizeof(imp_dir)); 149 | 150 | imp_dir.Name = static_cast(write(module.GetName())); 151 | 152 | _import_dirs.emplace_back(imp_dir, mod_name); 153 | 154 | return _import_dirs.back(); 155 | } 156 | 157 | public: 158 | ImportTable(const hadesmem::PeFile& pe_file, DWORD new_section_va, 159 | DWORD iat_va) 160 | : _process(::GetCurrentProcessId()), _pe_file(pe_file), 161 | _new_section_va(new_section_va), _iat_va(iat_va), _finalized(false) 162 | { 163 | } 164 | 165 | // TODO: shorten this function and reduce code duplication 166 | void add_function(PVOID thunk, PVOID function, bool force_new_dir) 167 | { 168 | if (_finalized) 169 | throw std::runtime_error( 170 | "Cannot add function to import table once finalized"); 171 | 172 | const hadesmem::Region region(_process, function); 173 | const hadesmem::PeFile mod_pe(_process, region.GetAllocBase(), 174 | hadesmem::PeFileType::kImage, 0); 175 | const hadesmem::Module module( 176 | _process, reinterpret_cast(region.GetAllocBase())); 177 | const hadesmem::ExportList export_list(_process, mod_pe); 178 | 179 | auto const mod_name = wstring_to_string(module.GetName()); 180 | 181 | auto const thunk_rva = static_cast( 182 | reinterpret_cast(thunk) - 183 | reinterpret_cast(_pe_file.GetBase())); 184 | 185 | IMAGE_THUNK_DATA64 thunk_data; 186 | ::memset(&thunk_data, 0, sizeof(thunk_data)); 187 | 188 | // if we are currently building the thunks for a different module, 189 | // it could be because that module has a forward to this function 190 | if (!force_new_dir && !_import_dirs.empty() && 191 | _import_dirs.back().name != mod_name) 192 | { 193 | // in this case, check the module we've been building to see if 194 | // there is a forwarder to this function 195 | auto const current_module = _import_dirs.back().name; 196 | auto const current_module_handle = 197 | ::GetModuleHandleA(current_module.c_str()); 198 | 199 | if (!current_module_handle) 200 | throw std::runtime_error("GetModuleHandle() failed"); 201 | 202 | const hadesmem::PeFile current_module_pe( 203 | _process, current_module_handle, hadesmem::PeFileType::kImage, 0); 204 | const hadesmem::ExportList current_module_export_list( 205 | _process, current_module_pe); 206 | 207 | for (auto const& exp : current_module_export_list) 208 | { 209 | if (!exp.IsForwarded()) 210 | continue; 211 | 212 | if (exp.IsForwardedByOrdinal()) 213 | { 214 | auto const target = ::GetProcAddress( 215 | module.GetHandle(), 216 | reinterpret_cast(exp.GetForwarderOrdinal())); 217 | 218 | if (target != function) 219 | continue; 220 | 221 | thunk_data.u1.AddressOfData = 222 | static_cast(write(static_cast(0xFACE))); 223 | write(exp.GetName()); 224 | if ((thunk_data.u1.AddressOfData + exp.GetName().length() + 225 | 1) % 226 | 2 != 227 | 0) 228 | write(0); 229 | 230 | gLog << "Thunk RVA: +0x" << std::hex << thunk_rva << " -> " 231 | << current_module << "!" << exp.GetName() 232 | << " (ordinal forward)" 234 | << std::endl; 235 | 236 | _import_dirs.back().thunks.push_back(thunk_data); 237 | ::memcpy(thunk, &thunk_data, sizeof(thunk_data)); 238 | 239 | return; 240 | } 241 | else 242 | { 243 | auto const target = ::GetProcAddress( 244 | module.GetHandle(), exp.GetForwarderFunction().c_str()); 245 | 246 | if (target != function) 247 | continue; 248 | 249 | // TODO: Make actual hint 250 | thunk_data.u1.AddressOfData = 251 | static_cast(write(static_cast(0xFEED))); 252 | write(exp.GetName()); 253 | if ((thunk_data.u1.AddressOfData + exp.GetName().length() + 254 | 1) % 255 | 2 != 256 | 0) 257 | write(0); 258 | 259 | gLog << "Thunk RVA: +0x" << std::hex << thunk_rva << " -> " 260 | << current_module << "!" << exp.GetName() 261 | << " (name forward)" << std::endl; 262 | 263 | _import_dirs.back().thunks.push_back(thunk_data); 264 | ::memcpy(thunk, &thunk_data, sizeof(thunk_data)); 265 | 266 | return; 267 | } 268 | } 269 | } 270 | 271 | // if we reach here the possibility of a forward has been handled 272 | 273 | auto& import_dir = get_import_dir(function); 274 | 275 | for (auto const& e : export_list) 276 | if (e.GetVa() == function) 277 | { 278 | // if this import directory was just created, set the 279 | // first thunk based on this function 280 | if (import_dir.header.FirstThunk == 0) 281 | import_dir.header.FirstThunk = thunk_rva; 282 | 283 | // hint is WORD value of index into the export name 284 | // pointer table 285 | thunk_data.u1.AddressOfData = 286 | static_cast(write(static_cast(0xFACE))); 287 | write(e.GetName()); 288 | 289 | // the above data must align to an even boundary. if not, 290 | // add a single zero byte for padding 291 | if ((thunk_data.u1.AddressOfData + e.GetName().length() + 1) % 292 | 2 != 293 | 0) 294 | write(0); 295 | 296 | gLog << "Thunk RVA: +0x" << std::hex << thunk_rva << " -> " 297 | << module.GetName() << "!" << e.GetName() << std::endl; 298 | 299 | import_dir.thunks.push_back(thunk_data); 300 | ::memcpy(thunk, &thunk_data, sizeof(thunk_data)); 301 | 302 | return; 303 | } 304 | 305 | throw std::runtime_error("Failed to create thunk"); 306 | } 307 | 308 | void finalize(hadesmem::Section& new_section, 309 | std::vector& buffer) 310 | { 311 | if (_finalized) 312 | throw std::runtime_error("Cannot finalize import table twice"); 313 | 314 | IMAGE_THUNK_DATA64 empty_thunk_data; 315 | ::memset(&empty_thunk_data, 0, sizeof(empty_thunk_data)); 316 | 317 | // first, write thunks and update headers for each import directory 318 | for (auto& import_dir : _import_dirs) 319 | { 320 | // RVA of import lookup table (ILT) 321 | import_dir.header.OriginalFirstThunk = 322 | _new_section_va + static_cast(_buffer.size()); 323 | 324 | write(import_dir.thunks); 325 | 326 | // terminate the list of thunks 327 | write(empty_thunk_data); 328 | } 329 | 330 | // second, write the import directory entries 331 | auto const import_dir_rva = static_cast(_buffer.size()); 332 | for (auto& import_dir : _import_dirs) 333 | auto const dir_rva = static_cast(write(import_dir.header)); 334 | 335 | // third, update section data 336 | hadesmem::NtHeaders nt_header(_process, _pe_file); 337 | 338 | // enforce file alignment of new section 339 | _buffer.resize(round_up(static_cast(_buffer.size()), 340 | nt_header.GetFileAlignment())); 341 | new_section.SetSizeOfRawData(static_cast(_buffer.size())); 342 | new_section.SetVirtualSize(round_up(static_cast(_buffer.size()), 343 | nt_header.GetSectionAlignment())); 344 | new_section.UpdateWrite(); 345 | 346 | // fourth, update PE header 347 | nt_header.SetDataDirectoryVirtualAddress( 348 | hadesmem::PeDataDir::Import, _new_section_va + import_dir_rva); 349 | nt_header.SetDataDirectorySize( 350 | hadesmem::PeDataDir::Import, 351 | static_cast(_import_dirs.size() * 352 | sizeof(IMAGE_IMPORT_DESCRIPTOR))); 353 | 354 | nt_header.SetDataDirectoryVirtualAddress(hadesmem::PeDataDir::IAT, 355 | _iat_va); 356 | nt_header.SetDataDirectorySize(hadesmem::PeDataDir::IAT, import_dir_rva); 357 | 358 | nt_header.UpdateWrite(); 359 | 360 | buffer = std::move(_buffer); 361 | 362 | _finalized = true; 363 | } 364 | }; 365 | 366 | hadesmem::Section add_section(const hadesmem::Process& process, 367 | const hadesmem::PeFile& pe_file) 368 | { 369 | hadesmem::NtHeaders nt_header(process, pe_file); 370 | const hadesmem::Section last_section(process, pe_file, 371 | nt_header.GetNumberOfSections() - 1); 372 | 373 | auto const section_header_va = 374 | reinterpret_cast(last_section.GetBase()) + 375 | sizeof(IMAGE_SECTION_HEADER); 376 | auto const section_header_end_va = 377 | section_header_va + sizeof(IMAGE_SECTION_HEADER); 378 | 379 | if (section_header_end_va > 380 | (reinterpret_cast(pe_file.GetBase()) + 381 | nt_header.GetSizeOfHeaders())) 382 | throw std::runtime_error("No space remaining for new header"); 383 | 384 | IMAGE_SECTION_HEADER new_section; 385 | ::memset(&new_section, 0, sizeof(new_section)); 386 | ::memcpy(new_section.Name, ".wowim", sizeof(".wowim")); 387 | new_section.VirtualAddress = 388 | last_section.GetVirtualAddress() + last_section.GetVirtualSize(); 389 | new_section.PointerToRawData = 390 | last_section.GetPointerToRawData() + last_section.GetSizeOfRawData(); 391 | new_section.Characteristics = 392 | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA; 393 | ::memcpy(section_header_va, &new_section, sizeof(new_section)); 394 | nt_header.SetNumberOfSections(nt_header.GetNumberOfSections() + 1); 395 | nt_header.UpdateWrite(); 396 | 397 | return hadesmem::Section(process, pe_file, section_header_va); 398 | } 399 | } // namespace 400 | 401 | void rebuild_imports(const hadesmem::Process& process, 402 | const hadesmem::PeFile& pe_file, PVOID rdata, 403 | PVOID readonly_base, std::vector& buffer) 404 | { 405 | // this many consecutive null pointers will be interpreted as the end of the 406 | // imports 407 | static auto constexpr max_consecutive_nulls = 5; 408 | int null_count = 0; 409 | 410 | static auto constexpr max_errors = 10; 411 | int error_count = 0; 412 | 413 | // step one, create a new section for the import data. while some generic 414 | // tools will attempt to expand the .rdata section, experimentally this is 415 | // not possible for wow. 416 | auto new_section = add_section(process, pe_file); 417 | 418 | const hadesmem::Region import_region(process, rdata); 419 | const hadesmem::Region readonly_region(process, readonly_base); 420 | 421 | auto const readonly_start = reinterpret_cast(readonly_base); 422 | auto const readonly_end = readonly_start + readonly_region.GetSize(); 423 | 424 | auto const base = reinterpret_cast(import_region.GetAllocBase()); 425 | 426 | auto const rdata_va = 427 | static_cast(reinterpret_cast(rdata) - base); 428 | 429 | // assemble results within this structure 430 | ImportTable import_table(pe_file, new_section.GetVirtualAddress(), rdata_va); 431 | 432 | auto const import_region_end = reinterpret_cast( 433 | reinterpret_cast(rdata) + import_region.GetSize()); 434 | 435 | #ifdef _DEBUG 436 | gLog << "pe_file.GetBase(): 0x" << pe_file.GetBase() << "\nbase: 0x" << base 437 | << "\nrdata: 0x" << rdata << " region end: 0x" << import_region_end 438 | << std::endl; 439 | #endif 440 | 441 | // when true, the next function will force creation of a new import 442 | // directory 443 | auto force_new_import_dir = true; 444 | 445 | // step two, iterate over .rdata section and decrypt encrypted trampolines 446 | for (auto current_import = reinterpret_cast(rdata); 447 | current_import < import_region_end; ++current_import) 448 | { 449 | if (reinterpret_cast(*current_import) >= readonly_start && 450 | reinterpret_cast(*current_import) <= readonly_end) 451 | { 452 | auto const old = *current_import; 453 | auto const rva = 454 | reinterpret_cast(*current_import) - readonly_start; 455 | *current_import = reinterpret_cast(base + rva); 456 | } 457 | 458 | auto const thunk_ea = *current_import; 459 | 460 | auto const import_rva = static_cast( 461 | reinterpret_cast(current_import) - base); 462 | auto const thunk_rva = 463 | static_cast(reinterpret_cast(thunk_ea) - base); 464 | 465 | if (!thunk_ea) 466 | { 467 | if (++null_count >= max_consecutive_nulls) 468 | break; 469 | 470 | force_new_import_dir = true; 471 | continue; 472 | } 473 | 474 | null_count = 0; 475 | 476 | std::stringstream log; 477 | 478 | #ifdef _DEBUG 479 | log << "Import RVA: +0x" << std::hex << import_rva << " Thunk EA: 0x" 480 | << thunk_ea << " Thunk RVA: +0x" << thunk_rva << "\n"; 481 | #endif 482 | 483 | // if the thunk ea is not sane, skip it. this heuristic is not 484 | // necessary but greatly speeds up the process. without it, each 485 | // of these addresses would be examined and result in an exception 486 | // when hadesmem calls VirtualQueryEx() and it fails. 487 | if (reinterpret_cast(thunk_ea) >> 0x30) 488 | { 489 | log << "Bad thunk ea RVA: 0x" << std::hex << import_rva 490 | << " thunk_ea: 0x" << std::hex 491 | << reinterpret_cast(thunk_ea); 492 | 493 | gLog << log.str() << std::endl; 494 | 495 | if (++error_count >= max_errors) 496 | break; 497 | 498 | continue; 499 | } 500 | 501 | try 502 | { 503 | const hadesmem::Region thunk_region(process, thunk_ea); 504 | 505 | if (thunk_region.GetType() == MEM_FREE) 506 | continue; 507 | 508 | if (thunk_region.GetProtect() == PAGE_EXECUTE) 509 | { 510 | auto const rva = static_cast( 511 | reinterpret_cast(current_import) - 512 | reinterpret_cast(pe_file.GetBase())); 513 | 514 | log << "[Import Resolution]: Skipping import from +0x" << std::hex 515 | << rva 516 | << " because it points to unreadble " 517 | "PAGE_EXECUTE memory. If you see this, please file a " 518 | "bug."; 519 | 520 | gLog << log.str() << std::endl; 521 | 522 | if (++error_count > max_errors) 523 | break; 524 | 525 | continue; 526 | } 527 | 528 | if (thunk_region.GetProtect() != PAGE_EXECUTE_READ && 529 | thunk_region.GetProtect() != PAGE_EXECUTE_READWRITE && 530 | thunk_region.GetProtect() != PAGE_EXECUTE_WRITECOPY) 531 | continue; 532 | 533 | ConclicThreadContext ctx; 534 | 535 | if (!conclic_begin(thunk_ea, ctx)) 536 | { 537 | log << "concolic failed"; 538 | gLog << log.str() << std::endl; 539 | 540 | if (++error_count >= max_errors) 541 | break; 542 | 543 | continue; 544 | } 545 | 546 | // the ImportTable class will build import directory entries as 547 | // needed for eventual serialization 548 | import_table.add_function(current_import, 549 | reinterpret_cast(ctx.rax), 550 | force_new_import_dir); 551 | 552 | force_new_import_dir = false; 553 | 554 | gLog << log.str() << std::endl; 555 | } 556 | catch (const std::exception& 557 | #ifdef _DEBUG 558 | e 559 | #endif 560 | ) 561 | { 562 | #ifdef _DEBUG 563 | gLog << "Exception: " << boost::diagnostic_information(e) 564 | << std::endl; 565 | #endif 566 | break; 567 | } 568 | } 569 | 570 | // serialize the import table into memory in preparation for dumping to 571 | // the disk 572 | import_table.finalize(new_section, buffer); 573 | } -------------------------------------------------------------------------------- /dll/imports.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | void rebuild_imports(const hadesmem::Process& process, 34 | const hadesmem::PeFile& pe_file, PVOID rdata, 35 | PVOID readonly_base, std::vector& buffer); -------------------------------------------------------------------------------- /dll/log.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "log.hpp" 26 | 27 | #include "misc.hpp" 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | int LogStreamBuffer::sync() 35 | { 36 | if (!str().empty()) 37 | { 38 | _callback(str()); 39 | str(""); 40 | } 41 | 42 | return std::stringbuf::sync(); 43 | } 44 | Log::Log(std::function callback) 45 | : _buffer(callback), std::ostream(&_buffer) 46 | { 47 | } 48 | 49 | std::ostream& operator<<(std::ostream& _Ostr, const std::wstring& _Str) 50 | { 51 | return _Ostr << wstring_to_string(_Str); 52 | } 53 | 54 | Log gLog( 55 | [](const std::string& buff) 56 | { 57 | auto const exe_path = get_exe_path(); 58 | auto const parent = exe_path.parent_path(); 59 | auto const log_path = parent / "log.txt"; 60 | 61 | std::ofstream out(log_path, std::ios::app); 62 | 63 | if (!out) 64 | throw std::runtime_error("Failed to open log file"); 65 | 66 | out << buff; 67 | out.close(); 68 | }); 69 | -------------------------------------------------------------------------------- /dll/log.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | class LogStreamBuffer : public std::stringbuf 32 | { 33 | private: 34 | std::function _callback; 35 | 36 | public: 37 | LogStreamBuffer(std::function callback) 38 | : _callback(callback) 39 | { 40 | } 41 | int sync(); 42 | }; 43 | 44 | class Log : public std::ostream 45 | { 46 | private: 47 | LogStreamBuffer _buffer; 48 | 49 | public: 50 | Log(std::function callback); 51 | }; 52 | 53 | std::ostream& operator<<(std::ostream& _Ostr, const std::wstring& _Str); 54 | 55 | // log to file 56 | extern Log gLog; 57 | -------------------------------------------------------------------------------- /dll/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "log.hpp" 26 | #include "misc.hpp" 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | // protect against TLS callbacks interfering with our task 42 | void InitializeTLSProtection(const hadesmem::Process& process, 43 | size_t ldrp_call_init_routine_rva, 44 | size_t tls_callback_caller_rva, 45 | size_t guard_dispatch_icall_offset, PVOID wow_base, 46 | DWORD main_thread_id, size_t text_start_rva, 47 | size_t text_end_rva); 48 | 49 | // hook BaseThreadInitThunk which will call the entry point 50 | void HookBaseThreadInitThunk(const hadesmem::Process& process, 51 | DWORD main_thread_id, PVOID wow_base, 52 | DWORD wow_pe_size); 53 | 54 | // hook NtSetInformationThread to prevent ThreadHideFromDebugger 55 | void HookSetInformationThread(const hadesmem::Process& process); 56 | 57 | extern void do_dump(PVOID, DWORD, PVOID); 58 | 59 | struct TlsCallbackEntry 60 | { 61 | PVOID address; 62 | PVOID callbacks_before[6]; 63 | std::uint8_t initial_bytes[16]; 64 | std::uint8_t last_seen_bytes[sizeof(TlsCallbackEntry::initial_bytes)]; 65 | }; 66 | 67 | static std::vector tls_callback_history; 68 | 69 | static PVOID g_tls_callbacks; 70 | 71 | extern "C" __declspec(dllexport) void 72 | DumpBinary(size_t ldrp_call_init_routine_rva, size_t tls_callback_caller_rva, 73 | size_t guard_dispatch_icall_offset, DWORD main_thread_id, 74 | PVOID wow_base, DWORD wow_pe_size, size_t text_start_rva, 75 | size_t text_end_rva) 76 | { 77 | tls_callback_history.clear(); 78 | tls_callback_history.reserve(10); 79 | 80 | try 81 | { 82 | const hadesmem::Process process(::GetCurrentProcessId()); 83 | const hadesmem::PeFile wow_pe(process, wow_base, 84 | hadesmem::PeFileType::kImage, wow_pe_size); 85 | const hadesmem::TlsDir tls_dir(process, wow_pe); 86 | 87 | g_tls_callbacks = 88 | reinterpret_cast(tls_dir.GetAddressOfCallBacks()); 89 | 90 | InitializeTLSProtection(process, ldrp_call_init_routine_rva, 91 | tls_callback_caller_rva, 92 | guard_dispatch_icall_offset, wow_base, 93 | main_thread_id, text_start_rva, text_end_rva); 94 | 95 | HookBaseThreadInitThunk(process, main_thread_id, wow_base, wow_pe_size); 96 | } 97 | catch (const std::exception& e) 98 | { 99 | std::wstringstream str; 100 | str << "Unpacker initialization failed: " << e.what(); 101 | ::MessageBox(nullptr, str.str().c_str(), L"Unpacker", 0); 102 | } 103 | } 104 | 105 | void __fastcall log_tls(PVOID base, DWORD reason, PVOID callback) 106 | { 107 | #ifdef _DEBUG 108 | // first, see if any previously encountered TLS callbacks have changed 109 | for (auto& e : tls_callback_history) 110 | { 111 | if (memcmp(e.last_seen_bytes, e.address, sizeof(e.last_seen_bytes))) 112 | { 113 | gLog << "Before calling TLS callback 0x" << callback 114 | << ", observed change to callback at 0x" << e.address 115 | << ". New bytes:"; 116 | for (auto i = 0u; i < sizeof(e.last_seen_bytes); ++i) 117 | { 118 | const unsigned int byte = *reinterpret_cast( 119 | reinterpret_cast(e.address) + i); 120 | gLog << " " << std::hex << std::uppercase << std::setw(2) 121 | << std::setfill('0') << byte; 122 | } 123 | gLog << std::endl; 124 | 125 | ::memcpy(e.last_seen_bytes, e.address, sizeof(e.last_seen_bytes)); 126 | } 127 | } 128 | #endif 129 | 130 | TlsCallbackEntry entry; 131 | ::memset(&entry, 0, sizeof(entry)); 132 | 133 | entry.address = callback; 134 | ::memcpy(entry.initial_bytes, callback, sizeof(entry.initial_bytes)); 135 | ::memcpy(entry.last_seen_bytes, callback, sizeof(entry.last_seen_bytes)); 136 | 137 | static auto constexpr callback_count = 138 | sizeof(entry.callbacks_before) / sizeof(entry.callbacks_before[0]); 139 | 140 | auto current_callback = reinterpret_cast(g_tls_callbacks); 141 | 142 | for (auto i = 0u; i < callback_count; ++i) 143 | { 144 | entry.callbacks_before[i] = *current_callback; 145 | 146 | if (!entry.callbacks_before[i]) 147 | break; 148 | 149 | ++current_callback; 150 | } 151 | 152 | // the last should always be nullptr 153 | if (entry.callbacks_before[callback_count - 1]) 154 | throw std::runtime_error("TLS callback history overflow"); 155 | 156 | tls_callback_history.push_back(entry); 157 | 158 | #ifdef _DEBUG 159 | gLog << "Callbacks before calling this one:\n"; 160 | 161 | for (auto i = 0u; i < callback_count; ++i) 162 | { 163 | if (!entry.callbacks_before[i]) 164 | break; 165 | 166 | gLog << " " << std::dec << i << ": 0x" << entry.callbacks_before[i] 167 | << std::endl; 168 | } 169 | 170 | gLog << "Calling TLS callback in main thread: 0x" << callback 171 | << " reason: " << std::dec << reason << " bytes:"; 172 | 173 | for (auto i = 0u; i < sizeof(entry.initial_bytes); ++i) 174 | { 175 | const unsigned int byte = *reinterpret_cast( 176 | reinterpret_cast(callback) + i); 177 | gLog << " " << std::hex << std::uppercase << std::setw(2) 178 | << std::setfill('0') << byte; 179 | } 180 | 181 | gLog << std::endl; 182 | 183 | gLog << "\n\n" << std::endl; 184 | #endif 185 | } 186 | 187 | void InitializeTLSProtection(const hadesmem::Process& process, 188 | size_t ldrp_call_init_routine_rva, 189 | size_t tls_callback_caller_rva, 190 | size_t guard_dispatch_icall_offset, PVOID wow_base, 191 | DWORD main_thread_id, size_t text_start_rva, 192 | size_t text_end_rva) 193 | { 194 | auto const ntdll = reinterpret_cast(::GetModuleHandle(L"ntdll")); 195 | 196 | auto const tls_callback_caller = ntdll + tls_callback_caller_rva; 197 | auto const ldrp_call_init_routine = ntdll + ldrp_call_init_routine_rva; 198 | 199 | // address of where guard_dispatch_icall is called for TLS callbacks 200 | auto const guard_dispatch_icall_call_site = 201 | ldrp_call_init_routine + guard_dispatch_icall_offset; 202 | 203 | // address of the function itself 204 | auto const guard_dispatch_icall = 205 | guard_dispatch_icall_call_site + 5 + 206 | hadesmem::Read( 207 | process, reinterpret_cast(guard_dispatch_icall_call_site + 1)); 208 | 209 | if (hadesmem::Read( 210 | process, reinterpret_cast(guard_dispatch_icall)) != 0xE9) 211 | throw std::runtime_error("Did not find JMP in guard_dispatch_icall"); 212 | 213 | auto const jmp_dest = 214 | guard_dispatch_icall + 5u + 215 | hadesmem::Read( 216 | process, reinterpret_cast(guard_dispatch_icall + 1)); 217 | 218 | static constexpr std::uint8_t payload[] = { 219 | // mov r9, gs:[0x48] (current thread id) 220 | 0x65, 0x4c, 0x8b, 0x0c, 0x25, 0x48, 0x00, 0x00, 0x00, 221 | // cmp r9,
222 | 0x49, 0x81, 0xf9, 0x00, 0x00, 0x00, 0x00, 223 | // jne 224 | 0x75, 0x49, 225 | // mov r9, 226 | 0x49, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 227 | // cmp rax, r9 228 | 0x4c, 0x39, 0xc8, 229 | // jb 230 | 0x72, 0x3a, 231 | // mov r9, 232 | 0x49, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 233 | // cmp rax, r9 234 | 0x4c, 0x39, 0xc8, 235 | // jae 236 | 0x73, 0x2b, 237 | // push rbp 238 | 0x55, 239 | // mov rbp, rsp 240 | 0x48, 0x89, 0xe5, 241 | // push rax 242 | 0x50, 243 | // push rcx 244 | 0x51, 245 | // push rdx 246 | 0x52, 247 | // push r8 248 | 0x41, 0x50, 249 | // mov r8, rax 250 | 0x49, 0x89, 0xc0, 251 | // mov rax, 252 | 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 253 | // sub rsp, 0x20 (shadow space) 254 | 0x48, 0x83, 0xec, 0x20, 255 | // call rax 256 | 0xff, 0xd0, 257 | // add rsp, 0x20 (shadow space) 258 | 0x48, 0x83, 0xc4, 0x20, 259 | // pop r8 260 | 0x41, 0x58, 261 | // pop rdx 262 | 0x5a, 263 | // mov edx, 1 (DLL_PROCESS_ATTACH) 264 | 0xba, 0x01, 0x00, 0x00, 0x00, 265 | // pop rcx 266 | 0x59, 267 | // pop rax 268 | 0x58, 269 | // leave 270 | 0xc9, 271 | // jmp 272 | 0xe9, 0x00, 0x00, 0x00, 0x00}; 273 | 274 | auto static constexpr main_thread_ofs = 12; 275 | auto static constexpr wow_base_ofs = 20; 276 | auto static constexpr wow_end_ofs = 35; 277 | auto static constexpr func_ptr_ofs = 62; 278 | auto static constexpr jmp_ptr_ofs = sizeof(payload) - 4; 279 | 280 | bool payload_written = false; 281 | for (auto i = 1u; i < 10; ++i) 282 | { 283 | // look for large enough alignment space for our payload 284 | auto const check_address = guard_dispatch_icall + 0x10u * i; 285 | 286 | // check if there is space in this location 287 | bool space = true; 288 | for (auto j = 0u; j <= sizeof(payload); ++j) 289 | if (hadesmem::Read( 290 | process, reinterpret_cast(check_address + j)) != 0xCC) 291 | { 292 | space = false; 293 | break; 294 | } 295 | 296 | if (!space) 297 | continue; 298 | 299 | auto const new_rva = static_cast( 300 | jmp_dest - (check_address + jmp_ptr_ofs + 4)); 301 | 302 | // write payload 303 | hadesmem::Write(process, reinterpret_cast(check_address), payload); 304 | 305 | // update values within payload 306 | hadesmem::Write(process, 307 | reinterpret_cast(check_address + main_thread_ofs), 308 | main_thread_id); 309 | hadesmem::Write(process, 310 | reinterpret_cast(check_address + wow_base_ofs), 311 | reinterpret_cast(wow_base) + text_start_rva); 312 | hadesmem::Write(process, 313 | reinterpret_cast(check_address + wow_end_ofs), 314 | reinterpret_cast(wow_base) + text_end_rva); 315 | hadesmem::Write(process, 316 | reinterpret_cast(check_address + func_ptr_ofs), 317 | &log_tls); 318 | hadesmem::Write(process, 319 | reinterpret_cast(check_address + jmp_ptr_ofs), 320 | new_rva); 321 | 322 | // update call within LdrpCallInitRoutine 323 | auto const trampoline_offset = static_cast( 324 | static_cast(check_address) - 325 | static_cast(guard_dispatch_icall_call_site) - 5); 326 | 327 | hadesmem::Write( 328 | process, reinterpret_cast(guard_dispatch_icall_call_site + 1), 329 | trampoline_offset); 330 | 331 | // done! 332 | payload_written = true; 333 | break; 334 | } 335 | 336 | if (!payload_written) 337 | throw std::runtime_error("No space for payload"); 338 | 339 | // redirect call to guard_dispatch_icall in LdrCallInitRoutine 340 | 341 | for (auto i = 0u; i < 0x100; ++i) 342 | { 343 | // static signature check 344 | if (hadesmem::Read( 345 | process, reinterpret_cast(ldrp_call_init_routine + i)) != 346 | 0x498BC4E8) 347 | continue; 348 | 349 | auto const offset = hadesmem::Read( 350 | process, reinterpret_cast(ldrp_call_init_routine + i + 4)); 351 | auto const target = 352 | reinterpret_cast(ldrp_call_init_routine + i + 8 + offset); 353 | 354 | std::stringstream str; 355 | 356 | str << "Address: " << (PVOID)(ldrp_call_init_routine + i) << " offset: 0x" 357 | << std::hex << offset << " target: " << target; 358 | 359 | ::MessageBoxA(nullptr, str.str().c_str(), "DEBUG", 0); 360 | } 361 | } 362 | 363 | using BaseThreadInitThunkT = std::uint64_t (*)(std::uint64_t, PVOID, 364 | std::uint64_t); 365 | 366 | std::uint64_t BaseThreadInitThunkHook(hadesmem::PatchDetourBase* detour, 367 | PVOID base, DWORD wow_pe_size, 368 | DWORD main_thread_id, std::uint64_t a1, 369 | PVOID func, std::uint64_t a3) 370 | { 371 | if (::GetCurrentThreadId() != main_thread_id) 372 | return detour->GetTrampolineT()(a1, func, a3); 373 | 374 | try 375 | { 376 | detour->Remove(); 377 | #ifdef _DEBUG 378 | static auto constexpr callback_count = 379 | sizeof(TlsCallbackEntry::callbacks_before) / 380 | sizeof(TlsCallbackEntry::callbacks_before[0]); 381 | 382 | auto current_callback = reinterpret_cast(g_tls_callbacks); 383 | 384 | gLog << "TLS callbacks before dump:\n"; 385 | for (auto i = 0u; i < callback_count; ++i) 386 | { 387 | if (!*current_callback) 388 | break; 389 | 390 | gLog << " " << std::dec << i << ": 0x" << *current_callback 391 | << std::endl; 392 | ++current_callback; 393 | } 394 | #endif 395 | do_dump(base, wow_pe_size, func); 396 | } 397 | catch (const std::exception& e) 398 | { 399 | ::MessageBoxA(nullptr, boost::diagnostic_information(e).c_str(), 400 | "Unpacker Error", MB_ICONERROR); 401 | } 402 | 403 | ::TerminateProcess(::GetCurrentProcess(), 0); 404 | 405 | detour->Remove(); 406 | return detour->GetTrampolineT()(a1, func, a3); 407 | } 408 | 409 | void HookBaseThreadInitThunk(const hadesmem::Process& process, 410 | DWORD main_thread_id, PVOID wow_base, 411 | DWORD wow_pe_size) 412 | { 413 | auto const k32 = ::GetModuleHandle(L"kernel32"); 414 | if (!k32) 415 | throw std::runtime_error("Could not find kernel32"); 416 | 417 | auto const orig = reinterpret_cast( 418 | ::GetProcAddress(k32, "BaseThreadInitThunk")); 419 | 420 | if (!orig) 421 | throw std::runtime_error("Could not find kernel32!BaseThreadInitThunk"); 422 | 423 | // this is a memory leak but the process will not be long running so we 424 | // dont care 425 | auto const init_detour = new hadesmem::PatchDetour( 426 | process, orig, 427 | [main_thread_id, wow_base, wow_pe_size](hadesmem::PatchDetourBase* detour, 428 | std::uint64_t a1, PVOID func, 429 | std::uint64_t a3) 430 | { 431 | return BaseThreadInitThunkHook(detour, wow_base, wow_pe_size, 432 | main_thread_id, a1, func, a3); 433 | }); 434 | 435 | init_detour->Apply(); 436 | } 437 | 438 | void HookSetInformationThread(const hadesmem::Process& process) 439 | { 440 | auto const ntdll = ::GetModuleHandle(L"ntdll"); 441 | if (!ntdll) 442 | throw std::runtime_error("Could not find ntdll"); 443 | 444 | using SetInformationThreadT = 445 | NTSTATUS (*)(HANDLE, THREADINFOCLASS, PVOID, ULONG); 446 | 447 | auto const orig = reinterpret_cast( 448 | ::GetProcAddress(ntdll, "NtSetInformationThread")); 449 | 450 | if (!orig) 451 | throw std::runtime_error("Could not find ntdll!NtSetInformationThread"); 452 | 453 | auto const set_information_thread_detour = 454 | new hadesmem::PatchDetour( 455 | process, orig, 456 | [](hadesmem::PatchDetourBase* detour, HANDLE thread_handle, 457 | THREADINFOCLASS info_class, PVOID thread_info, ULONG info_length) 458 | { 459 | static constexpr THREADINFOCLASS ThreadHideFromDebugger = 460 | static_cast(0x11); 461 | 462 | if (info_class == ThreadHideFromDebugger && !thread_info && 463 | !info_length) 464 | return static_cast(0); 465 | 466 | return detour->GetTrampolineT()( 467 | thread_handle, info_class, thread_info, info_length); 468 | }); 469 | 470 | set_information_thread_detour->Apply(); 471 | } -------------------------------------------------------------------------------- /dll/misc.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "misc.hpp" 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | fs::path get_exe_path() 33 | { 34 | TCHAR filename[1024]; 35 | if (!::GetModuleFileName(nullptr, filename, sizeof(filename) / sizeof(TCHAR))) 36 | { 37 | throw std::runtime_error("GetModuleFileName() failed"); 38 | } 39 | 40 | return fs::path(filename); 41 | } 42 | 43 | // modified from https://stackoverflow.com/a/9194117 44 | DWORD round_up(DWORD numToRound, DWORD multiple) 45 | { 46 | assert(multiple > 0); 47 | return ((numToRound + multiple - 1) / multiple) * multiple; 48 | } 49 | 50 | // taken from https://stackoverflow.com/a/18374698 51 | std::string wstring_to_string(const std::wstring& str) 52 | { 53 | using convert_typeX = std::codecvt_utf8; 54 | std::wstring_convert converterX; 55 | 56 | return converterX.to_bytes(str); 57 | } -------------------------------------------------------------------------------- /dll/misc.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | namespace fs = std::experimental::filesystem; 37 | 38 | fs::path get_exe_path(); 39 | DWORD round_up(DWORD numToRound, DWORD multiple); 40 | 41 | std::string wstring_to_string(const std::wstring& str); 42 | 43 | template 44 | T rebase(void* new_base, T address) 45 | { 46 | const hadesmem::Process process(::GetCurrentProcessId()); 47 | const hadesmem::Region region(process, 48 | reinterpret_cast(address)); 49 | 50 | if (!region.GetAllocBase()) 51 | throw std::runtime_error( 52 | "No alloc base for given address. Cannot compute offset."); 53 | 54 | auto const rva = static_cast( 55 | reinterpret_cast(address) - 56 | reinterpret_cast(region.GetAllocBase())); 57 | 58 | return reinterpret_cast(reinterpret_cast(new_base) + rva); 59 | } 60 | -------------------------------------------------------------------------------- /dll/relocations.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "relocations.hpp" 26 | 27 | #include "misc.hpp" 28 | 29 | void apply_relocations(PVOID image_base) 30 | { 31 | const auto module = PIMAGE_DOS_HEADER(image_base); 32 | auto headers = 33 | PIMAGE_NT_HEADERS64(reinterpret_cast(module) + module->e_lfanew); 34 | auto relocation = PIMAGE_BASE_RELOCATION( 35 | reinterpret_cast(module) + 36 | headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC] 37 | .VirtualAddress); 38 | 39 | while (relocation->SizeOfBlock && relocation->VirtualAddress) 40 | { 41 | const auto block_relocation_count = 42 | (relocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 43 | sizeof(WORD); 44 | const auto block_entries = PWORD(reinterpret_cast(relocation) + 45 | sizeof(IMAGE_BASE_RELOCATION)); 46 | 47 | for (size_t i = 0; i < block_relocation_count; i++) 48 | { 49 | switch (block_entries[i] >> 12) 50 | { 51 | case IMAGE_REL_BASED_DIR64: 52 | { 53 | const auto p = reinterpret_cast( 54 | reinterpret_cast(module) + 55 | relocation->VirtualAddress + (block_entries[i] & 0xFFF)); 56 | 57 | *p = rebase(reinterpret_cast( 58 | headers->OptionalHeader.ImageBase), 59 | *p); 60 | } 61 | break; 62 | case IMAGE_REL_BASED_ABSOLUTE: 63 | case IMAGE_REL_BASED_HIGHLOW: 64 | case IMAGE_REL_BASED_HIGH: 65 | case IMAGE_REL_BASED_LOW: 66 | default: 67 | { 68 | // No need to fix absolute relocation it's just a dummy for 69 | // alignment. Other relocation types are not used in 64bit 70 | // binaries. 71 | } 72 | break; 73 | } 74 | } 75 | 76 | relocation = PIMAGE_BASE_RELOCATION(reinterpret_cast(relocation) + 77 | relocation->SizeOfBlock); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /dll/relocations.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | 29 | void apply_relocations(PVOID base); 30 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(EXECUTABLE_NAME dumpwow) 2 | 3 | include_directories(Include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}) 4 | 5 | set(SOURCE_FILES 6 | main.cpp 7 | ) 8 | 9 | add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES}) 10 | target_link_libraries(${EXECUTABLE_NAME} shlwapi.lib asmjit.lib udis86.lib) 11 | 12 | install(TARGETS ${EXECUTABLE_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}") 13 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | #define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 25 | 26 | #include "raii_proc.hpp" 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | 49 | namespace fs = std::experimental::filesystem; 50 | 51 | bool launch_wow_suspended(const fs::path& path, PROCESS_INFORMATION& proc_info); 52 | hadesmem::PeFile find_wow_pe(const hadesmem::Process& process, size_t& text_start, 53 | size_t& text_end); 54 | bool find_tls_offsets(size_t& ldrp_call_init_routine_rva, 55 | size_t& tls_callback_caller_rva, 56 | size_t& guard_dispatch_icall_offset); 57 | void* find_tls_callback_directory(const hadesmem::Process& process, 58 | const hadesmem::PeFile& pe); 59 | BOOL control_handler(DWORD ctrl_type); 60 | void process_log_file(const fs::path& exe_path); 61 | 62 | std::atomic_bool g_exit_wow; 63 | 64 | int main(int argc, char* argv[]) 65 | { 66 | if (argc != 2) 67 | { 68 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 69 | return EXIT_FAILURE; 70 | } 71 | 72 | size_t ldrp_call_init_routine_rva, tls_callback_caller_rva, 73 | guard_dispatch_icall_offset; 74 | 75 | if (!find_tls_offsets(ldrp_call_init_routine_rva, tls_callback_caller_rva, 76 | guard_dispatch_icall_offset)) 77 | { 78 | std::cerr << "Failed to find TLS offsets" << std::endl; 79 | return EXIT_FAILURE; 80 | } 81 | 82 | const fs::path path(argv[1]); 83 | 84 | try 85 | { 86 | if (!::SetConsoleCtrlHandler(control_handler, TRUE)) 87 | { 88 | std::cerr << "SetConsoleCtrlHandler failed" << std::endl; 89 | return EXIT_FAILURE; 90 | } 91 | 92 | PROCESS_INFORMATION proc_info; 93 | if (!launch_wow_suspended(path, proc_info)) 94 | { 95 | std::cerr << "launch_wow_suspended failed" << std::endl; 96 | return EXIT_FAILURE; 97 | } 98 | 99 | std::cout << "Wow PID: " << proc_info.dwProcessId 100 | << std::endl; 101 | g_exit_wow = false; 102 | 103 | const hadesmem::Process process(proc_info.dwProcessId); 104 | 105 | // ensure process is killed upon exit 106 | const RaiiProc proc_killer(process.GetId()); 107 | 108 | size_t text_start_rva, text_end_rva; 109 | auto const pe_file = find_wow_pe(process, text_start_rva, text_end_rva); 110 | 111 | std::cout << "Wow base address: 0x" << std::hex 112 | << reinterpret_cast(pe_file.GetBase()) 113 | << std::endl; 114 | 115 | // temporarily disable TLS callbacks to prevent them from executing 116 | // when we inject 117 | auto const tls_callback_directory = 118 | find_tls_callback_directory(process, pe_file); 119 | 120 | if (!tls_callback_directory) 121 | { 122 | std::cerr << "Unable to find TLS callback directory" << std::endl; 123 | return EXIT_FAILURE; 124 | } 125 | 126 | std::cout << "TLS callback directory: 0x" << std::hex 127 | << reinterpret_cast(tls_callback_directory) 128 | << std::endl; 129 | 130 | auto const first_callback = 131 | hadesmem::Read(process, tls_callback_directory); 132 | 133 | std::cout << "First TLS callback: 0x" << std::hex 134 | << reinterpret_cast(first_callback) 135 | << std::endl; 136 | 137 | hadesmem::Write(process, tls_callback_directory, nullptr); 138 | 139 | auto const verify = 140 | hadesmem::Read(process, tls_callback_directory); 141 | 142 | if (verify) 143 | { 144 | std::cerr << "Failed to zero first TLS callback" << std::endl; 145 | return EXIT_FAILURE; 146 | } 147 | 148 | // with the TLS callbacks disabled, our DLL may be safely injected 149 | const hadesmem::Module unpacker( 150 | process, hadesmem::InjectDll(process, L"unpacker.dll", 151 | hadesmem::InjectFlags::kPathResolution)); 152 | 153 | // call init function in DLL 154 | auto const func = reinterpret_cast( 156 | hadesmem::FindProcedure(process, unpacker, "DumpBinary")); 157 | 158 | hadesmem::Call(process, func, hadesmem::CallConv::kDefault, 159 | ldrp_call_init_routine_rva, tls_callback_caller_rva, 160 | guard_dispatch_icall_offset, proc_info.dwThreadId, 161 | pe_file.GetBase(), pe_file.GetSize(), text_start_rva, 162 | text_end_rva); 163 | 164 | // restore first TLS callback 165 | hadesmem::Write(process, tls_callback_directory, first_callback); 166 | 167 | CONTEXT context; 168 | memset(&context, 0, sizeof(context)); 169 | context.ContextFlags = CONTEXT_ALL; 170 | ::GetThreadContext(proc_info.hThread, &context); 171 | 172 | if (!::ResumeThread(proc_info.hThread)) 173 | { 174 | std::cerr << "Failed to resume main thread" << std::endl; 175 | return EXIT_FAILURE; 176 | } 177 | 178 | DWORD exit_code = 0; 179 | 180 | do 181 | { 182 | if (g_exit_wow) 183 | { 184 | std::cout << "Received CTRL-C. Terminating wow..." << std::endl; 185 | ::TerminateProcess(process.GetHandle(), 0); 186 | g_exit_wow = false; 187 | } 188 | 189 | if (!::GetExitCodeProcess(proc_info.hProcess, &exit_code)) 190 | { 191 | std::cerr << "GetExitCodeProcess failed" << std::endl; 192 | return EXIT_FAILURE; 193 | } 194 | 195 | // if there is a different exit code, the process has exited 196 | if (exit_code != STILL_ACTIVE) 197 | break; 198 | 199 | // if STILL_ACTIVE is the exit code, the process may have chosen to 200 | // exit using that error code, so try one more check 201 | if (::WaitForSingleObject(proc_info.hProcess, 0) != WAIT_TIMEOUT) 202 | break; 203 | 204 | // if the waiting timed out, it means the process is still running. 205 | // so let us sleep for a little while and then check again 206 | std::this_thread::sleep_for(std::chrono::milliseconds(300)); 207 | } while (true); 208 | 209 | if (!::SetConsoleCtrlHandler(control_handler, FALSE)) 210 | { 211 | std::cerr << "SetConsoleCtrlHandler failed" << std::endl; 212 | return EXIT_FAILURE; 213 | } 214 | 215 | std::cout << "Wow exited with code: 0x" << std::hex << exit_code 216 | << std::endl; 217 | 218 | process_log_file(path); 219 | } 220 | catch (const std::exception& e) 221 | { 222 | std::cerr << "Error:\n" << boost::diagnostic_information(e) << std::endl; 223 | process_log_file(path); 224 | return EXIT_FAILURE; 225 | } 226 | 227 | return EXIT_SUCCESS; 228 | } 229 | 230 | bool launch_wow_suspended(const fs::path& path, PROCESS_INFORMATION& proc_info) 231 | { 232 | // disable ASLR for subprocesses. this will cause the base address used 233 | // in memory to match the base address that static analysis tools will use 234 | SIZE_T cb; 235 | if (!::InitializeProcThreadAttributeList(nullptr, 1, 0, &cb) && 236 | ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) 237 | return false; 238 | 239 | auto attribs = reinterpret_cast(malloc(cb)); 240 | 241 | if (!attribs) 242 | return false; 243 | 244 | if (!::InitializeProcThreadAttributeList(attribs, 1, 0, &cb)) 245 | { 246 | free(attribs); 247 | return false; 248 | } 249 | 250 | DWORD64 attribute = 251 | PROCESS_CREATION_MITIGATION_POLICY_FORCE_RELOCATE_IMAGES_ALWAYS_OFF; 252 | if (!::UpdateProcThreadAttribute( 253 | attribs, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &attribute, 254 | sizeof(DWORD64), nullptr, nullptr)) 255 | { 256 | free(attribs); 257 | return false; 258 | } 259 | 260 | // launch wow in a suspended state 261 | STARTUPINFO start_info {}; 262 | start_info.cb = static_cast(sizeof(start_info)); 263 | memset(&proc_info, 0, sizeof(proc_info)); 264 | 265 | wchar_t path_raw[MAX_PATH]; 266 | memcpy(&path_raw[0], path.wstring().c_str(), 267 | (1 + path.wstring().length()) * sizeof(wchar_t)); 268 | 269 | auto const result = 270 | !!::CreateProcessW(path_raw, nullptr, nullptr, nullptr, FALSE, 271 | CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, nullptr, 272 | nullptr, &start_info, &proc_info); 273 | 274 | return result; 275 | } 276 | 277 | hadesmem::PeFile find_wow_pe(const hadesmem::Process& process, size_t& text_start, 278 | size_t& text_end) 279 | { 280 | const hadesmem::RegionList region_list(process); 281 | 282 | // find the PE header for wow 283 | for (auto const& region : region_list) 284 | { 285 | if (region.GetState() == MEM_FREE) 286 | continue; 287 | 288 | if (region.GetType() != MEM_IMAGE) 289 | continue; 290 | 291 | if (region.GetProtect() != PAGE_READONLY) 292 | continue; 293 | 294 | if (region.GetAllocBase() != region.GetBase()) 295 | continue; 296 | 297 | hadesmem::PeFile pe_file(process, region.GetBase(), 298 | hadesmem::PeFileType::kImage, 299 | static_cast(region.GetSize())); 300 | 301 | std::vector callbacks; 302 | 303 | try 304 | { 305 | const hadesmem::TlsDir tls_dir(process, pe_file); 306 | tls_dir.GetCallbacks(std::back_inserter(callbacks)); 307 | } 308 | catch (const hadesmem::Error&) 309 | { 310 | continue; 311 | } 312 | 313 | // wow has tls callbacks 314 | if (callbacks.empty()) 315 | continue; 316 | 317 | const hadesmem::SectionList sl {process, pe_file}; 318 | 319 | for (auto const& sec : sl) 320 | { 321 | if (sec.GetName() == ".text") 322 | { 323 | text_start = sec.GetVirtualAddress(); 324 | text_end = text_start + sec.GetSizeOfRawData(); 325 | return pe_file; 326 | } 327 | } 328 | 329 | break; 330 | } 331 | 332 | throw std::runtime_error("WoW PE header not found"); 333 | } 334 | 335 | void* find_tls_callback_directory(const hadesmem::Process& process, 336 | const hadesmem::PeFile& pe) 337 | { 338 | try 339 | { 340 | const hadesmem::TlsDir tls_dir(process, pe); 341 | return reinterpret_cast(tls_dir.GetAddressOfCallBacks()); 342 | } 343 | catch (const hadesmem::Error&) 344 | { 345 | return nullptr; 346 | } 347 | } 348 | 349 | bool find_tls_offsets(size_t& ldrp_call_init_routine_rva, 350 | size_t& tls_callback_caller_rva, 351 | size_t& guard_dispatch_icall_offset) 352 | { 353 | auto const ntdll = ::GetModuleHandle(L"ntdll"); 354 | 355 | if (!ntdll) 356 | return false; 357 | 358 | const hadesmem::Process process(::GetCurrentProcessId()); 359 | 360 | auto const peb_access = reinterpret_cast( 361 | hadesmem::Find(process, L"ntdll.dll", 362 | L"48 89 54 24 ?? 44 89 44 24 ?? 65 48 8b 04 25 60 00 00 " 363 | L"00 48 8b 90 ?? 00 00 00", 364 | 0, 0)); 365 | 366 | if (!peb_access) 367 | return false; 368 | 369 | ldrp_call_init_routine_rva = 0; 370 | for (auto i = 1; i < 60; ++i) 371 | // did we find the start of alignment space? 372 | if (hadesmem::Read( 373 | process, reinterpret_cast(peb_access - i)) == 0xCCCCCCCC) 374 | { 375 | ldrp_call_init_routine_rva = 376 | peb_access - i + 4 - reinterpret_cast(ntdll); 377 | break; 378 | } 379 | 380 | if (!ldrp_call_init_routine_rva) 381 | return false; 382 | 383 | auto const call_guard_dispatch = reinterpret_cast(hadesmem::Find( 384 | process, 385 | reinterpret_cast(reinterpret_cast(ntdll) + 386 | ldrp_call_init_routine_rva), 387 | 0x200, L"48 8b ?? e8 ?? ?? ?? 00", 0, 0)); 388 | 389 | if (!call_guard_dispatch) 390 | return false; 391 | 392 | auto const offset = *reinterpret_cast(call_guard_dispatch + 4); 393 | auto const gdi = call_guard_dispatch + 8 + offset; 394 | 395 | if (*reinterpret_cast(gdi) != 0xE9) 396 | return false; 397 | 398 | // compute the offset into LdrpCallInitRoutine of the call to 399 | // guard_dispatch_icall 400 | guard_dispatch_icall_offset = call_guard_dispatch + 3 - 401 | reinterpret_cast(ntdll) - 402 | ldrp_call_init_routine_rva; 403 | 404 | uintptr_t start = 0u; 405 | 406 | do 407 | { 408 | auto current = hadesmem::Find(process, L"ntdll.dll", 409 | L"49 3b c0 74 ?? 45 33 c0 e8 ?? ?? ?? 00 " 410 | L"b8 01 00 00 00 48 83 c4 28 c3 cc", 411 | 0, start); 412 | 413 | if (!current) 414 | return false; 415 | 416 | auto const c = reinterpret_cast(current); 417 | 418 | auto const rel_jump = *reinterpret_cast(c + 9); 419 | auto const func = c + 13 + rel_jump; 420 | 421 | // did we find the right function call? if so, find the start of the 422 | // function and return 423 | if (func == gdi) 424 | { 425 | start = c; 426 | break; 427 | } 428 | 429 | // if not, keep searching 430 | start = c + 0x15; 431 | } while (true); 432 | 433 | for (auto i = 1u; i < 30; ++i) 434 | if (*reinterpret_cast(start - i) == 0xCCCCCCCC) 435 | { 436 | tls_callback_caller_rva = 437 | start - i + 4 - reinterpret_cast(ntdll); 438 | return true; 439 | } 440 | 441 | return false; 442 | } 443 | 444 | void process_log_file(const fs::path& exe_path) 445 | { 446 | auto const parent = exe_path.parent_path(); 447 | auto const log_path = parent / "log.txt"; 448 | 449 | std::ifstream in(log_path, std::ios::ate); 450 | 451 | if (!in) 452 | { 453 | std::cerr << "Failed to read " << log_path << std::endl; 454 | return; 455 | } 456 | 457 | auto const file_size = static_cast(in.tellg()); 458 | in.seekg(std::ios::beg); 459 | 460 | std::vector file_data(file_size + 1); 461 | in.read(&file_data[0], file_data.size()); 462 | file_data[file_data.size() - 1] = '\0'; 463 | in.close(); 464 | 465 | std::cout << "\nLog:\n\n" << &file_data[0]; 466 | 467 | std::remove(log_path.string().c_str()); 468 | } 469 | 470 | BOOL control_handler(DWORD ctrl_type) 471 | { 472 | if (ctrl_type == CTRL_C_EVENT) 473 | { 474 | g_exit_wow = true; 475 | return TRUE; 476 | } 477 | 478 | std::cout << "Received unrecognized event: " << std::dec << ctrl_type 479 | << std::endl; 480 | 481 | return FALSE; 482 | } 483 | -------------------------------------------------------------------------------- /src/raii_proc.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 namreeb http://github.com/namreeb/dumpwow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | 29 | // RAII class to ensure process is killed 30 | class RaiiProc 31 | { 32 | private: 33 | const DWORD _pid; 34 | const UINT _exit_code; 35 | 36 | public: 37 | RaiiProc(DWORD pid, UINT exit_code = 0) : _pid(pid), _exit_code(exit_code) {} 38 | ~RaiiProc() 39 | { 40 | auto const proc = ::OpenProcess(PROCESS_TERMINATE, FALSE, _pid); 41 | 42 | if (!proc) 43 | return; 44 | 45 | ::TerminateProcess(proc, _exit_code); 46 | } 47 | }; --------------------------------------------------------------------------------