Chủ Nhật, 14 tháng 7, 2013

Debugging

Debugger là một phần mềm hoặc phần cứng được sử dụng để kiểm tra và việc thực thi của một chương trình khác. Debuggers giúp sức trong quá trình phát triển phần mềm, bời vì các chương trình thường xuyên có những lỗi khi chúng được viết lần đầu tiên. Như khi bạn phát triển, bạn cung cấp input cho chương trình và nhìn vào output, nhưng bạn không thể biết được làm thế nào chương trình sinh ra kết quả như vậy. Debuggers cho bạn những cái nhìn sâu hơn về những gì chương trình làm khi nó đang thực thi. Debuggers được thiết kế để cho phép developers có thể đo lường (measure) và điều khiển (control) trạng thái bên trong và thực thi của một chương trình.

Debuggers cung cấp thông tin về một chương trình mà ta có thể rất khó hoặc là không thể khi sử dụng disassembler. Disassemblers cung cấp một snapshot về chương trình trước khi thực thi câu lệnh đầu tiên. Debuggers cung cấp một cái nhìn "động" về một chương trình như là nó run. Một ví dụ nhé, debuggers có thể show các giá trị memory addresses như chúng thay đổi xuyên suốt quá trình thực thi của một chương trình.

Khả năng đo lường và điều khiển thực thi của một chương trình cung cấp những hiểu biết quan trọng trong suốt quá trình phân tích malware. Debuggers cho phép bạn nhìn thấy mọi memory location, register, và argument đến mọi function. Debuggers còn giúp bạn thay đổi bất cứ thứ gì về thực thi chương trình vào bất cứ lúc nào. Cho ví dụ, bạn có thể thay đổi giá trị của một biến vào bất cứ thời điểm nào - tất cả những thứ bạn cần đó là thông tin đầy đủ về biến đó, bao gồm cả location của nó.

Source-Level và Assembly-Level Debuggers

Hầu hết software developers đều quen thuộc với source-level debuggers, những gì cho phép một programmer debug trong khi coding. Kiểu debugger này được tích hợp sẵn trong integrated development environments (IDEs). Source-level debuggers cho phép bạn đặt breakpoints, việc đặt breakpoints làm dừng thực thi chương trình  trên các dòng của source code, để kiểm tra trạng thái của các biến phía bên trong và đi qua (step through) thực thi của chương trình mỗi lần một dòng.

Assembly-level debuggers, thỉnh thoảng được gọi là low-level debuggers, hoạt động trên assembly code thay vì source code. Cũng như với source-code debuggers, bạn có thể sử dụng assembly-level debugger để step through một chương trình mỗi lệnh tại mỗi thời điểm, đặt breakpoints để dùng lại tại các dòng assembly code xác định, và phân tích memory locations.

Malware analysts thường sử dụng assembly-level debuggers bởi vì họ thường không có sẵn source code của một chương trình.

Kernel và User-Mode Debugging

Chúng ta gặp nhiều thách thức hơn khi debug kernel-mode code so với debug user-mode code bởi vì bạn phải cần tới 2 systems khác nhau cho kernel-mode. Trong user-mode, debugger đang running trên cùng một system với code được debugged. Khi debugging trong user mode, bạn đang debugging một single executable, những gì được tách rời với executables khác bởi hệ điều hành.

Kernel debugging được thực hiện trên 2 systems bởi vì chỉ có duy nhất một kernel; nếu kernel nằm tại một breakpoint, không có một ứng dụng nào có running trên system. Một system runs code được debugged, và system còn lại runs debugger. Ngoài ra, OS phải được cấu hình để cho phép kernel debugging, và bạn phải kết nối 2 machines.

Có 2 gói phần mềm dành cho user-mode debugging và kernel debugging. WinDbg là một công cụ phổ biến hỗ trợ cho kernel debugging. OllyDbg là một công cụ nổi tiếng  khác dành cho malware analysts . WinDbg cũng hỗ trợ user-mode debugging tốt, và IDA Pro có một built-in debugger, nhưng nó không cung cấp các tính năng hoặc dễ dàng sử dụng như OllyDbg.

Sử dụng Debugger

Có 2 cách debug một chương trình. Cách thứ nhất là start chương trình với debugger. Khi bạn start chương trình và nó được nạp vào trong bộ nhớ, nó dừng running trước thực thi của entry point của nó. Tại entry point, bạn có thể hoàn toàn điều khiển chương trình.

Bạn có thể gắn (attach) một debugger đến một chương trình khi nó đã running. tất cả threads của chương trình được paused, và bạn có thể debug nó. Đó là một cách tiếp cận tốt khi bạn muốn debug một chương trình sau khi nó đã running hoặc nếu bạn muốn debug một process bị ảnh hưởng bởi malware.

