Thứ Sáu, 31 tháng 5, 2013

Các kiểu dữ liệu trong C

1. Kiểu số nguyên
Trong C chúng ta có một khai báo cho một biến kiểu nguyên như sau: int variable ;
Trong đó từ khóa int nói cho C biết biến được khai báo chứa một giá trị nguyên. Kiểu int thường chiếm 2 đến 4 bytes. Trên các máy UNIX, các số nguyên là các con số 32bits (4 bytes), cung cấp một dải giá trị đi từ -2147483648 đến 2147483648 (2 ^ 31 - 1). Trên PC, hầu hết compilers sử dụng chỉ 16 bits (2 bytes ), đi từ -32768 đến 32767 (2 ^ 15 - 1). Các kích thước này là đặc trưng cho từng môi trường. Bạn có thể tìm thấy trong header file limits.h các hằng số cho các giới hạn. Kích thước của của một số nguyên có dấu (signed int) hoặc số nguyên không dấu (unsigned int) là một kích thước chuẩn tùy thuộc vào từng kiểu máy cụ thể. Ví dụ, trên các hệ điều hành 16-bit, kiểu int  thường là 16 bits, hay là 2 bytes. Trong các hệ điều hành 32-bit, kiểu int thường là 32 bits, hoặc là 4 bytes. Do đó kiểu int là tương đương với hoặc là kiểu short int hoặc là kiểu long int, và unsigned int hoặc là tương đương với kiểu unsigned short hoặc kiểu unsigned long, phụ thuộc vào môi trường nhắm tới. Các kiểu int biểu diễn tất cả các giá trị có dấu nếu không có chỉ định gì thêm.

Các đặc tả int unsigned int (hay đơn giản là unsigned) định nghĩa các đặc tính nhất định của ngôn ngữ C (cho ví dụ, kiểu enum). Trong những trường hợp như vây, các định nghĩa của int  và unsigned int cho một triển khai cụ thể xác định lưu trữ thực sự.

Microsoft Specific
Các số nguyên có dấu được biểu diễn trong "two-complement form". Bit quan trọng nhất giữ dấu: 1 là số âm, 0 cho số dương và số 0.  

END Microsoft Specific

Note: 
Các đặc tả kiểu int và unsigned int được sử dụng rộng rãi trong các chương trình C bởi vì chúng cho phép một máy cụ thể xử lý các giá trị nguyên theo cách hiệu quả nhất dành cho máy đó. Tuy nhiên, do kích thước của kiểu int và unsigned int là khác nhau, các chương trình phụ thuộc vào kích thước kiểu cố định có thể không "portable" đến các máy khác. Để làm cho các chương trình nhiều portable, bạn có thể sử dụng expressions với sizeof operator thay cho việc sử dụng hard-coded data sizes.

2. Kiểu số thực (float)

Các số thực sử dụng định dạng IEEE (Institue of Eletrical and Eletronics Engineers). Các giá trị với độ chính xác đơn (single-precison values) với kiểu float có 4 bytes, bao gồm một bit dấu (sign bit), 8-bit dành cho phần mũ, và 23 bit dành cho phần định trị. Phần định trị biểu diễn một số nằm giữa 1.0 và 2.0. Bởi vì bit thứ tự cao (high-order bit) của phần định trị luôn là 1, nó không được lưu trong số đó. Kiểu float có dải giá trị từ 3.4E-38 đến 3.4E+38.

Bạn có thể định nghĩa các biến (variables) như là float hoặc là double, điều đó còn tùy thuộc vào các nhu cầu của ứng dụng bạn tạo. Sự khác biệt chính yếu giữa hai kiểu này có ý nghĩa đối với những gì chúng có thể biểu diễn, lượng lưu trữ (storage) chúng yêu cầu, và dải giá trị của chúng. Dưới đây tôi sẽ trình bày sự khác nhau các yêu cầu quan trọng và yêu cầu lưu trữ.

Các kiểu số thực
Type               Các chữ số quan trọng                    Số bytes
float                         6-7                                            4
double                      15-16                                         8

Các biến thực được biểu diễn bởi một phần định trị, những gì chứa giá trị của số và một lũy thừa.

