RandoriSec 14 min

During the European Cyber Week (ECW), a Capture the Flag (CTF) was organised on November 16th at Rennes in France. A preselection took place from October 14, 2022 to October 30, 2022. Our security researcher Express got to the final round and wrote some write-ups for the reverse-engineering category. The challenges were created by the Thalium team and were interesting to solve.

On the agenda:

Without any further ado, here are the write-ups.



The challenge gives us an archive containing two files :

  • file.txt.lock (an encrypted file)
  • truc.sys

We started by googling the word minifilter. From the results, we understood that it’s a type of driver that can handle IRP-based I/O operations to filter them. For example, it is used by antiviruses.

If you want to understand more precisely the concept, you can check this Microsoft documentation.

Static analysis

When a Windows driver is loaded, the routine DriverEntry is called, in our case, the implementation of this routine is the following one:


We can see that it calls a function named setup() which will first fill an array of integers rand_bytes with random values thanks to RtlRandomEx(). Later, a filter is being registered with FltRegisterFilter(). If the filter is successfully registered, it starts filtering thanks to FltStartFiltering() :


The FltRegisterFilter() function takes a PDRIVER_OBJECT, FLT_REGISTRATION and PFLT_FILTER structures as arguments.

The FLT_REGISTRATION structure will contain all necessary callback routines for the filter. It’s used to provide information about a file system minifilter :

typedef struct _FLT_REGISTRATION {
    USHORT                                      Size;
    USHORT                                      Version;
    FLT_REGISTRATION_FLAGS                      Flags;
    const FLT_CONTEXT_REGISTRATION              *ContextRegistration;
    const FLT_OPERATION_REGISTRATION            *OperationRegistration;
    PFLT_FILTER_UNLOAD_CALLBACK                 FilterUnloadCallback;
    PFLT_INSTANCE_SETUP_CALLBACK                InstanceSetupCallback;
    PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK       InstanceQueryTeardownCallback;
    PFLT_INSTANCE_TEARDOWN_CALLBACK             InstanceTeardownStartCallback;
    PFLT_INSTANCE_TEARDOWN_CALLBACK             InstanceTeardownCompleteCallback;
    PFLT_GENERATE_FILE_NAME                     GenerateFileNameCallback;
    PFLT_NORMALIZE_NAME_COMPONENT               NormalizeNameComponentCallback;
    PFLT_NORMALIZE_CONTEXT_CLEANUP              NormalizeContextCleanupCallback;
    PFLT_TRANSACTION_NOTIFICATION_CALLBACK      TransactionNotificationCallback;
    PFLT_NORMALIZE_NAME_COMPONENT_EX            NormalizeNameComponentExCallback;

The PFLT_FILTER structure will be filled by FltRegisterFilter after execution, and will be then used by all functions related to filters like FltStartFiltering, etc.

One of the most important fields is OperationRegistration which is a pointer to a FLT_OPERATION_REGISTRATION structure array :

    UCHAR                            MajorFunction;
    PVOID                            Reserved1;

The MajorFunction field is holding the type of I/O operation. The PreOperation field contains the address of a PFLT_PRE_OPERATION_CALLBACK structure which is the entry point for its callback routines.

In our case, the minifilter is handling three different I/O operations so it has three FLT_OPERATION_REGISTRATION structures for :

  • IRP_MJ_CREATE : Used to open a handle for a file object or device object.
  • IRP_MJ_WRITE : Triggered when a write operation occurs on the device to send data.
  • IRP_MJ_CLEANUP : When the handle is closed, it’s called to remove process-specific resources, such as user memory, …
  • IRP_MJ_OPERATION_END : Marks the end of the Callbacks array.

By reading the challenge description, it’s said that “A user noticed a bug when saving his file from notepad.". It means that the minifilter is probably going to perform its job in the IRP_MJ_WRITE callback :


The prototype of a PFLT_PRE_OPERATION_CALLBACK is as follows :

        PVOID *CompletionContext);

First the handler for IRP_MJ_WRITE will retrieve the file context of the file we saved and then it’ll call a function renamed handle_write by us. We now fall in a big function, where it executes many interesting operations. It retrieves the size of the file we want to save with FsRtlGetFileSize().

After that, FltCreateFile creates the file requested by the user. In addition to that, it creates another one with the .lock extension :


Now come the important part where the minifilter is manipulating file content. It reads 7 bytes by 7 from the content of the file with ZwReadFile and writes the encrypted data to the other file with ZwWriteFile :


The function that encrypts data is as follows :

void encrypt(char *buf, uint32_t lenght, uint32_t value)
	uint32_t i;
  for (i = 0; i < lenght; i++)
  	*(buf + i) ^= value ^ rand_bytes[i % 4];

