MOV, PUSH, and POP in x86 Assembly - How Data Actually Moves (OllyDbg Guide)

OllyDbg Reverse Engineering Series - Part 6 of 60


:high_voltage: OllyDbg Reverse Engineering Series


Welcome to Phase 2

Phase 1 was setup. We got OllyDbg running, we understood number systems, registers, and got a first look at assembly.

Phase 2 is different. We load a real executable. We step through real instructions. We connect what we see in OllyDbg to what actually happens inside the CPU.

By the end of Phase 2 (Parts 6–12), you will be able to look at a disassembled function and understand what it is doing not guess, actually understand.

Let’s go.


Our Target Executable crackme.exe

For all of Phase 2, we are using a crackme we compiled ourselves. Here is why that is a good idea you know what the C code does before you look at the assembly. So when you see a MOV in OllyDbg, you already know what it is supposed to be doing. That connection is how you build pattern recognition fast.

I was confused about crackmes at first. I downloaded CrueHead’s crackme and it had anti-debug protection and encryption. Great for later. Not great for learning MOV. So we built our own.

Here is the C code. Save it as crackme.c:

#include <stdio.h>
#include <string.h>

int check_password(char *input) {
    char *secret = "iotsec";
    return strcmp(input, secret);
}

int main() {
    char buf[64];
    printf("Enter password: ");
    scanf("%63s", buf);
    
    if (check_password(buf) == 0) {
        printf("Correct!\n");
    } else {
        printf("Wrong!\n");
    }
    return 0;
}

Simple. One function that checks a password. One main function that takes input and prints the result.

Compiling on Kali Linux (or any Ubuntu/Debian):

First install the cross-compiler:

sudo apt install gcc-mingw-w64-i686

Then compile:

i686-w64-mingw32-gcc -o crackme.exe crackme.c -m32

The i686 part is important. That gives you a 32-bit Windows PE executable. OllyDbg works with 32-bit. Copy crackme.exe to your Windows machine and load it in OllyDbg via File → Open.


Finding Our Code in OllyDbg

When OllyDbg loads the exe, it stops at the entry point. But that is not our main(). MinGW adds CRT startup code before main ignore that for now.

To find our actual code, right click in the disassembler → Search for → All referenced text strings. You will see strings like "iotsec", "Enter password:", "Correct!", "Wrong!".

Double click on "iotsec" and it jumps you straight into check_password. Double click on "Enter password:" and it jumps you into main.


Double Click On IotSec

This is how reversers navigate large binaries. You search for strings you expect to find, then work backwards from there.


Concept 1 - The Function Prologue (The Pattern You Will See Everywhere)

When I jumped to the "iotsec" string, the first thing I scrolled up to see was this:

00821558  PUSH EBP
00821559  MOV EBP, ESP
0082155B  SUB ESP, 28

I had no idea what this was the first time. Now I cannot unsee it. This is the function prologue the 3-line setup that appears at the start of every single function. Every one. Without exception.

Here is what it means, in the way I understood it after thinking about it for a while.

Imagine two people sharing an office desk. Person A was working there. Their stuff is arranged a certain way. Now Person B needs to use the same desk.

Before Person B starts working, they:

  1. Take a photo of how the desk looked - so they can restore it later. That is PUSH EBP. We save the previous function’s EBP onto the stack before we overwrite it.
  2. Mark their own starting position - Person B puts their own reference marker down. That is MOV EBP, ESP. EBP now points to the current stack position our fixed reference point for this function.
  3. Clear some space on the desk - make room for their own papers. That is SUB ESP, 28. ESP moves down by 28 bytes (hex), carving out space for local variables.

The key thing I had to wrap my head around: EBP does not move after this. ESP moves up and down constantly. EBP stays fixed. Think of EBP as a nail in the wall. You measure everything from that nail.

After the prologue:

High address
|           |
|  old EBP  |  ← PUSH EBP saved it here
|           |
← EBP points HERE (fixed, never moves)
|           |
|  EBP-4    |  local variable slot 1
|  EBP-8    |  local variable slot 2
|  EBP-C    |  local variable slot 3
|           |
← ESP points here (moved down by 28)
Low address

The gap between EBP and ESP that is where local variables live.

How to spot a function boundary in OllyDbg:

RET           ← previous function ends here
PUSH EBP      ← new function starts here
MOV EBP, ESP
SUB ESP, xx

RET = end. PUSH EBP + MOV EBP,ESP + SUB ESP = start. You will use this pattern thousands of times.

image


Concept 2 - MOV Deep Dive

Part 5 introduced MOV. Now we go deeper with real examples from our crackme.

MOV is the most common instruction in x86. It copies data from one place to another. The source never changes only the destination gets the new value.

The variants you need to know:

Register to register:

MOV EAX, EBX

Copy the value in EBX into EAX. EBX stays the same. Simple.

Immediate value to register:

MOV EAX, 5

Put the number 5 directly into EAX. No memory involved.

Memory to register:

MOV EAX, [405000]

Go to address 405000, fetch whatever 4 bytes are there, put them in EAX. The square brackets mean “go fetch from this address.” Like a house address — the brackets tell you to go inside and get what is there.

Register to memory (writing TO memory):

MOV [405000], EAX

Take the value in EAX and write it to address 405000. You are writing data into memory.

The illegal one — memory to memory:

MOV [EAX], [EBX]   ← THIS IS ILLEGAL IN x86

x86 does not allow this. You cannot copy directly from one memory location to another in a single MOV. You always need a register in the middle. This trips up a lot of people.


Concept 3 - Reading DWORD PTR SS:[EBP-C]

The first real MOV I found in our crackme was this:

00B2155E  MOV DWORD PTR SS:[EBP-C], crackme.00B30044  ; "iotsec"

This looks scary. It is not. Break it apart:

DWORD PTR - size specifier. Tells the CPU how many bytes to work with.

  • BYTE PTR = 1 byte
  • WORD PTR = 2 bytes
  • DWORD PTR = 4 bytes ← this one

SS: - segment register. SS means Stack Segment. The CPU has different memory regions SS is where the stack lives. DS is Data Segment. CS is Code Segment. In OllyDbg you will mostly see DS: and SS:. Just know that SS: means “we are talking about stack memory.”

[EBP-C] - EBP is our fixed reference point. C in hex = 12 in decimal. So EBP-C means “go 12 bytes below EBP.” That is one of our local variable slots from the gap we saw earlier.

Put it all together: “In stack memory, go to the slot 12 bytes below EBP, treat it as 4 bytes, and write the address of ‘iotsec’ there.”

In our C code this was just:

char *secret = "iotsec";

That one line of C became that one line of assembly. That is what reversing is - connecting assembly back to logic.


Concept 4 - MOVZX and MOVSX (Sign and Zero Extension)

These two come up when you move a smaller value into a bigger register. When you take an 8-bit value (like AL) and put it into a 32-bit register (like EAX), what happens to the other 24 bits?

Two options:

MOVZX - Zero Extension. Fills the remaining bits with zeros. Safe for unsigned values.

MOVZX EAX, AL

If AL = 0xFF (255), EAX becomes 0x000000FF.

MOVSX - Sign Extension. Looks at the sign bit of the source. If the highest bit is 1 (negative number), fills remaining bits with 1s. If 0, fills with 0s.

MOVSX EAX, AL

If AL = 0xFF (-1 in signed), EAX becomes 0xFFFFFFFF (still -1 in signed 32-bit).
If AL = 0x7F (127), EAX becomes 0x0000007F (still 127).

Why does this matter in reversing? If you assume a value is unsigned but it is actually signed, your analysis of what a comparison does will be wrong. MOVZX vs MOVSX tells you whether the programmer intended signed or unsigned arithmetic.


Concept 5 - PUSH Variants

Part 5 covered basic PUSH. Here is the full picture.

PUSH EAX -push the value in register EAX onto the stack. ESP decreases by 4.

PUSH 0 - push an immediate value directly. Common before function calls.

PUSH [405000] - go to address 405000, fetch the value there, push that value onto the stack. Not the address the value stored AT the address.

PUSH DWORD PTR [EAX] - EAX contains an address. Go to that address, fetch 4 bytes, push them.

The most important pattern PUSH before CALL means function arguments. In 32-bit x86, arguments are passed by pushing them onto the stack before calling the function. Reading the PUSHes before a CALL tells you what arguments are being passed.

In our crackme:

MOV DWORD PTR SS:[ESP], EAX    ; push argument 1 (input string)
CALL JMP.&msvcrt.strcmp        ; call strcmp

That MOV to [ESP] is another way to pass an argument — same idea as PUSH. The callee finds its arguments on the stack.


Concept 6 - POP

POP is the reverse of PUSH. Takes the value at the top of the stack and puts it somewhere. ESP increases by 4.

POP EAX - take the top of stack, put it in EAX.

POP [405000] - take the top of stack, write it to memory address 405000. Valid, you will see this sometimes.

Why do you rarely see POP in normal functions? Because most code uses LEAVE instead of manually POPping everything. But in exploits and shellcode, POP is everywhere — it is used to adjust the stack pointer and grab return addresses.


Concept 7 - Reading the Full check_password Function

Here is the full check_password function from our crackme in OllyDbg:

