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'))

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.
| Address | Original | Patch |
|---|---|---|
| 0x1467 | 0F 8F 63 FF FF FF | 90 90 90 90 90 90 |
| 0x146D | 74 68 | 90 90 |
Save the license that was extracted earlier and pass it to binary.

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())