Bảng sau đây cho ta thấy số lượng bit được cấp phát đến phần định trị và phần lũy thừa của mỗi kiểu số thực. Bit quan trọng nhất trong cả float hay double đều là bit dấu. Cũng như trong kiểu số nguyên, số 1 biểu diễn số âm, số 0 là số âm.

Các độ dài của phần định trị và phần mũ

Type               Độ dài phần lũy thừa                    Độ dài phần định trị
float                         8 bits                                            23 bits
double                     11 bits                                           52 bits

Bởi vì các lũy thừa được lưu trong dạng không dấu, lũy thừa bị sai lệch một nửa giá trị có thể có của nó. Đối với kiểu float, sai lệch là 127; đối với kiểu double, nó là 1023. Bạn có thể tính toán giá trị lũy thừa thực sự bằng cách trừ giá trị lũy thừa cho giá trị sai lệch.

Phần định trị được lưu như một phần nhị phân (binary fraction) lớn hơn hoặc bằng 1 và nhỏ hơn 2. Đối với các kiểu float và double, còn có một bit 1 được dùng làm bit dấu (bit quan trọng nhất), vì vậy các phần định trị thực sự dài 24 và và 53 bits, mặc dù bit quan trọng nhất không bao giờ được lưu trong bộ nhớ.

Thay cho phương thức lưu trữ vừa được mô tả, các gói số thực (floating-point package) có thể lưu có số thực nhị phân (binary floating-point numbers) như các số không bình thường. Các số không bình thường là các số thực khác không với các giá trị lũy thừa lưu trữ trong những gì mà bit quan trọng nhất của phần định trị bằng 0. Bởi việc sử dụng định dạng không bình thường, dải giá trị của số thực có thể được mở rộng theo mức chính xác nhất. Bạn không thể điều khiển được số thực được biểu diễn theo dạng bình thường hay là không bình thường; các gói số thực quyết định việc biểu diễn này. Các gói số thực không bao giờ sử dụng dạng không bình thường nếu lũy thừa không trở nên nhỏ hơn giá trị nhỏ nhất có thể biểu diễn trong dạng bình thường.

Bảng sau đây hiển thị các giá trị nhỏ nhất và lớn nhất bạn có thể lưu một biến trong mỗi kiểu thực. Các giá trị được liệt kê trong bảng này áp dụng cho các số thực bình thường; dạng không bình thường có giá trị nhỏ nhất bé hơn. Chú ý rằng, các số được lưu trữ trong các thanh ghi 80x87 thường được biểu diễn trong dạng bình thường 80-bit; các số chỉ có thể được biểu diễn trong dạng không bình thường khi được lưu trong các biến thực 32-bit và 64-bit (các biến cho kiểu float và long)

Dải giá trị của các kiểu thực

Type                        Giá trị nhỏ nhất                     Giá trị lớn nhất
float                         1.175494351 E – 38                     3.402823466 E + 38                       
double                     2.2250738585072014 E – 308         1.7976931348623158 E + 308                                 

Nếu sự chính xác là ít lo ngại hơn việc lưu trữ, hãy dùng kiểu float. Ngược lại, nếu độ chính xác là một tiêu chí hàng đầu, sử dụng kiểu double.

Các biến thực có thể được thúc đẩy đến một kiểu có ý nghĩa hơn (từ kiểu float đến kiểu double). Việc thúc đẩy xảy ra khi bạn thực hiện tính toán trên các biến số thực. Việc tính toán đó đòi hỏi độ chính xác, nếu hai biến khác kiểu việc tính toán sẽ dựa trên biến có ý nghĩa hơn. Cho ví dụ, suy nghĩ về các kiểu khai báo sau:

float f_short;
double f_long;
long double f_longer
f_short = f_short * f_long

Trong ví dụ trên, biến f_short được thúc đẩy đến kiểu double và nhân với f_long; sau đó kết quả được làm tròn đến kiểu float trước khi gán cho f_short.

Trong ví dụ dưới đây (sử dụng khai báo của ví dụ trước), việc tính toán được hoàn thành trên các biến 32-bit; kết quả sẽ được đẩy đến kiểu double.

f_longer  = f_short * f_short




Thứ Năm, 30 tháng 5, 2013

Cùng nhau phân tích một chương trình cộng trừ số nguyên 32-bit trong assembly

