Online activation process

I tested the registration process using code “test”. The application appears to use the device’s UUID as part of the activation:

Device ID: 6C074FFE-0939-5A8A-BC0F-4994FCA543E0-5986

The application makes the following HTTP request:

curl 'http://reg.code-industry.net/mpe-5/pr/index-5.php?1=1&2=test&3=6C074FFE-0939-5A8A-BC0F-4994FCA543E0-5986&4=6d61634f532031352e33202d2031352e33' \
-H 'Host: reg.code-industry.net' \
-H 'Connection: Keep-Alive' \
-H 'Accept-Language: en-US,*' \
-H 'User-Agent: Mozilla/5.0'

Which returns:

not found

I confirmed the device UUID is retrieved using:

ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { print $3; }'

"6C074FFE-0939-5A8A-BC0F-4994FCA543E0"

The “5986” suffix indicates the software version (5.9.86).

According to Apple documentation, the hardware UUID persists across OS upgrades, reinstalls, and multi-boot installations, making it a reliable identifier for license enforcement.

Registration URL generation

All online activation is handled by the RegCrypt::GetRegistrationURL function, which:

  1. Checks subscription status:
  • If active: uses URL http://reg.code-industry.net/mpe-5/pr/sub-5.php
  • Otherwise: uses URL http://reg.code-industry.net/mpe-5/pr/index-5.php
  1. Modifies the URL based on flags:
  • If a4’s first bit is set: appends “?1=1&2=”
  • Otherwise: appends “?1=2&2=”
  • Then appends the user identifier, “&3=”, and additional parameters (a2 and a3)
  1. Handles subscriptions differently:
  • For active subscriptions: adds more parameters including timestamp and encoded values
  • Otherwise: simply appends identifiers directly

This function is referenced by:

  • RegDialog::pb_deactive
  • RegDialog::pb_reg
  • MainWindow::ValidateLicense
  • MainReg::Deactivate

Offline activation

The function sub_1002E4108 only initializes UI elements of the registration dialog (setting text for labels, buttons, and group boxes). It doesn’t handle button clicks or user interactions. This function is called by sub_1002E13F4.

License validation analysis

MainWindow::ValidateLicense

void __fastcall MainWindow::ValidateLicense(MainWindow *this, const QString *a2)
{
  // variables declarations removed for clarity

  if ( *(_DWORD *)(*((_QWORD *)this + 138) + 4LL) && !*((_BYTE *)this + 98) )
  {
    *((_BYTE *)this + 98) = 1;  // Set license flag
    
    if ( *(_DWORD *)(*((_QWORD *)this + 139) + 4LL) )
    {
      // License server validation path
      MainReg::ReadReg((MainReg *)&MainReg::GetInstance(void)::instance, &v32);
      
      // [Memory cleanup code removed]
      
      MainReg::ActivateFromLicenseServer(
        (MainReg *)&MainReg::GetInstance(void)::instance,
        (const QString *)this + 138,
        (const QString *)this + 143,
        (const QString *)this + 139,
        (const QString *)this + 140);
        
      MainReg::ReadReg((MainReg *)&MainReg::GetInstance(void)::instance, v28);
      *((_BYTE *)this + 97) = *((_DWORD *)v28[0].d + 1) != 0;  // Set validity flag
      
      // [Memory cleanup code removed]
      
      QDocTab::SetRegProgram(*((QDocTab **)this + 18), *((_BYTE *)this + 97));
      
      if ( *((_BYTE *)this + 97) )  // If license is valid
      {
        // Update window title and handle concurrent licensing
        // [Window title update code removed]
      }
      else if ( byte_101D16CC1 )
      {
        // Show license server connection failure
        QMetaObject::tr(&v29, (QMetaObject *)&MainWindow::staticMetaObject, 
                        "Failed to connect to License server", 0LL, -1);
        // [Message box code retained]
      }
    }
    else
    {
      // Local validation path
      QString::toLatin1_helper(v28, (QString *)this + 138, a2);
      IsConcurrent = RegCrypt::IsConcurrent((RegCrypt *)v28, v8);
      
      // [Memory cleanup code removed]
      
      if ( (IsConcurrent & 1) != 0 )
        return;
      
      // Generate registration URL and validate via network
      // [Network request setup code retained]
      v21 = QNetworkAccessManager::get(v19, (const QNetworkRequest *)&v29);
      QObject::connect(&v23, v21, "2finished()", this, "1Check_for_Validation_Finished()", 0LL);
      
      // [Cleanup code removed]
    }
  }
}

