Complete the Shellcode Primer in Jack’s Office. According to the last challenge, what is the secret to KringleCon success? “All of our speakers and organizers, providing the gift of _____, free to the community.” Talk to Chimney Scissorsticks in the NetWars are for hints.
Hints provided for Objective 6
- If you run into any shellcode primers at the North Pole, be sure to read the directions and the comments in the shellcode source!
- Also, troubleshooting shellcode can be difficult. Use the debugger step-by-step feature to watch values.
- Lastly, be careful not to overwrite any register values you need to reference later on in your shellcode.
Welcome! Are you ready to learn how to write shellcode? We hope so! First, some tips:
Comments are denoted with a semicolon (;) Don't forget to look at the debugger, line by line, if something is wrong Really, don't forget to read the error list! We check each place where you might go wrong in your code Your code for each level is saved in your browser, so you can leave and come back, refresh the page, and hop back to previous levels to borrow code This level currently fails to build because it has no code. Can you add a return statement at the end? Don't worry about what it's actually returning (yet!)
Feel free to check previous levels!
ret
Now that we have an empty function, we can start building some code! Let's learn what a register is.
A register is like a variable, except there are a small number of them - you have about eight general purpose 64-bit integers registers on amd64 (we won't talk about floating point or other special registers):
- rax
- rbx
- rcx
- rdx
- rdi
- rsi
- rbp
- rsp
All mathy stuff that a computer does (add, subtract, xor, etc) operates on registers, not directly on memory. So they're super important!
Specific registers have some implicit meaning, mostly by convention. For example, when a function returns, its return value is typically put in rax. Let's do that!
To move a value into a register, use the
mov
instruction; for example:
mov rdx, 1
In a higher-level language this would be equivalent to:
rdx = 1 —
For this level, can you return the number '1337' from your function?
That means that
rax
must equal1337
when the function returns.
mov rax, 1337
ret
If you've made it this far, I bet you're wondering how to make your shellcode do something!
If you're familiar with Python, you might know how to use the
open()
function. If you know C, you might know thefopen()
function. But what these and similar functions have in common is one thing: they're library code. And because shellcode needs to be self contained, we don't have (easy) access to library code!So how do we deal with that?
Linux has something called a
syscall
, or system call. Asyscall
is a request that a program makes that asks Linux - the kernel - to do something. And it turns out, at the end of the day, all of those library calls ultimately end with a syscall. Here is a list of available syscalls on x64 (alternative)To perform a syscall:
- The number for the desired syscall is moved into rax
- The first parameter is moved into rdi, the second into rsi, and the third into rdx (there are others, but not many syscalls need more than 3 parameters)
- Execute the
syscall
instructionThe second
syscall
executes, Linux flips into kernel mode and we can no longer debug it. When it's finished, it returns the result inrax
.For this challenge, we're going to call
sys_exit
to exit the process with exit code 99.Can you prepare
rax
andrdi
with the correct values to exit?As always, feel free to mess around as much as you like!
mov rax, 60
mov rdi, 99
syscall
Before we learn how to use the Really Good syscalls, let's try something fun: crash our shellcode on purpose!
You might think I'm mad, but there's a method to my madness. Run the code below and watch what happens! No need to modify it, unless you want to. :)
Be sure to look at the debugger to see what's going on! Especially notice the top of the stack at the
ret
instruction.
push 0x12345678
ret
What happened in the last exercise? Why did it crash at
0x12345678
? And did you notice that0x12345678
was on top of the stack whenret
happened?The short story is this:
call
pushes the return address onto the stack, andret
jumps to it. Whaaaat??This is going to be long, but hopefully will make it all clear!
Let's back up a bit. At any given point, the instruction currently being executed is stored in a special register called the instruction pointer (
rip
), which you may also hear called a program counter (pc
).What is the
rip
value at the first line in our code? Well, since we have a debugger, we know that it's0x13370000
. But sometimes you don't know and need to find out.The most obvious answer is to treat it like a normal register, like this:
mov rax, rip
ret
Does that work? Nope! You can't directly access
rip
. That means we need a trick!When you use
call
in x64, the CPU doesn't care where it's calling, or whether there's aret
waiting for it. The CPU assumes that, if the author put acall
in, there will naturally be aret
on the other end. Doing anything else would just be silly! So call pushes the return address onto the stack before jumping into a function. When the function complete, theret
instruction uses the return address on the stack to know where to return to.The CPU assumes that, sometime later, a
ret
will execute. The ret assumes that at some point earlier acall
happened, and that means that the top of the stack has the return address. Theret
will retrieve the return address off the top of the stack (usingpop
) and jump to it.Of course, we can execute
pop
too! If wepop
the return address off the stack, instead of jumping to it, the address goes into a register. Hmm! Does that also sound likemov REG, rip
to you?For this exercise, can you
pop
the address after the call - the No Op (nop
) instruction - intorax
then return?
call place_below_the_nop
nop
place_below_the_nop:
pop rax
ret
So remember how last level, we got the address of
nop
and returned it?Did you see that
nop
execute? Nope! We jumped right over it, but stored its address en-route. What can we do by knowing our own address?Well, since shellcode is, by definition, self-contained, you can do other fun stuff like include data alongside the code!
What if the return address isn't an instruction at all, but a string?
For this next exercise, we include a plaintext string -
'Hello World'
- as part of the code. It's just sitting there in memory. If you look at the compiled code, it's all basicallyHello World
, which doesn't run.Instead of trying to run it, can you
call
past it, andpop
its address intorax
?Don't forget to check the debugger after to see it in
rax
!
call something
db 'Hello World',0
something:
pop rax
ret
Remember syscalls? Earlier, we used them to call an exit. Now let's try another!
This time, instead of getting a pointer to the string
Hello World
, we're going to print it to standard output (stdout).Have another look at the syscall table. Can you find
sys_write
, and use to to print the stringHello World
! to stdout?Note: stdout's file descriptor is
1
.
call something
db 'Hello World!',0
something:
pop rsi
mov rax, 1
mov rdi,1
mov rdx, 12
syscall
ret
We're getting dangerously close to do something interesting! How about that?
Can you use the
sys_open
syscall to open/etc/passwd
, then return the file handle (inrax
)?Have another look at the syscall table. Can you call
sys_open
on the file/etc/passwd
, then return the file handle? Here's the syscall table again.
call placeholder
db '/etc/passwd',0
placeholder:
pop rdi
mov rax, 2
mov rsi, 0
mov rdx, 0
syscall
ret
Do you feel ready to write some useful code? We hope so! You're mostly on your own this time! Don't forget that you can reference your solutions from other levels!
For this exercise, we're going to read a specific file… let's say,
/var/northpolesecrets.txt
… and write it to stdout. No reason for the name, but since this is Jack Frost's troll-trainer, it might be related to a top-secret mission!Solving this is going to require three syscalls! Four if you decide to use
sys_exit
- you're welcome to return or exit, just don't forget to fix the stack if you return!First up, just like last exercise, call
sys_open
. This time, be sure to open/var/northpolesecrets.txt
.Second, find the
sys_read
entry on the syscall table, and set up the call. Some tips:
- The file descriptor is returned by sys_open
- The buffer for reading the file can be any writeable memory - rsp is a great option, temporary storage is what the stack is meant for
- You can experiment to find the right count, but if it's a bit too high, that's perfectly fine
Third, find the
sys_write
entry, and use it to write to stdout. Some tips on that:
- The file descriptor for stdout is always 1
- The best value for
count
is the return value fromsys_read
, but you can experiment with that as well (if it's too long, you might get some garbage after; that's okay!)Finally, if you use
rsp
as a buffer, you won't be able toret
- you're going to overwrite the return address andret
will crash. That's okay! You remember how tosys_exit
, right? :)(For an extra challenge, you can also subtract from
rsp
, use it, then add torsp
to protect the return address. That's how typical applications do it.)Good luck!
call placeholder
db '/var/northpolesecrets.txt',0
placeholder:
pop rdi
mov rax, 2
mov rsi, 0
mov rdx, 0
syscall
push rax
sub rsp, 16
mov rax, 0
mov rdi, [rsp+16]
mov rsi, rsp
mov rdx, 138
syscall
mov rax, 1
mov rdi, 1
mov rdx, 138
syscall;
mov rax, 60
mov rdi, 0
syscall
The 'Success! notice at the end of the challenge lets us know that we can add ?cheat
after the URL (before the #
) to unlock the recommended solutions