Saturday, March 3, 2007

Shellcode thần chưởng: luyện assembly

Sau bài viết đầu tiên về shellcode, một người bạn có hỏi tôi là nếu shellcode là bytecode - mã máy, vậy nó chỉ phụ thuộc vào bộ vi xử lí thôi chứ, tại sao nó còn phụ thuộc vào hệ điều hành nữa vậy?

Đây là một câu hỏi rất thường gặp ở những người mới bắt đầu, bản thân tôi cũng đã từng tự hỏi như thế khi mới tìm hiểu shellcode. Thật ra lý do shellcode bắt buộc phải phụ thuộc vào hệ điều hành khá hiển nhiên. Nếu bạn nhìn rộng ra một chút, bạn sẽ thấy rằng không phải chỉ shellcode mới là bytecode, mà tất cả phần mềm, dù được viết bằng ngôn ngữ gì, cuối cùng phải được dịch sang bytecode rồi mới có thể chạy được. Nếu shellcode không phụ thuộc vào hệ điều hành, vậy tất cả các phần mềm cũng sẽ không phụ thuộc vào hệ điều hành, phải không nào? Shellcode xét đến cùng cũng chỉ là một phần mềm, nó buộc phải lệ thuộc vào những gì hệ điều hành cung cấp để thực thi chức năng của nó.

Bạn muốn viết chương trình đọc một file rồi xuất ra màn hình? Dù bạn viết bằng C hay Assembly, chắc chắn mã nguồn của chương trình đó trên Windows và Linux sẽ khác nhau bởi lẽ các hàm mà Windows hay Linux cung cấp cho bạn rất khác nhau. Nói tóm lại, bạn buộc phải có một chương trình hay một shellcode chuyên biệt cho từng loại hệ điều hành mà bạn dự định chạy shellcode trên đó. Nếu bạn vẫn chưa nắm được vấn đề, hãy đọc tiếp, các ví dụ cụ thể trong bài viết này có thể giúp bạn hiểu được tại sao shellcode phải phụ thuộc vào hệ điều hành.

Không có ngôn ngữ nào đơn giản hơn Assembly

Như trong bài trước tôi đã nói, chúng ta sẽ viết shellcode theo hai cách:
  • viết bằng C, dịch sang Assembly rồi tiếp tục dịch sang mã máy
  • viết bằng Assembly rồi dịch luôn ra mã máy.
Có thể thấy rằng, dù chọn cách viết nào đi chăng nữa, bạn bắt buộc phải thông hiểu ngôn ngữ Assembly. Rồi coi như xong, tôi bỏ cuộc đây, Assembly phức tạp và khó học thấy mồ, sao tôi học nổi? Ấy ấy bạn đừng vội, hãy dành chút thời gian nghe tôi trình bày rồi bỏ đi cũng chưa muộn.

Chắc hẳn bạn cũng biết, các ngôn ngữ lập trình thường được chia làm hai lớp: lớp ngôn ngữ cấp cao và lớp ngôn ngữ cấp thấp. Lớp ngôn ngữ cấp cao lại được chia làm hai lớp, lớp cao vừa vừa như C/C++ chẳng hạn và lớp cao ngất ngưỡng như Java hay các loại ngôn ngữ scripting kiểu như PHP, Perl, Python, Ruby...Trong khi lớp ngôn ngữ cấp cao đông đúc như vậy thì lớp ngôn ngữ cấp thấp chỉ có một đại diện duy nhất là Assembly (tùy thuộc vào assembler mà cú pháp ngôn ngữ Assembly sẽ có những thay đổi nhất định nhưng xét tổng quát thì chúng ta chỉ có một ngôn ngữ Assembly duy nhất).

