Reverse engineering software licensing from early-2000s abandonware – Part 2
In part 1, we reverse engineered the registration code licensing mechanism of this particular software. However, that mechanism was not the mechanism actually in use in 2004; rather, a different mechanism was used based on licence files named license.bin. In this part, we investigate that licensing mechanism.
Identifying the licensing code
Using Ghidra, we note that a "license.bin"
string is defined within the software binary:
This string is referenced within a function that Ghidra/IDR has identified as TSecurity.Create. The relevant decompiled code reads:
void TSecurity.Create(int param_1,char param_2) {
// ...
TApplication.GetExeName(*(undefined4 *)Application,&local_30);
// ...
ExtractFilePath(puVar2,&local_2c);
// ...
@LStrCat3(&local_28,local_2c,"license.bin");
// ...
cVar3 = FileExists(local_28);
if (cVar3 != '\0') {
// ...
local_10 = (code **)TFileStream.Create(&PTR_TStream.GetSize_0041735c,1,local_28);
// ...
local_14 = (code **)TStringStream.Create(&PTR_TStream.GetSize_004174c8,1,0);
// ...
FUN_004e7fec(local_20,"innocuous-looking string",0);
// ...
TStringStream.ReadString(local_14,uVar6,&local_18,pcVar5,uVar4);
FUN_004e8028(local_20,local_18,local_1c);
// ...
return;
}
// ...
(**(code **)(**(int **)(local_8 + 4) + 0x38))(*(int **)(local_8 + 4),"Functionality=0");
// ...
(**(code **)(**(int **)(local_8 + 4) + 0x38))(*(int **)(local_8 + 4),"RegTo=UNREGISTERED");
// ...
}
It appears that the code gets the path to the software binary, appends "license.bin"
, and checks if the file exists. If the file does not exist, it calls some function with "Functionality=0"
and "RegTo=UNREGISTERED"
. If the file does exist, it opens the file, and calls TStringStream.ReadString, passing a pointer local_18. local_18 is then passed to a function FUN_004e8028.
Inspecting the raw disassembly, we see that, for the call to TStringStream.ReadString, a pointer to local_18 is stored in ecx:
We can investigate this further using the debugger. We create a license.bin file, and fill it with 0x50 ASCII a
characters (0x61). In the debugger, we set a breakpoint before the TStringStream.ReadString call and note the value of ecx:
$ winedbg --gdb foobar.exe
Wine-gdb> b *0x4e8206
Breakpoint 1 at 0x4e8206
Wine-gdb> c
Continuing.
Breakpoint 1, 0x004e8206 in ?? ()
Wine-gdb> info reg
eax 0x101215c 16851292
ecx 0x32fed0 3342032
edx 0x50 80
ebx 0x70 112
[...]
We then set another breakpoint immediately after the TStringStream.ReadString call and read the contents at the pointed address:
Wine-gdb> b *0x4e820b
Breakpoint 2 at 0x4e820b
Wine-gdb> c
Continuing.
Breakpoint 2, 0x004e820b in ?? ()
Wine-gdb> x/wx 0x32fed0
0x32fed0: 0x010123f4
Wine-gdb> x/32bx 0x010123f4
0x10123f4: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
0x10123fc: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
0x1012404: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
0x101240c: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
It looks like this buffer does indeed contain the contents of our license.bin file!
Investigating the format of a decrypted licence file
Returning to TSecurity.Create, examining the raw disassembly, we see that the decompiled code generated by Ghidra is not correct. The line identified by Ghidra as return;
actually transfers control to another part of the code. This is due to the use of structured exception handling (SEH), which Delphi uses to implement try–except–finally blocks. The relevant disassembly reads:
; ...
004e81ba 33 c0 XOR EAX,EAX ; Begin try block
004e81bc 55 PUSH EBP
004e81bd 68 37 82 PUSH DAT_004e8237
4e 00
004e81c2 64 ff 30 PUSH dword ptr FS:[EAX]
004e81c5 64 89 20 MOV dword ptr FS:[EAX],ESP
; ...
004e8200 8d 4d ec LEA ECX=>local_18,[EBP + -0x14] ; Try block contents
004e8203 8b 45 f0 MOV EAX,dword ptr [EBP + local_14]
004e8206 e8 81 43 CALL TStringStream.ReadString
f3 ff
004e820b 8d 4d e8 LEA ECX=>local_1c,[EBP + -0x18]
004e820e 8b 55 ec MOV EDX=>DAT_004e8374,dword ptr [EBP + local_18]
004e8211 8b 45 e4 MOV EAX,dword ptr [EBP + local_20]
004e8214 e8 0f fe CALL FUN_004e8028
ff ff
004e8219 33 c0 XOR EAX,EAX ; End try block contents
004e821b 5a POP EDX
004e821c 59 POP ECX
004e821d 59 POP ECX
004e821e 64 89 10 MOV dword ptr FS:[EAX],EDX
004e8221 68 3e 82 PUSH LAB_004e823e
4e 00
; ... ; Miscellaneous destructor code
004e8236 c3 RET ; "return" in decompiled code
; Jumps to LAB_004e823e
DAT_004e8237 ; SEH handler
004e8237 e9 ?? E9h
004e8238 64 ?? 64h d
004e8239 c0 ?? C0h
004e823a f1 ?? F1h
004e823b ff ?? FFh
004e823c eb ?? EBh
004e823d e8 ?? E8h
LAB_004e823e ; End try block
; ... (execution continues)
004e825b 8b 45 fc MOV EAX,dword ptr [EBP + -0x4]
004e825e 8b 40 04 MOV EAX,dword ptr [EAX + 0x4]
004e8261 8b 55 e8 MOV EDX,dword ptr [EBP + -0x18]
004e8264 8b 08 MOV ECX,dword ptr [EAX]
004e8266 ff 51 2c CALL dword ptr [ECX + 0x2c]
004e8269 8d 4d d0 LEA ECX,[EBP + -0x30]
004e826c 8b 45 fc MOV EAX,dword ptr [EBP + -0x4]
004e826f 8b 40 04 MOV EAX,dword ptr [EAX + 0x4]
004e8272 ba e4 83 MOV EDX=>s_RegBy_004e83e4,s_RegBy_004e83e4 ; "RegBy"
4e 00
004e8277 e8 d4 28 CALL TStrings.GetValue
f3 ff
; ...
Recall that local_18, containing the contents of license.bin, was passed to FUN_004e8028. local_1c (at EBP + -0x18
) is also passed to FUN_004e8028, and is then later passed to the dynamic call at 0x4e8266 (as edx).
Using the debugger, we can inspect the contents of edx at this point, and identify where the dynamic call leads:
$ winedbg --gdb foobar.exe
Wine-gdb> b *0x4e8266
Breakpoint 1 at 0x4e8266
Wine-gdb> c
Continuing.
Breakpoint 1, 0x004e8266 in ?? ()
Wine-gdb> info reg
eax 0x1012ea0 16854688
ecx 0x41715c 4288860
edx 0x1013ff4 16859124
ebx 0x70 112
[...]
Wine-gdb> x/64bx $edx
0x1013ff4: 0x86 0x0d 0x96 0x9f 0xb8 0xa3 0xec 0x46
0x1013ffc: 0x13 0x1f 0x7b 0x8a 0x4a 0x96 0x31 0xbd
0x1014004: 0x24 0x43 0x30 0x2c 0x72 0xc2 0x6c 0x0e
0x101400c: 0xd3 0x58 0xc3 0xca 0xed 0xf6 0xb9 0x13
0x1014014: 0x46 0x4a 0x2e 0xdf 0x1f 0xc3 0x64 0xe8
0x101401c: 0x71 0x16 0x65 0x79 0x27 0x97 0x39 0x5b
0x1014024: 0x86 0x0d 0x96 0x9f 0xb8 0xa3 0xec 0x46
0x101402c: 0x02 0x0f 0xb8 0x74 0x00 0x61 0x61 0x61
Wine-gdb> si
0x0041b198 in ?? ()
Ghidra identifies for us that the function at 0x41b198 is TStrings.SetTextStr. According to the Delphi documentation, TStrings represents a list of strings, and TStrings.SetTextStr initialises the list of strings according to the parameter passed, with each value in the list separated by newlines.
It is worth at this point briefly mentioning the default Delphi calling convention, known as Borland fastcall. In this convention, the first 3 arguments to a function are passed as eax, edx and ecx (in that order), with further arguments pushed to the stack. In TStrings.SetTextStr, the first argument (eax) would be a reference to the TStrings instance, and so the second argument (edx) should be the string to initialise the list from.
We surmise, then, that edx contains the decrypted version of the license.bin data, ready to be later parsed. But what format should it take?
We see that the next call is a call to TStrings.GetValue with the parameter "RegBy"
. Of that function, the Delphi documentation says:
When the list of strings for the TStrings object includes strings that are name-value pairs, use Values to get or set the value part of a string associated with a specific name part.
For more information on name-value pairs, refer to the NameValueSeparator property.
And of NameValueSeparator, the documentation says:
Strings that contain the NameValueSeparator character are considered name-value pairs. NameValueSeparator defaults to the equal sign (
=
). …Strings that are name-value pairs consist of a name part, the separator character, and a value part. … For example:
DisplayGrid=1 SnapToGrid=1 GridSizeX=8 GridSizeY=8
The code, then, seems to be getting the value associated with the RegBy key. We know now what format the decrypted data in edx should take – a sequence of Key=Value
pairs separated by newlines.
Injecting licence file data
Recall from earlier that, when the license.bin file did not exist, there was a reference to the string "Functionality=0"
. Perhaps this key relates to the value at DAT_007e8d44 from part 1, which determined whether the software was registered.
Using winedbg/GDB, we insert a breakpoint just before the call to TStrings.SetTextStr:
$ winedbg --gdb foobar.exe
Wine-gdb> b *0x4e8266
Breakpoint 1 at 0x4e8266
Wine-gdb> c
Continuing.
Breakpoint 1, 0x004e8266 in ?? ()
We then replace the buffer at edx with the string "Functionality=1"
, and continue execution:
Wine-gdb> set {char[16]} $edx = "Functionality=1"
Wine-gdb> x/s $edx
0x1013ff4: "Functionality=1"
Wine-gdb> c
Continuing.
And with that, the software again reports that it is a registered copy:
If we add further entries, such as "Functionality=1\nRegBy=foobar"
, the registration message adjusts accordingly:
Next steps
In this part, we reverse engineered the format of the decrypted licence file data, and demonstrated an approach injecting this data to enable the full functionality of the software. In the next and final part, we investigate the encryption mechanism, in order to produce valid licence files from scratch.