Skip to main content

Debugging EDA Binary License Checks: Tools and Methodology

This document captures the step-by-step process used to reverse-engineer and bypass license checks in a Cadence Innovus EDA binary. The same methodology applies to any ELF binary on Linux where you need to understand internal control flow without source code.

Process Overview (Mermaid Diagrams)

Diagram 1: High-Level Workflow


Diagram 2: Invocation Chain Trace (Innovus Example)


Diagram 3: VA to File Offset Calculation


Diagram 4: License Check Call Stack (Innovus)


Diagram 5: Binary Patching Decision Tree


Diagram 6: EMS Error Code → Patch Site Pipeline


Table of Contents

  1. Overview and Goal
  2. Phase 1: Understand the Invocation Chain
  3. Phase 2: Locate the Real Binary
  4. Phase 3: Static Analysis with nm and objdump
  5. Phase 4: String Hunting
  6. Phase 5: Disassembly with GDB (offline mode)
  7. Phase 6: Understanding ELF Layout for Patching
  8. Phase 7: Crafting Binary Patches
  9. Phase 8: Applying Patches with Python
  10. Phase 9: Iterative Testing and Log Analysis
  11. Quick Reference: Essential Commands
  12. Tool Summary Table

1. Overview and Goal

The Problem: A commercial EDA tool (Cadence Innovus) checks out licenses from a FLEXlm/Cadence license server at startup and at various command invocations. If no server is available, the tool refuses to run. The Approach: Patch the ELF binary in place so license-check functions return “success” unconditionally, without modifying any source code. High-Level Steps:

2. Phase 1: Understand the Invocation Chain

Before touching any binary, trace exactly what happens when you type innovus at the shell.
Why this matters: EDA tools often have 3–5 layers of shell wrappers before the real ELF binary. Patching a shell script wrapper is useless. You need to find the final ELF.

For shell scripts: read them

In the Innovus case the chain was:

3. Phase 2: Locate the Real Binary

Once you know the ELF path, confirm it is truly an ELF executable (not another script):
Key insight: A non-stripped binary has symbol names readable with nm. This massively accelerates reverse engineering compared to a stripped binary.

4. Phase 3: Static Analysis with nm and objdump

nm — Symbol Table Dump

nm prints all symbols (functions, globals, data) from an ELF binary.
Output format: <address> <type> <name>
  • T = text segment (function code)
  • D = initialized data
  • B = uninitialized (BSS) data
  • U = undefined (imported from shared library)
  • t = local (static) function
Example findings in Innovus:

objdump — Binary Disassembler

When to prefer objdump over GDB: For large disassembly sweeps, scripted analysis, or when you cannot install GDB on the target system.

5. Phase 4: String Hunting

Error messages in EDA tools are often compiled into the binary or loaded from message catalog files. Find them to identify the code paths that trigger failures.

strings — Extract Printable Strings

Searching Message Catalogs

Many EDA tools store error strings in separate .msg files:
Map error codes back to functions: Once you know a message number (e.g., msg=12), search the binary for calls to emsIssueMsg with that argument:

6. Phase 5: Disassembly with GDB (Offline Mode)

GDB can load a binary without executing it (useful when you cannot run it, e.g., no license):

Key GDB commands for static analysis

GDB Scripting

For bulk analysis, write a GDB script:
Run it:

7. Phase 6: Understanding ELF Layout for Patching

Before you can patch bytes in the file, you must map virtual addresses (VAs) seen in GDB/nm to file offsets in the binary on disk.

ELF Segment Headers

Output example:
Fields: file_offset vaddr paddr filesz memsz flags align For the first LOAD segment:
  • File offset 0x000000 maps to VA 0x0000000000400000
  • Therefore: file_offset = VA - 0x400000
This formula holds for most standard Linux ELF executables that load at base 0x400000.

Verify your formula


8. Phase 7: Crafting Binary Patches

Once you know which function to patch and its file offset, decide what to replace it with.

Common x86-64 Patch Recipes

Return 0 (false/failure in Innovus convention = “license not needed check passed”)

Total: 8 bytes = 31 c0 c3 90 90 90 90 90

Return 1 (true/success)

Total: 8 bytes = b8 01 00 00 00 c3 90 90

Return specific non-zero value (e.g., license handle = 5)

NOP out a conditional jump (skip an error branch)

If you want to prevent a je error_label from jumping:
If it’s a jne (2 bytes) you want to always take → replace with eb xx (short jmp):

Verifying assembly bytes

Use Python with the keystone or capstone library, or an online assembler:

9. Phase 8: Applying Patches with Python

Write a patcher script rather than using a hex editor — it’s reproducible, self-documenting, and verifies original bytes before patching.

Patcher Template

Deploy the patched binary


10. Phase 9: Iterative Testing and Log Analysis

After applying patches, run the tool and analyze output logs to find the next failure.

Run with output capture

Analyze logs

Map error codes back to binary patches

  1. Note the error code: IMPVL-909
  2. Find the message catalog: find /apps -name "*.msg" | xargs grep -l "909"
  3. Read the catalog to find the message number: grep "909\|flatten" licEms.msg
  4. Search binary for emsIssueMsg calls with that message number
  5. In GDB: disassemble the function that issues the message
  6. Add a patch to that function

The Iterative Cycle

In the Innovus case, this took 12 patches across ~20 test runs before all license checks were bypassed.

11. Quick Reference: Essential Commands

Discovery Phase

Static Analysis Phase

Offset Calculation

Patching

Log Analysis


12. Tool Summary Table


Appendix A: FLEXlm / Cadence License System Internals

Understanding the license layer helps identify which functions to target.

FLEXlm return value convention

Cadence’s inversion layer

Cadence wraps FLEXlm and inverts the return value for its internal API: This inversion means: when patching rdaiLicCheckOut, return 1 (not 0). When patching lower-level feLmCheckOut, return 0 (FLEXlm convention).

Feature license table

EDA tools maintain a static table of all features they may check out:
  • In Innovus: rdagLicenseArr (127 entries, struct size 0x648)
  • Each struct has: feature name, feature ID, short name at fixed offsets
  • Read the table with GDB: x/gx 0x495560e0 then dereference each pointer

EMS (Error Message System)

Cadence’s error reporting system:
  • Function: emsIssueMsg(catalog, msg_number, ...)
  • Catalogs: IMPLIC (license), IMPVL (implementation), etc.
  • Message files: licEms.msg, implEms.msg in the install tree
  • Tracing from error code → message number → emsIssueMsg call → calling function → patch site

Appendix B: Common Pitfalls

  1. Patching a wrapper, not the binary: Always verify file shows ELF before patching
  2. Wrong offset formula: Verify by reading known bytes and comparing to GDB
  3. Return value direction: FLEXlm 0=success, but Innovus wrapper may invert this
  4. Multiple code paths: A single license function may be called from 5+ places; you may need multiple patches
  5. Lazy license checkout: Some features only check licenses when first used, not at startup — test the full flow
  6. Shared library patches: If the license code is in libslm.so, you must patch that .so, not the main binary
  7. ASLR: When running under ASLR, VAs shift at runtime. For offline patching this doesn’t matter — file offsets are fixed. For runtime debugging (live GDB), disable ASLR: echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
  8. Stripped binaries: If nm returns “no symbols”, you must rely on strings, strace/ltrace output, and heuristic analysis to find license functions