License To Rev

We were trying to get the flag from this binary we purchased a few months ago, but we lost the license, maybe you can help?

Initial recon

Quick analysis of the file with file, checksec, and strings:

$ file license_to_rev
license_to_rev: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=57b17cbf053e49beb230558396cb8f08f90e7daa, for GNU/Linux 4.4.0, not stripped
                                                                                                                                                                                    
$ checksec license_to_rev
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/kali/.cache/.pwntools-cache-3.13/update to 'never' (old way).
    Or add the following lines to ~/.pwn.conf or ~/.config/pwn.conf (or /etc/pwn.conf system-wide):
        [update]
        interval=never
[*] A newer version of pwntools is available on pypi (4.14.1 --> 4.15.0).
    Update with: $ pip install -U pwntools
[*] '/home/kali/metactf_ctf 2026 feb/license_to_rev'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No


$ strings license_to_rev
...
Error: invalid embedded license archive.
That is not the correct license. Invalid license.
This license has expired. Please contact support for a new license.
EXPIRY_DATE=
license.txt=
license.txtPK
EMBEDDED_ZIP
ENCRYPTED_MESSAGE
...

$ ./license_to_rev         
Usage: ./license_to_rev <license-file>

A license file is required to use this product.
Each copy is individually licensed. Please provide the path to your
license file. Without a valid license, the program cannot continue.

Key observations:

It’s a 64-bit binary with several protections enabled, including stack canaries, NX, and PIE, which make straightforward buffer overflow and code-injection attacks more difficult. However, it only has Partial RELRO.
Since the binary is not stripped, symbol information is available, making static analysis and reversing easier. Overall, it’s relatively hardened, but not fully locked down.

The strings output suggests the binary validates a license file and contains multiple failure paths, such as “invalid embedded license archive,” “not the correct license,” and “license has expired.” References like EXPIRY_DATE=, license.txt, EMBEDDED_ZIP, and PK (ZIP file signature) imply the license may be a ZIP archive embedded or parsed by the program.
The presence of ENCRYPTED_MESSAGE also hints that some part of the license or validation logic involves encryption rather than plain-text comparison.

IDA and Static Analysis

