IDA loads the routerlocker binary with no complaints. Browsing the call graph, we quickly find the goodboy block in the lower right.

.text:00400D10 loc_400D10:

.text:00400D10 lui $v0, 0x40

.text:00400D14 addiu $a0, $v0, (aThankYouForPur - 0x400000) $ "Thank
you for purchasing RouterLocker v"...

.text:00400D18 jal puts

.text:00400D1C move $at, $at

.text:00400D20 lui $v0, 0x40

.text:00400D24 addiu $v1, $v0, (aYourFlagIsPanS - 0x400000) $ "Your
flag is: PAN%s\\n"

.text:00400D28 addiu $v0, $fp, 0x98+var_34

.text:00400D2C move $a0, $v1 $ format

.text:00400D30 move $a1, $v0

.text:00400D34 jal printf

With the flag stored in var_34, let’s look for cross-references. First we see the variable is initialized.

.text:004008EC sw $zero, 0x98+var_34($fp) $ initialize to zero

Next we see it populated from a call to fread.

.text:00400A48 addiu $v0, $fp, 0x98+var_34

.text:00400A4C move $a0, $v0 $ ptr

.text:00400A50 li $a1, 1 $ size

.text:00400A54 li $a2, 0x1D $ n

.text:00400A58 lw $a3, 0x98+stream($fp) $ stream

.text:00400A5C jal fread

Then we see its length checked with strlen. This is used as a conditional for a loop, comparing its value with var_80.

.text:00400CC8 addiu $v0, $fp, 0x98+var_34

.text:00400CCC move $a0, $v0 $ s

.text:00400CD0 jal strlen


.text:00400CE0 lw $v0, 0x98+var_80($fp)

.text:00400CE4 sltu $v0, $s0

.text:00400CE8 bnez $v0, loc_400BD0

The last cross-reference is in the goodboy block. Let’s rename our current players:

var_34 -> flag_from_file

var_80 -> loop_index

$ ———————————————————–

$ Filename Shenanigans

$ ———————————————————–

We know that the flag comes from a file, so let’s see what file is being read. Starting from the block with the fread call and working backward, we see a preceding fopen call.

.text:004009C0 addiu $v1, $fp, 0x98+var_64

.text:004009C4 la $v0, unk_400EF0

.text:004009CC move $a0, $v1 $ filename

.text:004009D0 move $a1, $v0 $ modes

.text:004009D4 jal fopen

IDA shows unk_400EF0 to be a single byte ‘r’ for opening the file in read-only mode. We also see that var_64 is the filename to open from where the flag will be read. Let’s rename:

var_64 -> file_with_flag

unk_400EF0 -> mode_r

Where is file_with_flag defined? None of the cross-references show a straight assignment or strcpy/strncpy/sprintf/snprintf/etc. The first xref is the initialization to zero, as we saw with flag_from_file.

.text:004008DC sw $zero, 0x98+file_with_flag($fp) $ initialize to

Scrolling down from there we see our next xrefs. The keen eye of a reverser recognizes some familiar byte ranges.

.text:00400954 sb $v0, 0x98+file_with_flag+1($fp)

.text:00400958 li $v0, 0x75


.text:0040099C sb $v0, 0x98+file_with_flag+2($fp)

.text:004009A0 li $v0, 0x2F

.text:004009A4 sb $v0, 0x98+file_with_flag($fp)

.text:004009A8 li $v0, 0x2E


.text:004009B4 sb $v0, 0x98+file_with_flag+3($fp)

.text:004009B8 li $v0, 0x6C

Using the R key in IDA, we can display each byte as its ASCII character. This leaves a jumble of characters that don’t really make any sense and appear out of order.

.text:00400948 li $v0, "c"

.text:0040094C sb $v0, 0x98+var_58+1($fp)

.text:00400950 li $v0, "t"

.text:00400954 sb $v0, 0x98+file_with_flag+1($fp)

.text:00400958 li $v0, "u"

.text:0040095C sb $v0, 0x98+var_60+3($fp)

.text:00400960 li $v0, "r"

.text:00400964 sb $v0, 0x98+var_5C+2($fp)

.text:00400968 li $v0, "k"