Thông thường ngôn ngữ cấp cao sẽ dễ học và dễ sử dụng hơn ngôn ngữ cấp thấp hơn, ví dụ như C/C++ khó học và khó xài hơn Java hay Python rất nhiều. Assembly còn ở cấp thấp hơn cả C/C++, vậy suy ra nó phải cực khó rồi. Tin vui là không đúng như vậy bạn ơi. Assembly là một ngôn ngữ cực kì đơn giản. Bản thân tôi đã sử dụng khá nhiều ngôn ngữ khác nhau và Assembly (cụ thể là NASM Assembly) là ngôn ngữ đơn giản nhất mà tôi từng biết. Theo tôi, sự đơn giản của Assembly thể hiện ở chỗ, ngôn ngữ này gần như không có bất kì khái niệm trừu tượng nào cả. Assembly không có pointer, không có class, không có function, không có int, không có string...Assembly chỉ có một tập lệnh (instruction set), bộ nhớ (stack + register) và tất cả dữ liệu đều được lưu trữ và xử lí theo từng byte. Người mới học Assembly dựa vào tập lệnh là đã có thể viết chương trình được ngay mà không phải tốn thời gian tìm hiểu các khái niệm trừu tượng như trong các ngôn ngữ cấp cao khác. Suy cho cùng, Assembly bắt buộc phải đơn giản, nó không thể phức tạp, bởi lẽ nó (gần như) là phiên bản human-readable của mã máy, thứ ngôn ngữ duy nhất mà vi xử lí có thể hiểu được và chúng ta đều biết, vi xử lí chỉ có thể hiểu được những thứ rất đơn giản như 0 và 1 mà thôi.

Rồi, nói nhiều quá rồi, phải trình diễn thôi!

Sơ lược về Assembly


Trước tiên, bạn download file này về (tạm thời để ở Yousendit, sẽ chuyển sang một host mới trong vài ngày nữa), in ra rồi đọc cho đến khi hiểu rõ nội dung của nó rồi hãy tiếp tục theo dõi bài này. Đây là tài liệu của lena151, một cao thủ về reverse code engineering, tóm gọn khá tốt và đầy đủ những khái niệm quan trọng nhất của Assembly. Mặc dù tài liệu này thiên về phục vụ cho reverse code engineering nhưng bạn hoàn toàn có thể áp dụng những kiến thức này để viết shellcode. Một lần nữa, tôi đề nghị bạn hãy đọc thật kĩ tài liệu này trước khi tiếp tục.

Hello, world!

Chúng ta hãy bắt đầu bằng cách cổ điển, viết một chương trình Hello, world! bằng Assembly.
Bạn hãy lưu đoạn chương trình sau đây vào file hello.asm (nhớ bỏ đi các số đầu dòng nhen):
1 global _start
2 _start:
3 xor eax, eax

4 jmp short string
5 code:
6 pop ecx
7 mov edx, 14
8 mov ebx, 1
9 mov al, 4
10 int 0x80

11 xor eax, eax
12 mov al, 1
13 int 0x80

14 string:
15 call code
16 db 'Hello, world!', 0x0a
Trời, có một cái Hello, world! mà đã dài đến 16 dòng vậy mà dám nói Assembly đơn giản!? Bạn ơi, dài hơn không có nghĩa là khó và phức tạp hơn mà nên hiểu rằng nó rõ ràng và rành mạch hơn. Nếu nhìn kĩ vào chương trình trên, bạn sẽ thấy nó chỉ sử dụng một số lệnh đơn giản như int, xor, mov, pop, push, call hay jump...cùng với các register như eax, ebx, ecx, edx hay esp ngoài ra không có bất kì lệnh nào khó hiểu khác. Tôi cũng cam đoan với bạn rằng, hầu hết shellcode mà bạn sẽ viết đều chỉ sử dụng bấy nhiêu đó lệnh mà thôi. Chúng ta sẽ để dành công việc phân tích đoạn chương trình trên ở những bài sau, trước mắt hãy chạy thử chương trình này đi đã:
$ nasm -f elf hello.asm
$ ld -o hello hello.o
$ ./hello
Hello, world!
Ở lệnh đầu tiên, nasm sẽ đọc mã nguồn ở file hello.asm và tạo ra file object có định dạng là ELF mang tên hello.o. Ở lệnh thứ hai, chương trình linker mang tên ld trên Linux sẽ sử dụng file object hello.o để tạo ra file thực thi hello. Hãy xem kích thước của chương trình này là bao nhiêu:
$ cat hello | wc -c
749
Bạn hãy thử viết một chương trình Hello, world! tương tự bằng C rồi so sánh kích thước của hai chương trình xem sao? Chắc chắn rằng chương trình viết bằng C sẽ to gần 10 lần chương trình viết bằng Assembly. Rõ ràng đây là một lợi thế đáng kể của Assembly và chúng ta có thể lợi dụng điều đó để viết những đoạn shellcode có kích thước cực nhỏ.

