This article was published on the 21st of June 2018, and was updated on the 28th of October 2021.
Analysing files requires a lot from an analyst, from understanding the used tooling all the way to understanding concepts in programming. This article will analyse a capture the flag challenge step-by-step, with an in-depth explanation along the way. However, many of the encountered concepts warrant their own article due to the complexity. In the coming chapters, all of these concepts are explained in the detail that they deserve.
This article will go over many of these concepts in a very quick pace, so do not be alarmed if it is hard(er) to follow along. The goal of this article is to be a showcase of sorts, providing insight as to what one can do after the first two chapters of the course.
Table of contents
The challenge
On the 14th of June 2018, I participated in the Grand Slam CTF by Secura in Nieuwegein, The Netherlands. This write-up covers theEasy Reverse challenge, which I solved together with a teammate who goes by the nickname of “Exploiteer”. The write-up is written solemnly by me.
The file can be downloaded here.
A first look
To easily execute the file in a safe environment, it is important to know the required operating system and architecture. This can be done using file, which is present in most Linux distributions. The output of it is given below.
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
The most important information to take away from the output are ELF, 32-bit, Intel 80386, and stripped. This means that the binary is of the ELF type for a 32-bit Intel architecture, and it is stripped from any debugging symbols.
Executing the binary on a suitable operating system, such as Ubuntu, shows the first hint, as can be seen below.
Please supply your corp. secret
The analysis
To disassemble the file, one can use many tools. In this example, Radare2 is used. Note that the output of Radare2 in this blog is based on the version that was the latest at the time of writing in 2018. For those following along: your output can differ, which is perfectly fine. To open the file using Radare2, run the command that is given below in the terminal, assuming the terminal’s working directory is in the same location as the challenge, which is named easy.
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, which includes experimental analysis methods. The output is given below.
[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)
Once the analysis is complete, it is useful to get an overview of the functions that are present in the binary. This can be done using afl, which stands for Analysis Function List. The output is given below.
[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, which are denoted with sym.imp. Imported functions are part of libraries which are used by this program. The functions that are denoted with fcn are functions that contain the binary’s code. The numbers behind fcn indicate the address at which the function starts.
The function main is the default user defined start function, which is where this adventure starts. One can issue commands for a given address in Radare2. Omitting the address will ensure that the command is processed as if the current address is used. To change the current address, one uses s, which stands for seek. To change the current address to that of the main function, one uses s main. To print the disassembly of the function at the current address, one can use pdf, which stands for Print Disassembly Function.
When looking at the main function, several things stand out.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 in the function signature, as can be seen below.
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 arg_8h), which is later compared to the value two.
The next six lines are another piece of the puzzle, as can be seen below.
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 [arg_ch]. This address is stored in the EAX (accumulating) register, to which then 4 bytes are added and later loaded. The four bytes are added as the second variable from the array is required, since that is the first command-line argument’s location. The reason why it is 4 bytes, is due to the 32-bit architecture: 32-bits equal 4 bytes.
The function “sym.imp.strlen” is the standard C function strlen in the string.h header. The value is provided in the general purpose stack pointer register ESP. The return value is stored, as is default, in the general purpose accumulator register EAX. The length of the string is compared to 4, meaning that the provided parameter has to 4 characters in length.
To test this, one can execute the executable with one parameter of 4 characters. This yields a different result, as can be seen below.
easy abcd Your UID is: 1684234849 Access...denied!!
The command-line argument expects the corp secret, as the initial error message already referred to. This begs the question: how is the UID generated? 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 UID is printed using printf, with two arguments. The arguments are passed in the inverse order due to the calling convention. The first argument is equal to Your UID is: %d\n. The %d is replaced with the second argument, 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. Everything that was printed based on the given input is covered, but there is more to it than meets the eye
A bit further down in the main function, the string Access is printed, regardless of the value of the user defined parameter. Additionally, there is a for-loop which prints the three dots. If the input value is incorrect, the string 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 that contains all command-line arguments, 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, as can be seen below.
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 correct answer. Firstly, the ror rotate right instruction is used to rotate the user input string 0x10 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
To get the correct value, one has to reverse the order of the steps, meaning that the eventual value is used as the starting value. The value that is deducted from the user-input, is added to the starting value, after the total should be rotated 10 places to the left. This provides is the flag that solves the challenge, when written in human readable characters.
Writing the password generator
Below is a proof-of-concept, written in C, with the above-described steps. 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 <stdio.h> /** * 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 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:
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 @Libranalysis.