int __fastcall main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rbx
  FILE *v4; // rax
  FILE *v5; // rbp
  int v6; // r14d
  __int64 v7; // r12
  char *v8; // rax
  char *v9; // rbx
  size_t v10; // r13
  void *v11; // r12
  char *v12; // rdi
  __int64 i; // rcx
  int v14; // r14d
  FILE *v15; // rcx
  size_t v16; // rdx
  const char *v17; // rdi
  int v18; // r13d
  char *v20; // rdi
  struct tm *v21; // rax
  int v22; // edx
  _BYTE *v23; // r14
  _BYTE *v24; // r15
  unsigned __int8 v25; // di
  int v26; // edx
  int *v27; // rax
  char *v28; // rax
  int v29; // [rsp+4h] [rbp-C4h] BYREF
  int v30; // [rsp+8h] [rbp-C0h] BYREF
  int v31; // [rsp+Ch] [rbp-BCh] BYREF
  time_t timer; // [rsp+10h] [rbp-B8h] BYREF
  int v33; // [rsp+18h] [rbp-B0h]
  char v34; // [rsp+1Ch] [rbp-ACh] BYREF
  void *v35; // [rsp+28h] [rbp-A0h]
  int v36; // [rsp+30h] [rbp-98h]
  unsigned __int64 v37; // [rsp+88h] [rbp-40h]

  v37 = __readfsqword(0x28u);
  if ( argc <= 1 )
  {
    fprintf(
      stderr,
      "Usage: %s <license-file>\n"
      "\n"
      "A license file is required to use this product.\n"
      "Each copy is individually licensed. Please provide the path to your\n"
      "license file. Without a valid license, the program cannot continue.\n",
      *argv);
LABEL_18:
    v18 = 1;
    goto LABEL_19;
  }
  v3 = argv[1];
  v4 = fopen(v3, "rb");
  v5 = v4;
  if ( !v4 )
  {
    v27 = __errno_location();
    v28 = strerror(*v27);
    fprintf(stderr, "Error: cannot open '%s': %s\n", v3, v28);
    goto LABEL_18;
  }
  v6 = fseek(v4, 0LL, 2);
  if ( v6 || (v7 = ftell(v5), v7 < 0) || (rewind(v5), v8 = (char *)malloc(v7 + 1), (v9 = v8) == 0LL) )
  {
    fclose(v5);
    goto LABEL_18;
  }
  v10 = fread(v8, 1uLL, v7, v5);
  fclose(v5);
  if ( v7 != v10 )
  {
LABEL_30:
    free(v9);
    goto LABEL_18;
  }
  v9[v7] = 0;
  v11 = malloc(0x155uLL);
  if ( !v11 )
  {
LABEL_29:
    fwrite("Error: invalid embedded license archive.\n", 1uLL, 0x29uLL, stderr);
    goto LABEL_30;
  }
  v12 = &v34;
  for ( i = 0x19LL; i; --i )
  {
    *(_DWORD *)v12 = v6;
    v12 += 4;
  }
  timer = (time_t)&unk_2249;
  v33 = 0x108;
  v35 = v11;
  v36 = 0x155;
  if ( (unsigned int)inflateInit2_(&timer, 0xFFFFFFF1LL, "1.3.1", 0x70LL)
    || (v14 = inflate(&timer, 4LL), inflateEnd(&timer), v14 != 1)
    || v36 )
  {
    free(v11);
    goto LABEL_29;
  }
  if ( v10 != 0x155 || memcmp(v9, v11, 0x155uLL) )
  {
    v15 = stderr;
    v16 = 0x32LL;
    v17 = "That is not the correct license. Invalid license.\n";
LABEL_17:
    fwrite(v17, 1uLL, v16, v15);
    free(v9);
    free(v11);
    goto LABEL_18;
  }
  v20 = v9;
  while ( *(_QWORD *)v20 != 0x445F595249505845LL || *((_DWORD *)v20 + 2) != 0x3D455441 )
  {
    if ( v9 + 0x14A == ++v20 )
      goto LABEL_27;
  }
  v18 = 0;
  if ( (unsigned int)__isoc99_sscanf(v20 + 0xC, "%d-%d-%d", &v29, &v30, &v31) != 3
    || (unsigned int)(v30 - 1) > 0xB
    || (unsigned int)(v31 - 1) > 0x1E
    || (timer = time(0LL), timer == 0xFFFFFFFFFFFFFFFFLL)
    || (v21 = localtime(&timer)) == 0LL
    || (v22 = v21->tm_year + 0x76C, v22 > v29)
    || v22 == v29 && ((v26 = v21->tm_mon + 1, v26 > v30) || v26 == v30 && v21->tm_mday > v31) )
  {
LABEL_27:
    v15 = stderr;
    v16 = 0x44LL;
    v17 = "This license has expired. Please contact support for a new license.\n";
    goto LABEL_17;
  }
  v23 = &ENCRYPTED_MESSAGE;
  v24 = v9;
  do
  {
    v25 = *v24++ ^ *v23++;
    putc(v25, _bss_start);
  }
  while ( v23 != (char *)&ENCRYPTED_MESSAGE + 0x23 );
  putc(0xA, _bss_start);
  free(v9);
  free(v11);
LABEL_19:
  if ( v37 != __readfsqword(0x28u) )
    start();
  return v18;
}

The pseudocode in IDA reveals a five-stage validation process.

Stage 1 - Read the license file.

The program opens argv[1] with fopen(), seeks to the end with fseek(), and ftell() to get the file size. It then mallocs() a buffer and reads the entire file into it.
The file size is stored for a later comparison.

Stage 2 - Inflate the embedded ZIP.

The code calls inflateInit2_(), inflate(), and inflateEnd() which are part of the zlib decompression API. The input stream pointer is set to EMBEDDED_ZIP, a global symbol.

  v33 = 0x155;
  if ( (unsigned int)inflateInit2_(&timer, 0xFFFFFFF1LL, "1.3.1", 0x70LL)
    || (v14 = inflate(&timer, 4LL), inflateEnd(&timer), v14 != 1)
    || v33 )
  {
    free(v11);
    goto LABEL_29;
  }

The output buffer is exactly 0x155 = 341 bytes. If decompression fails or there are bytes left over, it jumps to the error path.

Stage 3 - memcmp() against the license file.

This is the core check. IDA shows a memcmp call:

  if ( v10 != 0x155 || memcmp(v9, v11, 0x155uLL) )
  {
    v15 = stderr;
    v16 = 0x32LL;
    v17 = "That is not the correct license. Invalid license.\n";
LABEL_17:
    fwrite(v17, 1uLL, v16, v15);
    free(v9);
    free(v11);
    goto LABEL_18;
  }