.text:0040096C sb $v0, 0x98+var_58+2($fp)

.text:00400970 li $v0, "t"

.text:00400974 sb $v0, 0x98+var_5C($fp)

.text:00400978 li $v0, "e"

.text:0040097C sb $v0, 0x98+var_5C+1($fp)

.text:00400980 li $v0, "/"

.text:00400984 sb $v0, 0x98+var_60($fp)

.text:00400988 li $v0, "r"

.text:0040098C sb $v0, 0x98+var_60+1($fp)

.text:00400990 li $v0, "o"

.text:00400994 sb $v0, 0x98+var_60+2($fp)

.text:00400998 li $v0, "m"

.text:0040099C sb $v0, 0x98+file_with_flag+2($fp)

.text:004009A0 li $v0, "/"

.text:004009A4 sb $v0, 0x98+file_with_flag($fp)

.text:004009A8 li $v0, "."

.text:004009AC sb $v0, 0x98+var_5C+3($fp)

.text:004009B0 li $v0, "p"

.text:004009B4 sb $v0, 0x98+file_with_flag+3($fp)

.text:004009B8 li $v0, "l"

A quick review of MIPS shows that li loads a numeric value into the destination, and sb stores the least significant byte of the source into the destination. Let’s just trace the target real quick from our MIPS environment and watch for the open syscall.

user@debian-mips:~/RouterLocker$ strace ./routerlocker


child_tidptr=0x77f21498) = 3293

