Leviathan Writeup (Intro to GDB)

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.

Show Comments