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:
- a Windows challenge involving a minifilter driver
- an UEFI challenge with some EFI bytecode reversing
- an OpenGL based challenge
Without any further ado, here are the write-ups.
minifilter
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;
PFLT_SECTION_CONFLICT_NOTIFICATION_CALLBACK SectionNotificationCallback;
} FLT_REGISTRATION, *PFLT_REGISTRATION;
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 :
typedef struct _FLT_OPERATION_REGISTRATION {
UCHAR MajorFunction;
FLT_OPERATION_REGISTRATION_FLAGS Flags;
PFLT_PRE_OPERATION_CALLBACK PreOperation;
PFLT_POST_OPERATION_CALLBACK PostOperation;
PVOID Reserved1;
} FLT_OPERATION_REGISTRATION, *PFLT_OPERATION_REGISTRATION;
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 :
typedef FLT_PREOP_CALLBACK_STATUS
(FLTAPI *PFLT_PRE_OPERATION_CALLBACK)(
PFLT_CALLBACK_DATA Data,
PCFLT_RELATED_OBJECTS FltObjects,
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];
}
return;
}
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
:
#!/usr/bin/python3
with open("file.txt.lock", "rb") as file:
data = file.read()
file.close()
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
break
value += 1
print(flag[2:].decode('utf-16'))
Flag : ECW{Fl7_pO5T0P_fINIsH3D_PR0C3S5inG}
Resources
- https://0x00sec.org/t/kernel-mode-rootkits-file-deletion-protection/7616
- https://doxygen.reactos.org/d7/db2/fltkernel_8h_source.html
UEFI
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 :
typedef struct _EFI_DECOMPRESS_PROTOCOL {
EFI_DECOMPRESS_GET_INFO GetInfo;
EFI_DECOMPRESS_DECOMPRESS Decompress;
} EFI_DECOMPRESS_PROTOCOL;
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}
Ressources
HotShotGL
The challenge was an ELF binary involving OpenGL, a library that allows manipulating 2D / 3D objects.
Analysis
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
:
Hooking
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);
return;
}
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
.
Bruteforce
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;
unsigned char flag[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
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.