Security Forem

Hitanshu Gedam
Hitanshu Gedam

Posted on

How I Learned Syscalls by Building a Web Server on pwn.college

From Zero to Web Server

No full solutions here. Just the journey, the lessons, and the honest truth.

A Note on Learning (and Honesty)

Before I go any further: I'm not going to paste my solutions in this post.

pwn.college is a learning platform. The challenges are meant to be solved, not copied. If I just dumped my assembly code here, I'd be robbing someone else of the chance to struggle, fail, debug, and eventually feel that incredible rush when the checker program finally says PASS.

Also, I want to be completely transparent. Out of the 11 challenges in this module, there were fewer than 5 where I got so stuck that I reached for help from an AI. Not to generate full solutions, but to explain a syscall I didn't understand, or to help me reason through why something was failing. I still wrote every line of assembly myself. And every time I got help, I made sure I understood why the fix worked before moving on.

The rest, the majority, I solved on my own, using strace, gdb, the man pages, and a lot of trial and error.

Why am I telling you this? Because pretending I never needed help would be a lie. Getting stuck is normal. Asking for help, as long as you actually learn from it, is part of the process too. The goal isn't to be "pure." The goal is to understand.

And I understand this material now. That's what matters.

So instead of giving you code, I'm going to tell you what I learned. The concepts. The syscalls. The mistakes. The "aha!" moments. If you're working through the same dojo, this post will point you in the right direction, but you'll still have to do the work yourself.

Before the Web Server

Before I ever wrote a single line of HTTP response in assembly, I had to learn how computers actually work.

pwn.college's Computing 101 dojo isn't gentle. It throws you into the deep end and expects you to swim. Before reaching the "Building a Web Server" module, I completed eight modules in order:

  • Your First Program (5 challenges), How to make a program exit. Syscall 60, if you're counting.
  • Computer Memory (7 challenges), Pointers are just numbers. Memory is just bytes.
  • The Stack (4 challenges), Push, pop, call, ret, how functions really work.
  • Software Introspection (12 challenges), strace, ltrace, gdb, watching programs from the outside.
  • Output and Input (6 challenges), read and write are all you need.
  • Control Flow (7 challenges), Jumps, compares, loops, the logic of everything.
  • Assembly Assortment (4 challenges), Bitwise ops, shifts, condition codes.
  • Assembly Crash Course (30 challenges), Pure x86-64 assembly. 30 of them.

Total before the web server: 75 assembly programs.

By the time I reached "Building a Web Server," I had stared at register values until my eyes hurt. I had learned that mov is not a copy, it's a transfer. I had earned the right to be confused, stuck, and then unstuck.

So when I started the web server module, I wasn't starting from zero. I was starting from "I understand the stack, I understand syscalls, I understand that nothing is handed to me."

And I still spent a lot of time on it.

The Web Server Module: 11 Challenges

Here's the journey, what each challenge taught me, without giving away the actual code.

Challenge 1: Exit

What I had to do: Write a program that calls the exit syscall with status 0.

What I learned: Every program needs an exit. The kernel doesn't know you're done unless you tell it. The syscall convention on x86-64 Linux is: syscall number in rax, first argument in rdi, then syscall. That's the foundation everything else builds on.

Where I got stuck: Nowhere on this one. It's the warm-up.

Challenge 2: Socket

What I had to do: Create a TCP socket for IPv4.

What I learned: You can't just write AF_INET and SOCK_STREAM in assembly, those are C macros. You have to find the actual integer values. I learned to grep through /usr/include to find them. Turns out AF_INET is 2 and SOCK_STREAM is 1. The socket syscall returns a file descriptor that you'll use for everything else.

Where I got stuck: Nothing major. But it made me appreciate what C preprocessors actually do.

Challenge 3: Bind

What I had to do: Attach my socket to port 80 so clients could find it.

What I learned: bind takes a pointer to a sockaddr_in structure, 16 bytes of raw memory that you have to construct yourself. I learned what each field means: address family (2 bytes), port (2 bytes in network byte order, big-endian), IP address (4 bytes), and padding (8 bytes). Endianness matters: port 80 (0x0050) becomes 0x5000 when stored in memory.

