Seregon/ShadPKG

A tool for deriving PKG packet encryption keys for ps4 written in c++

C++/47.3 KB/No license
common/memory_patcher.cpp
ShadPKG / common / memory_patcher.cpp
1// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3 
4#include <algorithm>
5#include <codecvt>
6#include <sstream>
7#include <string>
8#include <pugixml.hpp>
9#ifdef ENABLE_QT_GUI
10#include <QDir>
11#include <QFile>
12#include <QJsonArray>
13#include <QJsonDocument>
14#include <QJsonObject>
15#include <QListView>
16#include <QMessageBox>
17#include <QString>
18#include <QXmlStreamReader>
19#endif
20#include "common/logging/log.h"
21#include "common/path_util.h"
22#include "memory_patcher.h"
23 
24namespace MemoryPatcher {
25 
26uintptr_t g_eboot_address;
27uint64_t g_eboot_image_size;
28std::string g_game_serial;
29std::string patchFile;
30std::vector<patchInfo> pending_patches;
31 
32std::string toHex(u64 value, size_t byteSize) {
33 std::stringstream ss;
34 ss << std::hex << std::setfill('0') << std::setw(byteSize * 2) << value;
35 return ss.str();
36}
37 
38std::string convertValueToHex(const std::string type, const std::string valueStr) {
39 std::string result;
40 
41 if (type == "byte") {
42 const u32 value = std::stoul(valueStr, nullptr, 16);
43 result = toHex(value, 1);
44 } else if (type == "bytes16") {
45 const u32 value = std::stoul(valueStr, nullptr, 16);
46 result = toHex(value, 2);
47 } else if (type == "bytes32") {
48 const u32 value = std::stoul(valueStr, nullptr, 16);
49 result = toHex(value, 4);
50 } else if (type == "bytes64") {
51 const u64 value = std::stoull(valueStr, nullptr, 16);
52 result = toHex(value, 8);
53 } else if (type == "float32") {
54 union {
55 float f;
56 uint32_t i;
57 } floatUnion;
58 floatUnion.f = std::stof(valueStr);
59 result = toHex(floatUnion.i, sizeof(floatUnion.i));
60 } else if (type == "float64") {
61 union {
62 double d;
63 uint64_t i;
64 } doubleUnion;
65 doubleUnion.d = std::stod(valueStr);
66 result = toHex(doubleUnion.i, sizeof(doubleUnion.i));
67 } else if (type == "utf8") {
68 std::vector<unsigned char> byteArray =
69 std::vector<unsigned char>(valueStr.begin(), valueStr.end());
70 byteArray.push_back('\0');
71 std::stringstream ss;
72 for (unsigned char c : byteArray) {
73 ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(c);
74 }
75 result = ss.str();
76 } else if (type == "utf16") {
77 std::wstring wide_str(valueStr.size(), L'\0');
78 std::mbstowcs(&wide_str[0], valueStr.c_str(), valueStr.size());
79 wide_str.resize(std::wcslen(wide_str.c_str()));
80 
81 std::u16string valueStringU16;
82 
83 for (wchar_t wc : wide_str) {
84 if (wc <= 0xFFFF) {
85 valueStringU16.push_back(static_cast<char16_t>(wc));
86 } else {
87 wc -= 0x10000;
88 valueStringU16.push_back(static_cast<char16_t>(0xD800 | (wc >> 10)));
89 valueStringU16.push_back(static_cast<char16_t>(0xDC00 | (wc & 0x3FF)));
90 }
91 }
92 
93 std::vector<unsigned char> byteArray;
94 // convert to little endian
95 for (char16_t ch : valueStringU16) {
96 unsigned char low_byte = static_cast<unsigned char>(ch & 0x00FF);
97 unsigned char high_byte = static_cast<unsigned char>((ch >> 8) & 0x00FF);
98 
99 byteArray.push_back(low_byte);
100 byteArray.push_back(high_byte);
101 }
102 byteArray.push_back('\0');
103 byteArray.push_back('\0');
104 std::stringstream ss;
105 
106 for (unsigned char ch : byteArray) {
107 ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
108 }
109 result = ss.str();
110 } else if (type == "bytes") {
111 result = valueStr;
112 } else if (type == "mask" || type == "mask_jump32") {
113 result = valueStr;
114 } else {
115 LOG_INFO(Loader, "Error applying Patch, unknown type: {}", type);
116 }
117 return result;
118}
119 
120void OnGameLoaded() {
121 
122 if (!patchFile.empty()) {
123 std::filesystem::path patchDir = Common::FS::GetUserPath(Common::FS::PathType::PatchesDir);
124 
125 auto filePath = (patchDir / patchFile).native();
126 
127 pugi::xml_document doc;
128 pugi::xml_parse_result result = doc.load_file(filePath.c_str());
129 
130 if (result) {
131 auto patchXML = doc.child("Patch");
132 for (pugi::xml_node_iterator it = patchXML.children().begin();
133 it != patchXML.children().end(); ++it) {
134 
135 if (std::string(it->name()) == "Metadata") {
136 if (std::string(it->attribute("isEnabled").value()) == "true") {
137 auto patchList = it->first_child();
138 
139 std::string currentPatchName = it->attribute("Name").value();
140 
141 for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
142 patchLineIt != patchList.children().end(); ++patchLineIt) {
143 
144 std::string type = patchLineIt->attribute("Type").value();
145 std::string address = patchLineIt->attribute("Address").value();
146 std::string patchValue = patchLineIt->attribute("Value").value();
147 std::string maskOffsetStr = patchLineIt->attribute("type").value();
148 
149 patchValue = convertValueToHex(type, patchValue);
150 
151 bool littleEndian = false;
152 
153 if (type == "bytes16") {
154 littleEndian = true;
155 } else if (type == "bytes32") {
156 littleEndian = true;
157 } else if (type == "bytes64") {
158 littleEndian = true;
159 }
160 
161 MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
162 int maskOffsetValue = 0;
163 
164 if (type == "mask") {
165 patchMask = MemoryPatcher::PatchMask::Mask;
166 
167 // im not sure if this works, there is no games to test the mask
168 // offset on yet
169 if (!maskOffsetStr.empty())
170 maskOffsetValue = std::stoi(maskOffsetStr, 0, 10);
171 }
172 
173 if (type == "mask_jump32")
174 patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
175 
176 MemoryPatcher::PatchMemory(currentPatchName, address, patchValue, false,
177 littleEndian, patchMask);
178 }
179 }
180 }
181 }
182 } else
183 LOG_ERROR(Loader, "couldnt patch parse xml : {}", result.description());
184 
185 ApplyPendingPatches();
186 return;
187 }
188 
189#ifdef ENABLE_QT_GUI
190 // We use the QT headers for the xml and json parsing, this define is only true on QT builds
191 QString patchDir;
192 Common::FS::PathToQString(patchDir, Common::FS::GetUserPath(Common::FS::PathType::PatchesDir));
193 QDir dir(patchDir);
194 QStringList folders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
195 
196 for (const QString& folder : folders) {
197 QString filesJsonPath = patchDir + "/" + folder + "/files.json";
198 
199 QFile jsonFile(filesJsonPath);
200 if (!jsonFile.open(QIODevice::ReadOnly)) {
201 LOG_ERROR(Loader, "Unable to open files.json for reading in repository {}",
202 folder.toStdString());
203 continue;
204 }
205 
206 QByteArray jsonData = jsonFile.readAll();
207 jsonFile.close();
208 
209 QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
210 QJsonObject jsonObject = jsonDoc.object();
211 
212 QString selectedFileName;
213 QString serial = QString::fromStdString(g_game_serial);
214 
215 for (auto it = jsonObject.constBegin(); it != jsonObject.constEnd(); ++it) {
216 QString filePath = it.key();
217 QJsonArray idsArray = it.value().toArray();
218 
219 if (idsArray.contains(QJsonValue(serial))) {
220 selectedFileName = filePath;
221 break;
222 }
223 }
224 
225 if (selectedFileName.isEmpty()) {
226 LOG_ERROR(Loader, "No patch file found for the current serial in repository {}",
227 folder.toStdString());
228 continue;
229 }
230 
231 QString filePath = patchDir + "/" + folder + "/" + selectedFileName;
232 QFile file(filePath);
233 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
234 LOG_ERROR(Loader, "Unable to open the file for reading.");
235 continue;
236 }
237 
238 QByteArray xmlData = file.readAll();
239 file.close();
240 
241 QString newXmlData;
242 
243 QXmlStreamReader xmlReader(xmlData);
244 bool insideMetadata = false;
245 
246 bool isEnabled = false;
247 std::string currentPatchName;
248 while (!xmlReader.atEnd()) {
249 xmlReader.readNext();
250 
251 if (xmlReader.isStartElement()) {
252 QJsonArray patchLines;
253 if (xmlReader.name() == QStringLiteral("Metadata")) {
254 insideMetadata = true;
255 
256 QString name = xmlReader.attributes().value("Name").toString();
257 currentPatchName = name.toStdString();
258 
259 // Check and update the isEnabled attribute
260 for (const QXmlStreamAttribute& attr : xmlReader.attributes()) {
261 if (attr.name() == QStringLiteral("isEnabled")) {
262 if (attr.value().toString() == "true") {
263 isEnabled = true;
264 } else
265 isEnabled = false;
266 }
267 }
268 } else if (xmlReader.name() == QStringLiteral("PatchList")) {
269 QJsonArray linesArray;
270 while (!xmlReader.atEnd() &&
271 !(xmlReader.tokenType() == QXmlStreamReader::EndElement &&
272 xmlReader.name() == QStringLiteral("PatchList"))) {
273 xmlReader.readNext();
274 if (xmlReader.tokenType() == QXmlStreamReader::StartElement &&
275 xmlReader.name() == QStringLiteral("Line")) {
276 QXmlStreamAttributes attributes = xmlReader.attributes();
277 QJsonObject lineObject;
278 lineObject["Type"] = attributes.value("Type").toString();
279 lineObject["Address"] = attributes.value("Address").toString();
280 lineObject["Value"] = attributes.value("Value").toString();
281 linesArray.append(lineObject);
282 }
283 }
284 
285 patchLines = linesArray;
286 if (isEnabled) {
287 foreach (const QJsonValue& value, patchLines) {
288 QJsonObject lineObject = value.toObject();
289 QString type = lineObject["Type"].toString();
290 QString address = lineObject["Address"].toString();
291 QString patchValue = lineObject["Value"].toString();
292 QString maskOffsetStr = lineObject["Offset"].toString();
293 
294 patchValue = QString::fromStdString(
295 convertValueToHex(type.toStdString(), patchValue.toStdString()));
296 
297 bool littleEndian = false;
298 
299 if (type == "bytes16") {
300 littleEndian = true;
301 } else if (type == "bytes32") {
302 littleEndian = true;
303 } else if (type == "bytes64") {
304 littleEndian = true;
305 }
306 
307 MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
308 int maskOffsetValue = 0;
309 
310 if (type == "mask") {
311 patchMask = MemoryPatcher::PatchMask::Mask;
312 
313 // im not sure if this works, there is no games to test the mask
314 // offset on yet
315 if (!maskOffsetStr.toStdString().empty())
316 maskOffsetValue = std::stoi(maskOffsetStr.toStdString(), 0, 10);
317 }
318 
319 if (type == "mask_jump32")
320 patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
321 
322 MemoryPatcher::PatchMemory(currentPatchName, address.toStdString(),
323 patchValue.toStdString(), false,
324 littleEndian, patchMask);
325 }
326 }
327 }
328 }
329 }
330 
331 if (xmlReader.hasError()) {
332 LOG_ERROR(Loader, "Failed to parse XML for {}", g_game_serial);
333 } else {
334 LOG_INFO(Loader, "Patches loaded successfully");
335 }
336 ApplyPendingPatches();
337 }
338#endif
339}
340 
341void AddPatchToQueue(patchInfo patchToAdd) {
342 pending_patches.push_back(patchToAdd);
343}
344 
345void ApplyPendingPatches() {
346 
347 for (size_t i = 0; i < pending_patches.size(); ++i) {
348 patchInfo currentPatch = pending_patches[i];
349 
350 if (currentPatch.gameSerial != g_game_serial)
351 continue;
352 
353 PatchMemory(currentPatch.modNameStr, currentPatch.offsetStr, currentPatch.valueStr,
354 currentPatch.isOffset, currentPatch.littleEndian, currentPatch.patchMask,
355 currentPatch.maskOffset);
356 }
357 
358 pending_patches.clear();
359}
360 
361void PatchMemory(std::string modNameStr, std::string offsetStr, std::string valueStr, bool isOffset,
362 bool littleEndian, PatchMask patchMask, int maskOffset) {
363 // Send a request to modify the process memory.
364 void* cheatAddress = nullptr;
365 
366 if (patchMask == PatchMask::None) {
367 if (isOffset) {
368 cheatAddress = reinterpret_cast<void*>(g_eboot_address + std::stoi(offsetStr, 0, 16));
369 } else {
370 cheatAddress =
371 reinterpret_cast<void*>(g_eboot_address + (std::stoi(offsetStr, 0, 16) - 0x400000));
372 }
373 }
374 
375 if (patchMask == PatchMask::Mask) {
376 cheatAddress = reinterpret_cast<void*>(PatternScan(offsetStr) + maskOffset);
377 }
378 
379 // TODO: implement mask_jump32
380 
381 if (cheatAddress == nullptr) {
382 LOG_ERROR(Loader, "Failed to get address for patch {}", modNameStr);
383 return;
384 }
385 
386 std::vector<unsigned char> bytePatch;
387 
388 for (size_t i = 0; i < valueStr.length(); i += 2) {
389 unsigned char byte =
390 static_cast<unsigned char>(std::strtol(valueStr.substr(i, 2).c_str(), nullptr, 16));
391 
392 bytePatch.push_back(byte);
393 }
394 
395 if (littleEndian) {
396 std::reverse(bytePatch.begin(), bytePatch.end());
397 }
398 
399 std::memcpy(cheatAddress, bytePatch.data(), bytePatch.size());
400 
401 LOG_INFO(Loader, "Applied patch: {}, Offset: {}, Value: {}", modNameStr,
402 (uintptr_t)cheatAddress, valueStr);
403}
404 
405static std::vector<int32_t> PatternToByte(const std::string& pattern) {
406 std::vector<int32_t> bytes;
407 const char* start = pattern.data();
408 const char* end = start + pattern.size();
409 
410 for (const char* current = start; current < end; ++current) {
411 if (*current == '?') {
412 ++current;
413 if (*current == '?')
414 ++current;
415 bytes.push_back(-1);
416 } else {
417 bytes.push_back(strtoul(current, const_cast<char**>(&current), 16));
418 }
419 }
420 
421 return bytes;
422}
423 
424uintptr_t PatternScan(const std::string& signature) {
425 std::vector<int32_t> patternBytes = PatternToByte(signature);
426 const auto scanBytes = static_cast<uint8_t*>((void*)g_eboot_address);
427 
428 const int32_t* sigPtr = patternBytes.data();
429 const size_t sigSize = patternBytes.size();
430 
431 uint32_t foundResults = 0;
432 for (uint32_t i = 0; i < g_eboot_image_size - sigSize; ++i) {
433 bool found = true;
434 for (uint32_t j = 0; j < sigSize; ++j) {
435 if (scanBytes[i + j] != sigPtr[j] && sigPtr[j] != -1) {
436 found = false;
437 break;
438 }
439 }
440 
441 if (found) {
442 foundResults++;
443 return reinterpret_cast<uintptr_t>(&scanBytes[i]);
444 }
445 }
446 
447 return 0;
448}
449 
450} // namespace MemoryPatcher