Article logo

CPU & Assembly

CPU & Assembly: Binary exploitation basics

Introduction


Today, we'll delve into the fundamentals of the CPU, assembly language, memory, and how programs operate at a low level. This serves as a foundational knowledge for Binary Exploitation. However, regardless of your field within computer science, this article will provide valuable insights. We won't discuss exploitation techniques here, as we're focusing on establishing basic concepts and knowledge necessary before delving into exploitation techniques. Let's begin.


NOTE:


I've simplified these concepts significantly. However, this is how programs operate in the real world, albeit with a bit more complexity.


What is the CPU?


A CPU (Central Processing Unit) is the primary component of a computer responsible for executing instructions, performing calculations, and managing data within a computer system. It serves as the brain of the computer, processing input from software programs and controlling the overall operation of the system.


CPU Architecture


CPU architecture refers to the design and structure of a Central Processing Unit (CPU). It encompasses elements such as the instruction set, organization of registers, data paths, control unit, and memory hierarchy. CPU architecture determines how instructions are processed, data is manipulated, and tasks are executed within a computer system.


What is memory?


Memory refers to the component where data and instructions are stored for immediate use by the CPU. It's like the computer's short-term memory. There are different types of memory, including RAM (Random Access Memory), which is volatile and used for temporary storage while the computer is running, and ROM (Read-Only Memory), which contains essential instructions and data that remain intact even when the computer is powered off. Memory allows the computer to quickly access and retrieve data needed for processing, facilitating smooth operation and multitasking.


What are instructions?


Instructions are commands given to the CPU (Central Processing Unit) to perform specific operations. These operations can include arithmetic calculations, data manipulation, input/output tasks, and control flow operations (such as branching and looping). Instructions are encoded in binary format and are executed sequentially by the CPU according to the program's logic. Each instruction typically consists of an opcode (operation code) specifying the type of operation to be performed and zero or more operands that provide the data or memory addresses needed for the operation. Instructions form the basic building blocks of programs and determine the actions performed by the computer.


What is Assembly?


Assembly language, often referred to as assembly, is a low-level programming language that closely corresponds to the machine language instructions of a specific CPU architecture. Unlike high-level programming languages, which use human-readable syntax and abstract away hardware details, assembly language provides a symbolic representation of machine instructions, making it easier for programmers to work directly with the underlying hardware.


In assembly language, each mnemonic instruction corresponds to a single machine language instruction, such as moving data between registers, performing arithmetic operations, or controlling program flow. Programmers write assembly code using mnemonic instructions, symbolic labels, and memory addresses, which are then translated into machine code by an assembler.


While assembly language programming requires a deep understanding of the underlying hardware architecture and can be more complex than high-level languages, it offers finer control over system resources and can be highly optimized for performance-critical tasks. As such, assembly language is commonly used in systems programming, embedded systems development, and performance-sensitive applications where efficiency and low-level control are paramount.


What are Registers?


Registers are small, high-speed storage locations within the CPU (Central Processing Unit) that are used to hold data temporarily during program execution. They are the fastest type of memory in a computer system, capable of storing and retrieving data much more quickly than main memory (RAM).


Registers are used by the CPU to store data that is being actively processed, including intermediate results of calculations, memory addresses, and control information. They are directly accessible by the CPU's arithmetic and logic units (ALU) and are used to perform arithmetic and logical operations, as well as to control program flow.


Common types of registers found in CPUs include:


General-Purpose Registers: Used to hold data and perform arithmetic and logical operations. Examples include the accumulator, data registers, and index registers.


Program Counter (PC) Register: Similar to the instruction pointer, it holds the memory address of the next instruction to be executed, especially in the context of branch instructions.


Stack Pointer (SP) Register: Points to the top of the stack in memory and is used to manage the stack during subroutine calls and returns.


The Base Pointer (BP) is a register in the x86 architecture that is used as a reference point for accessing data within the stack frame of a function. It typically points to the base or beginning of the current stack frame. The Base Pointer is often used in conjunction with the Stack Pointer (SP) to reference local variables, function parameters, and return addresses stored on the stack. It plays a crucial role in stack-based memory management and function calls within a program.