Tôi xin giới thiệu một chương trình nhỏ viết bằng assembly để cộng và trừ các số nguyên. Các thanh ghi (registers) được sử dụng để lưu giữ dữ liệu trung gian, chúng ta gọi các chương trình con từ thư viện (library subroutine) để hiển thị nội dung của các thanh ghi. Và đây là source code:

TITLE Add and Subtract (AddSub.asm)

; This program adds and subtracts 32-bit integers.

INCLUDE Irvine32.inc

.code
main PROC

mov eax, 10000h ; EAX = 10000h
add eax, 40000h ; EAX = 50000h
sub eax, 20000h ; EAX = 30000h
call DumpRegs ; display registers

exit
main ENDP
END main

Chúng ta sẽ lần lượt phân tích chương trình từng dòng một. Mỗi dòng của program code sẽ xuất hiện trước lời giải thích cho dòng đó.

    TITLE Add and Subtract        (AddSub.asm)

TITLE directive đánh dấu toàn bộ dòng này như là một comment. Bạn có thể đặt bất cứ thứ gì trên dòng này.

    ; This program adds and subtracts 32-bit integers.

Tất cả text phía bên phải của dấu chấm phải được assembler lờ đi, vì vậy tôi sử dụng nó cho comments.

    INCLUDE Irvine32.inc

INCLUDE directive sao chép tất cả các định nghĩa cần thiết và thông tin cài đặt từ một text file có tên là Irvine32.inc, đặt nó (các định nghĩa và thông tin) trong thư mục INCLUDE của assembler (việc mô tả file này sẽ được đề cập trong các bài viết sau này)

    .code

.code directive đánh dấu điểm bắt đầu của code segment, nơi tất cả các câu lệnh có thể thực thi được trong chương trình được đặt vào.

    main PROC

PROC directive nhận diện sự bắt đầu của một thủ tục (procedure). Một cái tên được chọn cho một thủ tục duy nhất trong chương trình của chúng ta là main.

    mov     eax, 10000h        ; EAX = 10000h

Lệnh MOV di chuyển (sao chép ) số nguyên 10000h đến thanh ghi EAX. Toán hạng đầu tiên (EAX) được gọi là toán hạng đích (destination operand), và toán hạng thứ hai được gọi là toán hạng nguồn (source operand). Comment phía bên tay phải hiển thị giá trị được mong đợi trong thanh ghi EAX.

    add    eax, 40000h         ; EAX = 50000h

Lệnh ADD cộng 40000h vào thanh ghi EAX. Comment hiển thị giá trị được mong đợi trong thanh ghi EAX.

    sub    eax, 20000h         ; EAX = 30000h

Lệnh SUB trừ giá trị thanh ghi EAX cho 20000h.

    call DumpRegs              ; hiển thị các thanh ghi

Câu lệnh CALL gọi một thủ tục để hiển thị các giá trị hiện hành của các thanh ghi CPU. Đây là một cách hữu dụng để xác minh xem chương trình có làm việc chính xác hay không.

             exit
    main   ENDP

Câu lệnh exit (gián tiếp ) gọi một hàm MS-Windows đã được định nghĩa để tạm ngưng chương trình.

ENDP directive đánh dấu kết thúc của thủ tục main.

Chú ý rằng exit không là một từ khóa của MASM; thay vào đó, nó là một macro command được định nghĩa trong  Irvine32.inc include file cung cấp một cách đơn giản để kết thúc chương trình.

    END    main

