Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GD Hax/GD Hax.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
Expand Down
26 changes: 23 additions & 3 deletions GD Hax/GDExploits.cpp
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
#include "GDExploits.hpp"

// completely revamped stat system (again), a lot of the struct info and names are guessed
// Address: GeometryDash.exe+0x1D21E0 (Windows GD Version 2.207)
stat_edits::StatLinkedList* stat_edits::get_stat_addr(driver& game, StatInfo* instance, StatType stat) {
const uint64_t list_length = game.read<uint64_t>(&instance->list_length);
if (list_length == 0 || list_length > 100000)
return nullptr;

uint64_t hash = hash_number(game, instance, reinterpret_cast<uint8_t*>(&stat));
StatLinkedList** stat_table = game.read<StatLinkedList**>(&instance->stat_table);
if (stat_table == nullptr)
return nullptr;

StatLinkedList* cur = game.read<StatLinkedList*>(&stat_table[hash + 1]);
if (cur == nullptr)
return nullptr;

void* end = game.read<void*>(&instance->list_end);
if (end == nullptr)
return nullptr;

if (cur == end)
return nullptr;

StatLinkedList* v12 = game.read<StatLinkedList*>(&stat_table[hash]);
if (v12 == nullptr)
return nullptr;

if (stat != game.read<StatType>(&cur->stat)) {
while (cur != v12) {
const uint64_t max_iters = list_length + 2;
for (uint64_t i = 0; i < max_iters && cur != v12; ++i) {
cur = game.read<StatLinkedList*>(&cur->next);
if (cur == nullptr || cur == end)
return nullptr;

if (stat == game.read<StatType>(&cur->stat))
break;
}

if (stat != game.read<StatType>(&cur->stat))
return nullptr;
}

return cur;
Expand Down
4 changes: 3 additions & 1 deletion GD Hax/Macros.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#pragma once
#include <iostream>

#define STR_MERGE_IMPL(a, b) a ## b
#define STR_MERGE(a, b) STR_MERGE_IMPL(a,b)
#define STRUCT_PAD(size) unsigned char STR_MERGE(__pad_, __COUNTER__)[size]

#define PAUSE() std::cin.ignore()
#define PAUSE_EXIT() { std::print( "Press any key to exit..." ); PAUSE(); }
#define PAUSE_EXIT() { std::cout << "Press any key to exit..."; PAUSE(); }

#define CLEAR_SCREEN "\033[2J\033[H"
168 changes: 145 additions & 23 deletions GD Hax/main.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include <iostream>
#include <string>
#include <stdexcept>
#include <print>
#include <vector>
#include <algorithm>
#include <iomanip>
#include <climits>
#include <cctype>

#include "memory.hpp"
#include "GDExploits.hpp"
Expand All @@ -18,15 +22,123 @@ static void init_console()

inline void print_stat_row(int index, const char* stat)
{
std::print(" {:2}) {:<25}", index, stat);
std::cout << " " << std::setw(2) << index << ") " << std::left << std::setw(25) << stat << std::right;
}

static bool possible_user_ptr(uintptr_t address)
{
return address >= 0x10000 && address < 0x800000000000ull;
}

static std::vector<MemRange> get_scan_ranges(driver& game)
{
std::vector<MemRange> ranges;
if (game.get_module_size() == 0)
return ranges;

IMAGE_DOS_HEADER dos = game.read<IMAGE_DOS_HEADER>(game.base);
if (dos.e_magic != IMAGE_DOS_SIGNATURE)
{
ranges.push_back({ game.base, game.base + game.get_module_size() });
return ranges;
}

const uintptr_t nt_addr = game.base + static_cast<uintptr_t>(dos.e_lfanew);
IMAGE_NT_HEADERS64 nt = game.read<IMAGE_NT_HEADERS64>(nt_addr);
if (nt.Signature != IMAGE_NT_SIGNATURE || nt.FileHeader.NumberOfSections == 0)
{
ranges.push_back({ game.base, game.base + game.get_module_size() });
return ranges;
}

uintptr_t section_header_addr = nt_addr + sizeof(uint32_t) + sizeof(IMAGE_FILE_HEADER) + nt.FileHeader.SizeOfOptionalHeader;
const WORD section_count = nt.FileHeader.NumberOfSections;

for (WORD i = 0; i < section_count; ++i)
{
IMAGE_SECTION_HEADER section = game.read<IMAGE_SECTION_HEADER>(section_header_addr + i * sizeof(IMAGE_SECTION_HEADER));

const bool is_initialized_data = (section.Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) != 0;
const bool is_writable = (section.Characteristics & IMAGE_SCN_MEM_WRITE) != 0;
if (!is_initialized_data || !is_writable)
continue;

const uintptr_t section_start = game.base + section.VirtualAddress;
const uintptr_t section_size = std::max<uintptr_t>(section.Misc.VirtualSize, section.SizeOfRawData);
if (section_size == 0)
continue;

ranges.push_back({ section_start, section_start + section_size });
}

if (ranges.empty())
ranges.push_back({ game.base, game.base + game.get_module_size() });

return ranges;
}

static bool validate_stat_info(driver& game, uintptr_t stat_info_addr)
{
auto stat_info = reinterpret_cast<stat_edits::StatInfo*>(stat_info_addr);
uintptr_t list_end = game.read<uintptr_t>(&stat_info->list_end);
uintptr_t stat_table = game.read<uintptr_t>(&stat_info->stat_table);
uint64_t list_length = game.read<uint64_t>(&stat_info->list_length);
uint64_t hash_mask = game.read<uint64_t>(&stat_info->hash_mask);

if (!possible_user_ptr(list_end) || !possible_user_ptr(stat_table))
return false;

if (list_length == 0 || list_length > 100000)
return false;

if (hash_mask == 0 || hash_mask > 0xFFFFF || (hash_mask & (hash_mask + 1)) != 0)
return false;

auto jumps = stat_edits::get_stat_addr(game, stat_info, stat_edits::StatType::JUMPS);
if (jumps == nullptr || game.read<stat_edits::StatType>(&jumps->stat) != stat_edits::StatType::JUMPS)
return false;

auto attempts = stat_edits::get_stat_addr(game, stat_info, stat_edits::StatType::ATTEMPTS);
if (attempts == nullptr || game.read<stat_edits::StatType>(&attempts->stat) != stat_edits::StatType::ATTEMPTS)
return false;

return true;
}

static bool detect_stats_instance_offset(driver& game, uintptr_t& out_offset)
{
const auto ranges = get_scan_ranges(game);

for (const auto& range : ranges)
{
uintptr_t start = (range.start + sizeof(uintptr_t) - 1) & ~(sizeof(uintptr_t) - 1);

for (uintptr_t address = start; address + sizeof(uintptr_t) <= range.end; address += sizeof(uintptr_t))
{
const uintptr_t instance = game.read<uintptr_t>(address);
if (!possible_user_ptr(instance))
continue;

if (!validate_stat_info(game, instance + 0x248))
continue;

if (!validate_stat_info(game, instance + 0x288))
continue;

out_offset = address - game.base;
return true;
}
}

return false;
}

int read_int(const std::string& prompt)
{
int value;
while (true)
{
std::print("{}", prompt);
std::cout << prompt << std::flush;
std::string input;
std::getline(std::cin, input);

Expand All @@ -37,11 +149,11 @@ int read_int(const std::string& prompt)
}
catch (const std::invalid_argument&)
{
std::println("Invalid input. Please enter an integer.");
std::cout << "Invalid input. Please enter an integer.\n";
}
catch (const std::out_of_range&)
{
std::println("Number is out of range. Please enter a valid integer.");
std::cout << "Number is out of range. Please enter a valid integer.\n";
}
}
}
Expand All @@ -51,10 +163,10 @@ stat_edits::StatType try_parse_input()
std::string input;
while (true)
{
std::print("\nInput: ");
std::cout << "\nInput: " << std::flush;
std::getline(std::cin, input);

if (input.size() == 1 && tolower(input[0]) == 'q')
if (input.size() == 1 && std::tolower(static_cast<unsigned char>(input[0])) == 'q')
{
return stat_edits::StatType::NONE;
}
Expand All @@ -71,20 +183,20 @@ stat_edits::StatType try_parse_input()
if (stat_edits::StatType::NONE < result && result <= stat_edits::StatType::TOTAL_COUNT)
return result;

std::println(CLEAR_SCREEN "Invalid input. Input a value that corresponds to a stat.");
std::cout << CLEAR_SCREEN "Invalid input. Input a value that corresponds to a stat.\n";
}
else
{
std::println(CLEAR_SCREEN "Invalid input. Please enter only an integer.");
std::cout << CLEAR_SCREEN "Invalid input. Please enter only an integer.\n";
}
}
catch (const std::invalid_argument&)
{
std::println(CLEAR_SCREEN "Invalid input. Please enter an integer.");
std::cout << CLEAR_SCREEN "Invalid input. Please enter an integer.\n";
}
catch (const std::out_of_range&)
{
std::println(CLEAR_SCREEN "Number is out of range. Please enter a valid integer.");
std::cout << CLEAR_SCREEN "Number is out of range. Please enter a valid integer.\n";
}

