Practical case: Secura Grand Slam CTF “Easy Reverse”

During the CTF on the 14th of June 2018, I participated in the Grand Slam CTF by Secura in Nieuwegein, The Netherlands. In this CTF was a binary challenge, which I solved together with a teammate who goes by the nickname of “Exploiteer”. The write-up is solemnly written by me.

The binary

For those who have requested the binary, the copyright belongs to Secura and I’m sharing the binary for educational purposes only. The file can be found here: SecuraGrandSlam-EasyReverse200

A first look

To start the challenge, it is important to check what kind of file it is and what the used architecture is. This can be done using “file” in any Linux distribution:

libra@laptop:~/Desktop/SecuraGrandSlamCTF$ file ./easy
./easy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.0.0, BuildID[sha1]=dc20ee648c7b1b86f97c62675f8fd92361ed9d0f, stripped

Note that the architecture that has been used is 32-bit (x86), which will be obvious when viewing the assembly of the binary, but it is always useful to know for sure. Since the binary is an ELF file, it can be executed on a Linux distribution. Executing the binary yields the following result:

libra@laptop:~/Desktop/SecuraGrandSlamCTF$ ./easy
Please supply your corp. secret

The analysis

We start by analysing the binary with Radare2:

r2 ./easy

The default address in the binary is located at the “entry0” function, which is the entry point of the binary. We then use Radare2 to analyse the binary with the command “aaaa”:

[0x08048460]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)

Now we want to know all the functions that are included in the binary. This can be done with the command “afl”, which stands for “All Functions List”.

[0x08048460]> afl
0x080483ac    3 35           fcn.080483ac
0x080483e0    1 6            sym.imp.printf
0x080483f0    1 6            sym.imp.fflush
0x08048400    1 6            sym.imp.sleep
0x08048410    1 6            sym.imp.puts
0x08048420    1 6            sym.imp.strlen
0x08048430    1 6            sym.imp.__libc_start_main
0x08048440    1 6            sym.imp.fprintf
0x08048450    1 6            sub.__gmon_start_450
0x08048460    1 33           entry0
0x08048490    1 4            fcn.08048490
0x080484a0    4 42           fcn.080484a0
0x08048510    3 30           entry2.fini
0x08048530    8 45   -> 99   entry1.init
0x08048563   16 447  -> 446  main

There are a couple of imported functions (“*.imp.*”) and three other functions. One of these is only 4 lines in size. Function “fcn.080483ac” calls the function “sub.__gmon_start_450”. Neither of these two are of any importance and do nothing related to the challenge.

Function “fcn.08048490” is used in “main + 467” but only contains two instructions:

0x08048490      8b1c24         mov ebx, dword [esp]
0x08048493      c3             ret

The function “fcn.080484a0” refers to “fcn.080484a0” and is not of any relevance.

Lastly, the function “main” remains and is of obvious relevance, as it is the default user defined start function, unless the binary has been altered in any other way. A quick way to check this is to check the function content of the “entry0” function with the “pdf” (Print Disassembly Function) command:

[0x08048460]> pdf
;-- section..text:
;-- eip:
/ (fcn) entry0 33
|   entry0 ();
|           0x08048460      31ed           xor ebp, ebp                ; [15] -r-x section size 834 named .text
|           0x08048462      5e             pop esi
|           0x08048463      89e1           mov ecx, esp
|           0x08048465      83e4f0         and esp, 0xfffffff0
|           0x08048468      50             push eax
|           0x08048469      54             push esp
|           0x0804846a      52             push edx
|           0x0804846b      68a0870408     push 0x80487a0
|           0x08048470      6830870408     push 0x8048730
|           0x08048475      51             push ecx
|           0x08048476      56             push esi                    ; void *stack_end
|           0x08048477      6863850408     push main                   ; 0x8048563 ; "U\x89\xe5S\x83\xe4\xf0\x83\xec \x83}\b\x02\x0f\x85\x95\x01" ; int argc
\           0x0804847c      e8afffffff     call sym.imp.__libc_start_main ; int __libc_start_main(func main, int argc, char **ubp_av, func init, func fini, func rtld_fini, void *stack_end)

Here we can see that the function main is passed to the “__libc_start_main” function, which was the expected behaviour.