Single-Stepping 

Thứ đơn giản nhất có thể làm với một debugger là single-step qua một chương trình, điều đó có nghĩa là bạn run một lệnh và trả quyền điều khiển cho debugger. Single-stepping cho phép bạn nhìn thấy mọi thứ đang diễn ra bên trong một chương trình.

Bạn có thể single-step qua toàn bộ chương trình, nhưng bạn không nên làm như vậy khi gặp các chương trình phức tập bởi vì nó sẽ tiêu tốn một lượng thời gian khá lớn. Single-stepping là một công cụ tốt cho việc hiểu các chi tiết của một section code, nhưng bạn phải lựa chọn những code nào muốn phân tích. Tập trung vào bức tranh lớn, toàn cảnh hoặc là bạn sẽ bị lạc trong các tiểu tiết.

Ví dụ, disassembly trong Example 9-1 shows cách bạn có thể sử dụng một debugger để giúp bạn hiểu về một section code.

Example 9-1. Stepping through code
mov edi, DWORD_00406904
mov ecx, 0x0d
LOC_040106B2
xor [edi], 0x9C
inc edi
loopw LOC_040106B2
...
DWORD:00406904:       F8FDF3D0 (1)

Đoạn code sau có thể hiểu là truy nhập vào dữ liệu và sửa đổi nó trong một loop. Dữ liệu được hiểu thị tại cuối của (1) không phải là dạng ASCII text hay là bất cứ giá trị có thể nhận dạng nào, nhưng bạn có thể sử dụng một debugger để step through loop này để làm lộ ra đoạn code này đang làm gì.

Nếu bạn single-step qua loop này với hoặc WinDbg hoặc OllyDbg, bạn có thể nhìn thấy dữ liệu bị sửa đổi. Cho ví dụ, trong Example 9-2, bạn nhìn thấy 13 bytes được sửa đổi bởi hàm này thay đổi mỗi lần qua loop.

Example 9-2. Single-stepping through a section of code to see how it changes memory
D0F3FDF8 D0F5FEEE FDEEE5DD 9C (.............)
4CF3FDF8 D0F5FEEE FDEEE5DD 9C (L............)
4C6FFDF8 D0F5FEEE FDEEE5DD 9C (Lo...........)
4C6F61F8 D0F5FEEE FDEEE5DD 9C (Loa..........)
. . . SNIP . . .
4C6F6164 4C696272 61727941 00 (LoadLibraryA.)

Với một debugger được gắn vào, rõ ràng thấy rằng hàm này đang sử dụng single-byte XOR function để decode string LoadLibrary. Có thể khó khăn để nhận dạng string này chỉ với phân tích tĩnh.

Stepping-Over và Stepping-Into

Khi single-stepping qua code, debuggers dùng lại sau mọi lệnh. Tuy nhiên, khi bạn quan tâm đến những gì chương trình đang làm, bạn có thể không cần lo lắng về chức năng của mỗi call. Cho ví dụ, nếu chương trình của bạn calls LoadLibrary , bạn có thể không muốn step through mọi lệnh của LoadLibrary function.

Để điều khiển các lệnh mà bạn nhìn thấy trong debugger của bạn, bạn thể step-over hoặc là step-into các lệnh. Khi bạn step-over call instructions, bạn bỏ quả chúng. Cho ví dụ, nếu bạn step-over một call, lệnh tiếp theo bạn sẽ nhìn thấy trong debugger của bạn sẽ là lệnh đằng sau function call trả về. Mặt khác, bạn step-into một call instruction, lệnh tiếp theo bạn sẽ nhìn thấy trong debugger là lệnh đầu tiên của hàm được gọi.

Stepping-over cho phép bạn giảm một số lượng đáng kể các lệnh bạn cần phân tích, nhưng nó cũng tiềm tàng việc bạn có thể missing các chức năng quan trọng của chương trình phân tích nếu bạn step-over wrong functions. Thêm vào đó là một số lượng function calls không bao giờ return, và nếu chương trình của bạn gọi một function mà không bao giờ returns và bạn step-over nó, debugger sẽ không bao giờ giành lại được điều khiển. Khi điều đó xảy ra, khởi động lại chương trình và step đến cùng vị trí đó, nhưng lúc này, step-into funtion.

Pausing Excution với Breakpoints

Breakpoints được sử dụng để pause excution và cho phép bạn kiểm tra trạng thái của một chương trình. Khi một chương trình bị paused tại một breakpoint, nó được nhắc tới như là broken . Breakpoints là cần thiết bởi vì bạn không thể access registers hoặc memory addresses trong khi một chương trình đang running, bởi vì các giá trị đó đang được thay đổi.