-Thái

30 comments:

rilwis said...

Anh Thái ơi, em dịch chương trình trên bằng MASM51 bị báo lỗi khi link (em dùng WinXP). Lỗi không có segment nào.

Em cũng thấy lạ. Anh giải thích giúp em được không?

thaidn said...

Chào rilwis,

Chắc chắn sẽ có lỗi khi em dịch trên WinXP rồi em, bởi vì chương trình này viết để chạy trên Linux mà.

-Thái

NQH said...

Hello Thái,

Bài viết tốt! Duy có điểm này anh không đồng ý.

Bản thân tôi đã sử dụng khá nhiều ngôn ngữ khác nhau và Assembly (cụ thể là NASM Assembly) là ngôn ngữ đơn giản nhất mà tôi từng biết.

Tôi cho rằng Lisp là ngôn ngữ đơn giản nhất và đồng thời hùng mạnh nhất về mặt thể hiện ý tưởng.

Vả lại, khi nói "đơn giản nhất" có lẽ Thái nên nói rõ là đơn giản về mặt gì?

Về syntax và cú pháp thì assembly có đơn giản hơn các scripting languages như Perl, Python, Tcl không? có đơn giản hơn các functional languages như Lisp/Scheme không?

Về biểu hiện tư tưởng thuật toán thì assembly có đơn giản bằng Lisp, C/C++, Prolog không?

Về scalability, extendability thì có hơn các ngôn ngữ OOP như Java/Smalltalk/C++ không?

ixij said...

Theo tôi phần giải thích về bytecode ở đầu bài, nếu nói rõ hơn một chút nữa sẽ hay hơn.

Đó là: bytecode bản thân nó chỉ lệ thuộc vào tập lệnh của bộ xử lý mà thôi, và nó chỉ bắt đầu lệ thuộc vào hệ điều hành, khi người viết đặt mục đích của mình vào việc cho thực thi hoặc khai thác chúng trên nền của hệ điều hành nào? lúc đó thì chúng mới bắt đầu phải lệ thuộc vào hệ điều hành nếu muốn hoạt động được ổn thỏa.

Anonymous said...

@rilwis: MASM 5.1 chỉ dịch ASM 16 bit thôi, muốn dịch ASM32 (như đoạn mã của Thái) bạn phải dùng MASM phiên bản ít nhất là 6.0


good job, thaidn. Keep 'em coming...

NQH said...

Đó là: bytecode bản thân nó chỉ lệ thuộc vào tập lệnh của bộ xử lý mà thôi, và nó chỉ bắt đầu lệ thuộc vào hệ điều hành, khi người viết đặt mục đích của mình vào việc cho thực thi hoặc khai thác chúng trên nền của hệ điều hành nào? lúc đó thì chúng mới bắt đầu phải lệ thuộc vào hệ điều hành nếu muốn hoạt động được ổn thỏa.

Cụ thể hơn nữa là shellcode cho Windows và shellcode cho Linux sẽ khác nhau khi shellcode dùng một hàm của hệ điều hành. Trong Linux có thể gọi system call bằng ngắt 0x80, chuyển sang kernel mode, và các hàm được đánh số tĩnh. Trong Windows thì phải load địa chỉ của hàm từ một DLL. Mà địa chỉ động kiểu này có thể khác nhau giữa các versions khác nhau của Windows.

