OverTheWire.org - Maze - Level 3 Writeup

We begin by running the maze3 executable in different ways:

  • with no command line parameters
  • with a command line parameter
  • with strace and a command line parameter

maze3@maze:~$ /maze/maze3
./level4 ev0lcmds!
maze3@maze:~$

maze3@maze:~$ /maze/maze3 A
maze3@maze:~$

maze3@maze:~$ strace /maze/maze3 A
execve("/maze/maze3", ["/maze/maze3", "A"], [/* 17 vars */]) = 0
strace: [ Process PID=4859 runs in 32 bit mode. ]
mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit(1)                                 = ?

We see that the presence of a parameter is essential and when it's present, the program  executes an mprotect() call
Let's disassemble the maze3 with objdump:

maze3@maze:~$ objdump -d /maze/maze3

/maze/maze3:     file format elf32-i386

Disassembly of section .text:

08048060 <_start>:
 8048060:       58                      pop    %eax
 8048061:       48                      dec    %eax
 8048062:       75 32                   jne    8048096 <fine>
 8048064:       e8 14 00 00 00          call   804807d <_start+0x1d>
 8048069:       2e 2f                   cs das
 804806b:       6c                      insb   (%dx),%es:(%edi)
 804806c:       65 76 65                gs jbe 80480d4 <d1+0x9>
 804806f:       6c                      insb   (%dx),%es:(%edi)
 8048070:       34 20                   xor    $0x20,%al
 8048072:       65 76 30                gs jbe 80480a5 <fine+0xf>
 8048075:       6c                      insb   (%dx),%es:(%edi)
 8048076:       63 6d 64                arpl   %bp,0x64(%ebp)
 8048079:       73 21                   jae    804809c <fine+0x6>
 804807b:       0a 00                   or     (%eax),%al
 804807d:       b8 04 00 00 00          mov    $0x4,%eax
 8048082:       bb 01 00 00 00          mov    $0x1,%ebx
 8048087:       59                      pop    %ecx
 8048088:       ba 14 00 00 00          mov    $0x14,%edx
 804808d:       cd 80                   int    $0x80
 804808f:       b8 01 00 00 00          mov    $0x1,%eax
 8048094:       cd 80                   int    $0x80

08048096 <fine>:
 8048096:       58                      pop    %eax
 8048097:       b8 7d 00 00 00          mov    $0x7d,%eax
 804809c:       bb 60 80 04 08          mov    $0x8048060,%ebx
 80480a1:       81 e3 00 f0 ff ff       and    $0xfffff000,%ebx
 80480a7:       b9 97 00 00 00          mov    $0x97,%ecx
 80480ac:       ba 07 00 00 00          mov    $0x7,%edx
 80480b1:       cd 80                   int    $0x80
 80480b3:       8d 35 cb 80 04 08       lea    0x80480cb,%esi
 80480b9:       89 f7                   mov    %esi,%edi
 80480bb:       b9 2c 00 00 00          mov    $0x2c,%ecx
 80480c0:       ba 78 56 34 12          mov    $0x12345678,%edx

080480c5 <l1>:
 80480c5:       ad                      lods   %ds:(%esi),%eax
 80480c6:       31 d0                   xor    %edx,%eax
 80480c8:       ab                      stos   %eax,%es:(%edi)
 80480c9:       e2 fa                   loop   80480c5 <l1>

080480cb <d1>:
 80480cb:       20 d7                   and    %dl,%bh
 80480cd:       0c cc                   or     $0xcc,%al
 80480cf:       b8 61 27 67 61          mov    $0x61672761,%eax
 80480d4:       67 f4                   addr16 hlt
 80480d6:       42                      inc    %edx
 80480d7:       10 79 1b                adc    %bh,0x1b(%ecx)
 80480da:       61                      popa
 80480db:       10 3e                   adc    %bh,(%esi)
 80480dd:       1b 70 11                sbb    0x11(%eax),%esi
 80480e0:       38 bd f1 28 05 bd       cmp    %bh,-0x42fad70f(%ebp)
 80480e6:       f3 49                   repz dec %ecx
 80480e8:       84 84 19 b5 d6 8c 13    test   %al,0x138cd6b5(%ecx,%ebx,1)
 80480ef:       78 56                   js     8048147 <d1+0x7c>
 80480f1:       34 23                   xor    $0x23,%al
 80480f3:       a3                      .byte 0xa3
 80480f4:       15                      .byte 0x15
 80480f5:       f9                      stc
 80480f6:       92                      xchg   %eax,%edx

There are four parts:

  • Main code (start)
  • function fine() 
  • function l1()
  • function d1()

Please note that the code of the functions are all subsequents: if no jump changes execution flow, the function l1() is executed just after fine(), and d1() is executed just after l1().

Let's look into fine() procedure

  • calls mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC):  this means that 151 bytes starting from 0x8048000 can be read, written and executed. A godd area we can use to store some arbitrary code to be executed
  • after that it stores in %edi and %esi  the address of  function d1 ()
  • it stores 0x2c (decimal 44) in ecx
  • it stores 0x12345678 (decimal 305419896) into edx
