Exploit Exercises - Protostar - Network levels

10 min read - 1951 words

Prequisites

Net 0

For this exercise we have got a service listening on port 2999. Let’s connect and see what it does.

user@protostar:/opt/protostar/bin$ telnet 127.0.0.1 2999
Connected to 127.0.0.1.
Please send '12992273' as a little endian 32bit int
AAAA
I'm sorry, you sent 1094795585 instead

The challenge changes for every run. So we have to write a script that extracts the challenge. Subsequently, we have to calculate the correct response.

1...
2  chal = int(chal)
3  chal = struct.pack('I', chal)
4...

And this basically solves this stage.

user@protostar:/opt/protostar/bin$ python /home/user/net0.py
Connected to remote host
RECV: Please send '1189267883' as a little endian 32bit int

SEND: �F

RECV: Thank you sir/madam

Net 1

For this level we have got a service on port 2998. This time we only receive some binary data.

user@protostar:/opt/protostar/bin$ telnet 127.0.0.1 2998
Connected to 127.0.0.1.
C12
you didn't send the data properly

The value is probably binary data. We can try to decode it and send back to the service.

1...
2  chal = struct.unpack('I', chal)
3  chal = chal[0]
4...

And this piece of code basically solves this stage.

user@protostar:/opt/protostar/bin$ python /home/user/net1.py
Connected to remote host
RECV: %�
        ?
SEND: 1057730853

RECV: you correctly sent the data

Net 2

The service for this level is listening on port 2997. We can connect and receive some binary output.

user@protostar:/opt/protostar/bin$ telnet 127.0.0.1 2997
Connected to 127.0.0.1.
J�N�X,��y1234
sorry, try again. invalid

By utilizing a recv on the socket we see that it’s four separate writes by the server. Subsequently, the server waits for our response. By unpacking the binary data we get four unsigned integers. We can add them up and pack them again. Finally send them back to the service.

 1...
 2  buf = s.recv(4)
 3  chal1 = struct.unpack('I', buf)
 4  chal1 = chal1[0]
 5
 6  buf = s.recv(4)
 7  chal2 = struct.unpack('I', buf)
 8  chal2 = chal2[0]
 9
10  buf = s.recv(4)
11  chal3 = struct.unpack('I', buf)
12  chal3 = chal3[0]
13
14  buf = s.recv(4)
15  chal4 = struct.unpack('I', buf)
16  chal4 = chal4[0]
17
18  chal = chal1 + chal2 + chal3 + chal4
19  chal = struct.pack('I', chal)
20...

Voila, level solved.

