📃 Challenge Description

So you think you cracked some license checkers before? Try this unique license checker and see if you have the requirements to crack all software!

The web-service is only used to verify the valid license and get the flag. You don’t need to attack it or do any scanning against it!

🔎 Research

I decompiled the binary and inferred some easy license chars:

state.solver.add(license_chars[0] == 0x41)
state.solver.add(license_chars[1] == 0x31)
state.solver.add(license_chars[2] == 0x4B)
state.solver.add(license_chars[3] != 0x32)
state.solver.add(license_chars[4] == 0x42)
state.solver.add(license_chars[0xC] == 0x42)
state.solver.add(license_chars[0xD] == license_chars[0x14] - 5)
state.solver.add(license_chars[0xE] == 0x38)
state.solver.add(license_chars[0xF] == 0x32)
state.solver.add(license_chars[0x10] == license_chars[0x19]

# [...]

expected_data = b"A@F01"
for idx, b in enumerate(expected_data):
    found_state.solver.add(found_state.memory.load(found_state.regs.rsi + idx, 1) == b)

Run angr with call_func() state preset until right before the strcmp("A@F01", data) call and with the above constraints gives some more concrete constraints:

Untitled

in particular, these constraints are added:

state.solver.add(license_chars[9] == 0x4C)
state.solver.add(license_chars[0x13] == 0x30)
state.solver.add(license_chars[0x1B] == 0x4E)

After the strcmp call, we can see that another function is called with license[0x12:] as an argument. license[0x12:] are the last two blocks of the license. The called function then populates some stack memory, which is then checked with memcmp against a static byte sequence embedded in the binary: c5366939f46a0f6bc44cfad38f99c0ce. This looked suspiciously like some hash, so I run some hash identifier and got the information that this is likely md5. A quick crypto scan confirms this theory:

Untitled1

So I ran hashcat with the following options:

hashcat -m0 -O -w 3 --keep-guessing -1 ?d?u -a3 c5366939f46a0f6bc44cfad38f99c0ce "?10?1?1?1-?1?1?1N?1"

After 31 seconds I got a result:

Untitled2

Now we can update our constraints of the last two blocks to match the obtained string:

for c, b in zip(license_chars[0x12:], b"00GHK-L1MNS"):
    state.solver.add(c == b)

After running our solve script again, we get the following output:

Untitled3

The license has a single concrete value!

🔐 Exploit Program

import angr
import claripy

license_len = 0x1D
base_addr = 0x400000
check_license_addr = base_addr + 0x2030

def const_strlen(len):
    def _strlen(s):
        s.regs.rax = len

    return _strlen

def strncpy_hook(s):
    print("strncpy")
    dst_addr = s.regs.rdi
    src_addr = s.regs.rsi
    n = s.regs.rdx
    src_mem = s.memory.load(src_addr, n)
    s.memory.store(dst_addr, src_mem, size=n)
    s.regs.rax = dst_addr

def strcmp_noop(s):
    s.regs.rax = 0x0

project = angr.Project(
    "./licensecheck", main_opts={"base_addr": base_addr}, auto_load_libs=False
)
project.hook(base_addr + 0x1DCA, const_strlen(0xB), length=5)
project.hook(base_addr + 0x2058, const_strlen(license_len), length=5)
project.hook(base_addr + 0x2300, strncpy_hook, length=5)
project.hook(base_addr + 0x2320, strncpy_hook, length=5)
project.hook(base_addr + 0x233F, strncpy_hook, length=5)
project.hook(base_addr + 0x234E, const_strlen(0x5), length=5)
project.hook(base_addr + 0x235B, const_strlen(0x5), length=5)
project.hook(base_addr + 0x23DE, const_strlen(0x5), length=5)
project.hook(base_addr + 0x23EB, const_strlen(0x5), length=5)
project.hook(base_addr + 0x244F, strcmp_noop, length=5)
project.hook(base_addr + 0x2469, strncpy_hook, length=5)

license_chars = [claripy.BVS(f"license_{i}", 8) for i in range(license_len)]
license = claripy.Concat(*license_chars)

state = project.factory.call_state(
    check_license_addr, angr.PointerWrapper(license, buffer=True)
)

# state.options.add(angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY)
state.options.add(angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS)
for idx, c in enumerate(license_chars):
    if (idx + 1) % 6 == 0:
        state.solver.add(c == 0x2D)
    else:
        state.solver.add(
            claripy.Or(
                claripy.And(c <= 0x39, c >= 0x30), claripy.And(c <= 0x5A, c >= 0x41)
            )
        )

state.solver.add(license_chars[0] == 0x41)
state.solver.add(license_chars[1] == 0x31)
state.solver.add(license_chars[2] == 0x4B)
state.solver.add(license_chars[3] != 0x32)
state.solver.add(license_chars[4] == 0x42)
state.solver.add(license_chars[9] == 0x4C)
state.solver.add(license_chars[0xC] == 0x42)
state.solver.add(license_chars[0xD] == license_chars[0x14] - 5)
state.solver.add(license_chars[0xE] == 0x38)
state.solver.add(license_chars[0xF] == 0x32)
state.solver.add(license_chars[0x10] == license_chars[0x19])
state.solver.add(license_chars[0x13] == 0x30)
state.solver.add(license_chars[0x1B] == 0x4E)
for c, b in zip(license_chars[0x12:], b"00GHK-L1MNS"):
    state.solver.add(c == b)

def log_insn(insn):
    print(str(insn))

state.inspect.b("instruction", action=log_insn)

simgr = project.factory.simgr(state, veritesting=True)
print(str(simgr.stashes))
simgr.explore(
    # find=base_addr + 0x22EA,
    find=base_addr + 0x244A,
    # find=base_addr + 0x245C,
    avoid=[base_addr + 0x20A4],
)
print(str(simgr.stashes))
found_state = simgr.found[0]

expected_data = b"A@F01"
for idx, b in enumerate(expected_data):
    found_state.solver.add(found_state.memory.load(found_state.regs.rsi + idx, 1) == b)

# -------------------------------------------------------------------------------
# General solver output stuff

found_state.solver._solver.timeout = 1000 * 60 * 10  # 10 minutes

width = len(str(len(license_chars)))
print("|idx", end="")
for i in range(len(license_chars)):
    print("|" + str(i).zfill(width), end="")
print("|")
print("|---", end="")
for i in range(len(license_chars)):
    print("|" + "-" * width, end="")
print("|")
print("|min", end="")
for c in license_chars:
    min_char = chr(found_state.solver.min(c))
    print("|" + min_char.rjust(width), end="")
print("|")
print("|max", end="")
for c in license_chars:
    max_char = chr(found_state.solver.max(c))
    print("|" + max_char.rjust(width), end="")
print("|")

min_license = found_state.solver.min(license)
max_license = found_state.solver.max(license)
print("total min_license: " + int.to_bytes(min_license, license_len, "big").decode())
print("total max_license: " + int.to_bytes(max_license, license_len, "big").decode())

💥 Run Exploit

Untitled4

Untitled5

FLAG: CSCG{l1c3ns3_r3v_m4st3r}

🗃️ Further References

angr documentation