Assembly and Debugging Refresher


CSE 466 - Fall 2026.

A focused review of the assembly skills and program understanding established in CSE365, designed to give you a feel for the level of difficulty of this course.

Only a few core challenges are included here for credit to avoid a daunting first module, but each of the modules here cover material that should be review with a few additions to get you started in CSE466.



Your First Program

The CPU thinks in very simple terms. It moves data around, changes data, makes decisions based on data, and takes action based on data. Most of the time, this data is stored in registers.

Simply put, registers are containers for data. The CPU can put data into registers, move data between registers, and so on. These registers, at a hardware level, are implemented using very expensive chips, crammed into shockingly microscopic spaces, and accessed at a frequency where even physical concepts such as the speed of light impact their performance. Hence, the number of registers that a CPU can have is extremely constrained. Different CPU architectures have different amounts of registers, different names for these registers, and so on, but typically, there are between 10 and 20 "general purpose" registers that program code can use for any reason, and up to a few dozen other ones that are used for special purposes.

In x86's modern incarnation, x86_64, programs have access to 16 general purpose registers. In this challenge, we will learn about our first one: rax. Hi, Rax!

rax, a single x86 register, is a tiny piece of the massively complex design of the x86 CPU, but this is where we'll start. Like the other registers, rax is a container for a small amount of data. You move data into rax with the mov instruction. Instructions are specified as an operator (in this case, mov), and operands, which represent additional data (in this case, it will be the specification of rax as a destination, and the value we will want to store there).

For example, if you wanted to store the value 1337 into rax, the x86 Assembly would look like:

mov rax, 1337

You can see a few things:

  1. The destination (rax) is specified before the source (the value 1337).
  2. The operands are separated by a comma.
  3. It is really simple!

In this challenge, you will write your first assembly. You must move the value 60 into rax. Write your program in a file with a .s extension, such as rax-challenge.s (while not mandatory, .s is the typical extension for assembly files). Pass that .s file to the checker as an argument:

hacker@dojo:~$ /challenge/check rax-challenge.s

The .s file is input to the checker; do not try to run it directly. You can use either your favorite text editor or the text editor in pwn.college's VSCode Workspace to implement your .s file!


ERRATA: If you've seen x86 assembly before, there is a chance that you've seen a slightly different dialect of it. The dialect used in pwn.college is "Intel Syntax", which is the correct way to write x86 assembly (as a reminder, Intel created x86). Some courses incorrectly teach the use of "AT&T Syntax", causing enormous amounts of confusion. We'll touch on this slightly in the next module and then, hopefully, never have to think about AT&T Syntax again.

So, your first program crashed... Don't worry, it happens! In this challenge, you'll learn how to make your program cleanly exit instead of crashing.

Starting your program and cleanly stopping it are actions handled by your computer's Operating System. The operating system manages the existence of programs and interactions between the programs, your hardware, the network environment, and so on.

Your programs "interact" with the CPU using assembly instructions such as the mov instruction you wrote earlier. Similarly, your programs interact with the operating system (via the CPU, of course) using the syscall, or System Call instruction.

Like how you might use a phone call to interact with a local restaurant to order food, programs use system calls to request the operating system to carry out actions on the program's behalf. As a bit of an overgeneralization, anything your program does that doesn't involve performing computation on data is done with a system call.

