Computing 101


CSE 365 - Fall 2024

Computers run an incredible managerie of programs that make modern life possible. But how do they work, deep down inside? In this module, we will dive into the depths of computation, reach (something close to) its very underpinnings, and strive to understand it all. Access the challenges, and accompanying material at the Computing 101 dojo.


Challenges

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), and pass it as an argument to the /challenge/check file (e.g., /challenge/check rax-challenge.s). 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 restaraunt 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 asm.s in this example).
  2. Assemble your binary into an executable 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 asm.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 asm.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. We'll talk about these later, but for now, we'll let the assembler figure it out!

Assembling object files!
Next, we'll assemble the code. This is done using the assembler, as, as so:

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

Here, the as tool reads in asm.s, assembles it into binary code, and outputs an object file called asm.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 executables.
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
asm.o   asm.s
hacker@dojo:~$ ld -o exe asm.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
hacker@dojo:~$ ls
asm.o   asm.s   exe
hacker@dojo:~$

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

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

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 asm.s
.intel_syntax noprefix
.global _start
_start:
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o asm.o asm.s
hacker@dojo:~$ ld -o exe asm.o
hacker@dojo:~$ ./exe
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!

As you write larger and larger programs, you (yes, even you!) might make mistakes when implementing certain functionality, introducing bugs into your programs. Throughout this module, we'll go over a few tools and techniques for debugging your program. The first one is pretty simple: the syscall tracer, strace.

Given a program to run, strace will use functionality of the Linux operating system to introspect and record every system call that the program invokes, and its result. For example, let's look at our program from the previous challenge:

hacker@dojo:~$ strace /tmp/your-program
execve("/tmp/your-program", ["/tmp/your-program"], 0x7ffd48ae28b0 /* 53 vars */) = 0
exit(42)                                 = ?
+++ exited with 42 +++
hacker@dojo:~$

As you can see, strace reports what system calls are triggered, what parameters were passed to them, and what data they returned. The syntax used here for output is system_call(parameter, parameter, parameter, ...). This syntax is borrowed from a programming language called C, but we don't have to worry about that yet. Just keep in mind how to read this specific syntax.

In this example, strace reports two system calls: the second is the exit system call that your program uses to request its own termination, and you can see the parameter you passed to it (42). The first is an execve system call. We'll learn about this system call later, but it's somewhat of a yin to exit's yang: it starts a new program (in this case, your-program). It's not actually invoked by your-program in this case: its detection by strace is a weird artifact of how strace works, that we'll investigate later.

In the final line, you can see the result of exit(42), which is that the program exits with an exit code of 42!

Now, the exit syscall is easy to introspect without using strace --- after all, part of the point of exit is to give you an exit code that you can access. But other system calls are less visible. For example, the alarm system call (syscall number 37!) will set a timer in the operating system, and when that many seconds pass, Linux will terminate the program. The point of alarm is to, e.g., kill the program when it's frozen, but in this case, we'll use alarm to practice our strace snooping!

In this challenge, you must strace the /challenge/trace-me program to figure out what value it passes as a parameter to the alarm system call, then call /challenge/submit-number with the number you've retrieved as the argument. Good luck!

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 like 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 diverse 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. Good luck!

