Using this CSAW qualifier as a means to test our the tool called revenge I've been working on. This challenge was an easy reversing one, but it was made easier through revenge.


beleaf: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=6d305eed7c9bebbaa60b67403a6c6f2b36de3ca4, stripped


Strings didn't reveal too much, so naturally we give it a run next:


Enter the flag
>>> test


Strait forward enough. It prompts for flag and then decides if it's correct. Using ltrace we can see that there's a string length check, but not much else. At this point, I opened it up in Ghidra. Here's main


int main(void)

  long lVar1;
  size_t iMyInputLen;
  long transformed;
  long in_FS_OFFSET;
  ulong iCounter;
  char bufMyInput [136];
  long canary;
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter the flag\n>>> ");
  iMyInputLen = strlen(bufMyInput);
  if (iMyInputLen < 0x21) {
                    /* WARNING: Subroutine does not return */
  iCounter = 0;
  while (iCounter < iMyInputLen) {
    transformed = do_transform((ulong)(uint)(int)bufMyInput[iCounter]);
    if (transformed != *(long *)(&correct_lookup + iCounter * 8)) {
                    /* WARNING: Subroutine does not return */
    iCounter = iCounter + 1;
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
  return 0;


We can immediately see the string length check, telling us the string must be at least 33 chars. More importantly, we see there's a loop checking the input string character by character against some "correct" buffer. Running the program, we find that our input gets translated into something else prior to the buffer check. This means we can't simply rip the flag out of the binary. That said, given that we have a function handy that performs the translation for us, as well as the expected output, this puts us in a great position to perform dynamic analysis.


To be honest, I didn't bother trying to reverse the actual transformation function. It wasn't needed. Instead, I used revenge to simply enumerate all the possible transformations (i.e.: a-z), then use the lookup table to figure out the flag.


To start, let's use revenge to pull out the winning array, dynamically.


from revenge import Process

# Load up the process
p = Process("./beleaf")

# Correct output table is at 0x2014E0 in program
# revenge correctly handles ASLR and relocations to find this
mem = p.memory['beleaf:0x2014E0']

# We want to grab the address
addr = mem.address

correct = []

# We know there should be 0x21 of these
for i in range(0x21):
    # p.memory[addr] will load up the relevant MemoryBytes object
    # int64 property will pull out a signed 64bit int from that location
    correct.append(p.memory[addr + (i*8)].int64)

# Output at this point:
assert correct == [1, 9, 17, 39, 2, 0, 18, 3, 8, 18, 9, 18, 17, 1, 3, 19, 4, 3, 5, 21, 46, 10, 3, 10, 18, 3, 1, 46, 22, 46, 10, 18, 6]


Now that we know what the correct integers should be, let's be lazy and simply use the program function itself to enumerate the possibilities, instead of attempting to manually reverse it.


# Define a couple lookup dictionaries
l = {}
l2 = {}

# This address is the lookup function that the binary itself is using
lookup = p.memory['beleaf:0x7FA']

# Since it's taking in a char, let's just brute force a char
for i in range(256):
    # Here, we are literally injecting and calling this native lookup function ourselves
    x = lookup(i)
    # Save the results off into our dictionaries
    l[i] = x
    l2[x] = i


That's really all there was to it. We can now use a little python to paste everything together:


''.join(chr(l2[x]) for x in correct)
# 'flag{we_beleaf_in_your_re_future}'


Full script here

Binary here