sketch.appmacososxreverseengineeringghidra night


It is a wonderful day. A Valentine to precise (14/02/2020). I've spent most of the day with my waifu, enjoying little bit of snow and a cup of coffee. Exchanging each other our gifts (just like in the days we were younger). Therefore, this post is also contribution to her.

Dedicated to my wife, Ara.

Inhere, I will show you how easily is to bypass a SketchApp Trial, using nothing more but a Ghidra SRE [1], and a little bit of thinkering. I'm writing this for educational purpose only, but we all have to admit Sketch.app is a bit too expensive. The version I'm working on is Sketch v63.1 (latest update).

Warming up

I highly advice you to create a backup of your Sketch.app (later refered only as Sketch) executable. The usual location for this executable is in /Applications/Sketch.app/Contents/MacOS. The only thing you have to do is copy Sketch in the same directory with a different name.

Its always good to backup

Open up Ghidra and create a new project importing Sketch executable from above views. To import, just drag your executable to project view in Ghidra. Refer to Ghidra docs or this [2] phenomenal video by Ghidra Ninja to learn more about Ghidra basics. This will ease up your reverse engineering if you need to stop and continue to work later on. Now double click on the Sketch from Ghidra project three and let Ghidra analyze the project in full (may take a couple of minutes).

Sketch.App in Ghidra

Finding the Trial implementation

There are different ways to find where the trial implementation takes a place. The first and foremost is using XREF string trial days remaining which is shown while starting up the Sketch. The other way is searching for string BCLicenseManager in Symbol Tree window. Since the above string is directly referencing to method numberOfDaysLeftInTrialMode, we can also search for that string excatly in the same window.

Method search in Ghidra Project Tree

When we point Ghidra to this function, we can see the next pseudo-code; which accepts two parameters. The interesting parameter is param_1. The strict business of this parameter is to refer which kind of license is in use. Two options are available in Sketch, if you follow the references: BCRegularLicense, and BCCloudLicense. One is used for offline activation, and the other is used for cloud-based activation. So this BCLicenseManager class has license selector, that returns some license instance.

long_long numberOfDaysLeftInTrialMode(ID param_1,SEL param_2)

  puVar1 = _objc_msgSendSuper2;
  uVar3 = (*(code *)_objc_msgSendSuper2)(param_1,"license");
  uVar3 = _objc_retainBlock(uVar3);
  (*(code *)puVar1)(uVar3,"remainingTimeInterval");
  uVar4 = (*(code *)puVar1)(&_OBJC_CLASS___NSDate,"dateWithTimeIntervalSinceNow:");
  uVar4 = _objc_retainBlock(uVar4);
  (*(code *)_objc_retain)(uVar3);
  uVar3 = (*(code *)puVar1)(&_OBJC_CLASS___NSCalendar,"currentCalendar");
  uVar3 = _objc_retainBlock(uVar3);
  uVar5 = (*(code *)puVar1)(&_OBJC_CLASS___NSDate,"date");
  uVar5 = _objc_retainBlock(uVar5);
  uVar6 = (*(code *)puVar1)(uVar3,"components:fromDate:toDate:options:",0x10,uVar5,uVar4,0);
  uVar6 = _objc_retainBlock(uVar6);
  puVar2 = _objc_retain;
  (*(code *)_objc_retain)(uVar5);
  (*(code *)puVar2)(uVar3);
  lVar7 = (*(code *)puVar1)(uVar6,"day");
  (*(code *)puVar2)(uVar6);
  (*(code *)puVar2)(uVar4);
  return lVar7;

Next, we have a call to function remainingTimeInterval, and later on, a calculation used for using remaining time through currentCalendar and dateWithTimeIntervalSinceNow. If we search for method called first (remainingTimeInterval), we can see we were pretty right about two possible license classes references through BCLicenseManager.

remainingTimeInterval visible in two classes

We will work with-in BCRegularLicense since we don't need to deal with cloud protection and adding stuff to /etc/hosts. Lets see what is inside. We have some interesting functions in there called through in: validityInterval, which basically works with endTime (when license should expire) and combination of networkTime/currentTime. We also have isValid method notifying impl. if license is still available for use.