Đằng nào thì bài viết của Thái cũng không đi quá sâu vào chi tiết, cho nên có lẽ giải thích Thái đã dẫn là tạm ổn.

thaidn said...

Chào anh NQH,

Em dự định là sẽ để cho bạn đọc tự nhận ra sự đơn giản của Assembly qua những ví dụ ở các bài sau. Sở dĩ em nói trước là Assembly rất đơn giản, mục tiêu duy nhất chỉ là trấn an và lên tinh thân cho những bạn mới bắt đầu mà thôi :p.

Tuy nhiên, em cũng đã sửa lại bài viết, thêm vào ý kiến chủ quan của em tại sao em cho rằng Assembly đơn giản.

Chào bạn ixij,

Anh NQH đã trả lời giùm ý của mình rồi đó.

-Thái

Anonymous said...

hì hì, Thái sửa xong thì mình lại thấy không ổn.

Java không phải là ngôn ngữ cao ngất ngưởng đâu, nếu tính theo chuẩn truyền thống Java cũng cỡ C++, SmallTalk... thôi, chứ tính ra vẫn "thấp" hơn Prolog hay LISP...

Còn Assembly không phải là đại diện duy nhất của ngôn ngữ cấp thấp đâu. Chẳng hạn hợp ngữ x86 (Intel/IBM based) đã có sự khác nhau giữa Assembly của dòng 16 bit (8088, 8086, 80286) với dòng 32 bit - ASM32 - ở 80386 đến Pentium. Trên Linux và Win cũng có những sự khác biệt nhau cơ bản về hợp ngữ. Sắp tới có lẽ sẽ có hợp ngữ Intel dành cho các dòng máy 64 bit.
Ngoài ra còn có các hợp ngữ của các loại máy ảo nền chéo (Java, .NET, probably more...) như Jasmin của JVM chẳng hạn.


Tốt nhất không nên chi tiết quá ;)

Anonymous said...

Í chà, ở trên tui quên nhắc đến các loại hợp ngữ của Motorola (PowerPC), Sun (Sparc), ... để so với x86 của Intel. Lúc đầu đã định nói rồi mà viết một hồi đến chỗ JVM lại quên mất, sorry.

Anonymous said...

Dù sao thì tôi cũng hoan nghênh tinh thần post bài của bạn Thái. Tuy nhiên, việc bạn Thái đưa ra những đặc điểm để cho rằng Assembly là đơn giản như " ngôn ngữ này gần như không có bất kì khái niệm trừu tượng nào cả. Assembly không có pointer, không có class, không có function, không có int, không có string.." thì không hiểu nếu như Martin Richards, Ken Thompson, Bjarne Stroustrup, hay James Gosling mà đọc được thì họ sẽ thấy buồn như thế nào vì những gì họ cố gắng tạo ra để giúp việc lập trình đơn giản và hiệu quả hơn lại bị bạn Thái gạt bỏ thẳng thừng như vậy :-)
- tunghack

thaidn said...

Chào bạn tunghack,

