Understanding CPUs
While doing buffer overflows we have to understand CPU functionality. CPU instructions visible in any debugger are in assembly. We talked about various pointers in the parent page. Let's focus on understandings instructions. Before that, one quick interesting fact about pointers.
Pointers
IP = instruction Pointer
What should the CPU execute? ->Determined by IP
Fetches the instruction at the address in IP->Decode it->Run it
But it is more complicated than that in modern CPUs. Infact, it is not just going to the current instruction to be executed, but it will try to execute like the next 10 instructions all at once. It's going to do weird things like if it encounters an if statement, it will guess which way the if statement will go and execute along that side for a while.
And then if it turns out it was wrong, it will just roll back the effects. It is called "speculative execution"
CPU developers have tried to hide this complexity and present to programmers in a way that they feel it is just one instruction then the next and the next and so on until CPU is turned off.
Instructions
Example of how instructions look in assembly
mov rax, 0xdeadbeef
In the format:
Operation Register(Destination) Immediate(Source)
----
mov rax, [0xdeadbeef + rbx*4]
This kind of notation is meant for doing things like array access. Compilers will use them this way too that's why instructions of this format is helpful.
eg:
int foo[128];
Let's say &foo (memory address where foo starts) is at 0xdeedadbeef
Then we can find foo[i] by doing 0xdeadbeef + i*4
This instruction could help you speculate the working:
This is some array at 0xdeadbeef memory here
rbx is currently holding the index into that array where each element is 4 bytes long
----
Step-Through example of assembly
Initialize eax with 0xdeadbeef. This instruction is at memory location 0x804000. When CPU executes it, rip changes (as this is the current instruction so it will point to memory address 0x804000)
Now rip changes to the next instruction. rax gets initialized with memory address 0xdeadbeef. Next instruction is at 0x0804005, which means mov instruction must have been 5 bytes long.
After the last operation rbx gets initialized with 0x1234. Now rip is at the next instruction 0x80400a
rip executes instruction at 0x0800a. rbx gets added in rax. rip is jumped.
rip now reaches 0x080400d.
rbx was incremented by 1 and becomes 0x1235. rip now jumps to the next instruction at 0x0804010
a subtract option is encountered. rax=rax-rbx is done.
rax is updated to 0xdeadbee. Then rip is jumped to 0x0804013
rax is initialized with rbx's value finally!
---
Control Flow
The instructions we saw above are good but we can't do much of high performance computing unless we start doing branches and loops and if statements and switch statements etc. These conditionals in x86 are represented as following:
-> Conditional jumps
jnz <address>
je <address>
jge <address>
jle <address>
etc.
All the higher level program construct like if, for, while, switch etc end up being some variant of jump instruction.
They will jump if their condition is true, or just go to the next instruction otherwise.
But what conditions are they checking? Answer is: EFLAGS registers
---
EFLAGS
They store a bunch of flags about what the current state of the CPU is and what the result of most recent operations were.
For example:
add rax,rbx
This instruction will set the o flag if sum is greater than a 64 bit register can hold and wraps around
->You can start a jump based on overflow eith a "jo" instruction
Another example:
cmp rax,rbx
jle
je or jz
jge
jl
jg
This will jump if 1. rax<=rbx, 2. rax=rbx, 3. rax>=rbx, 4. rax<rbx, 5. rax>rbx
Under the hood cmp subtracts rbx from rax but it isn't modifying rax this time rather storing an eflag which jump conditions can use to do something!
https://en.wikipedia.org/wiki/FLAGS_register
---
Memory
Everything- instructions, numbers, strings is represented in hex bytes.
-> null byte indicates end of string
Stack
CPU doesn't care where the stack is. All stack is basically a designated part of memory that we understand as a stack. When we use instructions like push and pop, they are going to implicitly take the value in RSP and use that as a location of where they ar doing these operations on memory. The picture is a view of what memory looks like.
People avoid putting anything at address 0x000000000 because in programming, 0 is NULL. Let's say a program compares something and it returns 0x0. Rather than throwing exception, CPU will starting doing whatever is put at address 0x000000000.
Stack follows LIFO
Push -> on top of stack
Pop -> remove from top of stack
rsp -> points to the top of the stack
push rax -> ends up decreasing rsp by 8 (64 bit register)
This would make: sub rsp, 8 mov [rsp], rax
pop rax -> increase rsp
mov rax, [rsp]
add rsp, 8
Little and Big endian
0xdeadbeef
Little endian: ef be ad de
Big endian: de ad be ef
Functions
Nothing more than a bunch of operations. Usually it does stuff with the stack to create a stack frame
push rbp
mov rbp, rsp
sub rsp, 0x100
Here, a function is pushing rbp on stack. Then rsp is made to point to the top of the stack which is currently only rbp's 8 bytes.
Then finally rsp is given 100 bytes of storage. This could mean that code has some local variables that will use 100 bytes.
Why are we using rbp instead of rsp?
Let's say there are 3 variables: x,y,z.
Stack will look something like this:
Here, we can see 3 variables assigned. These variables are denominated by memory addresses w.r.t rbp
Since rbp is pushed on to the stack where stack begins, it will remain constant throughout the execution of the program.
If we used rsp for variables, it will keep changing throughout the program!
Let's say we also passed some function parameters a,b,c,d,e,f, g and h to be used before we even started execution of the program, stack will look like this now:
And meanwhile since after executing a function, we need to return back to the bottom of the stack (with ESP still high) to see what to execute next, we add return address on rbp+8
When a function exits, control goes back to return address leaving all the memory in tact.
According to SystemV AMD64 ABI, the first 6 arguments to a function are passed from left to right in these registers:
rdi,rsi,rdx,rcx,r8,r9
Further arguments are pushed to the stack
The return value of the function is stored in rax when the function returns
Last updated