Nytro Posted November 30, 2019 Report Posted November 30, 2019 Practical case: Crack Me 0x03 In this article, a crack me challenge that was present in the Hackfest2019 classic CTF will be solved. The challenge has been created by myself. Firstly, information about the challenge will be given, after which it will be solved in multiple ways. At last, the challenge’s complete source code and design process will be discussed within this article. One can download the challenge here. In total, 52 teams participated in the classic Hackfest2019 CTF. Out of these, there were 18 teams that solved the challenge. Observations regarding the challenge The theme of Hackfest2019 was upside down, although the CTF itself was not bound to this theme. When opening the challenge page, one was greeted with the message that is given below. BRIEFING STATEMENT 02489184 This is a call for aid to any reverse engineer that is skilled enough to solve this riddle. Everybody here solved this ridiculously easy challenge, obviously. Duh! If you think you're worthy, then see for yourself. Oh, and uh, please do submit the answer in this online platform so we can, uh, verify it! END OF BRIEFING Aside from being both a taunt and a joke, the message contains a hint: the answer to the question is the flag for this challenge. As there is no flag format for this CTF, this hint provides valuable information. When using GNU Linux file utility, one will see that the challenge consists of a stripped x86_64 ELF binary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=db660f9010ec370828ed57e15cbc7d25cbf4c479, stripped Upon executing the binary, it prints a riddle. After that, user input is requested. If up is down, then where are you now? I am: The program takes a few seconds before providing an answer, as can be seen below. I am: asdf Not all those who wander are lost, although it seems you are! Upon entering a long string, the program crashes. This means that there is little to no sanitation on the provided input. If up is down, then where are you now? I am: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Not all those who wander are lost, although it seems you are! *** stack smashing detected ***: terminated Aborted (core dumped) Since this is not a binary exploitation challenge, there is not too much that can be gained from experimenting with the length of the input. Lastly, the strings utility can potentially provide valuable insight into the binary itself. The output is given below. /lib64/ld-linux-x86-64.so.2 libc.so.6 strcpy srand __isoc99_scanf puts time __stack_chk_fail printf sleep __cxa_finalize strcmp __libc_start_main GLIBC_2.7 GLIBC_2.4 GLIBC_2.2.5 _ITM_deregisterTMCloneTable __gmon_start__ _ITM_registerTMCloneTable AWAVI AUATL []A\A]A^A_ If up is down, then where are you now? I am: It seems to me, that you are home! Not all those who wander are lost, although it seems you are! ;*3$" GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 .shstrtab .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .dynamic .data .bss .comment The strings that are shown above, are the result of using strings. Noteworthy is the string that is shown when the user is home. This likely is the message one gets when the correct input is given. It is highlighted below. It seems to me, that you are home! Multiple approaches There are multiple approaches to solve such a challenge. In this article, the challenge will be solved dynamically using Radare2, via tracing with ltrace, and statically with Ghidra. Additionally, the creation of the challenge will be discussed, together with the source code. Note that each method only uses information from the observations above. This provides insight in the multiple ways that can be used to solve this challenge. Dynamic analysis – debugging The debugging of this binary will be done using Radare2. In this article, the version of Radare2 is radare2 4.1.0-git 23803 @ linux-x86-64 git.4.0.0-42-ged0873e2f. To debug a binary, use the -d flag, together with a path to the binary, as can be seen below. r2 -d ./whereAmI.elf Radare2 will then provide information about the process ID, the binary’s address, and the bitness of the sample. The output is given below. Process with PID 17757 started... = attach 17757 17757 bin.baddr 0x555ef9f54000 Using 0x555ef9f54000 asm.bits 64 One can then instruct Radare2 to analyse the loaded binary using aaa. After the analysis is complete, one can get an overview of all functions using afl (All Functions List), as can be seen below. 0x555ef9f547c0 1 42 entry00x555efa155fe0 1 4124 reloc.__libc_start_main 0x555ef9f54720 1 6 sym.imp.strcpy 0x555ef9f54730 1 6 sym.imp.puts 0x555ef9f54740 1 6 sym.imp.__stack_chk_fail 0x555ef9f54750 1 6 sym.imp.printf 0x555ef9f54000 2 25 map.home_libra_Downloads_whereAmI.elf.r_x 0x555ef9f54760 1 6 sym.imp.srand 0x555ef9f54770 1 6 sym.imp.strcmp 0x555ef9f54780 1 6 sym.imp.time 0x555ef9f54790 1 6 sym.imp.__isoc99_scanf 0x555ef9f547a0 1 6 sym.imp.sleep 0x555ef9f548c0 5 154 -> 67 entry.init0 0x555ef9f54880 5 58 -> 51 entry.fini0 0x555ef9f547b0 1 6 fcn.555ef9f547b0 0x555ef9f547f0 4 50 -> 40 fcn.555ef9f547f0 0x555ef9f548ca 15 1874 main Based on the size and purpose of the main function, this function is the first to check out. Using s main, one can seek towards the main function. Using pdf (Print Disassembly Function), one can get the complete disassembly of the function. The disassembled instructions are given below. Afterwards, the assembly instructions are analysed in smaller parts. 0x555ef9f548ca push rbp 0x555ef9f548cb mov rbp, rsp 0x555ef9f548ce sub rsp, 0x820 0x555ef9f548d5 mov dword [var_814h], edi ; argc 0x555ef9f548db mov qword [var_820h], rsi ; argv 0x555ef9f548e2 mov rax, qword fs:[0x28] 0x555ef9f548eb mov qword [var_8h], rax 0x555ef9f548ef xor eax, eax 0x555ef9f548f1 lea rdi, str.If_up_is_down__then_where_are_you_now ; 0x555ef9f550a8 ; "If up is down, then where are you now?" 0x555ef9f548f8 call sym.imp.puts ; int puts(const char *s) 0x555ef9f548fd lea rdi, str.I_am: ; 0x555ef9f550cf ; "I am: " 0x555ef9f54904 mov eax, 0 0x555ef9f54909 call sym.imp.printf ; int printf(const char *format) 0x555ef9f5490e lea rax, [var_20h] 0x555ef9f54912 mov rsi, rax 0x555ef9f54915 lea rdi, [0x555ef9f550d6] ; "%s" 0x555ef9f5491c mov eax, 0 0x555ef9f54921 call sym.imp.__isoc99_scanf ; int scanf(const char *format) 0x555ef9f54926 mov edi, 3 0x555ef9f5492b call sym.imp.sleep ; int sleep(int s) ;omitted variable initialisations 0x555ef9f54ea1 mov qword [var_4d8h], rax 0x555ef9f54ea8 mov dword [var_800h], 4 0x555ef9f54eb2 mov dword [var_7fch], 0x12c ; 300 0x555ef9f54ebc mov edi, 0 0x555ef9f54ec1 call sym.imp.time ; time_t time(time_t *timer) 0x555ef9f54ec6 mov edi, eax 0x555ef9f54ec8 call sym.imp.srand ; void srand(int seed) 0x555ef9f54ecd mov dword [var_804h], 0 0x555ef9f54ed7 mov dword [var_7f8h], 0x29 ; ')' ; 41 0x555ef9f54ee1 mov dword [var_7f4h], 3 0x555ef9f54eeb mov byte [var_9h], 0xa 0x555ef9f54eef mov dword [var_804h], 0 ┌─< 0x555ef9f54ef9 jmp 0x555ef9f54f24 ┌──> 0x555ef9f54efb mov eax, dword [var_804h] ╎│ 0x555ef9f54f01 movsxd rdx, eax ╎│ 0x555ef9f54f04 mov rax, qword [var_788h] ╎│ 0x555ef9f54f0b add rax, rdx ╎│ 0x555ef9f54f0e movzx edx, byte [rax] ╎│ 0x555ef9f54f11 mov eax, dword [var_804h] ╎│ 0x555ef9f54f17 cdqe ╎│ 0x555ef9f54f19 mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54f1d add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54ef9 ╎└─> 0x555ef9f54f24 mov eax, dword [var_804h] ╎ 0x555ef9f54f2a cmp eax, dword [var_7f4h] └──< 0x555ef9f54f30 jl 0x555ef9f54efb 0x555ef9f54f32 lea rax, [var_14h] 0x555ef9f54f36 add rax, 3 0x555ef9f54f3a mov rdx, qword [var_4e0h] 0x555ef9f54f41 mov rsi, rdx 0x555ef9f54f44 mov rdi, rax 0x555ef9f54f47 call sym.imp.strcpy ; char *strcpy(char *dest, const char *src) 0x555ef9f54f4c mov dword [var_804h], 0 ┌─< 0x555ef9f54f56 jmp 0x555ef9f54f85 ┌──> 0x555ef9f54f58 mov eax, dword [var_804h] ╎│ 0x555ef9f54f5e movsxd rdx, eax ╎│ 0x555ef9f54f61 mov rax, qword [var_670h] ╎│ 0x555ef9f54f68 add rax, rdx ╎│ 0x555ef9f54f6b mov edx, dword [var_804h] ╎│ 0x555ef9f54f71 lea ecx, [rdx + 6] ╎│ 0x555ef9f54f74 movzx edx, byte [rax] ╎│ 0x555ef9f54f77 movsxd rax, ecx ╎│ 0x555ef9f54f7a mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54f7e add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54f56 ╎└─> 0x555ef9f54f85 cmp dword [var_804h], 2 └──< 0x555ef9f54f8c jle 0x555ef9f54f58 0x555ef9f54f8e mov dword [var_804h], 0 ┌─< 0x555ef9f54f98 jmp 0x555ef9f54fc7 ┌──> 0x555ef9f54f9a mov eax, dword [var_804h] ╎│ 0x555ef9f54fa0 movsxd rdx, eax ╎│ 0x555ef9f54fa3 mov rax, qword [var_718h] ╎│ 0x555ef9f54faa add rax, rdx ╎│ 0x555ef9f54fad mov edx, dword [var_804h] ╎│ 0x555ef9f54fb3 lea ecx, [rdx + 9] ╎│ 0x555ef9f54fb6 movzx edx, byte [rax] ╎│ 0x555ef9f54fb9 movsxd rax, ecx ╎│ 0x555ef9f54fbc mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54fc0 add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54f98 ╎└─> 0x555ef9f54fc7 cmp dword [var_804h], 2 └──< 0x555ef9f54fce jle 0x555ef9f54f9a 0x555ef9f54fd0 lea rdx, [var_20h] 0x555ef9f54fd4 lea rax, [var_14h] 0x555ef9f54fd8 mov rsi, rdx 0x555ef9f54fdb mov rdi, rax 0x555ef9f54fde call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2) 0x555ef9f54fe3 test eax, eax ┌─< 0x555ef9f54fe5 jne 0x555ef9f54ff5 │ 0x555ef9f54fe7 lea rdi, str.It_seems_to_me__that_you_are_home ; 0x555ef9f55268 ; "It seems to me, that you are home!" │ 0x555ef9f54fee call sym.imp.puts ; int puts(const char *s) ┌──< 0x555ef9f54ff3 jmp 0x555ef9f55001 │└─> 0x555ef9f54ff5 lea rdi, str.Not_all_those_who_wander_are_lost__although_it_seems_you_are ; 0x555ef9f55290 ; "Not all those who wander are lost, although it seems you are!" │ 0x555ef9f54ffc call sym.imp.puts ; int puts(const char *s) │ ; CODE XREF from main @ 0x555ef9f54ff3 └──> 0x555ef9f55001 mov eax, 0 0x555ef9f55006 mov rcx, qword [var_8h] 0x555ef9f5500a xor rcx, qword fs:[0x28] ┌─< 0x555ef9f55013 je 0x555ef9f5501a │ 0x555ef9f55015 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) └─> 0x555ef9f5501a leave └ 0x555ef9f5501b ret Note that the comments and arrows are added by Radare2. The first part of the code contains the function prologue, sets the values of argc and argv, prints the riddle via puts and printf, and takes the user input via scanf. 0x555ef9f548ca push rbp 0x555ef9f548cb mov rbp, rsp 0x555ef9f548ce sub rsp, 0x820 0x555ef9f548d5 mov dword [var_814h], edi ; argc 0x555ef9f548db mov qword [var_820h], rsi ; argv 0x555ef9f548e2 mov rax, qword fs:[0x28] 0x555ef9f548eb mov qword [var_8h], rax 0x555ef9f548ef xor eax, eax 0x555ef9f548f1 lea rdi, str.If_up_is_down__then_where_are_you_now ; 0x555ef9f550a8 ; "If up is down, then where are you now?" 0x555ef9f548f8 call sym.imp.puts ; int puts(const char *s) 0x555ef9f548fd lea rdi, str.I_am: ; 0x555ef9f550cf ; "I am: " 0x555ef9f54904 mov eax, 0 0x555ef9f54909 call sym.imp.printf ; int printf(const char *format) 0x555ef9f5490e lea rax, [var_20h] 0x555ef9f54912 mov rsi, rax 0x555ef9f54915 lea rdi, [0x555ef9f550d6] ; "%s" 0x555ef9f5491c mov eax, 0 0x555ef9f54921 call sym.imp.__isoc99_scanf ; int scanf(const char *format) Note that this is a 64-bit binary, meaning that the first few arguments are passed via registers instead of the stack. The time that the program takes after the user input is not because it decrypts anything, but because the sleep function is called with 3 as an argument. The argument is the amount of seconds that the program should sleep. After that, there are a lot of variable declarations. In the disassembly below, these are omitted to improve the readability of the code. 0x555ef9f54926 mov edi, 3 0x555ef9f5492b call sym.imp.sleep ; int sleep(int s) ;omitted variable initialisations When the 3 second sleep is over, more variables are set. After that the current time since epoch is requested. The result of this is stored in eax. The value of eax is then moved into edi, where the current time is used as a seed for the random function. 0x555ef9f54ea1 mov qword [var_4d8h], rax 0x555ef9f54ea8 mov dword [var_800h], 4 0x555ef9f54eb2 mov dword [var_7fch], 0x12c ; 300 0x555ef9f54ebc mov edi, 0 0x555ef9f54ec1 call sym.imp.time ; time_t time(time_t *timer) 0x555ef9f54ec6 mov edi, eax 0x555ef9f54ec8 call sym.imp.srand ; void srand(int seed) A couple of other variables are then set, where one instruction is different from the majority that is encountered. At 0x555ef9f54eeb, only a single byte is moved. The value 0xa is also equal to a newline character. 0x555ef9f54ecd mov dword [var_804h], 0 0x555ef9f54ed7 mov dword [var_7f8h], 0x29 ; ')' ; 41 0x555ef9f54ee1 mov dword [var_7f4h], 3 0x555ef9f54eeb mov byte [var_9h], 0xa After that, a loop is encountered. At first, the variable var_804h is set to 0, making it likely that this variable is used as the counter, or i in the original source code. The unconditional jump downards moves i into eax. If eax is less than the value of var_7f4h, the upwards jump is taken again. Just above the loop, var_7f4h is set to equal 3. This means that this loop will iterate three times before the execution continues. The code is given below. 0x555ef9f54eef mov dword [var_804h], 0 ┌─< 0x555ef9f54ef9 jmp 0x555ef9f54f24 ┌──> 0x555ef9f54efb mov eax, dword [var_804h] ╎│ 0x555ef9f54f01 movsxd rdx, eax ╎│ 0x555ef9f54f04 mov rax, qword [var_788h] ╎│ 0x555ef9f54f0b add rax, rdx ╎│ 0x555ef9f54f0e movzx edx, byte [rax] ╎│ 0x555ef9f54f11 mov eax, dword [var_804h] ╎│ 0x555ef9f54f17 cdqe ╎│ 0x555ef9f54f19 mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54f1d add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54ef9 ╎└─> 0x555ef9f54f24 mov eax, dword [var_804h] ╎ 0x555ef9f54f2a cmp eax, dword [var_7f4h] └──< 0x555ef9f54f30 jl 0x555ef9f54efb Within the loop, the value of i is moved into eax, after which it is stored in rdx using the movsxd (Move With Sign Extension) instruction. The value that resides at var_788h is then moved into rax, and the value of i is added. The single byte that resides at the value of rax is moved into edx. The cdqe (Convert Doubleword to Quardword) instruction then increases the size. The value of dl is then stored at rbp plus rax minus 0x14. In other words, the value of dl is stored at base pointer plus the offset of the variable minus 0x14. At last, i is incremented with one. Based on the new value of i, the jump might or might not be taken. To simplify the explanation above, the value that is taken depends on the offset that is equal to i. When using arrays, i is often used to determine the offset within a loop. Note that the offset is used both when getting a value and setting a value. As such, one can rewrite the assembly code above into the C code that is given below. array1[i] = array2[i]; The address that var_14h points to is stored into eax, after which it is incremented with three. The value that resides at var_4e0h is then stored at rdx. Both values are then passed to the strcpy function, which copies the value of the second argument at the location that is stored in the first argument. Note that the offset of 3 is used when defining the destination of the strcpy function. The previous loop iterated for three times. 0x555ef9f54f32 lea rax, [var_14h] 0x555ef9f54f36 add rax, 3 0x555ef9f54f3a mov rdx, qword [var_4e0h] 0x555ef9f54f41 mov rsi, rdx 0x555ef9f54f44 mov rdi, rax 0x555ef9f54f47 call sym.imp.strcpy ; char *strcpy(char *dest, const char *src) The loop below is similar the the one that is observed above. It copies a variable into rbp + rax – 0x14, where rax is the iteration’s offset. This time, however, the offset starts at 6 before adding the value of the current iteration, which is stored in var_804h. 0x555ef9f54f4c mov dword [var_804h], 0 ┌─< 0x555ef9f54f56 jmp 0x555ef9f54f85 ┌──> 0x555ef9f54f58 mov eax, dword [var_804h] ╎│ 0x555ef9f54f5e movsxd rdx, eax ╎│ 0x555ef9f54f61 mov rax, qword [var_670h] ╎│ 0x555ef9f54f68 add rax, rdx ╎│ 0x555ef9f54f6b mov edx, dword [var_804h] ╎│ 0x555ef9f54f71 lea ecx, [rdx + 6] ╎│ 0x555ef9f54f74 movzx edx, byte [rax] ╎│ 0x555ef9f54f77 movsxd rax, ecx ╎│ 0x555ef9f54f7a mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54f7e add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54f56 ╎└─> 0x555ef9f54f85 cmp dword [var_804h], 2 └──< 0x555ef9f54f8c jle 0x555ef9f54f58 Alternatively, one could use i and the hardcoded offset in pseudo code as follows: array1[i + 6] = array2[i]; Similar to the loop above, this loop performs the same action, but using a default offset of 9 instead of 6. 0x555ef9f54f8e mov dword [var_804h], 0 ┌─< 0x555ef9f54f98 jmp 0x555ef9f54fc7 ┌──> 0x555ef9f54f9a mov eax, dword [var_804h] ╎│ 0x555ef9f54fa0 movsxd rdx, eax ╎│ 0x555ef9f54fa3 mov rax, qword [var_718h] ╎│ 0x555ef9f54faa add rax, rdx ╎│ 0x555ef9f54fad mov edx, dword [var_804h] ╎│ 0x555ef9f54fb3 lea ecx, [rdx + 9] ╎│ 0x555ef9f54fb6 movzx edx, byte [rax] ╎│ 0x555ef9f54fb9 movsxd rax, ecx ╎│ 0x555ef9f54fbc mov byte [rbp + rax - 0x14], dl ╎│ 0x555ef9f54fc0 add dword [var_804h], 1 ╎│ ; CODE XREF from main @ 0x555ef9f54f98 ╎└─> 0x555ef9f54fc7 cmp dword [var_804h], 2 └──< 0x555ef9f54fce jle 0x555ef9f54f9a The loops and string copy are used to create one string out of multiple others. The offset that increases with three every time, is used to determine where the new string should start. Two variables are then compared using the strcmp function. 0x555ef9f54fd0 lea rdx, [var_20h] 0x555ef9f54fd4 lea rax, [var_14h] 0x555ef9f54fd8 mov rsi, rdx 0x555ef9f54fdb mov rdi, rax 0x555ef9f54fde call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2) The return value of strcmp is then compared. If the two strings are equal, the success message is printed. If the two strings are not equal, the failure message is printed. The program then exits by returning zero, which is the exit code of a graceful exit. 0x555ef9f54fe3 test eax, eax ┌─< 0x555ef9f54fe5 jne 0x555ef9f54ff5 │ 0x555ef9f54fe7 lea rdi, str.It_seems_to_me__that_you_are_home ; 0x555ef9f55268 ; "It seems to me, that you are home!" │ 0x555ef9f54fee call sym.imp.puts ; int puts(const char *s) ┌──< 0x555ef9f54ff3 jmp 0x555ef9f55001 │└─> 0x555ef9f54ff5 lea rdi, str.Not_all_those_who_wander_are_lost__although_it_seems_you_are ; 0x555ef9f55290 ; "Not all those who wander are lost, although it seems you are!" │ 0x555ef9f54ffc call sym.imp.puts ; int puts(const char *s) │ ; CODE XREF from main @ 0x555ef9f54ff3 └──> 0x555ef9f55001 mov eax, 0 0x555ef9f55006 mov rcx, qword [var_8h] 0x555ef9f5500a xor rcx, qword fs:[0x28] ┌─< 0x555ef9f55013 je 0x555ef9f5501a │ 0x555ef9f55015 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) └─> 0x555ef9f5501a leave └ 0x555ef9f5501b ret To see when the two strings are equal, one can set a breakpoint on the strcmp function. By inspecting the arguments in the memory, one can see both the user input and the generated flag. The strcmp function resides at 0x555ef9f54fde. Within Radare2, one can set a breakpoint using db address (Debug Break) where address is the location of the breakpoint. As such, one has to issue the following command to set a breakpoint on the strcmp function: db 0x555ef9f54fde To continue execution until the breakpoint is hit, one uses the dc (Debug Continue) command. During the execution, one has to provide user input to the riddle in order to continue the execution. When the breakpoint is hit, the two arguments are located at rdx and rdi. Below, the argument set-up and function call to the strcmp function are given again. 0x555ef9f54fd0 lea rdx, [var_20h] 0x555ef9f54fd4 lea rax, [var_14h] 0x555ef9f54fd8 mov rsi, rdx 0x555ef9f54fdb mov rdi, rax 0x555ef9f54fde call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2) The variable names are based upon the offset compared to rbp. The location where all the loops wrote data to is located at rbp + rax – 0x14. In this case, rax was used to determine the offset into rbp – 0x14, which is also known as var_14h. As such, the variable var_14h contains the flag that was generated. The address of var_14h is stored in rax, after which it is moved into rdi. When the breakpoint is hit, one can use ps @ address to print the string at a given address or register. The result of ps @ rdi will provide the flag: D0wN_uN|)3r Dynamic analysis – tracing When tracing the binary, one will get to know information about the calls it makes without interfering with the binary itself. Given that the standard C library is used (based on the strings within the binary), one can assume that functions from this library are used to perform actions. As such, the GNU Linux ltrace (Library Trace) utility can be used. Below, an excerpt from the ltrace manual pages is given. ltrace is a program that simply runs the specified command until it exits. It intercepts and records the dynamic library calls which are called by the executed process and the signals which are received by that process. It can also intercept and print the system calls executed by the program. Starting the binary is simple: provide the path to ltrace as an argument and press enter. The output is given below. ltrace ./whereAmI.elf puts("If up is down, then where are yo"...If up is down, then where are you now? ) = 39 printf("I am: ") = 6 __isoc99_scanf(0x55799c2230d6, 0x7ffc8b93d7e0, 0, 0I am: The program then awaits the user input, as it normally would. The execution of the program is resumed normally after providing the user input, as can be seen below. __isoc99_scanf(0x55799c2230d6, 0x7ffc8b93d7e0, 0, 0I am: user_input ) = 1 sleep(3) = 0 time(0) = 1574117574 srand(0x5dd320c6, 0x7ffc8b93cfa0, 0, 0x7ff3ed5b29a4) = 0 strcpy(0x7ffc8b93d7ef, "N_u") = 0x7ffc8b93d7ef strcmp("D0wN_uN|)3r", "user_input") = -49 puts("Not all those who wander are los"...Not all those who wander are lost, although it seems you are! ) = 62 +++ exited (status 0) +++ As can be seen in the output, the program sleeps for 3 seconds, after which the current time is obtained. The random function is seeded, the string N_u is copied towards a specific address. The user input is then compared to D0wN_uN|)3r and the failure message is then written towards the output stream. This way, the flag is obtained without diassembling the binary. Static code analysis All default options were used when analysing the binary with Ghidra 9.0.2, as well as the Decompile Parameter ID option. Since Ghidra cannot find a main function, one will have to start at the entry function, which is given below. void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3) { undefined8 in_stack_00000000; undefined auStack8 [8]; __libc_start_main(FUN_001008ca,in_stack_00000000,&stack0x00000008,FUN_00101020,FUN_00101090, param_3,auStack8); do { /* WARNING: Do nothing block with infinite loop */ } while( true ); } The __libc_start_main function requires multiple arguments. By default, the first argument is a pointer towards the main function. Double clicking on it will show the main function. The complete function is given below, after which it is analysed in parts. undefined8 FUN_001008ca(void) { int iVar1; time_t tVar2; long in_FS_OFFSET; int local_80c; char local_28 [12]; char local_1c [11]; undefined local_11; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("If up is down, then where are you now?"); printf("I am: "); __isoc99_scanf(&DAT_001010d6,local_28); sleep(3); tVar2 = time((time_t *)0x0); srand((uint)tVar2); local_11 = 10; local_80c = 0; while (local_80c < 3) { local_1c[(long)local_80c] = (&DAT_0010110d)[(long)local_80c]; local_80c = local_80c + 1; } strcpy(local_1c + 3,"N_u"); local_80c = 0; while (local_80c < 3) { local_1c[(long)(local_80c + 6)] = (&DAT_00101198)[(long)local_80c]; local_80c = local_80c + 1; } local_80c = 0; while (local_80c < 3) { local_1c[(long)(local_80c + 9)] = (&DAT_00101145)[(long)local_80c]; local_80c = local_80c + 1; } iVar1 = strcmp(local_1c,local_28); if (iVar1 == 0) { puts("It seems to me, that you are home!"); } else { puts("Not all those who wander are lost, although it seems you are!"); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; } The purpose of the function becomes clear near the end, where two strings are compared. Based on the comparison result (which is stored in iVar1), one of two possible strings is printed. If the two values are equal, the success message is printed. If they are not equal, the failure message is printed. One can rename iVar1 into comparisionResult. Within the loops above, the variable local_80c is used to keep track of the amount o fiterations. As such, this variable can be renamed into i. In the beginning, the program prints the riddle and requests the user input. The user input is stored in local_28, which can be renamed into userInput. Based on the two arguments of the string compare function, this means that local_1c is the generated flag. One can rename local_1c into generatedFlag After storing the user input, the program sleeps for 3 seconds. The current time since epoch is then stored in tVar2, after which it is used to seed the random function. One can rename tVar2 into currentTime, as can be seen below. puts("If up is down, then where are you now?"); printf("I am: "); __isoc99_scanf(&DAT_001010d6,userInput); sleep(3); currentTime = time((time_t *)0x0); srand((uint)currentTime); All the global variables are marked with DAT_*. All global variables within the loops are character arrays. By changing their type into char[3] using the T hotkey in the diassembly window, their literal value will be shown within the decompiler. The code below reflects all changes that have been made. undefined8 FUN_001008ca(void) { long lVar1; int comparisionResult; time_t currentTime; long in_FS_OFFSET; int i; char userInput [12]; char generatedFlag [11]; lVar1 = *(long *)(in_FS_OFFSET + 0x28); puts("If up is down, then where are you now?"); printf("I am: "); __isoc99_scanf(&DAT_001010d6,userInput); sleep(3); currentTime = time((time_t *)0x0); srand((uint)currentTime); i = 0; while (i < 3) { generatedFlag[(long)i] = "D0w"[(long)i]; i = i + 1; } strcpy(generatedFlag + 3,"N_u"); i = 0; while (i < 3) { generatedFlag[(long)(i + 6)] = "N|)"[(long)i]; i = i + 1; } i = 0; while (i < 3) { generatedFlag[(long)(i + 9)] = "3r"[(long)i]; i = i + 1; } comparisionResult = strcmp(generatedFlag,userInput); if (comparisionResult == 0) { puts("It seems to me, that you are home!"); } else { puts("Not all those who wander are lost, although it seems you are!"); } if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; } Based on this, one can see how the flag is constructed. There are four variables that are concatenated into a single string to form the flag. The first three characters are copied into index zero through two within a loop. The next three characters are copied into index three through five using the strcpy function. The next two loops copy the last characters at different offsets. Each loop has an offset to ensure that the character that is copied, is placed at the correct location of the final string. One can simply copy and paste each part to form the flag: D0wN_uN|)3r. Challenge creation When creating a challenge, multiple methods to get the flag need to be possible whilst keeping a specific difficulty level in mind. In this case, the flag is not encrypted but spread out in multiple parts in no apparent order. Between the flag parts, there are a lot of unused variables of equal size to confuse the analyst. During runtime, the multiple flag parts are assembled in order, after which the user input is compared to the assembled flag. The length of each part, either part of the flag or a part of the garbage code to confuse the analyst, is 3 characters at most. This is done because the GNU Linux strings utility searches for strings that are at least four characters in size by default, as can be seen in the excerpt from the strings utility manual page. For each file given, GNU strings prints the printable character sequences that are at least 4 characters long (or the number given with the options below) and are followed by an unprintable character. By doing so, one cannot obtain the password by simply getting the strings from the binary. If one were to change the amount of characters to a smaller number, all the garbage strings will also show up. This provides some sort of a clue to the analyst, but it does not provide an answer. Due to the amount of garbage strings and the three second sleep after ingesting the user input, the application becomes infeasble to brute force. Using the current time to seed the random function is used to throw off analysts who are less certain of the analysis process. The variables are not used at all. The riddle is paired with the theme of Hackfest2019: upside down. The riddle asks where one is, if up is down. The event itself is hosted in Canada. Flipping the world map upside down, one can see that Canada would be at the place where Australia normally is. Note that one has to imagine the world map as a rectangle whilst turning it 180 degrees to the right. As such, one would be down under, or D0wN_uN|)3r in leetspeak. The full source of the challenge is given below. /* * File: whereAmI.c * Author: Max 'Libra' Kersten [@LibraAnalysis] * * Created on October 2, 2019, 9:06 PM */ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <unistd.h> #include <string.h> /* * Flag: D0wN_uN|)3r */ int main(int argc, char *argv[]) { printf("If up is down, then where are you now?\n"); printf("I am: "); char userInput[12]; scanf("%s", userInput); sleep(3); char* trash0 = "DnF"; char* trash1 = "nIP"; char* trash2 = "zEZ"; char* trash3 = "uRC"; char* trash4 = "LgP"; char* trash5 = "cdM"; char* trash6 = "qeB"; char* trash7 = "xbI"; char* trash8 = "KPK"; char* trash9 = "jpb"; char* trash10 = "pWL"; char* trash11 = "NXs"; char* trash12 = "Ckx"; char* part1 = "D0w"; char* trash14 = "GXO"; char* trash15 = "RjK"; char* trash16 = "YbL"; char* trash17 = "VWC"; char* trash18 = "IWF"; char* trash19 = "gZF"; char* trash20 = "Lon"; char* trash21 = "ayh"; char* trash22 = "dGC"; char* trash23 = "MZI"; char* trash24 = "oPO"; char* trash25 = "wwK"; char* trash26 = "ySv"; char* part4 = "3r"; char* trash28 = "rOK"; char* trash29 = "HRE"; char* trash30 = "Qdw"; char* trash31 = "UIY"; char* trash32 = "XpQ"; char* trash33 = "MOP"; char* trash34 = "ayt"; char* trash35 = "zWI"; char* trash36 = "opG"; char* trash37 = "AnG"; char* trash38 = "hjs"; char* trash39 = "DeC"; char* trash40 = "lFO"; char* trash41 = "lLk"; char* trash42 = "kPe"; char* trash43 = "nJb"; char* trash44 = "hXE"; char* trash45 = "gIj"; char* trash46 = "NTx"; char* trash47 = "sHw"; char* part3 = "N|)"; char* trash49 = "UsC"; char* trash50 = "mkg"; char* trash51 = "hMI"; char* trash52 = "mBL"; char* trash53 = "lqU"; char* trash54 = "Cou"; char* trash55 = "AoM"; char* trash56 = "tBO"; char* trash57 = "ZAq"; char* trash58 = "rbo"; char* trash59 = "YOx"; char* trash60 = "JiX"; char* trash61 = "rhy"; char* trash62 = "lxq"; char* trash63 = "OOF"; char* trash64 = "AiJ"; char* trash65 = "fjz"; char* trash66 = "CKA"; char* trash67 = "Nbm"; char* trash68 = "qQV"; char* trash69 = "DSe"; char* trash70 = "ebQ"; char* trash71 = "Rwl"; char* trash72 = "Fia"; char* trash73 = "uyx"; char* trash74 = "sZz"; char* trash75 = "kfn"; char* trash76 = "jGG"; char* trash77 = "mwA"; char* trash78 = "TKW"; char* trash79 = "GWI"; char* trash80 = "KOl"; char* trash81 = "HMo"; char* trash82 = "aea"; char* trash83 = "ZPM"; char* trash84 = "JQd"; char* trash85 = "rqL"; char* trash86 = "dlY"; char* trash87 = "yBR"; char* trash88 = "eaz"; char* trash89 = "TTX"; char* trash90 = "ZGE"; char* trash91 = "SRc"; char* trash92 = "xxG"; char* trash93 = "jZY"; char* trash94 = "DCS"; char* trash95 = "chc"; char* trash96 = "SDe"; char* trash97 = "sRL"; char* part2 = "N_u"; char* trash99 = "jVl"; int argc2 = 4; int array[300]; int size = 300; srand(time(NULL)); int i = 0; int j = 41; int length = 3; char result[12]; result[11] = '\n'; i = 0; for (i; i < length; i++) { result[i] = part1[i]; } strcpy((result + 3), part2); for (i = 0; i < 3; ++i) { result[i + 6] = part3[i]; } for (i = 0; i < 3; ++i) { result[i + 9] = part4[i]; } if (strcmp(result, userInput) == 0) { printf("It seems to me, that you are home!\n"); } else { printf("Not all those who wander are lost, although it seems you are!\n"); } return (EXIT_SUCCESS); } Conclusion Both CTF challenges and malware can be analysed in a variety of ways. The approach one uses is partially based upon knowledge, partially based upon time, and partially based upon preference. Objectively, one method is not better than the other, although it might be quicker overall. 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. Sursa: https://maxkersten.nl/binary-analysis-course/assembly-basics/practical-case-crack-me-0x03/ Quote