Bàn về sự đơn giản của Assembly, tôi nghĩ ngôn ngữ này giống như chiếc xe đạp, còn các ngôn ngữ cấp cao khác thì giống như xe máy, xe hơi hay các loại phương tiện giao thông tiên tiến khác. Tất cả chúng đều chỉ có một mục đích duy nhất: giúp chúng ta di chuyển từ nơi này sang nới khác. Xe đạp dĩ nhiên không hiệu quả hơn xe máy, nhưng rõ ràng xe đạp đơn giản hơn xe máy rất nhiều. Muốn học chạy xe đạp, bạn hầu như không cần phải hiểu học các thao tác như nổ máy, lên ga, sang số...chỉ cần ngồi lên và đạp là xong. Nói cách khác, chiếc xe đạp không cố che dấu nguyên lý hoạt động bên trong của nó, bạn có thể dễ dàng hiểu được vì sao khi bạn đạp thì xe nó chạy. Còn đối với trường hợp xe máy, khi bạn rồ ga, một loạt hoạt động bí mật bên trong sẽ diễn ra, và xe tự nhiên chạy mà hầu hết mọi người sẽ không (cần) hiểu được nguyên lý. Một khi bạn đã biết chạy xe máy, bạn sẽ cảm thấy nó cũng đơn giản, dễ sử dụng và dần dần những khái niệm trừu tượng sẽ trở nên quen thuộc, trở thành điều nhiển hiên, cứ rồ ga thì xe sẽ chạy lên phía trước, chẳng mấy ai quan tâm đến chuyện gì xảy ra bên trong nữa.

Function, class, pointer, int, string...tất cả đều là những khái niệm trừu tượng, được xây dựng nhằm mục đích giúp cho công việc lập trình dễ dàng và hiệu quả hơn nhưng chắc chắn sự xuất hiện của chúng cũng làm tăng tính phức tạp của ngôn ngữ. Muốn sử dụng C, bạn bắt buộc phải hiểu pointer. Muốn viết java, bạn phải hiểu OOP, thế nào là class, thế nào là inheritance, thế nào là abstraction, thế nào là encapsulation...Bạn có nhiều công cụ hơn nhưng đồng thời bạn phải tốn nhiều thời gian hơn để học cách sử dụng chúng. Assembly đơn giản vì bạn không cần phải học các khái niệm hay công cụ trừu tượng mới có thể sử dụng được nó. Cái giá phải trả là bạn sẽ phải tốn nhiều công sức hơn khi viết chương trình bằng Assembly. Do đó người ta chỉ sử dụng Assembly để viết những chương trình be bé kiểu như shellcode mà thôi. Giống như xe đạp, đơn giản thật, nhưng cũng chỉ có thể chạy được những đoạn đường ngắn.

-Thái.

$$$ said...

Hix nói ASM chỉ dùng như chiếc xe dạp thì đánh giá thấp nó quá. Nên hiều là tất cả những khái niệm trừu tượng ở các ngôn ngữ cấp cao đều được implement từ tổ hợp của ASM (nói chính xác hơn là tập lệnh mã máy)
ví dụ như function a(int x,int y) trong C
được implement thành
push y
push x
call a

a:
push bp
mov bp,sp
...
pop bp
ret

1 điều hay nữa là nếu bạn không thích cách implement của C thi xài cách implement của Pascal hay whatever bạn có thể tự chế ra "function" riêng của mình.

Tóm lại để chế tạo cái xe hơi, bạn có thể tự gia công từ đống nguyên liệu thép , cao su hay mua từ bộ phận làm sẳn lắp vào. Tất nhiên không ai dại gì dùng ASM để viết lại 1 hệ điều hành hay 1 chương trình Photoshop nhưng dùng ASM để hiểu tường tận hệ thống hoạt động như thế nào là tốt nhất.

Anonymous said...

Tôi nghĩ tốt nhất bạn Thái không nên tập trung chứng minh rằng Assembly là đơn giản vì nếu cứ lập luận như bạn Thái thì "why don't we just create 0 and 1 binary strings only and pass those strings to the processor? Isn't it even more simple than Assembly?". Bạn Thái nên tập trung vào phân tích sự hữu dụng của Assembly trong việc giúp chúng ta hiểu được nguyên lý hoạt động của máy tính (e.g: memory allocation, function calling convention(epilogue,prologue),..), và như vậy mới có thể giúp người đọc hiểu về buffer overflow và shellcode hơn.
Vài lời góp ý.
- tunghack

troc said...

Ngay từ cách phân loại đã sai rồi, làm gì có khái niệm cao vừa vừa, hay cao chót vót. Chỉ có thể phân loại cao hay thấp, và là biên dịch hay thông dịch.

