Hopper: Are your license checks secure?

The Big Idea

This article is for educational purposes only. Note the license agreement (EULA) states that modification of the software is strictly prohibited, however, reverse engineering rights are protected by the French copyright laws. Here, we’ll attempt to reverse engineer the Hopper Disassembler and figure out their license handling technique.

Please buy a license to support the creators of Hopper.

Getting it to work first

Alright, let’s get rolling. The first step is to locate the Hopper executable, which is usually at /opt/hopper-v4/bin. Know that the free version of Hopper does not allow you to save files and will display a dialogue saying: “You cannot save with the demo version.” The free version also has a session time limit of 30 minutes. Starting up Hopper, we see the license dialogue with a button labelled “Try the Demo.” Loading Hopper into IDA and performing a string search reveals the “Try the Demo” string at:

1
.rodata:000000000067219A	0000000D       C	Try the Demo

And following it leads us to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.rodata:0x0000000067217E ; char aLicenseFile[]
.rodata:0x0000000067217E aLicenseFile db 'License File:',0 ; DATA XREF: sub_506CD0+130↑o
.rodata:0x0000000067217E ; sub_567B90+17↑o
.rodata:0x0000000067218C ; char aValidating[]
.rodata:0x0000000067218C aValidating db 'Validating...',0 ; DATA XREF: sub_506CD0+1FC↑o
.rodata:0x0000000067219A ; char aTryTheDemo[]
.rodata:0x0000000067219A aTryTheDemo db 'Try the Demo',0 ; DATA XREF: sub_506CD0+265↑o
.rodata:0x000000006721A7 ; char aBuyALicense[]
.rodata:0x000000006721A7 aBuyALicense db 'Buy a License',0 ; DATA XREF: sub_506CD0+2CE↑o
.rodata:0x000000006721B5 ; char aOfflineActivat[]
.rodata:0x000000006721B5 aOfflineActivat db 'Offline Activation',0
.rodata:0x000000006721B5 ; DATA XREF: sub_506CD0+337↑o
.rodata:0x000000006721B5 ; sub_50F120+1AE↑o
.rodata:0x000000006721C8 ; char aValidateLicens[]
.rodata:0x000000006721C8 aValidateLicens db 'Validate License',0 ; DATA XREF: sub_506CD0+3A0↑o

Further following the XREF brings us finally to the function sub_506CD0 which is most likely responsible for showing the license dialog. Therefore, it’s best to rename it to ShowLicenseDialog. Simple logic deduction leads us to believe that this function is run only when a license is not installed/registered with the software.

Setting a breakpoint in GDB and viewing the call stack reveals several functions that were called:

1
2
3
4
5
6
7
8
9
10
11
12
13
───────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────
► f 0 506cd0
f 1 50636e
f 2 501152
f 3 638031
f 4 63f54f
f 5 568871
f 6 7ffff7f9f278
f 7 6380d1
f 8 7ffff2966122 QObject::event(QEvent*)+226
f 9 7ffff36f3743 QWidget::event(QEvent*)+2307
f 10 7ffff3806c4b QMainWindow::event(QEvent*)+347
Breakpoint *0x506CD0

Now we just need to open every address (f1 through f7) in IDA and see what’s there.

We finally found the right one!

Most functions on the call stack are boring, usually, Qt function calls and such. However, at 0x638031 we see a spicy one:

1
2
3
4
5
6
7
8
9
10
11
.text:0x00000000638019 loc_638019:                             ; CODE XREF: sub_637FC0+14↑j
.text:0x00000000638019 ; sub_637FC0+23↑j
.text:0x00000000638019 call sub_504550 ; Possible CheckLicense candidate
.text:0x0000000063801E test al, al
.text:0x00000000638020 jnz short loc_63806C
.text:0x00000000638022 lea rbx, [rsp+88h+var_88]
.text:0x00000000638026 mov rdi, rbx
.text:0x00000000638029 mov rsi, r15
.text:0x0000000063802C call sub_501110
.text:0x00000000638031 mov rax, [rsp+88h+var_88]
.text:0x00000000638035 mov rax, [rax+1A8h]

Note a peculiar pattern: a call to sub_504550 followed by a test instruction and a jnz past the function chain (the chain that eventually leads to ShowLicenseDialog). This is a strong candidate for a common coding pattern:

1
2
3
4
5
6
7
8
9
10
11
uint8_t al = sub_504550();
if(al == 0)
{
// Has License
DoStuff();
}
else
{
// No License
ShowLicenseDialog();
}

Ah, things are looking good! A closer look at sub_504550 confirms my suspicions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0x00000000504550 sub_504550      proc near               ; CODE XREF: sub_5B7850:loc_5B7896↓p
.text:0x00000000504550 ; sub_5ECC10:loc_5ED0BB↓p ...
.text:0x00000000504550 push rax
.text:0x00000000504551 call sub_5024E0
.text:0x00000000504556 cmp eax, 3
.text:0x00000000504559 jnz short loc_504565
.text:0x0000000050455B call sub_502E70
.text:0x00000000504560 call sub_5024E0 ; Suspicious function call with URL
.text:0x00000000504565
.text:0x00000000504565 loc_504565: ; CODE XREF: sub_504550+9↑j
.text:0x00000000504565 dec eax
.text:0x00000000504567 cmp eax, 2
.text:0x0000000050456A setb al
.text:0x0000000050456D pop rcx
.text:0x0000000050456E retn
.text:0x0000000050456E sub_504550 endp

