4 minutes
Most unique license checker Write-Up
📃 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:
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:
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:
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:
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
FLAG: CSCG{l1c3ns3_r3v_m4st3r}