This function checks if the application is licensed:

  1. If already licensed (flag at this+98 is set), returns immediately
  2. Otherwise, sets the license flag (this+98 = 1)
  3. Reads stored license key using MainReg::ReadReg
  4. Contacts licensing server via MainReg::ActivateFromLicenseServer
  5. Sets validity flag (this+97) based on response
  6. If valid:
    • Updates window title
    • Handles concurrent licensing if enabled
  7. If invalid:
    • Checks if concurrent using RegCrypt::IsConcurrent
    • Generates registration URL
    • Validates via network request
    • Displays failure message if needed

The key conditional check is:

if ( *(_DWORD *)(*((_QWORD *)this + 138) + 4LL) && !*((_BYTE *)this + 98) )

Where !*((_BYTE *)this + 98) evaluates if the license flag is 0 (false).

assembly.png

Looking at the ARM64 assembly:

SUB   SP, SP, #0xB0
STP   X22, X21, [SP,#0xA0+var_20]
STP   X20, X19, [SP,#0xA0+var_10]
STP   X29, X30, [SP,#0xA0+var_s0]
ADD   X29, SP, #0xA0

LDR   X8, [X0,#0x450]
LDR   W8, [X8,#4]
CBZ   W8, loc_1002E97C4

MOV   X19, X0
LDRB  W8, [X0,#0x62]
CBZ   W8, loc_1002E97D8

loc_1002E97D8:
ADD   X20, X19, #0x450
MOV   W8, #1
STRB  W8, [X19,#0x62]

I attempted to patch this by changing:

88 00 00 34 F3 03 00 AA 08 88 41 39 C8 00 00 34Original bytes00 00 00 14 F3 03 00 AA 08 88 41 39 C8 00 00 34Patched bytes

However, this caused the app to fail with:

00:38:30 23.02.25  Registration:  Error: REG06
00:38:30 23.02.25  Main:  Main window initilization finished

MainWindow::VerifyLicense

Simply calls MainWindow::ValidateLicense.

MainWindow::ReadReg

__int64 __fastcall MainWindow::ReadReg(MainWindow *this)
{
  // Reset license flag
  *((_BYTE *)this + 100) = 0;
  
  // Clear any existing registration data
  // [Memory cleanup code removed]
  
  // Read license information from settings
  GeneralSettings::GeneralSettings((GeneralSettings *)v67);
  MainVersion = GeneralSettings::GetMainVersion((GeneralSettings *)v67);
  
  // Get registration code
  GeneralSettings::GetRegistrationCode((GeneralSettings *)v67, v4, v5);
  QString::trimmed_helper(&v66, &v65, v6);
  *((QString *)this + 138) = v66;
  
  // [Memory cleanup code removed]
  
  // Get license server IP
  GeneralSettings::GetLicenseServerIp((__int64 *)&v66, (GeneralSettings *)v67, v10);
  *((QString *)this + 139) = v66;
  
  // Get license server port
  LicenseServerPort = (QString *)GeneralSettings::GetLicenseServerPort((GeneralSettings *)v67);
  QString::number(&v66, LicenseServerPort, 10, v15);
  *((QString *)this + 140) = v66;
  
  // Check if license is concurrent
  if ( !*((_DWORD *)*v13 + 1) )
    goto LABEL_47;
    
  QString::toLatin1_helper(&v66, (QString *)this + 138, v16);
  IsConcurrent = RegCrypt::IsConcurrent((RegCrypt *)&v66, v18);
  
  // [Memory cleanup code removed]
  
  if ( IsConcurrent )
  {
    // Remove activation code if license is concurrent
    QPDFSettings::Remove((QPDFSettings *)v67, v22);
    // [Memory cleanup code removed]
  }
  else
  {
LABEL_47:
    // Get activation code for non-concurrent license
    GeneralSettings::GetActivationCode((__int64 *)&v65, (GeneralSettings *)v67);
    QString::trimmed_helper(&v66, &v65, v24);
    *((QString *)this + 141) = v66;
  }
  
  // Load machine ID
  LoadMachineID(&v66);
  *((QString *)this + 143) = v66;
  
  // [Machine ID handling code removed]
  
  // Version check logic
  if ( MainVersion <= 5729 && MainVersion )
  {
    // Handle old version
    // [Old version code removed]
    return 0LL;
  }
  
  // Validation check
  if ( *(_DWORD *)(*((_QWORD *)this + 139) + 4LL) || *(int *)(*((_QWORD *)this + 141) + 4LL) > 50 )
  {
    if ( *((int *)*v13 + 1) < 18 || *(int *)(*((_QWORD *)this + 141) + 4LL) < 40 )
      return 0LL;
      
    // Perform license validation
    RegCrypt::CheckActivation(&v58, v41, v42, v40, &v65, 0LL);
    
    // [Memory cleanup code removed]
    
    return 1LL;  // This is the key return value for license validation success
  }
  
  // License validation failed path
  QPDFSettings::Remove((QPDFSettings *)v67, v34);
  
  // Log error
  Logger::LogMessage(v47, v50, v51);
  
  // [Memory cleanup code removed]
  
  return 0LL;  // Return failure
}

This function:

  1. Resets license flag: *((_BYTE *)this + 100) = 0
  2. Reads stored license code via GeneralSettings::GetRegistrationCode
GeneralSettings::GetRegistrationCode((__int64 *)&v73, (GeneralSettings *)v75, v4);
QString::trimmed_helper(&v74, &v73, v5);
*((QString *)this + 138) = v74;
  1. Reads license server IP and port
  2. Checks if license is concurrent
  3. Removes activation code if license is concurrent
  4. Retrieves and validates activation code
  5. Loads machine ID
  6. Performs validation using RegCrypt::CheckActivation
  7. If validation fails, removes stored registration and activation codes

I attempted to patch (the second return 1LL;): assembly3.png

74 00 00 36 20 00 80 52 6E 00 00 14 E8 00 80 52Original: TBZ W20, #0, loc_1000783281F 20 03 D5 20 00 80 52 6E 00 00 14 E8 00 80 52Patched: NOP (No Operation)

MainWindow::on_actionRegister_triggered

This function handles the “Register” button click:

  1. Opens registration dialog
  2. Updates program’s registration state
  3. Triggers license validation
void __fastcall MainWindow::on_actionRegister_triggered(MainWindow *this)
{
  // Initial setup and flags
  if ( (*(_BYTE *)(*((_QWORD *)this + 5) + 10LL) & 1) != 0 )
    *((_BYTE *)this + 101) = 1;
    
  // Create and show registration dialog
  v2 = (RegDialog *)operator new(0xE8uLL);
  RegDialog::RegDialog(v2, this);
  RegDialog::SetRegProgram(v2, *((_BYTE *)this + 97), (const QString *)this + 143);
  (*(void (__fastcall **)(RegDialog *))(*(_QWORD *)v2 + 424LL))(v2);
  
  // Get registration status from dialog
  v14 = 0;
  RegProgram = RegDialog::getRegProgram(v2, &v14);
  *((_BYTE *)this + 97) = RegProgram;  // Set validity flag
  *((_BYTE *)this + 98) = RegProgram;  // Set license flag
  
  // Update window title
  QString::operator=((char *)this + 1088, &qword_101D78C40);
  
  // Handle unregistered state
  if ( *((_BYTE *)this + 97) || *((_BYTE *)this + 391) )
    goto LABEL_17;
    
  // [Unregistered title update code removed]
  
LABEL_17:
  // Clean up dialog
  (*(void (__fastcall **)(RegDialog *))(*(_QWORD *)v2 + 32LL))(v2);
  
  // Update window title and document tabs
  MainWindow::SetWindowTitle(this, v8);
  QDocTab::SetRegProgram(*((QDocTab **)this + 18), *((_BYTE *)this + 97));
  
  // Validate license if newly registered
  if ( *((_BYTE *)this + 97) )
  {
    if ( v14 )
    {
      *((_BYTE *)this + 98) = 0;  // Reset license flag
      MainWindow::ReadReg(this);  // Read registration info
      MainWindow::ValidateLicense(this, v10);  // Validate license
    }
  }
}

Key code section to patch:

RegProgram = RegDialog::getRegProgram(v2, &v14);
*((_BYTE *)this + 97) = RegProgram;
*((_BYTE *)this + 98) = RegProgram;

assembly2.png

I modified:

1B 86 09 94BL RegDialog::getRegProgram60 86 01 39STRB W0, [X19,#0x61]60 8A 01 39STRB W0, [X19,#0x62]75 02 11 91ADD X21, X19, #0x440

Patched to:

20 00 80 52MOV W0, #160 86 01 39STRB W0, [X19,#0x61]60 8A 01 39STRB W0, [X19,#0x62]75 02 11 91ADD X21, X19, #0x440

MainReg::ReadReg

atomic_uint *__usercall MainReg::ReadReg@<X0>(MainReg *this@<X0>, _QWORD *a2@<X8>)
{
  // [Variables and initial setup removed for clarity]

  // Get registration and activation information
  GeneralSettings::GeneralSettings((GeneralSettings *)v33);
  GeneralSettings::GetRegistrationCode((GeneralSettings *)v33, v5, v6);
  // [Memory management code removed]
  GeneralSettings::GetActivationCode((__int64 *)&v34, (GeneralSettings *)v33);
  // [Memory management code removed]
  
  // Load machine ID for validation
  LoadMachineID(&v31[4]);
  *(_DWORD *)v31 = -1;
  
  // Check if registration/activation codes are valid length
  if ( d[1] >= 18 && v10[1] >= 19 )
  {
    // Prepare parameters for validation
    v30.d = d;  // Registration code
    v29 = *(atomic_uint **)&v31[4];  // Machine ID
    v28 = v10;  // Activation code
    
    // Validate the registration
    RegCrypt::CheckActivation(&v30, v14, v15, v13, (const QString *)v31, 0LL);
    
    // [Memory cleanup code removed]
    
    // Store result and return
    *a2 = *(_QWORD *)&v31[4];
    *(_QWORD *)&v31[4] = &QArrayData::shared_null;
  }
  else
  {
    // Log error if codes don't meet length requirements
    v19 = (Logger *)*((_QWORD *)this + 9);
    v27 = (atomic_uint *)QString::fromAscii_helper((QString *)"Registration", (const char *)0xC, v12);
    QString::number(&v34, (QString *)*(unsigned int *)v31, 10, v20);
    QString::fromUtf8_helper(&v26, (QString *)"Error: REG0", (const char *)0xB, v21);
    QString::append(&v26, &v34);
    Logger::LogMessage(v19, v22, v23);
    
    // [Memory cleanup code removed]
    
    *a2 = &QArrayData::shared_null;
  }
  
  // [Final memory cleanup removed]
  
  return result;
}

This function:

  1. Retrieves registration and activation codes
  • Calls GeneralSettings::GetRegistrationCode
  • Calls GeneralSettings::GetActivationCode
  • Calls LoadMachineID
  1. Validates registration using RegCrypt::CheckActivation
  2. Handles validation results

Potentially patchable section:

RegCrypt::CheckActivation(&v30, v14, v15, v13, (const QString *)v31, 0LL);
    v16 = v28;
    if ( *v28 != -1 )
    {
      if ( *v28 )
      {
        if ( atomic_fetch_add(v28, 0xFFFFFFFF) != 1 )
          goto LABEL_29;
        v16 = v28;
      }
      QArrayData::deallocate(v16, 2LL, 8LL);
    }

assembly4.png

20 05 00 36 E0 0F 40 F9 08 00 40 B9 1F 05 00 31BL RegCrypt::CheckActivation; TBZ W0, #0, loc_100392FF0

Could patch with:

E0 0F 40 F9MOV W0, #1

or

1F 20 03 D5NOP (No Operation)

Additional functions to analyse later

  • TrialMessage::Setup: Sets up trial message UI with watermark and “Register” button
  • MainWindow::MainWindow: Initializes main window
  • MainReg::RegFinished: Handles registration completion
  • RegCrypt::CheckActivation: Core validation function

Additional patch

I didn’t like the Incorrect registration code. even if the random code was working so I changed :

00496E636F727265637420726567697374726174696F6E20636F64652E00
to
00637261636B65642062792061717A73203A290000000000000000000000

Working for now

Activation check bypassed :)

Edit: 28-06-2025

Simplified activation bypass

After further analysis, I discovered a more elegant approach to bypass the activation check entirely by targeting the MainWindow::ReadReg function, which serves as the central validation point for all registration logic.

Understanding the registration flow

The registration validation follows this critical path:

  1. MainWindow::on_actionRegister_triggered → handles registration button click
  2. MainWindow::ReadReg → validates stored license information
  3. RegCrypt::CheckActivation → performs cryptographic validation

Rather than patching multiple validation points, we can intercept the flow at MainWindow::ReadReg, which returns:

  • 1 for valid registration
  • 0 for invalid/missing registration

Function analysis: MainWindow::ReadReg

This function performs comprehensive license validation:

__int64 __fastcall MainWindow::ReadReg(MainWindow *this)
{
  // Reset license validation flag
  *((_BYTE *)this + 100) = 0;
  
  // Load registration settings
  GeneralSettings::GeneralSettings((GeneralSettings *)v75);
  MainVersion = GeneralSettings::GetMainVersion((GeneralSettings *)v75);
  
  // Retrieve stored codes
  GeneralSettings::GetRegistrationCode((__int64 *)&v73, (GeneralSettings *)v75, v4);
  GeneralSettings::GetLicenseServerIp((__int64 *)&v74, (GeneralSettings *)v75, v9);
  GeneralSettings::GetActivationCode((__int64 *)&v73, (GeneralSettings *)v75);
  
  // Check for concurrent licensing
  if ( !*(_DWORD *)(*(_QWORD *)v12 + 4LL) )
    goto LABEL_47;
    
  IsConcurrent = RegCrypt::IsConcurrent((RegCrypt *)&v74, v17);
  
  // Core validation logic
  if ( *(int *)(*(_QWORD *)v12 + 4LL) >= 18 && *(int *)(*((_QWORD *)this + 139) + 4LL) >= 40 )
  {
    // Prepare validation parameters
    v66 = *(atomic_uint **)v12;        // Registration code
    v65 = (atomic_uint *)*((_QWORD *)this + 141);  // Machine ID
    v64 = (atomic_uint *)*((_QWORD *)this + 139);  // Activation code
    
    // Perform cryptographic validation
    v43 = RegCrypt::CheckActivation((RegCrypt *)&v66, v40, v41, v39, &v73, 0, v29);
    
    if ( (v43 & 1) != 0 )
      return 1;  // ← TARGET: Force return 1 here
  }
  
  // Cleanup and error handling
  LODWORD(v73.d) = 7;
  QPDFSettings::Remove((QPDFSettings *)v75, v48);  // Remove invalid codes
  
  return 0;  // Registration failed
}

The elegant solution

ida1.png

Instead of letting it execute, I overwrote the very first instructions of the ARM64 slice to just return 1:

FF 03 03 D1SUB SP, SP, #0xC0F8 5F 08 A9STP X24, X23, [SP,#0x80]F6 57 09 A9STP X22, X21, [SP,#0x90]

Patched to:

20 00 80 52MOV W0, #1C0 03 5F D6RETF6 57 09 A9STP X22, X21, [SP,#0x90] (unchanged)

Since this is a Universal Binary, I had to do the same for the Intel x64 slice. Stripped symbols made finding the equivalent function annoying, but I traced it by following cross-references from the registration button handler (case 272 in a switch statement) and comparing the disassembly structure.as stripped symbols, making function identification more complex.

Locating the intel function

To find the equivalent function in the Intel binary:

  1. Follow the call chain: MainWindow::on_actionRegister_triggered calls MainWindow::ReadReg xrefs.png
  2. Use cross-references: The registration button handler is called from a switch statement (case 272) xrefs2.png xrefs3.png
  3. Pattern matching: Compare disassembly structure between ARM64 and Intel versions

The Intel equivalent function sub_1000A7B00 can be identified by:

  • Similar call patterns to settings functions
  • Identical string references
  • Matching control flow structure Function comparison between ARM64 and Intel binaries

I grabbed the hex signature of the Intel prologue and applied the exact same logic:

55push rbp48 89 E5mov rbp, rsp41 57push r1541 56push r14

Patched to:

B8 01 00 00 00mov eax, 1C3ret41 56push r14 (unchanged)

Clean, single-point failure elimination. No risk of partial validation bypass.

Edit: 10-07-2026

On January 14th, a friend messaged me saying the patch broke. I checked, and sure enough, it was dead. The developers completely rewrote the registration system. I dug into it and found a new way to bypass it, but didn’t bother making a new edit for it, but since someone asked for it, here is it now !

What changed in version 5.9.94

The old MainWindow::ReadReg function we were targeting? Gone. It was replaced by a MainReg singleton class that handles licensing through a proper service layer.

Here is the new ValidateLicense (v5.9.98) pseudocode from IDA:

Details
void __fastcall MainWindow::ValidateLicense(MainWindow *this)
{
  // Get registration code storage from MainReg singleton
  MainReg::GetRegistrationCode(&v15, MainReg::GetInstance());
  size = v15.d.size;
  if ( v15.d.d && atomic_fetch_add(v15.d.d, 0xFFFFFFFF) == 1 )
    QArrayData::deallocate(v15.d.d, 2, 8);

  // Guard check: if we have a code AND validation not in progress
  if ( size && !*((_BYTE *)this + 107) )
  {
    *((_BYTE *)this + 107) = 1;  // Mark validation in progress

    MainReg::ReloadLicenseData(MainReg::GetInstance());

    // Connect signals: ForceConcurrentShutdown & activateFinished
    QObject::connectImpl(..., MainReg::ForceConcurrentShutdown, ..., this, MainWindow::ConcurrentShutdown, ...);
    QObject::connectImpl(..., MainReg::activateFinished, ..., this, MainWindow::Check_for_Validation_Finished, ...);

    // Check central server activation
    if ( !MainReg::IsCentralServerActivationValid(MainReg::GetInstance(), &errorCode)
      || MainReg::IsConcurrent(MainReg::GetInstance()) )
    {
      if ( errorCode == 17 || errorCode == 13 )
      {
        MainReg::InvalidateLicense(MainReg::GetInstance());
        MainReg::GetRegistrationCode(&v15, MainReg::GetInstance());
        LoadMachineID(&machineID);
        MainReg::ActivateStart(MainReg::GetInstance(), &v15, &machineID);
        Logger::LogMessage(..., "Activation", "Updating license info due to invalid version.");
      }
      return;
    }

    // Subscription check
    if ( MainReg::IsSubscription(MainReg::GetInstance()) )
    {
      if ( MainReg::IsSubscriptionExpired(MainReg::GetInstance()) )
      {
        MainWindow::Check_for_Validation_Finished(this, 3);
        return;
      }
      // Subscription valid, signal result=0
      MainWindow::Check_for_Validation_Finished(this, 0);
    }
    else
    {
      // Non-subscription: compare expiry date
      QDateTime::fromString(&expiryDate, "yyyy-MM-dd", ...);
      MainReg::GetDateExpired(&storedDate, MainReg::GetInstance());
      if ( QDateTime::precedes(&storedDate, &expiryDate) )
        MainWindow::Check_for_Validation_Finished(this, 2);
      else
        MainWindow::Check_for_Validation_Finished(this, 0);
    }

    // Date-based re-activation logic
    QDateTime::currentDateTime(&now, ...);
    QDateTime::addDays(&weekFromNow, &now, 7);
    MainReg::GetDateExpired(&storedDate, MainReg::GetInstance());
    if ( QDateTime::precedes(&storedDate, &weekFromNow) )
    {
      // If last activation was more than 1 day ago, schedule re-activation
      GeneralSettings::GetLastActivatedFromCentralServer(&lastActivated);
      QDateTime::addDays(&nextDay, &lastActivated, 1);
      if ( QDateTime::precedes(&nextDay, &now) )
        shouldReactivate = 1;
    }

    if ( shouldReactivate )
    {
      MainReg::GetRegistrationCode(&v15, MainReg::GetInstance());
      LoadMachineID(&machineID);
      MainReg::ActivateStart(MainReg::GetInstance(), &v15, &machineID);
      Logger::LogMessage(..., "Activation", "Updating license info.");
    }
  }
}

Stripping away the Qt noise, the core logic is:

void MainWindow::ValidateLicense() {
    QString regCode = MainReg::GetInstance()->GetRegistrationCode();

    if (regCode.length > 0 && !this->field_107) {
        this->field_107 = 1; // Guard flag

        MainReg::GetInstance()->ReloadLicenseData();

        if (MainReg::IsCentralServerActivationValid(&errorCode)
            && !MainReg::IsConcurrent()) {
            // Subscription checks...
        }

        this->field_105 = MainReg::IsValidLicense();
        QDocTab::SetRegProgram(qdoctab, this->field_105);
    }
}

Here are the key changes:

  • ReadReg is dead and now replaced by MainReg::ReloadLicenseData() which populates the singleton’s internal state
  • New guard flag: instead of this+98, it now checks this+107
  • Central server validation: MainReg::IsCentralServerActivationValid() runs before anything else
  • Subscription checks: there’s now a subscription path with expiry dates baked in
  • No single return point: the old return 1 patch targeted ReadReg which simply doesn’t exist anymore

Why binary patching is dead

The old approach of overwriting the first bytes of ReadReg with MOV W0, #1; RET worked because there was one function that returned 0 or 1 and everything downstream respected that return value. In the new architecture, validation is spread across multiple MainReg methods and there’s no single choke point you can patch without breaking something.

You could try patching MainReg::IsValidLicense() to always return 1, but you’d still fail the early this->field_99 guard check, and the central server validation runs before IsValidLicense is even called..

New approach: dylib injection with tinyhook

Instead of patching the binary on disk, I switched to runtime inline hooking using tinyhook.

If you aren’t familiar, DYLD_INSERT_LIBRARIES tells dyld to load a custom dynamic library before the app’s code executes (essentially macOS’s LD_PRELOAD). Inline hooking goes a step further: we find the target function in memory, save the original bytes, overwrite the prologue with a jump to our hook, and execute our logic instead. tinyhook handles the ARM64 branch calculation, W^X memory permissions, and thread safety automatically.

Targeting ValidateLicense

The ValidateLicense function at 0x10034d4f0 (5.9.98) is the perfect hook target because it’s the single entry point for all license validation, it’s called on startup, on registration button click, and on window activation. xrefs4.png

mess.png

Instead of patching the return value of a downstream function, I intercept the validation itself and short-circuit it.

Here’s the hook implementation I came up with:

void hooked_ValidateLicense(void *self) {
    // self+107 = validation-in-progress flag (prevents re-entry)
    *((uint8_t *)self + 107) = 1;
    
    // self+105 = license-valid flag  
    *((uint8_t *)self + 105) = 1;
    
    // Call QDocTab::SetRegProgram to update UI
    // self+184 (slot 23) = QDocTab pointer
    void **qdoctab = (void **)((char *)self + 184);
    if (qdoctab && *qdoctab) {
        SetRegProgram(*qdoctab, YES);
    }
}

Dealing with ASLR

Because macOS uses ASLR, the IDA static address isn’t the runtime address. I resolve the image slide using _dyld_get_image_vmaddr_slide():

int idx = imageIndex("Master PDF Editor");
intptr_t slide = _dyld_get_image_vmaddr_slide(idx);
void *runtimeAddress = (void *)(0x10034d4f0 + slide);

tiny_hook(runtimeAddress, hooked_ValidateLicense, NULL);

Resolving SetRegProgram

I also needed to call QDocTab::SetRegProgram from the hook (at 0x10026c430 in 5.9.98). Since symbol names are mangled in the Qt binary, I resolved using the exact same slide calculation:

uintptr_t kSetRegProgram = 0x10026c430;
intptr_t slide = _dyld_get_image_vmaddr_slide(imageIndex);
fn_SetRegProgram = (void *)(kSetRegProgram + slide);

The final implementation

Here is the complete dylib source code:

#import <Foundation/Foundation.h>
#import <mach-o/dyld.h>
#include "tinyhook.h"

#define kValidateLicenseAddress 0x10034D4F0
#define kSetRegProgramAddress   0x10026C430
#define kGuardFlagOffset        107
#define kLicenseFlagOffset      105
#define kQDocTabPointerOffset   184

static void (*setRegProgram)(void *, BOOL);

static void hooked_ValidateLicense(void *self) {
    *((uint8_t *)self + kGuardFlagOffset) = 1;
    *((uint8_t *)self + kLicenseFlagOffset) = 1;

    void **qdoctab = (void **)((char *)self + kQDocTabPointerOffset);
    if (qdoctab && *qdoctab) {
        setRegProgram(*qdoctab, YES);
    }
}

static int imageIndex(const char *name) {
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const char *path = _dyld_get_image_name(i);
        const char *last = strrchr(path, '/');
        if (last && strcmp(last + 1, name) == 0) return i;
        if (!last && strcmp(path, name) == 0) return i;
    }
    return -1;
}

__attribute__((constructor))
static void init() {
    @autoreleasepool {
        NSLog(@"[MPE] loaded");

        int idx = imageIndex("Master PDF Editor");
        if (idx < 0) {
            NSLog(@"[MPE] ERROR: image not found");
            return;
        }

        intptr_t slide = _dyld_get_image_vmaddr_slide(idx);

        void *target = (void *)(kValidateLicenseAddress + slide);
        if (tiny_hook(target, hooked_ValidateLicense, NULL) != 0) {
            NSLog(@"[MPE] ERROR: hook failed");
            return;
        }

        setRegProgram = (void (*)(void *, BOOL))(kSetRegProgramAddress + slide);

        NSLog(@"[MPE] ready");
    }
}

To launch the app with the hook:

% DYLD_INSERT_LIBRARIES=/path/to/MasterPDFEditor.dylib '/Applications/Master PDF Editor.app/Contents/MacOS/Master PDF Editor'

(Or you can inject the dylib into the binary with otool or insert_dylib if you prefer a permanent patch.)

When dyld loads the binary, our __attribute__((constructor)) fires, calculates the ASLR slide, and installs the tinyhook. By the time the app’s main() function runs, ValidateLicense is already ours.

The app opens fully registered and all features are enabled. And since it’s a runtime hook rather than a file patch, I don’t have to touch the binary on disk. If they push an update that shifts the addresses, I just update two #define constants.

Here is the git repository with the dylib source code and build instructions: NohamR/MasterPDFEditor-patch

That’s it :p