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 parameters 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?
- We'll pass the first parameter of a system call, as we reviewed above, in the
rdi register.
- 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.
- 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 you 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!