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
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.
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
0xbffff2d8 - EBP => 0x00000000 0xbffff2ec - RET => 0xb7dfa7e1 (reference) 0xbffff2f0 - arguments/parameters (0x00000002 & buf reference: 0xbffff384)
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.
-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:
Alignment of arrays, unions and structures are guided by certain conventions:
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.