These registers mentioned above are the most important ones. We'll discuss them extensively and explore how they function within a program. Additionally, we'll treat the Program Counter, Return Pointer, and Instruction Pointer as the same register in this article. Although they are essentially identical, the naming convention differs between assembly syntaxes. However, since we'll be focusing on Intel x86 syntax, we'll refer to it as the Instruction Pointer.


Don't worry about the complexity of these words and concepts; they'll become clearer once you see them in practice.


In various CPU architectures, registers may have different sizes and names based on the word size of the architecture. Here's a brief overview:



What are memory addresses?


Memory addresses are numerical identifiers used to locate and access data stored in computer memory. In a computer system, memory is organized as a sequence of bytes, each with a unique address. These addresses serve as references to specific locations within the memory space.


When a program is executed, data and instructions are loaded into memory, and the CPU uses memory addresses to read or write data and execute instructions. For example, when a variable is declared in a program, it is assigned a memory address where its value is stored. Similarly, when an instruction is fetched for execution, its corresponding memory address is used to retrieve the instruction from memory.


Memory addresses are typically represented in hexadecimal notation and can range from 0x00000000 to 0xFFFFFFFF (on a 32-bit system) or even higher on a 64-bit system. The size of a memory address determines the maximum amount of memory that can be addressed by the CPU, known as the addressable memory space.


To grasp it better, let's use the following image as an example:



As you can see, on the left, there are numbers; these numbers represent memory addresses. On the right, the blue text represents instructions in assembly language. Notice that each instruction has its own corresponding memory address.


The Stack


The stack is a region of memory used by computer programs to store temporary data and manage function calls. It operates on a Last-In-First-Out (LIFO) basis, meaning that the most recently pushed data onto the stack is the first to be popped off.


When a function is called, its local variables and parameters are typically stored on the stack, along with the return address, which points to the instruction to resume execution after the function call. As functions are called and return, this data is pushed and popped off the stack accordingly.


The stack is typically positioned at the bottom of the memory address space in a computer system. This means that it occupies the lowest memory addresses, with addresses increasing as you move up the stack.


In a system with a downward-growing stack, such as x86 architecture, the stack starts at the highest memory address and grows downward as more data is pushed onto it. Conversely, in systems with an upward-growing stack, such as some embedded architectures, the stack starts at the lowest memory address and grows upward.


Regardless of the direction, the bottom of the stack is where new data is pushed, while the top of the stack is where data is popped off. This arrangement allows the stack to efficiently manage function calls, local variables, and other temporary data within a program.


Let's visualize a graphic representation of what the stack looks like:



As you can see, at the top are our previous instructions. However, as mentioned earlier, the stack is located at the bottom of our program.


Assembly instructions


Before continuing to talk about the stack and other related topics, you should learn some concepts about assembly instructions.


Instruction Pointer


As explained earlier, the instruction pointer (IP) contains the memory address of the next instruction. Now, let's create a graphic representation of how the instruction pointer works.


Note:


Memory addresses will be represented as decimal values from 1 to 14. Of course, it doesn't work like this, but this makes it easier to explain.



In this example, the Instruction Pointer (EIP) points to the 9th instruction. This means that the next instruction will be a mov eax, 0x10, which essentially means that we'll copy the value 0x10 into eax.


After executing the 9th instruction, eax will be equal to 0x10, and EIP will point to the next address, indicating the start of the 10th instruction:



As you can see, the EIP has been incremented by one and now points to the 10th instruction. This instruction will move the value 0x1 into ebx.



In the 11th instruction, there is a sub eax, ebx operation, indicating that the value stored in ebx will be subtracted from the value stored in eax (eax - ebx). As a result, the value stored in eax will become 0xf:



The next and final instruction, located at memory address 12, performs an add eax, 0x1 operation, which increments the value stored in eax by one unit (eax + 1).


Stack Pointer


The stack pointer is a register that holds the memory address of the top of the stack. It indicates the current position in the stack where data is pushed or popped during program execution.



Each section of the stack occupies 4 bytes, so to keep the stack pointer aligned, it should be incremented or decremented by 4 bytes.


The numbers highlighted in orange represent memory addresses corresponding to positions within the stack. This accounts for the 4-byte difference between them. The stack pointer doesn't indicate the values stored within these sections but rather their addresses.



