Reversing a Delphi App's Registration Logic: SI ObjectBrowser
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Reversing a Delphi App’s Registration Logic: SI ObjectBrowser
SI ObjectBrowser is an Oracle management tool written in Delphi. I first ran into it back in 2007, handed to me by a Japanese client — and honestly, its performance was outstanding; even Oracle’s own later tools couldn’t keep up. It’s made by a Japanese company, once had a Chinese-language release that was plastered with ads, and after version 12 the official Chinese builds stopped — largely because Oracle has since ceded ground to MySQL and PostgreSQL outside the big banks and enterprises it still dominates.
A couple of days ago I got curious about cracking its registration logic. The registration is barely protected at all — likely because piracy was never a serious concern for the vendor.
This post is purely for studying assembly, reverse engineering, and registration algorithms — no crack tool is provided. I’ll use OB11 as the target, since IDA parses Delphi 2007 functions more accurately.
Packer check
As usual, check for a packer first:

No packer — plain Delphi 2006/2007. Drop it into IDA. Since the whole program is a single EXE with everything in one file, IDA takes a while to load.
Once loaded, search for Tfrm — Delphi’s form-event prefix.

The login button’s event handler is TfrmRegist_actCommitExecute. Open it and hit F5:
int __usercall TfrmRegist_actCommitExecute@<eax>(int a1@<eax>, double a2@<st0>)
{
int IsAdmin; // eax
int v4; // edx
int v5; // edx
int v6; // edx
int v7; // edx
int v8; // edx
int v9; // edx
int v10; // esi
unsigned __int16 v11; // ax
int v12; // edx
int v13; // edx
int v14; // edx
int v15; // eax
int v16; // edx
int v17; // edx
void *v19; // [esp-8h] [ebp-98h]
int v20; // [esp-4h] [ebp-94h]
void *v21; // [esp-4h] [ebp-94h]
void *v22; // [esp+0h] [ebp-90h]
unsigned __int16 v23; // [esp+0h] [ebp-90h]
void *v24; // [esp+4h] [ebp-8Ch]
void *v25; // [esp+4h] [ebp-8Ch]
int v26; // [esp+4h] [ebp-8Ch]
unsigned __int16 v27; // [esp+4h] [ebp-8Ch]
int v28; // [esp+4h] [ebp-8Ch]
void *v29; // [esp+4h] [ebp-8Ch]
unsigned int v30[2]; // [esp+8h] [ebp-88h] BYREF
int *v31; // [esp+10h] [ebp-80h]
void *v32[7]; // [esp+1Ch] [ebp-74h] BYREF
_TBYTE v33; // [esp+38h] [ebp-58h]
int v34; // [esp+44h] [ebp-4Ch] BYREF
int v35; // [esp+48h] [ebp-48h] BYREF
int System__AnsiString; // [esp+4Ch] [ebp-44h] BYREF
int v37[4]; // [esp+50h] [ebp-40h] BYREF
void *v38; // [esp+68h] [ebp-28h] BYREF
void *v39[5]; // [esp+6Ch] [ebp-24h] BYREF
double v40; // [esp+80h] [ebp-10h]
int v41; // [esp+8Ch] [ebp-4h] BYREF
int savedregs; // [esp+90h] [ebp+0h] BYREF
v31 = &savedregs;
v30[1] = (unsigned int)&loc_67C32E;
v30[0] = (unsigned int)NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, (unsigned int)v30);
IsAdmin = TfrmRegist_IsAdmin(a1);
if ( (_BYTE)IsAdmin )
{
if ( (unsigned __int8)sub_52D3BC(*(Controls::TControl **)(a1 + 908)) )
{
LOWORD(v5) = 48;
sub_7179AC((int)&str__v_____N_gNO____1[1], v5);
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
if ( (unsigned __int8)sub_52D3BC(*(Controls::TControl **)(a1 + 912)) )
{
LOWORD(v6) = 48;
sub_7179AC((int)&str__v_____N_gKEY___0[1], v6);
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
if ( (unsigned __int8)sub_52D3BC(*(Controls::TControl **)(a1 + 940)) )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 912));
v24 = v39[4];
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 908));
if ( (unsigned __int8)sub_7242FC((int)v39[3], (int)v24) )
{
LOWORD(v7) = 48;
sub_7179AC((int)&str____C_Z___X_F__K_2[1], v7);
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
}
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x38C));
if ( (unsigned __int8)((int (__fastcall *)(int))sub_7245F8)((int)v39[1]) )
{
if ( *(_DWORD *)(a1 + 0x3B8) == 1 )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
v25 = v39[0];
sub_722884((int)&v38);
v22 = v38;
v20 = System::__linkproc__ LStrClr(&v41);
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x38C));
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x3AC));
if ( (unsigned __int8)((int (__stdcall *)(int, void *, void *))sub_72307C)(v20, v22, v25) )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
if ( sub_7242A8(v37[3]) )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
v26 = v37[2];
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x38C));
if ( !(unsigned __int8)sub_7242FC(v37[1], v26) )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 912));
v10 = sub_724208(v37[0]);
sub_703160((int)&System__AnsiString);
v27 = Sysutils::StrToIntDef(System__AnsiString, 0);
sub_703160((int)&v35);
v23 = Sysutils::StrToIntDef(v35, 0);
sub_703160((int)&v34);
v11 = Sysutils::StrToIntDef(v34, 0);
Sysutils::EncodeDate(v11, v23, v27);
v40 = a2;
Sysutils::Now();
if ( a2 > v40 )
{
LOWORD(v12) = 64;
sub_7179AC((int)&str_______C_Z___X_[1], v12);
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
Sysutils::IncMonth(-v10, v12);
a2 = a2 - 1.0;
*(double *)&v33 = a2;
Sysutils::Now();
if ( *(double *)&v33 > a2 )
{
LOWORD(v13) = 64;
sub_7179AC((int)&str__V_X_e_____t_[1], v13);
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
}
}
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
v28 = sub_7242A8((int)v32[6]) != 0;
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x3AC));
if ( (unsigned __int8)sub_7235B8((unsigned int)v32[5], v28) )
{
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
v29 = v32[4];
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x38C));
if ( !(unsigned __int8)sub_7242FC((int)v32[3], (int)v29) )
{
LOWORD(v14) = 64;
sub_7179AC((int)&str_______C_Z___X__0[1], v14);
*(_DWORD *)(a1 + 660) = 0;
Sysutils::Abort();
}
}
Sysutils::Now();
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x3AC));
v21 = v32[2];
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x390));
v19 = v32[1];
Controls::TControl::GetText(*(Controls::TControl **)(a1 + 0x38C));
v15 = sub_721E54(v32[0], v19, (int)v21, HIDWORD(*(unsigned __int64 *)&a2), SLODWORD(a2));
if ( (_BYTE)v15 )
{
Sysutils::Now();
sub_723ECC(LODWORD(a2), HIDWORD(*(unsigned __int64 *)&a2));
LOWORD(v17) = 64;
sub_7179AC((int)&str____C_Z___X_o____0[1], v17);
*(_DWORD *)(a1 + 0x294) = 1;
}
else
{
LOBYTE(v15) = 1;
if ( sub_720E78(v15) )
{
LOWORD(v16) = 48;
sub_7179AC((int)&str__G______________3[1], v16);
}
else
{
LOWORD(v16) = 48;
sub_7179AC((int)&str__G______________4[1], v16);
}
*(_DWORD *)(a1 + 0x294) = 0;
Sysutils::Abort();
}
}
else
{
LOWORD(v9) = 48;
sub_7179AC((int)&str____C_Z___X_F__K_3[1], v9);
*(_DWORD *)(a1 + 660) = 0;
Sysutils::Abort();
}
}
}
else
{
LOWORD(v8) = 48;
sub_7179AC((int)&str_______________23[1], v8);
*(_DWORD *)(a1 + 660) = 0;
Sysutils::Abort();
}
}
else
{
LOBYTE(IsAdmin) = 1;
if ( sub_720E78(IsAdmin) )
{
LOWORD(v4) = 48;
sub_7179AC((int)&str_____________8[1], v4);
}
else
{
LOWORD(v4) = 48;
sub_7179AC((int)&str_____________9[1], v4);
}
}
__writefsdword(0, v30[0]);
v31 = (int *)&loc_67C335;
System::__linkproc__ LStrArrayClr(v32, 7);
System::__linkproc__ LStrArrayClr(&v34, 3);
System::__linkproc__ LStrArrayClr(v37, 6);
System::__linkproc__ LStrClr(&v38);
System::__linkproc__ LStrArrayClr(v39, 5);
return System::__linkproc__ LStrClr(&v41);
}
It’s a lot of code. sub_52D3BC checks whether a form field is empty, and sub_7179AC pops a message box. The rest I’ll break down below.
Reading the logic:
- Check whether the current user is an administrator; if not, error.
- Check that ProductNo and ProductKey are non-empty; if either is empty, error.
- Check whether the License key is empty. If it is, call
sub_7242FC; if that returns 1, error. (sub_7242FC’s logic is described later.) - Call
sub_7245F8; if it returns 0, error. (Detailed below.) - Call
sub_722884to get the hardware info. (Detailed below.) - Call
sub_72307Cto validate the license. - Write the registration info to the registry.
sub_7245F8 — validating the ProductKey
int __fastcall sub_7245F8(int System::AnsiString, int a2)
{
char v3; // zf
int v4; // ecx
int v5; // eax
int v6; // esi
int v7; // ebx
unsigned __int8 v8; // al
int v9; // eax
int v10; // eax
int v11; // eax
__int64 v12; // rax
int v13; // eax
int v14; // esi
int v15; // ebx
int v17; // [esp-14h] [ebp-64h]
void *v18; // [esp-10h] [ebp-60h]
void *v19; // [esp-10h] [ebp-60h]
char v20; // [esp-10h] [ebp-60h]
unsigned int v21[2]; // [esp-Ch] [ebp-5Ch] BYREF
int *v22; // [esp-4h] [ebp-54h]
int v23; // [esp+8h] [ebp-48h] BYREF
int System__AnsiString[4]; // [esp+Ch] [ebp-44h] BYREF
void *v25[3]; // [esp+1Ch] [ebp-34h] BYREF
void *v26; // [esp+28h] [ebp-28h] BYREF
int v27[3]; // [esp+2Ch] [ebp-24h] BYREF
int v28; // [esp+38h] [ebp-18h]
unsigned __int8 v29; // [esp+3Fh] [ebp-11h]
int v30; // [esp+40h] [ebp-10h]
int v31; // [esp+44h] [ebp-Ch] BYREF
int v32[2]; // [esp+48h] [ebp-8h]
int savedregs; // [esp+50h] [ebp+0h] BYREF
v30 = a2;
v22 = &savedregs;
v21[1] = (unsigned int)&loc_724867;
v21[0] = (unsigned int)NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, (unsigned int)v21);
v29 = 0;
Sysutils::Trim(System::AnsiString);
if ( v27[2] )
{
Sysutils::Trim(v30);
if ( v27[1] )
{
System::__linkproc__ LStrCopy(v27);
System::__linkproc__ LStrCmp(v27[0], &str_OB__1[1]);
if ( v3 )
{
v32[0] = 0;
v32[1] = 0;
v28 = 0;
if ( sub_7242A8(v30) )
System::__linkproc__ LStrCatN(&v31, 3, v4, "OB-del2007ent", "ohm");
else
System::__linkproc__ LStrCatN(&v31, 3, v4, "OB11-del2009ent", "usj");
v5 = v31;
if ( v31 )
v5 = *(_DWORD *)(v31 - 4);
v6 = v5;
if ( v5 > 0 )
{
v7 = 1;
do
{
v8 = *(_BYTE *)(v31 + v7 - 1);
*((_BYTE *)v32 + v7 % 8) ^= v8;
v28 += v8;
++v7;
--v6;
}
while ( v6 );
}
v9 = System::__linkproc__ LStrCopy(&v31);
sub_7222AC(v9);
v10 = sub_722230(v31);
Sysutils::IntToHex(v10, 2);
v18 = v25[2];
Sysutils::IntToHex(v28 % 256, 2);
sub_722E8C(v25[1], v18, &v26);
v19 = v26;
v11 = sub_722230(v31);
Sysutils::IntToHex(v11, 4);
v17 = System__AnsiString[3];
v12 = System::__linkproc__ _llmod(0, 256);
Sysutils::IntToHex(v12, 10);
sub_722E8C(System__AnsiString[2], v17, v25);
System::__linkproc__ LStrCat3((int)&v31, v25[0], v19);
v13 = v31;
if ( v31 )
v13 = *(_DWORD *)(v31 - 4);
v14 = v13;
if ( v13 > 0 )
{
v15 = 1;
do
{
if ( !(v15 % 3) )
{
unknown_libname_72(System__AnsiString, *(unsigned __int8 *)(v31 + v15 - 1));
Sysutils::LowerCase(System__AnsiString[0]);
v20 = *(_BYTE *)System__AnsiString[1];
*(_BYTE *)(j_unknown_libname_77_0(&v31) + v15 - 1) = v20;
}
++v15;
--v14;
}
while ( v14 );
}
System::__linkproc__ LStrInsert(&str___441[1]);
System::__linkproc__ LStrCopy(&v23);
System::__linkproc__ LStrCmp(v23, v31);
if ( v3 )
v29 = 1;
}
}
}
__writefsdword(0, v21[0]);
v22 = (int *)&loc_72486E;
System::__linkproc__ LStrArrayClr(&v23, 12);
System::__linkproc__ LStrClr(&v31);
return v29;
}
The logic:
- ProductKey must start with
OB-. - Call
sub_7242A8to check whether bytes 4–6 afterOB-are digits. Digits mean the old version; old and new use different concatenated strings. For the new version, the string is"OB11-del2009ent" + ProductNo + "usj". - XOR the concatenated string every 8 bytes, storing the result in bytes 2–1, and also sum every byte, keeping the low byte. In C:
int isNumber = is_numeric(Key + 6, 3);
size_t FullKeyLen = strlen(KeyBase[isNumber]) + strlen(No) + 3;
char *FullKey = (char*)malloc(FullKeyLen + 1);
memset(FullKey, 0, FullKeyLen + 1);
sprintf_s(FullKey, FullKeyLen + 1, "%s%s%s", KeyBase[isNumber], No, TailFlag[isNumber]);
unsigned char FullKeyCalcResult[8] = { 0 };
unsigned char FullKeySumResult = 0;
for (size_t i = 0; i < FullKeyLen; i++)
{
FullKeyCalcResult[(i + 1) % 8] ^= FullKey[i];
FullKeySumResult += FullKey[i];
}
The low 5 bytes of the result are converted to a hex string.
- Take bytes 4–9 of the Key and compute via
sub_722230, which in C is:
unsigned char realKey[6] = { 0 };
memcpy(realKey, Key + 3, 6);
int KeyCalcResult = 0;
for (int i = 0; i < 6; i++)
{
KeyCalcResult = 0x89 * KeyCalcResult + realKey[i];
}
KeyCalcResult = KeyCalcResult % 0x87C3;
Convert the result to a hex string.
- Concatenate the results from step 3, and look up position 1 in this table, byte by byte:
char KeyTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
char KeyTable2[] = "T6ERUB7391GW8Q04YZ5KDICA2FMXSVLPHNOJ";
- Look up the step-4 result in
KeyTablein byte order 2, 3, 4, 1 — that’s position 2. - Position 1 + position 2; if greater than 36, subtract 36; look up the character at that position in
KeyTable. 12 bytes total. - In the resulting string, lowercase every 3rd byte, and insert
-after the 6th byte. - Compare this 13-byte string against the Key starting at byte 7; mismatch means error.
sub_722884 — hardware info
In C:
DWORD LogicalDrivesFlags = GetLogicalDrives();
LogicalDrivesFlags = LogicalDrivesFlags << 1;
char DriverStr[5] = { 0 };
for (char Driver = 'A'; Driver <= 'Z'; Driver++)
{
if (_bittest((const LONG *)&LogicalDrivesFlags, (Driver - 0x40) & 0x7F))
{
DriverStr[0] = Driver;
DriverStr[1] = ':';
DriverStr[2] = '\\';
DriverStr[3] = '\\';
break;
}
}
DWORD VolumeSerialNumber;
DWORD MaximumComponentLength;
DWORD FileSystemFlags;
GetVolumeInformationA(DriverStr, 0, 0, &VolumeSerialNumber, &MaximumComponentLength, &FileSystemFlags, 0, 0);
char strVolumeSerialNumber[9] = { 0 };
sprintf_s(strVolumeSerialNumber, 9, "%08X", VolumeSerialNumber);
str_replace(strVolumeSerialNumber, '-', '\0');
printf("strVolumeSerialNumber:%s\n", strVolumeSerialNumber);
if (VolumeSerialNumber == 0)
{
memcpy(strVolumeSerialNumber, "0000", 8);
}
strVolumeSerialNumber[4] = 0;
char BiosInfo[12 + 1] = { 0 };
NCB ncb;
ncb.ncb_command = 0x37;
unsigned char ncb_buffer[256] = { 0 };
unsigned char ncb_buffer2[604] = { 0 };
unsigned char ncb_callname[16] = "* ";
ncb.ncb_buffer = ncb_buffer;
ncb.ncb_length = 0x100;
Netbios(&ncb);
char c = ncb.ncb_buffer[0];
for (char i = 0; i < c; i++)
{
memset(&ncb, 0, sizeof(NCB));
ncb.ncb_command = 50;
ncb.ncb_lana_num = ncb_buffer[1];
Netbios(&ncb);
memset(&ncb, 0, sizeof(NCB));
ncb.ncb_command = 51;
ncb.ncb_lana_num = ncb_buffer[1];
memcpy(ncb.ncb_callname, "* ", 2);
memset(ncb.ncb_callname + 4, 0, 12);
ncb.ncb_buffer = ncb_buffer2;
ncb.ncb_length = 0x258;
if (!Netbios(&ncb))
{
sprintf_s(BiosInfo + i * 12, 13, "%02X%02X%02X%02X%02X%02X", ncb.ncb_buffer[0], ncb.ncb_buffer[1], ncb.ncb_buffer[2], ncb.ncb_buffer[3], ncb.ncb_buffer[4], ncb.ncb_buffer[5]);
}
//System::__linkproc__ LStrCmp(v36, &str_444553540000[1]);
if (memcmp(BiosInfo, "444553540000", 12))
{
break;
}
}
char DevInfo[9] = { 0 };
sprintf_s(DevInfo, 9, "%s%s", strVolumeSerialNumber, BiosInfo + 8);
In short: take the C: drive’s VolumeSerialNumber (first 4 bytes), and concatenate it with the last four bytes of the NetBIOS info, to form a string.
sub_72307C — license validation
int __userpurge sub_72307C@<eax>(
int a1@<eax>,
int a2@<edx>,
__int32 a3@<ecx>,
int a4@<edi>,
int a5@<esi>,
volatile __int32 *a6,
void *a7,
void *a8)
{
char v9; // zf
int v10; // eax
int v11; // eax
int v12; // eax
int v14; // [esp-10h] [ebp-80h]
unsigned int v15[2]; // [esp-Ch] [ebp-7Ch] BYREF
int *v16; // [esp-4h] [ebp-74h]
int v17; // [esp+4h] [ebp-6Ch] BYREF
unsigned int v18; // [esp+8h] [ebp-68h] BYREF
int v19; // [esp+Ch] [ebp-64h] BYREF
unsigned int v20; // [esp+10h] [ebp-60h] BYREF
int v21; // [esp+14h] [ebp-5Ch] BYREF
int v22; // [esp+18h] [ebp-58h] BYREF
int v23; // [esp+1Ch] [ebp-54h] BYREF
int v24; // [esp+20h] [ebp-50h] BYREF
int v25; // [esp+24h] [ebp-4Ch] BYREF
int v26; // [esp+28h] [ebp-48h] BYREF
int v27; // [esp+2Ch] [ebp-44h] BYREF
int v28; // [esp+30h] [ebp-40h] BYREF
int v29; // [esp+34h] [ebp-38h] BYREF
int v30; // [esp+38h] [ebp-38h] BYREF
int v31; // [esp+3Ch] [ebp-34h] BYREF
int v32; // [esp+40h] [ebp-30h] BYREF
int v33; // [esp+44h] [ebp-2Ch] BYREF
int v34; // [esp+48h] [ebp-28h] BYREF
unsigned int v35; // [esp+4Ch] [ebp-24h] BYREF
void *v36; // [esp+50h] [ebp-20h] BYREF
void *v37; // [esp+54h] [ebp-1Ch] BYREF
unsigned int v38; // [esp+58h] [ebp-18h] BYREF
int v39; // [esp+5Ch] [ebp-14h] BYREF
int v40; // [esp+60h] [ebp-10h] BYREF
char v41[4]; // [esp+64h] [ebp-Ch] BYREF
int v42; // [esp+68h] [ebp-8h]
int System__AnsiString; // [esp+6Ch] [ebp-4h] BYREF
int savedregs; // [esp+70h] [ebp+0h] BYREF
v42 = _InterlockedExchange(&System__AnsiString, a3);
System__AnsiString = a2;
System::__linkproc__ LStrAddRef(a2);
System::__linkproc__ LStrAddRef(v42);
System::__linkproc__ LStrAddRef(a8);
System::__linkproc__ LStrAddRef(a7);
if ( a6 )
*a6 = 0;
v16 = &savedregs;
v15[1] = (unsigned int)&loc_723387;
v15[0] = (unsigned int)NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, (unsigned int)v15);
if ( (unsigned __int8)sub_7242FC(v42, (int)a8) )
{
sub_7222AC();
System::__linkproc__ LStrCopy(System__AnsiString, 1, 4, (int)v41);
if ( (a1 != 1 || (System::__linkproc__ LStrCmp(*(unsigned int *)v41, (unsigned int)&str_LKEY[1]), v9))
&& (a1 || (System::__linkproc__ LStrCmp(*(unsigned int *)v41, (unsigned int)&str_DKEY[1]), v9)) )
{
Sysutils::StringReplace(System__AnsiString, (int)&str___436[1], 0, a1, a4, a5, (int)v41, 1);
System::__linkproc__ LStrCopy(*(int *)v41, 5, 4, (int)&v40);
System::__linkproc__ LStrCopy(*(int *)v41, 9, 4, (int)&v39);
System::__linkproc__ LStrCopy(*(int *)v41, 13, 4, (int)&v38);
System::__linkproc__ LStrCopy(*(int *)v41, 17, 4, (int)&v37);
System::__linkproc__ LStrCopy(*(int *)v41, 21, 4, (int)&v36);
System::__linkproc__ LStrCopy(*(int *)v41, 25, 4, (int)&v35);
System::__linkproc__ LStrLAsg(&v34, &str_ACTIVATION[1]);
sub_722D68(v38, &v32);
sub_722F90(v32, v39, &v33);
System::__linkproc__ LStrLAsg(&v38, v33);
sub_722D68(v37, &v30);
sub_722F90(v30, v39, &v31);
System::__linkproc__ LStrLAsg(&v37, v31);
sub_722D68(v36, &v28);
sub_722F90(v28, v39, &v29);
System::__linkproc__ LStrLAsg(&v36, v29);
sub_722D68(v35, &v26);
sub_722F90(v26, v40, &v27);
System::__linkproc__ LStrLAsg(&v35, v27);
sub_722D68(v39, &v24);
sub_722F90(v24, v40, &v25);
System::__linkproc__ LStrLAsg(&v39, v25);
v10 = sub_722230(v34);
Sysutils::IntToHex(abs32(v10), 4u, (int)&v22);
v14 = v22;
sub_722D68(v40, &v21);
sub_722F90(v21, v14, &v23);
System::__linkproc__ LStrLAsg(&v40, v23);
System::__linkproc__ LStrCat3((int)&v19, a8, a7);
v11 = sub_722230(v19);
Sysutils::IntToHex(abs32(v11), 4u, (int)&v20);
System::__linkproc__ LStrCmp(v38, v20);
if ( v9 )
{
System::__linkproc__ LStrCat3((int)&v17, v37, v36);
v12 = sub_722230(v17);
Sysutils::IntToHex(abs32(v12), 4u, (int)&v18);
System::__linkproc__ LStrCmp(v35, v18);
if ( v9 )
{
System::__linkproc__ LStrCat3((int)a6, v37, v36);
LOBYTE(a1) = 1;
}
else
{
a1 = 0;
}
}
else
{
a1 = 0;
}
}
else
{
a1 = 0;
}
}
else
{
System::__linkproc__ LStrAsg(a6, (__int32)&str_99991231_0[1]);
LOBYTE(a1) = 1;
}
__writefsdword(0, v15[0]);
v16 = (int *)&loc_72338E;
System::__linkproc__ LStrArrayClr(&v17, 27);
System::__linkproc__ LStrArrayClr(&a7, 2);
return a1;
}
sub_7242FCdecides whether to check the license — presumably for backward compatibility. In short: it returns 1 if ProductNo isGOACTIVATION, 0 ifNOACTIVATION; returns 0 if the Key’s first 6 bytes areOB-999; returns 0 if the Key’s first 7 bytes areOBR-999.- If
sub_7242FCreturns 0, no license check — the validity runs to 9999/12/31. If it returns 1, the license is checked. - The license check is a chain of
sub_722D68/sub_722F90calls — essentially table lookups for character conversion.sub_722D68finds the position inKeyTable2;sub_722F90finds the position inKeyTable. - The license must start with
LKEYorDKEY, with-removed. - After removing
-, the license is 28 bytes total. - Bytes 13–16, looked up in
KeyTable2in order 1, 2, 3, 4, minus the values of bytes 9–12 looked up inKeyTablein order 2, 3, 4, 1; if the difference < 1, add 36; use the result as an index intoKeyTableto get a character. - Bytes 17–20 and 21–24 go through the same table conversion as step 6. Note the converted 8 bytes should be a valid date — bytes 17–24 can be reverse-derived from
99991231. - ProductKey + the 8 bytes from
sub_722884are concatenated and computed withsub_722230; the result must match the step-6 result. - Bytes 25–28, looked up in
KeyTable2in order 1, 2, 3, 4, minus bytes 5–8 looked up inKeyTablein order 2, 3, 4, 1; if the difference < 1, add 36; use as an index intoKeyTable. - The 8-byte string from step 7 is computed with
sub_722230; the result must match the step-9 result.
That’s the whole registration check. One caveat: the table indexes are Delphi-based — 1-indexed — while C is 0-indexed, so subtract 1 when indexing characters in C.
Everything from OB11 onward uses the same validation logic; only KeyBase and TailFlag differ per version, so find those for your target version yourself.
Finally, a successful OB24 registration:

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch