I needed to convert some ePub files to PDF for a project, and found a macOS app that does exactly that. Problem is, the demo version slaps an ugly watermark on everything and limits you to 12 conversions. first-launch.png

This is purely educational, I’m sharing the technical details of how the licensing works, not a complete bypass. The app wraps Calibre’s command-line tools, so you could just use Calibre directly anyway.

Finding the Watermark Logic

The first thing that annoyed me was this watermark slapped on every PDF: conversion.png

Getting rid of this seemed like a good starting point. I fired up IDA Pro and searched for the watermark string. Found it pretty quickly in the main conversion function:

int __cdecl -[BookObj runcalibre:dstfile:](BookObj *self, SEL a2, id a3, id a4)
{
  // variable declarations ...
  
  if ( !byte_100062870 )
  {
    // setup code ...
    
    if ( (unsigned int)objc_msgSend(v9, "isEqualToString:", CFSTR("pdf")) )
    {
      v10 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)qword_100062840, "lowercaseString"));
      v11 = objc_retainAutoreleasedReturnValue(+[NSString stringWithFormat:](&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("--paper-size=%@"), v10));
      
      // Here's the license check
      if ( ((unsigned __int8)objc_msgSend((id)qword_100062850, "isreg") & 1) != 0 )
        v12 = +[NSArray arrayWithObjects:](&OBJC_CLASS___NSArray, "arrayWithObjects:", v6, v7, 0, v17, v18);
      else
        // Demo version - adds the watermark
        v12 = +[NSArray arrayWithObjects:](
                &OBJC_CLASS___NSArray,
                "arrayWithObjects:",
                v6,
                CFSTR("--pdf-footer-template"),
                CFSTR("\"<div>******ebook converter DEMO Watermarks*******</div>\""),
                v7,
                0);
                
      // Calls calibre's ebook-convert with the arguments
      objc_msgSend(v14, "setLaunchPath:", CFSTR("/Applications/calibre.app/Contents/MacOS/ebook-convert"));
      objc_msgSend(v14, "setArguments:", v13);
    }
  }
  return 1;
}

I note that the hardcoded path /Applications/calibre.app/Contents/MacOS/ebook-convert suggests this is macOS-specific software that depends on Calibre being installed in the standard location, and will not run otherwise.

So the app is basically a GUI wrapper around Calibre’s ebook-convert tool. The license check is this line:

if ( ((unsigned __int8)objc_msgSend((id)qword_100062850, "isreg") & 1) != 0 )

If isreg returns true, it runs without the watermark. If false, it adds the --pdf-footer-template argument that creates the ugly watermark.

The License Check Implementation

I found two classes that implement isreg methods:

BuyController (handles purchase logic):

bool __cdecl -[BuyController isreg](BuyController *self, SEL a2)
{
  return self->_isreg;
}

RegController (handles registration UI):

bool __cdecl -[RegController isreg](RegController *self, SEL a2)
{
  return !self->_isreg;  // Notice the negation
}

The global qword_100062850 points to one of these controllers. After some testing, I found that patching BuyController’s isreg method was enough to remove the watermark.

Method Swizzling Solution

Using macOS standard functions it’s quite easy to patch this function to always return a desired value. To patch both -[BuyController isreg] method using Objective-C runtime swizzling, we just have to implement a replacement implementation that returns the desired value and then swizzle the instance method for each class.

Below is the full Objective-C code that achieves this. Note that that kind of patch is cleaner and works across app updates:

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface PatchedLicense : NSObject
- (BOOL)isreg_patched;
@end

@implementation PatchedLicense

+ (void)load {
  Class buyController = NSClassFromString(@"BuyController");
  
  if (buyController) {
    Method original = class_getInstanceMethod(buyController, @selector(isreg));
    Method patched = class_getInstanceMethod(self, @selector(isreg_patched));
    method_exchangeImplementations(original, patched);
    NSLog(@"[+] Patched BuyController isreg method");
  }
}

- (BOOL)isreg_patched {
  return YES; // Always return registered
}

@end

method_exchangeImplementations will swap the implementations of the original class with the methods in PatchedLicense. The +load method runs automatically when the dylib is injected, swapping the original isreg implementation with my patched version.

