Your First Program


Computing 101

Computers run computer programs to achieve different goals. One program might be your favorite video game, another is the web browser you're using to access this website, and so on.

Though your computer might be running multiple programs at the same time, each CPU will focus on one program at a time (note: this is not fully accurate, but good enough for us for now!). This has analogues in your normal life: you might subscribe to multiple YouTube channels, but at any given moment, you're most likely only watching one episode at a time. However, like a CPU switching between programs, you might indulge changing interests and interleave episodes of different shows.

A program is made of computer code, and this code is made of a huge amount of individual instructions that cause the computer to carry out computation and take certain actions based on the results. Each individual instruction is typically very simple, and only in aggregate do they enable awesome things like let you look at memes on the internet.

This computation is done by the Central Processing Unit (CPU), in tandem with other pieces of hardware inside your computer. Instructions are specified to the CPU in something called Assembly Language, and each CPU architecture uses a different flavor of this language. Any program, no matter what language it is originally written in (e.g., C, C++, Java, Python, etc.), is eventually converted to or interpreted by Assembly instructions.

Most of pwn.college's material uses the x86 CPU architecture, which is Zardus' favourite architecture. x86 was created by Intel in the dawn of the PC age, and has continued to evolve over the years. Together, x86 and ARM (a different, less cool architecture) make up the majority of PC CPUs out there.

In this module, we will start out with the simplest x86 program that we can imagine, which we will write in x86 assembly, and build up from there! Let's dig in, and write your first program!


Lectures and Reading


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!


30-Day Scoreboard:

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

Rank Hacker Badges Score