In this example, the ESP (Stack Pointer) points to the address 0x14 within the stack. This section holds the value 0x45454545, representing the string "EEEE". So instead of referring to the stack pointer as 0x45454545, it is represented as the address 0x14, which points to the value 0x45454545.


ESP -> 0x14 -> 0x45454545


I explain this because it might be confusing when explaining the stack frame to see that the stack pointer points directly to the stack section without including the address. This might lead you to believe that ESP is equal to the value stored in that section. However, this is not the case. ESP never stores a stack value; it is a pointer (meaning a memory address) that references a stack value.


Playing with The Stack


We've already discussed what the stack is and how it operates. Now, let's visualize how it works with a graphic representation:



As you can observe, the Stack Pointer (ESP) currently points to the memory address 48 (the highest address). The subsequent instruction will be mov eax, 0x1234, which initializes the register eax with the hexadecimal value 0x1234.



At this point, the stack becomes involved. The subsequent instruction is a push eax, indicating that the value stored in eax will be pushed onto the stack. As a reminder, the push instruction decrements the stack pointer (considering the stack grows downward) and places the value of the specified register (in this case, eax) at that location.


The instruction push eax can be translated into two separate instructions: sub esp, 0x4, which decrements the stack pointer by 4 bytes, and mov [esp], eax, which moves the value of eax onto the stack at the location pointed to by ESP.



As depicted in the image, the Stack Pointer has decreased and now points to the value pushed from eax.


The next instruction performs a pop ebx, which removes the value pointed to by ESP from the stack and stores it in ebx. After moving this value to ebx, the ESP decreases by one, causing it to point to memory address 16.


The pop instruction can be translated into two separate instructions: mov ebx, [esp], which moves the value from the memory location pointed to by ESP into ebx, and add esp, 0x4, which increments the stack pointer by 4 bytes.



Control Flow - GOTOs - Jumps/Branches/Calls


To manipulate the program flow, assembly language provides jump instructions, commonly known as GOTOs. These instructions work by modifying the instruction pointer (IP or EIP) when executed. There are two main types of jump instructions: unconditional jumps and conditional jumps.


Unconditional


An unconditional jump, often referred to simply as a "jump", modifies the instruction pointer to point directly to the specified address. When executed, the flow of control transfers to the address specified without any condition.


jmp 0x040800


In this example, the next instruction pointed by the EIP is in memory address 8. This instruction performs a jump to the memory address 3. As it is a jmp instruction, it is an unconditional jump, therefore, once this instruction is executed, the instruction pointer will jump to memory address 3:



Conditional


A conditional jump or branch modifies the instruction pointer to point directly to the specified address based on the state of certain flags. For example, the jne (jump if not equal) instruction jumps to the specified address only if the ZeroFlag (ZF) is 0, indicating that the previous arithmetic operation did not result in zero.



In this example, we first set eax to 10 and ebx to 5. Next, we compare these registers. Since they're not equal, the jne instruction will be executed, and the program flow will continue from address 3.



If the condition specified by the jne instruction is not met, meaning that the two registers being compared are equal, then the program flow will not jump to the target address specified by the jne instruction. Instead, the instruction pointer will simply proceed to the next instruction in sequence. In our example, if the jne instruction is located at line 8 and the condition is not met, the instruction pointer will move to line 9, effectively treating the jne instruction as a "no operation" (NOP).


CALL instruction


The call instruction is used for making a subroutine call. It pushes the address of the theoretically next instruction onto the stack and transfers control to the specified subroutine.


The ret instruction, on the other hand, is used to return from a subroutine. It pops the return address from the stack and transfers control back to the instruction following the original call instruction.


Together, call and ret instructions work in conjunction to allow for the execution of subroutines. The call instruction transfers control to the subroutine and saves the theoretically next return address, while the ret instruction retrieves the return address from the stack and resumes execution at that point.


Let's see an example:



In this case, the next instruction to execute is the call instruction, which should jump to the exampleFn function and push the theoretically next instruction onto the stack. By 'theoretically next instruction,' I mean the next value the EIP should be after the call instruction. In this case, the 'theoretically' next instruction is at address number 4.


Before executing the function, as you can see, the ESP points to some arbitrary data. Let's see what happens when the instruction gets executed.