Assembly hẳn nhiên là ngôn ngữ cấp thấp, nhưng nói nó đơn giản thì thật sai lầm (hoặc là hiểu chưa tới). Có thể tập lệnh của nó đơn giản, nhưng mà cách dùng nó thì cực kỳ phức tạp. Đơn giản khi chỉ cần so sánh 2 biến xem có bằng nhau hay lớn hơn nhau (lệnh if then),dân làm assembly phải move 2 biến đó vào 2 thanh ghi, trừ nhau, dùng 1 lệnh compare xem giá trị đó có lớn hơn 0 (hay bé hơn), rồi thực hiện lệnh jump đến đoạn code thỏa điều kiện.

Theo định nghĩa phổ biến của từ "đơn giản" trong giới làm computer về programming languages, theo tôi là nghiêng về phía người sử dụng. Theo đó thì Java, Tcl, Python sẽ là những ngôn ngữ đơn giản, cấp cao, thông dịch. Còn C++, OO Pascal (Delphi) sẽ là đơn giản, cấp cao, biên dịch.

Cái hình tượng chiếc xe đạp thì lại càng mắc cười. Bạn này làm mình liên tưởng đến các em học ở Aptech, nói về tool hay cái gì chi tiết về 1 cái tool nào đó thì rất rành, rành còn hơn cả dân học chính quy các trường Tổng hợp hay Bách khoa. Nhưng những khái niệm cốt lõi thì lại mù mờ. Nhưng chả sao nhỉ, đi làm ai quan tâm ba cái đó :)

Anonymous said...

Anh that su bo tay voi tat ca nhung thang nao comment trong cai entry nay: khong biet dung thoi gian de lam gi khac hay sao ma vao soi tung cau tung chu de khoe gioi?? May cai loi lat vat ma cung mat thoi gian de cai nhau den the co a?

Thang nao tu nhan la gioi, chi ra xem chung may invent hay contribute duoc nhung cai gi ra xem nao? Hay la chi code duoc may cai chuong trinh vo van, hoac hon ti nua la doc hieu duoc may cai phrack paper??

Neo said...

Thái ơi, tôi nghĩ anh nên sửa lại phần phân loại các ngôn ngữ lập trình. Theo tôi, cách nhìn nhận của anh về chức năng và đặc điểm của các ngôn ngữ lập trình có nhiều điểm không ổn. Tôi nghĩ bạn tunghack nói đúng, anh không nên quá sa đà vào chuyện ngôn ngữ lập trình. Đó không phải là sở trường của anh, và cũng không phải là mục đích chính của blog này.

Nếu anh nói chuyện lập trình bằng Assembly thì không cần phải học và biết nhiều và cũng giống như việc "ngồi lên xe đạp và chạy" thì không được hợp lý cho lắm. Nếu muốn am hiểu và sử dụng tốt Assembly, người dùng cần phải hiểu rõ về cấu trúc, chức năng của các thanh ghi, hiểu rõ về cách hoạt động của CPU, cấu trúc của RAM và ROM, hiểu rõ một số khái niệm về cấu trúc dữ liệu như stack, FIFO, FILO, v.v... Tôi còn nhớ khi xưa khi học assembly, phải nghiềm ngẫm suốt cả tháng trời cuốn sách dày cộm của Peter Norton. Giờ đây khi nghe phát biểu của Thái về tính đơn giản của Assembly, nghĩ lại hóa ra hồi đó mình học chậm quá.

Anonymous said...

Gửi tất cả mọi người, ai có chương trình SoftIce chạy trên win2k or XP không ? Post link cho tui download với
Cảm ơn trước,

Anonymous said...

Cám ơn Thái, nhờ bài này của anh tôi mới biết là có một con shellcode ngồi/ chồm hổm trên đầu blog (\x31\xc0 ...) . Tôi đã cố gắng lục lại kiến thức asm đã rusty 10 năm trước để giải mã nó. Nhưng mà hình như nó hơi tối nghĩa (?) kết quả là lệnh "/bin/sh bin/sh" phải không? để làm gì cà ... ?

