21 minute read

Introduction

This page details my reversing writeups for The University of Massachusetts Amherst CTF. This CTF was originally just an internal CTF but I knew one of the moderators Sam. Overall it was a great CTF and I really enjoyed it; they definitely had some unique challenges.

All my scripts and the provided files from the CTF can be found here.

Baby Crackme I

Prompt

I haven’t touched a computer since I retired. Can you help me decipher this program I wrote 30 years ago? …


You should be able to solve this one, even if you’ve never written C before; it isn’t essential to understand every single line. If you do want a quick (one page) reference on C, though, look here.

Created by @Jakob

Hint

Strings in C are arrays of characters.

TLDR;

The flag is declared in plaintext in the source file which is compared against the users input. You can pull it out yourself you have bash do the heavy lifting for you.

head -n38 crackme.c | tail -n +7 | cut -d "=" -f 2 | cut -d ')' -f 1 | tr -d "\n\r' "

Solution

Since the source code for this challenge is provide I just opened the file in a text editor.

#include <stdio.h>
#include <string.h>

int check_flag(const char *flag)
{
    if (strlen(flag) != 32) { return 0; }
    if (flag[0]  != 'U')    { return 0; }
    if (flag[1]  != 'M')    { return 0; }
    if (flag[2]  != 'A')    { return 0; }
    if (flag[3]  != 'S')    { return 0; }
    if (flag[4]  != 'S')    { return 0; }
    if (flag[5]  != '{')    { return 0; }
    if (flag[6]  != 's')    { return 0; }
    if (flag[7]  != 'o')    { return 0; }
    if (flag[8]  != 'm')    { return 0; }
    if (flag[9]  != '3')    { return 0; }
    if (flag[10] != 't')    { return 0; }
    if (flag[11] != '1')    { return 0; }
    if (flag[12] != 'm')    { return 0; }
    if (flag[13] != '3')    { return 0; }
    if (flag[14] != 's')    { return 0; }
    if (flag[15] != '_')    { return 0; }
    if (flag[16] != '1')    { return 0; }
    if (flag[17] != 't')    { return 0; }
    if (flag[18] != '_')    { return 0; }
    if (flag[19] != '1')    { return 0; }
    if (flag[20] != 's')    { return 0; }
    if (flag[21] != '_')    { return 0; }
    if (flag[22] != 't')    { return 0; }
    if (flag[23] != 'h')    { return 0; }
    if (flag[24] != '1')    { return 0; }
    if (flag[25] != 's')    { return 0; }
    if (flag[26] != '_')    { return 0; }
    if (flag[27] != '3')    { return 0; }
    if (flag[28] != '4')    { return 0; }
    if (flag[29] != 's')    { return 0; }
    if (flag[30] != 'y')    { return 0; }
    if (flag[31] != '}')    { return 0; }
    return 1;
}

int main(int argc, char **argv)
{
    char flag[64];

    fgets(flag, sizeof(flag), stdin);
    if (strchr(flag, '\n')) { *strchr(flag, '\n') = '\0'; }

    if (check_flag(flag)) {
            printf("License key accepted.\n");
    } else {
            printf("Try again.\n");
    }
}

For anyone who understands C this code is pretty easy to understand. For those who do not understand C they may not get what is going on. In C, and most languages main is the function that is called “first” when the program is executed.

In the function declaration we can see main takes two arguments int argc and char **argv. These are automatically passed by the operating system when the program is called. argc is the number of command line arguments being passed while argv is a array of strings, in C strings are arrays of characters. You can also things of argv as the commands you pass via the command line.

For example running the following comand ./chall test 1 b. argc would equal 4 and argv would contain ['/home/user/Documents/chall', 'test', '1', 'b']. Note: the first argument for argv is always the path of the running executable!

Once in main the program creates a buffer called flag. for a string of 64 characters. Following this it gets input from stdin of 64 characters and stores it in flag. After pulling the input from the user it removes any new lines in the string buffer. Then the flag is passed to check_flag.