The ESP decremented by 4 bytes (as it grows downwards) and now points to the saved EIP value in the stack, which is theoretically the next instruction of EIP in the main function, i.e., the next instruction after the call.


We could translate the call instruction as:


push nextInstruction ; Push the address of the next instruction onto the stack 
jmp exampleFn    ; Jump to the target function

After a few NOP instructions, we reach the ret instruction:



As explained above, the ret instruction pops the saved return address from the stack and restores it into the EIP, so the program returns to its original routine, in this case, the main function.



As you can see, the program flow returned to the main function from address 4. The ESP incremented by 4 bytes after the pop, so it now points to its previous address.


Stack Frame


A stack frame is a data structure within the call stack that stores information about a function's execution. It typically includes the function's parameters, local variables, and the return address. When a function is called, a new stack frame is created to store this information, and when the function returns, its stack frame is removed. This organization allows for nested function calls and facilitates the management of program flow and data.


Return Address: This is the memory address to which the program should return after the function finishes executing. It typically points to the instruction following the function call.


Parameters: Any parameters passed to the function are stored within the stack frame. These parameters can be accessed by the function during its execution.


Local Variables: Variables declared within the function are stored in the stack frame. These variables are only accessible within the scope of the function.


The stack frame is commonly initialized by an enter instruction. The enter instruction is commonly replaced by three separated instructions:


push ebp          ; Save the current base pointer 
mov ebp, esp      ; Set the base pointer to the current stack pointer 
sub esp, imm32    ; Allocate space for local variables and dynamic storage

Base Pointer (EBP) and Stack Pointer (ESP): These POINTERS are used to navigate within the stack frame. EBP typically points to the beginning of the current stack frame, while ESP points to the top of the stack. They help in accessing parameters, local variables, and other data within the stack frame.


Base Pointer


EBP typically points to the base or bottom of the current stack frame. It serves as a reference point for accessing parameters, local variables, and other data within the function's scope.


When a function is called, the calling function typically pushes the current value of EBP onto the stack and then updates EBP to point to the beginning of the newly created stack frame. This establishes a fixed reference point for accessing data within the function. Throughout the function's execution, EBP remains relatively constant, allowing easy access to variables and parameters relative to the base of the stack frame.


Before the function returns, the original value of EBP (saved during the function call) is restored from the stack, ensuring that the stack pointer is correctly aligned for the calling function.


Stack Pointer


ESP marks the top of the stack, defining its current limit. During function execution, ESP adjusts as data is added or removed from the stack. For instance, when local variables or parameters are added, ESP moves down to accommodate them. ESP is also responsible for managing the stack during function calls, adjusting to allocate space for return addresses, parameters, and local variables of new stack frames. Once a function completes, ESP readjusts to remove its stack frame. Similar to EBP, ESP may require restoration to its original value before function return to maintain stack integrity for the caller.


Let's see how the stack frame works graphically. Firstly, let's create an scenario where the main function calls the exampleFn function:



Here we're at the main function. The next address is a call to the exampleFn function. Remembering what the call instruction does, it will push the next address after the call (for returning purposes), and then jump to the beginning of the exampleFn function.



This is the exampleFn function. Following the call instruction, the address of the subsequent instruction, 0x80401, was pushed onto the stack. Currently, ESP points to the location where this address is stored. It's crucial to understand that ESP is a pointer, storing a memory address referencing the value on the stack, not the value itself.


The first instruction is a push ebp, which places the value of ebp onto the stack.



As you can see, the push ebp instruction pushed the ebp value onto the stack. In this case, the ebp was 0xffffa (it could be any address; this is only an example), so now the Stack Pointer points to the address of the saved EBP.


The following instruction is a mov ebp, esp, which effectively copies the Stack Pointer (ESP) address to the Base Pointer register (EBP). This instruction serves as a means to preserve the ESP for the leave instruction when returning to the original routine.



As you can see, both EBP and ESP point to the same address, which is indeed the saved value of EBP.


Then, there is a sub esp, 0x10 instruction, which decrements the stack pointer by 0x10 (16 in hex) bytes. This is the last instruction to set up the stack pointer for the exampleFn function.



