11 minute read

On the weekend of October 28th, through October 30th, the Danish Defence Intelligence Service (FE) held a CTF, named “Cyber Demon”, open for anyone. Read more about it here: https://fe-ctf.dk/

Some of these challenges are solved after the CTF was over, as FE has been kind enough to keep the instances running.

All flags have the format flag{ascii range 0x20-0x7e inclusive, except 0x5c and 0x7d}

Note: 0x5c is \, and 0x7d is }.

Web

These challenges were, without doubt, the most solved ones, as they were not very difficult.

Dig Host #1

Tags: web baby's 1st remote
Description:

Heard you like digging, so why not dig for some flags? http://dig-host-1-web.hack.fe-ctf.dk/


We are presented with web interface for using the dig (DNS lookup tool) command.

Entereing t1ng.dk into the client, gives the expected output of as plaintext.

dig web client

This suggests that we may simply inject another command. We try this by requesting

t1ng.dk ; whoami

Underneath the dig output, we now see user.

Listing the directory (ls), does not yield anything of interest, however the parent directory (ls ..) holds a flag file, which we read by using cat.

; cat ../flag

This gives us the flag.

Flag: flag{do people still use php?}

Dig Host #2

Tags: web remote
Description:

Seems like you are a digxpert! But can you keep up the phase? http://dig-host-2-web.hack.fe-ctf.dk/


The same web UI as the previous challenge, but we are not allowed to write spaces in the command. The spaces are simply removed prior to command execution, meaning it’s not client-side.

A quick web search suggests using ${IFS}, as IFS is a variable that holds whitespace characters.

;cat${IFS}../flag

Flag: flag{who needs to sanitize input anyway?}

Dig Host #3

Tags: web remote
Description:

Fine. This time, you will need more than a shovel to dig up the flag! http://dig-host-3-web.hack.fe-ctf.dk/


Once again we are presented with a web interface for dig, this time spaces are still ignored, as well as parameter expressions, using $.

We can still list the current directory without using spaces, which shows that flag is in the current directory. However, POSIX redirection allows for redirecting input, by using <.

This gives us the command

;cat<flag

Flag: flag{it's not bug, it's a feature}

Reversing

LibNotFound

Tags: rev baby's 1st local
File: Download
Description:

Fixme, please


We get a binary file, which does not run, but instead gives an error

$ ./challenge
./challenge: error while loading shared libraries: libnotfound.so: cannot open shared object file: No such file or directory

.so files are shared objects from the C language, which can be compiled seperately, to be included at runtime. It looks as though we will have to write our own shared object.

To compile it, we use gcc.

$ gcc -shared -o libnotfound.so -fPIC libnotfound.c

Let’s create a basic file, and compile it.

int main(void) {
    return 0;
}

To include a local library, after the binary (challenge) has been compiled, we include the library path in an environment variable named LD_LIBRARY_PATH.

$ LD_LIBRARY_PATH=${pwd}
$ export LD_LIBRARY_PATH
$ ./challenge
./challenge: symbol lookup error: ./challenge: undefined symbol: foo_add

So we try to write the function foo_add ourselves. Judging by the name, it would be logical, to make the function return the sum of two inputs.

int foo_add(int a, int b) {
    return a + b;
}

Recompile and try to run the binary again, only to find that we also need to write the function foo_sub. Again, declare a function, and make it return the difference between a and b.

This pattern repeats for foo_mul, foo_div and foo_mod.

Eventually, we end up with the following file.

int foo_add(int a, int b) { return a + b; }
int foo_sub(int a, int b) { return a - b; }
int foo_mul(int a, int b) { return a * b; }
int foo_div(int a, int b) { return a / b; }
int foo_mod(int a, int b) { return a % b; }

Finally, compiling this and running challenge again gives

$ ./challenge
flag{hello? yes, this is flag}

Flag: flag{hello? yes, this is flag}

Snake Jazz

Tags: rev local
File: Download
Description:

You love it? It’s snake jazz!


We are presented with two files, magic.py and runme.py. magic.py contains some kind of class, that seems to have been modified to make it as non-inuitive as possible. runme.py imports the magic file, and then does some mystery stuff, to represented by underscores _, plus-signs + and minus-signs -.

Runinng runme.py gives the following output

$ ./runme.py
                      .
                        `:.
                          `:.
                  .:'     ,::
                 .:'      ;:'
                 ::      ;:'
                  :    .:'
                   `.  :.
          _________________________
         : _ _ _ _ _ _ _ _ _ _ _ _ :
     ,---:".".".".".".".".".".".".":
    : ,'"`::.:.:.:.:.:.:.:.:.:.:.::'
    `.`.  `:-===-===-===-===-===-:'
      `.`-._:                   :
        `-.__`.               ,'
    ,--------`"`-------------'--------.
     `"--.__                   __.--"'
            `""-------------""'

Please enter key:

After trying to enter some key, the program prints “sadpanda.jpg”, and then exits.

I initially looked at this code, and thought to myself, that there is no way that I am going to be able to solve this challenge. But coming back to it later, helped me a lot.


The key part of the program is the last two lines of the magic.py file:

for i in range(1, 100):
    setattr(sys.modules['__main__'],'_'*i,X(3**i))

What this does, is set the attribute '_'*i of __main__ (in this case runme.py), to the object X(3**i). This means that attributes ranging from a single underscore, up to 100 underscores, are callable, and contain an object.

So the obfuscated code in runme.py, is actually different objects, thrown around to generate the text output (and hiding the flag somewhere). Investigating the code, also shows that no object is larger than six underscores, ______, meaning the loop actually only needs to run for range(1, 7).

After this I started to look deeper into the code, and try to understand how it was run, and how the different objects reacted with one another. I made some comprehensive comments on big parts of the code, to get a better understanding of how it works.