return stat_edits::StatType::INVALID;
Expand All @@ -98,51 +210,61 @@ int main()
auto game = driver(L"GeometryDash.exe");
if (!game.is_attached())
{
std::println("Failed to find game process!");
std::cout << "Failed to find game process!\n";
PAUSE_EXIT();
return EXIT_FAILURE;
}

uintptr_t stats_instance_offset = 0;
if (!detect_stats_instance_offset(game, stats_instance_offset))
{
std::cout << "Failed to auto-detect the stats offset for this game version!\n";
PAUSE_EXIT();
return EXIT_FAILURE;
}

std::cout << "Detected stats offset: 0x" << std::hex << stats_instance_offset << std::dec << "\n";

while (true)
{
stat_edits::StatType input = stat_edits::StatType::INVALID;
while (input < 0 || total_options < input)
{
std::println("Input 'q' to exit\nSelect a stat to modify [1 - {}]:", total_options);
std::cout << "Input 'q' to exit\nSelect a stat to modify [1 - " << total_options << "]:" << "\n";

for (int i = 1; i <= total_options; ++i)
{
print_stat_row(i, stat_edits::stat_types[i - 1]);

if (i % 2 == 0)
std::print("\n");
std::cout << "\n";
}

input = try_parse_input();
if (input == stat_edits::StatType::NONE)
{
std::println("Exiting...");
std::cout << "Exiting...\n";
return EXIT_SUCCESS;
}
}

std::println("\nInput value to set for stat '{}'!", stat_edits::stat_types[input - 1]);
int value = read_int(std::format("Value [{} - {}]: ", INT_MIN, INT_MAX));
std::println("Setting '{}' value to {}!", stat_edits::stat_types[input - 1], value);
std::cout << "\nInput value to set for stat '" << stat_edits::stat_types[input - 1] << "'!\n";
int value = read_int("Value [" + std::to_string(INT_MIN) + " - " + std::to_string(INT_MAX) + "]: ");
std::cout << "Setting '" << stat_edits::stat_types[input - 1] << "' value to " << value << "!\n";

// since those random achievements aren't in the list anymore, adjust to fix gaunlets and list rewards since they come after
if (input >= 30)
input = stat_edits::StatType(input + 10);

auto stats_instance = game.read<uintptr_t>(game.base + 0x6A4E78);
auto stats_instance = game.read<uintptr_t>(game.base + stats_instance_offset);

auto stat_info = reinterpret_cast<stat_edits::StatInfo*>(stats_instance + 0x248);
auto stat_info_delta = reinterpret_cast<stat_edits::StatInfo*>(stats_instance + 0x288);

stat_edits::StatLinkedList* stat_info_delta_addr = stat_edits::get_stat_addr(game, stat_info_delta, input);
if (stat_info_delta_addr == nullptr)
{
std::println("Failed to get the delta address for the specified stat type!");
std::cout << "Failed to get the delta address for the specified stat type!\n";
PAUSE_EXIT();
return EXIT_FAILURE;
}
Expand All @@ -152,16 +274,16 @@ int main()
stat_edits::StatLinkedList* stat_info_addr = stat_edits::get_stat_addr(game, stat_info, input);
if (stat_info_addr == nullptr)
{
std::println("Failed to get the stat value address for the specified stat type!");
std::cout << "Failed to get the stat value address for the specified stat type!\n";
PAUSE_EXIT();
return EXIT_FAILURE;
}

game.write<uint32_t>(&stat_info_addr->value, value + delta);

std::println("Success! Press any key to continue!");
std::cout << "Success! Press any key to continue!\n";
PAUSE();
std::print(CLEAR_SCREEN);
std::cout << CLEAR_SCREEN;
}

return EXIT_SUCCESS;
Expand Down
Loading