This function is where the “meat” of the problem is. Here we can see each value of the users input is compared against some hardcoded values. You COULD pull the flag out by hand, but I’ve been trying to practice my bash scripting so I decided to use bash to solve this challenge.

First we need to just get the lines relevent to use to do that we can use head and tail. After that we need to pull out just the flag values which can be achieved with two calls to cut. At this point the output is just the flag but its across multiple lines so we can use tr to remove the newline characters.

head -n38 crackme.c | tail -n +7 | cut -d "=" -f 2 | cut -d ')' -f 1 | tr -d "\n\r' "

Flag

UMASS{som3t1m3s_1t_1s_th1s_34sy}

Baby Crackme II

Prompt

… back then, compilers weren’t very good. I wrote this version, hoping it would be faster. Can you help me figure out what I chose for the “license key”? …


You might need to understand a little bit more C for this one, but I believe in you!

Created by @Jakob

Hint

Characters in C are just numbers. This is a useful mapping of numbers to letters.

TLDR;

Assembly is inlined to compare each character of the users input against hard coded values. This one is definitely a little more tedious to pull out by hand.

for word in $(grep -i CMP crackme.c | cut -d ',' -f2 | tail -n +2|cut -d ')' -f1); do printf "\x$(printf %x $word)"; done;

Solution

Like the prior challenge the source code is provided so I’ll just use a text editor to view this challenge.

#include <stdio.h>
#include <string.h>
#include <stdint.h>

int check_flag(const char *flag)
{
    struct registers {
            uint64_t rax;
            uint64_t flags;
    };

    #define FLAG_EQUAL 1 << 1

    #define MOV(dst, src) dst = (uint64_t) src
    #define ADD(dst, src) dst += src
    #define CMP(a, b)     if (a == b) regs.flags |= FLAG_EQUAL;
    #define JNE(label)    if ((regs.flags & FLAG_EQUAL) == 0) goto label;

    #define MOVZX_DEREFERENCE_BYTE(dst, src) dst = *((uint8_t *) src)

        if (strlen(flag) != 32) { return 0; }

        struct registers regs;
        regs.rax = 0;
        regs.flags = 0;

        MOV(regs.rax, flag);
        ADD(regs.rax, 0);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 85);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 1);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 77);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 2);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 65);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 3);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 83);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 4);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 83);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 5);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 123);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 6);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 118);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 7);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 49);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 8);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 114);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 9);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 55);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 10);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 117);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 11);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 52);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 12);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 108);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 13);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 95);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 14);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 109);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 15);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 52);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 16);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 99);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 17);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 104);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 18);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 49);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 19);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 110);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 20);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 51);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 21);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 53);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 22);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 95);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 23);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 52);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 24);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 114);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 25);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 51);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 26);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 95);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 27);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 99);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 28);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 48);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 29);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 48);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 30);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 108);
        JNE(fail);

        MOV(regs.rax, flag);
        ADD(regs.rax, 31);
        MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
        CMP(regs.rax, 125);
        JNE(fail);

        MOV(regs.rax, 1);
        goto exit;

    fail:
        MOV(regs.rax, 0);
        goto exit;

    exit:
        return regs.rax;
}

int main(int argc, char **argv)
{
    char flag[64];

    fgets(flag, sizeof(flag), stdin);
    if (strchr(flag, '\n')) { *strchr(flag, '\n') = '\0'; }

    if (check_flag(flag)) {
            printf("License key accepted.\n");
    } else {
            printf("Try again.\n");
    }
}

This challenge is pretty similar to Crackme I, it may just not look like it at first. The reason behind that is because the author obfuscated the code slightly by using custom macros to achieve pseudo assembly. The assembly language level is primarily where binary reverse engineering is performed.

Luckily this function’s main is identical to the prior challenges. The main difference here is the check_flag function. As I said earlier the author utilizes custom macros to define assembly instructions. There is essentially just one block that is repeated over and over.

