Intro
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.
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).
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.
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
.
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.
If you scroll down a bit in a Assembly View window, you will find isExpired
listed. Lets see what is inside.
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.
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.
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
.
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.
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.
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