Leviathan is one of the easier wargames on OverTheWire that doesn't require too much programming knowledge, but does provide a gentle introduction to the general format of capture the flag games.
Each level requires you to leverage some sort of 'exploit' in order to obtain a password that allows you to ssh into the next level.
Also it is expected that you have some prior knowledge of C programming and x86 assembly conventions (especially how functions are called, what different instructions do, the purpose of specific registers, etc). If not I would highly recommend going through Learn C The Hard Way and looking up a crash course in x86.
While there are many ways to solve these exercises, I will be focusing on using gdb
to act as a resource for people out there trying to get familiar with it. The explanations will get more terse once basic things are gone over, ask questions in the comments if you are wondering about a logical jump that doesn't make sense!
Table of Contents
Level 0:
This is just an introduction that doesn't take too much time. A listing of hidden directories shows a hidden folder in which inside is an html file containing a bunch of random links. A simple grep
should more than suffice, and sure enough, we get the password:
leviathan0@melinda:~$ ls -al
total 24
drwxr-xr-x 3 root root 4096 Nov 14 2014 .
drwxr-xr-x 167 root root 4096 May 3 12:32 ..
drwxr-x--- 2 leviathan1 leviathan0 4096 Feb 10 18:08 .backup
-rw-r--r-- 1 root root 220 Apr 9 2014 .bash_logout
-rw-r--r-- 1 root root 3637 Apr 9 2014 .bashrc
-rw-r--r-- 1 root root 675 Apr 9 2014 .profile
leviathan0@melinda:~$ cd .backup/
leviathan0@melinda:~/.backup$ ls
bookmarks.html
leviathan0@melinda:~/.backup$ grep "pass" bookmarks.html
*** Password Removed ***
Level 1
We get a binary called check
which running seems to ask for a password prompt.
In CTFs, most of the time binaries aren't stripped of debugging information (which makes using gdb
a lot harder), but let us just make sure:
leviathan1@melinda:~$ file check
check: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0d17ae20f672ebc7d440bb4562277561cc60f2d0, not stripped
Awesome, looks like it's intact. Let's fire up gdb
to see if we can get any information:
leviathan1@melinda:~$ gdb -q check
Reading symbols from check...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x0804852d <+0>: push ebp
0x0804852e <+1>: mov ebp,esp
0x08048530 <+3>: and esp,0xfffffff0
0x08048533 <+6>: sub esp,0x30
0x08048536 <+9>: mov eax,gs:0x14
0x0804853c <+15>: mov DWORD PTR [esp+0x2c],eax
0x08048540 <+19>: xor eax,eax
........ .....
0x080485a3 <+118>: mov DWORD PTR [esp+0x4],eax
0x080485a7 <+122>: lea eax,[esp+0x14]
0x080485ab <+126>: mov DWORD PTR [esp],eax
0x080485ae <+129>: call 0x80483b0 <strcmp@plt>
0x080485b3 <+134>: test eax,eax
0x080485b5 <+136>: jne 0x80485c5 <main+152>
........ .....
Let's explain a little. The -q
flag provided to gdb
simply starts it in 'quiet' mode and removes a lot of the clutter on startup (not necessary). set disassembly-flavor intel
formats the x86 instructions in a way that I prefer.
Doing disas main
is usually a good place to start examining instructions, and scanning down we are looking for something that resembles a string comparison. Lo-and-behold, we find it:
0x080485ae <+129>: call 0x80483b0 <strcmp@plt>
We know that strcmp
is a function that takes two arguments as strings. Lets see what is being passed to it:
0x0804859f <+114>: lea eax,[esp+0x18]
0x080485a3 <+118>: mov DWORD PTR [esp+0x4],eax
0x080485a7 <+122>: lea eax,[esp+0x14]
0x080485ab <+126>: mov DWORD PTR [esp],eax
0x080485ae <+129>: call 0x80483b0 <strcmp@plt>
....
(gdb) b *main+129
Breakpoint 1 at 0x80485ae
(gdb) r
Starting program: /home/leviathan1/check
password: hey
Breakpoint 1, 0x080485ae in main ()
(gdb) x/s $esp+0x18
0xffffd698: "sex"
(gdb) x/s $esp+0x14
0xffffd694: "hey"
(gdb)
After setting a breakpoint at strcmp
(using it's offset from main
), we give the program a password and resume execution. Looking at the assembly, we see two addresses being loaded into eax
before passed to the comparison call, and examining these addresses reveals two strings, our passed in, "hey", and another string, "sex", which must be the target of the comparison.
On an aside, the 'examine' function of gdb
is super useful and you should read up on it here.
Anyways, we try again with our new password, and we get a shell! (Make sure to quit gdb
)
leviathan1@melinda:~$ ./check
password: sex
$ whoami
leviathan2
$ cat /etc/leviathan_pass/leviathan2
*** Password Removed ***
Of course we could just use the utility ltrace
which shows library calls, giving us the answer, but it's good to get comfortable with messing around in memory since not all comparison checks will involve single library calls.
Level 2
Lets try running this:
leviathan2@melinda:~$ ./printfile
*** File Printer ***
Usage: ./printfile filename
leviathan2@melinda:~$ echo "testing" > /tmp/lv2/e
leviathan2@melinda:~$ ./printfile /tmp/lv2/e
testing
leviathan2@melinda:~$ ln -s /etc/leviathan_pass/leviathan3 /tmp/lv2/pass
leviathan2@melinda:~$ ./printfile /tmp/lv2/pass
You cant have that file...
So it seems to print whatever the target is, but wont let us access files we don't have permission to. Time to turn to GDB:
...... ....
0x08048587 <+90>: mov DWORD PTR [esp+0x4],0x4
0x0804858f <+98>: mov DWORD PTR [esp],eax
0x08048592 <+101>: call 0x8048420 <access@plt> <===
0x08048597 <+106>: test eax,eax
0x08048599 <+108>: je 0x80485ae <main+129>
...... ....
0x080485d7 <+170>: lea eax,[esp+0x2c]
0x080485db <+174>: mov DWORD PTR [esp],eax
0x080485de <+177>: call 0x80483e0 <system@plt> <===
0x080485e3 <+182>: mov eax,0x0
0x080485e8 <+187>: mov edx,DWORD PTR [esp+0x22c]
0x080485ef <+194>: xor edx,DWORD PTR gs:0x14
There are a couple of interesting things going on. The first is the call to access
, which must be what is gauging which files we can print. The second is system
, and a raw system call is always a point for vulnerabilities. Let's plop down a breakpoint and see what is getting passed in.
(gdb) b *main+177
Breakpoint 1 at 0x80485de
(gdb) r /tmp/lv2/e
Starting program: /home/leviathan2/printfile /tmp/lv2/e
Breakpoint 1, 0x080485de in main ()
(gdb) x/s $eax
0xffffd49c: "/bin/cat /tmp/lv2/e"
(gdb)
System is execing cat
on our argument we passed in. But looking a bit above we see snprintf
writing to a buffer at an address.
...... ....
0x080485bb <+142>: mov DWORD PTR [esp+0x8],0x80486d4 <==
0x080485c3 <+150>: mov DWORD PTR [esp+0x4],0x1ff
0x080485cb <+158>: lea eax,[esp+0x2c]
0x080485cf <+162>: mov DWORD PTR [esp],eax
0x080485d2 <+165>: call 0x8048410 <snprintf@plt>
...... ....
(gdb) x/s 0x80486d4
0x80486d4: "/bin/cat %s"
There is a simple string substitution from our argument that forms the string that is passed inside system
. Let us try to pass in a malformed argument and see what happens, at both access
and system
leviathan2@melinda:~$ cd /tmp
leviathan2@melinda:/tmp$ mkdir tmpl2
leviathan2@melinda:/tmp$ cd tmpl2
leviathan2@melinda:/tmp/tmpl2$ touch "filename space"
leviathan2@melinda:/tmp/tmpl2$ gdb -q ~/printfile
....... ....
(gdb) b *main+101
Breakpoint 1 at 0x8048592 <= access
(gdb) b *main+177
Breakpoint 2 at 0x80485de <= system
(gdb) r "filename space"
Starting program: /home/leviathan2/printfile "filename space"
Breakpoint 1, 0x08048592 in main ()
(gdb) x/s $eax
0xffffd893: "filename space"
(gdb) c
Continuing.
Breakpoint 2, 0x080485de in main ()
(gdb) x/s $eax
0xffffd49c: "/bin/cat filename space"
It looks like we have an inconsistency. Although access
is being called on our target file, because it is passed in through string format, the way cat
works is by displaying the output of each argument passed in in sequence, delineated by spaces. Therefore while we check permissions for a single file, the system call treats it as attempting to cat
two different files. What happens if we do a symbolic link to take advantage of this?
leviathan2@melinda:/tmp/tmpl2$ ln -s /etc/leviathan_pass/leviathan3 .
leviathan2@melinda:/tmp/tmpl2$ echo "w00t" > "gimme leviathan3"
leviathan2@melinda:/tmp/tmpl2$ ls -al
total 1104
drwxrwxr-x 2 leviathan2 leviathan2 4096 Jun 12 15:47 .
drwxrwx-wt 5833 root root 1118208 Jun 12 15:47 ..
-rw-rw-r-- 1 leviathan2 leviathan2 5 Jun 12 15:47 gimme leviathan3
lrwxrwxrwx 1 leviathan2 leviathan2 30 Jun 12 15:47 leviathan3 -> /etc/leviathan_pass/leviathan3
leviathan2@melinda:/tmp/tmpl2$ ~/printfile "gimme leviathan3"
/bin/cat: gimme: No such file or directory
*** Password Removed ****
Sweet!
Level 3
This is another easy one to solve with ltrace
, but lets brush up our gdb
skills. It seems this is another password prompt binary. Loading it up gives us a strcmp
function that we should probably examine:
...... ....
0x0804867d <+127>: lea eax,[esp+0x31] <=== second arg
0x08048681 <+131>: mov DWORD PTR [esp+0x4],eax
0x08048685 <+135>: lea eax,[esp+0x2a] <=== first arg
0x08048689 <+139>: mov DWORD PTR [esp],eax
0x0804868c <+142>: call 0x80483d0 <strcmp@plt>
0x08048691 <+147>: test eax,eax
0x08048693 <+149>: jne 0x804869d <main+159>
..........
(gdb) b *main+142
Breakpoint 1 at 0x804868c: file level3.c, line 33.
(gdb) r
Starting program: /home/leviathan3/level3
Breakpoint 1, 0x0804868c in main () at level3.c:33
(gdb) x/s $esp+0x31
0xffffd691: "kakaka"
(gdb) x/s $esp+0x2a
0xffffd68a: "h0no33"
(gdb) c
Continuing.
Enter the password> gimme
bzzzzzzzzap. WRONG
[Inferior 1 (process 21628) exited normally]
Huh something weird is happening. The breakpoint is hit before we actually give it a password, and two random strings are compared. What is going on?
Luckily because we have function symbols intact looking further in main
we see a call to another, non-library function called do_stuff
. Just like main
we can disassemble the instructions in that function:
...... ....
0x080486a4 <+166>: call 0x80483e0 <printf@plt>
0x080486a9 <+171>: call 0x804854d <do_stuff>
0x080486ae <+176>: mov eax,0x0
0x080486b3 <+181>: mov edx,DWORD PTR [esp+0x4c]
...... ....
(gdb) disas do_stuff
Dump of assembler code for function do_stuff:
0x0804854d <+0>: push ebp
0x0804854e <+1>: mov ebp,esp
0x08048550 <+3>: sub esp,0x128
0x08048556 <+9>: mov eax,gs:0x14
0x0804855c <+15>: mov DWORD PTR [ebp-0xc],eax
0x0804855f <+18>: xor eax,eax
0x08048561 <+20>: mov DWORD PTR [ebp-0x117],0x706c6e73
0x0804856b <+30>: mov DWORD PTR [ebp-0x113],0x746e6972
0x08048575 <+40>: mov WORD PTR [ebp-0x10f],0xa66
0x0804857e <+49>: mov BYTE PTR [ebp-0x10d],0x0
0x08048585 <+56>: mov eax,ds:0x804a03c
0x0804858a <+61>: mov DWORD PTR [esp+0x8],eax
0x0804858e <+65>: mov DWORD PTR [esp+0x4],0x100
0x08048596 <+73>: lea eax,[ebp-0x10c]
0x0804859c <+79>: mov DWORD PTR [esp],eax
0x0804859f <+82>: call 0x80483f0 <fgets@plt>
0x080485a4 <+87>: lea eax,[ebp-0x117] <== Arg1
0x080485aa <+93>: mov DWORD PTR [esp+0x4],eax
0x080485ae <+97>: lea eax,[ebp-0x10c] <== Arg2
0x080485b4 <+103>: mov DWORD PTR [esp],eax
0x080485b7 <+106>: call 0x80483d0 <strcmp@plt> <== Here
0x080485bc <+111>: test eax,eax
0x080485be <+113>: jne 0x80485da <do_stuff+141>
0x080485c0 <+115>: mov DWORD PTR [esp],0x8048760
0x080485c7 <+122>: call 0x8048410 <puts@plt>
0x080485cc <+127>: mov DWORD PTR [esp],0x8048774
0x080485d3 <+134>: call 0x8048420 <system@plt>
0x080485d8 <+139>: jmp 0x80485e6 <do_stuff+153>
0x080485da <+141>: mov DWORD PTR [esp],0x804877c
0x080485e1 <+148>: call 0x8048410 <puts@plt>
0x080485e6 <+153>: mov eax,0x0
0x080485eb <+158>: mov edx,DWORD PTR [ebp-0xc]
0x080485ee <+161>: xor edx,DWORD PTR gs:0x14
0x080485f5 <+168>: je 0x80485fc <do_stuff+175>
0x080485f7 <+170>: call 0x8048400 <__stack_chk_fail@plt>
0x080485fc <+175>: leave
0x080485fd <+176>: ret
End of assembler dump.
(gdb) b *do_stuff+106
Breakpoint 2 at 0x80485b7: file level3.c, line 12.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/leviathan3/level3
Breakpoint 1, 0x0804868c in main () at level3.c:33
33 in level3.c
(gdb) c
Continuing.
Enter the password> gimme
Breakpoint 2, 0x080485b7 in do_stuff () at level3.c:12
12 in level3.c
(gdb) x/s $ebp-0x117
0xffffd541: "snlprintf\n"
(gdb) x/s $ebp-0x10c
0xffffd54c: "gimme\n"
Cool stuff, looks like the first strcmp
was an obfuscation, looking at the second one gives us the right answer.
leviathan3@melinda:~$ ./level3
Enter the password> snlprintf
[You've got shell]!
$
Level 4
This is a simple level that involves converting binary to ASCII, and there isn't much to do here. On to the next.
Level 5
Another level that doesn't really require anything (it is actually easier than Level 2). Looks for a file /tmp/file.log
that it prints, and simply symlinking here will do the trick
Level 6
We have a binary in this folder that seems to want a four digit code:
leviathan6@melinda:~$ ./leviathan6
usage: ./leviathan6 <4 digit code>
leviathan6@melinda:~$ ./leviathan6 1234
Wrong
One option is to write a simple script that basically bruteforces the combination. But we can easily slip into gdb
and probably save ourself some time.
(gdb) disas main
Dump of assembler code for function main:
0x0804850d <+0>: push ebp
0x0804850e <+1>: mov ebp,esp
0x08048510 <+3>: and esp,0xfffffff0
0x08048513 <+6>: sub esp,0x20
0x08048516 <+9>: mov DWORD PTR [esp+0x1c],0x1bd3 <= Stack var
0x0804851e <+17>: cmp DWORD PTR [ebp+0x8],0x2 <= Argc == 2
0x08048522 <+21>: je 0x8048545 <main+56> <= Cont. if have args
0x08048524 <+23>: mov eax,DWORD PTR [ebp+0xc]
0x08048527 <+26>: mov eax,DWORD PTR [eax]
0x08048529 <+28>: mov DWORD PTR [esp+0x4],eax
0x0804852d <+32>: mov DWORD PTR [esp],0x8048620
0x08048534 <+39>: call 0x8048390 <printf@plt>
0x08048539 <+44>: mov DWORD PTR [esp],0xffffffff
0x08048540 <+51>: call 0x80483e0 <exit@plt> <= Exit if no args
0x08048545 <+56>: mov eax,DWORD PTR [ebp+0xc]
0x08048548 <+59>: add eax,0x4
0x0804854b <+62>: mov eax,DWORD PTR [eax]
0x0804854d <+64>: mov DWORD PTR [esp],eax
0x08048550 <+67>: call 0x8048400 <atoi@plt>
0x08048555 <+72>: cmp eax,DWORD PTR [esp+0x1c] <= Compare check
0x08048559 <+76>: jne 0x8048575 <main+104>
0x0804855b <+78>: mov DWORD PTR [esp],0x3ef
0x08048562 <+85>: call 0x80483a0 <seteuid@plt>
0x08048567 <+90>: mov DWORD PTR [esp],0x804863a
0x0804856e <+97>: call 0x80483c0 <system@plt>
0x08048573 <+102>: jmp 0x8048581 <main+116>
0x08048575 <+104>: mov DWORD PTR [esp],0x8048642
0x0804857c <+111>: call 0x80483b0 <puts@plt>
0x08048581 <+116>: leave
0x08048582 <+117>: ret
End of assembler dump.
(gdb) b *main+72
Breakpoint 1 at 0x8048555
(gdb) r 9999
Starting program: /home/leviathan6/leviathan6 9999
(gdb) x/wu $esp+0x1c
0xffffd69c: 7123
(gdb) i r
eax 0x270f 9999
ecx 0x0 0
edx 0xffffd89a -10086
ebx 0xf7fca000 -134438912
esp 0xffffd680 0xffffd680
ebp 0xffffd6a8 0xffffd6a8
esi 0x0 0
edi 0x0 0
eip 0x8048555 0x8048555 <main+72>
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
Looks like there is a compare check against the result of atoi
which is stored in eax. Checking our registers with i r
we can see our 9999 value, the 7123 value must be the target. (We change the arguments to the examine command x/
giving it u which means unsigned int, and w which means read a single word)
Furthermore at the beginning of the program we actually see 0x1bd3 being loaded into a local stack variable, which incidentally is the integer value 7123
leviathan6@melinda:~$ ./leviathan6 7123
$ whoami
leviathan7
$
Level 7
Congrats! Level 7 is the last level at this time, and while using gdb
is probably overkill especially for this series of challenges, hopefully you found this to be good practice in getting you ready for more difficult challenges.
The next writeup will move onto the next wargame in the series, Narnia.