Set the analysis on the function “main” using the “s” command together with the function name:

s main

To see the flow of the binary, one can enter visual mode using the command “VV”.

After the inspection, I noticed multiple things. Firstly, the provided argument count should equal two. By default, one argument is always provided: the binary itself. Additional parameters are provided by the user as command line arguments. The local variables, as defined by Radare2, are shown directly after the “pdf” command in the main function of the binary:

main (int arg_8h, int arg_ch);
[]
0x0804856d      837d0802       cmp dword [arg_8h], 2
0x08048571      0f8595010000   jne 0x804870c

The first value that is passed to the main function is the argument count (defined as “int arg_8h”), which is later compared to the value two.

The next six lines are another piece of the puzzle:

0x08048577      8b450c         mov eax, dword [arg_ch]     ; [0xc:4]=-1 ; 12
0x0804857a      83c004         add eax, 4
0x0804857d      8b00           mov eax, dword [eax]
0x0804857f      890424         mov dword [esp], eax        ; const char *s
0x08048582      e899feffff     call sym.imp.strlen         ; size_t strlen(const char *s)
0x08048587      83f804         cmp eax, 4

Remember how the argument count was the first parameter (“arg_8h”) that was passed to the main function. The second passed argument (“arg_ch”) is the argument value, as an array. The start location of the array is known, which is equal to the address of “arg_ch”. This is written as “dword [arg_ch]”. Since its a 32-bit address, it is a double word (written as dword). This address is stored in the “eax” register, to which then 4 bytes are added and later loaded.

Since the stack works upwards and the used architecture is 32-bits, this means that one int (or address) above the first item in the array (at index 0, so 1) is loaded.

The function “sym.imp.strlen” is the standard C function “strlen” in the “string.h” header. The value is provided in the general purpose register ESP. The return value is stored, as is default, in the general purpose register EAX. The length of the string is compared to 4, meaning that the provided parameter has to be equal to 4 characters.

To test this, one can execute the executable with 1 parameter of 4 characters. This yields a different result:

libra@laptop:~/Desktop/SecuraGrandSlamCTF$ ./easy abcd

Your UID is: 1684234849

Access...denied!!

One of the first questions that needs to be solved is the generation of the UID. Looking in the assembly of the binary, the question can be solved relatively fast:

; var int local_4h @ esp+0x4
[]
0x080485a0      89442404       mov dword [local_4h], eax
0x080485a4      c70424c08704.  mov dword [esp], str.Your_UID_is:__d ; [0x80487c0:4]=0x72756f59 ; "Your UID is: %d\n" ; const char *format
0x080485ab      e830feffff     call sym.imp.printf         ; int printf(const char *format)

The value in the general purpose register ESP is passed on to the function “sym.imp.printf”. This function is in the “stdio.h” header in C, which prints a value. The string that is used as a placeholder for the “printf” function equals “Your UID is: %d\n”. The “%d” is filled in with the value on the address of the variable “local_4h”, which resides at the value of the general purpose register ESP + 0x4. An integer is 4 bytes in size, which explains the 4 bytes above the value of ESP. A final note on this piece is the result of “%d”: the provided value is printed as a signed decimal number.

The system on which the binary is executed is Ubuntu 16.04, whose endianness equals little endian. The UID of “abcd” equals “1684234849” (in decimal notation), or “0x64636261” in hexadecimal notation.

Now everything that was printed with the use of the input is covered, but there is more to it than meets the eye

A bit further down in the main function, the string “Access” was printed, regardless of the value of the user defined parameter. Additionally, there is a little for-loop which prints the three dots. If the input value is incorrect, the word “denied!!” is printed with a one second sleep between each character. This is most likely to prevent any brute force attempts.

If the answer is correct, the string “granted!” is printed after the three dots. Directly after that, a blank line and the string “Win!” are printed.

The variable “arg_ch”  is the array of all parameters, with the first one (on index 0) being the binary itself. By adding 4 bytes (as it is a 32-bit binary), the next item in the array is selected, resulting in the parameter that the user provided.

The memory address 0x804a038 equals the parameter that was given as input by the user:

0x0804858c      8b450c         mov eax, dword [arg_ch]     ; [0xc:4]=-1 ; 12
0x0804858f      83c004         add eax, 4
[...]
0x08048596      a338a00408     mov dword [0x804a038], eax  ; [0x804a038:4]=0

