Before I start this tutorial, shout out to RedBullAddicted. This will have some overlap with his tutorial (which is awesome; I'm stealing some of your formatting). If you want to get up and running "quickly" with Linux ASM, read his tutorial
HERE. This tutorial will be much more drawn out and in slightly different order, but will cover EVERYTHING necessary for writing shellcode on 32-bit Linux.
AbstractThis tutorial is meant to teach the reader the basics of 32-bit assembly language for the Intel Architecture (IA-32) on the Linux platform. Understanding a little bit of C programming might help you a little bit, but prior knowledge isn't completely necessary. Being comfortable in a Linux environment, however will definitely be quite helpful. Once I've covered the basics of ASM, I'll shift the focus to writing shellcode and other advanced low level applications. So let's begin!
0x01 - What is Assembly Language?Assembly (or ASM) is a low level language. In fact, it's the lowest level language that's readable by humans (If you claim that you can "read" binary, you're full of shit). Its purpose is to communicate with the microprocessor. Because there are multiple families of processors (Intel,
AMD, ARM, etc.) there are multiple flavors of assembly language, each one being particular to its processor family. Assembly code is sent through an Assembler and Linker to be turned into machine language (binary) which is read by the processor.
"So how does ASM correlate with higher level languages like C?" Good question. Let's go through a little scenario. You just wrote a C program that prints "Hello World!" and it's called "hello.c". Here are the steps that your code goes through on its way to being converted from a high(er) level language to assembly code, to machine code, and finally run on the system.
- First your C source code file "hello.c" is sent through the C pre-processor and is converted into pre-processed code "hello.i". The pre-processor includes header files, converts macros, and evaluates conditional compilation.
- Next, your pre-processed code "hello.i" is compiled and turned into assembly code "hello.s"
- After that, the assembly code is sent through the assembler and is turned into object code "hello.o". The assembler essentially translates assembly mnemonics into opcodes and resolves symbolic names to relative memory locations.
- The second to last step is sending the object code "hello.o" through the linker to be converted to an executable file "hello, or hello.exe". The linker resolves references to function calls (like printf, etc.) and links your object files to libraries.
- Finally, that code is sent through the loader when you run it, and is sent into process address space.
"Why the fuck are you teaching me 32-bit assembly, when more people are switching to 64-bit machines?" Well first of all, many machines still run 32 bit operating systems, and they will for a considerable amount of time. Secondly, you can refer to the old saying "You need to learn to crawl before you can learn to walk". Finally, there are differences in shellcoding for 32-bit systems and 64-bit systems.
0x02 - Setting Up Your EnvironmentFor this tutorial, I'm using 32-bit Ubuntu 12.04 LTS in a VirtualBox VM. It's ridiculously easy to set up. I'm not going to explain in too much detail how to do this because it's kind of outside the scope of this tutorial. However, I'll link to the download pages for each and write a couple notes for setting up the VM.
Click
HERE to download VirtualBox. Click
HERE to download Ubuntu 12.04 LTS (make sure you choose 32-bit!).
Steps for creating a VM in VirtualBox:
- Click New. Name it anything. For "Type" choose Linux. For "Version" choose Ubuntu.
- Set memory size to 1024MB (1GB).
- Choose the default settings for everything else.
- Click on your VM and click Settings. Click on Storage.
- Click on Empty (underneath Controller: IDE). Click on the small disk on the right, "Choose virtual CD/DVD"
- Find where you saved your ubuntu .iso and boot your VM! Now just install ubuntu and you're good to go!
One more thing. You're going to need to install nasm (the netwide assembler). It's the assembler that we're going to be using. Here's how to do that:
detective6@computer:~$ sudo apt-get install nasm build-essential
HOMEWORK 1: Find out some details on your particular CPU using the terminal. Don't worry, I'll be covering this in the next segment, but it's more valuable to experiment and fumble your way through stuff before just being told how to do something.0x03 - Homework 1 SolutionThere are actually multiple ways to view your CPU information in Ubuntu. The first way is a small utility called lscpu. detective6@computer:~$ lscpu
Architecture: i686
CPU op-mode(s): 32-bit
Byte Order: Little Endian
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 42
Stepping: 7
CPU MHz: 3260.097
BogoMIPS: 6520.19
L1d cache: 32K
L1d cache: 32K
L2d cache: 6144K
Everything there should be pretty straight-forward. But what if we want to check whether our CPU supports capabilities like FPU, MMX, SSE, SSE2, etc.? Well the way to get this information is by echoing the information contained in /proc/cpuinfo.detective6@computer:~$ cat /proc/cpuinfo
This will give us more information on our CPU such as the flags variable. The acronyms next to this are the additional capabilities that our CPU supports.0x04 - IA-32 ArchitectureLet's try to organize the computer into its most basic building blocks. The first and most important part is the CPU, where instructions are decoded and executed. We also have memory where program data, etc. is stored. Third is I/O devices like your monitor, keyboard, mouse, etc. Finally, the system bus connects all of them. The rest of this section will focus on the architecture surrounding the CPU.The CPU (central processing unit) consists of a few main components. The Control Unit retrieves and decodes instructions. The Execution Unit, surprise surprise, executes instructions! The CPU registers are internal memory locations that you can think of as variables. There are 8 general purpose registers, 6 segment registers, an EFLAGS register, and an instruction pointer register.General Purpose Registers (each one is 32 bits)- EAX: This is the accumulator register. It's commonly used for storing operands and result data.
- EBX: This is the base register. It usually points to data.
- ECX: This is the counter register. It's often used for loop operations.
- EDX: This is the data register, or I/O pointer.
These four registers can also be sub-referenced. For example, to get the lower 16 bits of EAX, you would say AX. This can be further sub-referenced. To get the lower 8 bits of AX, you would say AL. To get the upper 8 bits of EAX you would say AH.
- ESP: This is the stack pointer register. It points to the last item pushed to the stack.
- EBP: This is the base pointer register. It points to the bottom of the current stack frame.
- ESI: This is the source index register. It points to data for memory operations.
- EDI: This is the destination index register. It also points to data for memory operations.
These registers can only be sub-referenced once. So the lower 16 bits of ESP would be SP.
Segment Registers (each one is 16 bits)- CS: code segment
- DS: data segment
- SS: stack segment
- ES: extra segment
- FS: data segment
- GS: used for accessing CPU-specific memory
We'll touch on these in more depth as we move on to more advanced assembly programming.
EFLAGS Register (32 bits)Each bit in this register represents a different flag. It's not necessary to memorize all of them right now. We'll come back to this when we get to conditional statements.
EIP (32 bits)The EIP is the instruction pointer. It tells the computer the next instruction to execute. When doing reverse engineering and exploit development this is the HOLY GRAIL. You control the EIP, you control the flow of execution (thanks for the edit, m0rph).
HOMEWORK 2: Try to find a way to inspect the general purpose, segment, and eflags registers on your system.0x05 - Homework 2 SolutionSo you can only view the CPU's registers within the context of a running program. In order to see the values of these registers, we'll need to attach what's called a debugger to a running process. A debugger is simply a program that is used to test or "debug" programs. The GNU Debugger (gdb for short) is the de facto debugger for many GNU/Linux and Unix-like systems. This is the tool we'll be using. Learning all the intricacies of gdb can be a tutorial in itself, so I'm only going to go over the most necessary functions. If you want to learn more, just read the man pages or Google it.Since we haven't written any applications to debug yet, we're going to open up the Unix shell /bin/bash. So open up your terminal and type the following (I'll explain what everything means):detective6@computer:~$ gdb /bin/bash
(gdb) break main
Breakpoint 1 at 0x805eba6
(gdb) run
Starting program: /bin/bash
Breakpoint 1, 0x0805eba6 in main ()
(gdb) info registers
First we open up /bin/bash in gdb. Next we type "break main" to set a breakpoint at the entry to the function called "main". This breakpoint will ensure that the program pauses when we reach the main function. In follow, we type "run" to actually start the program. Once the breakpoint is reached and the program is paused, we can examine the registers by typing "info registers".You should see three columns now (they may be poorly formatted). The leftmost column is a list of the general purpose registers, followed by eip, followed by the eflags register, followed by the segment registers. The second column contains the hexadecimal values for each register. Finally the third column contains the evaluated values of each register. However, what if we want to just display EAX, or AX, or AL? We can achieve this using the "display" function:(gdb) display /x $eax
(gdb) display /x $ax
(gdb) display /x $al
Note that the "/x" simply means that we want to receive the value in hexadecimal format. Now, I mentioned before that EIP points to the next instruction that's supposed to be executed. What if we want to actually view what instruction this is? We can do that by running the "disassemble" command:(gdb) disassemble $eip
Disassemble essentially resolves the value in a register to a location in memory. This output may look a little confusing but don't worry, that's fine. See the little hash rocket on the left? That is pointing to the next instruction that will be executed if we continue running our program. Cool right? Play around with gdb for a while and get comfortable with it. Like I said, it can be a tutorial in itself.0x06 - CPU Modes and Memory LayoutThe IA-32 architecture supports 3 modes of operation:- System Management Mode: This is only used for power management tasks.
- Real Mode: This mode is utilized during power up or reset. It only has access to 1MB of memory, does not allow memory protection, and does not distinguish between kernel space and user space.
- Protected Mode: This is the mode that we will be focusing on. In a 32-bit system this can utilize up to 4GB memory (2^32. Epiphany moment!). It allows memory protection, privilege levels, and multi-tasking.
Our architecture also supports 3 different memory models. The segmented model uses a selector and an offset (effective address) to address memory space. The real-address model is basically a special case of the segmented model. However, we'll be using the flat memory model, where addresses are linear across the memory space.So what does memory look like when we're executing a program? Here's a crude diagram:0xFFFFFFFF-------------Kernel Space-------------Stack (Function Arguments and Local Variables)-------------#######-------------Heap (Dynamic Memory)-------------BSS (Uninitialized Data)-------------Data (Initialized Data)-------------Text (Program Code)-------------0x08048000 (below here, down to 0x00000000 is more data)It's important to note that the stack and heap aren't of fixed sizes. As the program runs, calls functions, and accesses memory, the stack and heap will grow and shrink. The stack grows "downward" toward lower memory addresses, while the heap grows "upward" toward higher memory addresses. Also, the area noted by "#######" is usually used for loading shared libraries. If you'd like to actually visualize a program in memory, load up gdb with /bin/bash again, set a breakpoint at main, run it, and type the following:(gdb) info proc mappings
This will show you the start and end addresses of different sections, their size, and their labels!0x07 - Intel vs AT&TI swear, we're going to start writing some assembly soon! But before we do, I want to touch note on a popular dichotomy within the assembly world. There are two syntax branches: Intel, and AT&T. Intel is commonly used on Windows, while AT&T is used primarily on Unix environments (since it was created at AT&T Bell Labs). You can look at the differences between the two HERE. I personally prefer the Intel syntax because it looks much cleaner. It takes a little getting used to the destination coming before the source, but cleanliness is well worth the extra effort. However, gdb uses AT&T syntax by default, so I'll show you how to change it real quick.detective6@computer:~$ gdb
(gdb) set disassembly-flavor intel
Now that we've set up our environment, gained an understanding of our CPU, and learned about the IA-32 architecture, it's time to finally write an assembly program!0x08 - Syscalls and their ArgumentsI lied. We're not writing assembly yet. There's one more topic I want to cover before tackling our first assembly program. Assembly is a very low level language, right? Right. So imagine if you had to write code from scratch just to do things like writing to disk, printing on the screen etc. Although it might be a cool little project, it would probably fucking suck if you're trying to learn assembly from scratch. We're going to leverage the OS for these more "complicated" tasks by invoking syscalls. Syscalls provide a simple interface for user space programs to interact with the kernel.Here's how they work. First a user space program issues an interrupt with some arguments. Then the CPU checks the interrupt handlers table and invokes the system call handler, which figures out the specific system call routine we want to invoke based on the arguments we passed. We're going to be using the simple "int 0x80" method, but more modern implementations use VDSO which stands for Virtual Dynamic Shared Object. If you want to read about that, click HERE.So where are these syscalls defined anyway? We can use two resources for this. To find out the names of syscalls and their respective number codes we can open up /usr/include/i386-linux-gnu/asm/unistd_32.h. Note that if you're using a different distro, this may be located in a completely different place. Then if we want to find out how a particular syscall should be invoked, we can just look up the man page.When passing arguments before a syscall, here is what you need to know. The syscall code is put into EAX. Then each argument is put into EBX, ECX, EDX, ESI, EDI in order. If we have more than 5 arguments then we'll pass them in as a structure (we'll cover this when we get to more advanced stuff). Finally, when the syscall returns it usually stores the return value in EAX.Alright. Now we're ready to write some assembly. I promise this time.0x09 - "Hello World!"Let's face it. "Hello World!" is boring. But it's a rite of passage for learning a new language. Plus, you don't have to print "Hello World!"; you can print anything! I'll be printing "Go fuck yourself." to make it a little less droll. So, open up your favorite text editor: nano, emacs, vim, etc. (I like Sublime Text because it's pretty). Create a file named "gofuckyourself.asm". All set? Good.The first thing I'll teach you about the syntax is that the comment symbol is actually the semi-colon. So to start a comment on a line, put a semicolon like so:; gofuckyourself.asm
; author: detective6
Now, nasm detects the beginning of a program by recognizing the _start variable, so we're going to declare that as a global variable like so:global _start
What else do we need for our program? Our referenced string of course! We want it initialized so we're going to put it in the .data section. We're also going to need the length of our string (you'll see why shortly). So here's how to start the data section and declare both of our variables (you don't have to indent, but it helps with readability):section .data
message: db "Go fuck yourself."
message_len equ $-message
Declaring the data section is pretty self-explanatory. Now we declare our variable called "message" with the db command. This stands for "declare bytes" and can be used to set one or more bytes to values, so we set our message variable to the string "Go fuck yourself.". Next we use the equ command to set message_len equal to the length of the message. This is calculated by "$-message". The $ gets the current address and then we subtract the address of the beginning of message to find its length.We're ready to start the primary part of our program. Remember, the code is stored in the text section, and nasm will look for our start variable. So we open our code area like so:section .text
_start:
Now, let's think about what our program is going to do and break it into its essential blocks of code. First we want to print the text "Go fuck yourself." to the screen. Then we want to exit the program elegantly. Remember how I mentioned syscalls before? You'd better. We're going to be using two of them: write(), and exit(). So let's look up how to call the write() function.
If you open up unistd_32.h again, we can see that the write() function is associated with the number 4. Now we'll look up its man page by typing:
man 2 write
We see it takes the following arguments:
- int fd (this is where we want to write to)
- const void *buf (this is the reference to the string we want to write)
- size_t count (this is the length of our string)
Now since we want to write to STDOUT, we know that fd should be equal to 1. We also know what our string and string length are so we're ready to call the function!
section .text
_start:
; print "go fuck yourself" on the screen
mov eax, 0x4
mov ebx, 0x1
mov ecx, message
mov edx, message_len
int 0x80
Notice how first we put the syscall code for write [4] into eax. The mov command is used to move values to / from registers and memory. Remember, we're using intel syntax so the destination comes first. After we move the syscall code into eax, we move each argument into its respective register. Please note that I consistently used hexadecimal. You can use hexadecimal, binary, or whatever, just please be consistent. Finally, we call invoke the syscall by issuing an interrupt with the command int 0x80
Now all we have to do is gracefully exit the program! Let's look up the code for exit(). It happens to be 1. When we look at its man page we see it only takes one argument (int status). This status can be anything, as a success value is anything greater than 0. So let's arbitrarily set it to 7:
; exit the program gracefully
mov eax, 0x1
mov ebx, 0x7
int 0x80
And that's it! We've just written our first program! Here's the full program:
; gofuckyourself.asm
; author: detective6
global _start
section .data
message: db "Go fuck yourself."
message_len equ $-message
section .text
_start:
; print "go fuck yourself" on the screen
mov eax, 0x4
mov ebx, 0x1
mov ecx, message
mov edx, message_len
int 0x80
; exit the program gracefully
mov eax, 0x1
mov ebx, 0x7
int 0x80
Now we just need to assemble it and link it. We'll be making an ELF32, by the way. Here's how to do that:
detective6@computer:~$ nasm -f elf32 -o gofuckyourself.o gofuckyourself.asm
detective6@computer:~$ ld -o gofuckyourself gofuckyourself.o
Now we have an executable! Execute it like so:
detective6@computer:~$ ./gofuckyourself
Go fuck yourself.detective6@computer:~$
Yes, we forgot to put a carriage return, but that's not important right now. If you'd like to see the return value, after you execute your program, type this:
detective6@computer:~$ echo $?
HOMEWORK: Play around with your program in gdb. Try to figure out how to step through it and examine the registers after each piece of code is executed.0x0A - Homework 3 Solution (Coming Soon!)