END directive đánh dấu dòng cuối của chương trình được assembled. Nó nhận dạng tên của thủ tục bắt đầu chương trình (program's startup procedure) (thủ tục bắt đầu thực thi chương trình)


Program Output

EAX = 00030000  EBX = 7FFDF000  ECX = 00000101  EDX = FFFFFFFF
ESI   = 00000000  EDI =  000000000  EBP = 0012FFF0  ESP  = 0012FFC4
EIP   = 00401024  EPL = 000000206  CF =0  SF = 0  ZF = 0  OF = 0  AF = 0  PF = 1

Hai dòng đầu của output hiển thị các giá trị hexadicimal của cách thanh ghi mục đích chung (general-purpose registers). EAX bằng 00030000h, giá trị được sinh ra bởi các lênh ADD và SUB trong chương trình. Giá trị của các thanh ghi mục đích chung khác là không quan trọng, bởi vì giá trị của chúng không được set bởi chương trình của chúng ta. Cột thứ 3 hiển thị giá trị của các thanh ghi EIP (con trỏ lệnh mở rộng) và EFL (các cờ mở rộng), cũng như là giá trị của các cờ Carry, Sign, Zero, Overfow, Auxilary Carry, và Parity.    

Thứ Ba, 14 tháng 5, 2013

Malicious Code Analysis: Michael Murr Explains How and Why

Micheal Murr công tác tại viện SANS. trong bài phỏng vấn, anh ấy sẽ chia sẻ quan điểm của mình về vai trò của phân tích code trong quá trình dịch ngược, và làm thế nào một người có thể cải thiện tốt hơn trong khía cạnh này của malware forensics.

Mức độ dịch ngược code đóng vai trò gì trong việc tìm hiểu cách malware cư xử trong môi trường lab ?

Phân tích code của một mẫu thử cho phép bạn hiểu hơn về những gì thực sự xảy ra "đằng sau hậu trường". Nếu các chiến thuật anti-analysis được sử dụng (ví dụ VMware detection ) bạn có thể tìm ra chúng qua việc phân tích code. Bạn có thể biết nhiều hơn về khả năng của mẫu thử, giống như là commands nó có thể support nếu nó cho phép remote control. Một bất lợi lớn nhất của việc phân tích code đó là nó tiêu tốn khá nhiều thời gian.

Khi tôi chat với với IR và forensics professionals làm việc trong ngành phân tích mã độc, họ thường  bày tỏ quan ngại về sự thiếu hụt kiến thức lập trình nền tảng. Một nền tảng phát triển phần mềm đóng vai trò quan trọng thế nào trong phân tích mã độc ?

Tôi tin rằng tất cả kiến thức đều hữu dụng :). Nhưng trong ý nghĩ, một nền tảng phát triển phần mềm là hữu dụng, nhưng không thực sự cần thiết. Tôi tìm ra những gì giúp đỡ hầu hết mọi người là hai thứ: Một kiến thức cơ bản về kiến trúc máy tính và một kiến thức cơ bản về assembly. Một cuốn sách tôi giới thiệu là  Kip Irvine's Assembly Language for x86 ProcessorsCuốn sách là sách giáo khoa và bao gồm hầu hết những thứ cần thiết để bước chân vào quá trình dịch ngược code.

Các công cụ ưa thích của anh khi phân tích mã độc?

Tôi cố gắng để đưa ra những thứ đơn giản... Tôi là fan cuồng của IDA pro và Python. IDA pro là một công cụ thương mại, nhưng nếu bạn dịch ngược code như là nhu cầu thường xuyên nó là sự đầu tư đáng giá. Python thật tuyệt vời bởi vì nó tích hợp tốt với IDA pro, và có một lượng kha khá Pytho scripts dành cho dịch ngược code [(và exploit development :)]

Có nhiều công việc lặp đi lặp lại được giải quyết bằng viết code.

Việc phân tích code chịu ảnh hưởng thế nào bởi packers những gì che dấu code ?

Thông thường packers không phải là vấn đề chính. Sử dụng kết hợp giữa phân tích code, phân tích bộ nhớ và các kĩ thuật phân tích cư xử chúng ta sẽ có được những gì chúng ta cần. Có một vài packers thường gây khó khăn và tiêu tốn thời gian.

Những gợi ý của anh cho các chuyên gia an ninh để có thể có được hoặc là cải thiện các kĩ năng dịch ngược code ?

1. Practice: không có thiếu malware cho bạn phân tích.
2. Đọc những gì người khác đã làm được: Có một vài người thông minh nằm trong lĩnh vực này, hãy để mắt đến blog của họ, papers, presentations bạn có thể học được những thứ có ích.
3. kiên trì: Đừng bỏ cuộc, thỉnh thoảng nó gây ra những khó khăn (đặc biệt khi bạn là người không dư giả thì giờ) Tham khảo những người khác, hỏi họ để có được những phản hồi, những lời khuyên. khi bạn đang dịch ngược malware chìa khóa là sử dụng bất cứ cái gì bạn cần đến, giống như cả phân tích code và phân tích cứ xử, để hoàn thành công việc. :)