There are a lot of different system calls your program can invoke. For example, Linux has around 330 different ones, though this number changes over time as syscalls are added and deprecated. Each system call is indicated by a syscall number, counting upwards from 0, and your program invokes a specific syscall by moving its syscall number into the rax register and invoking the syscall instruction. For example, if we wanted to invoke syscall 42 (a syscall that you'll learn about sometime later!), we would write two instructions:

mov rax, 42
syscall

Very cool, and super easy!

In this challenge, we'll learn our first syscall: exit. The exit syscall causes a program to exit. By explicitly exiting, we can avoid the crash we ran into with our previous program!

Now, the syscall number of exit is 60. Go and write your first program: it should move 60 into rax, then invoke syscall to cleanly exit!

As you might know, every program exits with an exit code as it terminates. This is done by passing a parameter to the exit system call.

Similarly to how a system call number (e.g., 60 for exit) is specified in the rax variable, parameters are also passed to the syscall through registers. System calls can take multiple parameters, though exit takes only one: the exit code. The first parameter to a system call is passed via another register: rdi. rdi is what we will focus on in this challenge.

In this challenge, you must make your program exit with the exit code of 42. Thus, your program will need three instructions:

  1. Set your program's exit code (move it into rdi).
  2. Set the system call number of the exit syscall (mov rax, 60).
  3. syscall!

Now, go and do it!

So you've written your first program? But until now, we've handled the actual building of it into an executable that your CPU can actually run. In this challenge, you will build it!

To build an executable binary, you need to:

  1. Write your assembly in a file (often with a .S or .s syntax. We'll use program.s in this example).
  2. Assemble your assembly file into an object file (using the as command).
  3. Link one or more executable object files into a final executable binary (using the ld command)!

Let's take this step by step:

Writing assembly.
The assembly file contains, well, your assembly code. For the previous level, this might be:

hacker@dojo:~$ cat program.s
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$

But it needs to contain just a tad more info. We mentioned that we're using the Intel assembly syntax in this course, and we'll need to let the assembler know that. You do this by prepending a directive to the beginning of your assembly code, as such:

hacker@dojo:~$ cat program.s
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$

.intel_syntax noprefix tells the assembler that you will be using Intel assembly syntax, and specifically the variant of it where you don't have to add extra prefixes to every instruction. It isn't actually an x86 instruction (like mov and syscall), and so it doesn't end up in our final executable binary or runs on the CPU. We'll talk about other directives later, but for now, we'll let the assembler figure it out!

Assembling Assembly Code into Object Files.
Next, we'll assemble the code. This is done using the assembler, as, as so:

hacker@dojo:~$ ls
program.s
hacker@dojo:~$ cat program.s
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o program.o program.s
hacker@dojo:~$ ls
program.o   program.s
hacker@dojo:~$

Here, the as tool reads in program.s, assembles it into binary code, and outputs an object file called program.o. This object file has actual assembled binary code, but it is not yet ready to be run. First, we need to link it.

Linking Object Files into an Executable.
In a typical development workflow, source code is compiled and assembly is assembled to object files, and there are typically many of these (generally, each source code file in a program compiles into its own object file). These are then linked together into a single executable. Even if there is only one file, we still need to link it, to prepare the final executable. This is done with the ld (stemming from the term "link editor") command, as so:

hacker@dojo:~$ ls
program.o   program.s
hacker@dojo:~$ ld -o program program.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
hacker@dojo:~$ ls
program.o   program.s   program
hacker@dojo:~$

This creates an program file that we can then run! Here it is:

hacker@dojo:~$ ./program
hacker@dojo:~$ echo $?
42
hacker@dojo:~$

In the shell, $? holds the exit code of the last executed command.

Neat! Now you can build programs. In this challenge, go ahead and run through these steps yourself. Build your executable, and pass it to /challenge/check for the flag!


_start?
The attentive learner might have noticed that ld prints a warning about entry symbol _start. The _start symbol is, essentially, a note to ld about where in your program execution should begin when the ELF is executed. The warning states that, absent a specified _start, execution will start right at the beginning of the code. This is just fine for us!

If you want to silence the error, you can specify the _start symbol, in your code, as so:

hacker@dojo:~$ cat program.s
.intel_syntax noprefix
.global _start
_start:
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o program.o program.s
hacker@dojo:~$ ld -o program program.o
hacker@dojo:~$ ./program
hacker@dojo:~$ echo $?
42
hacker@dojo:~$

There are two extra lines here. The second, _start:, adds a label called start, pointing to the beginning of your code. The first, .global _start, directs as to make the _start label globally visible at the linker level, instead of just locally visible at the object file level. As ld is the linker, this directive is necessary for the _start label to be seen.

For all the challenges in this dojo, starting execution at the beginning of the file is just fine, but if you don't want to see those warnings pop up, now you know how to prevent them!

Okay, let's learn about one more register: rsi! Like rdi, rsi is a place you can park some data. For example:

mov rsi, 42

Of course, you can also move data around between registers! Watch:

mov rsi, 42
mov rdi, rsi

Just like the first line there moves 42 into rsi, the second line moves the value in rsi to rdi. Here, we have to mention one complication: by move, we really mean set. After the snippet above, rsi and rdi will be 42. It's a mystery as to why the mov was chosen rather than something reasonable like set (even very knowledgeable people resort to wild speculation when asked), but it was, and here we are.

Anyways, on to the challenge! In this challenge, we will store a secret value in the rsi register, and your program must exit with that value as the return code. Since exit uses the value stored in rdi as the return code, you'll need to move the secret value in rsi into rdi. Run /challenge/check and pass it your code for the flag! /challenge/check will set the secret value in rsi before running your code. Good luck!


Computer Memory

You look like you need just a tiny bit more practice. In this level, we put the secret value at 123400 instead of 133700, as so:

  Address │ Contents
+────────────────────+
│ 123400  │ ???      │
+────────────────────+

Go load it into rdi and exit with that as the exit code!

Did you prefer to access memory at 133700 or at 123400? Your answer might say something about your personality, but it's not super relevant from a technical perspective. In fact, in most cases, you don't deal with actual memory addresses when writing programs at all!

How is this possible? Well, typically, memory addresses are stored in registers, and we use the values in the registers to point to data in memory! Let's start with this memory configuration:

  Address │ Contents
+────────────────────+
│ 133700  │ 42       │
+────────────────────+

And consider this assembly snippet:

mov rax, 133700

Now, what you have is the following situation:

    Address │ Contents
  +────────────────────+
┌▸│ 133700  │ 42       │
│ +────────────────────+
│
└────────────────────────┐
                         │
   Register │ Contents   │
  +────────────────────+ │
  │ rax     │ 133700   │─┘
  +────────────────────+

rax now holds a value that corresponds with the address of the data that we want to load! Let's load it:

mov rdi, [rax]

Here, we are accessing memory, but instead of specifying a fixed address like 133700 for the memory read, we're using the value stored in rax as the memory address. By containing the memory address, rax is a pointer that points to the data we want to access! When we use rax in lieu of directly specifying the address that it stores to access the memory address that it references, we call this dereferencing the pointer. In the above example, we dereference rax to load the data it points to (the value 42 at address 133700) into rdi. Neat!

This also drives home another point: these registers are general purpose! Just because we've been using rax as the syscall index in our challenges so far doesn't mean that it can't have other uses as well. Here, it's used as a pointer to our secret data in memory.

Similarly, the data in the registers doesn't have an implicit purpose. If rax contains the value 133700 and we write mov rdi, [rax], the CPU uses the value as a memory address to dereference. But if we write mov rdi, rax in the same conditions, the CPU just happily puts 133700 into rdi. To the CPU, data is data; it only becomes differentiated when it's used in different ways.

In this challenge, we've initialized rax to contain the address of the secret data we've stored in memory. Dereference rax to load the secret data into rdi and use it as the exit code of the program to get the flag!

So now you can dereference pointers in memory like a pro! But pointers don't always point directly at the data you need. Sometimes, for example, a pointer might point to a collection of data (say, an entire book), and you'll need to reference partway into this collection for the specific data you need.

For example, if your pointer (say, rdi) points to a sequence of numbers in memory, as so:

    Address │ Contents
  +────────────────────+
┌▸│ 133700  │ 50       │
│ │ 133701  │ 42       │
│ │ 133702  │ 99       │
│ │ 133703  │ 14       │
│ +────────────────────+
│
└────────────────────────┐
                         │
   Register │ Contents   │
  +────────────────────+ │
  │ rdi     │ 133700   │─┘
  +────────────────────+

If you want the second number of that sequence, you could do:

mov rax, [rdi+1]

Wow, super simple! In memory terms, we call these number slots bytes: each memory address represents a specific byte of memory. The above example is accessing memory 1 byte after the memory address pointed to by rdi. In memory terms, we call this 1 byte difference an offset, so in this example, there is an offset of 1 from the address pointed to by rdi.

Let's practice this concept. As before, we will initialize rdi to point at the secret value, but not directly at it. This time, the secret value will have an offset of 8 bytes from where rdi points, something analogous to this:

    Address │ Contents
  +────────────────────+
┌▸│ 31337   │ 0        │
│ │ 31337+1 │ 0        │
│ │ 31337+2 │ 0        │
│ │ 31337+3 │ 0        │
│ │ 31337+4 │ 0        │
│ │ 31337+5 │ 0        │
│ │ 31337+6 │ 0        │
│ │ 31337+7 │ 0        │
│ │ 31337+8 │ ???      │
│ +────────────────────+
│
└────────────────────────┐
                         │
   Register │ Contents   │
  +────────────────────+ │
  │ rdi     │ 31337    │─┘
  +────────────────────+

Of course, the actual memory address is not 31337. We'll choose it randomly, and store it in rdi. Go dereference rdi with offset 8 and get the flag!


Output and Input

So far, your program has only interacted with stdin and stdout, but what about files on disk? To access a file, you first need to open it using the open system call.

The open system call (syscall number 2) takes a pointer to a filename string and returns a brand-new file descriptor referring to that file:

open("/flag", 0);

The second argument specifies additional modes and permissions for the file, but 0 requests the default: read-only.

The registers for open follow the same convention:

Register Purpose
rax 2 (syscall number for open)
rdi pointer to the filename string in memory
rsi 0 (read-only)

When open returns, rax contains the new file descriptor (fd) number. Recall that file descriptor 0 is stdin, file descriptor 1 is stdout, and file descriptor 2 is stderr. Other files that are open are just represented by other file descriptors, incrementing from 3 onwards! You'll use this fd as the first argument to read, just like you did for stdin earlier, but this time read will read from your file.

How to load the filename into memory? In this level, the path to the flag (/flag) will be passed as the first argument to your program. You already know how to load that: mov rdi, [rsp+16].

Your program should:

  1. Load a pointer to the filename (stored at [rsp+16], the first argument) into rdi
  2. Specify the default of read access for the second argument (set rsi to 0).
  3. open it (syscall 2)
  4. read from the returned fd into memory. The fd open returned is in rax; move it to rdi for read's first argument (do this before you set the syscall number for write!). Read a comfortably large count --- the flag is shorter.
  5. write to stdout exactly the number of bytes read returned (mov rdx, rax, just like in read-exact)
  6. exit with code 42 (syscall 60)

DEBUGGING: Having trouble? Use strace to see your system calls in action --- it will show you exactly what arguments each syscall receives and what it returns. If open is returning -1, double-check your filename pointer. If read returns 0, the file descriptor from open might be wrong.


Control Flow

So far, your programs have been fairly straightforward: move some values around, read from memory, and invoke a system call. But real programs need to make decisions: "if this condition is true, do one thing; otherwise, do something else." This is the foundation of control flow, and it starts with being able to compare values.

In x86 assembly, comparisons are done with the cmp instruction. cmp compares two values by subtracting the second operand from the first. Crucially, cmp doesn't store the result of the subtraction anywhere you can see directly. Instead, it updates the CPU's internal flags based on what the result looked like.

For example:

cmp rdi, 42

This internally computes rdi - 42, but rdi is not modified. Instead, the CPU sets a special bit called the Zero Flag (ZF): if the result of the subtraction was zero (meaning the two values were equal), ZF is set to 1. If rdi contains 42, then 42 - 42 = 0, and ZF becomes 1. If rdi contains anything else, the result is non-zero, and ZF becomes 0.

Great, so after cmp, the CPU knows whether the values were equal. But how do we actually use that information?

We can't directly mov the flags into a register. Instead, x86 provides a family of "set on condition" instructions that write a 0 or 1 to a byte-sized destination based on the current flags.

The one we'll use here is setz ("Set if Zero"):

setz dil

This checks the Zero Flag and:

  • If ZF = 1 (the values were equal, i.e., the subtraction result was zero), it writes 1 to dil.
  • If ZF = 0 (the values were not equal), it writes 0 to dil.

Simple: 1 means "yes, they matched!" and 0 means "no, they didn't." There's also a complementary instruction, setnz ("Set if Not Zero"), which does the opposite, but we won't need it here.

But what is dil? So far, you've worked with 64-bit registers like rdi, rax, and rsp. The setz instruction, however, only writes a single byte (8 bits). Luckily, you can access smaller portions of the full 64-bit registers. For rdi:

  • rdi is the full 64 bits
  • dil is just the lowest 8 bits --- the low byte of rdi

When you write setz dil, you're putting a 0 or 1 into just the lowest byte of rdi, leaving the upper bytes unchanged. rdi is the value passed to the exit system call, but Linux reports only that value's low 8 bits as the process's exit status. That is why changing only dil is enough here: the visible status becomes 1 (equal!) or 0 (not equal!), regardless of the upper bytes of rdi.

One more thing about cmp: it can compare a register with an immediate (cmp rdi, 42) or even a memory location with an immediate (cmp QWORD PTR [rsp], 42). But it cannot compare two memory locations at once --- at most one operand can be a memory dereference.

Now, your challenge: recall from the Stack module that [rsp] contains argc --- the number of command-line arguments passed to your program, including the program name. Write a program that:

  1. Compares argc with 42 (whether by first moving argc into a register or comparing against the memory directly).
  2. Uses setz dil to set the exit code: 1 if argc equals 42, 0 otherwise.
  3. Exits.

So far, every program you've written has been a complete executable: it starts at _start, runs from there, and exits with a syscall. In this challenge, your code will be a single function inside a shared library, not a standalone executable.

A shared library (called a .so file on Linux) is a chunk of compiled code that some other program loads at runtime and calls into. Typically, such libraries perform utility functions, such as parsing image files (e.g., libpng parses PNG files) or handling general system-facing tasks (libc provides a lot of memory management, file management, and system interaction code). Deep inside, the actual interaction with the operating system takes place using system calls, but libraries provide a better interface to interact with than raw system calls.

This challenge plays the role of a program that loads your library (using libc's dlopen functionality), looks up your function by name, and calls it with arguments. In Computer Science nomenclature, your code is the callee and the challenge is the caller.

The call instruction.
How does the grader get into your code in the first place? It executes a new instruction you haven't met yet: call.

call <target> is x86's function-call instruction. It does two things:

  1. Pushes the address of the next instruction after the call instruction (the return address) onto the stack.
  2. Jumps to <target>.

In our case, the grader runs the equivalent of call solve, and execution lands at the top of your solve function. You don't have to do anything special to "receive" the call --- you just start running.

For this first challenge, you also don't have to do anything special to finish the call. We'll deal with the saved return address in the next challenge; for now, just end your code with the exit syscall you already know. This is the same shape as every program you've written so far --- the only thing that has changed is who started executing you.

Writing the function.
Your assembly should look like this:

.intel_syntax noprefix
.global solve
solve:
    <your code, ending in an exit syscall>

The .global solve line tells the assembler "expose this code so other code can find it" --- just like .global _start did for executables back in the building level. The solve: label actually specifies where the code is.

Building a shared library.
You already know how to assemble a program with as and link it with ld. To produce a shared library instead of an executable, pass -shared to ld:

hacker@dojo:~$ as -o your-solve.o your-solve.s
hacker@dojo:~$ ld -shared -o your-solve.so your-solve.o

Then submit the .so to the grader:

hacker@dojo:~$ /challenge/check your-solve.so

The calling convention.
When the grader calls solve, it passes arguments in registers. In the case of this challenge, your solve function takes two arguments:

Register Role on entry
rdi First argument (a pointer to a buffer of bytes)
rsi Second argument (the length of that buffer)

You've already seen rdi used to hold the first argument of a syscall (the exit code, a file descriptor, etc.). That's because Linux syscalls and Linux functions use the same convention for the first few argument registers.

For this challenge, the challenge will pass you your flag as the buffer, with the flag's length in rsi. Write the rsi bytes starting at rdi to file descriptor 1 (stdout) using the write syscall (just like before!), and then exit the process cleanly with code 0. Get it right, and your solve will print your flag for you!


Hint: Keep in mind that write() takes arguments in the order of: file descriptor (1 in rdi for stdout), buffer (pointer to memory, in rsi), and size (in rdx). This is different from the arguments your function will be called with, so you'll need to move some stuff around!

Debugging your solution. Since your code is a function inside a shared library, there's no entry point to launch under gdb directly --- but you can give it one. Add a tiny _start to your code that fakes the grader's call: point rdi at a stand-in buffer, set rsi to its length, and call solve. Now you can step through your logic in plain gdb, with no flag and no privileges needed:

.global _start
_start:
    push 0x41414141   // put four 'A' bytes (0x41) on the stack to stand in for the flag
    mov rdi, rsp      // first argument: a pointer to those bytes
    mov rsi, 4        // second argument: how many bytes to print
    int3              // optional: gdb breaks here without setting a breakpoint
    call solve        // your solve runs, prints the bytes, and exits on its own

Assemble and link it as a normal executable (no -shared --- this version has an entry point), then load it in gdb:

hacker@dojo:~$ as -o debug.o debug.s
hacker@dojo:~$ ld -o debug debug.o
hacker@dojo:~$ gdb ./debug
(gdb) run

Execution stops at your int3; step through with the techniques from Software Introspection, watching the registers and the buffer. If your solve is correct, this prints AAAA --- and the same logic will print your real flag when you submit the .so to the grader.

You can also debug the native harness directly with stand-in bytes instead of the flag. /challenge/check is the Python checker script, so do not load it as the executable in gdb. The native program that loads your .so is /challenge/harness:

hacker@dojo:~$ gdb --args /challenge/harness your-solve.so
(gdb) run

The checker will run that same harness shape with the real flag when you submit your .so.


Endian Escapades

You have seen that byte, word, dword, and qword describe how many bytes an instruction reads or writes. Now add one more wrinkle: a smaller value can be copied into a larger register as either unsigned or signed.

If the byte is unsigned, filling the high bytes with zero is fine: 0x7f becomes 0x000000000000007f. But a signed byte uses two's complement. The byte 0xff is -1, so extending it to 64 bits must fill the new high bits with 1s: 0xffffffffffffffff.

That is sign extension. It copies the sign bit, not zeroes, into the new high bits. On x86-64, the form you need here is:

movsx rax, BYTE PTR [rdi]

This reads one byte from the address in rdi, treats that byte as signed, and returns the 64-bit signed value in rax.

Write a function called solve that takes a pointer to one byte in rdi. Load that byte as a signed 8-bit value, sign-extend it to 64 bits, return it in rax, and export it with .global solve.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

You just read how x86 stores multi-byte values little-endian --- low byte first. Time to use it: /challenge/reverse-me hides an 8-character password in a single qword, deep in its own code.

It loads your input and compares all 8 bytes at once against a hard-coded value:

movabs rbx, 0x4847464544434241
mov    rax, [rdi]
cmp    rax, rbx
jne    fail

(movabs is new, but it's just a mov: a normal mov's immediate maxes out at 32 bits, so the assembler uses this wider form --- "move absolute" --- when the constant fills all 64. Read it as a mov.)

That immediate is the password as the CPU read it from memory --- little-endian, so its bytes are the characters in reverse:

0x4847464544434241  ->  bytes 48 47 46 45 44 43 42 41  (high to low, as printed)
                    ->  low byte first: 41 42 43 44 45 46 47 48  ->  "ABCDEFGH"

Disassemble it, read that one movabs immediate, reverse its eight bytes into the password, and run it:

hacker@dojo:~$ objdump -d -M intel /challenge/reverse-me
hacker@dojo:~$ /challenge/reverse-me YOUR_PASSWORD_HERE

WARNING: /challenge/reverse-me is a SUID binary, so debugging it drops its privileges and the open("/flag") inside will silently fail under gdb. Use objdump to read it, but run it directly to get the flag.

Real programs rarely read a buffer at one uniform size. They read structs: a handful of fields of different sizes, laid out one after another in memory (in fact, struct is structure for short).

This /challenge/reverse-me treats your password as a struct. You might not know the C programming language, but if you did, this is what the structure would be defined as:

struct { uint64_t a; uint32_t b; uint16_t c; uint8_t d; uint8_t e; };

The disassembly loads each field at its own width and offset:

movabs rbx, 0x................   ; a: 8-byte field at +0
mov    rax, [rdi+0]
cmp    rax, rbx
mov    eax, [rdi+8]               ; b: 4-byte field at +8
cmp    eax, 0x........
mov    ax,  [rdi+12]              ; c: 2-byte field at +12
cmp    ax,  0x....
mov    al,  [rdi+14]              ; d: 1-byte field at +14
cmp    al,  0x..
mov    al,  [rdi+15]              ; e: 1-byte field at +15
cmp    al,  0x..

This is the whole module in one challenge. For each field, read three things off the access: its width (from rax/eax/ax/al), its offset ([rdi+X]), and its value (endian-correct the immediate according to the field's width). Reassemble the fields in offset order and you have the password.

hacker@dojo:~$ objdump -d -M intel /challenge/reverse-me
hacker@dojo:~$ /challenge/reverse-me YOUR_PASSWORD_HERE

WARNING: /challenge/reverse-me is a SUID binary, so debugging it drops its privileges and the open("/flag") inside will silently fail under gdb. Use objdump to read it, but run it directly to get the flag.


The Stack, Revisited

In addition to storing scratch data and return addresses, the stack stores the local variables of functions: data they use for functionality that's not necessarily needed by other functions of a program. In security situations where a hacker gets ``code execution'' inside a process, these variables are an open book: there is nothing preventing code in a process from reading data from all over the stack!

This challenge explores this concept. Once again, you write a solve function that the challenge calls, but the challenge passes you no arguments. Instead, the challenge's caller function has stored the flag in its own local variables before calling you. You have to reach over into the caller's "frame" (what we call the part of the stack including a function's local variables and the saved return address to which it will return) and grab those bytes.

Wait, what?
Let's walk through why this is possible. In this challenge, the main function calls the caller function, which then calls your solve function. Right before the challenge's caller function executed call solve, the stack looked like this:

                                          [smaller addresses]
   +───────────────────────────────────+  ◀── rsp, immediately before `call solve`
   │ caller's local region             │
   │ ... your flag is in here ...      │
   +───────────────────────────────────+
   │ caller's saved rbp                │
   +───────────────────────────────────+
   │ return address (back to main)     │
   +───────────────────────────────────+
   │ ... main's frame ...              │
   +───────────────────────────────────+
                                          [larger addresses]

The call solve instruction does two things:

  1. Pushes the return address onto the stack (8 bytes). Pushing decrements rsp, so the return address ends up at a smaller address than what was already on the stack.
  2. Jumps to your code.

That first step is critical: the stack grows backwards from what you might expect. pop actually adds 8 to rsp, and push subtracts 8. This is counter-intuitive and is a concept that often confuses learners. If you think of the stack as a page that is 8 bytes wide, you would start writing in this page at the very bottom, and move one line upwards on the page every time you push. In other words, say, pop rdi is equivalent to mov rdi, [rsp]; add rsp, 8 and push rdi is equivalent to sub rsp, 8; mov [rsp], rdi.

Note that this makes talking about the stack without confusion borderline impossible. For example, people with a math background tend to think of a coordinate of 0 as being on the bottom or the left of a page, whereas people with a video game or web development background tend to think of 0 as being on the top or the left. This leads to massive confusion about the definition of "higher address", "lower address", and so on. Everyone has different ways of dealing with this. In this document, because horizontal space is at a premium, we put diagrams from 0 (top) to 0xffffffff (bottom), but in everyday life when not restricted by horizontal space, we simply conceptualize memory from the "left" (0) to the "right" (0xffffffff).

Anyways, at the moment your solve starts running, the stack looks like this:

                                          [smaller addresses, where rsp goes if you grow your own frame]
   +───────────────────────────────────+
   │ return address (back to caller)   │  ◀── rsp points here
   +───────────────────────────────────+
   │ caller's stack frame              │
   │ ... your flag is in here ...      │
   +───────────────────────────────────+
   │ return address (back to main)     │
   +───────────────────────────────────+
   │ ... main's frame ...              │
   +───────────────────────────────────+
                                          [larger addresses]

The caller's locals sit at larger addresses than your rsp --- below your rsp in the diagram. The data you want is somewhere in that region. To find it, you index into memory with a positive offset from rsp.

If you go the other way --- negative offsets, at addresses smaller than rsp (above rsp in the diagram) --- you'll find unallocated stack space. There's nothing useful for you up there (yet!).

When your solve starts running, the layout looks like this:

   [rsp + 0x00]   your return address (back into caller's code)
   [rsp + 0x08]   first byte of caller's local region
   ...
   [rsp + 0x40]   the flag (copied here by the caller)
   ...
   [rsp + 0x110]  caller's return address (back to main)

Your job: reach into the caller's frame, grab the flag at [rsp + 0x40], and write it to stdout (you already know how to issue a write syscall!). Get it right, and your solve will print the flag for you!

In the last level, you reached rightward into your caller's frame. Now you'll look leftward, at bytes left behind by a function that already returned.

When your code calls a function, that callee can move rsp left and use stack memory of its own. When it returns, it moves rsp back right, but the bytes it wrote are not automatically erased. They become stale stack data: ordinary memory left behind by code that already finished. Software could erase those bytes before returning, but erasing data means running more instructions and writing more memory. When the leftover data is sensitive, skipping that erasure can become a vulnerability. This level starts with the smallest version of that issue: one stale 8-byte value.

This challenge passes your solve a function pointer named load_secret. Call it first. It stores one 8-byte secret in its own stack frame and returns, leaving those bytes at a negative offset from your current rsp. The checker will tell you the exact offset.

Because the goal is to return the 8-byte value itself, load it with mov:

mov rax, qword ptr [rsp-0x40]

That example offset is hypothetical; use the offset printed by the checker. Write a function called solve that calls load_secret, loads the stale 8-byte value into rax, and returns.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

For debugging a submitted function inside a shared library, refer back to Writing From a Shared Library.

In the last levels, you reached rightward into the caller's frame and leftward into stale data from an old callee. Now you'll carve out a frame of your own.

So far, your functions have kept their temporary values in registers. But a function can need more scratch space than registers can hold. On 64-bit x86, a function makes stack scratch space by modifying the stack pointer (rsp) to point to a lower address: sub rsp, 256 reserves 256 bytes to the right of the new stack pointer. Those bytes are then addressable as [rsp] through [rsp+255]. The stuff already on the stack is of course still there, but because rsp moved left, it now needs different offsets from rsp.

This does not give you freshly-zeroed bytes. The bytes you just moved rsp across are ordinary stack memory, and they may contain bytes left behind by earlier code. If you use those bytes as a table or a set of counters, stale values look exactly like values your function wrote. In fact, failure to initialize stack data, and the subsequent use of resulting garbage by the program, is a common source of vulnerabilities in software! That is why a stack frame that will hold scratch data normally starts with initialization: reserve the space, write known values into it, use it, then put rsp back before returning.

Initialization happens between allocation and deallocation of the stack frame:

sub rsp, 256       # allocate a 256-byte frame
    ...            # initialize and use [rsp] through [rsp+255]
add rsp, 256       # deallocate the frame
ret

That last step matters as much as the first. ret pops its return address from [rsp], so if rsp is not back where it started, ret will read the wrong bytes as an address and your program will crash.

Write a function called solve that reserves a 256-byte stack frame, clears every byte in it to zero, restores rsp, and returns. The grader fills the would-be frame with nonzero bytes before calling your function, then checks that all 256 bytes were cleared after your function returns. You may find mov byte ptr [rsp+rcx], 0 useful for clearing one byte at offset rcx.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

For debugging a submitted function inside a shared library, refer back to Writing From a Shared Library.

The stack stores more than just argc and argv! Right after the argument list, the kernel places the environment variables you learned about in the Linux Luminarium. Just like argv, these are stored on the stack as an array of pointers to strings, where each string includes both the name and value of the variable, as so: PATH=/usr/bin:..., HOME=/home/hacker, or PWN=COLLEGE.

If a program is called with no arguments (e.g., argc is 1 and the only string in argv is the name of the program itself) and a single environment variable named FLAG, its starting stack layout might look like this:

     Address    │ Contents
   +────────────────────────+
   │ rsp + 0    │ 1         │ ◀─── argc
   +────────────────────────+
   │ rsp + 8    │ rsp + 128 │──┐  argv[0]: pointer to the program name
   +────────────────────────+  │
   │ rsp + 16   │ 0         │  │  NULL (end of argv)
   +────────────────────────+  │
   │ rsp + 24   │ rsp + 200 │──┼──┐  envp[0]: pointer to the first env var
   +────────────────────────+  │  │
   │ rsp + 32   │ 0         │  │  │  NULL (end of envp)
   +────────────────────────+  │  │
                               │  │
     Address    │ Contents     │  │
   +────────────────────────+  │  │
   │ rsp + 128  │ "/tmp/..."│◀─┘  │  the program name
   +────────────────────────+     │
   │ ...        │ ...       │     │
   +────────────────────────+     │
   │ rsp + 200  │ "FLAG=..."│◀────┘  the first env var: the `FLAG` variable
   +────────────────────────+

Two new things to notice:

  1. Both argv and envp are NULL-pointer-terminated: the kernel writes a NULL pointer at the end of each list of pointers. That's how programs (and you!) know where each list ends --- walk the pointers until you hit a NULL. In the diagram, you can see the NULL at rsp+16 marking the end of argv, and another at rsp+32 marking the end of envp.

  2. The envp strings look like NAME=VALUE (e.g., PATH=/usr/bin:/bin). So envp[0] points to a string that starts with the first env var's name.

In this challenge, we will set the FLAG environment variable to the actual flag and run your program with no arguments and no other env vars. That means [rsp+24] will hold a pointer to the FLAG=... string, and you can get the flag by write()ing it out!

This is a whole-program level, so submit an executable, not a shared library. Assemble and link your program, then pass that executable to the checker:

hacker@dojo:~$ as -o envp.o envp.s
hacker@dojo:~$ ld -o envp envp.o
hacker@dojo:~$ /challenge/check envp

Debugging Refresher

Only the front half of the challenges are required for credit, but it is highly recommended that you spend some time here getting familiar with GDB.

This level gets you re-familiarized with gdb. To get started with this level, and all the other levels of this module, run /challenge/embryogdb_levelXYZ, where XYZ is the level number. That program will launch gdb. Run the actual level logic with r, and follow the prompts to get that flag!


RELEVANT DOCUMENTATION:

Next, we'll learn about how to print out the values of registers.

You can see the values for all your registers with info registers. Alternatively, you can also just print a particular register's value with the print command, or p for short. For example, p $rdi will print the value of $rdi in decimal. You can also print its value in hex with p/x $rdi.

In order to solve this level, you must figure out the current random value of register r12 in hex.

As before, start the challenge, invoke the run gdb command, then follow the instructions. When you've printed out what you need, remember to continue to move on to the next step of the challenge!


RELEVANT DOCUMENTATION:

Next, we'll learn to use gdb to peek into process memory!

You can examine the contents of memory using the x/<n><u><f> <address> parameterized command. In this format <u> is the unit size to display, <f> is the format to display it in, and <n> is the number of elements to display. Valid unit sizes are b (1 byte), h (2 bytes), w (4 bytes), and g (8 bytes). Valid formats are d (decimal), x (hexadecimal), s (string) and i (instruction). The address can be specified using a register name, symbol name, or absolute address. Additionally, you can supply mathematical expressions when specifying the address.

For example, x/8i $rip will print the next 8 instructions from the current instruction pointer. x/16i main will print the first 16 instructions of main. You can also use disassemble main, or disas main for short, to print all of the instructions of main. Alternatively, x/16gx $rsp will print the first 16 values on the stack. x/gx $rbp-0x32 will print the local variable stored there on the stack.

You will probably want to view your instructions using the CORRECT assembly syntax. You can do that with the command set disassembly-flavor intel.

In order to solve this level, you must figure out the random value on the stack (the value read in from /dev/urandom). Think about what the arguments to the read system call are.


RELEVANT DOCUMENTATION:

A critical part of dynamic analysis is getting your program to the state you are interested in analyzing. So far, these challenges have automatically set breakpoints for you to pause execution at states you may be interested in analyzing. It is important to be able to do this yourself.

There are a number of ways to move forward in the program's execution. You can use the stepi <n> command, or si <n> for short, in order to step forward one instruction. You can use the nexti <n> command, or ni <n> for short, in order to step forward one instruction, while stepping over any function calls. The <n> parameter is optional, but allows you to perform multiple steps at once. You can use the finish command in order to finish the currently executing function. You can use the break *<address> parameterized command in order to set a breakpoint at the specified-address. You have already used the continue command, which will continue execution until the program hits a breakpoint.

While stepping through a program, you may find it useful to have some values displayed to you at all times. There are multiple ways to do this. The simplest way is to use the display/<n><u><f> parameterized command, which follows exactly the same format as the x/<n><u><f> parameterized command. For example, display/8i $rip will always show you the next 8 instructions. On the other hand, display/4gx $rsp will always show you the first 4 values on the stack. Another option is to use the layout regs command. This will put gdb into its TUI mode and show you the contents of all of the registers, as well as nearby instructions.

In order to solve this level, you must figure out a series of random values which will be placed on the stack. As before, run will start you out, but it will interrupt the program and you must, carefully, continue its execution.

You are highly encouraged to try using combinations of stepi, nexti, break, continue, and finish to make sure you have a good internal understanding of these commands. The commands are all absolutely critical to navigating a program's execution.


RELEVANT DOCUMENTATION:

NOTE: This challenge will require you to read and understand assembly! Don't worry, this skill will come in quite handy later in pwn.college.

This challenge is optional, it will not count towards dojo completion.

We write code in order to express an idea which can be reproduced and refined. We can think of our analysis as a program which injests the target to be analyzed as data. As the saying goes, code is data and data is code.

While using gdb interactively as we've done with the past levels is incredibly powerful, another powerful tool is gdb scripting. By scripting gdb, you can very quickly create a custom-tailored program analysis tool. If you know how to interact with gdb, you already know how to write a gdb script--the syntax is exactly the same. You can write your commands to some file, for example x.gdb, and then launch gdb using the flag -x <PATH_TO_SCRIPT>. This file will execute all of the gdb commands after gdb launches. Alternatively, you can execute individual commands with -ex '<COMMAND>'. You can pass multiple commands with multiple -ex arguments. Finally, you can have some commands be always executed for any gdb session by putting them in ~/.gdbinit. You probably want to put set disassembly-flavor intel in there.

Within gdb scripting, a very powerful construct is breakpoint commands. Consider the following gdb script:

start
break *main+42
commands
  x/gx $rbp-0x32
  continue
end
continue

In this case, whenever we hit the instruction at main+42, we will output a particular local variable and then continue execution.

Now consider a similar, but slightly more advanced script using some commands you haven't yet seen:

start
break *main+42
commands
  silent
  set $local_variable = *(unsigned long long*)($rbp-0x32)
  printf "Current value: %llx\n", $local_variable
  continue
end
continue

In this case, the silent indicates that we want gdb to not report that we have hit a breakpoint, to make the output a bit cleaner. Then we use the set command to define a variable within our gdb session, whose value is our local variable. Finally, we output the current value using a formatted string.

Use gdb scripting to help you collect the random values in this level. This may feel difficult, but will serve you well in your journey ahead.


RELEVANT DOCUMENTATION:

This challenge is optional, it will not count towards dojo completion.

As it turns out, gdb has FULL control over the target process. Not only can you analyze the program's state, but you can also modify it. While gdb probably isn't the best tool for doing long term maintenance on a program, sometimes it can be useful to quickly modify the behavior of your target process in order to more easily analyze it.

You can modify the state of your target program with the set command. For example, you can use set $rdi = 0 to zero out $rdi. You can use set *((uint64_t *) $rsp) = 0x1234 to set the first value on the stack to 0x1234. You can use set *((uint16_t *) 0x31337000) = 0x1337 to set 2 bytes at 0x31337000 to 0x1337.

Suppose your target is some networked application which reads from some socket on fd 42. Maybe it would be easier for the purposes of your analysis if the target instead read from stdin. You could achieve something like that with the following gdb script:

start
catch syscall read
commands
  silent
  if ($rdi == 42)
    set $rdi = 0
  end
  continue
end
continue

This example gdb script demonstrates how you can automatically break on system calls, and how you can use conditions within your commands to conditionally perform gdb commands.

In the previous level, your gdb scripting solution likely still required you to copy and paste your solutions. This time, try to write a script that doesn't require you to ever talk to the program, and instead automatically solves each challenge by correctly modifying registers / memory.


RELEVANT DOCUMENTATION:

This challenge is optional, it will not count towards dojo completion.

This level will expose you to some of the true power of gdb.


RELEVANT DOCUMENTATION:

This challenge is optional, it will not count towards dojo completion.

The previous level showed you raw, but unrefined power. This level will force you to refine it, as the win function will no longer work. break at it, look around, and understand what is wrong.


RELEVANT DOCUMENTATION:


Debugging Exploits

Every hacker runs into exploits that fail, and the bugs can be elusive and frustrating to track down!

These challenges hand you a complete exploit script that should work but doesn't. To get started, copy the exploit script (exploit.py) in the challenge directory to your home directory:

hacker@dojo:~$ cp /challenge/exploit.py ~/exploit.py

In this challenge, /challenge/exploit.py is a complete exploit for /challenge/program, but it does not work. Copy it into your home directory and debug it to find the bug and get the flag!

hacker@dojo:~$ cp /challenge/exploit.py ~/exploit.py

In this challenge, /challenge/exploit.py is a complete exploit for /challenge/program, but it does not work. Copy it into your home directory and debug it to find the bug and get the flag!

This challenge is optional, it will not count towards dojo completion.

In this challenge, /challenge/exploit.py is a complete exploit for /challenge/program, but it does not work. Copy it into your home directory and debug it to find the bug and get the flag!

This challenge is optional, it will not count towards dojo completion.

In this challenge, /challenge/exploit.py is a complete exploit for /challenge/program, but it does not work. Copy it into your home directory and debug it to find the bug and get the flag!

This challenge is optional, it will not count towards dojo completion.

In this challenge, /challenge/exploit.py is a complete exploit for /challenge/program, but it does not work. Copy it into your home directory and debug it to find the bug and get the flag!


30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score