MOV(regs.rax, flag);
ADD(regs.rax, #1);
MOVZX_DEREFERENCE_BYTE(regs.rax, regs.rax);
CMP(regs.rax, #2);
JNE(fail);

In this block we load the flags register and grab an address offset from it. Following that we pull the actual contents of the address in register. Finally we do a comparison against a hardcoded value. If the comparison fails we jump to the fail block, otherwise we keep going.

The only values that change in the block are #1 and #2. For each consecutive block #1 is incremented, this is acting as the array, or string index. The #2 value is being used as the hardcoded value that the character SHOULD equal. The #2 value is what we need for the flag!

Like the past challenge I used bash to solve this one, for this it makes a little bit more sense because you’d have to pull out the values by hand and convert them to ASCII which is a pain.

First lets pull away everything except the CMP statements as those contain the values we need. This can be achieved with the grep command, unfortunately when CMP is defined it is pulled too so lets ignore that will tail. After we pull out the CMP lines we need to pull out the value which can be achieved with cut like in the prior challenge. Just like before we now have our “flag” except all the values are on separated lines and still in the decimal equivalent and not in its ASCII printable form. So instead of deleting all the newlines this time we will iterate over each line and convert it to ASCII using printf

for word in $(grep -i CMP crackme.c | tail -n +2 | cut -d ',' -f2 | cut -d ')' -f1); do printf "\x$(printf %x $word)"; done;

Flag

UMASS{v1r7u4l_m4ch1n35_4r3_c00l}

Baby Crackme III

Prompt

… and I lost the source code to this one. But you’ve worked your magic for the past two, so I’m sure you can figure this one out?


Welcome to hard mode! You’re going to have to learn to disassemble a binary.

Computers actually can’t run C or Java without compiling it down to “machine code” first. “Assembly” is a set of phrases we associate with certain “instructions” in machine code so that we can read it.

There are plenty of disassemblers out there, but an easy way of spitting out the assembly code to the terminal is:

objdump -D -Mintel crackme

But you’re going to have to do some digging on your own :)

Maybe try looking up “reverse engineering linux binary”?

Created by @Jakob

Hint

Once you get the disassembly, this is very similar to Baby Crackme II.

TLDR;

Loading the binary in IDA we see that it calls a check_flag function. Inside of that function it works almost identically to the prior challenge. We can use gdb to dump out the function and then the same script as before to get the flag!

gdb -batch -ex 'file crackme' -ex 'disassemble check_flag' | for word in $(grep -i CMP | cut -d ',' -f2 | tail -n +2|cut -d ')' -f1); do printf "\x$(printf %x $word)"; done;

Solution

For this challenge we are finally given a binary file instead of a source file. This means I won’t be able to just open it in a text editor and look at the source code, but instead will need to use a tool to disassemble the binary into assembly.

There are a bunch of good tools out there my favorites are either Ghidra or IDA. IDA has a nicer interface but it does not have as much features as Ghidra; in the free version that is.

Regardless, once the binary is opened in a disassembler the main function should pop up. The main is pretty simple (it’s actually the same main we’ve seen before). We can see a call to fgets to get the user input and after that the calls to strchr to remove the newline. After those calls check_flag is called and based on its output we either get the good or bad message just like before.

So just like before let’s check out check_flag.

For someone not too familiar with reversing check_flag may look pretty complicated, but after you understand what is going on it isn’t really.

The first few lines checks the strings input to see if its the right length. If it is we keep going. Then we perform four lines of assembly; this should look very familiar. It is essentially the same commands we saw back in Crackme II! So just like I and II this function checks each value individually to see if it is the correct value and then returns 1 or 0 depending on if it failed or not.

Like before we can pull this out manully or use some scripting to achieve our goal, I chose the later. We can actually use the same script as last time, however we just need to provide it with the disassembly as we don’t have it this time in a file. To do that we can use gdb, or the GNU Debugger [which every reverse engineer (that does stuff on Linux) should be familiar with], to dump the check_flag function.

gdb -batch -ex 'file crackme' -ex 'disassemble check_flag' | for word in $(grep -i CMP | cut -d ',' -f2 | tail -n +2|cut -d ')' -f1); do printf "\x$(printf %x $word)"; done;

Flag

UMASS{now_you_c4n_put_4ss3mbly_on_your_r3sum3}

Linear Algebra

Prompt

Meh. I never understood why they make the CS students take MATH 235.


You’re going to have to disassemble this one, too. Sorry :)