wait4(-1, License file not found.

Lock it up, and lock it out.

Looks like the target is forking. Let’s try again, this time with the -f flag to follow the forked process.

user@debian-mips:~/RouterLocker$ strace -f ./routerlocker


[pid 3312] open("/tmp/router.lck", O_RDONLY) = -1 ENOENT (No such
file or directory)

[pid 3312] write(2, "License file not found.\\n", 24License file not

) = 24

[pid 3312] write(2, "Lock it up, and lock it out.\\n", 29Lock it up,
and lock it out.

At this point we know the code looks something like this:

f = fopen("/tmp/router.lck", "r");

fread(flag_from_file, 1, 0x1D, f); // 0x1D == 29

$ ———————————————————–

$ The 29 Bytes in /tmp/router.lck

$ ———————————————————–

Scrolling a bit more in the call graph we see the proceeding call to fclose, followed by another block of assignments similar to how the file_with_flag variable was defined.

.text:00400AE0 li $v0, 0xFFFFFF88

.text:00400AE4 sb $v0, 0x98+var_4C($fp)

.text:00400AE8 li $v0, 0xA

.text:00400AEC sb $v0, 0x98+var_4C+2($fp)


.text:00400BC0 li $v0, 0x38

.text:00400BC4 sb $v0, 0x98+var_44+2($fp)

Some of these bytes are in ASCII printable range, but others are not. Reordering these bytes by their address, we can create a list in Python.


[194, 27, 196, 223, 222, 221, 149, 173, 136, 65, 10, 84, 37, 94, 186,
22, 6, 121, 56, 222, 144, 239, 180, 192, 156, 155, 93, 106, 115]

Python>[hex(b) for b in bytes]

['0xc2', '0x1b', '0xc4', '0xdf', '0xde', '0xdd', '0x95', '0xad',
'0x88', '0x41', '0xa', '0x54', '0x25', '0x5e', '0xba', '0x16', '0x6',
'0x79', '0x38', '0xde', '0x90', '0xef', '0xb4', '0xc0', '0x9c', '0x9b',
'0x5d', '0x6a', '0x73']

Coincidentally, did you guess how many bytes there were?



Looks like we’re on the right track. Following the call graph, we get to some weirdness.

.text:00400BD0 loc_400BD0:

.text:00400BD0 lbu $v0, 0x98+var_7C+3($fp)

.text:00400BD4 sll $v0, 2

.text:00400BD8 andi $v1, $v0, 0xFF

.text:00400BDC lbu $v0, 0x98+var_7C+3($fp)

.text:00400BE0 andi $v0, 3

.text:00400BE4 andi $v0, 0xFF

.text:00400BE8 addu $v0, $v1, $v0

.text:00400BEC sb $v0, 0x98+var_6C($fp)

.text:00400BF0 lbu $v1, 0x98+var_6C($fp)

.text:00400BF4 la $v0, runtime_pad

.text:00400BFC addu $v0, $v1, $v0

.text:00400C00 lbu $v0, 0($v0)

.text:00400C04 sb $v0, 0x98+var_6B($fp)

.text:00400C08 lw $v0, 0x98+var_7C($fp)

.text:00400C0C addiu $v1, $fp, 0x98+loop_index

.text:00400C10 addu $v0, $v1, $v0

.text:00400C14 lbu $v1, 0x2C($v0)

.text:00400C18 lw $v0, 0x98+var_7C($fp)

.text:00400C1C addiu $a0, $fp, 0x98+loop_index

.text:00400C20 addu $v0, $a0, $v0

.text:00400C24 lb $v0, 0x4C($v0)

.text:00400C28 andi $a0, $v0, 0xFF

.text:00400C2C lbu $v0, 0x98+var_6B($fp)

.text:00400C30 xor $v0, $a0, $v0

.text:00400C34 andi $v0, 0xFF

.text:00400C38 beq $v1, $v0, loc_400C9C

.text:00400C3C move $at, $at

This looks to be where the target decides whether to continue with the next iteration of the loop, or bail to a badboy message. We also notice the variable runtime_pad; which consists of 128 bytes that currently have no meaning. Let’s get to understanding.

.text:00400BD0 loc_400BD0:

.text:00400BD0 lbu $v0, 0x98+var_7C+3($fp) $ load unsigned byte from
var_7C+3($fp) and store into $v0

.text:00400BD4 sll $v0, 2 $ $v0 = $v0 << 2

.text:00400BD8 andi $v1, $v0, 0xFF $ $v1 = $v0 & 0xff

.text:00400BDC lbu $v0, 0x98+var_7C+3($fp) $ load unsigned byte from
var_7C+3($fp) and store into $v0

.text:00400BE0 andi $v0, 3 $ $v0 = $v0 & 3

.text:00400BE4 andi $v0, 0xFF $ $v0 = $v0 & 0xff

.text:00400BE8 addu $v0, $v1, $v0 $ $v1 += $v0

.text:00400BEC sb $v0, 0x98+var_6C($fp) $ var_6C = ($v0 & 0xff)

.text:00400BF0 lbu $v1, 0x98+var_6C($fp) $ $v1 = var_6C

.text:00400BF4 la $v0, runtime_pad $ $v0 = &runtime_pad

.text:00400BFC addu $v0, $v1, $v0 $ $v0 is an offset $v1 bytes
into runtime_pad

.text:00400C00 lbu $v0, 0($v0) $ $v0 = runtime_pad[$v0]

.text:00400C04 sb $v0, 0x98+var_6B($fp) $ var_6B = $v0

.text:00400C08 lw $v0, 0x98+var_7C($fp) $ $v0 = var_7C

.text:00400C0C addiu $v1, $fp, 0x98+loop_index $ $v1 = $fp +

.text:00400C10 addu $v0, $v1, $v0 $ $v0 = $v1 + var_7C

.text:00400C14 lbu $v1, 0x2C($v0) $ $v1 = *(&$v0 + 0x2C)

.text:00400C18 lw $v0, 0x98+var_7C($fp) $ $v0 = var_7C

.text:00400C1C addiu $a0, $fp, 0x98+loop_index

.text:00400C20 addu $v0, $a0, $v0 $ $v0 += $a0

.text:00400C24 lb $v0, 0x4C($v0) $ $v0 = *(&$v0 + 0x4C)

.text:00400C28 andi $a0, $v0, 0xFF $ $a0 = $v0 & 0xff

.text:00400C2C lbu $v0, 0x98+var_6B($fp) $ $v0 = var_6B

.text:00400C30 xor $v0, $a0, $v0 $ $v0 = $a0 ^ $v0

.text:00400C34 andi $v0, 0xFF $ $v0 = $v0 & 0xff

.text:00400C38 beq $v1, $v0, loc_400C9C $ branch to loc_400C9C if
$v0 == $v1

More confused or less confused? A few things we can clear up right away:

  • The sll $v0, 2 is the same as multiplying by 4

  • Any and with 0xff is a cast to an 8-bit type: [un]signed char or [u]int8_t depending on context

  • A fast way of doing modulo is to use and; so the instruction at 00400BE0 is likely modulo 4

  • var_7C+3($fp) is the least significant byte of loop_index (assignment in blocks at 400CF0 and 400D00)

With these things in mind, we can start to make more sense of this, especially with the help of debugging and a helpful .gdbinit ( Here’s what we’re looking at in rough pseudo code:

pad_offset = (uint8_t)((uint8_t)loop_index * 4)

pad_offset += (uint8_t)((uint8_t)loop_index & 3)

bail if flag_from_file[loop_index] != (bytes[loop_index] ^

With a few quick edits in vim, I was able to curate runtime_pad into a Python list.


[182, 165, 2, 182, 221, 115, 55, 53, 95, 35, 165, 201, 176, 141, 233,
171, 129, 163, 223, 12, 128, 175, 180, 125, 99, 121, 244, 92, 79, 22,
19, 195, 251, 190, 94, 48, 34, 46, 130, 102, 27, 222, 103, 193, 153, 76,
190, 35, 68, 180, 123, 245, 231, 44, 65, 105, 209, 120, 223, 219, 69,
91, 207, 73, 116, 7, 19, 143, 64, 24, 231, 230, 206, 250, 86, 55, 252,
116, 51, 129, 227, 220, 164, 12, 68, 128, 87, 186, 62, 249, 217, 167,
180, 94, 205, 165, 235, 68, 91, 239, 70, 243, 91, 109, 207, 224, 56,
167, 199, 184, 106, 24, 22, 225, 197, 3, 30, 24, 82, 222, 132, 246, 39,
240, 96, 45, 39, 75]

Now let’s see if we can make this happen:

Python>serial = ''

Python>for loop_index in range(len(bytes)):

Flushing buffers, please wait...ok

Python> pad_offset = ((loop_index * 4) + (loop_index & 3))

Python> serial += chr(bytes[loop_index] ^


Python>print serial


Looks like the flag should be PAN{that_ransomware_ran_somewhere} based on the 400D34, but we can confirm in our MIPS environment.

user@debian-mips:~/RouterLocker$ echo -n
"that_ransomware_ran_somewhere" > /tmp/router.lck

user@debian-mips:~/RouterLocker$ ./routerlocker

Thank you for purchasing RouterLocker v2.0

Your flag is: PAN{that_ransomware_ran_somewhere}

Solving with Angr

This challenge is a great example to use the binary analysis framework angr to symbolically solve.

#!/usr/bin/env python
""" Routerlocker angr symbolic solver for Palo Alto Networks LabyREnth CTF

__author__ = "fdivrp"

import logging
import sys

import angr
import simuvex


# Start = main
START = 0x4008b0
# Find = goodboy
FIND = 0x400d58
# Avoid = badboy
AVOID = (0x4008ec, 0x400a34, 0x400ac0, 0x400c88)

def main(filename):
    """main method sets up angr and solves

    :param filename:
    # Setup Angr
    p = angr.Project(filename)
    state = p.factory.blank_state(addr=START)

    # Setup License File
    # license filename stack string from 0x400990
    license_name = "/tmp/router.lck"

    # Symbolic buffer 29 bytes long from 0x400ab4
    license_len = 29
    license_state ='password_bytes', license_len * 8)

    # Symbolic memory
    content = simuvex.SimSymbolicMemory(
    content.set_state(state), license_state)

    # Symbolic file
    license_file = simuvex.SimFile(
        license_name, 'rw', size=license_len, content=content)
    fs = {license_name: license_file}
    state.posix.fs = fs

    # Explore Path Group and Run
    pg = p.factory.path_group(state)
    pg.explore(find=FIND, avoid=AVOID)

    # Return the flag
    return pg.deadended[-1].state.posix.dumps(1)

if __name__ == "__main__":
        print main(sys.argv[1])
    except IndexError:
        print "usage: %s <filename>" % __file__