The license file must be byte-for-byte identical to the content of license.txt inside the embedded ZIP. The correct license is baked into the binary.

Stage 4 - Expiry date check.

After the memcmp passes, the code scans the license buffer for the string EXPIRY_DATE= and parses a YYYY-MM-DD date with sscanf. It then compares that against localtime(time(NULL)).

  v20 = v9;
  while ( *(_QWORD *)v20 != 0x445F595249505845LL || *((_DWORD *)v20 + 2) != 0x3D455441 )
  {
    if ( v9 + 0x14A == ++v20 )
      goto LABEL_27;
  }
  v18 = 0;
  if ( (unsigned int)__isoc99_sscanf(v20 + 0xC, "%d-%d-%d", &v26, &v27, &v28) != 3
    || (unsigned int)(v27 - 1) > 0xB
    || (unsigned int)(v28 - 1) > 0x1E
    || (timer = time(0LL), timer == 0xFFFFFFFFFFFFFFFFLL)
    || !localtime(&timer) )
  {
LABEL_27:
    v15 = stderr;
    v16 = 0x44LL;
    v17 = "This license has expired. Please contact support for a new license.\n";
    goto LABEL_17;
  }

Stage 5 - XOR decryption for the flag.

If all checks pass, the code XORs the first 35 bytes (0x23) of the license file against ENCRYPTED_MESSAGE (another global in .rodata) and prints each byte with putc().

  v21 = &ENCRYPTED_MESSAGE;
  v22 = v9;
  do
  {
    v23 = *v22++ ^ *v21++;
    putc(v23, _bss_start);
  }
  while ( v21 != (char *)&ENCRYPTED_MESSAGE + 0x23 );

This is the flag output. The ENCRYPTED_MESSAGE bytes at offset 0x21E0 in the binary are XOR’d with the license plaintext to reveal the flag.

Extracting the embedded license

A Python script to extract the ZIP file from the binary.

import struct, zlib

with open('./license_to_rev', 'rb') as f:
    data = f.read()

print(f"File size: {len(data)} bytes")

pk = data.find(b'PK\x03\x04')
print(f"PK magic found at offset: {hex(pk)}")

if pk == -1:
    print("No ZIP found")
else:
    z = data[pk:]

    _, _, _, comp, _, _, _, comp_sz, _, fname_len, extra_len = \
        struct.unpack('<4sHHHHHIIIHH', z[:30])

    print(f"Compression method: {comp}")
    print(f"Compressed size: {comp_sz}")
    print(f"Fname len: {fname_len}, Extra len: {extra_len}")

    offset = 30 + fname_len + extra_len
    raw = z[offset : offset + comp_sz]

    if comp == 0:
        license_bytes = raw
    elif comp == 8:
        license_bytes = zlib.decompress(raw, -15)
    else:
        print(f"Unknown compression method: {comp}")
        license_bytes = raw

    print(license_bytes.decode(errors='replace'))

license flag extracted

Getting to the flag

We have two paths: decrypt statically (no execution needed) or patch the binary to skip the expiry check and run it.

Patch the binary.

NOP out the conditional jump at 0x1467 and 0x146D by changing the bytes to 0x90

Address 0x1467 - jumps to the “expired” path if year is already past.
Address 0x146D - jumps to “the expired” path if same year.

AddressOriginalPatch
0x14670F 8F 63 FF FF FF90 90 90 90 90 90
0x146D74 6890 90

Save the license that was extracted earlier and pass it to binary.

rev flag

Binary patch.
Download rev_patch.diff

Apply the patch.

bspatch license_to_rev license_to_rev_patched rev_patch.diff

Alternatively, this can be done via a static XOR decryption without the need of binary patching.

import struct, zlib

with open('license_to_rev', 'rb') as f:
    data = f.read()

# 1. Decompress embedded license
pk = data.find(b'PK\x03\x04')
z  = data[pk:]
_, _, _, comp, _, _, _, comp_sz, _, fl, el = \
    struct.unpack('<4sHHHHHIIIHH', z[:30])
license_bytes = zlib.decompress(z[30+fl+el:30+fl+el+comp_sz], -15)

# 2. Extract ENCRYPTED_MESSAGE (35 bytes at 0x21E0)
enc = data[0x21E0 : 0x21E0 + 0x23]

# 3. XOR to recover the flag
flag = bytes(a ^ b for a, b in zip(enc, license_bytes))
print(flag.decode())