Created by @Jakob

Hint

I’d go about this by writing down the equalities that are being checked in terms of symbolic variables (i.e. α + β + γ = 277), figuring out where the equations overlap, and then using the math skills I learned in high school.

TLDR;

Throw it at ANGR

import os
import angr


PATH = os.path.join(os.path.dirname(__file__), "crackme")


def main():

    proj = angr.Project(PATH, auto_load_libs=False)
    simulation = proj.factory.simgr()

    constraint = lambda s: b"License key accepted" in s.posix.dumps(1)
    simulation.explore(find=constraint)
    if simulation.found:
        pprint(simulation.one_found)


def pprint(solutions):
    str_solutions = solutions.posix.dumps(0).replace(
        b'\x00', b'\n').decode('utf8', errors='ignore').strip().split('\n')

    for solution in str_solutions:
        print(f"Flag found: {solution}")


if __name__ == '__main__':
    main()

Solution

Like Crackme III we are provided with a binary for this challenge so load it up in your favorite disassembler. Once in main we can see its still the same ole thing, so let’s go ahead to check_flag.

check_flag is pretty much the same this time but I really do not wanna do the math to figure out the flag so I’m gonna let a robot do it.

For this challenge I used ANGR a symbolic execution framework. Symbolic execution tools purpose, or at least one of their purposes, is to find all the paths of a program. Therefore we can use this to get us our flag!

ANGR abstracts away a lot of things for you which makes it easier to write quick scripts.

import os
import angr


PATH = os.path.join(os.path.dirname(__file__), "crackme")


def main():

    proj = angr.Project(PATH, auto_load_libs=False)
    simulation = proj.factory.simgr()

    constraint = lambda s: b"License key accepted" in s.posix.dumps(1)
    simulation.explore(find=constraint)
    if simulation.found:
        pprint(simulation.one_found)


def pprint(solutions):
    """ Helper that prints the solution in a more human readable format

    simulation - angr simulation object that represents the state the program is in
    """

    str_solutions = solutions.posix.dumps(0).replace(
        b'\x00', b'\n').decode('utf8', errors='ignore').strip().split('\n')

    for solution in str_solutions:
        print(f"Flag found: {solution}")


if __name__ == '__main__':
    main()

In main we first setup our environment with creating proj and simulation. After that we create our constraint or what we want ANGR to solve for. In this case we are looking for License key accepted being found in the output (standard output is the file descriptor 1 for *NIX Systems). We then pass this constraint to explore and ask our simulation object if anything is found. If there is a solution we then print it with pprint.

Fun fact: you can actually use the exact same script for Crackme III since the output we are looking for is the same. The only change that needs to be made is the filename in the PATH variable.

Flag

UMASS{c0mpu73r_5c13nc3_15_ju57_m47h}

Even/Odd

Prompt

I made this program that generates the flag and writes it to the console. It’s really fast!


Created by @Jakob

Hint

It isn’t “really fast”. This is an “optimize me” challenge. There are a couple of ways to go about this, but the easier routes involve “binary patching”. That’s the keyword you should be looking for online.

TLDR;

Patch the binary to calculate if an number is even / odd in a more optimized manner. At address 0x11BD modify the next 8 bytes from 0x48 0x8b 0x45 0xE8 0x48 0x89 0x45 0xF8 0xeb 0x1c to 0x48 0x89 0xF8 0x48 0x83 0xE0 0x01 0x90 0xeb 0x41

