Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <llvm/Object/MachO.h>
#include <llvm/Object/MachOUniversal.h>
#include <llvm/Support/MemoryBuffer.h>
#include <llvm/Support/raw_ostream.h>
#include <filesystem>
#include <optional>
#include <random>
#include <set>
#include <string>
#include <string_view>
using namespace llvm;
using namespace object;
namespace fs = std::filesystem;
namespace {
struct CommandLineArgs
{
fs::path binaryPath;
std::set<std::string> excludedClasses;
bool quietMode {false};
bool dryRun {false};
std::string pattern;
std::string replacement;
};
void printUsage(const char *progName)
{
errs() << "Usage: " << progName << " [OPTIONS] <binary_to_patch>\n\n"
<< "A tool to patch Objective-C metadata in Mach-O binaries.\n\n"
<< "Positional arguments:\n"
<< " binary_to_patch The binary file to patch\n\n"
<< "Options:\n"
<< " -h, --help Show this help message and exit\n"
<< " --quiet Suppress output messages\n"
<< " --dry-run Perform a dry run without modifying the file\n"
<< " --exclude CLASS Exclude class name from patching (can be repeated)\n"
<< " --replace PATTERN REPLACEMENT\n"
<< " Replace pattern with replacement string\n"
<< " (must be same length for binary safety)\n\n"
<< "Examples:\n"
<< " " << progName << " myapp.app/Contents/MacOS/myapp\n"
<< " " << progName << " --quiet --exclude MyClass myapp\n"
<< " " << progName << " --replace QtCore QTCore myapp\n";
}
std::optional<CommandLineArgs> parseCommandLine(int argc, char **argv)
{
CommandLineArgs args;
fs::path binaryPath;
if (argc < 2) {
errs() << "Error: No binary file specified.\n\n";
printUsage(argv[0]);
return std::nullopt;
}
int i = 1;
while (i < argc) {
std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
printUsage(argv[0]);
return std::nullopt;
} else if (arg == "--quiet") {
args.quietMode = true;
i++;
} else if (arg == "--dry-run") {
args.dryRun = true;
i++;
} else if (arg == "--exclude") {
if (i + 1 >= argc) {
errs() << "Error: --exclude requires a CLASS argument.\n";
return std::nullopt;
}
args.excludedClasses.insert(argv[i + 1]);
i += 2;
} else if (arg == "--replace") {
if (i + 2 >= argc) {
errs() << "Error: --replace requires PATTERN and REPLACEMENT arguments.\n";
return std::nullopt;
}
args.pattern = argv[i + 1];
args.replacement = argv[i + 2];
if (args.pattern.empty()) {
errs() << "Error: replacement pattern cannot be empty.\n";
return std::nullopt;
}
if (args.pattern.length() != args.replacement.length()) {
errs() << "Error: for binary safety, the replacement pattern and "
<< "the replacement string must be the same length.\n";
return std::nullopt;
}
i += 3;
} else if (arg[0] == '-') {
errs() << "Error: unknown option '" << arg << "'.\n\n";
printUsage(argv[0]);
return std::nullopt;
} else {
if (!binaryPath.empty()) {
errs() << "Error: multiple binary files specified.\n";
return std::nullopt;
}
binaryPath = argv[i];
i++;
}
}
if (binaryPath.empty()) {
errs() << "Error: No binary file specified.\n\n";
printUsage(argv[0]);
return std::nullopt;
}
std::error_code ec;
if (!fs::exists(binaryPath, ec) || ec) {
errs() << "Error: file '" << binaryPath.string() << "' does not exist.\n";
return std::nullopt;
}
args.binaryPath = binaryPath;
return args;
}
std::string generateRandomString(size_t length)
{
constexpr std::string_view charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234"
"56789";
static std::mt19937 generator(std::random_device {}());
std::uniform_int_distribution<int> distribution(0, charset.length() - 1);
std::string result;
for (size_t i = 0; i < length; ++i)
result += charset[distribution(generator)];
return result;
}
// Converts a virtual address to a file offset relative to the start of the Mach-O slice.
std::optional<uint64_t> virtualAddressToFileOffset(const MachOObjectFile *obj, uint64_t va)
{
for (const auto &lci : obj->load_commands()) {
if (lci.C.cmd == MachO::LC_SEGMENT_64) {
const MachO::segment_command_64 seg = obj->getSegment64LoadCommand(lci);
if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
return (va - seg.vmaddr) + seg.fileoff;
} else if (lci.C.cmd == MachO::LC_SEGMENT) {
const MachO::segment_command seg = obj->getSegmentLoadCommand(lci);
if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
return (va - seg.vmaddr) + seg.fileoff;
}
}
return std::nullopt;
}
void patchClassNameSection(const SectionRef §ion,
const MachOObjectFile *machOObj,
WritableMemoryBuffer &writableMB,
uint64_t sliceOffset,
const CommandLineArgs &args)
{
uint64_t sectionFileOffset = 0;
if (machOObj->is64Bit()) {
const MachO::section_64 sec = machOObj->getSection64(section.getRawDataRefImpl());
sectionFileOffset = sec.offset;
} else {
const MachO::section sec = machOObj->getSection(section.getRawDataRefImpl());
sectionFileOffset = sec.offset;
}
Expected<StringRef> contentsOrErr = section.getContents();
if (auto e = contentsOrErr.takeError()) {
consumeError(std::move(e));
return;
}
StringRef contents = *contentsOrErr;
const char *current = contents.begin();
while (current < contents.end()) {
StringRef name(current);
if (name.empty()) {
current++;
continue;
}
if (args.excludedClasses.count(name.str())) {
if (!args.quietMode)
outs() << "[CLASS] Skipping excluded class: " << name.str() << "\n";
current += name.size() + 1;
continue;
}
uint64_t realFileOffset = sliceOffset + sectionFileOffset + (current - contents.begin());
if (!args.pattern.empty()) {
std::string originalName = name.str();
std::string newName = originalName;
size_t pos = 0;
bool replaced = false;
while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
newName.replace(pos, args.pattern.length(), args.replacement);
pos += args.replacement.length();
replaced = true;
}
if (replaced) {
if (!args.quietMode) {
outs() << "[CLASS] Found: " << originalName << " at file offset "
<< realFileOffset << "\n"
<< " -> Replaced with: " << newName << "\n";
}
char *patchLocation = writableMB.getBufferStart() + realFileOffset;
memcpy(patchLocation, newName.c_str(), originalName.length());
}
} else {
if (!args.quietMode)
outs() << "[CLASS] Found: " << name.str() << " at file offset "
<< realFileOffset << "\n";
std::string randomString = generateRandomString(name.size());
char *patchLocation = writableMB.getBufferStart() + realFileOffset;
memcpy(patchLocation, randomString.c_str(), randomString.length());
if (!args.quietMode)
outs() << " -> Replaced with: " << randomString << "\n";
}
current += name.size() + 1;
}
}
void patchCategoryListSection(const SectionRef §ion,
const MachOObjectFile *machOObj,
const MemoryBuffer &originalMB,
WritableMemoryBuffer &writableMB,
uint64_t sliceOffset,
const CommandLineArgs &args)
{
Expected<StringRef> contentsOrErr = section.getContents();
if (auto e = contentsOrErr.takeError()) {
consumeError(std::move(e));
return;
}
StringRef contents = *contentsOrErr;
const char *data = contents.data();
unsigned ptrSize = machOObj->is64Bit() ? 8 : 4;
for (unsigned i = 0; i + ptrSize <= contents.size(); i += ptrSize) {
uint64_t categoryVa = (ptrSize == 8) ? *(const uint64_t *)(data + i)
: *(const uint32_t *)(data + i);
auto categoryOffsetOpt = virtualAddressToFileOffset(machOObj, categoryVa);
if (!categoryOffsetOpt)
continue;
const char *categoryStructPtr
= originalMB.getBufferStart() + sliceOffset + *categoryOffsetOpt;
uint64_t categoryNameVa = (ptrSize == 8) ? *(const uint64_t *)categoryStructPtr
: *(const uint32_t *)categoryStructPtr;
auto nameOffsetOpt = virtualAddressToFileOffset(machOObj, categoryNameVa);
if (!nameOffsetOpt)
continue;
uint64_t realNameOffset = sliceOffset + *nameOffsetOpt;
StringRef categoryName(originalMB.getBufferStart() + realNameOffset);
if (categoryName.empty())
continue;
if (!args.pattern.empty()) {
std::string originalName = categoryName.str();
std::string newName = originalName;
size_t pos = 0;
bool replaced = false;
while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
newName.replace(pos, args.pattern.length(), args.replacement);
pos += args.replacement.length();
replaced = true;
}
if (replaced) {
if (!args.quietMode) {
outs() << "[CATEGORY] Found: " << originalName << " at file offset "
<< realNameOffset << "\n"
<< " -> Replaced with: " << newName << "\n";
}
char *patchLocation = writableMB.getBufferStart() + realNameOffset;
memcpy(patchLocation, newName.c_str(), originalName.length());
}
} else {
if (!args.quietMode)
outs() << "[CATEGORY] Found: " << categoryName.str() << " at file offset "
<< realNameOffset << "\n";
std::string randomString = generateRandomString(categoryName.size());
char *patchLocation = writableMB.getBufferStart() + realNameOffset;
memcpy(patchLocation, randomString.c_str(), randomString.length());
if (!args.quietMode)
outs() << " -> Replaced with: " << randomString << "\n";
}
}
}
Error patchMachOSlice(MachOObjectFile *machOObj,
const MemoryBuffer &originalMB,
WritableMemoryBuffer &writableMB,
uint64_t sliceOffset,
const CommandLineArgs &args)
{
if (!args.quietMode) {
outs() << "--- Patching architecture: " << machOObj->getArchTriple().getArchName()
<< " (slice offset: " << sliceOffset << ") ---\n";
}
for (const SectionRef §ion : machOObj->sections()) {
Expected<StringRef> sectionNameOrErr = section.getName();
if (auto e = sectionNameOrErr.takeError())
return e;
StringRef sectionName = *sectionNameOrErr;
if (sectionName == "__objc_classname")
patchClassNameSection(section, machOObj, writableMB, sliceOffset, args);
else if (sectionName == "__objc_catlist")
patchCategoryListSection(section, machOObj, originalMB, writableMB, sliceOffset, args);
}
return Error::success();
}
} // namespace
int main(int argc, char **argv)
{
auto argsOpt = parseCommandLine(argc, argv);
if (!argsOpt)
return 1;
const auto &args = *argsOpt;
Expected<OwningBinary<Binary>> binOrErr = createBinary(args.binaryPath.string());
if (auto e = binOrErr.takeError()) {
errs() << "Error opening binary: " << toString(std::move(e)) << "\n";
return 1;
}
OwningBinary<Binary> &bin = binOrErr.get();
ErrorOr<std::unique_ptr<MemoryBuffer>> mbOrErr = MemoryBuffer::getFile(args.binaryPath.string());
if (std::error_code ec = mbOrErr.getError()) {
errs() << "Error reading file into buffer: " << ec.message() << "\n";
return 1;
}
std::unique_ptr<MemoryBuffer> originalMB {std::move(mbOrErr.get())};
std::unique_ptr<WritableMemoryBuffer> writableMB
= WritableMemoryBuffer::getNewMemBuffer(originalMB->getBufferSize());
memcpy(writableMB->getBufferStart(), originalMB->getBufferStart(), originalMB->getBufferSize());
if (auto *machOUni = dyn_cast<MachOUniversalBinary>(bin.getBinary())) {
for (const auto &objForArch : machOUni->objects()) {
Expected<std::unique_ptr<MachOObjectFile>> machOObjOrErr = objForArch.getAsObjectFile();
if (auto e = machOObjOrErr.takeError()) {
errs() << "Failed to get object for architecture: " << toString(std::move(e))
<< "\n";
continue;
}
if (auto e = patchMachOSlice(
machOObjOrErr->get(), *originalMB, *writableMB, objForArch.getOffset(), args)) {
errs() << "Failed to patch Mach-O slice: " << toString(std::move(e)) << "\n";
}
}
} else if (auto *machOObj = dyn_cast<MachOObjectFile>(bin.getBinary())) {
if (auto e = patchMachOSlice(machOObj, *originalMB, *writableMB, 0, args)) {
errs() << "Failed to patch Mach-O file: " << toString(std::move(e)) << "\n";
return 1;
}
} else {
errs() << "The provided file is not a valid Mach-O binary.\n";
return 1;
}
if (args.dryRun) {
if (!args.quietMode)
outs() << "\nDry run complete. Binary was not modified.\n";
return 0;
}
std::error_code ec;
raw_fd_ostream outFile(args.binaryPath.string(), ec);
if (ec) {
errs() << "Error opening file for writing: " << ec.message() << "\n";
return 1;
}
outFile.write(writableMB->getBufferStart(), writableMB->getBufferSize());
outFile.close();
if (!args.quietMode)
outs() << "\nSuccessfully patched binary in-place: " << args.binaryPath.string() << "\n";
return 0;
}