Assembly Basics
Contents
Basic Assembly
Preface
Understanding of assembly and stack growth are important for the machine coder. In assembly, there are a limited amount of temporary variables, also called registers, with only the stack or heap to contain additional application data. These registers are of a fixed size and fulfil various functions; the general purpose registers work as small variables for use by the machine coder, whereas special registers such as the instruction pointer fulfil specific functions.
There is a limited number of instructions that can be issued to a processor. While assembly instructions can be compared to the functions of higher-level languages, it is important to keep in mind that they are processor-specific operations that are performed, often peforming alterations on the registers mentioned in the previous paragraph. It is possible to write cross-operating-system shellcoded applications, also known as flat binaries, or flat binary applications.
Because of the limitations of most shellcoding and overflow tutorials, the code examples and notations in this article will be given in both AT&T and Intel Syntax. Intel Syntax and AT&T System V syntax are syntax standards for coding assembly. The two syntaxes are both translated into the same machine code, however they are translated from machine code to human-readable code differently. On Windows XP SP2 systems, we will be demonstrating our code in Intel 32 Bit Standard Syntax. On Linux and UNIX systems, we will be demonstrating our code in AT&T System V syntax, of course for the intel 32 bit processor.
Again, there is no functional difference between the two syntaxes. They are simply methods of displaying assembly (as much as assembly itself is a method of displaying machine code); opinions differ on which of the two is more readable.
On the low level, a machine is actually very simple. A processor is able to understand absolute hexadecimal values, then perform mathematical operations(add, subtract, multiply, divide) upon them as it is asked to. Other than that, the functionality of a processor is mostly limited to memory operations and execution procedures, such as the management of functions and stack growth.
The C programming language is more or less assembly that is arranged so that it is more easily written by humans, including the C libraries that provide a framework with which to work.
From assembly to machine code
When we write code in assembly, we are writing in a form of "computer shorthand" - that is to say that assembly is a way of writing machine code that is relatively human friendly. When we want to convert our code into an executable program, we do this in two stages; firstly, we pass it through an assembler. The assembler translates the assembly instructions (which are, in effect, mmemonics) into their machine code 'opcode' equivalent. It also converts memory references into symbolic references to avoid manual calculation of memory locations. These opcodes are the numerical representations of mathematical operations - they are what most people envision when they think of the 'binary code' of a program, although they are usually represented in a demical, hexadecimal or octal format.
The next stage in creating an executable is linking. Linking combines various object files into a single executable, and also creates references to dynamically linked libraries. More information of linking can be found in the section on the C Calling Convention. In effect, linking strings the different object files that make up your completed program into one executable that can access all of the code contained in these objects.
Binary & Hexadecimal
Counting
A nybble consists of 4 bits, a half-byte, or one hexadecimal digit - all three of these are the same thing. Hex works in conjunction with binary based on this fact. Hexadecimal is base 16, using the characters 0-9 and a-f, each nybble is able to represent values 0-15. Binary works by base 2, using only the values 0 or 1, corresponding to the on or off state.
In order to construct binary numbers easily, it is useful to remember that the value of a place in a binary number is twice that in the place to the right. In other words, because binary assumes that 1 is on and 0 is off, we can only represent values with static place holders. A nybble takes the following layout :
? ? ? ? 8 4 2 1
That is to say, 0001 would have a value of 1, whereas 0100 would have a value of 4. 0101, on the other hand would have a value of 4 + 1; 5. Binary is the same as our own numerary system, the key difference being that there are only 2 'numbers' (0-1 instead of 0-9) that a place value can hold.
In the above diagram, a "?" represents a 1 or 0. In hexadecimal, however, these values are represented differently.
Hex Nybble | Binary | Decimal |
---|---|---|
0 | 0000 | 0 |
1 | 0001 | 1 |
2 | 0010 | 2 |
3 | 0011 | 3 |
4 | 0100 | 4 |
5 | 0101 | 5 |
6 | 0110 | 6 |
7 | 0111 | 7 |
8 | 1000 | 8 |
9 | 1001 | 9 |
a | 1010 | 10 |
b | 1011 | 11 |
c | 1100 | 12 |
d | 1101 | 13 |
e | 1110 | 14 |
f | 1111 | 15 |
Analyzing Code
Time to go to expanded view and show you WHY the addresses work the way they do, notice that the machine code in the middle contains a certain amount of bytes. Now we'll take a look at the expanded view of the execution stack or what is refered to as the .text segment. Instead of viewing it by one line of assembly instructions at a time, we'll view it one Byte at a time.
Memory Address | Machine Code Byte |
---|---|
0x101c879d | \xb8 |
0x101c879e | \x01 |
0x101c879f | \x00 |
0x101c87a0 | \x00 |
0x101c87a1 | \x00 |
0x101c87a2 | \x83 |
0x101c87a3 | \xc0 |
0x101c87a4 | \x03 |
Now if we look at the expanded view, we can tell how 0x101c87a2 is the beginning of the next line of code, and even get a feel for certain raw instructions. How can we do this? Well, we can see the code for each instruction:
<syntaxhighlight lang="asm"> mov eax, 1 ; \xb8\x01\x00\x00\x00 add eax, 3 ; \x83\xc0\x03 </syntaxhighlight>
So lets rip this apart a little bit. We know that the eax register can contain a DWORD value, so lets extrapolate on the mov eax, 1 instruction. Because eax can hold a DWORD value, we can safely assume that the machine code instruction for moving or setting a value to the eax register must be done in proper DWORD format, that is, it must take 4 bytes. So we notice that the value \x01 is followed by \x00\x00\x00. These are three "null bytes". Seeing these null bytes and knowing the way that assembly operates, we can safely say that the value of eax is assigned backwards in DWORD format. Following all of these logical trains of thought, we can draw the following conclusions:
The machine code instruction "\xb8" is equivilent to mov eax, [dword].
<syntaxhighlight lang="asm"> mov eax, 1 ; \xb8 \x01\x00\x00\x00 mov eax, 0x00000001 ; \xb8 \x01\x00\x00\x00 </syntaxhighlight>
All of these instructions are synonymous. Now lets analyze the add eax, 3 instruction:
<syntaxhighlight lang="asm"> add eax, 3 \x83\xc0 \x03
add eax, \x03 \x83\xc0, 3 </syntaxhighlight>
Can also be seen to be all synonymous. Therefore we can draw the conclusion that the machine code instructions \x83\xc0 is equivilent to the assembly instructions add eax, [byte].
Instructions & Concepts
Registers
So, now we have a good framework of the concepts of assembly language, but how can we see them be utilized to actually do something? Well, we'll start with explaining a bit about registers. General purpose registers on an intel 32 bit processor are 4 bytes in length, also referred to as a "double word" or "DWORD". A Byte consists of 8 bits. It takes four bits to compose a hexadecimal digit, or a nybble.
The general-purpose registers include eax, ecx, ebx, edi, and edx. Other, non general purpose registers are esi, esp, ebp, and eip. So lets test out a few small instructions to get the hang of this. In intel syntax, to put the value of 1 into eax, we will say
<syntaxhighlight lang="asm">mov eax, 1</syntaxhighlight>
In AT&T System V syntax, we would say
<syntaxhighlight lang="asm">movl $1, %eax</syntaxhighlight>
Intel syntax uses the format:
[instruction] [destination],[source]
Whereas AT&T System V syntax uses the syntax format:
[instruction] [source],[destination]
Now in our small amount of code, eax = 1. If we want to add 3 to eax, we would say:
Intel:
<syntaxhighlight lang="asm">add eax, 3</syntaxhighlight>
AT&T:
<syntaxhighlight lang="asm">addl $3, %eax</syntaxhighlight>
This is the simple explanation of some instructions. A full list of instructions and a table of their purpose is as follows :
Basic Arithmetic
Instruction | Purpose |
---|---|
ADD | Adds value to register |
SUB | Subtracts value from register |
IMUL | Multiplies value into register |
IDIV | Divides value into register |
MOV | Places value into register |
CMP | Compare register with value |
INC | Increment specified register |
DEC | Decrement specified register |
Bitwise Operations
- xor
- or
- and
- not
- test
- shl
- shr
- ror
- rol
Control Flow & Stack Instructions
Instruction | Description |
---|---|
CALL | Calls a function |
JMP | Jumps to a different segment of code |
RET | Returns from function |
PUSH | Push something onto the stack |
POP | Pop the value off the stack and place it in a register |
Instruction | Description |
---|---|
JNE | Jump if not equal |
JE | Jump if equal |
JGE | Jump if greater than or equal to >= |
JLE | Jump if less than or equal to >= |
JL | Jump if less than |
JG | Jump if greater than |
JZ | Jump if ZFLAG set |
JNZ | Jump if ZFLAG not set |
JP | Jump with parity |
JNP | Jump if no parity |
A simple exit program in AT&T Syntax
This is an example of a very simple program that simply makes the exit system call in linux. In linux, the exit system call is called by applying a kernel interrupt with the int instruction in the form "int $0x80". When it is called, a specific system call (which is an integral function of the operating system) will be executed - we select which system call to execute with a value stored in the eax register. In this case, we are calling exit - system call 1, and exiting with the exit code 03.
.section .text _start: |
Note that this is a very simple example that does nothing but exit. It also employs a GNU/Linux system call; system calls are rarely uniform between Operating Systems, and in particular between Linux and Windows.
If this program was saved to exit.s, you would go through the following steps to compile and link your program:
as exit.s -o exit.o # convert our code into an object file ld exit.o -o exit.out # link our object file into a prepared ELF executable ./exit.out #execute our program |
If the program exits successfully, you should be able to type echo $? immediately after the program finishes and see the code 3 returned.
Special Registers
These registers are not intended to be used for general data processing, but have some specific function that they are used by the processor for.
ESP
Stack Pointer - This register remembers the current index of the stack. At the beginning of the small code with the pushes, esp is 0x100010ff, and then it becomes 0x100010f8, f0, so on and so forth as DWORD values are pushed onto the stack. The program uses this to keep track of where it is pushing and popping data from inside of the stack.
EIP
Instruction Pointer - This register represents the location of the actual code executing, for example when add eax, 1 is executed, eip is the memory address of the \x83 byte.
EBP
Base Pointer - This points to the very bottom of the stack, however it can be used to hold addresses of other bases.
ESI
Function call register - when a function is called using the call instruction, the location of the beginning of the function is placed into esi.
Needless to say, our goal when attempting a buffer overflow is going to be manipulating the value of the eip register to make the computer execute our code as opposed to the original code. There are several ways to do this, but first we need to look at a few of our "special instructions" in assembly:
The Stack
In order to understand how special instructions and special jumps are used, it is necessary to have a solid understanding of the nature of functions and stack-based operations. The execution stack and the data stack are two wholly seperate things and must be understood as such.
When an application loads into memory, it begins at what is referred to as the "entry point" of the binary. Program execution then occurs in increasing memory addresses. Memory addresses are stored in DWORD values. For example, suppose that the entry point to app.exe is at the hexadecimal address value 0x101c879d in memory. Suppose that the code there is 'mov eax, 1' (or mov 1, %eax), and the next segment of instruction is 'add eax, 3'. The program would look something like the following in memory:
Memory Address | Machine Code | Assembly Instructions |
---|---|---|
0x101c879d | \xb8\x01\x00\x00\x00 | <syntaxhighlight lang="asm">mov eax, 1</syntaxhighlight> |
0x101c87a2 | \x83\xc0\x03 | <syntaxhighlight lang="asm">add eax, 3</syntaxhighlight> |
The stack is a construct used to make procedural execution and storage easier - it takes its name from the order in which it is altered; it is possible to "push" a value onto the "top" of the stack, or "pop" a value from the "top" of the stack. Keep in mind that the stack starts at a relatively high address and grows downards in memory, so the "top" of the stack is actually located at a lower memory adress than the "bottom".
The execution stack refers to the "stack" of instructions to be executed in a binary. When a program loads into memory, the entry point of the binary will be at the top of the execution stack. The data stack, on the other hand, is used to store data for later use - such as when a function is needed. The use of the data stack is vital to the C calling convention, which is a technique of executing modular code in a program that takes its name from the method that the C programming languages uses to execute functions.
The main reason for usage of the stack is the fact that you can arbitrarily push and pop elements onto or off from the stack without having to worry about the actual memory address that they are located at. Furthermore, the stack pointer(esp or %esp) is altered every time and element is added to or removed from the stack. It is possible to use this to refer not only to the element at the top of the stack, but to other elements within the stack. It is also possible it allocate or deallocate space on the stack simply by incrementing or decrementing the stack pointer.
Overflows
The stack is a linear region of memory with a particularly allocated size, which cannot exceed 16 megabytes on 32 bit x86, for various reasons. Think of the stack like a stack of paper. While the location of each piece of paper is stored in a hexadecimal DWORD address, the stack grows backwards. This is to say that a stack is assigned a certain memory range and that range of memory is written to from the highest value to the lowest, not the lowest value to the highest. The stack is considered a part of the .data segment of an application, or the .bss segment. This is important because of something called DEP, or Data Execution Prevention. The stack region of an application is the region that can be "overflowed" to allow us to execute arbitrary machine code, however because it is a data region of the program certain safeguards have been put in place to prevent code located within the stack from executing.
Lets make a fake stack, for the sake of example and explanation so that the reader may better grasp this concept. lets say that our stack exists between addresses 0x100010ff and 0x10001000. The stack would look like this, in memory :
Memory Address/Range | Location | Contents |
---|---|---|
0x10001000 | [Top of the Stack] | |
0x10001001-0x100010fe | [Data] | |
0x100010ff | [Bottom of the Stack] |
Now we can see that the top of the stack has a lower memory address than the bottom of the stack. Now we're going to need a bit more information about our non-general purpose registers and a bit more information about our stack operation special instructions.
Push & Pop
Instruction | Purpose |
---|---|
PUSH | "Pushes" a value on top of the stack |
POP | "Pops" a value off of the top of the stack |
Obviously push applies to both registers as well as static data. For example, we can push direct data in hexadecimal format like push 0x1badc0de or we can push a byte like push 0x1b or we can push a register's dword value like push eax. Pushing a nybble is not supported. Lets go over the push and pop concept a bit more with our "fake stack" that we're creating for the sake of education. My fake machine code is as follows :
Assembly | Machine Code |
---|---|
<syntaxhighlight lang="asm">push 0x1badc0de</syntaxhighlight> | \x68\xde\xc0\xad\x1b |
<syntaxhighlight lang="asm">push 0xabad1dea</syntaxhighlight> | \x68\xea\x1d\xad\xab |
<syntaxhighlight lang="asm">push 0xcafebabe</syntaxhighlight> | \x68\xbe\xba\xfe\xca |
<syntaxhighlight lang="asm">push 0xdeadbeef</syntaxhighlight> | \x68\xef\xbe\xad\xde |
Alright, so say we execute that code starting with the top line first. The data stack will now look something like the following :
Memory Address | Location | Value |
---|---|---|
0x100010ff | [Top of Stack] | ??? |
0x100010f0-0x100010f3 | "deadbeef" | |
0x100010f4-0x100010f7 | "cafebabe" | |
0x100010f8-0x100010fb | "abad1dea" | |
0x100010fc-0x100010ff | [Bottom of Stack] | "1badcode" |
A detailed view of the bottom of the stack:
Memory Address | Value |
---|---|
0x100010f8 | "ab" |
0x100010f9 | "ad" |
0x100010fa | "1d" |
0x100010fb | "ea" |
0x100010fc | "1b" |
0x100010fd | "ad" |
0x100010fe | "c0" |
0x100010ff | "de" |
So we can sort of understand how the stack grows backwards now, as 1badcode was the first thing to get pushed onto the stack and is at the bottom of the stack. Think of the stack like a stack of papers. When you push something onto it, its like putting a piece of paper on it. When you push something else on it, you're putting another piece of paper on the first piece of paper. This is where these "special registers" come into play.
The C Calling Convention
As mentioned previously, one of the data stack's most useful applications is as a placeholder for important data when executing a function or subprocess. Although there are multiple methods of doing this, one very widely used technique is known as the C Calling Convention, after the language that originally employed it.
As mentioned in above, there are several ways in which the stack can be used by an executable, and the C calling convention applies all of them. Firstly, it is possible to use the push and pop instructions to add to or remove from the top of the stack.
|
It is possible to reference a value contained in the stack using the stack pointer register, esp. The stack pointer always holds the memory address of the top of the stack. This allows you to create or remove space from the stack by incrementing or decrementing esp, and it allows you to reference items stored on the stack by using indirect addressing. with esp.
|
Before a function can be called, it must be defined in the data section of the code. In, AT&T syntax, this is done with:
.type function_name, @function |
In order to call this function, push each paramater of the function onto the stack in reverse order:
pushl $5 pushl %ebx |
Once this is done, call the function with the call speical insruction:
call function_name |
The call instruction pushes the address of the instruction after the call onto the top of the stack. This is the address that would normally be stored in %eip, the instruction pointer. The reason this address, known as the return address, is pushed to the stack is that the function is called by changing the instruction pointer; pushing the return address ensures that once our function has completed, we can return to the program's normal execution. After it has pushed the return address onto the stack, it modifies the instruction pointer so that it points to the first line of our function.
Example code:
.section .data _start: pushl $5 pushl $3 call someoname somename: <function code> |
At the point we execute the call instruction, our stack looks like this:
Argument 2 |
The function code can contain whatever is needed to achieve the desired effect, but once the function has completed it must be handled properly. If any changes have been made to the stack, they must be altered so that the return address is once more at the top of the stack. We then use the ret instruction to pop the return address back into eip, returning execution to the main body of the program.
Example of an add1 function [AT&T syntax]:
.section .text .type add1,@function .globl _start _start: pushl $5 #this is our argument call add1 #saves the return address and transfers execution to the add1 label #add1 happens here movl %eax, %ebx #move the return value from add1 into %ebx for exit call movl $1, %eax #exit is sycall 1 int $0x80 #kernel interrupt, exiting (syscall 1) with exit code equal to return value from add1 add1: pushl %ebp #push the value of ebp so we can use it to store values movl %esp, %ebp #store the value of esp into ebp so we can use it as a static pointer to the top of the stack at this point movl 8(%ebp), %eax #move the argumen into eax incl %eax #add1 to eax #now we retore everything so the return address is at the top of the stack movl %ebp, %esp #restore the stack pointer so it points to the stored value of ebp popl %ebp #pop the value of ebp back into ebp, and now the return address is at the top of the stack ret #pop the return address into eip and resume execution |
Although assembly is a very powerful tool in the right hands, it is often counterproductive to write your own code when libraries exist to perform the functions you require. This is more true in assembly than any other language, because assembly operates on such a low level; a task as simple as printing to STDOUT can require a large amount of effort and coding in assembly.
The libraries that a program can directly reference are known as shared objects (or simply SO's) under a UNIX architecture or Dynamic Linked Libraries (DLLs) under Windows. They can be called with the C Calling Convention, but they first must be linked into the program binary. After the assembler has translated the code of a program into object files, these objects are then linked together so that when the executable file is called, any necessary libraries or objects will be dynamically loaded. In order for a specific shared object file - for example, the libc libraries - to be called by the program, the shared object needs to be passed to the linker so that it knows to include it.
It is from this process that ELF format for executables takes its name - "Executable and Linkable Format".
An example of assembling and linking a program that references libc under GNU/Linux may be as follows:
as file.s -o file.o :: this is where we assemble our code into an object file, file.o |
Looking at the link command, we can see that we have called the dynamic linker to link our object files into an output executable. Furthermore, we have passed ld the '-lc' option, which indicates that the libc libraries should also be added to the linked executable. For a program that is properly linked with the libc shared object, it would be possible to call on functions from that library using the C Calling Convention. For example, to call on the printf function:
.section .data |
Special Instructions
JMP
Jumps to a location - this location can be defined by a byte offset (jump this many bytes forward or backward), but can also be defined with a DWORD memory address. The jmp instruction effectively modifies the value of the eip register to match that of the location to jump into.
CALL
Calls a function - this is a somewhat difficult concept in assembly, however a function will be demonstrated before the end of this section. When a call instruction is executed, the location of the function that is being called is placed in the esi register, while the location of the instruction immediately after the call is pushed to a stack for return purposes
RET
Exits a function - keep in mind that the call instruction pushes the location of the instruction after it to the stack, so at the end of the function, assuming that the esp register has the same value it contained when the function was first called, ret will jump back to the value that call pushed onto the stack. In other words, call pushes a pointer to the stack for the ret instruction to use when the function is finished executing so that the program can return to "normal" execution flow. This is what is being exploited during a buffer overflow attack.
NOP
NO OPERATION - this instruction literally does nothing. It is the equivilent of saying do nothing.
Call and Return
Lets make another fake flat binary. Reason being, we have to demonstrate the call and return sequence of assembly so that the reader can understand exactly what's going on here. Lets make our function that we are calling near pointless. There's not really much reason to have some uber-awesome code in this part of the article, we are still learning basic concepts. Our function will simply add 3 to the current value of eax. So here goes with another fake program.
Memory Address | Machine Code | Assembly Instructions |
---|---|---|
0x0010109d | \x83\xc0\x03 | <syntaxhighlight lang="asm">add eax, 3</syntaxhighlight> |
0x001010a0 | \xc3 | <syntaxhighlight lang="asm">ret</syntaxhighlight> |
0x001010a1 | \xb8\x02\x00\x00\x00 | <syntaxhighlight lang="asm">mov eax, 2</syntaxhighlight> |
0x001010a6 | \xe8\xf8\xff\xff\xff | <syntaxhighlight lang="asm">call 0x0010109d</syntaxhighlight> |
0x001010ab | \x90 | <syntaxhighlight lang="asm">nop</syntaxhighlight> |
Lets say our entry point is at 0x001010a1. This would put the first instruction at mov eax, 2. When the call instruction is executed, it pushes 0x001010ab onto the stack. When the ret instruction at 0x001010a0 is executed, it pops the value 0x001010ab off of the stack and jumps to that location, jumping to the line after the call function, where no operation is preformed.
Lets take a closer look at all of these registers for a moment so we can try to go ahead and get a better idea of where all our application's data is stored in the computer's memory. The eax register is a standard 32 bit register (4 bytes), however it has several sub-registers, like the other standard 32 bit registers. The registers break down as follows :
That's all for now, have fun with bytecode-asm comparison. I reccomend OllyDBG or GDB to test and expirement. Both are free and available from our friend google :)