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:

Packer check

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.

Searching for Tfrm

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:

  1. Check whether the current user is an administrator; if not, error.
  2. Check that ProductNo and ProductKey are non-empty; if either is empty, error.
  3. 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.)
  4. Call sub_7245F8; if it returns 0, error. (Detailed below.)
  5. Call sub_722884 to get the hardware info. (Detailed below.)
  6. Call sub_72307C to validate the license.
  7. 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:

  1. ProductKey must start with OB-.
  2. Call sub_7242A8 to check whether bytes 4–6 after OB- 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".
  3. 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.

  1. 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.

  1. Concatenate the results from step 3, and look up position 1 in this table, byte by byte:
    char KeyTable[]  = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    char KeyTable2[] = "T6ERUB7391GW8Q04YZ5KDICA2FMXSVLPHNOJ";
  1. Look up the step-4 result in KeyTable in byte order 2, 3, 4, 1 — that’s position 2.
  2. Position 1 + position 2; if greater than 36, subtract 36; look up the character at that position in KeyTable. 12 bytes total.
  3. In the resulting string, lowercase every 3rd byte, and insert - after the 6th byte.
  4. 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;
}
  1. sub_7242FC decides whether to check the license — presumably for backward compatibility. In short: it returns 1 if ProductNo is GOACTIVATION, 0 if NOACTIVATION; returns 0 if the Key’s first 6 bytes are OB-999; returns 0 if the Key’s first 7 bytes are OBR-999.
  2. If sub_7242FC returns 0, no license check — the validity runs to 9999/12/31. If it returns 1, the license is checked.
  3. The license check is a chain of sub_722D68 / sub_722F90 calls — essentially table lookups for character conversion. sub_722D68 finds the position in KeyTable2; sub_722F90 finds the position in KeyTable.
  4. The license must start with LKEY or DKEY, with - removed.
  5. After removing -, the license is 28 bytes total.
  6. Bytes 13–16, looked up in KeyTable2 in order 1, 2, 3, 4, minus the values of bytes 9–12 looked up in KeyTable in order 2, 3, 4, 1; if the difference < 1, add 36; use the result as an index into KeyTable to get a character.
  7. 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.
  8. ProductKey + the 8 bytes from sub_722884 are concatenated and computed with sub_722230; the result must match the step-6 result.
  9. Bytes 25–28, looked up in KeyTable2 in order 1, 2, 3, 4, minus bytes 5–8 looked up in KeyTable in order 2, 3, 4, 1; if the difference < 1, add 36; use as an index into KeyTable.
  10. 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:

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