forelsec

Protostar Solutions - Network

Previous posts:

Protostar – Stack
Protostar – Format String
Protostar – Heap

The network stages were pretty simple and emphasized more on data representation, rather than remote exploitation.

Network 00

The network levels all have binaries running on the system on various ports. The first level runs on port 2999. We can attach to the process with gdb and send over some junk data:

1
2
3
# echo "xxxx" | nc 192.168.1.106 2999
Please send '652467094' as a little endian 32bit int
I'm sorry, you sent 2021161080 instead

Looking at the source code, we want to send a string that, when cast to an unsigned integer, equals some random value. To do this, we simply open up a socket to the listener, read in the number, transform it to little endian, and send it back. Using a bit of Python-fu we quickly arrive at a solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import socket
import struct
import sys
from re import findall

try:
    sock = socket.socket()
    sock.connect(('192.168.1.106', 2999))

    data = sock.recv(256)

    m = findall("Please send '(.*?)' as", data)
    if len(m) > 0:
        m = int(m[0])
    else:
        print 'No data received?'
        sys.exit(1)

    print 'Swapping %s to little endian 32bit int' % m
    swapped = struct.pack('<Q', m)

    print 'Swapped to %s' % repr(swapped)
    sock.sendall(str(swapped))

    data = sock.recv(512)
    print data
except Exception, e:
    print e
finally:
    sock.close()

And when ran:

1
2
3
4
# python protostar_net0.py 
Swapping 1295208340 to little endian 32bit int
Swapped to '\x94O3M\x00\x00\x00\x00'
Thank you sir/madam

Network 01

This stage simply requires us to unpack an unsigned integer:

1
2
3
4
5
6
7
8
9
 unsigned int wanted;

  wanted = random();

  sprintf(fub, "%d", wanted);

  if(write(0, &wanted, sizeof(wanted)) != sizeof(wanted)) {
      errx(1, ":(\n");
  }

Trivially accomplished with some struct packing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket
import struct

try:
    sock = socket.socket()
    sock.connect(('192.168.1.106', 2998))

    data = sock.recv(256)

    swapped = str(struct.unpack('<I', data)[0])
    print swapped
    sock.sendall(swapped + '\r\n')
    print sock.recv(256)
except Exception, e:
    print e
finally:
    sock.close()

Which gives us:

1
2
3
# python protostar_net1.py 
1413209085
you correctly sent the data

Network 02

The third network level tasks us with reading in 4 unsigned integers, adding them together, and sending back the result. The “trick” in this stage, if you will, is that uint32 arithmetic needs to take into account wrapping. To accomplish this, we want our result to never be negative, which requires us to define the result modulo 2n, where n = bit length.

This makes for a pretty simple stage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import struct
import socket

try:
    sock = socket.socket()
    sock.connect(('192.168.1.106', 2997))

    # read in our four numbers
    total = 0
    for i in xrange(4):
        n = sock.recv(4)
        n = struct.unpack('<I', n)[0]
        print 'Read digit %d' % n
        total += n

    total = total & 0xffffffffL
    total = struct.pack('<I', total)
    sock.sendall(total)
    print sock.recv(256)
except Exception, e:
    print e
finally:
    sock.close()

Note that we AND the final result by 2n, ensuring we have a positive 32 bit value.

When run:

1
2
3
4
5
6
# python protostar_net2.py 
Read digit 1533721612
Read digit 363484815
Read digit 1200915436
Read digit 1572177737
you added them correctly

Network 03

The most enjoyable of the four levels, this stage requires us to reverse a dead simple login routine and perform the login. We’ll start by checking out the code and figuring out what it’s doing:

1
2
3
4
5
6
7
8
9
10
11
12
while(1) {

      // from the connection, read in the total length of the packet, then
      // parse with ntohs and allocate the buffer with malloc
      nread(fd, &len, sizeof(len));
      len = ntohs(len);
      buffer = malloc(len);

      if(! buffer) errx(1, "malloc failure for %d bytes", len);

      // read in LEN bytes into BUFFER
      nread(fd, buffer, len);

I’ve commented the code inline to ease understanding. Essentially, the first byte of our packet must be the total length of the packet.

1
2
3
4
5
6
7
8
9
10
11
12
13
  // switch on the first byte of the buffer
  switch(buffer[0]) {
          // if the first byte is \x17, initiate login routine
          case 23:
              // invoke login() with buffer + 1 (skip length byte) and total len - 1 of the packet
              loggedin = login(buffer + 1, len - 1);
              send_string(fd, 33, loggedin ? "successful" : "failed");
              break;
          
          default:
              send_string(fd, 58, "what you talkin about willis?");
              break;
      }

If the first byte of the buffer (second in the packet) is \x17, it initiates login.

1
2
3
4
5
6
7
8
  deduct = get_string(&resource, buffer, len);
  deduct += get_string(&username, buffer+deduct, len-deduct);
  deduct += get_string(&password, buffer+deduct, len-deduct);

  success = 0;
  success |= strcmp(resource, "net3");
  success |= strcmp(username, "awesomesauce");
  success |= strcmp(password, "password");

Here’s the real meat of the login routine; we need three, null-terminated strings that match “net3”, “awesomesauce”, and “password”, respectively, to successfully authenticate to the application. The get_string fuction pulls these strings out one by one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  unsigned char byte;

  byte = *buffer;

  // if it's malformed, i.e. not null term'd
  if(byte > len) errx(1, "badly formed packet");

  // malloc size of byte, which is the first byte at the start of the buffer
  *result = malloc(byte);

  // strcpy buffer + 1 into the result pointer location
  strcpy(*result, buffer + 1);

  // return number of bytes read
  return byte + 1;

The caveat here is that we need to prepend the string length to each entry, then null terminate it. This was quite simple to hack up, once it’d been reversed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import struct
import socket

IP = '192.168.1.106'
PORT = 2996

try:
    sock = socket.socket()
    sock.connect((IP, PORT))

    resource = "net3\x00"
    username = "awesomesauce\x00"
    password = "password\x00"

    # build login packet 
    packet = struct.pack('B', 23)
    packet += struct.pack('B', len(resource)) + resource
    packet += struct.pack('B', len(username)) + username
    packet += struct.pack('B', len(password)) + password
    packet_len = struct.pack('>H', len(packet))

    print '[!] Sending packet (%d)' % (len(packet) + len(packet_len))

    sock.sendall(packet_len + packet)

    print sock.recv(128)

except Exception, e:
    print e
finally:
    sock.close()

Note that we’re sending the packet length big endian due to the ntohs call returning a little endian value. And once run:

1
2
3
4
# python protostar_net3.py 
[!] Sending packet (33)

!successful

Network 04

Although net04 is not listed anywhere on the site, I noticed that it was both running and available in the /opt/protostar/bin directory. Disassembling it reveals the answer:

1
2
3
4
 80497a8:   e8 ff f3 ff ff          call   8048bac <srandom@plt>
 80497ad:   8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 80497b1:   89 04 24                mov    DWORD PTR [esp],eax
 80497b4:   e8 a1 ff ff ff          call   804975a <run>

does exactly what the rest of the stages have done: backgrounding the process as the current user, serving the socket up indefinitely, changing STDIO, and invoking run(). However, stage fours run is a big nop:

1
2
3
4
5
0804975a <run>:
 804975a:   55                      push   ebp
 804975b:   89 e5                   mov    ebp,esp
 804975d:   5d                      pop    ebp
 804975e:   c3                      ret  

Not so much of a stage! On to the final stages.