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. There is a limited number of instructions that can be issued to a processor. Understand that at this "low level", this close to the machine, we are designing code to execute processor-specific sequences. This would imply that 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, I have chosen to document both intel standard syntax and some AT&T System V syntax for the techniques described by Security 101.
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.
On the low level, a machine is actually very simple. A processor is able to understand absolute hexadecimal values, then add, subtract, multiply, and divide them by whatever 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 with a bit more english strapped to it. For now, that's all I'll describe in reference to the C programming language. More details are included later on in Security 101.
Instructions & Concepts
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.
Your general-purpose registers are 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
mov eax, 1
In AT&T System V syntax, we would say
movl $1, %eax
I'll explain this quite quickly for you. 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. Say we want to add 3 to eax, we would say:
Intel:
add eax, 3
ATT:
addl $3, %eax
This is the simple explanation of some instructions. A full list of instructions which we will be using and a table of their purpose is as follows :
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 (register = register + 1)
- dec Decrement specified register (register = register - 1)
- Special Instructions***
- call Calls a function
- jmp Jumps to a different segment of code
- ret Returns from function
- xor Bitwise exclusive or
- push Push something onto the stack
- pop Pop the value off the stack and place it in a register
- Special Jumps***
- jne Jump if not equal
- je Jump if equal
- jge Jump if greater than or equal
- jle Jump if less than or equal
- jl Jump if less than
- jg Jump if greater than
- jz Jump if ZFLAG set
- jnz Jump if no ZFLAG set
- jp Jump with parity
- jnp Jump if no parity
The Stack
So obviously you'll want me to go over these so-called 'special instructions' and the 'special jumps' as soon as possible. Well lets look through this. First we need to get a solid understanding of functions, and then we also need to get a solid understanding of stack-based operations. The execution stack and the data stack are two different things and it 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. Let us suppose that the entry point to app.exe is 0x101c879d. Let's suppose for a moment that the code there is mov eax, 1, and the next segment of instruction is add eax, 3. The program would look something like the following in memory:
Mem Address Machine Code Assembly Instructions 0x101c879d \xb8\x01\x00\x00\x00 mov eax, 1 0x101c87a2 \x83\xc0\x03 add eax, 3
Binary & Hexadecimal
Counting
In case we missed something here, lets go over a bit of binary and a little bit of hexadecimal. A nybble consists of 4 bits, a half-byte, or one hexadecimal digit. 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 doubles. Lets take a quick detour for binary.
Binary works by doubling with each digit to the left. 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
In the above diagram, a "?" represents a 1 or 0. So, lets say we had 0010. This is the value 2. Now, when there is more than one "1" in a binary nibble, we add the two place-held values together, for example 0011 = 3 because the 2 and the 1 bits are active so we add 2 and 1 together to get three, where 1011 is 11 because the 8, 2, and 1 bits are activated, we add the three together to get the decimal value 11. 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
This kind of gives us an understanding of how hex values work, so lets revisit the previous shellcode of mov eax, 1 and add eax, 3.
Mem Address Machine Code Assembly Instructions 0x101c879d \xb8\x01\x00\x00\x00 mov eax, 1 0x101c87a2 \x83\xc0\x03 add eax, 3
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. In stead of viewing it by one line of assembly instructions at a time, we'll view it one Byte at a time.
Mem 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:
mov eax, 1 \xb8\x01\x00\x00\x00 add eax, 3 \x83\xc0\x03
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].
mov eax, 1 \xb8 \x01\x00\x00\x00 mov eax, 0x00000001 \xb8 \x01\x00\x00\x00
All of these instructions are synonymous. Now lets analyze the add eax, 3 instruction:
add eax, 3 \x83\xc0 \x03
add eax, \x03 \x83\xc0, 3
Overflows
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]. Now lets get to the stack. The stack is a linear region of memory with a particularly allocated size, which cannot exceed 16 megabytes, 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 :
Mem Addr 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.
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 push 0x1badc0de \x68\xde\xc0\xad\x1b push 0xabad1dea \x68\xea\x1d\xad\xab push 0xcafebabe \x68\xbe\xba\xfe\xca push 0xdeadbeef \x68\xef\xbe\xad\xde
Alrighty, so say we execute that code starting with the top line first. The data stack will now look something like the following :
Memory Address/Range 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:
Mem 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:
Special Registers
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:
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 add eax, 3 0x001010a0 \xc3 ret 0x001010a1 \xb8\x02\x00\x00\x00 mov eax, 2 0x001010a6 \xe8\xf2\xff\xff\xff call 0x0010109d 0x001010ab \x90 nop
Lets say our EP or 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 :)