Thứ Bảy, 11 tháng 5, 2013

Bài 2: Cấu trúc của một compiler

Một compiler có 5 đoạn chính :

  1. Lexical Analysis (Phân tích từ vựng)
  2. Parsing (Phân tích cú pháp)
  3. Semantic Analysis (Phân tích ngữ nghĩa)
  4. Optimization (Tối ưu hóa)
  5. Code Generation (Sinh code)
Chúng ta sẽ cùng nhau nói về từng cái một, và để dễ hiểu chúng ta lấy qua việc tìm hiểu xem làm thế nào con người hiểu được tiếng anh. 
Bước thứ nhất là hiểu về chương trình, cả compiler và con người, là để hiểu words.
  • bước thứ nhất: nhận dạng các từ (words)
         - đơn vị nhỏ nhất phía trên letters
                           
                                  This is a sentence

Con người có thể nhìn vào đoạn ví dụ này và nhận ra ngay lập tức có 4 words 'this is a' và 'sentence'. Đó là một cách tự động, và ta thậm chí không nghĩ về nó nhưng có một phép tính toán thực sự diễn ra tại đây. Bạn phải nhận dạng được các phân cách (separators), cụ thể là các khoảng trắng (blanks) và dấu chấm câu và những đầu mối (clues) như là các chữ viết hoa. Và nhờ đó giúp bạn phân chia được các letters thành các nhóm, thành một loạt words bạn có thể hiểu được. Và nhấn mạnh rằng nó không hề tầm thường. hãy nhìn vào sentence sau:
                                 ist his ase nte nce
Bạn có thể đọc nó nhưng nó mất thời gian Bởi vì việc đặt separators không đúng vị trí, rõ ràng chúng ta có thể nhận dạng được các words như 'is', 'this', 'a', 'sentence' nhưng chúng không đến ngay lập tức, bạn thực sự phải làm một vài công việc để nhìn xem chỗ nào là nơi phân chia. Mục đích của lexical analysis:
  • Lexical analysis chia program text thành "words" hoặc "tokens"
                if x == y then z =1; else z = 2; 
Ví dụ về một mảnh program text, thanh cho một đoạn tiếng anh. Chúng ta có thể nhận dạng được tokens. Có một vài thứ rõ ràng như là keywords, giống như if, then, và else. Chúng ta cũng có tên các biến, những thứ giống như là X, Y, và Z. chúng ta còn các hằng như số 1, số 2. ngoài ra chúng ta còn có những phép tính như là phép gán (=), phép so sánh (==). và ở đây chúng ta có một câu hỏi thú vị. Làm thế nào để chúng ta biết được rằng double equals không phải là 2 individual equals signs.và làm thế nào chúng ta biết được chúng ta muốn nó (double equals) chứ không phải là 2 equals. 

Dấu ";" là một dấu kết thúc cũng là tokens và các blanks cũng vậy. 

Đối với con người, mỗi khi words được hiểu bước tiếp theo là hiểu cấu trúc của sentence. Việc đó gọi là parsing (phân tích cú pháp).
  • Mỗi khi words được hiểu, bước tiếp theo là hiểu về cấu trúc của sentence
  • Parsing = Lập biểu đồ Sentences
         - biểu đồ là một cây

Chúng ta cùng phân tích sentence sau:

    This         line               is                    a                  long                     sentence
   article       noun             verb               article            adjective               noun
          subject                   verb                                      object
                                      sentence
Bước thứ nhất là phân tích cú pháp để nhận dạng vai trò của mỗi word trong sentence. chúng ta có những thứ như danh từ, động từ, tính từ. Nhưng sau đó, công việc thực sự của parsing là nhóm words lại với nhau thành các cấu trúc bậc cao hơn. ví dụ chúng ta nhóm word 'this' và word 'line' thành subject, 'a', 'long', 'sentence' thành object, 'is' vẫn là verb. và như đã nói, biểu đồ là một cây, mà cây thì phải có rễ, rễ của cây được gọi là sentence. Đó là cách ta phân tích trong English.

if x == y, z = 1; then z = 2;

khi chúng ta parsing program text công việc cũng tương tự. chúng ta cùng parsing đoạn code phía trên. Đó là một if then else statement, vì vậy root của biểu đồ chúng ta, của cây phân tích