00821558  PUSH EBP                              ; save caller's EBP
00821559  MOV EBP, ESP                          ; set up our reference point
0082155B  SUB ESP, 28                           ; carve out local variable space
0082155E  MOV DWORD PTR SS:[EBP-C], 00B30044   ; secret = "iotsec"
00821565  MOV EAX, DWORD PTR SS:[EBP-C]        ; load secret into EAX
0082156C  MOV EAX, DWORD PTR SS:[EBP+8]        ; load input argument into EAX
0082156F  MOV DWORD PTR SS:[ESP], EAX           ; pass input as argument to strcmp
00821572  CALL JMP.&msvcrt.strcmp               ; call strcmp(input, secret)
00821577  LEAVE                                  ; restore stack frame
00821578  RET                                    ; return to caller

Notice [EBP+8] on line 4. EBP+8 means 8 bytes ABOVE EBP. That is where function arguments live above EBP, not below. Local variables are below (EBP-x). Arguments from the caller are above (EBP+x).

And LEAVE at the end that is shorthand for the function epilogue:

MOV ESP, EBP   ; restore stack pointer to where EBP is
POP EBP        ; restore caller's EBP from stack

Then RET sends execution back to the caller.

So the full story of a function:

; Prologue - function starts
PUSH EBP          ; save caller's EBP
MOV EBP, ESP      ; set our reference point
SUB ESP, xx       ; make room for locals

; ... function body ...

; Epilogue -function ends
LEAVE             ; restore EBP and ESP
RET               ; go back to caller

Memorize this. You will see it in every function for the rest of your reversing life.


Concept 8 — Reading main() in OllyDbg

Now let us look at main(). Jump to "Enter password:" string in OllyDbg. Scroll up to the prologue. You will see:

00821579  PUSH EBP
0082157A  MOV EBP, ESP
0082157C  AND ESP, FFFFFFF0    ; stack alignment (MinGW adds this, ignore it)
0082157F  SUB ESP, 50

Then further down you can see your entire program logic:

; printf("Enter password: ")
00821587  MOV DWORD PTR SS:[ESP], crackme.0083004B   ; "Enter password: "
0082158E  CALL crackme.0082151C

; scanf("%63s", buf)
0082159B  MOV DWORD PTR SS:[ESP], crackme.0083005C   ; "%63s"
008215A2  CALL crackme.008214E0

; check_password(buf)
008215AB  MOV DWORD PTR SS:[ESP], EAX
008215AE  CALL crackme.00821558                       ; our check_password function

; if (result == 0)
008215B3  TEST EAX, EAX
008215B5  JNZ SHORT crackme.008215C5                 ; jump if NOT zero (wrong password)

; printf("Correct!")
008215B7  MOV DWORD PTR SS:[ESP], crackme.00830061   ; "Correct!"
008215BE  CALL crackme.0082151C

; printf("Wrong!")
008215C5  MOV DWORD PTR SS:[ESP], crackme.0083006B   ; "Wrong!"
008215CC  CALL crackme.0082151C

Your entire C program is right there. Every function call, every if/else, every string. You wrote this code. Now you are reading it in assembly. That feeling of recognizing your own logic in disassembly that is what reversing is.


What I Found Confusing (And Now Don’t)

EBP vs ESP - I kept mixing these up.

ESP is the top of the stack. It moves constantly. EBP is a fixed reference point for the current function. It does not move after the prologue. Once I visualised EBP as a nail in the wall and ESP as something sliding along the wall, it clicked.

[EBP-C] - what is that C?

C is hex for 12. EBP-C means 12 bytes below EBP. It is just a local variable slot. The function has multiple slots EBP-4, EBP-8, EBP-C, EBP-10, etc. Each one is a local variable.

DWORD PTR SS: why is it so verbose?

x86 assembly needs to be explicit. DWORD PTR tells the CPU “4 bytes.” SS: tells it “stack segment memory.” [EBP-C] tells it the exact address. Every piece of that line is necessary information for the CPU. Once you know what each part means, it stops looking scary.

Memory to memory MOV is illegal.

You cannot do MOV [EAX], [EBX]. x86 simply does not support it. Always need a register in the middle. MOV ECX, [EBX] then MOV [EAX], ECX. This one confused me because logically it seems like it should work.


What We Learned

Term What It Means
MOV Copies data from source to destination. Source is unchanged.
DWORD PTR Size specifier - means 4 bytes
WORD PTR Size specifier - means 2 bytes
BYTE PTR Size specifier - means 1 byte
SS: Stack Segment - the memory region where the stack lives
DS: Data Segment - the memory region where global data lives
[EBP-C] Address 12 bytes below EBP - a local variable slot
[EBP+8] Address 8 bytes above EBP - a function argument from the caller
PUSH Puts a value on top of the stack. ESP decreases by 4.
POP Takes the top value off the stack. ESP increases by 4.
MOVZX Move with zero extension - fills upper bits with zeros
MOVSX Move with sign extension - fills upper bits based on sign bit
Function Prologue PUSH EBP + MOV EBP,ESP + SUB ESP,xx - every function starts with this
Function Epilogue LEAVE + RET - every function ends with this
LEAVE Shorthand for MOV ESP,EBP then POP EBP
RET Return to the caller function