Get the flag

The function responsible for content encryption can be reversed easily because we know that the flag format is ECW{xxxx} so we can retrieve the random bytes generated by the driver with no problem because only four of them are used. Don’t forget that it’s UTF-16, so the flag begins with \xff\xfeE\x00 :


with open("file.txt.lock", "rb") as file:
    data = file.read()

key = [
    data[0] ^ 1 ^ 0xff,	# \xff
    data[1] ^ 1 ^ 0xfe,	# \xfe
    data[2] ^ 1 ^ 0x45,	# E
    data[3] ^ 1 ^ 0x0		# \0

off, value = 0, 1
lock = False

flag = b""

while (lock == False):
    for idx in range(0x7):
        flag += bytes([data[off] ^ value ^ key[idx % 4]])
        off += 1
        if off == len(data):
            lock = True
    value += 1


Flag : ECW{Fl7_pO5T0P_fINIsH3D_PR0C3S5inG}




The provided file is an archive with the following files :


There are three important files here, the disk.img which defines which disk image the driver uses, and two OVMF files.

OVMF files are here for UEFI support, the OVMF_CODE is the UEFI firmware and OVMF_VARS is the vars storage. On the other side, the disk.img is an EFI system partition which is used to boot an EFI firmware, we can extract the bootx64.efi file with binwalk or FTKImager and analyze it :


First it’s locating the decompress protocol with LocateProtocol which returns the first interface of the protocol :


The protocol interface is defined as this structure :


According to the official UEFI specification, EFI_DECOMPRESS_PROTOCOL provides a decompression service that allows a compressed buffer in memory to be decompressed into a destination buffer in memory. But it requires a temporary buffer to perform the decompression process.

The job of the GetInfo function is to retrieve the size of the destination buffer and that of the temporary buffer. It also allocates two buffers, with both sizes returned :


Following that, the Decompress function will perform the decompression :


After that, every byte of the decompressed buffer is xored with 0x71 :


The buffer is then loaded into memory with LoadImage and the function StartImage execute then the image beginning at its entry point :


Our goal is now to retrieve that decompressed and unxored data, and there’s actually many ways of doing so. The simplest way is to dump the memory after the decompression, but we can also do it statically and decompress ourselves.

We can do this with GDB, first we add to the run.sh QEMU command line the -s -S arguments, the first will share a debugging session and the second will break at the entry point of the CPU 0xfffffff0.

After that we can locate the bootx64.efi image at base address 0x6804000 by searching in memory for the string /home/valkheim/workspace/ecw_uefi/edk2/MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.c that’s in the firmware :


With that we can deduce the base address and put a breakpoint at 0x680512B, which is the moment LoadImage is called :


We can see in R9 the image location and on the stack the size of the image. Now we can dump it with the command :

  • dump binary memory dump.bin $r9 $r9+0x800

Now we can check the dump and we see with the file command it’s an EFI byte code :


In order to analyze the byte code, we can use ebcvm which comes with a debugger and a disassembler, as IDA also supports EFI byte code we’ll use it for the write-up.

Here’s the disassembled code for the checking part, and we can see it’s not going to be hard :


It’s simply iterating over our input and checking that our byte is equal to the byte value in the ciphertext + 4, starting from offset 0x200 and increasing by 2 cause it’s UTF-16.

Get the flag

This script solves the challenge and we can get our flag. This challenge was a nice one to learn internals of UEFI.

cipher = "x0cV$2ekF2Qizv6^oyq^pUHKUgFj1Jd__V4LKW45H3R3__QvN3@sMwGeWw0VKBYFzRbviq6u#7RA9ArnM8XDIEEvHQ&HGT@Sv&LUZdb4BF6%2_4dci33595^VZQeoji^z^ucPVhc#&cT6#NH0^97O$7WqofM3pHpyMsY4WeTtS&eeNwq466kV6__GHG7e&S&ReuO353pv^UppLd5*$5!TD__nipgduZdxzv#oDWd&DFNzVWAmO_7jEH38DGb%dkAA?SwABE[>up/[_,`/[nqh/vy4PrhsulP%wMNpg&4cRY7S8x^!Veptn9kK__P8D3j41V%qktB7i_L&ViJdr1%#P&Dhy4C3H"
cipher = cipher.encode("utf-16")[2:]

flag = ""
for off in range(0x200, 0x200 + (2 * 24), 2):
	flag += chr(cipher[off] + 4)

print("Flag :", flag)

Flag : ECW{EFI_Byt3_c0d3_rul3z}




The challenge was an ELF binary involving OpenGL, a library that allows manipulating 2D / 3D objects.


The program is first checking if there is an argument passed, if not then it shows the message You will never fly ! and exits the program :


If an argument is passed, it checks that its length is 58 bytes, if not then it shows the message Okay, Houston, I believe we've had a problem here ! and exits the program :


If the length is good, it checks if the first four beginning characters are ECW{ and the last one is }, if it’s not the case then it shows the message Mon coeur s’entirbouchonne autour de mes chevilles comme un vieux slip moite. C'est pas ca ! :


Now before digging into the part where OpenGL things begin, there’s a specificity in the binary with the usage of OpenGL functions, it’s resolving some of the functions by using an internal OpenGL function named glXGetProcAddress in sub_3A00 :



It was probably done to complicate the debugging of the binary, but we can overcome this by hooking glXGetProcAddress and use LD_PRELOAD so we can then monitor functions called easily to follow the program flow.

An example of a hooking code for the function glCreateShader is :

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>
#include <unistd.h>
#include <string.h>

// gcc -fPIC -shared hook.c -o hook.so

GLuint glCreateShader(GLenum shaderType)
    GLuint (*func)(GLenum);
    func = dlsym(RTLD_NEXT, "glCreateShader");
    printf("[glCreateShader] shader type : 0x%x\n", shaderType);
    return func(shaderType);

void (*glXGetProcAddress(const GLubyte *procName))(void)
    void * (*func)(const GLubyte *);
    func = dlsym(RTLD_NEXT, "glXGetProcAddress");
    if (!strcmp(procName, "glCreateShader"))
        return glCreateShader;

    return func(procName);

Analyzing sub_5D60

Now that we can hook the functions of OpenGL, we can check the function sub_5D60 that’s initializing the window and more.

It opens a connection to an X server and checks the version of the OpenGL GLX extension to ensure its greater than version 1.2 :


It also configures some parameters and creates a 100x100 window of depth 0, which is invisible to the user.

Note : if you are executing the binary on a VM you might need to add the LIBGL_ALWAYS_SOFTWARE=1 environment variable to have the binary work properly.

Now come the interesting parts, the program creates a texture based on an array of 128-bit values, we can easily get the content of the texture and specifications with this hook :

void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void * data)
    void (*func)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void *);
    func = dlsym(RTLD_NEXT, "glTexImage2D");
    func(target, level, internalformat, width, height, border, format, type, data);
    printf("[glTexImage2D] width : %d, height : %d, format : 0x%x, type : 0x%x, data content : %s\n", width, height, format, type, (char *)data);

