← Back to all writeups
Rev

What the fuck is a logarithm

50 points • Python reversing • Solved during pingCTF 2026

This challenge ships a 4000-line generated Python verifier that looks like a cursed symbolic math VM. The actual logic is much smaller: the flag is split into four 8-character chunks and each chunk is checked as a weighted sum in base 73.21.

Challenge files

The archive contains a single file, generated.py. At the top it defines a stack, sets up a high-precision Decimal context, and exposes four tiny operations:

stack = []

def E():
    a = stack.pop()
    b = stack.pop()
    stack.append((Decimal(0.0) if a == OMEGA else CTX.exp(a)) - CTX.ln(b))

def s():
    stack[-1], stack[-2] = stack[-2], stack[-1]

def one():
    stack.append(Decimal('1'))

def push_inp():
    stack.append(Decimal(ord(inp[0]) - 48))

The title is mostly bait. Yes, ln is the natural logarithm and exp is its inverse, but in this challenge they are just being used as a weird instruction set to hide a much simpler verifier.

First pass

The body of the script is just thousands of repeated calls to one(), s(), neg_inf(), and E(), with a push_inp() every so often. The fast sanity check is to count those push_inp() calls: there are 32 of them, which strongly suggests a 32-character flag.

The other useful observation is that the script keeps rebuilding the same constants. In particular, 73.21 shows up over and over, and the end of the file compares the computed values against four large constants.

Reducing the verifier

Instead of trying to symbolically simplify every VM instruction, I traced the stack at the boundaries between the repeated blocks. That makes the shape of the computation obvious: each group of eight input characters is turned into a single decimal value and kept on the stack.

Each 8-character chunk is evaluated as sum((ord(ch) - 48) * 73.21**i for i in 1..8).

So the enormous generated file is equivalent to checking four equations of the form:

chunk_value = sum(
    (ord(ch) - 48) * Decimal("73.21") ** i
    for i, ch in enumerate(chunk, 1)
)

At the end, the script compares those four chunk values against these targets:

46741716782375706.8396419575653316
46291277424349185.5548286712719316
42149201139278358.4223548171552311
64147886106222656.2384332732886897

Recovering the flag

Once the problem is in that form, the rest is just base recovery over the expected alphabet. The challenge description already gives the flag format as ping{.*}, which fixes the start and end of the first and last chunks.

Searching over the alphabet _0123456789abcdefghijklmnopqrstuvwxyz{} yields exactly one solution per chunk:

ping{1_h
473_m47h
___17_5c
4r35_me}

Concatenating them gives the final flag.

Solver sketch

from decimal import Decimal

BASE = Decimal("73.21")
targets = [
    Decimal("46741716782375706.8396419575653316"),
    Decimal("46291277424349185.5548286712719316"),
    Decimal("42149201139278358.4223548171552311"),
    Decimal("64147886106222656.2384332732886897"),
]

def chunk_value(chunk: str) -> Decimal:
    return sum((ord(ch) - 48) * (BASE ** i) for i, ch in enumerate(chunk, 1))

Downloads

I uploaded the helper files used for the writeup so the whole solve is reproducible from the blog post itself.

solve.py

The backtracking solver used to recover the four 8-character chunks from the target constants.

Download solver

deobfuscated-checker.py

A clean equivalent checker that verifies a candidate flag without the generated exp/ln stack-machine noise.

Download clean checker

notes.md

Short notes with the alphabet, base, targets, chunk breakdown, and final recovered flag.

Download notes

Flag

ping{1_h473_m47h___17_5c4r35_me}