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
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
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
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.
...
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.
...
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.
...
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.
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.
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.
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 :(