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:
- 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
- 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)
- 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_deactiveRegDialog::pb_regMainWindow::ValidateLicenseMainReg::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:
- If already licensed (flag at this+98 is set), returns immediately
- Otherwise, sets the license flag (this+98 = 1)
- Reads stored license key using
MainReg::ReadReg - Contacts licensing server via
MainReg::ActivateFromLicenseServer - Sets validity flag (this+97) based on response
- If valid:
- Updates window title
- Handles concurrent licensing if enabled
- If invalid:
- Checks if concurrent using
RegCrypt::IsConcurrent - Generates registration URL
- Validates via network request
- Displays failure message if needed
- Checks if concurrent using
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).

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 bytesHowever, 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:
- Resets license flag:
*((_BYTE *)this + 100) = 0 - Reads stored license code via
GeneralSettings::GetRegistrationCode
GeneralSettings::GetRegistrationCode((__int64 *)&v73, (GeneralSettings *)v75, v4);
QString::trimmed_helper(&v74, &v73, v5);
*((QString *)this + 138) = v74;
- Reads license server IP and port
- Checks if license is concurrent
- Removes activation code if license is concurrent
- Retrieves and validates activation code
- Loads machine ID
- Performs validation using
RegCrypt::CheckActivation - If validation fails, removes stored registration and activation codes
I attempted to patch (the second return 1LL;):

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:
- Opens registration dialog
- Updates program’s registration state
- 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;

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, #0x440Patched 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, #0x440MainReg::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:
- Retrieves registration and activation codes
- Calls
GeneralSettings::GetRegistrationCode - Calls
GeneralSettings::GetActivationCode - Calls
LoadMachineID
- Validates registration using
RegCrypt::CheckActivation - 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);
}

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, #1or
1F 20 03 D5NOP (No Operation)Additional functions to analyse later
TrialMessage::Setup: Sets up trial message UI with watermark and “Register” buttonMainWindow::MainWindow: Initializes main windowMainReg::RegFinished: Handles registration completionRegCrypt::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
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:
MainWindow::on_actionRegister_triggered→ handles registration button clickMainWindow::ReadReg→ validates stored license informationRegCrypt::CheckActivation→ performs cryptographic validation
Rather than patching multiple validation points, we can intercept the flow at MainWindow::ReadReg, which returns:
1for valid registration0for 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

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:
- Follow the call chain:
MainWindow::on_actionRegister_triggeredcallsMainWindow::ReadReg
- Use cross-references: The registration button handler is called from a switch statement (case 272)

- 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

I grabbed the hex signature of the Intel prologue and applied the exact same logic:
55push rbp48 89 E5mov rbp, rsp41 57push r1541 56push r14Patched 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:
ReadRegis dead and now replaced byMainReg::ReloadLicenseData()which populates the singleton’s internal state- New guard flag: instead of
this+98, it now checksthis+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 1patch targetedReadRegwhich 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.


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