mov rax, rdi
and rax, 1
nop
jmp short locret_1208

Solution

Let’s open this binary up. Its main is pretty bare bones. First a string is dumped and then lsr is called; lsr it is.

lsr is a tad bit more complicated. We initalize some variables (rbp-8, rbp-9, rbp-4) to 0. Following that we check rbp-4 against a hardcoded value 0x1327 and if we are less than or equal to it we jump, otherwise we leave this function. It is safe to assume rbp-4 is the loop index value. After that we do some “maths” to the value at rbp-4 and use the resulting value to pull a value from value a hardcoded array. At this point do not be to worried about that math stuff that is going on. All we care about is that based on the index value we perform some calculations and pull a value from values based on the result. This new value along with a partial result from the math calculates is passed to is_really_odd. Based on the return from is_really_odd we either perform an or operation or keep going. Following that we check to see if rbp-8 is 8 if it is we will print a character to the screen. We can assume that the flag will get printed to the screen. Following the print rbp-8 is set to 0 and rbp-9 is set to the contents of eax and the loop index is incremented.

At this point there is a lot going on in this function and it seems pretty complicated especially since we are only looking at this statically and not dynamically. Before trying to debug this lets look into is_really_odd, especially since the name of the challeneg is Even/Odd.

In even we seem to loop and perform some math eventually calling is_odd and either returning 0 or 1. At this point I am assuming if it is odd 1 is returned otherwise 0 is returned. Let’s look into is_odd.

Here we see the input is compared against 0 if it is 0 we return 0 otherwise we subtract the input by 1 and call is_even with it. Alright… let’s look into is_even

Hmm.. okay so the same code except it calls is_odd and this returns 1 if the value is 0. So essentially this code will recursively call is_even / is_odd until one reaches 0 and whoever reaches zero determines if a function is even or odd; this makes sense but it is not really efficient. Let’s go back to is_really_odd and try to see if we can patch this program to be more efficient.

As far as I understand the value in RSI is just used to speed up the calculations by shrinking the input value a little bit. I’m not entirely sure as I didn’t really look much into it as it looks like all we really care about is if RSI is even or odd.

For us to see if an number is even or odd all you have to do is AND it against 1. If it is even it will return 0 and if it is odd it will return 1; so lets do that.

By patching out lines 6, 7, and 8 we can modify this program to better calculate even odds.

Out of all the reversing challenges I have to say this one was probably my favorite because it was fairly unique.

Flag

UMASS{w0w_y0ur3_p4713n7}

Marius

Prompt

I wanted to do my 575 homework in LaTeX, so I asked him what he used for editing. He sent me this.

TLDR;

The elisp script takes in a 30 long character string. Discards the first 6 characters and the last character. It then swaps characters through out the string and compares the final manged string against a hardcoded one 1v_ms14__ks1rtpk_1dd13s.

Solution

For this challenge we are given an .el file which stands for Emacs Lisp. Since this is just a source file we can open it in a text editor. Unfortunately it doesn’t make a lot of sense. Turns out the alias check-key is elips byte code so it will need to be disassembled before we can figure out what to do.