double remainingTimeInterval(ID param_1,SEL param_2)
  puVar2 = _objc_msgSendSuper2;
  uVar1 = (*(code *)_objc_msgSendSuper2)(param_1,"validityInterval");
  uVar4 = _objc_retainBlock(uVar1);
  uVar1 = (*(code *)puVar2)(uVar4,"endDate");
  uVar1 = _objc_retainBlock(uVar1);
  uVar6 = (*(code *)puVar2)(param_1,"networkTime");
  uVar5 = _objc_retainBlock(uVar6);
  uVar6 = (*(code *)puVar2)(uVar5,"currentDate");
  uVar6 = _objc_retainBlock(uVar6);
  (*(code *)puVar2)(uVar1,"timeIntervalSinceDate:",uVar6);
  puVar2 = _objc_retain;
  (*(code *)_objc_retain)(uVar6);
  (*(code *)puVar2)(uVar5);
  (*(code *)puVar2)(uVar1);
  (*(code *)puVar2)(uVar4);
  cVar3 = (*(code *)_objc_msgSendSuper2)(param_1,"isValid");
  auVar7 = ZEXT816(0);
  if (cVar3 != '\0') {
    auVar7 = ZEXT816(extraout_XMM0_Qa);
  auVar7 = maxsd(auVar7,ZEXT816(0));
  return SUB168(auVar7,0);

Lets see what other methods are available in this class named BCRegularLicense. First, filter the Symbol Tree window to reflect the name and once found, scroll to method_list_t where you will right click on it and use Show Reference To.

List Method for Class

If you scroll down a bit in a Assembly View window, you will find isExpired listed. Lets see what is inside.

isExpired Method in BCRegularLicense

char isExpired(ID param_1,SEL param_2)

  if (lVar4 == 0) {                                            // expired
    bVar6 = true;
  else {                                                                // yet to expired
    uVar3 = (*(code *)_objc_msgSendSuper2)(param_1,"networkTime");
    uVar3 = _objc_retainBlock(uVar3);
    uVar5 = (*(code *)puVar1)(uVar3,"currentDate");
    uVar5 = _objc_retainBlock(uVar5);
    cVar2 = (*(code *)puVar1)(lVar4,"containsDate:",uVar5);
    puVar1 = _objc_retain;
    bVar6 = cVar2 == '\0';
    (*(code *)_objc_retain)(uVar5);
    (*(code *)puVar1)(uVar3);
  (*(code *)_objc_retain)(lVar4);
  return (char)bVar6;

We basically have a simple method to check if the trial is expired or not.

Pathching it up

If we follow the about bVar6, it can be either true for expired license, or false for unexpired license. Go to this method in Ghidra Listing (Dissasemble) and find a ASM function which moves value of 0x1 (true) to R12B (bVar6), at address 0x1004a2a50.

1004a2a4b 41 ff d5        CALL       R13=>__stubs::_objc_release                   undefined _objc_release()
1004a2a4e eb 03           JMP        LAB_1004a2a53
                           LAB_1004a2a50                                   XREF[1]:     1004a29ec(j)  
1004a2a50 41 b4 01        MOV        R12B,0x1

To patch your executable, right click on the instruction on this address and click Patch Instruction, or rather you can select the address and press keyboard shortcut Shift+Command+G. Patch this instruction to always return 0x0 (false), meaning the trial will never expire. See below picture for patched ASM instruction.

isExpired Patching

At address 0x1004a2a4e we see initial JMP instruction which jumps (goto) checking procedure. We need to patch this instruction to jump to our patch at 0x1004a2a50. The final code assembly looks like this.

1004a2a48 4c 89 ff        MOV        param_1,R15
1004a2a4b 41 ff d5        CALL       R13=>__stubs::_objc_release                      undefined _objc_release()
1004a2a4e eb 00           JMP        LAB_1004a2a50                                    Jump to return `false` instruction   -+
                     LAB_1004a2a50                                   XREF[2]:         .......................               |
1004a2a50 41 b4 00        MOV        R12B,0x0                                         Always return `false` on isExpired  <-+
1004a2a53 4c 89 f7        MOV        param_1,R14
1004a2a56 ff 15 ec        CALL       qword ptr [->__stubs::_objc_release]             undefined _objc_release()
         68 12 00

Bypassing Sketch Code Signature

We are not yet done. The Sketch tries to be smart on us; it checks code signature, meaning if the code signature is not valid, it will exit upon running. Since we patched the binary, the signature of the app will be invalid. But similary to other anti-crack techniques, this one is easy to tackle down.

While inside your Ghidra project, go to 0x1004a1724 in Dissasemble view, and you will see this code.

1004a1736 85 db           TEST       EBX,EBX
1004a1738 0f 85 58        JNZ        LAB_1004a1896
         01 00 00

Update: I was asked by a fellow follower (@kiwamizamurai) on my Twitter post, how I've found this address.

Twitter Question

I've found this address by setting up a breakpoint just before the exit syscall and then going step by step in dissasembler. If you try to open up Sketch.app with bad signature you'd get a system error with BAD_CODE_SIGNATURE code. Therefore, I knew the error was due to bad signature. Then I checked which instruction reference to this call.

Anyway, At the address 0x1004a1738, is instruction JNZ (Jump not equal), which calls code signature method and exit the Sketch. Just replace this jump to the next instruction at 0x1004a173e.

Sketch.App Code Signature #1

 1004a1736 85 db           TEST       EBX,EBX
 1004a1738 0f 85 00        JNZ        LAB_1004a173e
           00 00 00
                       LAB_1004a173e                                   XREF[1]:     1004a1738(j)  
 1004a173e 49 89 c7        MOV        R15,RAX                                       Jumps here
 1004a1741 4d 89 f4        MOV        R12,R14

Likewise, edit JZ (Jump equal) at address 0x1004a1879 to instruction JNZ.

                     LAB_1004a186b                                   XREF[1]:     1004a180f(j)  
1004a186b 4c 89 ff        MOV        RDI,R15
1004a186e 41 ff d6        CALL       R14=>__stubs::_objc_release                      undefined _objc_release()
1004a1871 4c 89 ef        MOV        RDI,R13
1004a1874 41 ff d6        CALL       R14=>__stubs::_objc_release                      undefined _objc_release()
1004a1877 84 db           TEST       BL,BL
1004a1879 74 1b           JZ         LAB_1004a1896                                            Replace with JNZ

Both of this functions reference to FUN_1004a1896 (Exit upon Invalid Code Sign), so we had to fix those to bypass this check.

Export your binary in Ghidra through Export Program function.

Code Sign the Binary

Once you exported your binary; you will get Sketch.bin to your export location. Move this file to Sketch.App bundle in /Applications directory. Then, rename and chmod your executable.

# In /Applications/Sketch.App/Content/MacOS
$ mv Sketch.bin Sketch
$ chmod +x Sketch

Next, execute code signature on your binary. I had to remove empty padding sections from application signature first.

Create a new certificate

To create a certificate, open up Keychain Access in MacOS. Then go to Keychain Access in the menubar, select Certificate Assistant and click Create a Certificate. Enter your name and make sure to select a proper Certificate Type.

Create Certificate in Keychain Access

Create Certificate, Certification Type

Now codesign your new binary file.

# Sign the application
$ codesign --deep --force -s "signature" /Applications/Sketch.app

# If you get errors while codesigning, try this
$ xattr -lr /Applications/Sketch.app    # lists all empty attrs in app
$ xattr -cr /Applications/Sketch.app    # clear empty attrs in binary app

Voila! There you go, a never ending Sketch.App trial.

Patched Sketch.App

If you want, you can disable Sketch.App update feature through Terminal:

$ defaults write com.bohemiancoding.sketch3.plist SUEnableAutomaticChecks -bool false

[1] https://ghidra-sre.org/
[2] https://www.youtube.com/watch?v=fTGTnrgjuGA