The Capture The Flag challenge offered in the book consists of finding a hidden flag (a string) in a binary, without access to its source code, by using reverse engineering techniques.
Once discovered, the flag unlocks the next levels and so on and so forth.
Only basics tools like a hexeditor, gdb, objdump, nm, readelf, strings will be used, and not more complex tools like IDA, Ghidra or Binary Ninja to be sure to understand the basics first.
Everything has been completed on a Kali Linux VM or on the Linux VM provided by the book author.
This post is about my solutions for the levels 5, 6 and 7.
Level 5
We have a classic, stripped binary offering a nice, warm welcome message :p
1
2
3
4
5
$ file ./lvl5
./lvl5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c4f1d4d245a8e252b77c38c9c1ba936f70d8245, stripped
$ ./lvl5
nothing to see here
This binary does not apparently interact with its environment, as shown by strace and ltrace:
1
2
3
4
5
$ ltrace -i ./lvl5
[0x400549] __libc_start_main(0x400500, 1, 0x7fff5f45d768, 0x4006f0 <unfinished ...>
[0x40050e] puts("nothing to see here"nothing to see here
)=20[0xffffffffffffffff] +++ exited (status 1) +++
After all the glibc internal initialization has been completed, control will be handed off to the main function of the binary, which is here the first parameter as a function pointer: int (\*main) (int, char\*\*, char\*\*)
Some instructions deal with interesting 64bits values in (2) (3) (4) (5) which are very likely involved in the flag computation:
(2) 0x6223331533216010
(3) 0x6675364134766545
(4) 0x6570331064756717
(5) 0x6671671162763518
The string key = 0x%08x is a format specifier, and __printf_chk@ a variation of the printf() function
I wasn’t able to find any instruction calling out this function.
What happen if we try to patch the program Entry point address from 0x400520 to 0x400620 to get this code executed? This can be done quickly with bvi -s 0x18 ./lvl5:
We got this code to run, the outcome is a segfault assorted with some hints: a corrupted decrypted flag and a key, which value 0x400520 is just the program entry point address we just patched, what a coincidence!
What is the purpose of that key? Let’s try to understand what is happening here, comments on the right:
400620: 53 push rbx
400621: be 74 07 40 00 mov esi,0x400774 ; esi=0x400774 "key = 0x%08x"
400626: bf 01 00 00 00 mov edi,0x1 ; edi=0x1
40062b: 48 83 ec 30 sub rsp,0x30 ; allocate 48 bytes on the stack
g 40062f: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 ; rax=0xc902d681f5be2700 stack-guard value
400636: 00 00
400638: 48 89 44 24 28 mov QWORD PTR [rsp+0x28],rax ; [rsp+0x28] = 0xc902d681f5be2700
40063d: 31 c0 xor eax,eax ; eax=0
40063f: 48 b8 10 60 21 33 15 movabs rax,0x6223331533216010 ; rax=0x6223331533216010
400646: 33 23 62
400649: c6 44 24 20 00 mov BYTE PTR [rsp+0x20],0x0 ; [rsp+20] = 0x0
40064e: 48 89 04 24 mov QWORD PTR [rsp],rax ; [rsp] = 0x6223331533216010
400652: 48 b8 45 65 76 34 41 movabs rax,0x6675364134766545 ; rax = 0x6675364134766545
400659: 36 75 66
40065c: 48 89 44 24 08 mov QWORD PTR [rsp+0x8],rax ; [rsp+0x8] = 0x6675364134766545
400661: 48 b8 17 67 75 64 10 movabs rax,0x6570331064756717 ; rax = 0x6570331064756717
400668: 33 70 65
40066b: 48 89 44 24 10 mov QWORD PTR [rsp+0x10],rax ; [rsp+0x10] = 0x6570331064756717
400670: 48 b8 18 35 76 62 11 movabs rax,0x6671671162763518 ; rax = 0x6671671162763518
400677: 67 71 66
40067a: 48 89 44 24 18 mov QWORD PTR [rsp+0x18],rax ; [rsp+0x18] = 0x6671671162763518
g 40067f: 8b 1c 25 40 05 40 00 mov ebx,DWORD PTR ds:0x400540 ; ebx = 0x400500 - this is the key for the xor
400686: 31 c0 xor eax,eax ; eax = 0
400688: 89 da mov edx,ebx ; edx = 0x400500
40068a: e8 51 fe ff ff call 4004e0 <__printf_chk@plt>
40068f: 48 8d 54 24 20 lea rdx,[rsp+0x20] ; rdx = 0x7fffffffe3c8 - end of the 4 64bits values in the stack
400694: 48 89 e0 mov rax,rsp ; rax = 0x7fffffffe3a8 - start of the values
400697: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0] ; padding
40069e: 00 00 ; the 4 values are on the stack, between rax and rdx
4006a0: 31 18 xor DWORD PTR [rax],ebx ; xor 4 bytes of the values with the key 0x400500
4006a2: 48 83 c0 04 add rax,0x4 ; rax used as an index - increase it by 4 bytes
4006a6: 48 39 d0 cmp rax,rdx ; have we reached to end of the 4 values range?
4006a9: 75 f5 jne 4006a0 <__printf_chk@plt+0x1c0> ; no: keep xor'ing next 4 bytes - yes: display the key
4006ab: 31 c0 xor eax,eax ; eax = 0
4006ad: 48 89 e2 mov rdx,rsp ; rdx = 0x7fffffffe3a8
4006b0: be 82 07 40 00 mov esi,0x400782 ; esi = "decrypted flag = %s"
4006b5: bf 01 00 00 00 mov edi,0x1 ; edi=0x1
4006ba: e8 21 fe ff ff call 4004e0 <__printf_chk@plt> ; actually display the flag
4006bf: 31 c0 xor eax,eax
4006c1: 48 8b 4c 24 28 mov rcx,QWORD PTR [rsp+0x28]
4006c6: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28
4006cd: 00 00
4006cf: 75 06 jne 4006d7 <__printf_chk@plt+0x1f7>
4006d1: 48 83 c4 30 add rsp,0x30
4006d5: 5b pop rbx
4006d6: c3 ret
Lines prefixed with g mean I had to actually run the code to figure out the resulting values. This is the stack before the initial xor loop, we can see the 4 64 bits values:
$ ./lvl5
key = 0x00400620
decrypted flag = 0fa355cbec64a05f7a5d050e836b1a1f
$ ./oracle 0fa355cbec64a05f7a5d050e836b1a1f
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 5 completed, unlocked lvl6 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
It works.
This level took me way more effort than the previous ones, just to make the connection between the xor key and the ‘hidden’ function address/program entry point.
$ ./lvl6
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
$ echo $?
0
$ objdump -s -j .rodata ./lvl6
./lvl6: file format elf64-x86-64
Contents of section .rodata:
400910 01000200 44454255 473a2061 7267765b ....DEBUG: argv[
400920 315d203d 2025730a 00676574 5f646174 1] = %s..get_dat
400930 615f6164 64720030 78256a78 00444154 a_addr.0x%jx.DAT
400940 415f4144 44520025 642000 A_ADDR.%d .
$ readelf -sh ./lvl6
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400790
Start of program headers: 64 (bytes into file)
Start of section headers: 4496 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putchar@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND setenv@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __printf_chk@GLIBC_2.3.4 (4)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __sprintf_chk@GLIBC_2.3.4 (4)
8: 00000000004005b0 0 FUNC GLOBAL DEFAULT UND strcmp@GLIBC_2.2.5 (2)
We have here some starting points with the setenv() and strcmp() functions, the .rodata section eventually indicate the use of command line arguments, due to the argv string.
strace and ltrace don’t tell us more right now, no file involved, no specific functions calls:
$ strace -i ./lvl6
[00007f9741915777] execve("./lvl6", ["./lvl6"], [/* 35 vars */])=0[00007fcc709674b9] brk(NULL)= 0x1fa6000
[00007fcc709683c7] access("/etc/ld.so.nohwcap", F_OK)= -1 ENOENT (No such file or directory)[00007fcc709683c7] access("/etc/ld.so.preload", R_OK)= -1 ENOENT (No such file or directory)[00007fcc70968367] open("/home/binary/code/chapter5/tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC)= -1 ENOENT (No such file or directory)[00007fcc709682b5] stat("/home/binary/code/chapter5/tls/x86_64", 0x7ffcad19b090)= -1 ENOENT (No such file or directory)[00007fcc70968367] open("/home/binary/code/chapter5/tls/libc.so.6", O_RDONLY|O_CLOEXEC)= -1 ENOENT (No such file or directory)[00007fcc709682b5] stat("/home/binary/code/chapter5/tls", 0x7ffcad19b090)= -1 ENOENT (No such file or directory)[00007fcc70968367] open("/home/binary/code/chapter5/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC)= -1 ENOENT (No such file or directory)[00007fcc709682b5] stat("/home/binary/code/chapter5/x86_64", 0x7ffcad19b090)= -1 ENOENT (No such file or directory)[00007fcc70968367] open("/home/binary/code/chapter5/libc.so.6", O_RDONLY|O_CLOEXEC)= -1 ENOENT (No such file or directory)[00007fcc709682b5] stat("/home/binary/code/chapter5", {st_mode=S_IFDIR|0775, st_size=4096, ...})=0[00007fcc70968367] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC)=3[00007fcc709682f4] fstat(3, {st_mode=S_IFREG|0644, st_size=98537, ...})=0[00007fcc709684ba] mmap(NULL, 98537, PROT_READ, MAP_PRIVATE, 3, 0)= 0x7fcc70b59000
[00007fcc70968467] close(3)=0[00007fcc709683c7] access("/etc/ld.so.nohwcap", F_OK)= -1 ENOENT (No such file or directory)[00007fcc70968367] open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC)=3[00007fcc70968387] read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832)=832[00007fcc709682f4] fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...})=0[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x7fcc70b58000
[00007fcc709684ba] mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0)= 0x7fcc70583000
[00007fcc70968557] mprotect(0x7fcc70743000, 2097152, PROT_NONE)=0[00007fcc709684ba] mmap(0x7fcc70943000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000)= 0x7fcc70943000
[00007fcc709684ba] mmap(0x7fcc70949000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0)= 0x7fcc70949000
[00007fcc70968467] close(3)=0[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x7fcc70b57000
[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x7fcc70b56000
[00007fcc7094dbd5] arch_prctl(ARCH_SET_FS, 0x7fcc70b57700)=0[00007fcc70968557] mprotect(0x7fcc70943000, 16384, PROT_READ)=0[00007fcc70968557] mprotect(0x600000, 4096, PROT_READ)=0[00007fcc70968557] mprotect(0x7fcc70b72000, 4096, PROT_READ)=0[00007fcc70968537] munmap(0x7fcc70b59000, 98537)=0[00007fcc70679c34] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...})=0[00007fcc7067fe19] brk(NULL)= 0x1fa6000
[00007fcc7067fe19] brk(0x1fc7000)= 0x1fc7000
[00007fcc7067a2c0] write(1, "DEBUG: argv[1] = (null)\n", 24DEBUG: argv[1]=(null))=24[00007fcc7067a2c0] write(1, "2 3 5 7 11 13 17 19 23 29 31 37 "..., 722357111317192329313741434753596167717379838997)=72[00007fcc7064f748] exit_group(0)= ?
[????????????????] +++ exited with 0 +++
As a next step I’ll do a static analysis of the code, to learn more about what is happening in main()
There must be a better way, but as a beginner I did this analysis by adding comments to the asm listing issued by the dump of objdump -Mintel -d --insn-width=10 lvl6
Starting with the instruction at the entry point address, 0x400790.
The entry point leads us to the glibc initial setup function, which tells us the main() function is located at 0x4005f0 (first argument to libc_start_main() stored in rdi)
00000000004005f0 <.text>:
4005f0: 55 push rbp
4005f1: 53 push rbx
4005f2: 89 fd mov ebp,edi ; argc stored in edi
4005f4: 48 89 f3 mov rbx,rsi ; argv pointer stored in rsi
4005f7: 48 81 ec a8 05 00 00 sub rsp,0x5a8 ; reserve 1448 bytes on the stack
4005fe: 8b 15 60 0a 20 00 mov edx,DWORD PTR [rip+0x200a60] ; 0x60105e: within .data section
400604: 64 48 8b 04 25 28 00 00 00 mov rax,QWORD PTR fs:0x28 ; retrieve stack smash protection canary value
40060d: 48 89 84 24 98 05 00 00 mov QWORD PTR [rsp+0x598],rax ; write it as the first 16 bytes of the stack
400615: 31 c0 xor eax,eax
400617: 85 d2 test edx,edx ; equivalent to cmp edx, 0
400619: 0f 85 13 01 00 00 jne 400732 ; not taken
40061f: 83 fd 01 cmp ebp,0x1 ; is argc == 1? (cmdline parameter count)
400622: 48 c7 05 3b 0a 20 00 b0 05 40 mov QWORD PTR [rip+0x200a3b],0x4005b0 ; 0x60105d: within .data section
40062c: 00
40062d: 7e 16 jle 400645 <__sprintf_chk@plt+0x75> ; jump taken
40062f: 48 8b 7b 08 mov rdi,QWORD PTR [rbx+0x8] ; move to rdi the first cmdline parameter
400633: be 29 09 40 00 mov esi,0x400929 ; "get_data_addr"
400638: e8 73 ff ff ff call 4005b0 <strcmp@plt> ; compare the 1st parameter to "get_data_addr"
40063d: 85 c0 test eax,eax
40063f: 0f 84 05 01 00 00 je 40074a <__sprintf_chk@plt+0x17a>
How do I know about the .data section addresses range? These can be retrieved from $ readelf -S ./lvl6 output:
1
2
3
4
5
6
7
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
(...)[25] .data PROGBITS 00000000006010500000105000000000000000100000000000000000 WA 008(...)
.data section
starts at 0x601050(Address 0x601050)
ends at 0x601060(Address 0x601050 + size 0x10)
The System V ABI specification - page 32 shows the full main() function prototype, argc (argument count) and argv (argument vector) being the command line arguments passed to main():
1
externintmain(intargc,char*argv[],char*envp[]);
Additionaly, the System V AMD64 ABI tells us the 2 first arguments, here argc and argv, are passed through the rdi and rsi registers.
Side note about stack smashing protection: line 8: what is this stack smash protection canary? This has greatly confused me at the beginning, especially because the canary value was changing on every execution. You don’t know what you don’t know :^)
More details on this security mechanism directly handled by the compiler.
So this chunk of code checks if we are passing a single command line argument called get_data_addr, let’s try and ltrace it:
The output did not change, however ltrace shows us another clue setenv(\"DATA_ADDR\", \"0x4006c1\", 1)
If we look at the program instruction at this address, the instructions starting on line2 seem very weird:
1
2
3
4
5
4006c1: 2e 29 c6 cs sub esi,eax
4006c4: 4a 0f 03 a6 ee 2a 30 7f rex.WX lsl rsp,WORD PTR [rsi+0x7f302aee]
4006cc: ec in al,dx
4006cd: c8 c3 ff 42 enter 0xffc3,0x42
If you count the number of bytes of these instructions, this is 16 bytes, which happen to be the length of our flag! I just have to concatenate them and try to submit to the oracle:
1
2
3
4
5
$ ./oracle 2e29c64a0f03a6ee2a307fecc8c3ff42
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 6 completed, unlocked lvl7 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
Level cleared!
That was a nice level which a red herring - the suite of numbers. I have to admit I’ve wasted time trying to understand all the logic behind the numbers generation, kudos to the author!
Level 7
Not a binary file, but several within lvl7, which is a compressed tarball:
stage1 - binary executable
stage2.zip - password protected zip archive
TIL: file can sneak into compressed files with -z:
1
2
3
4
5
6
7
8
9
$ file lvl7
lvl7: gzip compressed data, last modified: Sat Dec 1 17:30:15 2018, from Unix
$ file -z lvl7
lvl7: POSIX tar archive (GNU) (gzip compressed data, last modified: Sat Dec 1 17:30:15 2018, from Unix)
$ tar tvzf lvl7
-rwxr-xr-x dnx/dnx 6304 2018-12-01 18:25 stage1
-rw-r--r-- dnx/dnx 7738 2018-08-09 17:20 stage2.zip
stage1 does not call any external libraries function:
1
2
3
4
5
6
7
8
$ readelf -s ./stage1
Symbol table '.dynsym' contains 3 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
There is a somehow crippled string in the .rodata section:
While starting to follow the code from the program entry point 0x400420 to the main function at 0x0x4003e0, I did notice again some weird instructions, especially the ones starting at 0x4003f7
0x4003f7 being obviously in the .text section we are currently within, there might be something hidden? I first noticed that with a .text section dump with objdump:
See the string dump ecx" ! That could have been noticed within gdb as well at runtime:
1
2
3
gdb> x/s 0x4003f7
0x4003f7: "dump ecx"
What do we do with that clue?
I’ll start tracing the main function starting at 0x4003e0 with gdb and see how ecx value changes over time, since in this code ecx is only written to from eax, never copied somewhere else so there is no ‘history’ of the ecx previous values:
1
2
400400: 89 c1 mov ecx,eax
1
2
3
4
5
gdb ./stage1
>>> b *0x4003e0
Breakpoint 1 at 0x4003e0
>>> r
Start the program and wait for the breakpoint on main() address to be reached. We only want to monitor ecx changes from that part of the code on, not from the very beginning with all the initial glibc housekeeping.
Set a memory watch on the ecx register (watchpoint), which mean the program will stop and bring us back to the gdb prompt when ecx value change, with the older and new value. We can now continue the execution, hopefully this is what dump ecx meant!
>>> c
>>> c
Continuing.
Watchpoint 1: $ecx
Old value = 0
New value = 83
0x0000000000400402 in ?? ()
>>>
Continuing.
Watchpoint 1: $ecx
Old value = 83
New value = 84
0x0000000000400402 in ?? ()
>>>
Continuing.
Watchpoint 1: $ecx
Old value = 84
New value = 65
0x0000000000400402 in ?? ()
>>>
Continuing.
Watchpoint 1: $ecx
Old value = 65
New value = 71
0x0000000000400402 in ?? ()
I’ve trimmed the output, but you get the grist. So far the sequential changes of ecx have been: 83 -> 84 -> 65 -> 71...
Do it until the program exits abruptly:
1
2
3
4
5
Old value = 89
New value = -136500144
0x00007ffff7a46f43 in __run_exit_handlers (status=0, listp=0x7ffff7dd15f8 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true) at exit.c:50
50 exit.c: No such file or directory.
Finally we got these values: 838465716950756989, using their ascii representation we got:
1
2
3
83 84 65 71 69 50 75 69 89
S T A G E 2 K E Y
And here we see we should have tried what was already visible on the first overview of the .rodata section! Remember:
std::srand(55) indicates that the random number generator is initialized every time with the same seed, here 55, therefore random number sequence will always be the same, as confirmed by man rand(3)
The srand() function sets its argument as the seed for a new sequence of pseudo-random integers to be returned by rand(). These sequences
are repeatable by calling srand() with the same seed value.
Does the changing variable name be a part of the flag we are looking for?
I’ll try to repeat the following steps until I have 16 bytes worth of data (32 hex characters, expected flag length), starting with the provided stage2 binary, using something along the lines:
1
2
./stage$N| g++ -x c++ - -o stage$N+1
As this is tedious to run manually, this has to be automated (the egrep thingy is to extract the changing values we are trying to track):
1
2
3
4
5
6
7
8
#!/bin/bashfor i in {2..17}do((j=i+1)) ./stage$i| g++ -x c++ - -o stage$j ./stage$i|egrep '^int i, _'|cut -f2 -d','|tr -d '_; 'done
Remove the newlines characters, because we are lazy and it saves a lot of copy/paste operations:
$ ./auto.sh
0F
25
E5
12
A7
76
3E
EF
B7
69
6B
3A
ED
A1
F9
64
$ ./auto.sh |tr -d '\n'
0F25E512A7763EEFB7696B3AEDA1F964
It does the trick :))
1
2
3
4
5
6
$ ./oracle 0F25E512A7763EEFB7696B3AEDA1F964
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 7 completed, unlocked lvl8 |+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
Next levels writeup in an upcoming post.
Feedback
Constructive criticism always welcome! (comments here, contact in About ) as I would be more than happy to learn more!