This article was published on the 21st of June 2018.
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
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.