After that l1() is executed because it is just the following code.


But what does l1() do?
Well, it's a loop based on lods xor and stos instructions. 
For each loop execution:
  • stores into eax the content of memory at esi
  • xor eax with edx (containing a fixed value 0x12345678) and stores the result in eax
  • copy the value in eax into memory address edi
  • (automatically) lods and stos automatically increase esi and edi
  • (automatically) loop decrease ecx
The result is that the string at esi (i.e. d1() code location) is replaced by other code using an xor decoding function that rewrites 44 bytes.
This also means that l1() replaces code of d1() at runtime.

Let's verify this by placing a break at the last line of fine() and execute 176 steps.

Why 176 steps? Because every loop execution of l1() takes 4 steps that multiplied by the length of d1() code (44 bytes) makes 176 steps in total.

(gdb) disas 0x080480cb
Dump of assembler code for function d1:
=> 0x080480cb <+0>:     pop    %eax
   0x080480cc <+1>:     cmpl   $0x1337c0de,(%eax)
   0x080480d2 <+7>:     jne    0x80480ed <d1+34>
   0x080480d4 <+9>:     xor    %eax,%eax
   0x080480d6 <+11>:    push   %eax
   0x080480d7 <+12>:    push   $0x68732f2f
   0x080480dc <+17>:    push   $0x6e69622f
   0x080480e1 <+22>:    mov    %esp,%ebx
   0x080480e3 <+24>:    push   %eax
   0x080480e4 <+25>:    push   %ebx
   0x080480e5 <+26>:    mov    %esp,%ecx
   0x080480e7 <+28>:    xor    %edx,%edx
   0x080480e9 <+30>:    mov    $0xb,%al
   0x080480eb <+32>:    int    $0x80
   0x080480ed <+34>:    mov    $0x1,%eax
   0x080480f2 <+39>:    xor    %ebx,%ebx
   0x080480f4 <+41>:    inc    %ebx
   0x080480f5 <+42>:    int    $0x80
End of assembler dump.

And we see that d1() code is totally different and it seems a shellcode.
The lines 0x080480d7 push "/bin//sh" and the line 0x080480eb calls an execve()

Now let's dive into the new d1() code: The third line compares a static value of 0x1337c0de to the value at a memory address contained in  (%eax)

If they are different jumps to the end of d1() and execute an exit(1)
if not, it executes the interesting part of the shellcode (i.e. the execve())

Let's advance one step in execution and reach the cmpl instruction

(gdb)  stepi
0x080480cc in d1 ()
(gdb) info reg
eax            0xffffd8c0       -10048
ecx            0x0      0
edx            0x12345678       305419896
ebx            0x8048000        134512640
esp            0xffffd79c       0xffffd79c
ebp            0x0      0x0
esi            0x804817b        134513019
edi            0x804817b        134513019
eip            0x80480cc        0x80480cc <d1+1>
eflags         0x206    [ PF IF ]
cs             0x23     35
ss             0x2b     43
ds             0x2b     43
es             0x2b     43
fs             0x0      0
gs             0x0      0
(gdb) x/wx 0xffffd8c0
0xffffd8c0:     0x434c0041

We show content of memory at %eax and it contains a 0x41 (ASCII "A").

This seems to be our command line parameter

Retry with "AAAA" parameter:

(gdb)  set args AAAA
(gdb)  r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /maze/maze3 AAAA

Breakpoint 1, 0x080480c0 in fine ()
(gdb)  stepi 177
0x080480cb in d1 ()
(gdb) stepi
0x080480cc in d1 ()
(gdb) info reg
eax            0xffffd8bd       -10051
ecx            0x0      0
edx            0x12345678       305419896
ebx            0x8048000        134512640
esp            0xffffd79c       0xffffd79c
ebp            0x0      0x0
esi            0x804817b        134513019
edi            0x804817b        134513019
eip            0x80480cc        0x80480cc <d1+1>
eflags         0x206    [ PF IF ]
cs             0x23     35
ss             0x2b     43
ds             0x2b     43
es             0x2b     43
fs             0x0      0
gs             0x0      0
(gdb) x/wx 0xffffd8bd
0xffffd8bd:     0x41414141

The code compares our parameter  with 0x1337c0de

So far it seems that:
  • the main function call mprotect to make part of the code section writable and executable
  • a decoding function is called to turn part of the code to a shellcode
  • the shellcode checks if the argv[1] is equal to a fixed value and execute the remaining part of the shellcode if the argv[1] satisfies the condition

So now we only have to use a specific value for the command line parameter to match 0x1337c0de.

Recalling that Intel is little endian:

maze3@maze:~$ /maze/maze3 $(python -c 'print "\xde\xc0\x37\x13"')
$ id
uid=15003(maze3) gid=15003(maze3) euid=15004(maze4) groups=15003(maze3)
$ cat /etc/maze_pass/maze4
d*******k
$

Very fun challenge!
It took a while even if it didn't require strange shellcodes or buffer overwrites..

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.