Output :

[glTexImage2D] width : 164, height : 1, format : 0x1903, type : 0x1401, data content : <izmlvpq?,,/?|pmzs~fpjk7sp|~kvpq?"?/6?pjk?ysp~k?pI~sjz$ipv{?r~vq76d????pI~sjz?"?ysp~k77jvqk7xs@Ym~x\ppm{1g6?5?jvqk7/gY..(6?4?jvqk7/g^,'/66?:?-*)J6?0?-**1$b

Analyzing shader

GLSL is a high-level GPU programming language. So this texture will be used to get the key to decipher content thanks to this GLSL fragment shader :

#version 330 core

layout(location = 0) out float oValue;

uniform uint AN225;

void main()
    oValue = float(AN225 % 256U) / 255.;

It communicates with the shader thanks to glGetUniformLocation API, that allows to get the location of a uniform variable, here, AN225. The program will set the value of AN225 using glUniform1ui function, we can hook that to see what is the value passed or use Ga breakpoint :


We also knows that the value sent to the shader comes from here :


So the first three characters in the flag brackets are converted to an integer and this value is then sent to the shader that will output the value modulo 256.

This value is used when the program calls glLogicOp(GL_XOR), and xors every byte of the frame buffer with this value. This output will be then used as a new shader, so if the unxored data is not valid, the shader will be broken and the program will terminate.

Now that we know the first three chars are responsible for the unxoring of the encrypted shader, we have two choices, either find the good key thanks to the known plaintext, or do a bruteforce of all the possibilities and check when the program doesn’t terminate.

Known plaintext

We know that a shader begins with #version 330 core, so we can find that the key is 0x1f because '#' ^ '<' == 0x1f.


If we’re to lazy we can bruteforce with this command for example :

for i in {000..999};do echo -ne "$i " ; ./HotShotGL ECW\{"$i"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\};done 

But don’t forget that the fragment shader was doing a modulo 256, leading to four other valid possibilities that matches the three characters long rule :

  • 178, 434, 690, 946

The shader decrypted is as following :

#version 330 core

layout(location = 0) out float oValue;

void main(){
    oValue = float((uint(gl_FragCoord.x) * uint(0xF117) + uint(0xA380)) % 256U) / 255.;

Now that we have the fragment shader, we can continue the analysis and see that there’s a second texture created. This texture contains our input without the three characters prefix, so 50x1.

The same thing is done as for the precedent’s step but now with another logical operations, GL_EQUIV, that’s doing ~(a ^ b). So our texture will be encoded with the output of the fragment shader, that gives an output for every gl_FragCoord.x that is the x-axis value (0..50).

We can encode our input with this code :

int main(void)
    uint8_t a, b;

    for (int i = 0; i < sizeof(flag); i++)
        a = (i * 0xF117 + 0xA380) % 256;
        b = flag[i];
        flag[i] = ~(a ^ b);
        printf("flag[%d] : 0x%x\n", i, flag[i]);

Now that we have the flag encoded, let’s see how this is then used. After this fragment shader, there’s another one that is manipulating and array, X15, it’s a 63 long array of integers, and we see that its output is the element from index 13 to the last one :

#version 330 core

layout(location = 0) out float oValue;

uniform int X15[63];

void main()
    int jet = int(gl_FragCoord.x) + 13;
    oValue = float(X15[jet]) / 255.;

This time the shader is applying a glLogicOp(GL_XOR) but with the output of the last fragment shader so our encoded input.

Before applying the logical operation, X15 array is initialized with some values in a predefined array at 0xBD40 :

int X15[63] = {
    0x32, 0x43, 0x58, 0x97, 0xf3, 0x31, 0x87, 0x32, 
    0xa4, 0xbe, 0xfa, 0x01, 0xaa, 0x28, 0x0d, 0x3d, 
    0x59, 0x4c, 0x61, 0x90, 0x81, 0xa8, 0xde, 0xc6, 
    0xc0, 0x04, 0x35, 0x4f, 0x42, 0x23, 0xa7, 0xb5, 
    0xa2, 0xda, 0xef, 0xda, 0x07, 0x24, 0x1f, 0x70, 
    0x7d, 0x8e, 0x96, 0x92, 0xf5, 0xfe, 0xf8, 0x05, 
    0x3b, 0x2a, 0x42, 0x4a, 0xad, 0x97, 0xb5, 0xd8, 
    0xc9, 0xe2, 0x1a, 0x3a, 0x19, 0x14, 0x31

So now the array will be modified using the encoded flag as here :

for (int i = 0; i < sizeof(flag); i++)
    X15[i + 13] ^= flag[i];

The program performs a check to determine if we have the good flag or not. And this is checking that the xmm0 register value is equal to 0, which is the floating value of the pixel at x = 0 and y = 0 :


This value is defined with a fragment shader doing some strange stuff :

#version 330 core

layout(location = 0) out float oValue;

uniform sampler2D Input;

void main()
    ivec2 p = 2 * ivec2(gl_FragCoord.xy);
    oValue = texelFetch(Input, p, 0).r;
    if((p.x + 1) < textureSize(Input, 0).x) {
        oValue += texelFetch(Input, p + ivec2(1, 0), 0).r;

With a bit of googling, we find that a technique is used here, as explained here, it’s calculating the average sum of pixels from half-resolution texture until 1x1 pixel. glViewport is used to divide every time the texture area and if we hook this function we see it :


We need to have 0 as final pixel value, we need to have every index in X15 from index 13 filled with 0’s.

Get the flag

Now we can try and solve the challenge easily with this code :

void main(void)
    uint8_t a, b;
    int X15[63] = {
        0x32, 0x43, 0x58, 0x97, 0xf3, 0x31, 0x87, 0x32,
        0xa4, 0xbe, 0xfa, 0x01, 0xaa, 0x28, 0x0d, 0x3d,
        0x59, 0x4c, 0x61, 0x90, 0x81, 0xa8, 0xde, 0xc6,
        0xc0, 0x04, 0x35, 0x4f, 0x42, 0x23, 0xa7, 0xb5,
        0xa2, 0xda, 0xef, 0xda, 0x07, 0x24, 0x1f, 0x70,
        0x7d, 0x8e, 0x96, 0x92, 0xf5, 0xfe, 0xf8, 0x05,
        0x3b, 0x2a, 0x42, 0x4a, 0xad, 0x97, 0xb5, 0xd8,
        0xc9, 0xe2, 0x1a, 0x3a, 0x19, 0x14, 0x31

    unsigned char flag[50] = {0};

    for (int i = 0; i < sizeof(flag); i++)
        a = (i * 0xF117 + 0xA380) % 256;
        b = X15[i + 13];
        flag[i] = ~(a ^ b);
    printf("flag : %s\n", flag);

Flag : ECW{178Welcome_on_Board,_This_is_Your_Captain_Speaking_;)}

Thanks to the Thalium team, it was a very interesting challenge to learn OpenGL.