I compiled this into a dylib and injected it with DYLD_INSERT_LIBRARIES (on a system with SIP turned off) or with otool.

The PDF watermark disappeared immediately, and the app even prompts as registered :p licensed.png

Hex Patching Solution

Note that instead of swizzling the function, another patch could be to hex patch the app, i.e., edit the assembly directly:

100008760 |             | -[BuyController isreg]:
100008760 | C8 02 00 90 |   ADRP            X8, #_OBJC_IVAR_$_BuyController._isreg@PAGE; bool _isreg;
100008764 | 08 35 8B B9 |   LDRSW           X8, [X8,#_OBJC_IVAR_$_BuyController._isreg@PAGEOFF]; bool _isreg;
100008768 | 00 68 68 38 |   LDRB            W0, [X0,X8]
10000876C | C0 03 5F D6 |   RET

On recent macOS systems, binaries are compiled to run on both Intel and ARM - here is the ARM version. For reference, here is the Intel version:

1000066E7 |                      | -[BuyController isreg]:
1000066E7 | 55                   |   push    rbp
1000066E8 | 48 89 E5             |   mov     rbp, rsp
1000066EB | 48 8B 05 EE A3 05 00 |   mov     rax, cs:_OBJC_IVAR_$_BuyController__isreg; char _isreg;
1000066F2 | 0F BE 04 07          |   movsx   eax, byte ptr [rdi+rax]
1000066F6 | 5D                   |   pop     rbp
1000066F7 | C3                   |   retn

We want to patch the function to return 1, let’s do that in assembly! On ARM:

20 00 80 52   // MOV  W0, #0x1
C0 03 5F D6   // RET

This replaces the original 16 bytes (4 instructions) with just 8 bytes. If we want to cleanly NOP the remaining bytes, we can pad with 1F 20 03 D5 twice (NOP instruction in ARM64).

On the Intel side:

B8 01 00 00 00 // mov eax, 1
C3             // ret

To be clean and match the original function size (11 bytes), pad with NOPs 5 times (90).

The Trial Limit Problem

Removing the watermark was just the start. The app still showed this annoying dialog on launch:

first-launch.png

Even with the watermark gone, it would eventually lock me out after 12 conversions. Time to dig deeper.

I found the function that displays this dialog by searching for the string “You can try %d ebooks in demo version”:

void __cdecl -[BuyController openbuywindow:](BuyController *self, SEL a2, id a3)
{
  void *v4 = objc_retainAutoreleasedReturnValue(-[BuyController window](self, "window", a3));
  
  // Calculate remaining trial uses
  LODWORD(v5) = 12 - self->times;
  if ( (int)v5 <= 0 )
    v5 = 0;
  else
    v5 = (unsigned int)v5;
    
  v6 = objc_retainAutoreleasedReturnValue(
         +[NSString stringWithFormat:](
           &OBJC_CLASS___NSString,
           "stringWithFormat:",
           CFSTR("You can try %d ebooks in demo version, %d left.\n\nif you would like to get the full version, please click 'Buy now' button."),
           12,
           v5));
           
  objc_msgSend(self->timeslabel, "setStringValue:", v6);
  objc_msgSend(NSApp, "runModalForWindow:", v4);
  // cleanup code ...
}

The trial limit is tracked in self->times; it starts at 0 and increments with each conversion. The app shows “12 - times” remaining uses.

A simple patch would be to hook the -[BuyController times] method to always return 0, the same way as before, but the program would still show this window on launch and I don’t consider this as fully patched software.

Breaking Down the License Validation

The real license check happens in -[BuyController checkkey]. This is where it gets interesting:

void __cdecl -[BuyController checkkey](BuyController *self, SEL a2)
{
  // Get stored license data
  -[BuyController getkey2](self, "getkey2");
  id ssn = objc_retainAutoreleasedReturnValue(objc_msgSend((id)qword_100062858, "ssn"));
  id skey = objc_retainAutoreleasedReturnValue(objc_msgSend((id)qword_100062858, "skey"));
  
  if ( ssn )
  {
    // Append magic salt to license key
    void *salted = objc_retainAutoreleasedReturnValue(objc_msgSend(skey, "stringByAppendingString:", CFSTR("43534")));
    void *saltedCopy = objc_msgSend(salted, "copy");
    objc_release(salted);
    
    // Compute MD5 hash
    id md5Hash = objc_retainAutoreleasedReturnValue(-[BuyController MD5:](self, "MD5:", saltedCopy));
    void *hashPrefix = objc_retainAutoreleasedReturnValue(objc_msgSend(md5Hash, "substringWithRange:", 0, 16));
    objc_release(md5Hash);
    
    // Check if computed hash matches stored serial number
    self->_isreg = (unsigned __int8)objc_msgSend(ssn, "isEqualToString:", hashPrefix);
    
    objc_release(hashPrefix);
    objc_release(saltedCopy);
  }
  
  self->times = 0;
  if ( !self->_isreg )
  {
    -[BuyController checkver](self, "checkver");
    self->times = -[BuyController loadtimes](self, "loadtimes");
    -[BuyController openbuywindow:](self, "openbuywindow:", self);  // Show trial dialog
  }
  
  objc_release(skey);
  objc_release(ssn);
}

So the license algorithm is pretty simple:

  1. Take the license key (skey)
  2. Append magic salt “43534”
  3. Compute MD5 hash
  4. Take first 16 characters
  5. Compare with stored serial number (ssn)

If they match, _isreg is set to true. Otherwise, it loads the trial counter and shows the purchase dialog.

Key Generation

Since the algorithm is so straightforward, I wrote a quick Python script to generate valid licenses:

import hashlib

def generate_license_pair(license_key):
    salted = license_key + "43534"
    md5_hash = hashlib.md5(salted.encode()).hexdigest()
    serial_number = md5_hash[:16]
    return serial_number

skey = "FAKE-KEY"
print("ssn =", generate_license_pair(skey)) # 7ed501cc30644011

Here is the Objective-C code that achieves the same result using the swizzling method employed previously:

@interface LicenseSpoofer : NSObject
- (NSString *)ssn_patched;
- (NSString *)skey_patched;
@end

@implementation LicenseSpoofer

+ (void)load {
  Class regController = NSClassFromString(@"RegController");
  
  if (regController) {
    Method originalSSN = class_getInstanceMethod(regController, @selector(ssn));
    Method patchedSSN = class_getInstanceMethod(self, @selector(ssn_patched));
    if (originalSSN && patchedSSN) {
      method_exchangeImplementations(originalSSN, patchedSSN);
    }

    Method originalSKey = class_getInstanceMethod(regController, @selector(skey));
    Method patchedSKey = class_getInstanceMethod(self, @selector(skey_patched));
    if (originalSKey && patchedSKey) {
      method_exchangeImplementations(originalSKey, patchedSKey);
    }
    
    NSLog(@"[+] License data spoofed successfully");
  }
}

- (NSString *)ssn_patched {
  return @"7ed501cc30644011";  // Generated serial
}

- (NSString *)skey_patched {
  return @"FAKE-KEY";  // Our custom key
}

@end

With this new dylib injected, the app is now fully spoofed to be activated!

While writing this post, I still wanted to understand how the times counter is initialized. It happens here:

self->times = -[BuyController loadtimes](self, "loadtimes");
int __cdecl -[BuyController loadtimes](BuyController *self, SEL a2)
{
  NSUserDefaults *v3; // x20
  id v4; // x19
  id v5; // x21
  int v6; // w22

  v3 = objc_retainAutoreleasedReturnValue(+[NSUserDefaults standardUserDefaults](&OBJC_CLASS___NSUserDefaults, "standardUserDefaults"));
  v4 = objc_retainAutoreleasedReturnValue(-[BuyController GetTimesKey](self, "GetTimesKey"));
  v5 = objc_retainAutoreleasedReturnValue(-[NSUserDefaults objectForKey:](v3, "objectForKey:", v4));
  v6 = (unsigned int)objc_msgSend(v5, "intValue");
  objc_release(v5);
  objc_release(v4);
  objc_release(v3);
  return v6;
}

The number is loaded here:

v4 = objc_retainAutoreleasedReturnValue(-[BuyController GetTimesKey](self, "GetTimesKey"));
id __cdecl -[BuyController GetTimesKey](BuyController *self, SEL a2)
{
  return +[NSString stringWithFormat:](&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@times"), CFSTR("pdfdrm"));
}

This program uses NSUserDefaults with key “pdfdrmtimes”. This means that users can manually reset their trial by deleting this preference:

~/Library/Preferences/*.pdfdrm.plist

Or set it to 0:

defaults write *.pdfdrm pdfdrmtimes -int 0

Finally, I wanted to fully understand the activation process, the real one, not the swizzled version. I opened Proxyman, a MITM proxy to capture the traffic. On activation, the app makes the following request:

curl 'https://domain.com/download/api/active200.php?id=serial&pid=91' \
-H 'Host: domain.com' \
-H 'Accept: */*' \
-H 'Accept-Language: en-GB,en;q=0.9' \
-H 'Connection: keep-alive' \
-H 'User-Agent: PDF%20ePub%20DRM%20Removal/300 CFNetwork/3826.500.131 Darwin/24.5.0'

The response is the following, base64 encoded:

c3RhdHVzPTQwCg1tc2c9WW91ciBzbiAqZSoqYSogaXMgd3JvbmcsIGNvcHkgYW5kIHBhc3RlIHNuLCBub3QgdHlwZSBpdCwgdHJ5IGFnYWluLgoN
$ echo "c3RhdHVzPTQwCg1tc2c9WW91ciBzbiAqZSoqYSogaXMgd3JvbmcsIGNvcHkgYW5kIHBhc3RlIHNuLCBub3QgdHlwZSBpdCwgdHJ5IGFnYWluLgoN" | base64 -d
status=40
msg=Your sn *e**a* is wrong, copy and paste sn, not type it, try again.

So, status=40 indicates an invalid serial number.

The API endpoint appears twice in the assembly code, in -[RegController RecheckKey] and -[RegController activatew:]:

void __cdecl -[RegController activatew:](RegController *self, SEL a2, id a3)
{
  // Variables declarations removed for clarity

  // Extract username from UI field
  v4 = objc_retainAutoreleasedReturnValue(-[NSTextField stringValue](self->nameedit, "stringValue", a3));
  suser = self->suser;
  self->suser = v4;
  objc_release(suser);
  
  // Extract serial number from UI field
  v6 = objc_retainAutoreleasedReturnValue(-[NSTextField stringValue](self->snedit, "stringValue"));
  ssn = self->ssn;
  self->ssn = v6;
  objc_release(ssn);
  
  // Clean up the serial number (trim whitespace)
  v8 = self->ssn;
  v9 = objc_retainAutoreleasedReturnValue(
         +[NSCharacterSet whitespaceAndNewlineCharacterSet](
           &OBJC_CLASS___NSCharacterSet,
           "whitespaceAndNewlineCharacterSet"));
  v10 = objc_retainAutoreleasedReturnValue(-[NSString stringByTrimmingCharactersInSet:](v8, "stringByTrimmingCharactersInSet:", v9));
  v11 = self->ssn;
  self->ssn = v10;
  objc_release(v11);
  objc_release(v9);
  
  // Build activation URL with serial number and product ID (91)
  v12 = objc_retainAutoreleasedReturnValue(
          +[NSString stringWithFormat:](
            &OBJC_CLASS___NSString,
            "stringWithFormat:",
            CFSTR("https://domain.com/download/api/active200.php?id=%@&pid=%@"),
            self->ssn,
            CFSTR("91")));
  
  // Make HTTP request to activation server
  v13 = objc_retainAutoreleasedReturnValue(+[NSURL URLWithString:](&OBJC_CLASS___NSURL, "URLWithString:", v12));
  v14 = objc_retainAutoreleasedReturnValue(+[NSData dataWithContentsOfURL:](&OBJC_CLASS___NSData, "dataWithContentsOfURL:", v13));
  objc_release(v13);
  
  // Handle network failure
  if ( !v14 )
  {
    -[RegController manaulbtnclick:](self, "manaulbtnclick:", 0);  // Manual activation fallback
    v16 = 0;
    goto LABEL_6;
  }
  
  // Process server response and save license file
  v15 = objc_msgSend(objc_alloc((Class)&OBJC_CLASS___NSString), "initWithData:encoding:", v14, 4);
  -[RegController writekeyfile:](self, "writekeyfile:", v15);  // Save to converter.dat
  v16 = objc_retainAutoreleasedReturnValue(-[RegController loadkeyfile](self, "loadkeyfile"));  // Load it back
  objc_release(v15);
  
  // Validate the license data
  if ( -[RegController validsn:](self, "validsn:", v16) == 10 )  // 10 = Valid license
  {
LABEL_6:
    // SUCCESS: Close registration window and show success message
    v19 = objc_retainAutoreleasedReturnValue(-[RegController window](self, "window"));
    objc_msgSend(v19, "close");
    objc_release(v19);
    objc_msgSend(NSApp, "stopModal");
    
    // Show success dialog and quit app
    v17 = objc_msgSend(objc_alloc((Class)&OBJC_CLASS___NSAlert), "init");
    objc_msgSend(v17, "setAlertStyle:", 0);
    objc_msgSend(v17, "setMessageText:", self->activatemsg);
    v20 = objc_unsafeClaimAutoreleasedReturnValue(objc_msgSend(v17, "addButtonWithTitle:", CFSTR("Quit")));
    objc_msgSend(v17, "runModal");
    objc_msgSend(NSApp, "terminate:", self);
    goto LABEL_7;
  }
  
  // FAILURE: Show error message if license data exists but is invalid
  if ( objc_msgSend(v16, "length") )
  {
    v17 = objc_msgSend(objc_alloc((Class)&OBJC_CLASS___NSAlert), "init");
    objc_msgSend(v17, "setAlertStyle:", 0);
    objc_msgSend(v17, "setMessageText:", self->activatemsg);  // Error message from server
    v18 = objc_unsafeClaimAutoreleasedReturnValue(objc_msgSend(v17, "addButtonWithTitle:", CFSTR("OK")));
    objc_msgSend(v17, "runModal");
LABEL_7:
    objc_release(v17);
  }
  
  // Cleanup
  objc_release(v14);
  objc_release(v16);
  objc_release(v12);
}

This method handles online license activation by contacting the remote server. The flow is as follows:

  • Gets username and serial number from UI fields
  • Makes HTTP request to domain.com activation API
  • Receives license data from server
  • Saves license data to local file (converter.dat) converter.png The license file is then loaded and verified using -[RegController validsn:] (result == 10); once again, this function could be hooked to always return 10.

I took a quick glance at -[RegController validsn2:], which is called by -[RegController validsn:]. For reference:

int __cdecl -[RegController validsn2:](RegController *self, SEL a2, id a3)
{
  // Variables declarations removed for clarity
  int status;

  // Retain the license data string
  v4 = objc_retain(a3);
  
  // Check if license data has minimum length
  if ( (unsigned __int64)objc_msgSend(v4, "length") >= 3 )
  {
    // Clean up newlines and split by carriage returns
    v6 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "stringByReplacingOccurrencesOfString:withString:", CFSTR("\n"), &stru_100055D58));
    objc_release(v4);
    v7 = objc_retainAutoreleasedReturnValue(objc_msgSend(v6, "componentsSeparatedByString:", CFSTR("\r")));
    
    // If only one component, license is malformed
    if ( objc_msgSend(v7, "count") == (void *)1 )
    {
      status = 1;  // Invalid license
      goto LABEL_26;
    }
    
    // Parse license fields from key-value pairs
    v8 = objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("status")));
    self->status = (unsigned int)objc_msgSend(v8, "intValue");
    
    v9 = objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("orderstat")));
    objc_release(v8);
    if ( objc_msgSend(v9, "length") )
      self->orderstat = (unsigned int)objc_msgSend(v9, "intValue");
    
    // Extract product ID
    v10 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("pid")));
    pid = self->pid;
    self->pid = v10;
    objc_release(pid);
    
    // Extract license duration
    v12 = objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("licenseday")));
    objc_release(v9);
    self->licenseday = (unsigned int)objc_msgSend(v12, "intValue");
    
    // Extract check date and activation message
    v13 = objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("check")));
    objc_release(v12);
    v14 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("msg")));
    activatemsg = self->activatemsg;
    self->activatemsg = v14;
    objc_release(activatemsg);
    
    // Convert check date and get current date
    v16 = objc_retainAutoreleasedReturnValue(-[RegController strtodate:](self, "strtodate:", v13));
    v17 = objc_retainAutoreleasedReturnValue(+[NSDate date](&OBJC_CLASS___NSDate, "date"));
    NSLog(&CFSTR("%@").isa, v13);
    
    // Handle different license types
    if ( self->licenseday < 11 )
    {
      // PERMANENT LICENSE: No time restrictions
      v26 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("sn")));
      ssn = self->ssn;
      self->ssn = v26;
      objc_release(ssn);
      
      v28 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("key")));
      skey = self->skey;
      self->skey = v28;
      v18 = v13;
    }
    else
    {
      // TRIAL LICENSE: Time-limited validation
      v18 = objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("orderdate")));
      objc_release(v13);
      
      // Parse order date
      v19 = (NSDate *)objc_retainAutoreleasedReturnValue(-[RegController strtodate:](self, "strtodate:", v18));
      orderdate = self->orderdate;
      self->orderdate = v19;
      objc_release(orderdate);
      
      // Calculate days since order (absolute value)
      v21 = -[RegController daysBetween:and:](self, "daysBetween:and:", v17, self->orderdate);
      v22 = (v21 >= 0) ? v21 : -v21;
      
      // Calculate days since check date (absolute value)
      v23 = -[RegController daysBetween:and:](self, "daysBetween:and:", v17, v16);
      v24 = (v23 >= 0) ? v23 : -v23;
      
      self->TrialDaysTotal = v24;
      
      // Mark as expired if past license period
      if ( v22 > self->licenseday )
        self->TrialDaysTotal = 999;  // Expiration marker
      
      licenseday = self->licenseday;
      
      // Check if license is still valid or within grace period
      if ( v22 <= licenseday )
      {
        // Within license period, check for grace period expiry
        if ( v22 > licenseday + 40 )  // 40-day grace period
        {
          status = 50;  // Expired
          goto LABEL_25;
        }
      }
      else
      {
        // Past license period, check additional conditions
        status = 50;  // Default to expired
        if ( self->orderstat > 49 || v22 > licenseday + 40 )
          goto LABEL_25;
      }
      
      // License is valid, extract credentials
      v30 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("sn")));
      v31 = self->ssn;
      self->ssn = v30;
      objc_release(v31);
      
      v32 = (NSString *)objc_retainAutoreleasedReturnValue(-[RegController getkeyvalue:key:](self, "getkeyvalue:key:", v7, CFSTR("key")));
      skey = self->skey;
      self->skey = v32;
    }
    
    objc_release(skey);
    status = self->status;  // Return the parsed status
    