user@protostar:/opt/protostar/bin$ python /home/user/net2.py
Connected to remote host
RECV: N
RECV: :
RECV:
RECV: k-4
SEND: y�(�

RECV: you added them correctly

Net 3

The service for this level is listening on port 2996. We can establish a connection but the server is not sending any data. So, we have to take a look at the assembly. Luckily, the binary is the most complex encountered so far. In order to reverse engineer we have to take some little steps.

...
0x08049b49 <main+32>:   call   0x8048ef8 <background_process>
0x08049b4e <main+37>:   movl   $0xbb4,(%esp)
0x08049b55 <main+44>:   call   0x8049395 <serve_forever>
0x08049b5a <main+49>:   mov    %eax,0x18(%esp)
0x08049b5e <main+53>:   mov    0x18(%esp),%eax
0x08049b62 <main+57>:   mov    %eax,(%esp)
0x08049b65 <main+60>:   call   0x8049475 <set_io>
0x08049b6a <main+65>:   movl   $0x0,(%esp)
0x08049b71 <main+72>:   call   0x8048c98 <time@plt>
0x08049b76 <main+77>:   mov    %eax,(%esp)
0x08049b79 <main+80>:   call   0x8048c58 <srandom@plt>
0x08049b7e <main+85>:   mov    0x18(%esp),%eax
0x08049b82 <main+89>:   mov    %eax,(%esp)
0x08049b85 <main+92>:   call   0x8049a26 <run>        ; main loop
...

We get the idea. The binary starts a background process that handles all the requests. At main+92 the respective funtion is started. Let’s take a closer look.

run()

...
0x08049a2c <run+6>:     movl   $0x2,0x8(%esp)         ; pass 2
0x08049a34 <run+14>:    lea    -0x12(%ebp),%eax       ; &Length
0x08049a37 <run+17>:    mov    %eax,0x4(%esp)         ; pass &Length
0x08049a3b <run+21>:    mov    0x8(%ebp),%eax         ; socket
0x08049a3e <run+24>:    mov    %eax,(%esp)            ; pass socket
0x08049a41 <run+27>:    call   0x80495f0 <nread>      ; read 8 bytes
0x08049a46 <run+32>:    movzwl -0x12(%ebp),%eax       ; Length
0x08049a4a <run+36>:    movzwl %ax,%eax
0x08049a4d <run+39>:    mov    %eax,(%esp)
0x08049a50 <run+42>:    call   0x8048be8 <ntohs@plt>  ; convert Length to host byte order
0x08049a55 <run+47>:    mov    %ax,-0x12(%ebp)        ; store integer
...

So the services starts by reading two bytes.

...
0x08049a59 <run+51>:    movzwl -0x12(%ebp),%eax       ; Length
0x08049a5d <run+55>:    movzwl %ax,%eax
0x08049a60 <run+58>:    mov    %eax,(%esp)
0x08049a63 <run+61>:    call   0x8048cc8 <malloc@plt> ; malloc Length bytes
0x08049a68 <run+66>:    mov    %eax,-0x10(%ebp)       ; store address
0x08049a6b <run+69>:    cmpl   $0x0,-0x10(%ebp)
0x08049a6f <run+73>:    jne    0x8049a90 <run+106>    ; error branch
...

This value is subsequently utilized to allocate the memory on the heap.

...
0x08049a90 <run+106>:   movzwl -0x12(%ebp),%eax       ; Length
0x08049a94 <run+110>:   movzwl %ax,%eax
0x08049a97 <run+113>:   mov    %eax,0x8(%esp)         ; pass Length
0x08049a9b <run+117>:   mov    -0x10(%ebp),%eax       ; Data
0x08049a9e <run+120>:   mov    %eax,0x4(%esp)         ; pass Data
0x08049aa2 <run+124>:   mov    0x8(%ebp),%eax         ; socket
0x08049aa5 <run+127>:   mov    %eax,(%esp)            ; pass socket
0x08049aa8 <run+130>:   call   0x80495f0 <nread>      ; read length bytes
...

After the memory space has been allocated the service reads the required number of bytes.

...
0x08049aad <run+135>:   mov    -0x10(%ebp),%eax       ; Data
0x08049ab0 <run+138>:   movzbl (%eax),%eax
0x08049ab3 <run+141>:   movzbl %al,%eax
0x08049ab6 <run+144>:   cmp    $0x17,%eax             ; hard coded value 0x17
0x08049ab9 <run+147>:   jne    0x8049b09 <run+227>    ; jmp not equal
...

The first byte is checked against a fixed value = 23.

...
0x08049abb <run+149>:   movzwl -0x12(%ebp),%eax       ; Length
0x08049abf <run+153>:   sub    $0x1,%eax              ; Length - 1
0x08049ac2 <run+156>:   movzwl %ax,%eax
0x08049ac5 <run+159>:   mov    -0x10(%ebp),%edx       ; Data
0x08049ac8 <run+162>:   add    $0x1,%edx              ; Data+1
0x08049acb <run+165>:   mov    %eax,0x4(%esp)         ; pass Length
0x08049acf <run+169>:   mov    %edx,(%esp)            ; pass Data
0x08049ad2 <run+172>:   call   0x8049861 <login>
...

When the first byte matches the signature the login() function is called. The length variable is reduced by one. At the same time, the pointer to Data is incremented by one. The magic value is therefore not relevant for the login() function.

...
0x08049ad7 <run+177>:   mov    %eax,-0xc(%ebp)
0x08049ada <run+180>:   cmpl   $0x0,-0xc(%ebp)
0x08049ade <run+184>:   je     0x8049ae7 <run+193>
0x08049ae0 <run+186>:   mov    $0x8049ff4,%eax         ; "successful"
0x08049ae5 <run+191>:   jmp    0x8049aec <run+198>
0x08049ae7 <run+193>:   mov    $0x8049fff,%eax         ; "failed"
0x08049aec <run+198>:   mov    %eax,0x8(%esp)          ; pass String
0x08049af0 <run+202>:   movl   $0x21,0x4(%esp)         ; pass 33
0x08049af8 <run+210>:   mov    0x8(%ebp),%eax
0x08049afb <run+213>:   mov    %eax,(%esp)             ; pass socket
0x08049afe <run+216>:   call   0x8049989 <send_string>
0x08049b03 <run+221>:   nop
0x08049b04 <run+222>:   jmp    0x8049a2c <run+6>       ; Loop again
...

The return value is stored and determines the response. Afterwards it jumps back to the start. Obviously, we have to dive into the login() function.

Login()

...
0x08049864 <login+3>:   sub    $0x48,%esp
0x08049867 <login+6>:   mov    0xc(%ebp),%eax       ; Length
0x0804986a <login+9>:   mov    %ax,-0x2c(%ebp)      ; store Length
0x0804986e <login+13>:  cmpw   $0x2,-0x2c(%ebp)     ; cmp against 0x2
0x08049873 <login+18>:  ja     0x8049889 <login+40> ; jmp > 2
...

First of all, the Length is checked against a minimum of 3.

...
0x08049889 <login+40>:  movl   $0x0,-0x1c(%ebp)     ; zero out A
0x08049890 <login+47>:  mov    -0x1c(%ebp),%eax
0x08049893 <login+50>:  mov    %eax,-0x18(%ebp)     ; zero out B
0x08049896 <login+53>:  mov    -0x18(%ebp),%eax
0x08049899 <login+56>:  mov    %eax,-0x14(%ebp)     ; zero out C
...

Subsequently, three pointers are zeroed. We therefore have to deal with three different sections in the Data variable. Let’s find out how they are composed.

...
0x0804989c <login+59>:  movzwl -0x2c(%ebp),%eax   ; Length
0x080498a0 <login+63>:  mov    %eax,0x8(%esp)     ; pass Length
0x080498a4 <login+67>:  mov    0x8(%ebp),%eax     ; Data
0x080498a7 <login+70>:  mov    %eax,0x4(%esp)     ; pass Data
0x080498ab <login+74>:  lea    -0x14(%ebp),%eax   ; &C
0x080498ae <login+77>:  mov    %eax,(%esp)        ; pass C
0x080498b1 <login+80>:  call   0x80497fa <get_string>
0x080498b6 <login+85>:  mov    %eax,-0x10(%ebp)   ; store ret value
...

So there we go again. Data is passed into some kind of parsing function. The result will be stored in variable C.

...
0x080498b9 <login+88>:  mov    -0x10(%ebp),%eax   ; ret
0x080498bc <login+91>:  movzwl -0x2c(%ebp),%edx   ; Length
0x080498c0 <login+95>:  mov    %edx,%ecx
0x080498c2 <login+97>:  sub    %ax,%cx            ; Length - ret
0x080498c5 <login+100>: mov    %ecx,%eax
0x080498c7 <login+102>: movzwl %ax,%edx
0x080498ca <login+105>: mov    -0x10(%ebp),%eax   ; ret
0x080498cd <login+108>: add    0x8(%ebp),%eax     ; Data + ret
0x080498d0 <login+111>: mov    %edx,0x8(%esp)     ; pass (Length - ret)
0x080498d4 <login+115>: mov    %eax,0x4(%esp)     ; pass (Data + ret)
0x080498d8 <login+119>: lea    -0x18(%ebp),%eax   ; &B
0x080498db <login+122>: mov    %eax,(%esp)        ; pass B
0x080498de <login+125>: call   0x80497fa <get_string>
0x080498e3 <login+130>: add    %eax,-0x10(%ebp)   ; adjust ret value
...

The return value is utilized to adjust the Length. At the same time, the pointer to Data is incremented by the return value. This is done to extract the value of variable B.

...
0x080498e6 <login+133>: mov    -0x10(%ebp),%eax   ; ret
0x080498e9 <login+136>: movzwl -0x2c(%ebp),%edx   ; Length
0x080498ed <login+140>: mov    %edx,%ecx
0x080498ef <login+142>: sub    %ax,%cx            ; Length - ret
0x080498f2 <login+145>: mov    %ecx,%eax
0x080498f4 <login+147>: movzwl %ax,%edx
0x080498f7 <login+150>: mov    -0x10(%ebp),%eax   ; ret
0x080498fa <login+153>: add    0x8(%ebp),%eax     ; Data + ret
0x080498fd <login+156>: mov    %edx,0x8(%esp)     ; pass (Length - ret)
0x08049901 <login+160>: mov    %eax,0x4(%esp)     ; pass (Data + ret)
0x08049905 <login+164>: lea    -0x1c(%ebp),%eax   ; &A
0x08049908 <login+167>: mov    %eax,(%esp)        ; pass A
0x0804990b <login+170>: call   0x80497fa <get_string>
0x08049910 <login+175>: add    %eax,-0x10(%ebp)   ; adjust ret value
...

And once again for variable A. What might follow?

...
0x08049913 <login+178>: movl   $0x0,-0xc(%ebp)
0x0804991a <login+185>: mov    -0x14(%ebp),%eax        ;
0x0804991d <login+188>: movl   $0x8049f94,0x4(%esp)    ; "net3"
0x08049925 <login+196>: mov    %eax,(%esp)
0x08049928 <login+199>: call   0x8048d28 <strcmp@plt>
0x0804992d <login+204>: or     %eax,-0xc(%ebp)
0x08049930 <login+207>: mov    -0x18(%ebp),%eax
0x08049933 <login+210>: movl   $0x8049f99,0x4(%esp)    ; "awesomesauce"
0x0804993b <login+218>: mov    %eax,(%esp)
0x0804993e <login+221>: call   0x8048d28 <strcmp@plt>
0x08049943 <login+226>: or     %eax,-0xc(%ebp)
0x08049946 <login+229>: mov    -0x1c(%ebp),%eax
0x08049949 <login+232>: movl   $0x8049fa6,0x4(%esp)    ; "password"
0x08049951 <login+240>: mov    %eax,(%esp)
0x08049954 <login+243>: call   0x8048d28 <strcmp@plt>
0x08049959 <login+248>: or     %eax,-0xc(%ebp)
...

After this, the function compares the three variables against a fixed set of strings. Now the question is what exactly is happening in the get_string() function.

get_string()

...
0x080497fd <get_string+3>:      sub    $0x38,%esp
0x08049800 <get_string+6>:      mov    0x10(%ebp),%eax        ; Length
0x08049803 <get_string+9>:      mov    %ax,-0x1c(%ebp)        ; store Length
0x08049807 <get_string+13>:     mov    0xc(%ebp),%eax         ; Data
0x0804980a <get_string+16>:     movzbl (%eax),%eax            ; extract first byte
0x0804980d <get_string+19>:     mov    %al,-0x9(%ebp)         ; store first byte
0x08049810 <get_string+22>:     movzbl -0x9(%ebp),%eax
0x08049814 <get_string+26>:     cmp    -0x1c(%ebp),%ax
0x08049818 <get_string+30>:     jbe    0x804982e <get_string+52>
...

This further details the structure of the message. The first byte of each of the three message parts probably includes the length of the respective part.

...
0x0804982e <get_string+52>:     movzbl -0x9(%ebp),%eax        ; load variable length
0x08049832 <get_string+56>:     mov    %eax,(%esp)
0x08049835 <get_string+59>:     call   0x8048cc8 <malloc@plt> ;
0x0804983a <get_string+64>:     mov    %eax,%edx
0x0804983c <get_string+66>:     mov    0x8(%ebp),%eax         ; target buffer
0x0804983f <get_string+69>:     mov    %edx,(%eax)
0x08049841 <get_string+71>:     mov    0xc(%ebp),%eax         ; Data
0x08049844 <get_string+74>:     lea    0x1(%eax),%edx         ; increment pointer
0x08049847 <get_string+77>:     mov    0x8(%ebp),%eax         ; target buffer
0x0804984a <get_string+80>:     mov    (%eax),%eax            ; follow pointer
0x0804984c <get_string+82>:     mov    %edx,0x4(%esp)         ; pass Data
0x08049850 <get_string+86>:     mov    %eax,(%esp)            ; pass target buffer
0x08049853 <get_string+89>:     call   0x8048c18 <strcpy@plt>
0x08049858 <get_string+94>:     movzbl -0x9(%ebp),%eax
0x0804985c <get_string+98>:     add    $0x1,%eax
...

Indeed, an apropriate piece of memory is allocated, based to the first byte of the message part. Afterwards the remaining bytes are copied to that location before the function returns.

Debugging

As we have got the binary we can also do dynamic analysis with gdb. This should make analysis much easier. First of all, let’s attach gdb to the service.

 1user@protostar:/$ sudo gdb /opt/protostar/bin/net3 -p $(pgrep -f net3)
 2...
 3gdb$ set follow-fork-mode child
 4gdb$ set detach-on-fork off
 5gdb$ info inferiors
 6  Num  Description
 7* 1    process 7622
 8gdb$ break *0x08049ad2
 9gdb$ c
10[New process 8560]
11[Switching to process 8560]
12...
13gdb$ info inferiors
14  Num  Description
15* 2    process 8560
16  1    process 7622

We want to follow the child process after fork. Also, we don’t want to detach from the main process. With a breakpoint just before login() we can examine the execution flow.

Summary

So the services first requires the number of bytes to read as a short (2 bytes). Afterwards we have to include the magic value 23. The next part of the message is split up in three strings. Each string is prefixed with its length. The three strings need to be “net3”, “awesomesauce” and “password”.

 1...
 2  msg = '\x17'
 3  msg += '\x05net3\x00'
 4  msg += '\x0dawesomesauce\x00'
 5  msg += '\x0apassword\x00'
 6
 7  msg = struct.pack(">H", len(msg)) + msg
 8  print("SEND: %s" % hexlify(msg))
 9  s.send(msg)
10
11  buf = s.recv(1024)
12  print("RECV: %s" % buf)
13...

Voila, level solved.

Net 4

Another undocumented level awaits!

0x0804975a <run+0>:     push   %ebp
0x0804975b <run+1>:     mov    %esp,%ebp
0x0804975d <run+3>:     pop    %ebp
0x0804975e <run+4>:     ret    

Nothing here :(

Exploit Exercises