THE

SPRAWL

  •  
  •  
  •  
  • Exploit Exercises - Protostar wargame includes a number of carefully prepared exercises to help hone your basic exploitation skills. In this walkthrough I will go over the network exploitation portion of the wargame.

    Changing gears a bit from the memory corruption exploits, this section challenges players to perform various data manipulations over the network and reverse engineer program logic to complete challenges. While the source code is available for all of these challenges, I would recommend you to study the disassembly to catch the nuances in the compiled binaries. This is the approach I have taken in many of the exercises; however, feel free to skip the reversing sections.

    Spoiler Warning: I would highly recommend you to go over the exercises yourself and come back to this article to find possibly different solutions or in case you get stuck.

    Net 0

    The first exercise starts a server on port 2999 and asks players to convert a randomly generated value to Little-Endian format:

    $ nc localhost 2999
    Please send '809553086' as a little endian 32bit int
    $ nc localhost 2999
    Please send '52684726' as a little endian 32bit int
    $ nc localhost 2999
    Please send '529252961' as a little endian 32bit int
    

    The output can be quickly parsed and repacked to Little-Endian with the following Python script:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2999
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    data = s.recv(1024)
    print "%s" % data      # print the greeting message
    
    value = data.split()[2]  # third word in a string
    value = value[1:-1]      # chop off quotes
    value = int(value)       # string to integer
    
    print "[*] Big Endian: 0x%x" % value
    value = pack("<I", value) # little endian
    
    print "[*] Lit Endian: 0x%s" % hexlify(value)
    s.sendall(value)         # send the value back
    
    data = s.recv(1024)
    print "%s" % data      # print the confirmation
    
    s.close()
    

    Here is the output of the script which successfully completes the challenge:

    Please send '1840257222' as a little endian 32bit int
    
    [*] Big Endian: 0x6db018c6
    [*] Lit Endian: 0xc618b06d
    Thank you sir/madam
    

    Net 1

    The next exercise returns raw bytes to the user instead of the integer string and expects some input:

    $ nc localhost 2998
    bp6:
    you didn't send the data properly
    

    Let's work backwards to figure out which conditions resulted in the fail string:

    0x080498ef <run+213>:   lea    eax,[ebp-0x18]            ; buf
    0x080498f2 <run+216>:   mov    DWORD PTR [esp+0x4],eax
    0x080498f6 <run+220>:   lea    eax,[ebp-0x24]            ; fub
    0x080498f9 <run+223>:   mov    DWORD PTR [esp],eax
    0x080498fc <run+226>:   call   0x8048d50 <strcmp@plt>    ; compare buffers
    0x08049901 <run+231>:   test   eax,eax
    0x08049903 <run+233>:   jne    0x8049913 <run+249>       ; jump if not equal
    0x08049905 <run+235>:   mov    DWORD PTR [esp],0x8049d5b ; success string
    0x0804990c <run+242>:   call   0x8048cf0 <puts@plt>
    0x08049911 <run+247>:   jmp    0x804991f <run+261>
    0x08049913 <run+249>:   mov    DWORD PTR [esp],0x8049d78 ; fail string
    0x0804991a <run+256>:   call   0x8048cf0 <puts@plt>
    

    In the above disassembly buffers buf and fub are compared using the strcmp(). A success or fail message is displayed based on whether the two buffers are equal or not.

    In order to see how to make the two buffers equal let's analyze how the buffer fub is populated:

    0x08049820 <run+6>:     call   0x8048b10 <random@plt>     ; generate random
    0x08049825 <run+11>:    mov    DWORD PTR [ebp-0x28],eax   ; uint and store it
    0x08049828 <run+14>:    mov    edx,DWORD PTR [ebp-0x28]
    0x0804982b <run+17>:    mov    eax,0x8049d54              ; "%d" fmt string
    0x08049830 <run+22>:    mov    DWORD PTR [esp+0x8],edx
    0x08049834 <run+26>:    mov    DWORD PTR [esp+0x4],eax
    0x08049838 <run+30>:    lea    eax,[ebp-0x24]             ; fub
    0x0804983b <run+33>:    mov    DWORD PTR [esp],eax
    0x0804983e <run+36>:    call   0x8048a90 <sprintf@plt>    ; write string to fub
    

    A random unsigned integer is generated with a call to random() and its string representation is stored in the buffer fub.

    0x08049843 <run+41>:    mov    DWORD PTR [esp+0x8],0x4    ; number of bytes
    0x0804984b <run+49>:    lea    eax,[ebp-0x28]             ; random uint
    0x0804984e <run+52>:    mov    DWORD PTR [esp+0x4],eax
    0x08049852 <run+56>:    mov    DWORD PTR [esp],0x0        ; STDOUT (daemon)
    0x08049859 <run+63>:    call   0x8048b50 <write@plt>      ; write string to STDOUT
    

    Next the program sends the randomly generated value to the user. This operation explains the four garbled bytes we have received in the sample session above.

    The following assembly snippet shows how the buffer buf gets populated before it is used in the comparison with fub:

    0x08049877 <run+93>:    mov    eax,ds:0x804af68           ; STDIN
    0x0804987c <run+98>:    mov    DWORD PTR [esp+0x8],eax
    0x08049880 <run+102>:   mov    DWORD PTR [esp+0x4],0xb    ; number of bytes
    0x08049888 <run+110>:   lea    eax,[ebp-0x18]             ; buf
    0x0804988b <run+113>:   mov    DWORD PTR [esp],eax
    0x0804988e <run+116>:   call   0x8048b70 <fgets@plt>      ; get string to buf
    

    The application simply reads up to 0xb or 11 bytes from the network and stores them in the buffer buf.

    As we have previously observed, the bytes read into the buf must be equal to the ones stored in fub. Unfortunately, the buffer boundaries are properly checked making it impossible to just overflow the random value in fub with an arbitrary value. Looks like we have to do some real work =).

    First, let's modify the previous solution to simply echo back received bytes to the server:

    data = s.recv(1024)                 # receive data
    print "[*] Input: %s (%s)" % (data, hexlify(data))
    
    value = int( hexlify(data) , 16)    # convert to int
    print "[*] Output: %d (%x)" % (value, value)
    
    s.sendall("%d" % value)             # send data back
    

    Below is the output:

    [*] Input: éU╦  (8255cb20)
    [*] Output: 2186660640 (8255cb20)
    you didn't send the data properly
    

    Hmm, something went wrong. We have sent the exact data back, but the two buffers did no match. Let's attach a debugger to the running daemon (need sudo or root privileges) and see exactly what happens after the randomly generated uint is copied to the buffer:

    $ sudo killall net1
    $ sudo gdb ./net1
    (gdb) set follow-fork-mode child
    (gdb) break *run+41
    (gdb) r
    Starting program: /opt/protostar/bin/net1
    [New process 15977]
    [New process 15978]
    [Switching to process 15978]
    
    Breakpoint 1, run () at net1/net1.c:20
    
    (gdb) x/x $ebp-0x28
    0xbffff790:     0x20cb5582      <-- random value (hex)
    (gdb) x/4xb $ebp-0x28
    0xbffff790:     0x82    0x55    0xcb    0x20  <-- random value stored
                                                      in little endian.
    (gdb) x/d $ebp-0x28
    0xbffff790:     550196610       <-- random value (int)
    (gdb) x/s $ebp-0x24
    0xbffff794:      "550196610"    <-- string in fub
    

    In the session above, the program has generated the random value 0x20cb5582 and stored its decimal ("%d") representation in the buffer fub. Let's see what happens to the user provided string:

    (gdb) break *run+121
    (gdb) c
    Continuing.
    Breakpoint 2, 0x08049893 in run () at net1/net1.c:24
    (gdb) x/s $ebp-0x18
    0xbffff7a0:      "2186660640"   <-- user string in buf
    (gdb) p/x 2186660640
    $1 = 0x8255cb20                 <-- hex representation
    

    Now that's bizarre, the user string 2186660640 or 0x8255cb20 is in the exact opposite order as the random number 0x20cb5582. However, if you recall the bytes we have received in the client python script actually corresponded to 0x8255cb20. The reason for the discrepancy is due to the network library interpreting and sending the randomly generated value in the network (big endian) order and not the little endian order as it is used on the Intel machine. As a result the client script must do additional conversion to little endian before echoing the data back:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2998
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    data = s.recv(1024)                 # receive data
    print "[*] Input: %s (%s)" % (data, hexlify(data))
    
    
    value = int( hexlify(data) , 16)    # convert to int
    
    value = pack("<I", value)           # little endian
    value = int( hexlify(value) , 16)   # convert to int
    
    print "[*] Output: %d (%x)" % (value, value)
    
    s.sendall("%d\r\n" % value)             # send data back
    
    data = s.recv(1024)
    print "%s" % data      # print the confirmation
    
    s.close()
    

    The output for the above:

    [*] Input: ┼3A8 (c5334138)
    [*] Output: 943797189 (384133c5)
    you correctly sent the data
    

    NOTE: Alternatively you could have also used struct.unpack("<I", data)[0] to perform a similar conversion.

    Net 2

    Building upon the previous exercise, Net 2 challenges players to perform an arithmetic operation on four randomly generated numbers and send the result back to the server:

    0x08049791 <run+7>:     mov    DWORD PTR [ebp-0xc],0x0
    0x08049798 <run+14>:    mov    DWORD PTR [ebp-0x10],0x0         ; i = 0
    0x0804979f <run+21>:    jmp    0x80497fb <run+113>
    ;------------------------------------------------------------------------------
    0x080497a1 <run+23>:    mov    ebx,DWORD PTR [ebp-0x10]         ; i
    0x080497a4 <run+26>:    call   0x8048a98 <random@plt>           ; random value
    0x080497a9 <run+31>:    mov    DWORD PTR [ebp+ebx*4-0x20],eax   ; set quad[i]
    0x080497ad <run+35>:    mov    eax,DWORD PTR [ebp-0x10]         ; i
    0x080497b0 <run+38>:    mov    eax,DWORD PTR [ebp+eax*4-0x20]   ; quad[i]
    0x080497b4 <run+42>:    add    DWORD PTR [ebp-0xc],eax          ; sum+=quad[i]
      :
      :
    0x080497f7 <run+109>:   add    DWORD PTR [ebp-0x10],0x1         ; i++
    0x080497fb <run+113>:   cmp    DWORD PTR [ebp-0x10],0x3         ; i <= 3
    0x080497ff <run+117>:   jle    0x80497a1 <run+23>               ; loop
    

    In the disassembly snippet above, the four generated random numbers are sent to the user. The user input is later collected and compared to the sum:

    0x08049835 <run+171>:   mov    eax,DWORD PTR [ebp-0x24]         ; user input
    0x08049838 <run+174>:   cmp    DWORD PTR [ebp-0xc],eax          ; sum
    0x0804983b <run+177>:   jne    0x804984b <run+193>              ; jump if not equal
    0x0804983d <run+179>:   mov    DWORD PTR [esp],0x8049c9c        ; success string
    0x08049844 <run+186>:   call   0x8048c58 <puts@plt>
    0x08049849 <run+191>:   jmp    0x8049857 <run+205>
    0x0804984b <run+193>:   mov    DWORD PTR [esp],0x8049cb5        ; fail string
    0x08049852 <run+200>:   call   0x8048c58 <puts@plt>
    

    We can modify the previous solution to separately accept and add four different random dwords:

    sum = 0
    
    for i in range(4):
        data = s.recv(4)                    # receive data
    
        print "[*] Input: %s (%s)" % (data, hexlify(data))    
        value = unpack("<I",data)[0]        # little endian
        sum += value
    
    print "[*] Sum: %d (%x)" % (sum, sum)
    sum = pack("<I",sum)                    # little endian
    s.sendall(sum)                          # send data back
    

    The above solution works in most cases; however, periodically you will encounter the following Python error:

    Traceback (most recent call last):
      File "net2.py", line 21, in <module>
        sum = pack("<I",sum)                    # little endian
    struct.error: integer out of range for 'I' format code
    

    Indeed we forgot to account for the sum of four random integers exceeding 4 bytes resulting in the overflow. Python happily expands the size of sum thus causing the error. In order to emulate the integer overflow condition on the server, we must chop-off any overflown bits by applying a 32bit mask:

    sum = sum & 0xffffffff
    

    We can now complete the exploit:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2997
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    sum = 0
    
    for i in range(4):
        data = s.recv(4)                    # receive data
    
        print "[*] Input: %s (%s)" % (data, hexlify(data))    
        value = unpack("<I",data)[0]        # little endian
        sum += value
    
    sum = sum & 0xffffffff
    print "[*] Sum: %d (%x)" % (sum, sum)
    sum = pack("<I",sum)                    # little endian
    s.sendall(sum)                          # send data back
    
    data = s.recv(1024)
    print "%s" % data               # print the confirmation
    
    s.close()
    

    Here is the output:

    [*] Input: LxUk (4c78556b)
    [*] Input: V↨fq (56176671)
    [*] Input: ⌐!▒O (a921b14f)
    [*] Input: ∟┬½☺ (1cc2ab01)
    [*] Sum: 773354343 (2e187367)
    you added them correctly
    

    Net 3

    The last exercise involves to reverse engineering of the authentication protocol. Since the application is much larger than previous examples, I will analyze it small chunks.

    Reversing run()

    Let's begin with the main request processing function - run():

    0x08049a2c <run+6>:     mov    DWORD PTR [esp+0x8],0x2  ; how many bytes to read
    0x08049a34 <run+14>:    lea    eax,[ebp-0x12]           ; len 
    0x08049a37 <run+17>:    mov    DWORD PTR [esp+0x4],eax
    0x08049a3b <run+21>:    mov    eax,DWORD PTR [ebp+0x8]  ; get fd
    0x08049a3e <run+24>:    mov    DWORD PTR [esp],eax
    0x08049a41 <run+27>:    call   0x80495f0 <nread>        ; read two bytes from fd
    

    The run() function begins by reading a two byte (word) length value and storing it in a local variable.

    0x08049a46 <run+32>:    movzx  eax,WORD PTR [ebp-0x12]  ; len
    0x08049a4a <run+36>:    movzx  eax,ax                   ; expand word to dword
    0x08049a4d <run+39>:    mov    DWORD PTR [esp],eax
    0x08049a50 <run+42>:    call   0x8048be8 <ntohs@plt>    ; net to host byte order
    0x08049a55 <run+47>:    mov    WORD PTR [ebp-0x12],ax   ; update len
    0x08049a59 <run+51>:    movzx  eax,WORD PTR [ebp-0x12]  ; len
    0x08049a5d <run+55>:    movzx  eax,ax                   ; expand word to dword
    0x08049a60 <run+58>:    mov    DWORD PTR [esp],eax
    0x08049a63 <run+61>:    call   0x8048cc8 <malloc@plt>   ; allocate chunk
    
    0x08049a68 <run+66>:    mov    DWORD PTR [ebp-0x10],eax ; store heap chunk address
    0x08049a6b <run+69>:    cmp    DWORD PTR [ebp-0x10],0x0 ; compare it to nullptr
    0x08049a6f <run+73>:    jne    0x8049a90 <run+106>      ; jump if valid address
    

    Next, the function dynamically allocates a heap chunk using the first two bytes as the size parameter for malloc().

    0x08049a90 <run+106>:   movzx  eax,WORD PTR [ebp-0x12]  ; load len
    0x08049a94 <run+110>:   movzx  eax,ax                   ; zero expand word to dword
    0x08049a97 <run+113>:   mov    DWORD PTR [esp+0x8],eax
    0x08049a9b <run+117>:   mov    eax,DWORD PTR [ebp-0x10] ; heap address
    0x08049a9e <run+120>:   mov    DWORD PTR [esp+0x4],eax
    0x08049aa2 <run+124>:   mov    eax,DWORD PTR [ebp+0x8]  ; get fd
    0x08049aa5 <run+127>:   mov    DWORD PTR [esp],eax
    0x08049aa8 <run+130>:   call   0x80495f0 <nread>        ; read [len] bytes
    

    After allocating a heap chunk of size len, the program reads the same number of bytes from the network and stores them on the heap.

    0x08049aad <run+135>:   mov    eax,DWORD PTR [ebp-0x10] ; heap address
    0x08049ab0 <run+138>:   movzx  eax,BYTE PTR [eax]       ; read one byte
    0x08049ab3 <run+141>:   movzx  eax,al                   ; zero expand byte to dword
    0x08049ab6 <run+144>:   cmp    eax,0x17                 ; compare with 0x17 (23)
    0x08049ab9 <run+147>:   jne    0x8049b09 <run+227>      ; jump if not equal
    

    From the data that was just stored on the heap, the function run() reads a single byte and makes sure it is equal to the magic number 0x17 or 23.

    0x08049abb <run+149>:   movzx  eax,WORD PTR [ebp-0x12]  ; len
    0x08049abf <run+153>:   sub    eax,0x1                  ; len - 1
    0x08049ac2 <run+156>:   movzx  eax,ax                   ; expand word to dword
    0x08049ac5 <run+159>:   mov    edx,DWORD PTR [ebp-0x10] ; heap address
    0x08049ac8 <run+162>:   add    edx,0x1                  ; address + 1
    0x08049acb <run+165>:   mov    DWORD PTR [esp+0x4],eax  ; pass len -1 to login
    0x08049acf <run+169>:   mov    DWORD PTR [esp],edx      ; pass address + 1 to login
    0x08049ad2 <run+172>:   call   0x8049861 <login>        ; call login
    

    After making sure the first byte contains the expected magic number, the remainder (skipping the magic number) of the packet is passed to the function login() along with len - 1 to compensate for the chopped off byte. Before we jump into the login() function let's analyze how the return value is processed in the remainder of the run():

    0x08049ad7 <run+177>:   mov    DWORD PTR [ebp-0xc],eax  ; login return value
    0x08049ada <run+180>:   cmp    DWORD PTR [ebp-0xc],0x0  ; check if it's zero
    0x08049ade <run+184>:   je     0x8049ae7 <run+193>
    0x08049ae0 <run+186>:   mov    eax,0x8049ff4            ; "successful"
    0x08049ae5 <run+191>:   jmp    0x8049aec <run+198>
    0x08049ae7 <run+193>:   mov    eax,0x8049fff            ; "failed"
    0x08049aec <run+198>:   mov    DWORD PTR [esp+0x8],eax  ; pass string
    0x08049af0 <run+202>:   mov    DWORD PTR [esp+0x4],0x21 ; pass 33 to send_string
    0x08049af8 <run+210>:   mov    eax,DWORD PTR [ebp+0x8]  ; fd
    0x08049afb <run+213>:   mov    DWORD PTR [esp],eax      ; pass fd to send_stirng
    0x08049afe <run+216>:   call   0x8049989 <send_string>  ; send success or fail
    0x08049b03 <run+221>:   nop
    0x08049b04 <run+222>:   jmp    0x8049a2c <run+6>        ; loop
    

    So depending whether the call to login() returns 0 (false) or 1 (true) a respective string failed or successful is sent to the user. Thus, we want the login() to return 1.

    Debugging daemons with gdb

    Before proceeding any further, let's verify that we can replicate the authentication packet which passes all of the tests up to the point that the `login() is called. The following python script will craft the authentication packet that satisfies all of the conditions reversed so far:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2996
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    auth = "\x17AAAA"          # authentication packet
    len = pack(">H",len(auth)) # packed in network/big endian order
    
    packet = len + auth
    print hexlify(packet)      # debug: print packet in hex
    
    s.sendall(packet)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Running the above script against the net3 daemon produces the following output:

    00051741414141 <- debug output
    net3:          <- net3 reply
    

    The first line is the print out of the packet in hex form just before it is sent. This is useful for debugging. Notice that the length parameter is dynamically calculated and packed using the network (big endian) order since the application converts it to little endian using the ntohs() call. The first byte of the actual authentication payload is 0x17 just as the program expects.

    Now let's attach gdb to the daemon and follow the application flow. Dealing with forking processes requires some additional configuration of gdb:

    $ ps aux | grep net3
    996      25110  0.0  0.0   1536   344 ?        Ss   Apr24   0:00 /opt/protostar/bin/net3
    $ sudo gdb ./net3 -p 25110
      :
      :
    (gdb) set follow-fork-mode child
    (gdb) set detach-on-fork off
    (gdb) info inferiors
      Num  Description
    * 1    process 25110
    (gdb) inferior 1
    [Switching to thread 1 (process 25110)]
    

    The first parameter, follow-form-mode child, makes sure that gdb automatically follows forked child processes so we can set and catch breakpoints in child processes. The second parameter ,set detach-on-fork off, makes sure gdb does not detach from the main process, so we could switch to it after a child process terminates using the inferior 1 command.

    Let's set a breakpoint just before login() is called and see if we can reach it:

    (gdb) break *run+172
    Breakpoint 1 at 0x8049ad2: file net3/net3.c, line 95.
    (gdb) c
    Continuing.
    [New process 30760]
    [Switching to process 30760]
    
    Breakpoint 1, 0x08049ad2 in run (fd=6) at net3/net3.c:95
    (gdb) x/2x $esp
    0xbffff790:     0x0804c009      0x00000004
                         |               |
       auth packet body--+               +-- auth packet len - 1
    
    (gdb) x/x 0x0804c009
    0x804c009:      0x41414141   <-- auth packet body
    

    Great! The debugger has successfully switched to the forked process and caught the breakpoint just before the call to login(). The stack contains the expected data: authentication packet body and length.

    Just so I can illustrate how to switch back to the parent process, I will let the debugger to continue executing until the child process exits:

    (gdb) c
    Continuing.
    
    Program exited with code 01.
    (gdb) inferior 1
    [Switching to thread 1 (process 25110)]
    

    At this point we are ready to explore the login() function.

    Reversing login()

    The login() function is also fairly big, so let's go through it step by step just as before:

    0x08049867 <login+6>:   mov    eax,DWORD PTR [ebp+0xc]  ; len
    0x0804986a <login+9>:   mov    WORD PTR [ebp-0x2c],ax   ; store len
    0x0804986e <login+13>:  cmp    WORD PTR [ebp-0x2c],0x2  ; compare to 0x2
    0x08049873 <login+18>:  ja     0x8049889 <login+40>     ; make sure len > 2
    

    The first check makes sure the authentication packet body length is greater than 2.

    0x08049889 <login+40>:  mov    DWORD PTR [ebp-0x1c],0x0 ; set pwd to 0
    0x08049890 <login+47>:  mov    eax,DWORD PTR [ebp-0x1c]
    0x08049893 <login+50>:  mov    DWORD PTR [ebp-0x18],eax ; set user to 0
    0x08049896 <login+53>:  mov    eax,DWORD PTR [ebp-0x18]
    0x08049899 <login+56>:  mov    DWORD PTR [ebp-0x14],eax ; set rsrc to 0
    

    Three local variables pwd, user, and rsrc are all initialized to zero.

    0x0804989c <login+59>:  movzx  eax,WORD PTR [ebp-0x2c]  ; len
    0x080498a0 <login+63>:  mov    DWORD PTR [esp+0x8],eax
    0x080498a4 <login+67>:  mov    eax,DWORD PTR [ebp+0x8]  ; auth body
    0x080498a7 <login+70>:  mov    DWORD PTR [esp+0x4],eax
    0x080498ab <login+74>:  lea    eax,[ebp-0x14]           ; rsrc
    0x080498ae <login+77>:  mov    DWORD PTR [esp],eax
    0x080498b1 <login+80>:  call   0x80497fa <get_string>   ; call get_string
    

    The function get_string() gets called with pointers to resource, authentication body, and length as parameters. It looks like get_string() retrieves and populates resource local variable here. What's more interesting is what happens with the returned value:

    0x080498b6 <login+85>:  mov    DWORD PTR [ebp-0x10],eax ; deduct = ret value
    0x080498b9 <login+88>:  mov    eax,DWORD PTR [ebp-0x10] ; load deduct
    0x080498bc <login+91>:  movzx  edx,WORD PTR [ebp-0x2c]  ; len
    0x080498c0 <login+95>:  mov    ecx,edx
    0x080498c2 <login+97>:  sub    cx,ax                    ; len - deduct
    0x080498c5 <login+100>: mov    eax,ecx
    0x080498c7 <login+102>: movzx  edx,ax                   ; expand word to dword
    
    0x080498ca <login+105>: mov    eax,DWORD PTR [ebp-0x10] ; deduct
    0x080498cd <login+108>: add    eax,DWORD PTR [ebp+0x8]  ; *authbody + deduct
    0x080498d0 <login+111>: mov    DWORD PTR [esp+0x8],edx  ; len - deduct
    0x080498d4 <login+115>: mov    DWORD PTR [esp+0x4],eax  ; *authbody + deduct
    0x080498d8 <login+119>: lea    eax,[ebp-0x18]           ; user
    0x080498db <login+122>: mov    DWORD PTR [esp],eax 
    0x080498de <login+125>: call   0x80497fa <get_string>   ; call get_string
    

    The get_string() returns some kind of delta used to increment the pointer to the authentication body as well as decrement the length before repeating the same operation to populate the local variable user.

    0x080498e3 <login+130>: add    DWORD PTR [ebp-0x10],eax ; deduct+= ret value
    0x080498e6 <login+133>: mov    eax,DWORD PTR [ebp-0x10] ; deduct
    0x080498e9 <login+136>: movzx  edx,WORD PTR [ebp-0x2c]  ; len
    0x080498ed <login+140>: mov    ecx,edx
    0x080498ef <login+142>: sub    cx,ax                    ; len - deduct
    0x080498f2 <login+145>: mov    eax,ecx
    0x080498f4 <login+147>: movzx  edx,ax                   ; expand word to dword
    
    0x080498f7 <login+150>: mov    eax,DWORD PTR [ebp-0x10] ; deduct
    0x080498fa <login+153>: add    eax,DWORD PTR [ebp+0x8]  ; *authbody + deduct
    0x080498fd <login+156>: mov    DWORD PTR [esp+0x8],edx  ; len - dedcut
    0x08049901 <login+160>: mov    DWORD PTR [esp+0x4],eax  ; *authbody + deduct
    0x08049905 <login+164>: lea    eax,[ebp-0x1c]           ; pwd
    0x08049908 <login+167>: mov    DWORD PTR [esp],eax
    0x0804990b <login+170>: call   0x80497fa <get_string>   ; call get_string
    0x08049910 <login+175>: add    DWORD PTR [ebp-0x10],eax ; deduct+= ret value
    

    Same operation repeats to retrieve the value stored in the local variable password.

    0x08049913 <login+178>: mov    DWORD PTR [ebp-0xc],0x0  ; success = 0
    
    0x0804991a <login+185>: mov    eax,DWORD PTR [ebp-0x14] ; rsrc
    0x0804991d <login+188>: mov    DWORD PTR [esp+0x4],0x8049f94 ; "net3"
    0x08049925 <login+196>: mov    DWORD PTR [esp],eax
    0x08049928 <login+199>: call   0x8048d28 <strcmp@plt>   ; compare strings
    0x0804992d <login+204>: or     DWORD PTR [ebp-0xc],eax  ; success | return value
    
    0x08049930 <login+207>: mov    eax,DWORD PTR [ebp-0x18] ; user
    0x08049933 <login+210>: mov    DWORD PTR [esp+0x4],0x8049f99 ; "awesomesauce"
    0x0804993b <login+218>: mov    DWORD PTR [esp],eax
    0x0804993e <login+221>: call   0x8048d28 <strcmp@plt>   ; compare strings
    0x08049943 <login+226>: or     DWORD PTR [ebp-0xc],eax  ; success | return value
    
    0x08049946 <login+229>: mov    eax,DWORD PTR [ebp-0x1c] ; password
    0x08049949 <login+232>: mov    DWORD PTR [esp+0x4],0x8049fa6 ; "password"
    0x08049951 <login+240>: mov    DWORD PTR [esp],eax
    0x08049954 <login+243>: call   0x8048d28 <strcmp@plt>   ; compare strings
    0x08049959 <login+248>: or     DWORD PTR [ebp-0xc],eax  ; success | return value
    

    What follows is a series of strings comparisons which set the local variable success to true or false if the three retrieved values resource, user, and password match strings net3, awesomesauce, and password respectively.

    0x0804997d <login+284>: cmp    DWORD PTR [ebp-0xc],0x0  ; compare success with null
    0x08049981 <login+288>: sete   al                       ; set AL if success == 0
    0x08049984 <login+291>: movzx  eax,al                   ; zero expand word to dword
    

    The function login() finishes by checking whether the local variable success is equal to zero and returning 1 if that's the case. This essentially returns the opposite value of success. Recall that in order for the successful string to be returned to the user, the login() must return 1. This odd behavior is the result of strcmp() returning zero when the two strings match, so if all three strings are equal the two OR operations will keep the variable success as zero.

    Reversing get_string()

    The last piece of the puzzle is how the program retrieves the three variables and calculates the return value:

    0x08049800 <get_string+6>:   mov    eax,DWORD PTR [ebp+0x10]  ; len
    0x08049803 <get_string+9>:   mov    WORD PTR [ebp-0x1c],ax    ; store as a word
    0x08049807 <get_string+13>:  mov    eax,DWORD PTR [ebp+0xc]   ; auth body
    0x0804980a <get_string+16>:  movzx  eax,BYTE PTR [eax]        ; retrieve a byte
    0x0804980d <get_string+19>:  mov    BYTE PTR [ebp-0x9],al     ; store byte
    0x08049810 <get_string+22>:  movzx  eax,BYTE PTR [ebp-0x9]    ; expand byte to dword
    0x08049814 <get_string+26>:  cmp    ax,WORD PTR [ebp-0x1c]    ; compare to len
    0x08049818 <get_string+30>:  jbe    0x804982e <get_string+52> ; jump if <=
    

    Looks each string is prepended with a single byte length parameter. A sanity check is performed that the length parameter does not exceed the limits of the authentication body.

    0x0804982e <get_string+52>:  movzx  eax,BYTE PTR [ebp-0x9]    ; string length
    0x08049832 <get_string+56>:  mov    DWORD PTR [esp],eax
    0x08049835 <get_string+59>:  call   0x8048cc8 <malloc@plt>    ; alloc heap chunk
    0x0804983a <get_string+64>:  mov    edx,eax
    0x0804983c <get_string+66>:  mov    eax,DWORD PTR [ebp+0x8]   ; get parameter
    0x0804983f <get_string+69>:  mov    DWORD PTR [eax],edx       ; fill with heap addr
    

    The prepended length parameter is used as the size parameter for the allocated heap chunk. The returned heap address is assigned to one of the passed parameters (user, password, etc.).

    0x08049841 <get_string+71>:  mov    eax,DWORD PTR [ebp+0xc]   ; auth body
    0x08049844 <get_string+74>:  lea    edx,[eax+0x1]             ; *authbody + 1
    0x08049847 <get_string+77>:  mov    eax,DWORD PTR [ebp+0x8]   ; *parameter
    0x0804984a <get_string+80>:  mov    eax,DWORD PTR [eax]       ; heap address
    0x0804984c <get_string+82>:  mov    DWORD PTR [esp+0x4],edx   
    0x08049850 <get_string+86>:  mov    DWORD PTR [esp],eax
    0x08049853 <get_string+89>:  call   0x8048c18 <strcpy@plt>    ; copy string
    

    The null-terminated string is copied from the authentication packet's body to the newly allocated heap chunk that was assigned to the passed parameter.

    0x08049858 <get_string+94>:  movzx  eax,BYTE PTR [ebp-0x9]    ; string length
    0x0804985c <get_string+98>:  add    eax,0x1                   ; +1
    

    The retrieved string length + 1 value are returned back as an offset to chop off from the packet so that the next parameter could be retrieved.

    Authentication packet

    Using the information gathered from reverse engineering the binary we can craft the authentication packet using the following script:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2996
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    resource = "net3"
    resource = pack("B",len(resource)+1) + resource + "\x00"
    
    user = "awesomesauce"
    user = pack("B",len(user)+1) + user + "\x00"
    
    password = "password"
    password = pack("B",len(password)+1) + password + "\x00"
    
    auth = "\x17" + resource + user + password        # auth packet
    len = pack(">H",len(auth)) # packed in network/big endian order
    
    packet = len + auth
    print hexlify(packet)      # print packet in hex
    
    s.sendall(packet)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Here is the output:

    001f17056e657433000d617765736f6d657361756365000970617373776f726400
     ♂!successful
    

    Net4

    The Protostar wargame includes the net4 binary in the /opt/protostar/bin directory; however, there is no mention of it on the website. Let's try to see what this challenge is about.

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

    Oops! Nothing to see here move along =).

    External Links and References

    Special Note

    Thanks to the folks at Exploit Exercises for creating the excellent wargame. Particularly making the exercises highly pedagogical with progressive difficulty and building on previously learned material.

    Published on June 1st, 2014 by iphelix

    sprawlsimilar

    exploit exercises - protostar - final levels

    Exploit Exercises' Protostar wargame includes a number of carefully prepared exercises to help hone your basic exploitation skills. The final portion of the wargame combines Stack, Format String, Heap, and Network exploitation techniques into three excellent challenges to help solidify knowledge gained from previous exercises. Read more.

    exploit exercises - protostar - format string levels

    Exploit Exercises' Protostar wargame includes a number of carefully prepared exercises to help hone your basic exploitation skills. In this walkthrough I will go over the format string exploitation portion of the wargame. Read more.

    exploit exercises - protostar - stack levels

    Exploit Exercises' Protostar wargame includes a number of carefully prepared exercises to help hone your basic exploitation skills. In this walkthrough I will go over the stack exploitation portion of the wargame. Read more.

    exploit exercises - protostar - heap levels

    Exploit Exercises' Protostar wargame includes a number of carefully prepared exercises to help hone your basic exploitation skills. In this walkthrough I will go over the heap exploitation portion of the wargame. Read more.


    sprawlcomments

    All original content on this site is copyright protected and licensed under Creative Commons - Attribution, NonCommercial, ShareAlike 4.0 International.

    π
    ///\oo/\\\