Jump to content
Nytro

Practical case: Crack Me 0x03

Recommended Posts

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 0x555ef9f54ff50x555ef9f54fe7      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 0x555ef9f5501a0x555ef9f55015      call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
 └─> 0x555ef9f5501a      leave0x555ef9f5501b      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 0x555ef9f54ff50x555ef9f54fe7      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 0x555ef9f5501a0x555ef9f55015      call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
 └─> 0x555ef9f5501a      leave0x555ef9f5501b      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/

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...