Creating a software license bypass
I had to use Windows 11 for a few tasks recently. However, I am simply incompatible with combined icons in the taskbar. I do not know how other people use such a confusing and indistinguishable user interace. In Win10, there was an option to show all open windows side-by-side in the taskbar with their associated window title (Combine taskbar buttons: Never
). Win11 removed this option.
Luckily for me, I am not the only user who is incompatible with such questionable UI design. That is why the program called StartAllBack
was created. This program is wonderful, and restores the option to show all window lables, uncombined. Unfortunately, the program is not free as in freedom or free beer. But atleast the program is very affordable (5 dollars) and doesn’t phone home. It also comes with a long trial of 100 days which doesn’t come with any restrictions, even when the trial expires. The only effect of using an expired trial is that the start menu will have a very sad smiley in the background.
Ofcourse I bought the program, it is exactly what I needed. But it did make me wonder ‘How easy would it be to bypass the license verification?‘. As it turns out, it is not too difficult for this specific program.
The following items are required to bypass the trial/license verification:
- A Windows 11 installation (VM)
- An installed version of StartAllBack
- Reverse engineering tools (I used Cutter)
- Time, YMMV
Bypassing the trial.
The StartAllBack trial works as follows:
- When the program is installed/first ran it stores a timestamp, or a derivative of a timestamp.
- When the program starts, it checks the timestamp and subtracts the days since from the trial length (100 days).
- If the result is greater than zero, the trial is valid. Otherwise it has expired.
- If the trial is expired, add sad smileys to the start menu.
The method that is used has a few obvious attacks (that I can think of):
- In step 1, modify the stored timestamp to a very distant future date to make the (unrestricted) trial semi-permanent
- In step 2, increase the default trial length by changing the 100 days
- In step 3, change the jumps to make the trial valid, even when expired
- In step 4, prevent the sad smileys from being added even if the trial is expired.
And each of these attacks has many possible implementations which I will not list here. There are probably more attacks possible, but you can think of them yourself. In the following chapters, I will show how I implemented some of them.
modifying the total trial length
The default trial is 100 days. But because the trial does not limit the functionality in any way, extending the trial (through binary modification) will effectively bypass the requirement for a license.
StartAllBack uses two main places for its business logic:
StartAllBackX64.dll
for the main implementationStartAllBackCfg.exe
for the configuration and license activation
The UI/configuration component also imports several functions from the .dll library, namely:
- sub.StartAllBackX64.dll_GlassControls
- sub.StartAllBackX64.dll_LoadSVG
- sub.StartAllBackX64.dll_LoadSVGOrb
- sub.StartAllBackX64.dll_Ordinal_100
- sub.StartAllBackX64.dll_Ordinal_101
- sub.StartAllBackX64.dll_Ordinal_102
- sub.StartAllBackX64.dll_Ordinal_103
These last few, unnamed Ordinal
functions are interesting because we have no clue at all what they may do. The other functions seem to be related to SVG’s and other UI elements; which is not related to our goal.
1. Ordinal_100
The Ordinal_100 function is quite small, and does not use any branches. The default syscalls seem to indicate some activity/relationship to the windows registry. Running the function through Ghidra gives the following code:
void Ordinal_100(int64_t arg1)
{
int64_t var_8h;
HKEY hKey;
DWORD lpcSubKeys;
REGSAM lpcbMaxSubKeyLen;
undefined8 lpSecurityAttributes;
PHKEY lpcValues;
LPDWORD lpcbMaxValueNameLen;
LPDWORD lpcbMaxValueLen;
LPDWORD lpcbSecurityDescriptor;
PFILETIME lpftLastWriteTime;
LPCSTR lpString2;
char *var_13bh;
int64_t var_131h;
LPSTR lpString1;
fcn.180001b24(&lpString2);
var_13bh._0_1_ = 0x2d;
var_13bh._4_2_ = 0x2d31;
var_131h._0_1_ = 0x2d;
var_131h._3_1_ = 0x39;
(*KERNEL32.dll_lstrcpyA)(&lpString1, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CLSID\\{");
(*KERNEL32.dll_lstrcatA)(&lpString1, &lpString2);
(*KERNEL32.dll_lstrcatA)(&lpString1, data.18007bc8c);
_lpcSubKeys = _lpcSubKeys & 0xffffffff00000000;
(*ADVAPI32.dll_RegCreateKeyExA)(0xffffffff80000001, &lpString1, 0, 0, _lpcSubKeys, 0x101, 0, &hKey, 0);
(*ADVAPI32.dll_RegQueryInfoKeyW)(hKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, arg1);
(*ADVAPI32.dll_RegCloseKey)(hKey);
return;
}
A decompilation makes figuring out what a function does much easier. The code above shows some string concatination to create a registry URI, and then a creation of a registry key using the RegCreateKeyExA
function. According to the docs, the function signature is as follows:
LSTATUS RegCreateKeyExA(
[in] HKEY hKey,
[in] LPCSTR lpSubKey,
DWORD Reserved,
[in, optional] LPSTR lpClass,
[in] DWORD dwOptions,
[in] REGSAM samDesired,
[in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[out] PHKEY phkResult,
[out, optional] LPDWORD lpdwDisposition
);
Using the docs for known functions as a guide, we can figure out the meaning behind many unnamed variables in the decompilation/disassembly. This technique is also used in future chapters of this article.
If we look at the specified registry location, we find the following keys:
None of which have interesting subkeys or values. Thus we can claim that the Ordinal_100
function is not interesting, atleast not directly.
(This function is further described in the Ordinal_101 section.)
2. Ordinal_101
The Ordinal_101
function is shorter, but does use branching. Cutter shows branching in the Graph view as follows:
The decompilation is as follows:
bool Ordinal_101(LPSYSTEMTIME lpSystemTime)
{
bool bVar1;
LPFILETIME lpSystemTimeAsFileTime;
int64_t var_10h;
int64_t var_18h;
uint16_t auStack_18 [8];
Ordinal_100((int64_t)&var_10h);
(*KERNEL32.dll_GetSystemTimeAsFileTime)(&lpSystemTimeAsFileTime);
if (lpSystemTimeAsFileTime < (uint64_t)var_10h) {
bVar1 = true;
} else {
(*KERNEL32.dll_FileTimeToSystemTime)(&lpSystemTimeAsFileTime, auStack_18);
if (auStack_18[0] < 0x7e9) {
bVar1 = (LPFILETIME)(var_10h + 86400000000000U) < lpSystemTimeAsFileTime;
} else {
bVar1 = false;
}
}
return bVar1;
}
Curiously enough, this function seems to call the previous function Ordinal_100
. It then compares that with the system time, and if it determines that the system time is smaller (this is normally an earlier timepoint), it returns true. Otherwise the function return false. (Both the system time as file time, and the file time as system time are checked for some reason).
Studying this function tells us two things:
- The
Ordinal_100
function gets a timestamp from the registry (perhaps the install time?) - The
Ordinal_101
function comapres the timestamp to the current time.
The timestamp from the registry is obtained using the RegQueryInfoKeyW
function. We can also manually check the timestamp of a key by exporting a key using the .txt
format. Exporting the yyyy yyyy
from before gives the following result:
Key Name: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{yyyy yyyy}
Class Name: <NO CLASS>
Last Write Time: 24/06/2023 - 19:09
Currently, my trial is still valid for 97 days.
I wonder if changing the timestamp of any of the keys makes a difference, lets try changing the timestamp by adding a test value.
Key Name: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{yyyy yyyy}
Class Name: <NO CLASS>
Last Write Time: 27/06/2023 - 22:37
Value 0
Name: test
Type: REG_SZ
Data:
And success! We have successfully reset the trial!
This makes me curious, what if I change the system time to the future, change the installation timestamp, and the restore the correct system time?
Key Name: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{yyyy yyyy}
Class Name: <NO CLASS>
Last Write Time: 27/06/2025 - 22:45
This makes our license appear as expired in the configuration tool, but the start menu accepts the license. This means that the configuration tool also has additional logic to verify the license. But the logic of the other functions is clear, the Ordinal_100
gets the installation timestamp, and the Ordinal_101
function checks if that timestamp is within the last 100 days. Rewriting the function to make it more readable creates the following:
var installTime = Ordinal_100();
var currentTime = GetSystemTime();
if (currentTime < installTime) {
// Installed in the future.
return true;
} else {
// 1000 * 1000 * 60 * 60 * 24 * 100 = 8640000000000
// us/ms * ms/sec * sec/min * min/hr * hr/day * 100 days
if (installTime + 60 * 60 * 24 * 100 * 1000 < currentTime) {
// Within trial period.
return true;
} else {
// Trial is expired.
return false;
}
}
Extending the trial length
Ordinal_102
and Ordinal_103
are related to validating an existing license, which we do not have. For now, lets extend the maximum trial. As we did above, simply storing a future timestamp extends the trial, but not for the entire program.
If we open the configuration tool in the the disassembler, and check the places where the Ordinal_XXX
functions are used, we will quickly run into the following section of disassembled code:
iVar2 = sub.StartAllBackX64.dll_Ordinal_102(&var_19h); // Existing license function
if ((iVar2 == 0) || (var_19h == '\0')) {
sub.kernel32.dll_GetSystemTime(&lpSystemTime); // Get system time
sub.StartAllBackX64.dll_Ordinal_100(&var_34h); // Get install time
lpFileTime = (FILETIME *)var_34h;
sub.kernel32.dll_FileTimeToSystemTime(&lpFileTime, &var_44h);
fcn.0059ddb0(&var_88h, 0x5cae8c);
uVar4 = fcn.0042b110(&lpSystemTime);
uVar5 = fcn.0042b110(&var_44h);
var_98h._0_4_ = fcn.004778c0(uVar4, uVar5); // Function takes system and install time
var_98h._0_4_ = 100 - (int32_t)var_98h; // 100 - result of above function = trial time left
var_90h._0_1_ = 0;
fcn.00427ac0(&var_80h, var_88h, &var_98h, 0); // Function that takes the trial time left.
fcn.004a5900(*(undefined8 *)(param_1 + 0x498), var_80h);
...
}
We now have two places where the trial length is stored, once in the configuration tool and once in the library. Simply changing both values results in an extended trial. In the example below, I changed the trial length to 3650 days.
Bypassing the license verification
But lets say that we don’t want to extend the trial, but that we want to fully bypass the license verification. A simple string search leads us to the string called str.trialover
. We can see that the code calls the Ordinal_101
(trial duration check) function after the Ordinal_102
(License check) function. If we simply change the jump after the license verification function, we can fully bypass the start menu’s license verification. An example of this can be seen in the image below.
But as before, the configuration tool implements the license verification logic differently. If we look back at the previous change to the configuration tool, where we changed the trial length. Just before the trial length logic, there is a call to the Ordinal_102
function. If we simply change the jump right after that functon call, we bypass the license verification. Atleast partly, since another part of the logic still thinks that we need a license key. The weird half-activated state can be seen below.
Removing the license-key input field is trivial however, simply overriding the second license check fully activates the configuration tool.
And there you have it, a fully bypassed license.