Info
- Download page : https://manytricks.com/leech/releasenotes/
- Latest version : Leech 3.2.1 (3171) https://manytricks.com/download/_do_not_hotlink_/leech321.dmg
- Main executable : Leech
Reversing
I quickly located the function associated with license : -[PMMainController enterLicense:]
Here is the code (edited by aidapal)
// This function enters a license for the PMMainController object. It checks if the
// license window exists and loads it if not. Then, it retrieves the license status
// strings from the main bundle and sets them in the license status fields. If the
// license is already entered or the user has already nagged, it displays
// appropriate messages. Additionally, it handles periodic events to check the
// license status and nag the user if necessary.
void __cdecl enterLicense_100019414(PMMainController *mainController, SEL unusedParameter, id unusedSecondParameter)
{
// variables declarations removed for clarity
currentPmMainController = mainController;
if ( !mainController->licenseWindow )
{
+[NSBundle vintageStyleLoadNibNamed:owner:](
&OBJC_CLASS___NSBundle,
"vintageStyleLoadNibNamed:owner:",
CFSTR("License"),
mainController);
-[NSWindow setLevel:](mainController->licenseWindow, "setLevel:", 1LL);
}
launchCountRemaining = 0LL;
statusLength = &v59;
if ( (unsigned __int8)sub_10000545B(&launchCountRemaining, 0LL, &v59, v60, &v61) )
{
fileManagerInstance = +[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager");
pathToPreferencesFile = objc_msgSend(CFSTR("~/Library/Preferences"), "stringByExpandingTildeInPath");
fullPathToPreferenceFile = objc_msgSend(pathToPreferencesFile, "stringByAppendingString:", CFSTR("/.byhost.das"));
finalPathToFile = objc_msgSend(fullPathToPreferenceFile, "stringByAppendingString:", CFSTR("h02b0a"));
checkFileExistenceResult = objc_msgSend(finalPathToFile, "stringByAppendingString:", CFSTR("rd.plist"));
if ( -[NSFileManager fileExistsAtPath:](fileManagerInstance, "fileExistsAtPath:", checkFileExistenceResult) )
-[NSFileManager removeItemAtPath:error:](
fileManagerInstance,
"removeItemAtPath:error:",
checkFileExistenceResult,
0LL);
byte_1000CA9C8 = 1;
licenseMainStatusField = mainController->licenseMainStatusField;
if ( !objc_msgSend(v59, "length") )
statusLength = (id *)v60;
-[NSTextField setStringValue:](licenseMainStatusField, "setStringValue:", *statusLength);
topStatusField = mainController->licenseTopStatusField;
mainBundle = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
v78 = 0LL;
licensedStatusMessage = -[NSBundle localizedStringForKey:value:table:](
mainBundle,
"localizedStringForKey:value:table:",
CFSTR("Leech - Licensed to"),
&stru_10009EBD8,
0LL);
-[NSTextField setStringValue:](topStatusField, "setStringValue:", licensedStatusMessage);
licenseBottomStatusField = mainController->licenseBottomStatusField;
mainBundleSelector = "mainBundle";
mainBundleForUpgradeCheck = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
localizedStringSelector = "localizedStringForKey:value:table:";
thankYouMessage = -[NSBundle localizedStringForKey:value:table:](
mainBundleForUpgradeCheck,
"localizedStringForKey:value:table:",
CFSTR("Thank you"),
&stru_10009EBD8,
0LL);
setStringValueSelector = "setStringValue:";
-[NSTextField setStringValue:](licenseBottomStatusField, "setStringValue:", thankYouMessage);
currentPmMainController = mainController;
goto LABEL_9;
}
if ( v61 == 1 )
{
mainBundleForElseCheck = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
eligibleForUpgradeMessage = -[NSBundle localizedStringForKey:value:table:](
mainBundleForElseCheck,
"localizedStringForKey:value:table:",
CFSTR("Eligible for upgrade"),
&stru_10009EBD8,
0LL);
}
else
{
if ( v61 == 3 )
{
mainBundleForNagCheck = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
v78 = -[NSBundle localizedStringForKey:value:table:](
mainBundleForNagCheck,
"localizedStringForKey:value:table:",
CFSTR("Leech 1 license not found"),
&stru_10009EBD8,
0LL);
+[PMCertificateView alertToIncompleteCertificateWithSourceWindow:forDelegate:upgradeURLString:](
&OBJC_CLASS___PMCertificateView,
"alertToIncompleteCertificateWithSourceWindow:forDelegate:upgradeURLString:",
mainController->licenseWindow,
mainController,
0LL);
goto LABEL_42;
}
eligibleForUpgradeMessage = 0LL;
}
v78 = (__CFString *)eligibleForUpgradeMessage;
LABEL_42:
mainStatusField = mainController->licenseMainStatusField;
mainBundleForLicenseCheck = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
licenseStatusMessageForElse = -[NSBundle localizedStringForKey:value:table:](
mainBundleForLicenseCheck,
"localizedStringForKey:value:table:",
CFSTR("This license could be yours"),
&stru_10009EBD8,
0LL);
-[NSTextField setStringValue:](mainStatusField, "setStringValue:", licenseStatusMessageForElse);
mainBundleSelector = "mainBundle";
localizedStringSelector = "localizedStringForKey:value:table:";
if ( launchCountRemaining < 26 )
{
launchCountRemaining = 25 - launchCountRemaining;
-[NSTextField setStringValue:](mainController->licenseTopStatusField, "setStringValue:", CFSTR("Leech"));
bottomStatusFieldForNaggedCheck = mainController->licenseBottomStatusField;
futureDateInstance = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
launchCountFormatString = CFSTR("%d launches remaining");
if ( launchCountRemaining == 1 )
launchCountFormatString = CFSTR("1 launch remaining");
launchCountFormattedMessage = -[NSBundle localizedStringForKey:value:table:](
futureDateInstance,
"localizedStringForKey:value:table:",
launchCountFormatString,
&stru_10009EBD8,
0LL);
launchCountMessage = +[NSString stringWithFormat:](
&OBJC_CLASS___NSString,
"stringWithFormat:",
launchCountFormattedMessage,
(unsigned int)launchCountRemaining);
if ( v78 )
{
finalLaunchCountMessage = +[NSString stringWithFormat:](
&OBJC_CLASS___NSString,
"stringWithFormat:",
CFSTR("%@\n%@"),
launchCountMessage);
setStringValueSelector = "setStringValue:";
-[NSTextField setStringValue:](bottomStatusFieldForNaggedCheck, "setStringValue:", finalLaunchCountMessage);
LABEL_9:
licenseEnteredFlag = 1;
goto LABEL_10;
}
setStringValueSelector = "setStringValue:";
-[NSTextField setStringValue:](bottomStatusFieldForNaggedCheck, "setStringValue:", launchCountMessage);
licenseEnteredFlag = 1;
LABEL_59:
v78 = 0LL;
goto LABEL_10;
}
licenseTopStatusField = mainController->licenseTopStatusField;
mainBundleForNaggedCheck = +[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle");
naggedMessage = &stru_10009EBD8;
launchRemainingMessage = -[NSBundle localizedStringForKey:value:table:](
mainBundleForNaggedCheck,
"localizedStringForKey:value:table:",
CFSTR("Leech - Launched %d times"),
&stru_10009EBD8,
0LL);
formattedLaunchRemainingMessage = +[NSString stringWithFormat:](
&OBJC_CLASS___NSString,
"stringWithFormat:",
launchRemainingMessage,
(unsigned int)launchCountRemaining);
objc_msgSend(licenseTopStatusField, "setStringValue:", formattedLaunchRemainingMessage);
hasNaggedFlag = mainController->_hasNagged == 0;
setStringValueSelector = "setStringValue:";
if ( !hasNaggedFlag )
{
licenseStatusMessageForNaggedCheck = v78;
if ( v78 )
{
licenseTopStatusField = mainController->licenseBottomStatusField;
bundleObjectForNaggedCheck = objc_msgSend(&OBJC_CLASS___NSBundle, mainBundleSelector);
licenseEnteredFlag = 0;
naggedMessageForNaggedCheck = objc_msgSend(
bundleObjectForNaggedCheck,
localizedStringSelector,
CFSTR("Please buy Leech"),
&stru_10009EBD8,
0LL);
formattedNaggedMessageForNaggedCheck = +[NSString stringWithFormat:](
&OBJC_CLASS___NSString,
"stringWithFormat:",
CFSTR("%@\n%@"),
naggedMessageForNaggedCheck,
licenseStatusMessageForNaggedCheck);
objc_msgSend(licenseTopStatusField, "setStringValue:", formattedNaggedMessageForNaggedCheck);
goto LABEL_10;
}
licenseEnteredFlag = 0;
goto LABEL_59;
}
bottomStatusFieldForFinalCheck = mainController->licenseBottomStatusField;
if ( v78 )
naggedMessage = v78;
-[NSTextField setStringValue:](bottomStatusFieldForFinalCheck, "setStringValue:", naggedMessage);
licenseEnteredFlag = 0;
LABEL_10:
buttonSuperview = -[NSButton superview](currentPmMainController->licenseBuyButton, "superview");
objc_msgSend(buttonSuperview, "setHidden:", (unsigned __int8)byte_1000CA9C8);
-[NSWindow adjustHeightToFitContent](currentPmMainController->licenseWindow, "adjustHeightToFitContent");
standardWindowButton = -[NSWindow standardWindowButton:](
currentPmMainController->licenseWindow,
"standardWindowButton:",
0LL);
-[NSButton setEnabled:](standardWindowButton, "setEnabled:", (unsigned __int8)byte_1000CA9C8);
if ( !-[NSWindow isVisible](currentPmMainController->licenseWindow, "isVisible") )
-[NSWindow center](currentPmMainController->licenseWindow, "center");
-[NSWindow makeKeyAndOrderFront:](
currentPmMainController->licenseWindow,
"makeKeyAndOrderFront:",
currentPmMainController);
if ( !licenseEnteredFlag && !currentPmMainController->_hasNagged && (byte_1000CA9C9 & 1) == 0 )
{
byte_1000CA9C9 = 1;
-[NSButton setEnabled:](currentPmMainController->licenseBuyLaterButton, "setEnabled:", 0LL);
maxLaunchCount = 20LL;
if ( (launchCountRemaining - 25) / 3 < 20 )
maxLaunchCount = (launchCountRemaining - 25) / 3;
licenseTopStatusField = +[NSDate distantFuture](&OBJC_CLASS___NSDate, "distantFuture");
+[NSEvent startPeriodicEventsAfterDelay:withPeriod:](
&OBJC_CLASS___NSEvent,
"startPeriodicEventsAfterDelay:withPeriod:",
0.0,
1.0);
if ( !byte_1000CA9C8 && maxLaunchCount >= -10 )
{
remainingLaunchCount = (char *)(maxLaunchCount + 10);
runLoopMode = NSDefaultRunLoopMode;
typeSelector = "type";
mainBundleSelector = "mainBundle";
localizedStringSelector = "localizedStringForKey:value:table:";
formatStringSelector = "stringWithFormat:";
setStringValueSelector = "setStringValue:";
contentViewSelector = "contentView";
locationInWindowSelector = "locationInWindow";
hitTestSelector = "hitTest:";
respondsToSelectorSelector = "respondsToSelector:";
addToShoppingCartSelector = "addToShoppingCart:";
sendEventSelector = "sendEvent:";
currentMainControllerInstance = currentPmMainController;
do
{
nextEvent = objc_msgSend(
NSApp,
"nextEventMatchingMask:untilDate:inMode:dequeue:",
65538LL,
licenseTopStatusField,
runLoopMode,
1LL);
if ( !nextEvent )
break;
eventObject = nextEvent;
if ( objc_msgSend(nextEvent, typeSelector) == (id)1 )
{
v73 = remainingLaunchCount;
contentView = objc_msgSend(currentPmMainController->licenseWindow, contentViewSelector);
objc_msgSend(eventObject, locationInWindowSelector);
hitTestResult = objc_msgSend(contentView, hitTestSelector);
if ( (unsigned __int8)objc_msgSend(hitTestResult, respondsToSelectorSelector, "action") )
{
actionName = (const char *)objc_msgSend(hitTestResult, "action");
if ( actionName == addToShoppingCartSelector )
objc_msgSend(NSApp, sendEventSelector, eventObject);
currentPmMainController = currentMainControllerInstance;
remainingLaunchCount = (char *)v73;
}
else
{
remainingLaunchCount = (char *)v73;
currentPmMainController = currentMainControllerInstance;
}
}
else if ( !byte_1000CA9C8 )
{
v73 = currentPmMainController->licenseBottomStatusField;
formatString = formatStringSelector;
bundleObject = objc_msgSend(&OBJC_CLASS___NSBundle, mainBundleSelector);
localizedString = objc_msgSend(
bundleObject,
localizedStringSelector,
CFSTR("Nagging: %d"),
&stru_10009EBD8,
0LL);
formattedString = objc_msgSend(
&OBJC_CLASS___NSString,
formatString,
localizedString,
(unsigned int)remainingLaunchCount);
if ( v78 )
formattedString = objc_msgSend(&OBJC_CLASS___NSString, formatString, CFSTR("%@\n%@"), formattedString);
objc_msgSend(v73, setStringValueSelector, formattedString);
--remainingLaunchCount;
currentPmMainController = currentMainControllerInstance;
}
}
while ( !byte_1000CA9C8 && (__int64)remainingLaunchCount >= 0 );
}
+[NSEvent stopPeriodicEvents](&OBJC_CLASS___NSEvent, "stopPeriodicEvents");
if ( !byte_1000CA9C8 )
{
bottomStatusField = currentPmMainController->licenseBottomStatusField;
licenseStatusMessage = v78;
if ( v78 )
{
bundleObjectForNagMessage = objc_msgSend(&OBJC_CLASS___NSBundle, mainBundleSelector);
nagMessage = objc_msgSend(
bundleObjectForNagMessage,
localizedStringSelector,
CFSTR("Please buy Leech"),
&stru_10009EBD8,
0LL);
finalNagMessage = +[NSString stringWithFormat:](
&OBJC_CLASS___NSString,
"stringWithFormat:",
CFSTR("%@\n%@"),
nagMessage,
licenseStatusMessage);
}
else
{
bundleObjectForFinalCheck = objc_msgSend(&OBJC_CLASS___NSBundle, mainBundleSelector);
finalNagMessage = (NSString *)objc_msgSend(
bundleObjectForFinalCheck,
localizedStringSelector,
CFSTR("Please buy Leech"),
&stru_10009EBD8,
0LL);
}
objc_msgSend(bottomStatusField, setStringValueSelector, finalNagMessage);
}
-[NSButton setEnabled:](currentPmMainController->licenseBuyLaterButton, "setEnabled:", 1LL);
currentPmMainController->_hasNagged = 1;
}
}
I quickly located the file located at ~/Library/Preferences/.byhost.dash02b0ard.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>c</key>
<integer>29</integer>
</dict>
</plist>
c
is clearly the number of times the app was launched.
Resetting c
to 0 every day/week using a shell script could be an option, but that’s not fun.
I did try to input a negative value, which turned to 1 after the app launched.
I dug a bit into the function sub_10000545B
(renamed after analysis initializeAndUpdatePreferences_10000545b
):
// This function initializes and updates preferences based on the provided
// parameters. It first attempts to retrieve existing preference values for 'Name'
// and 'E-Mail'. If these values are not found or need updating, it creates new
// NSDateComponents objects with default values, sets the year, month, day, and
// then retrieves the current calendar date. If the input parameter 'a6' is less
// than or equal to zero, it constructs a file path for a plist file, reads the
// contents of that file, and updates an integer value associated with the key 'c'.
// The function returns 1 if successful in updating preferences, otherwise it
// returns 0.
__int64 __fastcall initializeAndUpdatePreferences_10000545b(
_QWORD *updateYearFlag,
char isUpdateRequired,
id *namePtr,
__CFString **emailPtr,
__int64 *initializationFlag,
double timeIntervalSinceNow)
{
// variables declarations removed for clarity
v9 = +[PMMainController mainController](&OBJC_CLASS___PMMainController, "mainController", timeIntervalSinceNow);
v10 = processCertificates_100039bbc(v9, initializationFlag);
if ( v10 )
{
v11 = v10;
if ( updateYearFlag )
*updateYearFlag = 0LL;
if ( namePtr )
{
v12 = objc_msgSend(v10, "objectForKey:", CFSTR("Name"));
*namePtr = objc_msgSend(v12, "trimmedString");
}
v13 = 1;
if ( emailPtr )
{
v14 = (__CFString *)objc_msgSend(v11, "objectForKey:", CFSTR("E-Mail"));
v15 = &stru_10009EBD8;
if ( v14 )
v15 = v14;
*emailPtr = v15;
}
}
else
{
v16 = objc_alloc((Class)&OBJC_CLASS___NSDateComponents);
v17 = objc_msgSend(v16, "init");
v18 = objc_autorelease(v17);
objc_msgSend(v18, "setDay:", 1LL);
objc_msgSend(v18, "setMonth:", 2LL);
v19 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", 2000LL);
v20 = -[NSNumber integerValue](v19, "integerValue");
v21 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", 8LL);
v22 = &v20[-[NSNumber integerValue](v21, "integerValue")];
v23 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", 8LL);
v24 = -[NSNumber integerValue](v23, "integerValue");
objc_msgSend(v18, "setYear:", &v22[v24]);
v25 = +[NSCalendar currentCalendar](&OBJC_CLASS___NSCalendar, "currentCalendar");
v26 = -[NSCalendar dateFromComponents:](v25, "dateFromComponents:", v18);
-[NSDate timeIntervalSinceNow](v26, "timeIntervalSinceNow");
if ( timeIntervalSinceNow <= 0.0 )
{
v27 = objc_msgSend(CFSTR("~/Library/Preferences"), "stringByExpandingTildeInPath");
v28 = objc_msgSend(v27, "stringByAppendingString:", CFSTR("/.byhost.das"));
v29 = objc_msgSend(v28, "stringByAppendingString:", CFSTR("h02b0a"));
v30 = objc_msgSend(v29, "stringByAppendingString:", CFSTR("rd.plist"));
v31 = +[NSDictionary dictionaryWithContentsOfFile:](
&OBJC_CLASS___NSDictionary,
"dictionaryWithContentsOfFile:",
v30);
v32 = -[NSDictionary objectForKey:](v31, "objectForKey:", CFSTR("c"));
v33 = (char *)objc_msgSend(v32, "integerValue");
v34 = 0LL;
if ( (__int64)v33 > 0 )
v34 = v33;
if ( isUpdateRequired )
{
v35 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", ++v34);
v36 = +[NSDictionary dictionaryWithObject:forKey:](
&OBJC_CLASS___NSDictionary,
"dictionaryWithObject:forKey:",
v35,
CFSTR("c"));
-[NSDictionary writeToFile:atomically:](v36, "writeToFile:atomically:", v30, 1LL);
}
if ( updateYearFlag )
*updateYearFlag = v34;
}
else if ( updateYearFlag )
{
*updateYearFlag = 0LL;
}
return 0;
}
return v13;
}
The function calls processCertificates_100039bbc
with parameters v9
and initializationFlag
. This function seems to be the one verifying the license ? I can read kernelForCertificateView
.. Probably beyond my skills and not necessary.
I’ll come back to initializeAndUpdatePreferences_10000545b
analysis later, let take a look at enterLicense_100019414
.
First patch I tried
I saw if ( launchCountRemaining < 26 )
so I tried to patch the program by putting a higher value, I have issues because of signed values 😅
I tried patching the binary and replacing by a number <127 (because of signed bytes comparaison), didn’t work..
Instead of 26 let try 75, 4B in hexadecimal.
Using https://defuse.ca/online-x86-assembler.htm#disassembly2 I’m able to read the assembly instruction :
I need to edit 1A
value to `4B
48 83 F8 1A 4C 89 7D B8 4C 89 6D C0 0F 8C F3 00
to
48 83 F8 4B 4C 89 7D B8 4C 89 6D C0 0F 8C F3 00
The Pseudocode reflects the change ! I had issues using IDA’s patching method so I’ll be using Hex Fiend manually.
In the “config” file I set the number of app launch to 30, it shouldn’t trigger the function.
Still triggered the nagging :/
Second patch
Back to initializeAndUpdatePreferences_10000545b
function.
The launchCount is increased at every launch by :
if ( isUpdateRequired )
{
v35 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", ++v34);
v36 = +[NSDictionary dictionaryWithObject:forKey:](
&OBJC_CLASS___NSDictionary,
"dictionaryWithObject:forKey:",
v35,
CFSTR("c"));
[NSDictionary writeToFile:atomically:](v36, "writeToFile:atomically:", v30, 1LL);
}
The goal was to prevent the counter increment by changing
35 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", ++v34);
to
35 = +[NSNumber numberWithInteger:](&OBJC_CLASS___NSNumber, "numberWithInteger:", v34);
Assembly corresponding:
changing inc r12
to mov r12, r12
?
49 FF C4 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C
So I replaced :
49 FF C4 ; inc r12
to 49 89 C4 ; mov r12, r12
Pseudocode looks fine :
Like before using Hex Fiend :
49 FF C4 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C
to
49 89 C4 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C
The counter is still increased :
(base) noham@propulsion ~ % more ~/Library/Preferences/.byhost.dash02b0ard.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>c</key>
<integer>32</integer>
</dict>
</plist>
(base) noham@propulsion ~ % /Applications/Leech.app/Contents/MacOS/Leech
2025-02-20 23:27:19.710 Leech[79935:1145029] +[IMKClient subclass]: chose IMKClient_Modern
2025-02-20 23:27:19.711 Leech[79935:1145029] +[IMKInputSession subclass]: chose IMKInputSession_Modern
(base) noham@propulsion ~ % more ~/Library/Preferences/.byhost.dash02b0ard.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>c</key>
<integer>33</integer>
</dict>
</plist>
Third patch :(
I tried tweaking value returned by initializeAndUpdatePreferences_10000545b
without any result :/
Comparing hex notes
[STW] Leech 3.1.7 U2B https://manytricks.com/download/_do_not_hotlink_/leech317.dmg
x86_64:
74 24 48 8B 85 B8 FE FF FF
to
90 90 48 8B 85 B8 FE FF FF
ARM64:
00 01 00 34 55 00 00 B4
to
1F 20 03 D5 55 00 00 B4
Based on the original hex 00 01 00 34 55 00 00 B4
I’ll try to understand what was patched.
I split the binary to keep the arm64 part :
lipo Leech -thin arm64 -output Leech_arm64
This patch targets the processCertificates_100039bbc
function. The original instruction:
__text:0000000100039EB8 sub_10005F250 CBZ W0, loc_100039ED8
was replaced with:
`NOP ; No Operation`
We are within processCertificates_100039bbc
, that function that was called by initializeAndUpdatePreferences_10000545b
:
v9 = +[PMMainController mainController](&OBJC_CLASS___PMMainController, "mainController", timeIntervalSinceNow);
v10 = processCertificates_100039bbc(v9, initializationFlag);
I’ll take a look at this function then :
// This function processes certificates by iterating through a list of
// certificates, checking if they match certain conditions, and performing actions
// based on those conditions. It uses various selectors to interact with
// NSFileManager, PMCertificateView, and other classes. The function returns an id
// object.
id __fastcall processCertificates_100039bbc(void *inputObject, __int64 *stackGuardPtr)
{
// variables declarations removed for clarity
certificateStatus = stackGuardPtr;
guardCheckPointer = &qword_1000BF000;
outputObject = (id)qword_1000BF0C0;
if ( qword_1000BF0C0 )
{
if ( stackGuardPtr )
*stackGuardPtr = 0LL;
return outputObject;
}
if ( !inputObject )
{
if ( stackGuardPtr )
goto LABEL_40;
return 0LL;
}
fileManagerInstance = +[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager");
outerEnumerationCount = 0u;
outerTotalObjects = 0u;
outerRemainingObjects = 0u;
outerCounterValue = 0u;
obj = (id)sub_10005EDD8();
enumerationResult = objc_msgSend(obj, "countByEnumeratingWithState:objects:count:", &outerEnumerationCount, v46, 16LL);
if ( enumerationResult )
{
finalCertificateReference = 0LL;
currentObject = 0LL;
initialCount = *(_QWORD *)outerTotalObjects;
setWindowSelectorRef = &selRef_setWindow_;
vintageStyleLoadNibNamedOwnerSelectorRef = &selRef_vintageStyleLoadNibNamed_owner_;
stackGuardPointerCopy = certificateStatus;
previousCount = *(_QWORD *)outerTotalObjects;
defaultFileManager = fileManagerInstance;
while ( 1 )
{
currentObjectReference = currentObject;
counter = 0LL;
setWindowSelectorName = setWindowSelectorRef[238];
vintageStyleLoadNibNamedSelectorName = vintageStyleLoadNibNamedOwnerSelectorRef[110];
if ( (unsigned __int64)enumerationResult <= 1 )
totalObjects = 1LL;
else
totalObjects = (__int64)enumerationResult;
totalIterations = totalObjects;
setWindowSelectorPathComponent = vintageStyleLoadNibNamedOwnerSelectorRef[110];
do
{
if ( *(_QWORD *)outerTotalObjects != initialCount )
objc_enumerationMutation(obj);
currentIndex = counter;
currentCertificate = objc_msgSend(
*(id *)(*((_QWORD *)&outerEnumerationCount + 1) + 8 * counter),
setWindowSelectorName,
CFSTR("Many Tricks/Licenses"),
previousCount,
defaultFileManager);
innerEnumerationCount = 0u;
innerTotalObjects = 0u;
innerRemainingObjects = 0u;
innerCounterValue = 0u;
certificateEnumerator = objc_msgSend(
fileManagerInstance,
vintageStyleLoadNibNamedSelectorName,
currentCertificate,
0LL);
nextCertificate = objc_msgSend(
certificateEnumerator,
"countByEnumeratingWithState:objects:count:",
&innerEnumerationCount,
v45,
16LL);
if ( !nextCertificate )
goto LABEL_36;
nextEnumerationCount = *(_QWORD *)innerTotalObjects;
while ( 2 )
{
innerCounter = 0LL;
if ( (unsigned __int64)nextCertificate <= 1 )
remainingObjects = 1LL;
else
remainingObjects = (__int64)nextCertificate;
do
{
if ( *(_QWORD *)innerTotalObjects != nextEnumerationCount )
objc_enumerationMutation(certificateEnumerator);
currentCertificateId = *(_QWORD *)(*((_QWORD *)&innerEnumerationCount + 1) + 8 * innerCounter);
if ( +[PMCertificateView isCertificateFile:](
&OBJC_CLASS___PMCertificateView,
"isCertificateFile:",
currentCertificateId) )
{
processedCertificate = (void *)sub_100039A78(
objc_msgSend(
currentCertificate,
setWindowSelectorName,
currentCertificateId),
inputObject,
&processingResult);
if ( processedCertificate )
{
switch ( processingResult )
{
case 1LL:
if ( finalCertificateReference )
{
if ( stackGuardPointerCopy )
*stackGuardPointerCopy = 0LL;
outputObject = objc_msgSend(finalCertificateReference, "retain");
goto LABEL_57;
}
tempCertificate = currentObjectReference;
if ( !currentObjectReference )
tempCertificate = processedCertificate;
currentObjectReference = tempCertificate;
finalCertificateReference = 0LL;
break;
case 2LL:
if ( currentObjectReference )
{
LABEL_41:
if ( stackGuardPointerCopy )
*stackGuardPointerCopy = 0LL;
outputObject = objc_msgSend(processedCertificate, "retain");
LABEL_57:
guardCheckPointer = &qword_1000BF000;
goto LABEL_58;
}
finalCertificate = finalCertificateReference;
if ( !finalCertificateReference )
finalCertificate = processedCertificate;
currentObjectReference = 0LL;
finalCertificateReference = finalCertificate;
break;
case 0LL:
goto LABEL_41;
}
}
}
++innerCounter;
}
while ( remainingObjects != innerCounter );
nextCertificate = objc_msgSend(
certificateEnumerator,
"countByEnumeratingWithState:objects:count:",
&innerEnumerationCount,
v45,
16LL);
if ( nextCertificate )
continue;
break;
}
LABEL_36:
counter = currentIndex + 1;
fileManagerInstance = defaultFileManager;
initialCount = previousCount;
vintageStyleLoadNibNamedSelectorName = setWindowSelectorPathComponent;
}
while ( currentIndex + 1 != totalIterations );
enumerationResult = objc_msgSend(
obj,
"countByEnumeratingWithState:objects:count:",
&outerEnumerationCount,
v46,
16LL);
guardCheckPointer = &qword_1000BF000;
certificateStatus = stackGuardPointerCopy;
currentObject = currentObjectReference;
setWindowSelectorRef = &selRef_setWindow_;
vintageStyleLoadNibNamedOwnerSelectorRef = &selRef_vintageStyleLoadNibNamed_owner_;
if ( !enumerationResult )
goto LABEL_45;
}
}
finalCertificateReference = 0LL;
currentObject = 0LL;
LABEL_45:
if ( !(unsigned int)objc_msgSend(inputObject, "respondsToSelector:", "kernelForCertificateView:")
|| !(unsigned int)sub_10005F250(objc_msgSend(inputObject, "kernelForCertificateView:", 0LL)) )
{
if ( certificateStatus )
{
if ( currentObject )
{
outputObject = 0LL;
statusCode = 1LL;
goto LABEL_61;
}
if ( finalCertificateReference )
{
outputObject = 0LL;
statusCode = 2LL;
goto LABEL_61;
}
LABEL_40:
outputObject = 0LL;
statusCode = -1LL;
LABEL_61:
*certificateStatus = statusCode;
return outputObject;
}
return 0LL;
}
if ( certificateStatus )
*certificateStatus = 0LL;
outputObject = objc_msgSend((id)sub_100039F6C(), "retain");
LABEL_58:
guardCheckPointer[24] = (__int64)outputObject;
return outputObject;
}
This modification effectively removes a license check branch in this key function :
if ( !(unsigned int)objc_msgSend(a1, "respondsToSelector:", "kernelForCertificateView:")
|| !(unsigned int)sub_10005F250(objc_msgSend(a1, "kernelForCertificateView:", 0LL)) )
is now
if ( !(unsigned int)objc_msgSend(a1, "respondsToSelector:", "kernelForCertificateView:") )
Arm part, finally
The goal was to prevent a launch counter from incrementing. Here are the attempts made:
First Try: Modifying ADD Instruction
I changed :
D6 06 00 91 15 CF 45 F9 40 D3 45 F9 1F 20 03 D5
ADD X22, X22, #1 ; Original (increases X22)
to :
D6 02 00 91 15 CF 45 F9 40 D3 45 F9 1F 20 03 D5
ADD X22, X22, #0 ; Modified (does nothing)
But for some reason the app crash
zsh: killed /Applications/Leech.app/Contents/MacOS/Leech
Second Try: Using NOP
D6 06 00 91 15 CF 45 F9 40 D3 45 F9 1F 20 03 D5
ADD X22, X22, #1
To :
1F 20 03 D5 15 CF 45 F9 40 D3 45 F9 1F 20 03 D5
NOP ; 1F 20 03 D5
Finally
The crashes were happening because of macOS code signing requirements. A basic code signing attempt wasn’t sufficient:
(base) noham@propulsion ~ % sudo codesign --force --sign - /Applications/Leech.app
/Applications/Leech.app: replacing existing signature
/Applications/Leech.app: code object is not signed at all
In subcomponent: /Applications/Leech.app/Contents/MacOS/Leech_arm64.id1
The solution was to use the --deep
flag to sign all components:
(base) noham@propulsion ~ % sudo codesign --force --deep --sign - /Applications/Leech.app
/Applications/Leech.app: replacing existing signature
After proper code signing, the patched app launches successfully and the launch counter no longer increases:
(base) noham@propulsion ~ % more ~/Library/Preferences/.byhost.dash02b0ard.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>c</key>
<integer>20</integer>
</dict>
</plist>
(base) noham@propulsion ~ % /Applications/Leech.app/Contents/MacOS/Leech
2025-02-21 20:55:58.574 Leech[12948:97064] +[IMKClient subclass]: chose IMKClient_Modern
2025-02-21 20:55:58.574 Leech[12948:97064] +[IMKInputSession subclass]: chose IMKInputSession_Modern
(base) noham@propulsion ~ % more ~/Library/Preferences/.byhost.dash02b0ard.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>c</key>
<integer>20</integer>
</dict>
</plist>
Patching latest version
The version I was working on was Leech 3.1.7, the latest version is Leech 3.2.1, let’s patch it the same way I did before.
x86_64 (not tested):
49 FF C4 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C
to
90 90 90 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C
ARM64:
F7 06 00 91 35 5B 40 F9 00 5F 40 F9 E2 03 17 AA
to
1F 20 03 D5 35 5B 40 F9 00 5F 40 F9 E2 03 17 AA