Việt-Anh

Anonymous said...

Quên, phải là lệnh
/bin/sh -c /bin/sh
mới đúng. Nhưng vẫn không hiểu nó làm cái gì

Việt-Anh

nolateforbegin said...

chào anh, em gà món này lắm, chỉ dám học hỏi, chứ chẳng dám bình luận gì, em đọc tới đoạn anh đoạn: Trước tiên, bạn download file này về (tạm thời để ở Yousendit, sẽ chuyển sang một host mới trong vài ngày nữa), là ngừng theo lời anh, trỏ sang yousendit thì nhận được thông báo This file has expired., anh ơi up lên đâu đó đi để em lấy về mà còn xem nữa chứ, hay anh liên hệ em, nick im_nlfb (YM) đi, em upload lên host của em rồi đưa link lại cho anh gắn lên bài viết này, bảo đảm không die. :D

H2O said...

Cám ơn về bài viết nhưng mà chưa viết hết ý mà Thái. Hay tại mấy cái comment buồn cười kia làm bạn mất hứng.

Chữ đơn giản có nhiều ý nghĩa lắm.
So sánh với xe đạp của đúng, khi so sánh ngôn ngữ lập trình với phương tiện vận chuyển thì lúc đó phần mềm viết ra sẽ là địa điểm đến được.

Lúc đó bạn sẽ thấy rằng chiếc xe đạp đơn giản vì ít chức năng, nhưng để chạy được từ VN đế USA là cả một vấn đề phức tạp. Cho khi đó, lái máy bay thì rất khó học nhưng lái đến mỹ rất dễ

chuongvd said...

Anh em mất thời gian cãi nhau làm gì nhỉ ví dụ như Thái cũng đúng và như anh em nói cũng đúng . Chúng ta nên hiểu đơn giản như thế này: Khi nhỏ chúng ta bắt đầu tập đi xe đạp rất khó và ngã khá nhiều (đối với người bình thường). Khi đi được xe đạp lớn nên một chút tập đi xe máy chắc chắn là vô tư và dễ dàng hơn lúc mới tập đi xe đạp rồi, nhưng đòi hỏi tay lái phải vững hơn và buộc phải xử lý nhiều hơn tập trung hơn vì nhiều thao tác hơn (ga, số...)và tốc độ của nó nhanh hơn.Tôi nghĩ từ đấy các bạn đã hiểu ra vấn đề , đừng tranh luận kiểu sinh viên nhỏ như vậy hiiii
Good luck.

nhatanh1982 said...

link: yousendit die.

Hải Nam said...

Link yousendit đã die, bạn có thể upload tài liệu đó lên host khác được ko? rất cảm ơn!

Git.aloso said...

Anh Thái ơi link tài liệu die rồi, anh up lại được không ạ! Em rất thích các bài viết của anh, ngày nào cũng vào ngóng bài mới.. hix, còn rất nhiều người "thực sự" muốn học hỏi anh ạ.

Hoàng Hà said...

@anh Thái
Em đọc đến đoạn anh bảo phải down cái file của lena151 về đọc thì link đã die! và chưa dám đi tiếp! Mong anh fix lại cái link cho em có thể đi tiếp được nhé! :)
Cảm ơn anh!!!

tuilatui said...

nó là xe đạp cũng đc xe hơi cũng đc , quan trọng là nó "mạnh" và em thích nó . ai còn cái tài liệu kia share với link die!

long nguyen said...

Anh Thái ơi tài liệu lena151 File not found link yousendid.com đó anh

Ronal said...

Up lai link di anh Thai

-- @ bỏ chạy @ -- said...

Hi Thái (Trước tiên, bạn download file này về (tạm thời để ở Yousendit, sẽ chuyển sang một host mới trong vài ngày nữa))
không download được ??