As seen by your program, computer memory is a huge place where data is housed. Like houses on a street, every part of memory has a numeric address, and like houses on a street, these numbers are (mostly) sequential. Modern computers have enormous amounts of memory, and the view of memory of a typical modern program actually has large gaps (think: a portion of the street that hasn't had houses built on it, and so those addresses are skipped). But these are all details: the point is, computers store data, mostly sequentially, in memory.

In this level, we will practice accessing data stored in memory. How might we do this? Recall that to move a value into a register, we did something like:

mov rdi, 31337

After this, the value of rdi is 31337. Cool. Well, we can use the same instruction to access memory! There is another format of the command that, instead, uses the second parameter as an address to access memory! Consider that our memory looks like this:

  Address │ Contents
+────────────────────+
│ 31337   │ 42       │
+────────────────────+

To access the memory contents at memory address 31337, you would can do:

mov rdi, [31337]

When the CPU executes this instruction, it of course understands that 31337 is an address, not a raw value. If you think of the instruction as a person telling the CPU what to do, and we stick with our "houses on a street" analogy, then instead of just handing the CPU data, the instruction/person points at a house on the street. The CPU will then go to that address, ring its doorbell, open its front door, drag the data that's in there out, and put it into rdi. Thus, the 31337 in this context is the memory address and serves to point to the data stored at that memory address. After this instruction executes, the value stored in rdi will be 42!

Let's put this into practice! I've stored a secret number at memory address 133700, as so:

  Address │ Contents
+────────────────────+
│ 133700  │ ???      │
+────────────────────+

You must retrieve this secret number and use it as the exit code for your program. To do this, you must read it into rdi, whose value, if you recall, is the first parameter to exit and is used as the exit code. Good luck!

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 rdi, 133700

Now, what you have is the following situation:

  Address │ Contents
+────────────────────+
│ 133700  │ 42       │◂┐
+────────────────────+ │
                       │
 Register │ Contents   │
+────────────────────+ │
│ rdi     │ 133700   │─┘
+────────────────────+

rdi now holds a value that corresponds with the address of the data that 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 the secret data into rdi and use it as the exit code of the program to get the flag!

In the previous level, you dereferenced rax to read data into rdi. The interesting thing here is that our choice of rax was pretty arbitrary. We could have used any other pointer, even rdi itself! Nothing stops you from dereferencing a register to overwrite its own content with the dereferenced value!

For example, here is us doing this exact thing with rax. I've annotated each line with comments:

mov [133700], 42
mov rax, 133700  # after this, rax will be 133700
mov rax, [rax]   # after this, rax will be 42

Throughout this snippet, rax goes from being used as a pointer to being used to hold the data that's been read from memory. The CPU makes this all work!

In this challenge, you'll explore this concept. Rather than initializing rax, as before, we've made rdi the pointer to the secret value! You'll need to dereference it to load that value into rdi, then exit with that value as the exit code. Good luck!

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 rax. 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!

Pointers can get even more interesting! Imagine that your friend lives in a different house on your street. Rather than remembering their address, you might write it down, and store the paper with their house address in your house. Then, to get data from your friend, you'd need to point the CPU at your house, have it go in there and find the friend's address, and use that address as a pointer to their house.

Similarly, since memory addresses are really just values, they can be stored in memory, and retrieved later! Let's explore a scenario where we store the value 133700 at the address 123400, and store the value 42 at the address 133700. Consider the following instructions:

mov rdi, 123400    # after this, rdi becomes 123400
mov rdi, [rdi]     # after this, rdi becomes the value stored at 123400 (which is 133700)
mov rax, [rdi]     # here we dereference rdi, reading 42 into rax!

Wow! This storing of addresses is extremely common in programs. Addresses and data are stored, loaded, moved around, and, sometimes, mixed up with each other! When that happens, security issues can arise, and you'll romp through many such issues during your pwn.college journey.

For now, let's practice dereferencing an address stored in memory. I'll store a secret value at a secret address, then store that secret address at the address 567800. You must read the address, dereference it, get the secret value, and then exit with it as the exit code. You got this!

In the last few levels, you have:

  • Used an address that we told you (in one level, 133700, and in another, 123400) to load a secret value from memory.
  • Used an address that we put into rax for you to load a secret value from memory.
  • Used an address that we told you (in the last level, 567800) to load the address of a secret value from memory into a register, then used that register as a pointer to retrieve the secret value from memory!

Let's put those last two together. In this challenge, we stored our SECRET_VALUE in memory at the address SECRET_LOCATION_1, then stored SECRET_LOCATION_1 in memory at the address SECRET_LOCATION_2. Then, we put SECRET_ADDRESS_2 into rax! The result looks something like this, using 133700 for SECRET_LOCATION_1 and 123400 for SECRET_LOCATION_2 (not, in the real challenge, these values will be different and hidden from you!):

     Address │ Contents
   +────────────────────+
 ┌─│ 133700  │ 123400   │◂┐
 │ +────────────────────+ │
 └▸│ 123400  │ 42       │ │
   +────────────────────+ │
                          │
                          │
                          │
    Register │ Contents   │
   +────────────────────+ │
   │ rdi     │ 133700   │─┘
   +────────────────────+

Here, you will need to perform two memory reads: one dereferencing rax to read SECRET_LOCATION_1 from the location that rax is pointing to (which is SECRET_LOCATION_2), and the second one dereferencing whatever register now holds SECRET_LOCATION_1 to read SECRET_VALUE into rdi, so you can use it as the exit code!

That sounds like a lot, but you've done basically all of this already. Go put it together!

Okay, let's stretch that to one more depth! We've added an additional level of indirection in this challenge, so now you'll need three dereferences to find the secret value. Something like this:

     Address │ Contents
   +────────────────────+
 ┌─│ 133700  │ 123400   │◂──┐
 │ +────────────────────+   │
 └▸│ 123400  │ 100000   │─┐ │
   +────────────────────+ │ │
   │ 100000  │ 42       │◂┘ │
   +────────────────────+   │
                            │
                            │
    Register │ Contents     │
   +────────────────────+   │
   │ rdi     │ 133700   │───┘
   +────────────────────+

As you can see, we'll place the first address that you must dereference into rdi. Go get the value!

Let's learn to write text!

Unsurprisingly, your program writes text to the screen by invoking a system call. Specifically, this is the write system call, and its syscall number is 1. However, the write system call also needs to specify, via its parameters, what data to write and where to write it to.

You may remember, from the Practicing Piping module of the Linux Luminarium dojo, the concept of File Descriptors (FDs). As a reminder, each process starts out with three FDs:

  • FD 0: Standard Input is the channel through which the process takes input. For example, your shell uses Standard Input to read the commands that you input.
  • FD 1: Standard Output is the channel through which processes output normal data, such as the flag when it is printed to you in previous challenges or the output of utilities such as ls.
  • FD 2: Standard Error is the channel through which processes output error details. For example, if you mistype a command, the shell will output, over standard error, that this command does not exist.

It turns out that, in your write system call, this is how you specify where to write the data to! The first (and only) parameter to your exit system call was your exit code (mov rdi, 42), and the first (but, in this case, not only!) parameter to write is the file descriptor. If you want to write to standard output, you would set rdi to 1. If you want to write to standard error, you would set rdi to 2. Super simple!

This leaves us with what to write. Now, you could imagine a world where we specify what to write through yet another register parameter to the write system call. But these registers don't fit a ton of data, and to write out a long story like this challenge description, you'd need to invoke the write system call multiple times. Relatively speaking, this has a lot of performance cost --- the CPU needs to switch from executing the instructions of your program to executing the instructions of Linux itself, do a bunch of housekeeping computation, interact with your hardware to get the actual pixels to show up on your screen, and then switch back. This is slow, and so we try to minimize the number of times we invoke system calls.

Of course, the solution to this is to write multiple characters at the same time. The write system call does this by taking two parameters for the "what": a where (in memory) to start writing from and a how many characters to write. These parameters are passed as the second and third parameter to write. In the kinda-C syntax that we learned from strace, this would be:

write(file_descriptor, memory_address, number_of_characters_to_write)

For a more concrete example, if you wanted to write 10 characters from memory address 1337000 to standard output (file descriptor 1), this would be:

write(1, 1337000, 10);

Wow, that's simple! Now, how do we actually specify these parameters?

  1. We'll pass the first parameter of a system call, as we reviewed above, in the rdi register.
  2. We'll pass the second parameter via the rsi register. The agreed-upon convention in Linux is that rsi is used as the second parameter to system calls.
  3. We'll pass the third parameter via the rdx register. This is the most confusing part of this entire module: rdi (the register holding the first parameter) has such a similar name to rdx that it's really easy to mix up and, unfortunately, the naming is this way for historic reasons and is here to stay. Oh well... It's just something we have to be careful about. Maybe a mnemonic like "rdi is the initial parameter while rdx is the xtra parameter"? Or just think of it as having to keep track of different friends with similar names, and you'll be fine.

And, of course, the write syscall index into rax itself: 1. Other than the rdi vs rdx confusion, this is really easy!

Now, you know how to point a register at a memory address (from the Memory module!), and yo know how to set the system call number, and how to set the rest of the registers. So, this should be cake!

Similar to before, we wrote a single secret character value into memory at address 1337000. Call write to that single character (for now! We'll do multiple-character writes later) value onto standard out, and we'll give you the flag!

Okay, our previous solution wrote output but then crashed. In this level, you will write output, and then not crash!

We'll do this by invoking the write system call, and then invoking the exit system call to cleanly exit the program. How do we invoke two system calls? Just like you invoke two instructions! First, you set up the necessary registers and invoke write, then you set up the necessary registers and invoke `exit!

Your previous solution had 5 instructions (setting rdi, setting rsi, setting rdx, setting rax, and syscall). This one should have those 5, plus three more for exit (setting rdi to the exit code, setting rax to syscall index 60, and syscall). For this level, let's exit with exit code 42!

Okay, we have one thing left for this run of challenges. You've written out a single byte, and now we'll practice writing out multiple bytes. I've stored a 14-character secret string at memory location 1337000. Can you write it out?


Hint: The only thing you should have to change compared to your previous solution is the value in rdx!

In this level, you will be working with registers. You will be asked to modify or read from registers.

In this level, you will work with registers! Please set the following:

rdi = 0x1337

In this level, you will be working with registers. You will be asked to modify or read from registers.

In this level, you will work with multiple registers. Please set the following:

  • rax = 0x1337
  • r12 = 0xCAFED00D1337BEEF
  • rsp = 0x31337

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

Many instructions exist in x86 that allow you to perform all the normal math operations on registers and memory.

For shorthand, when we say A += B, it really means A = A + B.

Here are some useful instructions:

  • add reg1, reg2 <=> reg1 += reg2
  • sub reg1, reg2 <=> reg1 -= reg2
  • imul reg1, reg2 <=> reg1 *= reg2

div is more complicated, and we will discuss it later. Note: all regX can be replaced by a constant or memory location.

Do the following:

  • Add 0x331337 to rdi

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

Using your new knowledge, please compute the following:

  • f(x) = mx + b, where:
    • m = rdi
    • x = rsi
    • b = rdx

Place the result into rax.

Note: There is an important difference between mul (unsigned multiply) and imul (signed multiply) in terms of which registers are used. Look at the documentation on these instructions to see the difference.

In this case, you will want to use imul.

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result, which is usually rax.

Division in x86 is more special than in normal math. Math here is called integer math, meaning every value is a whole number.

As an example: 10 / 3 = 3 in integer math.

Why?

Because 3.33 is rounded down to an integer.

The relevant instructions for this level are:

  • mov rax, reg1
  • div reg2

Note: div is a special instruction that can divide a 128-bit dividend by a 64-bit divisor while storing both the quotient and the remainder, using only one register as an operand.

How does this complex div instruction work and operate on a 128-bit dividend (which is twice as large as a register)?

For the instruction div reg, the following happens:

  • rax = rdx:rax / reg
  • rdx = remainder

rdx:rax means that rdx will be the upper 64-bits of the 128-bit dividend and rax will be the lower 64-bits of the 128-bit dividend.

You must be careful about what is in rdx and rax before you call div.

Please compute the following:

  • speed = distance / time, where:
    • distance = rdi
    • time = rsi
    • speed = rax

Note that distance will be at most a 64-bit value, so rdx should be 0 when dividing.

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform a formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

Modulo in assembly is another interesting concept!

x86 allows you to get the remainder after a div operation.

For instance: 10 / 3 results in a remainder of 1.

The remainder is the same as modulo, which is also called the "mod" operator.

In most programming languages, we refer to mod with the symbol %.

Please compute the following: rdi % rsi

Place the value in rax.

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result, which is typically in rax.

Another cool concept in x86 is the ability to independently access the lower register bytes.

Each register in x86_64 is 64 bits in size, and in the previous levels, we have accessed the full register using rax, rdi, or rsi.

We can also access the lower bytes of each register using different register names.

For example, the lower 32 bits of rax can be accessed using eax, the lower 16 bits using ax, and the lower 8 bits using al.

MSB                                    LSB
+----------------------------------------+
|                   rax                  |
+--------------------+-------------------+
                     |        eax        |
                     +---------+---------+
                               |   ax    |
                               +----+----+
                               | ah | al |
                               +----+----+

Lower register bytes access is applicable to almost all registers.

Using only one move instruction, please set the upper 8 bits of the ax register to 0x42.

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

It turns out that using the div operator to compute the modulo operation is slow!

We can use a math trick to optimize the modulo operator (%). Compilers use this trick a lot.

If we have x % y, and y is a power of 2, such as 2^n, the result will be the lower n bits of x.

Therefore, we can use the lower register byte access to efficiently implement modulo!

Using only the following instruction(s):

  • mov

Please compute the following:

  • rax = rdi % 256
  • rbx = rsi % 65536

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with bit logic and operations. This will involve heavy use of directly interacting with bits stored in a register or memory location. You will also likely need to make use of the logic instructions in x86: and, or, not, xor.

Shifting bits around in assembly is another interesting concept!

x86 allows you to 'shift' bits around in a register.

Take, for instance, al, the lowest 8 bits of rax.

The value in al (in bits) is:

rax = 10001010

If we shift once to the left using the shl instruction:

shl al, 1

The new value is:

al = 00010100

Everything shifted to the left, and the highest bit fell off while a new 0 was added to the right side.

You can use this to do special things to the bits you care about.

Shifting has the nice side effect of doing quick multiplication (by 2) or division (by 2), and can also be used to compute modulo.

Here are the important instructions:

  • shl reg1, reg2 <=> Shift reg1 left by the amount in reg2
  • shr reg1, reg2 <=> Shift reg1 right by the amount in reg2

Note: 'reg2' can be replaced by a constant or memory location.

Using only the following instructions:

  • mov, shr, shl

Please perform the following: Set rax to the 5th least significant byte of rdi.

For example:

rdi = | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
Set rax to the value of B4

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with bit logic and operations. This will involve heavy use of directly interacting with bits stored in a register or memory location. You will also likely need to make use of the logic instructions in x86: and, or, not, xor.

Bitwise logic in assembly is yet another interesting concept! x86 allows you to perform logic operations bit by bit on registers.

For the sake of this example, say registers only store 8 bits.

The values in rax and rbx are:

  • rax = 10101010
  • rbx = 00110011

If we were to perform a bitwise AND of rax and rbx using the and rax, rbx instruction, the result would be calculated by ANDing each bit pair one by one, hence why it's called bitwise logic.

So from left to right:

  • 1 AND 0 = 0
  • 0 AND 0 = 0
  • 1 AND 1 = 1
  • 0 AND 1 = 0
  • ...

Finally, we combine the results together to get:

  • rax = 00100010

Here are some truth tables for reference:

  • AND

    A | B | X
    ---+---+---
    0 | 0 | 0
    0 | 1 | 0
    1 | 0 | 0
    1 | 1 | 1
    
  • OR

    A | B | X
    ---+---+---
    0 | 0 | 0
    0 | 1 | 1
    1 | 0 | 1
    1 | 1 | 1
    
  • XOR

    A | B | X
    ---+---+---
    0 | 0 | 0
    0 | 1 | 1
    1 | 0 | 1
    1 | 1 | 0
    

Without using the following instructions: mov, xchg, please perform the following:

Set rax to the value of (rdi AND rsi)

In this level, you will be working with registers. You will be asked to modify or read from registers.

We will set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it is rax.

In this level, you will be working with bit logic and operations. This will involve heavy use of directly interacting with bits stored in a register or memory location. You will also likely need to make use of the logic instructions in x86: and, or, not, xor.

Using only the following instructions:

  • and
  • or
  • xor

Implement the following logic:

if x is even then
  y = 1
else
  y = 0

Where:

  • x = rdi
  • y = rax

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Up until now, you have worked with registers as the only way for storing things, essentially variables such as 'x' in math.

However, we can also store bytes into memory!

Recall that memory can be addressed, and each address contains something at that location. Note that this is similar to addresses in real life!

As an example: the real address '699 S Mill Ave, Tempe, AZ 85281' maps to the 'ASU Brickyard'. We would also say it points to 'ASU Brickyard'. We can represent this like:

['699 S Mill Ave, Tempe, AZ 85281'] = 'ASU Brickyard'

The address is special because it is unique. But that also does not mean other addresses can't point to the same thing (as someone can have multiple houses).

Memory is exactly the same!

For instance, the address in memory where your code is stored (when we take it from you) is 0x400000.

In x86, we can access the thing at a memory location, called dereferencing, like so:

mov rax, [some_address]        <=>     Moves the thing at 'some_address' into rax

This also works with things in registers:

mov rax, [rdi]         <=>     Moves the thing stored at the address of what rdi holds to rax

This works the same for writing to memory:

mov [rax], rdi         <=>     Moves rdi to the address of what rax holds.

So if rax was 0xdeadbeef, then rdi would get stored at the address 0xdeadbeef:

[0xdeadbeef] = rdi

Note: Memory is linear, and in x86_64, it goes from 0 to 0xffffffffffffffff (yes, huge).

Please perform the following: Place the value stored at 0x404000 into rax. Make sure the value in rax is the original value stored at 0x404000.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Please perform the following: Place the value stored in rax to 0x404000.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Please perform the following:

  • Place the value stored at 0x404000 into rax.
  • Increment the value stored at the address 0x404000 by 0x1337.

Make sure the value in rax is the original value stored at 0x404000 and make sure that [0x404000] now has the incremented value.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Recall that registers in x86_64 are 64 bits wide, meaning they can store 64 bits. Similarly, each memory location can be treated as a 64-bit value. We refer to something that is 64 bits (8 bytes) as a quad word.

Here is the breakdown of the names of memory sizes:

  • Quad Word = 8 Bytes = 64 bits
  • Double Word = 4 bytes = 32 bits
  • Word = 2 bytes = 16 bits
  • Byte = 1 byte = 8 bits

In x86_64, you can access each of these sizes when dereferencing an address, just like using bigger or smaller register accesses:

  • mov al, [address] <=> moves the least significant byte from address to rax
  • mov ax, [address] <=> moves the least significant word from address to rax
  • mov eax, [address] <=> moves the least significant double word from address to rax
  • mov rax, [address] <=> moves the full quad word from address to rax

Remember that moving into al does not fully clear the upper bytes.

Please perform the following: Set rax to the byte at 0x404000.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, refer to the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Recall the following:

  • The breakdown of the names of memory sizes:
    • Quad Word = 8 Bytes = 64 bits
    • Double Word = 4 bytes = 32 bits
    • Word = 2 bytes = 16 bits
    • Byte = 1 byte = 8 bits

In x86_64, you can access each of these sizes when dereferencing an address, just like using bigger or smaller register accesses:

  • mov al, [address] <=> moves the least significant byte from address to rax
  • mov ax, [address] <=> moves the least significant word from address to rax
  • mov eax, [address] <=> moves the least significant double word from address to rax
  • mov rax, [address] <=> moves the full quad word from address to rax

Please perform the following:

  • Set rax to the byte at 0x404000
  • Set rbx to the word at 0x404000
  • Set rcx to the double word at 0x404000
  • Set rdx to the quad word at 0x404000

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

It is worth noting, as you may have noticed, that values are stored in reverse order of how we represent them.

As an example, say:

[0x1330] = 0x00000000deadc0de

If you examined how it actually looked in memory, you would see:

[0x1330] = 0xde
[0x1331] = 0xc0
[0x1332] = 0xad
[0x1333] = 0xde
[0x1334] = 0x00
[0x1335] = 0x00
[0x1336] = 0x00
[0x1337] = 0x00

This format of storing things in 'reverse' is intentional in x86, and it's called "Little Endian".

For this challenge, we will give you two addresses created dynamically each run.

The first address will be placed in rdi. The second will be placed in rsi.

Using the earlier mentioned info, perform the following:

  • Set [rdi] = 0xdeadbeef00001337
  • Set [rsi] = 0xc0ffee0000

Hint: it may require some tricks to assign a big constant to a dereferenced register. Try setting a register to the constant value, then assigning that register to the dereferenced register.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it’s rax.

In this level, you will be working with memory. This will require you to read or write to things stored linearly in memory. If you are confused, go look at the linear addressing module in 'ike. You may also be asked to dereference things, possibly multiple times, to things we dynamically put in memory for your use.

Recall that memory is stored linearly.

What does that mean?

Say we access the quad word at 0x1337:

[0x1337] = 0x00000000deadbeef

The real way memory is laid out is byte by byte, little endian:

[0x1337] = 0xef
[0x1337 + 1] = 0xbe
[0x1337 + 2] = 0xad
...
[0x1337 + 7] = 0x00

What does this do for us?

Well, it means that we can access things next to each other using offsets, similar to what was shown above.

Say you want the 5th byte from an address, you can access it like:

mov al, [address+4]

Remember, offsets start at 0.

Perform the following:

  • Load two consecutive quad words from the address stored in rdi.
  • Calculate the sum of the previous steps' quad words.
  • Store the sum at the address in rsi.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with the stack, the memory region that dynamically expands and shrinks. You will be required to read and write to the stack, which may require you to use the pop and push instructions. You may also need to use the stack pointer register (rsp) to know where the stack is pointing.

In these levels, we are going to introduce the stack.

The stack is a region of memory that can store values for later.

To store a value on the stack, we use the push instruction, and to retrieve a value, we use pop.

The stack is a last in, first out (LIFO) memory structure, and this means the last value pushed is the first value popped.

Imagine unloading plates from the dishwasher. Let's say there are 1 red, 1 green, and 1 blue. First, we place the red one in the cabinet, then the green on top of the red, then the blue.

Our stack of plates would look like:

Top ----> Blue
          Green
Bottom -> Red

Now, if we wanted a plate to make a sandwich, we would retrieve the top plate from the stack, which would be the blue one that was last into the cabinet, ergo the first one out.

On x86, the pop instruction will take the value from the top of the stack and put it into a register.

Similarly, the push instruction will take the value in a register and push it onto the top of the stack.

Using these instructions, take the top value of the stack, subtract rdi from it, then put it back.

We will now set some values in memory dynamically before each run. On each run the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with the stack, the memory region that dynamically expands and shrinks. You will be required to read and write to the stack, which may require you to use the pop and push instructions. You may also need to use the stack pointer register (rsp) to know where the stack is pointing.

In this level, we are going to explore the last in first out (LIFO) property of the stack.

Using only the following instructions:

  • push
  • pop

Swap values in rdi and rsi.

Example:

  • If to start rdi = 2 and rsi = 5
  • Then to end rdi = 5 and rsi = 2

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with the stack, the memory region that dynamically expands and shrinks. You will be required to read and write to the stack, which may require you to use the pop and push instructions. You may also need to use the stack pointer register (rsp) to know where the stack is pointing.

In the previous levels, you used push and pop to store and load data from the stack. However, you can also access the stack directly using the stack pointer.

On x86, the stack pointer is stored in the special register, rsp. rsp always stores the memory address of the top of the stack, i.e., the memory address of the last value pushed.

Similar to the memory levels, we can use [rsp] to access the value at the memory address in rsp.

Without using pop, please calculate the average of 4 consecutive quad words stored on the stack. Push the average on the stack.

Hint:

  • RSP+0x?? Quad Word A
  • RSP+0x?? Quad Word B
  • RSP+0x?? Quad Word C
  • RSP Quad Word D

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

Earlier, you learned how to manipulate data in a pseudo-control way, but x86 gives us actual instructions to manipulate control flow directly.

There are two major ways to manipulate control flow:

  • Through a jump
  • Through a call

In this level, you will work with jumps.

There are two types of jumps:

  • Unconditional jumps
  • Conditional jumps

Unconditional jumps always trigger and are not based on the results of earlier instructions.

As you know, memory locations can store data and instructions. Your code will be stored at 0x400042 (this will change each run).

For all jumps, there are three types:

  • Relative jumps: jump + or - the next instruction.
  • Absolute jumps: jump to a specific address.
  • Indirect jumps: jump to the memory address specified in a register.

In x86, absolute jumps (jump to a specific address) are accomplished by first putting the target address in a register reg, then doing jmp reg.

In this level, we will ask you to do an absolute jump. Perform the following: Jump to the absolute address 0x403000.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

Recall that for all jumps, there are three types:

  • Relative jumps
  • Absolute jumps
  • Indirect jumps

In this level, we will ask you to do a relative jump. You will need to fill space in your code with something to make this relative jump possible. We suggest using the nop instruction. It's 1 byte long and very predictable.

In fact, the assembler that we're using has a handy .rept directive that you can use to repeat assembly instructions some number of times: GNU Assembler Manual

Useful instructions for this level:

  • jmp (reg1 | addr | offset)
  • nop

Hint: For the relative jump, look up how to use labels in x86.

Using the above knowledge, perform the following:

  • Make the first instruction in your code a jmp.
  • Make that jmp a relative jump to 0x51 bytes from the current position.
  • At the code location where the relative jump will redirect control flow, set rax to 0x1.

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to do some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

Now, we will combine the two prior levels and perform the following:

  • Create a two jump trampoline:
    • Make the first instruction in your code a jmp.
    • Make that jmp a relative jump to 0x51 bytes from its current position.
    • At 0x51, write the following code:
      • Place the top value on the stack into register rdi.
      • jmp to the absolute address 0x403000.

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

We will be testing your code multiple times in this level with dynamic values! This means we will be running your code in a variety of random ways to verify that the logic is robust enough to survive normal use.

We will now introduce you to conditional jumps--one of the most valuable instructions in x86. In higher-level programming languages, an if-else structure exists to do things like:

if x is even:
    is_even = 1
else:
    is_even = 0

This should look familiar since it is implementable in only bit-logic, which you've done in a prior level. In these structures, we can control the program's control flow based on dynamic values provided to the program.

Implementing the above logic with jmps can be done like so:

; assume rdi = x, rax is output
; rdx = rdi mod 2
mov rax, rdi
mov rsi, 2
div rsi
; remainder is 0 if even
cmp rdx, 0
; jump to not_even code if it's not 0
jne not_even
; fall through to even code
mov rbx, 1
jmp done
; jump to this only when not_even
not_even:
mov rbx, 0
done:
mov rax, rbx
; more instructions here

Often though, you want more than just a single 'if-else'. Sometimes you want two if checks, followed by an else. To do this, you need to make sure that you have control flow that 'falls-through' to the next if after it fails. All must jump to the same done after execution to avoid the else.

There are many jump types in x86, it will help to learn how they can be used. Nearly all of them rely on something called the ZF, the Zero Flag. The ZF is set to 1 when a cmp is equal, 0 otherwise.

Using the above knowledge, implement the following:

if [x] is 0x7f454c46:
    y = [x+4] + [x+8] + [x+12]
else if [x] is 0x00005A4D:
    y = [x+4] - [x+8] - [x+12]
else:
    y = [x+4] * [x+8] * [x+12]

Where:

  • x = rdi, y = rax.

Assume each dereferenced value is a signed dword. This means the values can start as a negative value at each memory position.

A valid solution will use the following at least once:

  • jmp (any variant), cmp

In this level, you will work with control flow manipulation. This involves using instructions to indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

We will be testing your code multiple times in this level with dynamic values! This means we will run your code in various random ways to verify that the logic is robust enough to survive normal use.

The last jump type is the indirect jump, often used for switch statements in the real world. Switch statements are a special case of if-statements that use only numbers to determine where the control flow will go.

Here is an example:

switch(number):
  0: jmp do_thing_0
  1: jmp do_thing_1
  2: jmp do_thing_2
  default: jmp do_default_thing

The switch in this example works on number, which can either be 0, 1, or 2. If number is not one of those numbers, the default triggers. You can consider this a reduced else-if type structure. In x86, you are already used to using numbers, so it should be no surprise that you can make if statements based on something being an exact number. Additionally, if you know the range of the numbers, a switch statement works very well.

Take, for instance, the existence of a jump table. A jump table is a contiguous section of memory that holds addresses of places to jump.

In the above example, the jump table could look like:

[0x1337] = address of do_thing_0
[0x1337+0x8] = address of do_thing_1
[0x1337+0x10] = address of do_thing_2
[0x1337+0x18] = address of do_default_thing

Using the jump table, we can greatly reduce the amount of cmps we use. Now all we need to check is if number is greater than 2. If it is, always do:

jmp [0x1337+0x18]

Otherwise:

jmp [jump_table_address + number * 8]

Using the above knowledge, implement the following logic:

if rdi is 0:
  jmp 0x40301e
else if rdi is 1:
  jmp 0x4030da
else if rdi is 2:
  jmp 0x4031d5
else if rdi is 3:
  jmp 0x403268
else:
  jmp 0x40332c

Please do the above with the following constraints:

  • Assume rdi will NOT be negative.
  • Use no more than 1 cmp instruction.
  • Use no more than 3 jumps (of any variant).
  • We will provide you with the number to 'switch' on in rdi.
  • We will provide you with a jump table base address in rsi.

Here is an example table:

[0x40427c] = 0x40301e (addrs will change)
[0x404284] = 0x4030da
[0x40428c] = 0x4031d5
[0x404294] = 0x403268
[0x40429c] = 0x40332c

We will now set some values in memory dynamically before each run. On each run, the values will change. This means you will need to perform some type of formulaic operation with registers. We will tell you which registers are set beforehand and where you should put the result. In most cases, it's rax.

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

In a previous level, you computed the average of 4 integer quad words, which was a fixed amount of things to compute. But how do you work with sizes you get when the program is running?

In most programming languages, a structure exists called the for-loop, which allows you to execute a set of instructions for a bounded amount of times. The bounded amount can be either known before or during the program's run, with "during" meaning the value is given to you dynamically.

As an example, a for-loop can be used to compute the sum of the numbers 1 to n:

sum = 0
i = 1
while i <= n:
    sum += i
    i += 1

Please compute the average of n consecutive quad words, where:

  • rdi = memory address of the 1st quad word
  • rsi = n (amount to loop for)
  • rax = average computed

In this level, you will be working with control flow manipulation. This involves using instructions to both indirectly and directly control the special register rip, the instruction pointer. You will use instructions such as jmp, call, cmp, and their alternatives to implement the requested behavior.

We will be testing your code multiple times in this level with dynamic values! This means we will be running your code in a variety of random ways to verify that the logic is robust enough to survive normal use.

In previous levels, you discovered the for-loop to iterate for a number of times, both dynamically and statically known, but what happens when you want to iterate until you meet a condition?

A second loop structure exists called the while-loop to fill this demand. In the while-loop, you iterate until a condition is met.

As an example, say we had a location in memory with adjacent numbers and we wanted to get the average of all the numbers until we find one bigger or equal to 0xff:

average = 0
i = 0
while x[i] < 0xff:
  average += x[i]
  i += 1
average /= i

Using the above knowledge, please perform the following:

Count the consecutive non-zero bytes in a contiguous region of memory, where:

  • rdi = memory address of the 1st byte
  • rax = number of consecutive non-zero bytes

Additionally, if rdi = 0, then set rax = 0 (we will check)!

An example test-case, let:

  • rdi = 0x1000
  • [0x1000] = 0x41
  • [0x1001] = 0x42
  • [0x1002] = 0x43
  • [0x1003] = 0x00

Then: rax = 3 should be set.

We will be testing your code multiple times in this level with dynamic values! This means we will be running your code in a variety of random ways to verify that the logic is robust enough to survive normal use.

In this level, you will be working with functions! This will involve manipulating the instruction pointer (rip), as well as doing harder tasks than normal. You may be asked to use the stack to store values or call functions that we provide you.

In previous levels, you implemented a while loop to count the number of consecutive non-zero bytes in a contiguous region of memory.

In this level, you will be provided with a contiguous region of memory again and will loop over each performing a conditional operation till a zero byte is reached. All of which will be contained in a function!

A function is a callable segment of code that does not destroy control flow.

Functions use the instructions "call" and "ret".

The "call" instruction pushes the memory address of the next instruction onto the stack and then jumps to the value stored in the first argument.

Let's use the following instructions as an example:

0x1021 mov rax, 0x400000
0x1028 call rax
0x102a mov [rsi], rax
  1. call pushes 0x102a, the address of the next instruction, onto the stack.
  2. call jumps to 0x400000, the value stored in rax.

The "ret" instruction is the opposite of "call".

ret pops the top value off of the stack and jumps to it.

Let's use the following instructions and stack as an example:

Stack ADDR  VALUE
0x103f mov rax, rdx         RSP + 0x8   0xdeadbeef
0x1042 ret                  RSP + 0x0   0x0000102a

Here, ret will jump to 0x102a.

Please implement the following logic:

str_lower(src_addr):
  i = 0
  if src_addr != 0:
    while [src_addr] != 0x00:
      if [src_addr] <= 0x5a:
        [src_addr] = foo([src_addr])
        i += 1
      src_addr += 1
  return i

foo is provided at 0x403000. foo takes a single argument as a value and returns a value.

All functions (foo and str_lower) must follow the Linux amd64 calling convention (also known as System V AMD64 ABI): System V AMD64 ABI

Therefore, your function str_lower should look for src_addr in rdi and place the function return in rax.

An important note is that src_addr is an address in memory (where the string is located) and [src_addr] refers to the byte that exists at src_addr.

Therefore, the function foo accepts a byte as its first argument and returns a byte.

We will be testing your code multiple times in this level with dynamic values! This means we will be running your code in a variety of random ways to verify that the logic is robust enough to survive normal use.

In this level, you will be working with functions! This will involve manipulating the instruction pointer (rip), as well as doing harder tasks than normal. You may be asked to use the stack to store values or call functions that we provide you.

In the previous level, you learned how to make your first function and how to call other functions. Now we will work with functions that have a function stack frame.

A function stack frame is a set of pointers and values pushed onto the stack to save things for later use and allocate space on the stack for function variables.

First, let's talk about the special register rbp, the Stack Base Pointer.

The rbp register is used to tell where our stack frame first started. As an example, say we want to construct some list (a contiguous space of memory) that is only used in our function. The list is 5 elements long, and each element is a dword. A list of 5 elements would already take 5 registers, so instead, we can make space on the stack!

The assembly would look like:

; setup the base of the stack as the current top
mov rbp, rsp
; move the stack 0x14 bytes (5 * 4) down
; acts as an allocation
sub rsp, 0x14
; assign list[2] = 1337
mov eax, 1337
mov [rbp-0x8], eax
; do more operations on the list ...
; restore the allocated space
mov rsp, rbp
ret

Notice how rbp is always used to restore the stack to where it originally was. If we don't restore the stack after use, we will eventually run out. In addition, notice how we subtracted from rsp, because the stack grows down. To make the stack have more space, we subtract the space we need. The ret and call still work the same.

Once again, please make function(s) that implement the following:

most_common_byte(src_addr, size):
  i = 0
  while i <= size-1:
    curr_byte = [src_addr + i]
    [stack_base - curr_byte] += 1
    i += 1

  b = 0
  max_freq = 0
  max_freq_byte = 0
  while b <= 0xff:
    if [stack_base - b] > max_freq:
      max_freq = [stack_base - b]
      max_freq_byte = b
    b += 1

  return max_freq_byte

Assumptions:

  • There will never be more than 0xffff of any byte
  • The size will never be longer than 0xffff
  • The list will have at least one element

Constraints:

  • You must put the "counting list" on the stack
  • You must restore the stack like in a normal function
  • You cannot modify the data at src_addr

Program that exits

Program that creates a socket

Program that binds a socket

Program that listens on a socket

Program that accepts a connection

Program that statically responds to an HTTP request

Program that dynamically responds to an HTTP GET request

Program that dynamically responds to multiple HTTP GET requests

Multi-processed program that dynamically responds to multiple HTTP GET requests

Multi-processed program that dynamically responds to multiple HTTP POST requests

Multi-processed program that dynamically responds to multiple HTTP GET and POST requests


30-Day Scoreboard:

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

Rank Hacker Badges Score