Example 9-3 demo nơi mà breakpoint trở nên hữu ích. Trong ví dụ này, có một lời gọi đến EAX. Trong khi disassembler không thể nói cho bạn biết function nào được gọi, bạn có thể set một breakpoint trên lệnh đó để tìm hiểu. Khi chương trình đụng phải breakpoint, nó sẽ bị dừng lại, và debugger sẽ show cho bạn giá trị của EAX, những gì là destination của hàm được gọi.

Example 9-3. Call to EAX
00401008 mov ecx, [ebp+arg_0]
0040100B mov eax, [edx]
0040100D call eax

Một ví dụ khác là trong Example 9-4 show điểm bắt đầu của một hàm với một lời gọi tới CreateFile để mở một handle đến file. Trong assembly, rất khó khăn để xác định được tên của file, mặc dù một phần của tên được pass như là một parameter đến hàm. Để tìm file trong disassembly, bạn có thể sử dụng IDA Pro để search tất cả các lần mà hàm này được gọi để nhìn xem những arguments nào được gửi vào, nhưng nhiều giá trị cũng có thể được passed như là parameters hoặc đến từ các function calls khác. Có thể rất khó khăn để xác định filename. Sử dụng một debugger làm cho công việc trở nên rất dễ dàng.

Example 9-4. Using a debugger to determine a filename
0040100B xor eax, esp
0040100D mov [esp+0D0h+var_4], eax
00401014 mov eax, edx
00401016 mov [esp+0D0h+NumberOfBytesWritten], 0
0040101D add eax, 0FFFFFFFEh
00401020 mov cx, [eax+2]
00401024 add eax, 2
00401027 test cx, cx
0040102A jnz short loc_401020
0040102C mov ecx, dword ptr ds:a_txt ; ".txt"
00401032 push 0 ; hTemplateFile
00401034 push 0 ; dwFlagsAndAttributes
00401036 push 2 ; dwCreationDisposition
00401038 mov [eax], ecx
0040103A mov ecx, dword ptr ds:a_txt+4
00401040 push 0 ; lpSecurityAttributes
00401042 push 0 ; dwShareMode
00401044 mov [eax+4], ecx
00401047 mov cx, word ptr ds:a_txt+8
0040104E push 0 ; dwDesiredAccess
00401050 push edx ; lpFileName
00401051 mov [eax+8], cx
00401055 (1) call CreateFileW ; CreateFileW(x,x,x,x,x,x,x)

Chúng ta set một breakpoint trên một lời gọi đến CreateFileW tại (1), và sau đó nhìn vào các giá trị trên stack khi breakpoint được kích hoạt.

Bây giờ hãy tưởng tượng bạn có một mảnh malware và một packet capture. Trong packet capture, chúng ta nhìn thấy encrypted data. Chúng ta có thể tìm thấy lời gọi để gửi đi và chúng ta có thể khai phá ra encryption code, nhưng rất khó khăn để decrypt các dữ liệu đó, bởi vì bạn không biết encrytion routine hoặc key. May mắn thay, bạn có thể sử dụng một debugger để đơn giản hóa công việc bởi vì encryption routines thường là các hàm rời rạc mà chuyển dữ liệu.

Nếu bạn có thể tìm thấy encrytion routine được gọi, chúng ta có thể set một breakpoint trước khi dữ liệu đó được encrypted và nhìn xem dữ liệu được gửi.

Bạn có thể sử dụng một vài kiểu breakpoints, bao gồm software execution, hardware execution, và conditional breakpoints. Mặc dù tất cả breakpoints đều phục vụ chung một mục đích, phụ thuộc vào tình hình, một số breakpoints sẽ không làm việc được trong khi số khác làm việc được. Nào hãy cùng xét từng breakpoint một

Software Execution Breakpoints

Cho đến bây giờ, chúng ta đang nói về software execution breakpoints , những gì làm cho một chương trình dừng lại khi một lệnh được thực thi. Khi bạn set một breakpoint mà không có bất cứ options nào, hầu hết debbuggers phổ biến sẽ set software execution breakpoint bởi mặc định.

debugger implement một software breakpoint thông qua việc overwriting byte đầu tiên của một lệnh với OxCC, lệnh tương ứng với INT 3, breakpoint interrupt được thiết kế cho việc sử dụng với debuggers. Khi lệnh OxCC được thực thi, OS sinh ra một ngoại lệ và chuyển quyền điều khiển đến debugger.


  

1 nhận xét:

  1. Tác giả bài viết nên ghi rõ nguồn mà mình lược dịch.

    Trả lờiXóa