Note the call to sub_502E70 which is a function that references the string “https://www.hopperapp.com/validate_license_v4.php" followed by several network calls and requests. We now know that sub_504550 is a CheckLicense function. Jackpot! Patching the sub_504550 function to always return true (al=1 ) is easy, we can do it like so:

1
2
3
4
.text:0x00000000504550 CheckLicense:                           ; CODE XREF: sub_5B7850:loc_5B7896↓p
.text:0x00000000504550 ; sub_5ECC10:loc_5ED0BB↓p ...
.text:0x00000000504550 mov al, 1
.text:0x00000000504552 retn

Good Eyecandy

You may have noticed the “Demo Version” watermark present in the background of the program. Getting rid of it is as simple as searching for the string “Demo Version” in IDA:

1
.rodata:0x0000000067B380	0000000D	C	Demo Version

A quick patch overwriting the string with all 0’s should do the trick:

1
2
.rodata:0x0000000067B380 ; QString aDemoVersion
.rodata:0x0000000067B380 aDemoVersion db 0,0,0,0,0,0,0,0,0,0,0,0,0

Now, we’ll attempt to customize the license window. Hopper demo version "About" window

We should search for the string “Demo version” or “Hopper Standard Edition,” as those should appear somewhere near the “About” window dialog code.

1
2
.rodata:0x000000006655B0 ; QString aHopperStandard
.rodata:0x000000006655B0 aHopperStandard db 'Hopper Standard Edition %1',0

Note the following variables declared several bytes further down:

1
2
3
4
5
6
7
8
9
10
.rodata:0x000000006655D8 ; QString aPersonalLicens
.rodata:0x000000006655D8 aPersonalLicens db 'Personal License',0Ah
.rodata:0x000000006655D8 ; DATA XREF: PrintLicenseName+41D↑o
.rodata:0x000000006655D8 db 'Registered to %1',0Ah
.rodata:0x000000006655D8 db '%2',0Ah
.rodata:0x000000006655D8 db '(%3)',0
.rodata:0x00000000665602 ; QString aComputerLicens
.rodata:0x00000000665602 aComputerLicens db 'Computer License %1',0Ah
.rodata:0x00000000665602 ; DATA XREF: PrintLicenseName+191↑o
.rodata:0x00000000665602 db 'Registered to %2',0

Again, following the XREF leads us to sub_4C0930 (which we’ll rename to PrintLicenseName). Looking at the graph, we see:

IDA Graph

I’ve labeled the graph this time since it’s quite messy when taken out of context. The pseudocode for this block would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Block 1
...
const char* rdi = "Hopper Standard Edition %1";
int esi = 0x1A;
QString::fromAscii_helper(rdi, esi);
...
// Block 2 via A
int eax = sub_45FC40(); // Returns int in eax
if(eax == 0)
{
// To loc_4C0CD2 via B
...
}
// Block 3 via C
else if(eax == 1)
{
// Block 4 via D
...
const char* rdi = "Personal License\nRegistered to %1\n%2";
int esi = 0x29;
QString::fromAscii_helper(rdi, esi);
...
}
// To other case via E
else if(eax == 2)
{
// Code not shown on graph
...
const char* rdi = "Computer License %1\nRegistered to %2";
int esi = 0x24;
QString::fromAscii_helper(rdi, esi);
...
}
else
{
// Code not shown
...
}

The following block contains the string aPersonalLicens, which should be the string we want to print to the screen.

1
2
3
4
5
6
7
.text:0x000000004C0D45 loc_4C0D45:                             ; CODE XREF: PrintLicenseName+171↑j
.text:0x000000004C0D45 ; PrintLicenseName+17A↑j
.text:0x000000004C0D45 mov rax, [r15+30h]
.text:0x000000004C0D49 mov r14, [rax+40h]
.text:0x000000004C0D4D lea rdi, aPersonalLicens
.text:0x000000004C0D54 mov esi, 29h
.text:0x000000004C0D59 call __ZN7QString16fromAscii_helperEPKci ; QString::fromAscii_helper(char const*,int)

Upon further investigation, we see that many calls branch from loc_4C0A9A. Here, we must ensure all branches to loc_4C0D45 are valid with a direct jump (bypassing loc_4C0A9A). When verified, we can just replace the jnz loc_4C0CD2 instruction with a plain jmp loc_4C0A9A, thus bypassing all checks. Then we have the entire aPersonalLicens to ourselves to customize. So, we first patch:

1
.text:0x000000004C0AA1                 jz      loc_4C0CD2

To:

1
.text:0x000000004C0AA1                 jmp     loc_4C0D45

Then override aPersonalLicens to display a custom message. Note the message length of 29h or 41 characters. That is quite restrictive. But also notice that aComputerLicens (which is unused as we skipped over all logical branches leading to its XREF) immediately follows aPersonalLicens, granting us a total of 77 characters to work with (by overwriting aComputerLicens).