4#include <llvm/Object/MachO.h>
5#include <llvm/Object/MachOUniversal.h>
6#include <llvm/Support/MemoryBuffer.h>
7#include <llvm/Support/raw_ostream.h>
17using namespace object;
19namespace fs =
std::filesystem;
26 std::set<std::string> excludedClasses;
27 bool quietMode {
false};
30 std::string replacement;
33void printUsage(
const char *progName)
35 errs() <<
"Usage: " << progName <<
" [OPTIONS] <binary_to_patch>\n\n"
36 <<
"A tool to patch Objective-C metadata in Mach-O binaries.\n\n"
37 <<
"Positional arguments:\n"
38 <<
" binary_to_patch The binary file to patch\n\n"
40 <<
" -h, --help Show this help message and exit\n"
41 <<
" --quiet Suppress output messages\n"
42 <<
" --dry-run Perform a dry run without modifying the file\n"
43 <<
" --exclude CLASS Exclude class name from patching (can be repeated)\n"
44 <<
" --replace PATTERN REPLACEMENT\n"
45 <<
" Replace pattern with replacement string\n"
46 <<
" (must be same length for binary safety)\n\n"
48 <<
" " << progName <<
" myapp.app/Contents/MacOS/myapp\n"
49 <<
" " << progName <<
" --quiet --exclude MyClass myapp\n"
50 <<
" " << progName <<
" --replace QtCore QTCore myapp\n";
53std::optional<CommandLineArgs> parseCommandLine(
int argc,
char **argv)
59 errs() <<
"Error: No binary file specified.\n\n";
66 std::string arg = argv[i];
68 if (arg ==
"-h" || arg ==
"--help") {
71 }
else if (arg ==
"--quiet") {
72 args.quietMode =
true;
74 }
else if (arg ==
"--dry-run") {
77 }
else if (arg ==
"--exclude") {
79 errs() <<
"Error: --exclude requires a CLASS argument.\n";
82 args.excludedClasses.insert(argv[i + 1]);
84 }
else if (arg ==
"--replace") {
86 errs() <<
"Error: --replace requires PATTERN and REPLACEMENT arguments.\n";
89 args.pattern = argv[i + 1];
90 args.replacement = argv[i + 2];
92 if (args.pattern.empty()) {
93 errs() <<
"Error: replacement pattern cannot be empty.\n";
96 if (args.pattern.length() != args.replacement.length()) {
97 errs() <<
"Error: for binary safety, the replacement pattern and "
98 <<
"the replacement string must be the same length.\n";
102 }
else if (arg[0] ==
'-') {
103 errs() <<
"Error: unknown option '" << arg <<
"'.\n\n";
107 if (!binaryPath.empty()) {
108 errs() <<
"Error: multiple binary files specified.\n";
111 binaryPath = argv[i];
116 if (binaryPath.empty()) {
117 errs() <<
"Error: No binary file specified.\n\n";
123 if (!fs::exists(binaryPath, ec) || ec) {
124 errs() <<
"Error: file '" << binaryPath.string() <<
"' does not exist.\n";
128 args.binaryPath = binaryPath;
132std::string generateRandomString(size_t length)
134 constexpr std::string_view charset =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234"
136 static std::mt19937 generator(
std::random_device {}());
137 std::uniform_int_distribution<
int> distribution(0, charset.length() - 1);
139 for (size_t i = 0; i < length; ++i)
140 result += charset[distribution(generator)];
145std::optional<uint64_t> virtualAddressToFileOffset(
const MachOObjectFile *obj, uint64_t va)
147 for (
const auto &lci : obj->load_commands()) {
148 if (lci.C.cmd == MachO::LC_SEGMENT_64) {
149 const MachO::segment_command_64 seg = obj->getSegment64LoadCommand(lci);
150 if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
151 return (va - seg.vmaddr) + seg.fileoff;
152 }
else if (lci.C.cmd == MachO::LC_SEGMENT) {
153 const MachO::segment_command seg = obj->getSegmentLoadCommand(lci);
154 if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
155 return (va - seg.vmaddr) + seg.fileoff;
161void patchClassNameSection(
const SectionRef §ion,
162 const MachOObjectFile *machOObj,
163 WritableMemoryBuffer &writableMB,
164 uint64_t sliceOffset,
165 const CommandLineArgs &args)
167 uint64_t sectionFileOffset = 0;
168 if (machOObj->is64Bit()) {
169 const MachO::section_64 sec = machOObj->getSection64(section.getRawDataRefImpl());
170 sectionFileOffset = sec.offset;
172 const MachO::section sec = machOObj->getSection(section.getRawDataRefImpl());
173 sectionFileOffset = sec.offset;
176 Expected<StringRef> contentsOrErr = section.getContents();
177 if (
auto e = contentsOrErr.takeError()) {
178 consumeError(
std::move(e));
181 StringRef contents = *contentsOrErr;
182 const char *current = contents.begin();
183 while (current < contents.end()) {
184 StringRef name(current);
190 if (args.excludedClasses.count(name.str())) {
192 outs() <<
"[CLASS] Skipping excluded class: " << name.str() <<
"\n";
193 current += name.size() + 1;
197 uint64_t realFileOffset = sliceOffset + sectionFileOffset + (current - contents.begin());
199 if (!args.pattern.empty()) {
200 std::string originalName = name.str();
201 std::string newName = originalName;
203 bool replaced =
false;
204 while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
205 newName.replace(pos, args.pattern.length(), args.replacement);
206 pos += args.replacement.length();
211 if (!args.quietMode) {
212 outs() <<
"[CLASS] Found: " << originalName <<
" at file offset "
213 << realFileOffset <<
"\n"
214 <<
" -> Replaced with: " << newName <<
"\n";
216 char *patchLocation = writableMB.getBufferStart() + realFileOffset;
217 memcpy(patchLocation, newName.c_str(), originalName.length());
221 outs() <<
"[CLASS] Found: " << name.str() <<
" at file offset "
222 << realFileOffset <<
"\n";
224 std::string randomString = generateRandomString(name.size());
225 char *patchLocation = writableMB.getBufferStart() + realFileOffset;
226 memcpy(patchLocation, randomString.c_str(), randomString.length());
228 outs() <<
" -> Replaced with: " << randomString <<
"\n";
231 current += name.size() + 1;
235void patchCategoryListSection(
const SectionRef §ion,
236 const MachOObjectFile *machOObj,
237 const MemoryBuffer &originalMB,
238 WritableMemoryBuffer &writableMB,
239 uint64_t sliceOffset,
240 const CommandLineArgs &args)
242 Expected<StringRef> contentsOrErr = section.getContents();
243 if (
auto e = contentsOrErr.takeError()) {
244 consumeError(
std::move(e));
247 StringRef contents = *contentsOrErr;
248 const char *data = contents.data();
249 unsigned ptrSize = machOObj->is64Bit() ? 8 : 4;
251 for (
unsigned i = 0; i + ptrSize <= contents.size(); i += ptrSize) {
252 uint64_t categoryVa = (ptrSize == 8) ? *(
const uint64_t *)(data + i)
253 : *(
const uint32_t *)(data + i);
254 auto categoryOffsetOpt = virtualAddressToFileOffset(machOObj, categoryVa);
255 if (!categoryOffsetOpt)
258 const char *categoryStructPtr
259 = originalMB.getBufferStart() + sliceOffset + *categoryOffsetOpt;
260 uint64_t categoryNameVa = (ptrSize == 8) ? *(
const uint64_t *)categoryStructPtr
261 : *(
const uint32_t *)categoryStructPtr;
263 auto nameOffsetOpt = virtualAddressToFileOffset(machOObj, categoryNameVa);
267 uint64_t realNameOffset = sliceOffset + *nameOffsetOpt;
268 StringRef categoryName(originalMB.getBufferStart() + realNameOffset);
269 if (categoryName.empty())
272 if (!args.pattern.empty()) {
273 std::string originalName = categoryName.str();
274 std::string newName = originalName;
276 bool replaced =
false;
277 while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
278 newName.replace(pos, args.pattern.length(), args.replacement);
279 pos += args.replacement.length();
284 if (!args.quietMode) {
285 outs() <<
"[CATEGORY] Found: " << originalName <<
" at file offset "
286 << realNameOffset <<
"\n"
287 <<
" -> Replaced with: " << newName <<
"\n";
289 char *patchLocation = writableMB.getBufferStart() + realNameOffset;
290 memcpy(patchLocation, newName.c_str(), originalName.length());
294 outs() <<
"[CATEGORY] Found: " << categoryName.str() <<
" at file offset "
295 << realNameOffset <<
"\n";
297 std::string randomString = generateRandomString(categoryName.size());
298 char *patchLocation = writableMB.getBufferStart() + realNameOffset;
299 memcpy(patchLocation, randomString.c_str(), randomString.length());
302 outs() <<
" -> Replaced with: " << randomString <<
"\n";
307Error patchMachOSlice(MachOObjectFile *machOObj,
308 const MemoryBuffer &originalMB,
309 WritableMemoryBuffer &writableMB,
310 uint64_t sliceOffset,
311 const CommandLineArgs &args)
313 if (!args.quietMode) {
314 outs() <<
"--- Patching architecture: " << machOObj->getArchTriple().getArchName()
315 <<
" (slice offset: " << sliceOffset <<
") ---\n";
317 for (
const SectionRef §ion : machOObj->sections()) {
318 Expected<StringRef> sectionNameOrErr = section.getName();
319 if (
auto e = sectionNameOrErr.takeError())
321 StringRef sectionName = *sectionNameOrErr;
323 if (sectionName ==
"__objc_classname")
324 patchClassNameSection(section, machOObj, writableMB, sliceOffset, args);
325 else if (sectionName ==
"__objc_catlist")
326 patchCategoryListSection(section, machOObj, originalMB, writableMB, sliceOffset, args);
328 return Error::success();
335 auto argsOpt = parseCommandLine(argc, argv);
338 const auto &args = *argsOpt;
340 Expected<OwningBinary<Binary>> binOrErr = createBinary(args.binaryPath.string());
341 if (
auto e = binOrErr.takeError()) {
342 errs() <<
"Error opening binary: " << toString(
std::move(e)) <<
"\n";
345 OwningBinary<Binary> &bin = binOrErr.get();
347 ErrorOr<std::unique_ptr<MemoryBuffer>> mbOrErr = MemoryBuffer::getFile(args.binaryPath.string());
348 if (
std::error_code ec = mbOrErr.getError()) {
349 errs() <<
"Error reading file into buffer: " << ec.message() <<
"\n";
352 std::unique_ptr<MemoryBuffer> originalMB {
std::move(mbOrErr.get())};
353 std::unique_ptr<WritableMemoryBuffer> writableMB
354 = WritableMemoryBuffer::getNewMemBuffer(originalMB->getBufferSize());
355 memcpy(writableMB->getBufferStart(), originalMB->getBufferStart(), originalMB->getBufferSize());
357 if (
auto *machOUni = dyn_cast<MachOUniversalBinary>(bin.getBinary())) {
358 for (
const auto &objForArch : machOUni->objects()) {
359 Expected<std::unique_ptr<MachOObjectFile>> machOObjOrErr = objForArch.getAsObjectFile();
360 if (
auto e = machOObjOrErr.takeError()) {
361 errs() <<
"Failed to get object for architecture: " << toString(std::move(e))
365 if (
auto e = patchMachOSlice(
366 machOObjOrErr->get(), *originalMB, *writableMB, objForArch.getOffset(), args)) {
367 errs() <<
"Failed to patch Mach-O slice: " << toString(std::move(e)) <<
"\n";
370 }
else if (
auto *machOObj = dyn_cast<MachOObjectFile>(bin.getBinary())) {
371 if (
auto e = patchMachOSlice(machOObj, *originalMB, *writableMB, 0, args)) {
372 errs() <<
"Failed to patch Mach-O file: " << toString(
std::move(e)) <<
"\n";
376 errs() <<
"The provided file is not a valid Mach-O binary.\n";
382 outs() <<
"\nDry run complete. Binary was not modified.\n";
387 raw_fd_ostream outFile(args.binaryPath.string(), ec);
389 errs() <<
"Error opening file for writing: " << ec.message() <<
"\n";
392 outFile.write(writableMB->getBufferStart(), writableMB->getBufferSize());
396 outs() <<
"\nSuccessfully patched binary in-place: " << args.binaryPath.string() <<
"\n";
int main(int argc, char *argv[])
[ctor_close]