Where I got stuck: This was my first real wall. I kept getting bind failures because I had the port byte order wrong. strace and the bind man page eventually saved me.

Challenge 4: Listen

What I had to do: Turn my bound socket into a passive listener.

What I learned: A socket created with socket() is "active", it expects to initiate connections. listen() makes it "passive" so it can receive incoming connections. The backlog parameter tells the kernel how many pending connections to queue.

Where I got stuck: I initially forgot that listen needs to be called after bind but before accept. My program hung forever until I looked up the correct order.

Challenge 5: Accept

What I had to do: Wait for a client to connect and get a new file descriptor for that client.

What I learned: accept blocks, it puts your program to sleep until someone connects. That's actually good, the kernel handles the waiting efficiently. When a client connects, accept returns a new file descriptor just for talking to that client. The original listening socket stays open for more connections.

Where I got stuck: I accidentally overwrote my listening socket fd with the client fd and lost the ability to accept more connections. Had to carefully separate my register usage.

Challenge 6: Static Response

What I had to do: Send a fixed HTTP response ("HTTP/1.0 200 OK\r\n\r\n") to any client that connects.

What I learned: This is where assembly stops being abstract. You can't just write printf(...). You have to put those bytes in memory yourself, one byte at a time. I also learned that HTTP uses \r\n for line endings, and a blank line (\r\n\r\n) separates headers from body.

Where I got stuck: Counting bytes. I miscounted the response length and the checker failed me because the response was truncated. Staring at hex dumps fixed it.

Challenge 7: Dynamic Response

What I had to do: Parse the GET request, extract the file path, open that file, read its contents, and send them back.

What I learned: Parsing HTTP manually means scanning byte by byte. Find the space after "GET", find the next space after the path, null-terminate the path string. Then open with O_RDONLY, read the file into a buffer, and write the header plus file contents.

Where I got stuck: Off-by-one errors in finding the spaces. Also forgot to null-terminate the path string at first, so open was getting garbage after the filename.

Challenge 8: Iterative GET Server

What I had to do: Keep the server running after one request, handling multiple clients sequentially.

What I learned: One infinite loop. After handling a client and closing its fd, just jump back to accept. The server stays alive forever. This is called an iterative server, one client at a time.

Where I got stuck: I forgot to close the client fd at the end of the loop. File descriptors leaked and eventually the server couldn't accept new connections.

Challenge 9: Concurrent GET Server

What I had to do: Handle multiple clients at the same time using fork().

What I learned: fork() creates an exact copy of the running process. The parent gets the child's PID; the child gets 0. Parent closes the client fd and goes back to accept. Child closes the listening socket and handles the request. Classic Unix pattern: parent listens, child handles.

Where I got stuck: Figuring out which process closes which file descriptor. Parent should never touch the request. Child should never call accept. Getting this separation right took a few tries.

Challenge 10: Concurrent POST Server

What I had to do: Handle POST requests by extracting the body and writing it to a file.

What I learned: POST requests have a body after the headers. To find it, scan for \r\n\r\n, the blank line that separates headers from body. Calculate body length = total bytes read minus header size. Open the file with O_WRONLY | O_CREAT (flags 1 and 64 combined = 65) and write the body bytes.

Where I got stuck: This was the hardest challenge. The body parsing logic was tricky, scanning for four bytes in a row. I also kept miscalculating the body length. And there was a specific requirement from the checker about closing (or not closing) the client socket that took me a while to discover.

Challenge 11: Web Server

What I had to do: Combine GET and POST into a single concurrent server.

What I learned: Check the first byte of the request: 'G' means GET, 'P' means POST. Branch to the right handler. Both send 200 OK when done. Both run inside fork(). I moved the 200 OK response to the .rodata section so I wasn't rebuilding it every time.

Where I got stuck: Making sure the parent and child didn't step on each other. Clear separation of responsibilities was the key. By this point, I had enough confidence from the previous 10 challenges to put it all together myself.