(defalias 'check-key #[(key) "\303\304!rq\210\305\216	c\210eb\210\306 G\307U\205F\310\311!\210\312\210\313u\210\310\314!\210\315 \210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\316\312!\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\316\312!\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\316\312!\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\316\312!\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\316\312!\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\316\312!\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\312u\210\316\312!\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\313u\210\316\312!\210\315 \210\317\320\321\306 \"\n\232)+\207" [#1=#:temp-buffer key toast generate-new-buffer " *temp*" #[nil "\301!\205

(define-derived-mode pro-mode prog-mode "Professional Mode"
  "Major mode for professional Emacs users."
  :group 'pro
  (let ((key (read-from-minibuffer "Enter license key: ")))
    (unless (check-key key)
      (message "Key verification failed. Falling back to fundamental-mode.")
      (fundamental-mode))))

(provide 'pro-mode)

I have never messed with Emacs before but thankfully the author gave a hint on how to load the file.

  1. Open GNU Emacs:
    1. Press Alt+X
    2. Type “load-file” and hit enter
    3. Type in “pro-mode.el” and hit enter
  2. To run pro-mode
    1. Press Alt-X
    2. Type in “pro-mode” and press enter
  3. To disassemble an elisp function
    1. Press Alt-X
    2. Type in “disassemble” and press enter
    3. Type the name of the function and press enter

Elisp disassembly is strange in that almost every operation pushes something to the stack. One can read about the disassembly here

If we run the instructions and disassemble check-key we get a dump of the disassembly of the function. Which starts with defining a buffer.

byte code for check-key:
  args: (key)
0       constant  generate-new-buffer
1       constant  " *temp*"
2       call      1
3       varbind   temp-buffer
4       save-current-buffer
5       varref    temp-buffer
6       set-buffer
7       discard
8       constant  <compiled-function>
      args: nil
    0       constant  buffer-name
    1       varref    temp-buffer
    2       call      1
    3       goto-if-nil-else-pop 1
    6       constant  kill-buffer
    7       varref    temp-buffer
    8       call      1
    9:1     return

9       unwind-protect

Following this we set the key, the users input, to the current buffer

10      varref    key
11      insert
12      discard
13      point-min
14      goto-char
15      discard
16      constant  buffer-string
17      call      0

After we setup the buffer we compare the length of the key against 30. If it isnt 30 we quit.

18      length
19      constant  30
20      eqlsign
21      goto-if-nil-else-pop 1

Now we delete the first six characters of the key; removes UMASS{


24      constant  delete-char
25      constant  6
26      call      1
27      discard

After that the function deletes the last character of the key; removes }

28      constant  nil
29      end-of-line
30      discard
31      constant  -1
32      forward-char
33      discard
34      constant  delete-char
35      constant  1
36      call      1
37      discard

Now we begin the encryption. First we move back to the beginning of the line.

38      constant  beginning-of-line
39      call      0
40      discard

After that we move forward 10 characters and swap the character at the 10th location with the 11th location (counting as if the characters start at 1).

41      constant  nil
42      forward-char
43      discard
44      constant  nil
45      forward-char
46      discard
47      constant  nil
48      forward-char
49      discard
50      constant  nil
51      forward-char
52      discard
53      constant  nil
54      forward-char
55      discard
56      constant  nil
57      forward-char
58      discard
59      constant  nil
60      forward-char
61      discard
62      constant  nil
63      forward-char
64      discard
65      constant  nil
66      forward-char
67      discard
68      constant  nil
69      forward-char
70      discard
71      constant  transpose-chars
72      constant  nil
73      call      1
74      discard

Afterwards we walk back 10 characters, not the swap moves us forward a character so after this we will be at the second character.

75      constant  -1
76      forward-char
77      discard
78      constant  -1
79      forward-char
80      discard
81      constant  -1
82      forward-char
83      discard
84      constant  -1
85      forward-char
86      discard
87      constant  -1
88      forward-char
89      discard
90      constant  -1
91      forward-char
92      discard
93      constant  -1
94      forward-char
95      discard
96      constant  -1
97      forward-char
98      discard
99      constant  -1
100     forward-char
101     discard
102     constant  -1
103     forward-char
104     discard

We then swap character 2 and 3

105     constant  transpose-chars
106     constant  nil
107     call      1
108     discard

This loops four times so we can build a little Python script to solve this for us.

s = '1v_ms14__ks1rtpk_1dd13s'

for x in range(4):
    tmp = s[9+i]
    s[9+i] = s[10+i]
    s[10+i] = tmp

    tmp = s[0+i]
    s[0+i] = s[1+i]
    s[1+i] = tmp

    i += 1

Flag

UMASS{v1m_1s_4_skr1pt_k1dd13s}