In the end, I realized that all this might actually not be necesarry for me to retrieve the flag. Instead I started looking at the internal values, after the user inputs the key.

All the “magic” happens in the __del__ method, which is run when the object goes out of scope. There is a lot of obfuscation going on, and I didn’t understand much of it, but one thing was clear, the interesting part was here:

elif a==18:
    y[b]=ord(sys.stdin.read(1))
elif a==19:
    sys.stdout.write(chr(y[b]));sys.stdout.flush()

As this is where the user input is handled (a==18) and program output is written (a==19).

All of this code, is run inside a while-loop, defined by

while 3**y[8]<x.a:

Here x is self. Printing x.a gives an unreasonably large number, and looking it up on factordb.com reveals that it’s actually equal to \(3^{6867} \approx 2.464 \times 10^{3267}\). Therefore I think it’s safe to assume, that this condition is always true, making the loop run infinitely. This also makes sense, as the program is terminated at the a==0 condition instead.

if a==0:
    os._exit(0)

I ended up implementing some logic to only print the variables, after the ascii art had been printed and the user had been prompted for a key. Then, however, I printed everything I could think of, for each iteration of the while loop, specifically

print(y, a, b, c, d, z)

Where y is an array and the rest are integers.

I then started experimenting with different inputs, namely flag and flac. To see if I could spot the difference in the output. Thankfully, the website text-compare.com, provides excellent support for showing the exact difference between two texts.

Comparison of outputs
Left: flag, right: flac.

Here, it becomes clear that our input value, is stored in the array, on the 4th index, and the comparison is made between it, and the 3rd index. This also means, that we can look forward, one character at a time, by looking at the 3rd index.

Being the noob that I am, I did not create any script to create the flag for me, but instead I manually printed the value each time, and appanded the new character to the flag (key) each time. The character conversion is done fairly easily though, using chr(y[3]).

Finally, I ended up with the following key:

$ echo "flag{it's, it's a device Morty"'!'"}" | ./runme.py
                      .
                        `:.
                          `:.
                  .:'     ,::
                 .:'      ;:'
                 ::      ;:'
                  :    .:'
                   `.  :.
          _________________________
         : _ _ _ _ _ _ _ _ _ _ _ _ :
     ,---:".".".".".".".".".".".".":
    : ,'"`::.:.:.:.:.:.:.:.:.:.:.::'
    `.`.  `:-===-===-===-===-===-:'
      `.`-._:                   :
        `-.__`.               ,'
    ,--------`"`-------------'--------.
     `"--.__                   __.--"'
            `""-------------""'

Please enter key: Correct!

Note: the formatting of the echo string is weird, as ! is interpreted as a command, when used in double quotes, but the flag itself contains single quotes.

I still have no idea how this challenge was made, and I am truely fascinated by the logic beheind it.

Flag: flag{it's, it's a device Morty!}

Miscellaneous

Guessing Game

Tags: OMGACM, remote
Description:

Running at guess.hack.fe-ctf.dk:1337


Connecting to the host shows the following message

$ nc guess.hack.fe-ctf.dk 1337
== proof-of-work: disabled ==
 == Welcome to Number Guess ==

I'm thinking of a number between 0 and 4000000 (both inclusive); try and guess which!
You have 99 guesses.  But! You're only allowed to guess too high 3 times.

Good luck!


Round #0
Your guess:

This looks to be a logic / programming task. The task is to evenly distribute guesses, gradually getting closer to the actual number, without guessing over more than 3 times.

This is a common programming challenge, known as “the egg problem”. I highly recommend reading the linked blogpost, as it explains the concept very well.

To see how how big a number we can guess correct every time, given the constraints \(t=99\), and \(n=4\) we will look at the following

def d1(t):
    return t

def d2(t):
    return 1 + d1(t - 1) + d2(t - 1) if t > 0 else 0

def d3(t):
    return 1 + d2(t - 1) + d3(t - 1) if t > 0 else 0

def d4(t):
    return 1 + d3(t - 1) + d4(t - 1) if t > 0 else 0

print(d4(99))

\(d_4(99) = 3926175\), meaning as long as the random number is not bigger than this, we will be able to guess it, that is \(\frac{3926175}{4000000} \approx 98.15\%\) of the time.

Had we had 100 tries instead of 99, then it would be possible to guess the number correctly every time, as \(d_4(100) = 4087975\).

To implement this, I created a get_guess function, that calls the correct function, based on n.

def get_guess(t, n):
    if n == 3:
        return 1 + d3(t-1)
    if n == 2:
        return 1 + d2(t-1)
    if n == 1:
        return 1 + d1(t-1)
    if n == 0:
        return 1

After this, it’s just a matter of writing a short script, for interacting with the server, and incrementing / decrementing the correct variables. At initialization, the following variables are declared.

runs = 1
guess = 0
guesses = 99
wrong = 3

Then, at each iteration, we calculate how much we should increase our guess with

step = get_guess(guesses, wrong)

And we decrement guesses with 1.

We then guess guess + step. If the guess is too high, we decrement wrong with 1. And if the guess is too low, we increment guess with step.

If guesses reaches 0, we simply reset all the variables and try again.


After some testing, I have found that this solution, guesses the number on the 50th guess every time (not that random after all, huh). However, sometimes the server resets the connection, throwing an EOFError, which I could not find a way around (so instead I simply create a new connection). Furthermore, the server often sends “Round #0”, in the middle of the guessing, meaning we have to start over, which also would explain why guessing correctly takes that many tries.

My full implementation can be found here, (requires pwntools to be installed).

$ # pip install pwntools
$ ./guess.py
-- output omitted --
flag{this has many real-life applications}

Flag: flag{this has many real-life applications}