After the Web Server: Debugging Refresher

After building the web server, I completed Debugging Refresher (8 challenges). This module taught me how to properly inspect what I had built:

  • strace to trace every syscall my server made to the kernel. Incredibly useful for seeing exactly where something failed.
  • gdb for breakpoints, stepping through instructions, inspecting registers and memory.
  • ltrace for library calls (though my server made none, pure syscalls only).

Without debugging skills, assembly is blind. With them, you can see everything.

What I Actually Learned

The most important lesson

I spent a lot of time on these challenges. I don't remember every instruction I wrote. But I remember this: I can figure things out and make them work.

That's not arrogance. That's earned confidence. Before pwn.college, I wasn't sure I could write anything meaningful in assembly. Now I know I can build a concurrent web server from scratch, no libc, no runtime, just me and the kernel.

Once you've done that, everything else feels possible.

What the previous modules gave me

  • Syscall convention: number in rax, arguments in rdi, rsi, rdx, then r10, r8, r9
  • Stack discipline: sub rsp, N to allocate, add rsp, N to deallocate
  • Register preservation: rbx, r12-r15 survive function calls
  • Debugging: gdb and strace are your eyes into a running program

What the web server module taught me

  • Socket syscalls create network endpoints
  • HTTP is just text over TCP, parsed byte by byte
  • fork() is concurrency, simple, reliable, and ancient
  • \r\n\r\n is the most important 4-byte sequence in HTTP
  • File descriptors are just integers, and they get copied on fork()
  • Nothing is handed to you, but everything is possible

On getting help (the honest version)

Using AI on a few challenges didn't give me the answers, it gave me direction. I still wrote the code. I still understood why it worked. And I made sure I could explain the solution in my own words before moving on.

I think that's the right way to use AI in learning: as a tutor, not a crutch. Ask it to explain a concept, not to write the code for you. The difference matters.

The Full Journey (94 Challenges)

Before the web server (75 challenges):

  • Your First Program (5)
  • Computer Memory (7)
  • The Stack (4)
  • Software Introspection (12)
  • Output and Input (6)
  • Control Flow (7)
  • Assembly Assortment (4)
  • Assembly Crash Course (30)

The web server (11 challenges):

  • Exit → exit syscall
  • Socket → socket syscall, finding AF_INET and SOCK_STREAM
  • Bind → bind syscall, manual sockaddr_in, endianness
  • Listen → listen syscall
  • Accept → accept syscall
  • Static Response → hardcoded write, byte-by-byte strings
  • Dynamic Response → open, read, file serving
  • Iterative GET Server → infinite loop
  • Concurrent GET Server → fork
  • Concurrent POST Server → body parsing, open with O_CREAT
  • Web Server → GET + POST + concurrency

After the web server (8 challenges):

  • Debugging Refresher (8)

Total: 94 challenges. One dojo. One working web server in assembly.

If You Build Systems That Actually Matter

I don't know who's reading this. But if you work on operating systems, embedded devices, aerospace or defense software, cybersecurity tooling, or anything where "it just works" isn't good enough, you need "I understand exactly why it works", then you know why this matters.

I built this because I wanted to understand. Now I do.

Try It Yourself

The Computing 101 dojo is free on pwn.college. Start with "Your First Program." See how far you get.

If you get stuck, and you will, don't look for full solutions. Use strace. Use gdb. Read the man pages. Figure it out. That's where the learning happens.

And if you're truly stuck after genuinely trying? Ask for help, but make sure you learn from it. That's what I did.

Acknowledgments

pwn.college and Arizona State University for building this. The checker program for never lying to me. The 75 assembly programs before this one that made it possible. And the AI tutor I asked for help on fewer than 5 challenges, not for answers, but for explanations that unblocked me.

Some resources they recommend:


I built this because I wanted to understand. Now I do.
VENI. VIDI. VICI.
AD MELIORA!

Here's my LinkedIN if you wanna connect!

Top comments (0)