Info

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..

comparaison-26.png

Instead of 26 let try 75, 4B in hexadecimal.

comparaison-26-bytes.png

Using https://defuse.ca/online-x86-assembler.htm#disassembly2 I’m able to read the assembly instruction :

comparaison-26-disassembly.png

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

comparaison-26-75.png

The Pseudocode reflects the change ! I had issues using IDA’s patching method so I’ll be using Hex Fiend manually.

hex-fiend.png Located and replaced (and saved).

In the “config” file I set the number of app launch to 30, it shouldn’t trigger the function. Still triggered the nagging :/ triggered.png

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: inc.png

changing inc r12 to mov r12, r12 ?

49 FF C4 4C 8B 3D 59 07 0C 00 48 8B 3D 5A 07 0C

inc-assembly.png

So I replaced : 49 FF C4  ; inc r12 to 49 89 C4  ; mov r12, r12

Pseudocode looks fine : inc-pseudocode.png

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

not-patched-hex.png patched-hex.png

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`

hex-ref.png

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:") )

disassembly-before.png after disassembly-after.png

pseudocode-after.png

Arm part, finally

The goal was to prevent a launch counter from incrementing. Here are the attempts made: try1-pseudocode-before.png try1-disassembler-before.png

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)

try1-pseudocode-after.png try1-disassembler-after.png

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

try2-disassembler-after.png But the app still crash..

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