[CTF] ECW CTF 2022 – Reverse challenges writeup

WriteUp 1年前 (2022) admin
632 0 0

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.


minifilter

[CTF] ECW CTF 2022 - Reverse challenges writeup

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:

[CTF] ECW CTF 2022 - Reverse challenges writeup

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() :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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_REGISTRATIONstructure 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_REGISTRATIONstructures 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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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


UEFI

[CTF] ECW CTF 2022 - Reverse challenges writeup

The provided file is an archive with the following files :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

Following that, the Decompress function will perform the decompression :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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.cthat’s in the firmware :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 ! :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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=1environment 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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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 1×1 pixel. glViewport is used to divide every time the texture area and if we hook this function we see it :

[CTF] ECW CTF 2022 - Reverse challenges writeup

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.

 

版权声明:admin 发表于 2022年11月22日 上午11:42。
转载请注明:[CTF] ECW CTF 2022 – Reverse challenges writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...