Between the printing of the output, the given user input is changed a fair bit, resulting in only a single user input being accepted as the right (or winning) answer. Firstly, the “ror” macro is called to rotate the user input string 10 places to the right:

0x080485b0      a138a00408     mov eax, dword [0x804a038]  ; [0x804a038:4]=0
[...]
0x080485bd      c1c810         ror eax, 0x10

Then, the result of the rotation is subtracted with 0x12d70fde:

0x080485ca      a138a00408     mov eax, dword [0x804a038]  ; [0x804a038:4]=0
0x080485cf      2dde0fd712     sub eax, 0x12d70fde

To enter the part where the win condition is triggered, the general purpose register EAX is compared to the value 0x216e6957.

0x080485e5      a138a00408     mov eax, dword [0x804a038]  ; [0x804a038:4]=0
0x080485ea      3d57696e21     cmp eax, 0x216e6957

This brings us back to the end of our first analysis: the UID that is printed. By using the value to which it is originally compared, adding the value that is subtracted and rotating the string 10 places to the left, we find the flag. Below is a proof-of-concept (written in C) with this functionality. The code is commented to explain every step of the process.

/* 
 * File:   main.c
 * Author: Max 'Libra' Kersten
 *
 * Created on June 14, 2018, 11:43 AM
 */
 
#include 
 
/**
 * Taken from Wikipedia (https://en.wikipedia.org/wiki/Circular_shift)
 * @param value the value to be changed
 * @param shift the amount of times to shift
 * @return the shifted value
 */
unsigned int rotate_left(const unsigned int value, int shift) {
    if ((shift &= sizeof (value)*8 - 1) == 0)
        return value;
    return (value << shift) | (value >> (sizeof (value)*8 - shift));
}
 
/*
 * PoC for the "Easy Reverse" challenge in the Secura Grand Slam CTF. Binary SHA-256 hash equals "935ed42837f4b940beaafd8d4f887acd5154416c32ab38d6343bd2d9e246a570"
 */
int main(int argc, char** argv) {
    //Original value to which the user input is compared
    int finalCompareValue = 0x216e6957;
 
    //The subtraction that is done before the finalCompareValue is compared with the "cmp" operand
    int subtraction = 0x12d70fde;
 
    //The total value before the ror (rotation_right) is done
    int totalPreRotation = finalCompareValue + subtraction;
 
    //The value of the int before the ror (rotation_right) is done. This is displayed as the UID of the user
    int rotatedValue = rotate_left(totalPreRotation, 0x10); //rotate_right was used in the binary, to revert this, use rotate_left
 
    //The decimal value, known as the UID of the flag was given to us:
    printf("Decimal value (shown as UID):     %d\n", rotatedValue);
 
    //The hexadecimal value of the flag, as it is found in the 
    printf("Hexadecimal value of the flag:    %x\n", rotatedValue);
 
    //Variable to store the flag before printing
    char flag[4];
    //Convert the first character of the decimal integer (little endian, so only the last two digits are used). The value is stored at the address of the first character of the character array "flag"
    sprintf(&flag[0], "%c", rotatedValue & 0xFF);
    //Convert the second character, ignoring the first character (so 8 bits)
    sprintf(&flag[1], "%c", (rotatedValue >> 8) & 0xFF);
    //Convert the third character, ignoring the first two characters (so 16 bits)
    sprintf(&flag[2], "%c", (rotatedValue >> 16) & 0xFF);
    //Convert the fourth and final character, ignoring the first three characters (so 24 bits)
    sprintf(&flag[3], "%c", (rotatedValue >> 24) & 0xFF);
    //Print the ASCIi value of the flag
    printf("The flag in ASCII:                %s\n", flag);
}

The output of this program leaves us with the correct answer:

Decimal value (shown as UID):     2033529925

Hexadecimal value of the flag:       79353445

The flag in ASCII:                             E45y

 

To test this, we can enter “E45y” as the parameter when we start the binary:

libra@laptop:~/Desktop/SecuraGrandSlamCTF$ ./easy E45y

Your UID is: 2033529925

Access...granted!



Win!

To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit or DM me on Twitter @LibraAnalysis.