Stack Structure Overview [GDB]


Stack Structure Overview [GDB]

In order to play around with debugging or things like buffer overflow, you’ll definitely need a full understanding of how things work behind the scene. In this stack structure overview using GDB, we’ll quickly go through some basics, where to look, what to expect. It’s certainly far from being a complete overview of anything related to both stack or GDB, but it’s a start. To get around, you’ll need some basic C/C++, assembler and of course GDB knowledge. We would definitely recommend reading/checking: System V Application Binary interface (ABI).

Related articles:

Stack Structure Overview

Stack’s frame pointer structure:

     HIGH (0xff)
|----------------| 
|    parameter1  |  -> Funcation parameters added in reverse order
|----------------| 
|    parameter2  | 
|----------------| 
|   return adr   |  -> Where to go after function is finished
|----------------|   
|      EBP       |  -> data pointer
|----------------|  
|      buf       |  -> local variables
|      buf       | 
|----------------|    
    LOW (0x00)

Those frames get “connected” when program execution goes deeper, for e.g. if we call f1(name) function from main function and f2 (number) from f1 (main->f1(name)->f2(number), we would have:

       HIGH
|----------------|   ------------ f1 start   
|      name      |
|----------------| 
|      RET       |  
|----------------|   
|      EBP       |  -> points to previous / main's frame
|----------------|  
|   local vars   |
|----------------|  ------------ f1 end f2 start
|     number     |
|----------------| 
|      RET       |  
|----------------|   
|      EBP       |  -> points to f1's EBP
|----------------|  
|   local vars   |
|----------------|   ------------ f2 end 
       LOW
Stack Structure Overview: High ADR (0xffff)

The “field” size depends on the OS/Compiler word size, 4 bytes (32 bit) or 8 bytes (64 bit). Don’t forget, stack grows downwards, buffer fills upwards.

EBP: base pointer
EIP: instruction pointer
ESP: top of the stack pointer

Stack Details – Function Calling Sequence

Related to function calling sequence:

ESP - Stack pointer : pointer of current stack frame (word-aligned area)
EBP - Frame potiner : hold a base address of the current stack frame (local vars are references with negative offsets from ebp)
EAX - Integral and pointer return value appear here
EBX - Register serves as the global offset table  base register(GOT) for position-indpendent code. For absolute code it servers as a local regiser.
ESI & EDI - no specific role in function calling sequence. Function must preserve their values for the caller
ECX & EDX - Scratch registers, no specific role in calling sequence. Functions do not need to preserve their values for the caller
st(0) - Floating point return values. If function doesn't return floating point, this register must be empty (also before entry to a function).
st(1-7) - no specific role in calling sequence. Must be empty before and after function call
EFLAGS - forward (zero) flag must be empty before and after.

Function prologue:

pushl   %ebp        /save previous frame pointer
movl %esp, %ebp     /set new function's frame pointer
subl $80, %esp      /allocate stack space
pushl %edi          /save local register 
pushl %esi          /save local register          
pushl %ebx          /save local register

Function epilogue:

movl %edi, %eax     /set up return value
epilogue:
pop %ebx            / restore local register
pop %esi            / restore local register 
pop %edi            / restore local register 
leave               / restore frame pointer
ret                 / pop return addr

First integral or pointer argument is at offset 8 (from ebp), second one at 12, etc.

Main Example

We’ll go through quick stack structure overview with of a most basic example using GDB (GNU Project debugger):

 #include <stdio.h>
 #include <string.h>
 
 int main(int argc, char** argv)
 {
    char buf[16];
    strcpy(buf, argv[1]);              <--- BREAKPOINT
    return 0;                          <--- BREAKPOINT
 } 

…a quick look on disassemble before we proceed:

(gdb) disas
Dump of assembler code for function main:
0x0040119d <+0>: lea 0x4(%esp),%ecx
0x004011a1 <+4>: and $0xfffffff0,%esp
0x004011a4 <+7>: pushl -0x4(%ecx)
0x004011a7 <+10>: push %ebp
0x004011a8 <+11>: mov %esp,%ebp
0x004011aa <+13>: push %ebx
0x004011ab <+14>: push %ecx
0x004011ac <+15>: sub $0x10,%esp
...

Line 4 (and with $0xfffffff0) indicates allignment. Line 15 is “creating” space for the buffer (0x10 hex => 16 bytes). Although we need just 10 bytes for the buffer, we’re getting 16 due to alignment (check the details below). If we run it:

(gdb) run AAAABBBBCCCCDDDD

 Starting program: /root/TEST_AREA/stack/test1 AAAABBBBCCCCDDDD
 Breakpoint 1, main (argc=2, argv=0xbffff384) at test1.c:7
 7       strcpy(buf, argv[1]);

Right away, we’ll see where argv’s are situated. If we check the value 0xbffff384 :

(gdb) x/4xw 0xbffff384
0xbffff384:    0xbffff51c  0xbffff548  0x00000000  0xbffff559

Checking the reference (@) of 0xbffff384:

(gdb) x/16xw *0xbffff384
0xbffff51c:    0x6f6f722f  0x45542f74  0x415f5453  0x2f414552
0xbffff52c:    0x65737361  0x796c626d  0x6675622f  0x6f726566
0xbffff53c:    0x66726576  0x2f776f6c  0x00326f62  0x41414141
0xbffff54c:    0x42424242  0x43434343  0x44444444  0x45485300

We see our inputed values (argv). Ok, quick check on the frame info and registers:

(gdb) info frame

Stack level 0, frame at 0xbffff2f0:
 eip = 0x4011bb in main (bo2.c:7); saved eip = 0xb7dfa7e1
 source language c.
 Arglist at 0xbffff2d8, args: argc=2, argv=0xbffff384
 Locals at 0xbffff2d8, Previous frame's sp is 0xbffff2f0
 Saved registers:
  ebx at 0xbffff2d4, ebp at 0xbffff2d8, eip at 0xbffff2ec

 (gdb) i r esp ebp eip
 esp            0xbffff2d0          0xbffff2d0
 ebp            0xbffff2e8          0xbffff2e8
 eip            0x4011d5            0x4011d5  

.. and we get some basic info on the frame (EIP, EBP, ESP). Ok, let’s check the top of the stack at this point (ESP):

(gdb) x/40xw $esp

0xbffff2c0:    0x00000002  0xbffff384  0xbffff390  0x0040120d
0xbffff2d0:    0xbffff2f0  0x00000000  0x00000000  0xb7dfa7e1
0xbffff2e0:    0xb7fb3000  0xb7fb3000  0x00000000  0xb7dfa7e1
0xbffff2f0:    0x00000002  0xbffff384  0xbffff390  0xbffff314
0xbffff300:    0x00000001  0x00000000  0xb7fb3000  0x00000000
0xbffff310:    0xb7fff000  0x00000000  0xb7fb3000  0xb7fb3000

Ok, we’ll continue execution, stopping at the next breakpoint. The ESP after:

(gdb) x/20xw $esp

0xbffff2c0:    0x41414141  0x42424242  0x43434343  0x00444444
0xbffff2d0:    0xbffff2f0  0x00000000  0x00000000  0xb7dfa7e1
0xbffff2e0:    0xb7fb3000  0xb7fb3000  0x00000000  0xb7dfa7e1
0xbffff2f0:    0x00000002  0xbffff384  0xbffff390  0xbffff314
0xbffff300:    0x00000001  0x00000000  0xb7fb3000  0x00000000

Based on the stack structure we mentioned on the begining, we should expect: local vars, EBP, RET, arguments. We can roughly see what’s where. Local vars:

0xbffff2c0 [0..3] - AAAA
0xbffff2c0 [4..7] - BBBB
0xbffff2c0 [8..11] - CCCC 
0xbffff2c0 [12..15] - DDD
Note: Than we see some “excess bytes” (0xbffff2f0 & 0x00000000 ) before EBP. GCC is known to be trigger happy with stack usage. It often reservers a bit more stack space than strictly needed, also trying to achieve stack alignment (8 – 16 bytes) even when not needed. These two might be just that. Continuing:
0xbffff2d8 - EBP => 0x00000000
0xbffff2ec - RET => 0xb7dfa7e1 (reference)
0xbffff2f0 - arguments/parameters (0x00000002 & buf reference: 0xbffff384)
Note: EBP is 0, found to be normal in “main” function, an entry point (__libc_start_main) and way to denote end of call stack for debuggers.

Deeper Stack Example

Here we’ll extend stack structure overview by using nested function calls:

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

void func(char *name)
{
     char buf[100];
     strcpy(buf, name);                   <--- BREAKPOINT
     printf("Welcome %s\n", buf);         <--- BREAKPOINT
}

int main(int argc, char *argv[])
{
    func(argv[1]);
    return 0;
}

Checking “func” disassemble:

(gdb) disas
 Dump of assembler code for function func:
    0x004011ad <+0>:    push   %ebp
    0x004011ae <+1>:    mov    %esp,%ebp
    0x004011b0 <+3>:    push   %ebx
    0x004011b1 <+4>:    sub    $0x74,%esp
    0x004011b4 <+7>:    call   0x4010b0 <__x86.get_pc_thunk.bx>
    0x004011b9 <+12>:    add    $0x2e47,%ebx
 => 0x004011bf <+18>:    sub    $0x8,%esp
    0x004011c2 <+21>:    pushl  0x8(%ebp)
    0x004011c5 <+24>:    lea    -0x6c(%ebp),%eax
    0x004011c8 <+27>:    push   %eax
    0x004011c9 <+28>:    call   0x401040 
    0x004011ce <+33>:    add    $0x10,%esp
    0x004011d1 <+36>:    sub    $0x8,%esp
    0x004011d4 <+39>:    lea    -0x6c(%ebp),%eax
    0x004011d7 <+42>:    push   %eax
    0x004011d8 <+43>:    lea    -0x1ff8(%ebx),%eax
    0x004011de <+49>:    push   %eax
    0x004011df <+50>:    call   0x401030 
    0x004011e4 <+55>:    add    $0x10,%esp
    0x004011e7 <+58>:    nop
    0x004011e8 <+59>:    mov    -0x4(%ebp),%ebx
    0x004011eb <+62>:    leave  
    0x004011ec <+63>:    ret    
 End of assembler dump.

Alghough we have a 100 bytes buffer, on line 4 we an allocation of 116 bytes (0x74 =>116). Why? Most likely due to alignment or space allocation.

Note: We can control boundary on compile with -mpreferred-stack-boundary=2 (default: 4 , 16B aligned)

Nonetheless we’ll examine the situation. Run it (filling the buffer with 100 NOPs):

(gdb) run $(python -c 'print "\x90"100') 
Starting program: /root/TEST_AREA/test2 $(python -c 'print "\x90"100')
Breakpoint 1, func (name=0xbffff4f0 '\220' ) at test2.c:7
 7        strcpy(buf, name);

Ok, name buffer is situated on 0xbffff4f0. Quick check of that address and frame/registers:

(gdb) x/40xw 0xbffff4f0
 0xbffff4f0:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff500:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff510:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff520:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff530:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff540:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff550:    0x90909090  0x45485300  0x2f3d4c4c  0x2f6e6962
 0xbffff560:    0x68736162  0x53455300  0x4e4f4953  0x4e414d5f
 0xbffff570:    0x52454741  0x636f6c3d  0x6b2f6c61  0x3a696c61
 0xbffff580:    0x6d742f40  0x492e2f70  0x752d4543  0x2f78696e

(gdb) info frame
 Stack level 0, frame at 0xbffff270:
  eip = 0x4011bf in func (matilda.c:7); saved eip = 0x40121b
  called by frame at 0xbffff2a0
  source language c.
  Arglist at 0xbffff268, args: name=0xbffff4f0 '\220' 
  Locals at 0xbffff268, Previous frame's sp is 0xbffff270
  Saved registers:
   ebx at 0xbffff264, ebp at 0xbffff268, eip at 0xbffff26c

 (gdb) i r esp ebp esp
 esp            0xbffff1f0          0xbffff1f0
 ebp            0xbffff268          0xbffff268
 esp            0xbffff1f0          0xbffff1f0

(gdb) bt
 0  func (name=0xbffff4f0 '\220' ) at test2.c:7
 1  0x0040121b in main (argc=2, argv=0xbffff334) at test2.c:13

Ok, we’re in “func” frame, and we see all the vital registers. Current ESP:

(gdb) x/40xw $esp
 0xbffff1f0:    0xb7fffab0  0x00000001  0xb7fd0410  0x00000001
 0xbffff200:    0x00000000  0x00000001  0xb7fff950  0x00000001
 0xbffff210:    0x00000000  0x00ca0000  0x00000001  0xb7ffe840
 0xbffff220:    0xbffff270  0x00000000  0xb7fff000  0x00000000
 0xbffff230:    0x00000000  0xbffff334  0xb7fb3000  0xb7fb19e0
 0xbffff240:    0x00000000  0xb7fb3000  0xb7ffe840  0xb7fb6d08
 0xbffff250:    0xb7fe62d0  0xb7fb3000  0x00000000  0xb7e119eb
 0xbffff260:    0xb7fb33fc  0x00000000  0xbffff288  0x0040121b
 0xbffff270:    0xbffff4f0  0xbffff334  0xbffff340  0x00401203
 0xbffff280:    0xb7fe62d0  0xbffff2a0  0x00000000  0xb7dfa7e1 

We’ll continue execution until second breakpoint “(gdb) c”. ESP after “strcpy”:

(gdb) x/40xw $esp
 0xbffff1f0:    0xb7fffab0  0x00000001  0xb7fd0410  0x90909090 <== buf
 0xbffff200:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff210:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff220:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff230:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff240:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff250:    0x90909090  0x90909090  0x90909090  0x90909090
 0xbffff260:    0xb7fb3300  0x00000000  0xbffff288  0x0040121b
 0xbffff270:    0xbffff4f0  0xbffff334  0xbffff340  0x00401203
 0xbffff280:    0xb7fe62d0  0xbffff2a0  0x00000000  0xb7dfa7e1

We’re on it. Buffer apparently starts on 0xbffff1fc. The EBP points to 0xbffff288 (main’s EBP) and right behind it is RET (0x0040121b). If we check main’s disasemble, we’ll see that func’s RET ends up behind the func’s call (as expected):

(gdb) disas main
 Dump of assembler code for function main:
    0x004011ed <+0>:    lea    0x4(%esp),%ecx
    0x004011f1 <+4>:    and    $0xfffffff0,%esp
    0x004011f4 <+7>:    pushl  -0x4(%ecx)
    0x004011f7 <+10>:    push   %ebp
    0x004011f8 <+11>:    mov    %esp,%ebp
    0x004011fa <+13>:    push   %ecx
    0x004011fb <+14>:    sub    $0x4,%esp
    0x004011fe <+17>:    call   0x40122b <__x86.get_pc_thunk.ax>
    0x00401203 <+22>:    add    $0x2dfd,%eax
    0x00401208 <+27>:    mov    %ecx,%eax
    0x0040120a <+29>:    mov    0x4(%eax),%eax
    0x0040120d <+32>:    add    $0x4,%eax
    0x00401210 <+35>:    mov    (%eax),%eax
    0x00401212 <+37>:    sub    $0xc,%esp
    0x00401215 <+40>:    push   %eax
    0x00401216 <+41>:    call   0x4011ad 
    0x0040121b <+46>:    add    $0x10,%esp
    0x0040121e <+49>:    mov    $0x0,%eax
    0x00401223 <+54>:    mov    -0x4(%ebp),%ecx
    0x00401226 <+57>:    leave  
    0x00401227 <+58>:    lea    -0x4(%ecx),%esp
    0x0040122a <+61>:    ret    
 End of assembler dump.

If we inspect EBP reference it would lead us to main’s function frame:

(gdb) x/40xw 0xbffff288
 0xbffff288:    0x00000000  0xb7dfa7e1  0x00000002  0xbffff324
 0xbffff298:    0xbffff330  0xbffff2b4  0x00000001  0x00000000
 0xbffff2a8:    0xb7fb3000  0x00000000  0xb7fff000  0x00000000
 0xbffff2b8:    0xb7fb3000  0xb7fb3000  0x00000000  0x2b476483
 0xbffff2c8:    0x6bed0293  0x00000000  0x00000000  0x00000000
 0xbffff2d8:    0x00000002  0x00401070  0x00000000  0xb7feb450

We can see main’s EBP (0), RET (0xb7dfa7e1) and main’s arguments/parameters (argc = 0x00000002, argv pointer to 0xbffff324) . If we would continue to argv pointer we would see the initial NOPs input aside other things.

Space Alocation and Alignment

A number of factors affects how much space the compiler is allocating for function’s stack frame (process runtime stack):

  • function’s arguments
  • local variables
  • stack alignment to a 16-byte boundary (GCC’s default on i386)

The stack is word aligned. Although arcitecture doesn’t require stack alignment, software/OS impose such requirement (aligned on a word boundary). If necessary argument’s size is being increased to make it a multiple of words (including tail padding, depending on the argument’s size).

Other areas depend on the code/compiler. Maximum stack frame size is not defined by the standard and there’s no mention on how language system use the “unspecified” area of the stack frame. That stack frame’s area is managed by the compiler and it’s there for local variables and function’s arguments.

Code/Compliler

Position           contents              frame
----------------------------------------------------------------
4n+8(%ebp)       argument word n                       high addr
                     ...                 previous
 8(%ebp)         argument word 0          
 --------------------------------------------------------------- 
 4 (%ebp)        return address          
 0 (%ebp)        previous %ebp (optional)
 -4 (%ebp)       unspecified             current
                      ...  
 0 (%ebp)       variable size                           low addr              
 --------------------------------------------------------------- 

Compiler manages stack frames and for them to be aligned, variable(s) alignment must also be known. Variable alignment depends on their type and CPU architecture, and it’s specified in the ABI:

Space Alocation and Alignment ABI
Scalar types

Alignment of arrays, unions and structures are guided by certain conventions:

Aggregates (structures and arrays) and unions assume the alignment of their most strictly aligned component. The size of any object, including aggregates and unions, is always a multiple of the object’s alignment. An array uses the same alignment as its elements. Structure and union objects can require padding to meet size and alignment constraints. The contents of any padding is undefined.
SYSTEM V APPLICATION BINARY INTERFACE (Intel386 Arch Processor Supplement)

On i386 based systems, GCC aligns the stack to a 16-byte boundary by default. You can try and change it:

-mpreferred-stack-boundary=num : Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If -mpreferred-stack-boundary is not specified, the default is 4 (16 bytes or 128 bits).

Based on all this, compiler will allocate 16 bytes on stack frame even for vars whose type size are less than that. E.g. if we have an int of 4 bytes on i386 system, compiler would take 16 bytes for it:

 void pointer_example(void) {
     char *i = "TEST";
 }

Dump of assembler code for function  pointer_example :
    0x060583db <+0>:     push   %ebp
    0x060583dc <+1>:     mov    %esp,%ebp
    0x060583de <+3>:     sub    $0x10,%esp  <-- 16 bytes of space created for 4-byte pointer    0x060583e1 <+6>:     movl   $0x6058480,-0x4(%ebp)
    0x060583e8 <+13>:    nop
    0x060583e9 <+14>:    leave  
    0x060583ea <+15>:    ret   

Here we see that 16 bytes of space were allocated for a 4-byte pointer. Next, array example:

void array_example(void) {
     char buffer[100];
 }

Dump of assembler code for function  array_example :
    0x0605844b <+0>:     push   %ebp
    0x0605844c <+1>:     mov    %esp,%ebp
    0x0605844e <+3>:     sub    $0x78,%esp  <-- 120 bytes of space created for 100-byte array    0x06058451 <+6>:     mov    %gs:0x14,%eax
    0x06058457 <+12>:    mov    %eax,-0xc(%ebp)
    0x0605845a <+15>:    xor    %eax,%eax
    0x0605845c <+17>:    nop
    0x0605845d <+18>:    mov    -0xc(%ebp),%eax
    0x06058460 <+21>:    xor    %gs:0x14,%eax
    0x06058467 <+28>:    je     0x605846e 
    0x06058469 <+30>:    call   0x6058310 <__stack_chk_fail@plt>
    0x0605846e <+35>:    leave  
    0x0605846f <+36>:    ret

We can see that 120 bytes of space was allocated for a 100-byte array.

void vulnerable_function(char* string) { 
     char buffer[100];
 }

0x06058464 <+0>:  push   %ebp
0x06058465 <+1>:  mov    %esp,%ebp
0x06058467 <+3>:  sub    $0x88,%esp

It should be 0x66 instead of 0x88. mod 16. GCC only does this extra stack alignment in main, that function is special. You won’t see it if you check any other function, unless there are a local with alignas(32) or similar.

-fno-omit-frame-pointer : option instructs the compiler to store the stack frame pointer in a register

Conclusion

This stack structure overview was done in a “hurry” maybe, but we’ve managed to show some of the things like stack structure, frame structure, position of certain fields/registers within the stack, alignment issues, etc. Mastery of GDB or similar debugger/disassembler is a must. Add to that a good analytical skills with a bit of “don’t give up attitude” and you’re well on your way to become a good reverse engineer/pentester.