The stack pointer was decremented by 0x10 bytes, and finally, the stack frame is created. The next instruction seems to start using it since it refers to [ebp-0x4], which is an address that belongs to the current stack frame. This instruction is probably creating a local variable in the exampleFn. In C, the code should look like this:


int variable = 1;

So, this instruction creates a local variable in exampleFn and saves its value onto the new stack frame, which belongs to the exampleFn function:



Now there's a push 0x3 instruction, so it will push 0x3 onto the stack. Remember that the push instruction decrements the ESP by 4 and then places its operand into the contents of the 32-bit location at the address ESP.



Then there's a pop eax instruction, which will move the value from the top of the stack into the eax register.



So now, the value of EAX is 0x3 (the value that was at the top of the stack), and the stack has been incremented by 4 bytes.


After a few nop instructions, we reach a mov eax, 0x0 instruction:



This instruction, when executed, will set the value of eax to 0x0. In many calling conventions, the eax register is used to hold the return value of a function. Setting it to 0 ensures that the function returns a known value, especially when the function doesn't explicitly return a value.



As you can see, the next instruction is the leave instruction, which, as explained in the article on assembly instructions, transfers the frame pointer stored in the EBP register to the stack pointer register (ESP), freeing up the allocated stack space for the current stack frame. Next, it retrieves the old frame pointer (which was saved by the ENTER command) from the stack and places it back into the EBP register, thus restoring the stack frame of the calling procedure.


The leave instruction could be translated to this:


mov esp, ebp   ; Copies the value from ebp to esp
pop ebp        ; Pops the saved EBP value from the stack

Put simply, when executing the leave instruction, it adjusts the stack pointer (ESP) to match the base pointer (EBP). This action makes ESP point to the saved value of EBP, allowing EBP to then point to its previous address when popping values from the stack.


Let's visualize this process step by step with a graphical representation:



Initially, after executing mov esp, ebp, the stack pointer (ESP) points to the saved EBP value. Consequently, the top of the stack holds the address of the saved EBP. Therefore, when the leave instruction pops EBP, it sets EBP to the address pointed to by ESP, which in this case is the address of the saved EBP. Therefore, after the leave instruction, the graphic representation should resemble this:



As observed, the base pointer (EBP) holds the value 0xffffa, which was its value prior to the enter instruction. At present, the stack pointer (ESP) points to the saved instruction pointer (EIP). Consequently, when executing the ret instruction, the instruction pointer (EIP) will return to its original routine, retrieving its value from the saved EIP (as ret effectively behaves like a pop eip instruction).



After the execution of the ret instruction, the program flow resumes from the subsequent instruction following the call to exampleFn. EBP has been restored, now pointing to the beginning of the stack frame of the main function. Notably, it points to another saved EBP address, which will also be restored upon the main function's return.


Conclusion


I understand that these concepts can be difficult to grasp; however, they are fundamental for diving into binary exploitation. While there are additional concepts I haven't covered and I've simplified quite a bit, I believe this serves as a solid starting point. In future articles, we'll delve into more essential concepts for binary exploitation; for now, consider this the beginning.


Don't be discouraged if you don't fully grasp these concepts upon first reading. Take your time and revisit them as needed. Learning these concepts was a challenging process for me as well, but I eventually mastered them!


I hope this article has facilitated your learning process and made these topics more accessible.


Joaquín (AKA elswix).


References


Here are the sources from which I learned these concepts and gathered information to write this article:


https://www.cs.virginia.edu/~evans/cs216/guides/x86.html
https://ctf101.org/binary-exploitation/what-is-the-stack/
https://ctf101.org/binary-exploitation/what-are-registers/
https://ctf101.org/reverse-engineering/what-is-assembly-machine-code/
https://ir0nstone.gitbook.io/notes
https://ir0nstone.gitbook.io/notes/types/stack/introduction
https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
https://en.wikipedia.org/wiki/Stack_register
https://en.wikipedia.org/wiki/Program_counter
https://en.wikipedia.org/wiki/Instruction_set_architecture
https://en.wikipedia.org/wiki/Call_stack
https://www.ibm.com/docs/en/openxl-fortran-aix/17.1.2?topic=conventions-stack
https://wiki.tcl-lang.org/page/stack+frame