LABEL_25:
    objc_release(v17);
    objc_release(v16);
    objc_release(v18);
LABEL_26:
    objc_release(v7);
    v4 = v6;
    goto LABEL_27;
  }
  
  status = 1;  // Invalid/malformed license
LABEL_27:
  objc_release(v4);
  return status;
}

Since the network request failed (I don’t have a valid license), I can only assume the license file format based on the analysis of this function. This method implements a sophisticated license validation system that handles both permanent licenses and time-limited trials. The license data uses a key-value format separated by carriage returns (\r), containing fields such as: status, orderstat, pid, licenseday, check, msg, orderdate, sn, key It appears that there are two license types:

  1. Permanent License (licenseday < 11)
  • No expiration checks
  • Immediate acceptance
  1. Trial License (licenseday >= 11)
  • Uses both orderdate and check dates
  • Computes number of days since order
  • Adds 40-day grace period
  • Marks as expired (TrialDaysTotal = 999) if exceeded

The function then returns:

  • 1: Invalid/malformed license
  • 10: Valid license (from previous analysis)
  • 50: Expired or blocked license

I haven’t bothered creating a fake license file since the previous activation method is quite straightforward.

Conclusion

This macOS app’s licensing system is surprisingly simple to bypass. The watermark removal requires just patching a single isreg method, while full activation can be achieved through method swizzling to spoof license credentials. The MD5-based validation with a hardcoded salt makes key generation trivial. For educational purposes, this demonstrates common weaknesses in client-side license enforcement, but honestly, just use Calibre directly instead :p