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:
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):
-Thái
Đâ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.
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 _startTrờ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 đã:
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
$ nasm -f elf hello.asmỞ 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:
$ ld -o hello hello.o
$ ./hello
Hello, world!
$ cat hello | wc -cBạ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ỏ.
749
-Thái
Comments
Em cũng thấy lạ. Anh giải thích giúp em được không?
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
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?
Đó 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.
good job, thaidn. Keep 'em coming...
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.
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
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á ;)
- 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.
Vài lời góp ý.
- tunghack
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 đó :)
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??
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á.
Cảm ơn trước,
Việt-Anh
/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
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ễ
Good luck.
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!!!
không download được ??
Viết chương trình nhập vào 2 chữ số a và N (0à9). Tính lũy thừa a mũ N sau đó in kết quả ra màn hình.
Em thank anh trước ạ
Anh viết tiếp được không ạ!!!!!